diff --git a/README.md b/README.md index 1064a38..f50f593 100644 --- a/README.md +++ b/README.md @@ -85,5 +85,10 @@ numeric and date byte order, seekable stream overloads, and position-bearing errors for malformed or truncated input. Its parser and encoder enforce the same byte, depth, node, and allocation bounds; map keys are encoded in sorted order to make output stable despite the model's intentionally unordered maps. +The bounded Notation LLSD codec preserves UTF-16 length-prefix semantics, the +reference escape rules and accepted scalar spellings, base64, base16 and +length-prefixed binary forms, and both compact and formatted output. Malformed +notation reports UTF-16 offsets with parser context, while input, output, +nesting, node, and aggregate allocation limits apply to untrusted text. The controlled audit aggregates every expected failure by standardized C# member ID and rejects unrelated fixture, assertion, compile, or symbol errors. diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 826d970..5006821 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -12,7 +12,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim | | `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim | -| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 15 types / 268 members; remaining surface is callable failure-only shims | +| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 15 types / 281 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain | | `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim | | `LibreMetaverse.Voice.Vivox` | 64 | 531 | callable failure-only shim | diff --git a/crates/libremetaverse-structured-data/src/generated.rs b/crates/libremetaverse-structured-data/src/generated.rs index 9632bba..d2dfa22 100644 --- a/crates/libremetaverse-structured-data/src/generated.rs +++ b/crates/libremetaverse-structured-data/src/generated.rs @@ -242,9 +242,7 @@ impl OSDParser { buffer: Vec, offset: i32, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.BufferCharactersEqual(System.IO.StringReader,System.Char[],System.Int32)", - ) + crate::notation::buffer_characters_equal(reader, buffer, offset) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)`. pub fn consume_bytes( @@ -303,17 +301,13 @@ impl OSDParser { pub fn deserialize_llsd_notation_with_string_reader( reader: libremetaverse_types::compat::StringReader, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.IO.StringReader)", - ) + crate::notation::deserialize_reader(reader) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)`. pub fn deserialize_llsd_notation_with_string( notation_data: String, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)", - ) + crate::notation::deserialize_string(notation_data) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])`. pub fn deserialize_llsd_protobuf_with_bytes( @@ -368,9 +362,7 @@ impl OSDParser { s: String, c: libremetaverse_types::compat::Utf16CodeUnit, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)", - ) + crate::notation::escape_character(s, c) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)`. pub fn find_byte( @@ -390,18 +382,14 @@ impl OSDParser { pub fn get_length_in_brackets( reader: libremetaverse_types::compat::StringReader, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.GetLengthInBrackets(System.IO.StringReader)", - ) + crate::notation::get_length_in_brackets(reader) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)`. pub fn get_string_delimited_by( reader: libremetaverse_types::compat::StringReader, delimiter: libremetaverse_types::compat::Utf16CodeUnit, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)", - ) + crate::notation::get_string_delimited_by(reader, delimiter) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)`. pub fn host_to_network_int_bytes(int_host_end: i32) -> Result, crate::Error> { @@ -419,17 +407,13 @@ impl OSDParser { pub fn peek_and_skip_whitespace( reader: libremetaverse_types::compat::StringReader, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.PeekAndSkipWhitespace(System.IO.StringReader)", - ) + crate::notation::peek_and_skip_whitespace(reader) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)`. pub fn read_and_skip_whitespace( reader: libremetaverse_types::compat::StringReader, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)", - ) + crate::notation::read_and_skip_whitespace(reader) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeJsonString(LibreMetaverse.StructuredData.OSD,System.Boolean)`. pub fn serialize_json_string( @@ -478,33 +462,25 @@ impl OSDParser { pub fn serialize_llsd_notation( osd: libremetaverse_structured_data::OSD, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)", - ) + crate::notation::serialize(osd) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)`. pub fn serialize_llsd_notation_formatted( osd: libremetaverse_structured_data::OSD, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)", - ) + crate::notation::serialize_formatted(osd) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)`. pub fn serialize_llsd_notation_stream( osd: libremetaverse_structured_data::OSD, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)", - ) + crate::notation::serialize_stream(osd) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)`. pub fn serialize_llsd_notation_stream_formatted( osd: libremetaverse_structured_data::OSD, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)", - ) + crate::notation::serialize_stream_formatted(osd) } /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDProtobuf(LibreMetaverse.StructuredData.OSD,System.Boolean)`. pub fn serialize_llsd_protobuf( @@ -551,9 +527,7 @@ impl OSDParser { s: String, c: libremetaverse_types::compat::Utf16CodeUnit, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)", - ) + crate::notation::unescape_character(s, c) } } diff --git a/crates/libremetaverse-structured-data/src/lib.rs b/crates/libremetaverse-structured-data/src/lib.rs index 8a35102..542ddaf 100644 --- a/crates/libremetaverse-structured-data/src/lib.rs +++ b/crates/libremetaverse-structured-data/src/lib.rs @@ -6,6 +6,7 @@ mod binary; mod dispatch; mod generated; mod model; +mod notation; pub mod xml { /// Project-owned XML element boundary; external XML APIs are not copied. diff --git a/crates/libremetaverse-structured-data/src/model.rs b/crates/libremetaverse-structured-data/src/model.rs index c7e4435..6f3f1d7 100644 --- a/crates/libremetaverse-structured-data/src/model.rs +++ b/crates/libremetaverse-structured-data/src/model.rs @@ -273,11 +273,11 @@ impl OSD { Ok(match self { Self::Boolean(value) => if *value { "1" } else { "0" }.to_owned(), Self::Integer(value) => value.to_string(), - Self::Real(value) => format_real(*value), + Self::Real(value) => format_real_for_codec(*value), Self::String(value) | Self::LlsdXml(value) => value.clone(), Self::UUID(value) => value.to_string(), - Self::Date(value) => format_system_time(*value), - Self::Uri(value) => value.0.clone(), + Self::Date(value) => format_system_time_for_codec(*value), + Self::Uri(value) => format_uri_for_codec(&value.0), Self::Binary(value) => base64::engine::general_purpose::STANDARD.encode(value), _ => String::new(), }) @@ -296,7 +296,7 @@ impl OSD { pub fn as_date(&self) -> Result { Ok(match self { Self::Date(value) => *value, - Self::String(value) => parse_system_time(value).unwrap_or(UNIX_EPOCH), + Self::String(value) => parse_system_time_for_codec(value).unwrap_or(UNIX_EPOCH), _ => UNIX_EPOCH, }) } @@ -776,7 +776,7 @@ impl OSDReal { Ok(self.value) } pub fn as_string(&self) -> Result { - Ok(format_real(self.value)) + Ok(format_real_for_codec(self.value)) } pub fn as_u_integer(&self) -> Result { Ok(real_to_u32(self.value)) @@ -795,7 +795,7 @@ impl OSDString { Ok(string_as_boolean(&self.value)) } pub fn as_date(&self) -> Result { - Ok(parse_system_time(&self.value).unwrap_or(UNIX_EPOCH)) + Ok(parse_system_time_for_codec(&self.value).unwrap_or(UNIX_EPOCH)) } pub fn as_integer(&self) -> Result { Ok(string_to_f64(&self.value).map_or(0, floor_to_i32)) @@ -854,7 +854,7 @@ impl OSDDate { Ok(i64::from(unix_seconds_u32(self.value))) } pub fn as_string(&self) -> Result { - Ok(format_system_time(self.value)) + Ok(format_system_time_for_codec(self.value)) } pub fn as_u_integer(&self) -> Result { Ok(unix_seconds_u32(self.value)) @@ -1414,7 +1414,7 @@ fn real_to_u64(value: f64) -> u64 { value.round_ties_even() as u64 } } -fn format_real(value: f64) -> String { +pub(crate) fn format_real_for_codec(value: f64) -> String { if value.is_nan() { "NaN".to_owned() } else if value == f64::INFINITY { @@ -1438,7 +1438,7 @@ fn unix_seconds_u32(value: SystemTime) -> u32 { } } -fn format_system_time(value: SystemTime) -> String { +pub(crate) fn format_system_time_for_codec(value: SystemTime) -> String { let (whole, nanos) = match value.duration_since(UNIX_EPOCH) { Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()), Err(error) => { @@ -1485,7 +1485,7 @@ fn civil_from_unix(seconds: i64) -> (i32, u32, u32, u32, u32, u32) { ) } -fn parse_system_time(value: &str) -> Option { +pub(crate) fn parse_system_time_for_codec(value: &str) -> Option { let value = value.trim(); let (date, time) = value.split_once('T').or_else(|| value.split_once(' '))?; let mut date_parts = date.split('-'); @@ -1556,6 +1556,29 @@ fn valid_uri_reference(value: &str) -> bool { !value.chars().any(char::is_whitespace) && !value.chars().any(char::is_control) } +fn format_uri_for_codec(value: &str) -> String { + if !value.contains("://") { + return value.to_owned(); + } + let mut output = String::with_capacity(value.len()); + for character in value.chars() { + match character { + ' ' => output.push_str("%20"), + '"' => output.push_str("%22"), + '<' => output.push_str("%3C"), + '>' => output.push_str("%3E"), + character if character.is_control() => { + let mut bytes = [0_u8; 4]; + for byte in character.encode_utf8(&mut bytes).as_bytes() { + write!(output, "%{byte:02X}").expect("writing to String is infallible"); + } + } + character => output.push(character), + } + } + output +} + fn escape_text(value: &str) -> String { let mut output = String::with_capacity(value.len() + 2); output.push('"'); @@ -1597,7 +1620,7 @@ fn json_text_with_defaults(value: &OSD) -> String { OSD::Undefined => "null".to_owned(), OSD::Boolean(value) => value.to_string(), OSD::Integer(value) => value.to_string(), - OSD::Real(value) => format_real(*value), + OSD::Real(value) => format_real_for_codec(*value), OSD::String(value) => escape_text(value), OSD::LlsdXml(_) => "null".to_owned(), OSD::UUID(_) | OSD::Date(_) | OSD::Uri(_) => { diff --git a/crates/libremetaverse-structured-data/src/notation.rs b/crates/libremetaverse-structured-data/src/notation.rs new file mode 100644 index 0000000..4a5dbcc --- /dev/null +++ b/crates/libremetaverse-structured-data/src/notation.rs @@ -0,0 +1,971 @@ +//! Bounded LLSD notation lexer, parser, serializer, and public helper functions. + +#![allow(clippy::missing_errors_doc)] // Public Result shapes are fixed by the mapped API. +#![allow(clippy::needless_pass_by_value)] // Owned arguments mirror mapped C# value parameters. +#![allow(clippy::unnecessary_wraps)] // Result return shapes are fixed by the mapped API. +#![allow(clippy::unused_self)] // Parser error helpers retain local cursor context call sites. + +use crate::{Error, OSD}; +use base64::Engine as _; +use libremetaverse_types::UUID; +use libremetaverse_types::compat::{StringReader, StringWriter, Uri, Utf16CodeUnit}; +use std::collections::HashMap; +use std::mem::size_of; + +const INDENT: &str = " "; +const MAX_INPUT_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES; + +const fn is_notation_whitespace(unit: u16) -> bool { + unit == b' ' as u16 || unit == b'\t' as u16 || unit == b'\n' as u16 || unit == b'\r' as u16 +} + +pub(crate) fn deserialize_string(input: String) -> Result { + if input.len() > MAX_INPUT_BYTES { + return Err(parse_error( + 0, + "notation LLSD input exceeds allocation limit", + )); + } + let mut parser = Parser::new(&input); + if parser.peek_non_whitespace().is_none() { + return Ok(OSD::Undefined); + } + let value = parser.parse_value(0)?; + value.validate_limits( + OSD::DEFAULT_MAX_DEPTH, + OSD::DEFAULT_MAX_NODES, + OSD::DEFAULT_MAX_BINARY_BYTES, + )?; + Ok(value) +} + +pub(crate) fn deserialize_reader(reader: StringReader) -> Result { + deserialize_string(reader.0) +} + +pub(crate) fn serialize(value: OSD) -> Result { + serialize_internal(value, false) +} + +pub(crate) fn serialize_formatted(value: OSD) -> Result { + serialize_internal(value, true) +} + +pub(crate) fn serialize_stream(value: OSD) -> Result { + Ok(StringWriter(serialize(value)?)) +} + +pub(crate) fn serialize_stream_formatted(value: OSD) -> Result { + Ok(StringWriter(serialize_formatted(value)?)) +} + +fn serialize_internal(value: OSD, formatted: bool) -> Result { + value.validate_limits( + OSD::DEFAULT_MAX_DEPTH, + OSD::DEFAULT_MAX_NODES, + OSD::DEFAULT_MAX_BINARY_BYTES, + )?; + let mut encoder = Encoder::new(); + if formatted { + encoder.write_formatted(&value, "", 0)?; + } else { + encoder.write_compact(&value, 0)?; + } + Ok(encoder.finish()) +} + +pub(crate) fn buffer_characters_equal( + reader: StringReader, + buffer: Vec, + offset: i32, +) -> Result { + if reader.0.len() > MAX_INPUT_BYTES || buffer.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + let mut index = usize::try_from(offset).map_err(|_| Error::Argument)?; + if index > buffer.len() { + return Err(Error::Argument); + } + let input: Vec<_> = reader.0.encode_utf16().collect(); + let mut input_index = 0; + while index < buffer.len() + && input + .get(input_index) + .is_some_and(|unit| *unit == buffer[index].0) + { + index += 1; + input_index += 1; + } + i32::try_from(index).map_err(|_| Error::Argument) +} + +pub(crate) fn peek_and_skip_whitespace(reader: StringReader) -> Result { + if reader.0.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + Ok(reader + .0 + .encode_utf16() + .find(|unit| !is_notation_whitespace(*unit)) + .map_or(-1, i32::from)) +} + +pub(crate) fn read_and_skip_whitespace(reader: StringReader) -> Result { + peek_and_skip_whitespace(reader) +} + +pub(crate) fn get_length_in_brackets(reader: StringReader) -> Result { + if reader.0.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + let mut parser = Parser::new(&reader.0); + let length = parser.parse_parenthesized_length("invalid notation LLSD length")?; + i32::try_from(length).map_err(|_| Error::Argument) +} + +pub(crate) fn get_string_delimited_by( + reader: StringReader, + delimiter: Utf16CodeUnit, +) -> Result { + if reader.0.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + let mut parser = Parser::new(&reader.0); + parser.parse_delimited(delimiter.0) +} + +pub(crate) fn escape_character(value: String, delimiter: Utf16CodeUnit) -> Result { + if value.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + let delimiter = char::from_u32(u32::from(delimiter.0)).ok_or(Error::Argument)?; + let mut output = String::with_capacity(value.len().saturating_add(2)); + for character in value.chars() { + if character == '\\' || character == delimiter { + output.push('\\'); + } + output.push(character); + if output.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + } + Ok(output) +} + +pub(crate) fn unescape_character(value: String, delimiter: Utf16CodeUnit) -> Result { + if value.len() > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + let delimiter = char::from_u32(u32::from(delimiter.0)).ok_or(Error::Argument)?; + let collapsed = value.replace("\\\\", "\\"); + Ok(collapsed.replace(&format!("\\{delimiter}"), &delimiter.to_string())) +} + +struct Parser { + units: Vec, + position: usize, + nodes: usize, + allocated: usize, +} + +impl Parser { + fn new(input: &str) -> Self { + Self { + units: input.encode_utf16().collect(), + position: 0, + nodes: 0, + allocated: 0, + } + } + + fn parse_value(&mut self, depth: usize) -> Result { + if depth > OSD::DEFAULT_MAX_DEPTH { + return Err(self.error("notation LLSD nesting depth exceeded")); + } + self.nodes = self + .nodes + .checked_add(1) + .ok_or_else(|| self.error("notation LLSD node count overflow"))?; + if self.nodes > OSD::DEFAULT_MAX_NODES { + return Err(self.error("notation LLSD node limit exceeded")); + } + self.skip_whitespace(); + let marker_position = self.position; + let marker = self.read_unit("missing notation LLSD value marker")?; + match marker { + unit if unit == u16::from(b'!') => Ok(OSD::Undefined), + unit if unit == u16::from(b'1') => Ok(OSD::Boolean(true)), + unit if unit == u16::from(b'0') => Ok(OSD::Boolean(false)), + unit if unit == u16::from(b't') => { + self.consume_boolean_suffix("true", "invalid notation LLSD true value")?; + Ok(OSD::Boolean(true)) + } + unit if unit == u16::from(b'T') => { + self.consume_boolean_suffix("TRUE", "invalid notation LLSD true value")?; + Ok(OSD::Boolean(true)) + } + unit if unit == u16::from(b'f') => { + self.consume_boolean_suffix("false", "invalid notation LLSD false value")?; + Ok(OSD::Boolean(false)) + } + unit if unit == u16::from(b'F') => { + self.consume_boolean_suffix("FALSE", "invalid notation LLSD false value")?; + Ok(OSD::Boolean(false)) + } + unit if unit == u16::from(b'i') => self.parse_integer(), + unit if unit == u16::from(b'r') => self.parse_real(), + unit if unit == u16::from(b'u') => self.parse_uuid(), + unit if unit == u16::from(b'b') => self.parse_binary(), + unit if unit == u16::from(b's') => self.parse_sized_string(), + unit if unit == u16::from(b'\'') || unit == u16::from(b'"') => { + Ok(OSD::String(self.parse_delimited(unit)?)) + } + unit if unit == u16::from(b'l') => self.parse_uri(), + unit if unit == u16::from(b'd') => self.parse_date(), + unit if unit == u16::from(b'[') => self.parse_array(depth), + unit if unit == u16::from(b'{') => self.parse_map(depth), + _ => Err(self.error_at(marker_position, "unknown notation LLSD type marker")), + } + } + + fn consume_boolean_suffix( + &mut self, + expected: &str, + context: &'static str, + ) -> Result<(), Error> { + let expected: Vec<_> = expected.encode_utf16().collect(); + let mut matched = 1; + while matched < expected.len() + && self + .units + .get(self.position) + .is_some_and(|unit| *unit == expected[matched]) + { + matched += 1; + self.position += 1; + } + if matched > 1 && matched < expected.len() { + Err(self.error(context)) + } else { + Ok(()) + } + } + + fn parse_integer(&mut self) -> Result { + let start = self.position; + if self.peek_unit() == Some(u16::from(b'-')) { + self.position += 1; + } + while self.peek_unit().is_some_and(is_ascii_digit) { + self.position += 1; + } + let text = self.decode_range(start, self.position, "invalid notation LLSD integer")?; + let value = text + .parse::() + .map_err(|_| self.error_at(start, "invalid notation LLSD integer"))?; + Ok(OSD::Integer(value)) + } + + fn parse_real(&mut self) -> Result { + let start = self.position; + while self.peek_unit().is_some_and(|unit| { + is_ascii_digit(unit) || matches!(unit, 0x2e | 0x65 | 0x45 | 0x2b | 0x2d) + }) { + self.position += 1; + } + let text = self.decode_range(start, self.position, "invalid notation LLSD real")?; + let value = text + .parse::() + .map_err(|_| self.error_at(start, "invalid notation LLSD real"))?; + Ok(OSD::Real(value)) + } + + fn parse_uuid(&mut self) -> Result { + let start = self.position; + let end = start + .checked_add(36) + .ok_or_else(|| self.error("notation LLSD UUID length overflow"))?; + let text = self.decode_range(start, end, "truncated notation LLSD UUID")?; + self.position = end; + let uuid = UUID::new_with_string(text) + .map_err(|_| self.error_at(start, "invalid notation LLSD UUID"))?; + Ok(OSD::UUID(uuid)) + } + + fn parse_binary(&mut self) -> Result { + let marker_position = self.position.saturating_sub(1); + let value = if self.peek_unit() == Some(u16::from(b'(')) { + let length = self.parse_parenthesized_length("invalid notation LLSD binary length")?; + let delimiter = self.read_quote("missing notation LLSD raw binary delimiter")?; + let start = self.position; + let end = start + .checked_add(length) + .ok_or_else(|| self.error("notation LLSD binary length overflow"))?; + let units = self + .units + .get(start..end) + .ok_or_else(|| self.error("truncated notation LLSD raw binary"))?; + let bytes: Vec = units + .iter() + .copied() + .map(|unit| { + u8::try_from(unit).map_err(|_| { + self.error_at(start, "invalid notation LLSD raw binary character") + }) + }) + .collect::>()?; + self.position = end; + self.expect_unit(delimiter, "missing notation LLSD raw binary end delimiter")?; + bytes + } else { + let base_start = self.position; + let base = self.read_ascii_digits(2, "invalid notation LLSD binary base")?; + let delimiter = self.read_quote("missing notation LLSD binary delimiter")?; + let encoded = self.parse_delimited(delimiter)?; + match base.as_str() { + "64" => { + decode_base64(&encoded).map_err(|context| self.error_at(base_start, context))? + } + "16" => { + decode_base16(&encoded).map_err(|context| self.error_at(base_start, context))? + } + _ => { + return Err( + self.error_at(marker_position, "unsupported notation LLSD binary base") + ); + } + } + }; + self.add_allocation( + value.len(), + "notation LLSD binary allocation limit exceeded", + )?; + Ok(OSD::Binary(value)) + } + + fn parse_sized_string(&mut self) -> Result { + let length = self.parse_parenthesized_length("invalid notation LLSD string length")?; + let delimiter = self.read_quote("missing notation LLSD string delimiter")?; + let start = self.position; + let end = start + .checked_add(length) + .ok_or_else(|| self.error("notation LLSD string length overflow"))?; + let value = self.decode_range(start, end, "truncated notation LLSD string")?; + self.position = end; + self.expect_unit(delimiter, "missing notation LLSD string end delimiter")?; + self.add_allocation(value.len(), "notation LLSD text allocation limit exceeded")?; + Ok(OSD::String(value)) + } + + fn parse_uri(&mut self) -> Result { + let delimiter = self.read_quote("missing notation LLSD URI delimiter")?; + let start = self.position; + let value = self.parse_delimited(delimiter)?; + if value + .chars() + .any(|character| character == '\0' || character.is_control()) + { + return Err(self.error_at(start, "invalid notation LLSD URI")); + } + Ok(OSD::Uri(Uri(value))) + } + + fn parse_date(&mut self) -> Result { + let delimiter = self.read_quote("missing notation LLSD date delimiter")?; + let start = self.position; + let value = self.parse_delimited(delimiter)?; + let date = crate::model::parse_system_time_for_codec(&value) + .ok_or_else(|| self.error_at(start, "invalid notation LLSD date"))?; + Ok(OSD::Date(date)) + } + + fn parse_array(&mut self, depth: usize) -> Result { + let mut values = Vec::new(); + self.skip_whitespace(); + if self.consume_unit(u16::from(b']')) { + return Ok(OSD::Array(values)); + } + loop { + if values.len() >= OSD::DEFAULT_MAX_NODES { + return Err(self.error("notation LLSD array node limit exceeded")); + } + values.push(self.parse_value(depth + 1)?); + self.skip_whitespace(); + if self.consume_unit(u16::from(b']')) { + break; + } + self.expect_unit(u16::from(b','), "invalid notation LLSD array delimiter")?; + self.skip_whitespace(); + if self.consume_unit(u16::from(b']')) { + break; + } + } + self.add_allocation( + values.len().saturating_mul(size_of::()), + "notation LLSD array allocation limit exceeded", + )?; + Ok(OSD::Array(values)) + } + + fn parse_map(&mut self, depth: usize) -> Result { + let mut values = HashMap::new(); + self.skip_whitespace(); + if self.consume_unit(u16::from(b'}')) { + return Ok(OSD::Map(values)); + } + loop { + if values.len() >= OSD::DEFAULT_MAX_NODES { + return Err(self.error("notation LLSD map node limit exceeded")); + } + let key_position = self.position; + let OSD::String(key) = self.parse_value(depth + 1)? else { + return Err(self.error_at(key_position, "invalid notation LLSD map key")); + }; + self.skip_whitespace(); + self.expect_unit(u16::from(b':'), "invalid notation LLSD map key delimiter")?; + let value = self.parse_value(depth + 1)?; + values.insert(key, value); + self.skip_whitespace(); + if self.consume_unit(u16::from(b'}')) { + break; + } + self.expect_unit(u16::from(b','), "invalid notation LLSD map delimiter")?; + self.skip_whitespace(); + if self.consume_unit(u16::from(b'}')) { + break; + } + } + self.add_allocation( + values.len().saturating_mul(size_of::<(String, OSD)>()), + "notation LLSD map allocation limit exceeded", + )?; + Ok(OSD::Map(values)) + } + + fn parse_parenthesized_length(&mut self, context: &'static str) -> Result { + self.skip_whitespace(); + let start = self.position; + self.expect_unit(u16::from(b'('), context)?; + let digits_start = self.position; + while self.peek_unit().is_some_and(is_ascii_digit) { + self.position += 1; + } + if self.position == digits_start { + return Err(self.error_at(start, context)); + } + let digits = self.decode_range(digits_start, self.position, context)?; + self.expect_unit(u16::from(b')'), context)?; + let length = digits + .parse::() + .map_err(|_| self.error_at(start, context))?; + if length > MAX_INPUT_BYTES { + return Err(self.error_at(start, context)); + } + Ok(length) + } + + fn parse_delimited(&mut self, delimiter: u16) -> Result { + let start = self.position; + let mut output = Vec::new(); + let mut escaped = false; + loop { + let position = self.position; + let unit = self.read_unit("unterminated notation LLSD string")?; + if escaped { + output.push(match unit { + unit if unit == u16::from(b'a') => 0x07, + unit if unit == u16::from(b'b') => 0x08, + unit if unit == u16::from(b'f') => 0x0c, + unit if unit == u16::from(b'n') => 0x0a, + unit if unit == u16::from(b'r') => 0x0d, + unit if unit == u16::from(b't') => 0x09, + unit if unit == u16::from(b'v') => 0x0b, + _ => unit, + }); + escaped = false; + } else if unit == u16::from(b'\\') { + escaped = true; + } else if unit == delimiter { + break; + } else { + output.push(unit); + } + if output.len() > MAX_INPUT_BYTES { + return Err(self.error_at(position, "notation LLSD text allocation limit exceeded")); + } + } + let value = String::from_utf16_lossy(&output); + self.add_allocation(value.len(), "notation LLSD text allocation limit exceeded")?; + if self.position < start { + return Err(self.error("notation LLSD parser position overflow")); + } + Ok(value) + } + + fn read_quote(&mut self, context: &'static str) -> Result { + let position = self.position; + let delimiter = self.read_unit(context)?; + if matches!(delimiter, 0x22 | 0x27) { + Ok(delimiter) + } else { + Err(self.error_at(position, context)) + } + } + + fn read_ascii_digits(&mut self, count: usize, context: &'static str) -> Result { + let start = self.position; + let end = start + .checked_add(count) + .ok_or_else(|| self.error(context))?; + let units = self + .units + .get(start..end) + .ok_or_else(|| self.error(context))?; + if !units.iter().copied().all(is_ascii_digit) { + return Err(self.error_at(start, context)); + } + let value = String::from_utf16(units).map_err(|_| self.error_at(start, context))?; + self.position = end; + Ok(value) + } + + fn decode_range( + &self, + start: usize, + end: usize, + context: &'static str, + ) -> Result { + let units = self + .units + .get(start..end) + .ok_or_else(|| self.error_at(start, context))?; + Ok(String::from_utf16_lossy(units)) + } + + fn peek_non_whitespace(&mut self) -> Option { + self.skip_whitespace(); + self.peek_unit() + } + + fn skip_whitespace(&mut self) { + while self.peek_unit().is_some_and(is_notation_whitespace) { + self.position += 1; + } + } + + fn peek_unit(&self) -> Option { + self.units.get(self.position).copied() + } + + fn read_unit(&mut self, context: &'static str) -> Result { + let unit = self.peek_unit().ok_or_else(|| self.error(context))?; + self.position += 1; + Ok(unit) + } + + fn consume_unit(&mut self, expected: u16) -> bool { + if self.peek_unit() == Some(expected) { + self.position += 1; + true + } else { + false + } + } + + fn expect_unit(&mut self, expected: u16, context: &'static str) -> Result<(), Error> { + let position = self.position; + if self.read_unit(context)? == expected { + Ok(()) + } else { + Err(self.error_at(position, context)) + } + } + + fn add_allocation(&mut self, amount: usize, context: &'static str) -> Result<(), Error> { + self.allocated = self + .allocated + .checked_add(amount) + .ok_or_else(|| self.error(context))?; + if self.allocated > OSD::DEFAULT_MAX_BINARY_BYTES { + Err(self.error(context)) + } else { + Ok(()) + } + } + + const fn error(&self, context: &'static str) -> Error { + self.error_at(self.position, context) + } + + const fn error_at(&self, position: usize, context: &'static str) -> Error { + parse_error(position, context) + } +} + +struct Encoder { + output: String, +} + +impl Encoder { + fn new() -> Self { + Self { + output: String::with_capacity(128), + } + } + + fn finish(self) -> String { + self.output + } + + fn write_compact(&mut self, value: &OSD, depth: usize) -> Result<(), Error> { + self.check_depth(depth)?; + match value { + OSD::Undefined => self.push('!'), + OSD::Boolean(value) => self.push(if *value { 't' } else { 'f' }), + OSD::Integer(value) => { + self.push('i')?; + self.push_str(&value.to_string()) + } + OSD::Real(value) => { + self.push('r')?; + self.push_str(&crate::model::format_real_for_codec(*value)) + } + OSD::UUID(value) => { + self.push('u')?; + self.push_str(&value.to_string()) + } + OSD::String(value) => self.write_quoted(value, '\''), + OSD::Binary(value) => { + self.push_str("b64\"")?; + self.push_str(&base64::engine::general_purpose::STANDARD.encode(value))?; + self.push('"') + } + OSD::Date(value) => { + self.push_str("d\"")?; + self.push_str(&crate::model::format_system_time_for_codec(*value))?; + self.push('"') + } + OSD::Uri(Uri(value)) => { + self.push('l')?; + self.write_quoted(value, '"') + } + OSD::Array(values) => { + self.push('[')?; + for (index, value) in values.iter().enumerate() { + if index > 0 { + self.push(',')?; + } + self.write_compact(value, depth + 1)?; + } + self.push(']') + } + OSD::Map(values) => { + self.push('{')?; + for (index, (key, value)) in sorted_entries(values).into_iter().enumerate() { + if index > 0 { + self.push(',')?; + } + self.write_quoted(key, '\'')?; + self.push(':')?; + self.write_compact(value, depth + 1)?; + } + self.push('}') + } + OSD::LlsdXml(_) => Err(parse_error( + self.output.len(), + "LLSD XML fragments have no notation LLSD representation", + )), + } + } + + fn write_formatted(&mut self, value: &OSD, indent: &str, depth: usize) -> Result<(), Error> { + self.check_depth(depth)?; + match value { + OSD::Array(values) => { + let child_indent = format!("{indent}{INDENT}"); + self.write_array_formatted(values, &child_indent, depth) + } + OSD::Map(values) => { + let child_indent = format!("{indent}{INDENT}"); + self.write_map_formatted(values, &child_indent, depth) + } + _ => self.write_compact(value, depth), + } + } + + fn write_array_formatted( + &mut self, + values: &[OSD], + indent: &str, + depth: usize, + ) -> Result<(), Error> { + self.push('\n')?; + self.push_str(indent)?; + self.push('[')?; + for (index, value) in values.iter().enumerate() { + if !matches!(value, OSD::Array(_) | OSD::Map(_)) { + self.push('\n')?; + } + self.push_str(indent)?; + self.push_str(INDENT)?; + self.write_formatted(value, indent, depth + 1)?; + if index + 1 < values.len() { + self.push(',')?; + } + } + self.push('\n')?; + self.push_str(indent)?; + self.push(']') + } + + fn write_map_formatted( + &mut self, + values: &HashMap, + indent: &str, + depth: usize, + ) -> Result<(), Error> { + self.push('\n')?; + self.push_str(indent)?; + self.push_str("{\n")?; + let entries = sorted_entries(values); + for (index, (key, value)) in entries.iter().enumerate() { + self.push_str(indent)?; + self.push_str(INDENT)?; + self.write_quoted(key, '\'')?; + self.push(':')?; + self.write_formatted(value, indent, depth + 1)?; + if index + 1 < entries.len() { + self.push('\n')?; + self.push_str(indent)?; + self.push_str(INDENT)?; + self.push_str(",\n")?; + } + } + self.push('\n')?; + self.push_str(indent)?; + self.push('}') + } + + fn write_quoted(&mut self, value: &str, delimiter: char) -> Result<(), Error> { + self.push(delimiter)?; + for character in value.chars() { + if character == '\\' || character == delimiter { + self.push('\\')?; + } + self.push(character)?; + } + self.push(delimiter) + } + + fn check_depth(&self, depth: usize) -> Result<(), Error> { + if depth > OSD::DEFAULT_MAX_DEPTH { + Err(parse_error( + self.output.len(), + "notation LLSD nesting depth exceeded", + )) + } else { + Ok(()) + } + } + + fn push(&mut self, character: char) -> Result<(), Error> { + let length = self + .output + .len() + .checked_add(character.len_utf8()) + .ok_or(Error::Argument)?; + if length > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + self.output.push(character); + Ok(()) + } + + fn push_str(&mut self, value: &str) -> Result<(), Error> { + let length = self + .output + .len() + .checked_add(value.len()) + .ok_or(Error::Argument)?; + if length > MAX_INPUT_BYTES { + return Err(Error::Argument); + } + self.output.push_str(value); + Ok(()) + } +} + +fn sorted_entries(values: &HashMap) -> Vec<(&String, &OSD)> { + let mut entries: Vec<_> = values.iter().collect(); + entries.sort_unstable_by_key(|(key, _)| *key); + entries +} + +fn decode_base16(value: &str) -> Result, &'static str> { + if value.len() % 2 != 0 { + return Err("invalid notation LLSD base16 length"); + } + value + .as_bytes() + .chunks_exact(2) + .map(|pair| { + let high = hex_value(pair[0]).ok_or("invalid notation LLSD base16 digit")?; + let low = hex_value(pair[1]).ok_or("invalid notation LLSD base16 digit")?; + Ok((high << 4) | low) + }) + .collect() +} + +fn decode_base64(value: &str) -> Result, &'static str> { + let compact: Vec = value + .bytes() + .filter(|byte| !matches!(byte, b' ' | b'\t' | b'\n' | b'\r')) + .collect(); + base64::engine::general_purpose::STANDARD + .decode(compact) + .map_err(|_| "invalid notation LLSD base64 value") +} + +const fn hex_value(value: u8) -> Option { + match value { + b'0'..=b'9' => Some(value - b'0'), + b'a'..=b'f' => Some(value - b'a' + 10), + b'A'..=b'F' => Some(value - b'A' + 10), + _ => None, + } +} + +const fn is_ascii_digit(unit: u16) -> bool { + unit >= b'0' as u16 && unit <= b'9' as u16 +} + +const fn parse_error(position: usize, context: &'static str) -> Error { + Error::Parse { position, context } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{Duration, UNIX_EPOCH}; + + #[test] + fn compact_and_formatted_round_trip_every_variant() { + let value = OSD::Array(vec![ + OSD::Undefined, + OSD::Boolean(true), + OSD::Integer(-42), + OSD::Real(2.5), + OSD::UUID( + UUID::new_with_string("97f4aeca-88a1-42a1-b385-b97b18abb255".into()).unwrap(), + ), + OSD::String("quote '\\ and 𐄷".into()), + OSD::Binary(vec![0, 1, 255]), + OSD::Date(UNIX_EPOCH + Duration::from_millis(1_199_134_150_100)), + OSD::Uri(Uri("https://example.test/a path".into())), + OSD::Map(HashMap::from([("nested".into(), OSD::Array(vec![]))])), + ]); + let compact = serialize(value.clone()).unwrap(); + assert_eq!(serialize_stream(value.clone()).unwrap().0, compact); + assert_eq!(deserialize_string(compact).unwrap(), value); + let formatted = serialize_formatted(value.clone()).unwrap(); + assert_eq!( + serialize_stream_formatted(value.clone()).unwrap().0, + formatted + ); + assert!(formatted.contains('\n')); + assert_eq!(deserialize_string(formatted).unwrap(), value); + assert_eq!( + serialize_formatted(OSD::Array(vec![OSD::Integer(1), OSD::Integer(2)])).unwrap(), + "\n [\n i1,\n i2\n ]" + ); + } + + #[test] + fn accepted_string_binary_boolean_and_escape_variants_are_exact() { + assert_eq!( + deserialize_string("true".into()).unwrap(), + OSD::Boolean(true) + ); + assert_eq!(deserialize_string("T".into()).unwrap(), OSD::Boolean(true)); + assert_eq!( + deserialize_string(r#"s(2)"𐄷""#.into()).unwrap(), + OSD::String("𐄷".into()) + ); + assert_eq!( + deserialize_string(r#"b16"0001fF""#.into()).unwrap(), + OSD::Binary(vec![0, 1, 255]) + ); + assert_eq!( + deserialize_string("b64\"AA E=\r\n\"".into()).unwrap(), + OSD::Binary(vec![0, 1]) + ); + assert_eq!( + deserialize_string("b(3)\"A\\0\"".into()).unwrap(), + OSD::Binary(vec![b'A', b'\\', b'0']) + ); + assert_eq!( + deserialize_string(r"'a\n\t\'\\b'".into()).unwrap(), + OSD::String("a\n\t'\\b".into()) + ); + assert_eq!( + escape_character("a\\'b".into(), Utf16CodeUnit(u16::from(b'\''))).unwrap(), + "a\\\\\\'b" + ); + assert_eq!( + unescape_character("a\\\\\\'b".into(), Utf16CodeUnit(u16::from(b'\''))).unwrap(), + "a\\'b" + ); + assert_eq!( + get_length_in_brackets(StringReader(" \t(123)remaining".into())).unwrap(), + 123 + ); + assert_eq!( + get_string_delimited_by( + StringReader(r"line\nquote\'tail'ignored".into()), + Utf16CodeUnit(u16::from(b'\'')), + ) + .unwrap(), + "line\nquote'tail" + ); + assert_eq!( + peek_and_skip_whitespace(StringReader(" \r\nX".into())).unwrap(), + i32::from(b'X') + ); + } + + #[test] + fn malformed_inputs_report_positions_and_depth_is_bounded() { + for (input, context) in [ + ("i", "invalid notation LLSD integer"), + ("r", "invalid notation LLSD real"), + ("u123", "truncated notation LLSD UUID"), + ("b64\"%%%\"", "invalid notation LLSD base64 value"), + ("b16\"0\"", "invalid notation LLSD base16 length"), + ("s(4)\"abc\"", "missing notation LLSD string end delimiter"), + ("'unterminated", "unterminated notation LLSD string"), + ("[i1 i2]", "invalid notation LLSD array delimiter"), + ("{'key'i1}", "invalid notation LLSD map key delimiter"), + ] { + let Error::Parse { + position, + context: actual, + } = deserialize_string(input.into()).unwrap_err() + else { + panic!("malformed notation did not return a positional parse error"); + }; + assert!(position <= input.encode_utf16().count()); + assert_eq!(actual, context); + } + + let mut nested = "[".repeat(OSD::DEFAULT_MAX_DEPTH + 1); + nested.push('!'); + nested.push_str(&"]".repeat(OSD::DEFAULT_MAX_DEPTH + 1)); + assert!(matches!( + deserialize_string(nested), + Err(Error::Parse { + context: "notation LLSD nesting depth exceeded", + .. + }) + )); + } +} diff --git a/tests/compat/tests/structured_data.rs b/tests/compat/tests/structured_data.rs index fbe74d6..22e2d1c 100644 --- a/tests/compat/tests/structured_data.rs +++ b/tests/compat/tests/structured_data.rs @@ -802,11 +802,11 @@ fn notation_deserialize_string() { for (input, expected) in [ ("''", ""), (r#"'test\'\"test'"#, "test'\"test"), - ("'test \\\\lest'", "test \\\\lest"), + ("'test \\\\lest'", "test \\lest"), ("'aa\t la'", "aa\t la"), ("'\\\\'", "\\"), (r#"s(10)"1234567890""#, "1234567890"), - (r#"s(5)"\\\""#, "\\\\\\\\\\"), + (r#"s(5)"\\\\\""#, "\\\\\\\\\\"), ( r#""aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh""#, "aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh", diff --git a/tests/red-suite-baseline.json b/tests/red-suite-baseline.json index 9eb0d12..76cc748 100644 --- a/tests/red-suite-baseline.json +++ b/tests/red-suite-baseline.json @@ -39,6 +39,31 @@ "LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUUID::test", "LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUndef::test", "LibreMetaverse.Tests/MarketplaceFolderClassifierTests.cs::MarketplaceFolderClassifierTests.ValidateListing_ValidFlags_AreBitmaskComposable::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeArray::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeBoolean::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeDate::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeInteger::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeMap::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeReal::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeRealWorldExamples::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeString::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeURI::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeUUID::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeUndef::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.HelperFunctions::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeArray::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeBinary::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeBoolean::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeDate::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeFormattedTest::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeInteger::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeMap::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeReal::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeString::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeURI::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeUUID::test", + "LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeUndef::test", + "LibreMetaverse.Tests/TypeTests.cs::TypeTests.LLSDTerseParsing::test", "LibreMetaverse.Tests/TypeTests.cs::TypeTests.Quaternions::test", "LibreMetaverse.Tests/TypeTests.cs::TypeTests.TestMatrix::test", "LibreMetaverse.Tests/TypeTests.cs::TypeTests.UUIDs::test", @@ -46,5 +71,5 @@ "LibreMetaverse.Tests/TypeTests.cs::TypeTests.VectorCasting::test", "LibreMetaverse.Tests/UtilsConversionsTests.cs::UtilsConversionsTests.StringToBytes::test" ], - "support_passes": 76 + "support_passes": 79 } diff --git a/tests/upstream-tests.json b/tests/upstream-tests.json index 16b7684..d69c3f5 100644 --- a/tests/upstream-tests.json +++ b/tests/upstream-tests.json @@ -8611,7 +8611,7 @@ "body_sha256": "a29dc440ccde574c1302c57d3ef4c5404aa4fd419a5f160ba4920ab5a3349cc7", "categories": [], "fixture_dependencies": [], - "rust_body_sha256": "0bec6e4d609c3acd083b0e7e4c0588cb2fc3be1dcde3ed685866e559ec567e49", + "rust_body_sha256": "780d333102a826359866f70bf895c6dbeaf7489b77f071efd20cac029b7b9fb8", "status": "translated", "rust_file": "tests/compat/tests/structured_data.rs", "rust_line": 799, diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index 6faf75c..9816f3d 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -79,22 +79,38 @@ NATIVE_TYPES = { } NATIVE_MEMBER_BODIES = { + "M:LibreMetaverse.StructuredData.OSDParser.BufferCharactersEqual(System.IO.StringReader,System.Char[],System.Int32)": + "crate::notation::buffer_characters_equal(reader, buffer, offset)", "M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)": "crate::binary::consume_bytes(stream, consume_bytes)", "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.Byte[])": "crate::binary::deserialize_bytes(binary_data)", "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.IO.Stream)": "crate::binary::deserialize_stream(stream)", + "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.IO.StringReader)": + "crate::notation::deserialize_reader(reader)", + "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)": + "crate::notation::deserialize_string(notation_data)", + "M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)": + "crate::notation::escape_character(s, c)", "M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)": "crate::binary::find_byte(stream, to_find)", "M:LibreMetaverse.StructuredData.OSDParser.FindString(System.IO.Stream,System.String)": "crate::binary::find_string(stream, to_find)", + "M:LibreMetaverse.StructuredData.OSDParser.GetLengthInBrackets(System.IO.StringReader)": + "crate::notation::get_length_in_brackets(reader)", + "M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)": + "crate::notation::get_string_delimited_by(reader, delimiter)", "M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)": "crate::binary::host_to_network_int_bytes(int_host_end)", "M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostDouble(System.Byte[])": "crate::binary::network_to_host_double(binary_net_end)", "M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostInt(System.Byte[])": "crate::binary::network_to_host_int(binary_net_end)", + "M:LibreMetaverse.StructuredData.OSDParser.PeekAndSkipWhitespace(System.IO.StringReader)": + "crate::notation::peek_and_skip_whitespace(reader)", + "M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)": + "crate::notation::read_and_skip_whitespace(reader)", "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD)": "crate::binary::serialize(osd)", "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD,System.Boolean)": @@ -103,8 +119,18 @@ NATIVE_MEMBER_BODIES = { "crate::binary::serialize_stream(data)", "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)": "crate::binary::serialize_stream_with_header(data, prepend_header)", + "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)": + "crate::notation::serialize(osd)", + "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)": + "crate::notation::serialize_formatted(osd)", + "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)": + "crate::notation::serialize_stream(osd)", + "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)": + "crate::notation::serialize_stream_formatted(osd)", "M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)": "crate::binary::skip_whitespace(stream)", + "M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)": + "crate::notation::unescape_character(s, c)", "M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.Byte[])": "crate::dispatch::deserialize_bytes(data)", "M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.IO.Stream)": diff --git a/tools/generate_rust_mapping.py b/tools/generate_rust_mapping.py index cf25337..a068d69 100644 --- a/tools/generate_rust_mapping.py +++ b/tools/generate_rust_mapping.py @@ -982,6 +982,7 @@ def validate_generated_shims() -> None: "crate::byte_order::", "crate::binary::", "crate::dispatch::", + "crate::notation::", ) ): raise ValueError(f"generated shim function does not use the standardized failure: {path.relative_to(ROOT)}")