Implement bounded Notation LLSD codec

This commit is contained in:
2026-08-09 00:51:05 +00:00
parent 7a4f1f2fb1
commit 2308e55671
11 changed files with 1081 additions and 55 deletions

View File

@@ -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 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 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. 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# The controlled audit aggregates every expected failure by standardized C#
member ID and rejects unrelated fixture, assertion, compile, or symbol errors. member ID and rejects unrelated fixture, assertion, compile, or symbol errors.

View File

@@ -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.RLV` | 28 | 499 | callable failure-only shim |
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | 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.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim | | `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |
| `LibreMetaverse.Voice.Vivox` | 64 | 531 | callable failure-only shim | | `LibreMetaverse.Voice.Vivox` | 64 | 531 | callable failure-only shim |

View File

@@ -242,9 +242,7 @@ impl OSDParser {
buffer: Vec<libremetaverse_types::compat::Utf16CodeUnit>, buffer: Vec<libremetaverse_types::compat::Utf16CodeUnit>,
offset: i32, offset: i32,
) -> Result<i32, crate::Error> { ) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::buffer_characters_equal(reader, buffer, offset)
"M:LibreMetaverse.StructuredData.OSDParser.BufferCharactersEqual(System.IO.StringReader,System.Char[],System.Int32)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)`.
pub fn consume_bytes( pub fn consume_bytes(
@@ -303,17 +301,13 @@ impl OSDParser {
pub fn deserialize_llsd_notation_with_string_reader( pub fn deserialize_llsd_notation_with_string_reader(
reader: libremetaverse_types::compat::StringReader, reader: libremetaverse_types::compat::StringReader,
) -> Result<libremetaverse_structured_data::OSD, crate::Error> { ) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::deserialize_reader(reader)
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.IO.StringReader)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)`.
pub fn deserialize_llsd_notation_with_string( pub fn deserialize_llsd_notation_with_string(
notation_data: String, notation_data: String,
) -> Result<libremetaverse_structured_data::OSD, crate::Error> { ) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::deserialize_string(notation_data)
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])`.
pub fn deserialize_llsd_protobuf_with_bytes( pub fn deserialize_llsd_protobuf_with_bytes(
@@ -368,9 +362,7 @@ impl OSDParser {
s: String, s: String,
c: libremetaverse_types::compat::Utf16CodeUnit, c: libremetaverse_types::compat::Utf16CodeUnit,
) -> Result<String, crate::Error> { ) -> Result<String, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::escape_character(s, c)
"M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)`.
pub fn find_byte( pub fn find_byte(
@@ -390,18 +382,14 @@ impl OSDParser {
pub fn get_length_in_brackets( pub fn get_length_in_brackets(
reader: libremetaverse_types::compat::StringReader, reader: libremetaverse_types::compat::StringReader,
) -> Result<i32, crate::Error> { ) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::get_length_in_brackets(reader)
"M:LibreMetaverse.StructuredData.OSDParser.GetLengthInBrackets(System.IO.StringReader)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)`.
pub fn get_string_delimited_by( pub fn get_string_delimited_by(
reader: libremetaverse_types::compat::StringReader, reader: libremetaverse_types::compat::StringReader,
delimiter: libremetaverse_types::compat::Utf16CodeUnit, delimiter: libremetaverse_types::compat::Utf16CodeUnit,
) -> Result<String, crate::Error> { ) -> Result<String, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::get_string_delimited_by(reader, delimiter)
"M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)`.
pub fn host_to_network_int_bytes(int_host_end: i32) -> Result<Vec<u8>, crate::Error> { pub fn host_to_network_int_bytes(int_host_end: i32) -> Result<Vec<u8>, crate::Error> {
@@ -419,17 +407,13 @@ impl OSDParser {
pub fn peek_and_skip_whitespace( pub fn peek_and_skip_whitespace(
reader: libremetaverse_types::compat::StringReader, reader: libremetaverse_types::compat::StringReader,
) -> Result<i32, crate::Error> { ) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::peek_and_skip_whitespace(reader)
"M:LibreMetaverse.StructuredData.OSDParser.PeekAndSkipWhitespace(System.IO.StringReader)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)`.
pub fn read_and_skip_whitespace( pub fn read_and_skip_whitespace(
reader: libremetaverse_types::compat::StringReader, reader: libremetaverse_types::compat::StringReader,
) -> Result<i32, crate::Error> { ) -> Result<i32, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::read_and_skip_whitespace(reader)
"M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeJsonString(LibreMetaverse.StructuredData.OSD,System.Boolean)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeJsonString(LibreMetaverse.StructuredData.OSD,System.Boolean)`.
pub fn serialize_json_string( pub fn serialize_json_string(
@@ -478,33 +462,25 @@ impl OSDParser {
pub fn serialize_llsd_notation( pub fn serialize_llsd_notation(
osd: libremetaverse_structured_data::OSD, osd: libremetaverse_structured_data::OSD,
) -> Result<String, crate::Error> { ) -> Result<String, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::serialize(osd)
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)`.
pub fn serialize_llsd_notation_formatted( pub fn serialize_llsd_notation_formatted(
osd: libremetaverse_structured_data::OSD, osd: libremetaverse_structured_data::OSD,
) -> Result<String, crate::Error> { ) -> Result<String, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::serialize_formatted(osd)
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)`.
pub fn serialize_llsd_notation_stream( pub fn serialize_llsd_notation_stream(
osd: libremetaverse_structured_data::OSD, osd: libremetaverse_structured_data::OSD,
) -> Result<libremetaverse_types::compat::StringWriter, crate::Error> { ) -> Result<libremetaverse_types::compat::StringWriter, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::serialize_stream(osd)
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)`.
pub fn serialize_llsd_notation_stream_formatted( pub fn serialize_llsd_notation_stream_formatted(
osd: libremetaverse_structured_data::OSD, osd: libremetaverse_structured_data::OSD,
) -> Result<libremetaverse_types::compat::StringWriter, crate::Error> { ) -> Result<libremetaverse_types::compat::StringWriter, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::serialize_stream_formatted(osd)
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)",
)
} }
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDProtobuf(LibreMetaverse.StructuredData.OSD,System.Boolean)`. /// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDProtobuf(LibreMetaverse.StructuredData.OSD,System.Boolean)`.
pub fn serialize_llsd_protobuf( pub fn serialize_llsd_protobuf(
@@ -551,9 +527,7 @@ impl OSDParser {
s: String, s: String,
c: libremetaverse_types::compat::Utf16CodeUnit, c: libremetaverse_types::compat::Utf16CodeUnit,
) -> Result<String, crate::Error> { ) -> Result<String, crate::Error> {
libremetaverse_types::not_implemented( crate::notation::unescape_character(s, c)
"M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)",
)
} }
} }

View File

@@ -6,6 +6,7 @@ mod binary;
mod dispatch; mod dispatch;
mod generated; mod generated;
mod model; mod model;
mod notation;
pub mod xml { pub mod xml {
/// Project-owned XML element boundary; external XML APIs are not copied. /// Project-owned XML element boundary; external XML APIs are not copied.

View File

@@ -273,11 +273,11 @@ impl OSD {
Ok(match self { Ok(match self {
Self::Boolean(value) => if *value { "1" } else { "0" }.to_owned(), Self::Boolean(value) => if *value { "1" } else { "0" }.to_owned(),
Self::Integer(value) => value.to_string(), 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::String(value) | Self::LlsdXml(value) => value.clone(),
Self::UUID(value) => value.to_string(), Self::UUID(value) => value.to_string(),
Self::Date(value) => format_system_time(*value), Self::Date(value) => format_system_time_for_codec(*value),
Self::Uri(value) => value.0.clone(), Self::Uri(value) => format_uri_for_codec(&value.0),
Self::Binary(value) => base64::engine::general_purpose::STANDARD.encode(value), Self::Binary(value) => base64::engine::general_purpose::STANDARD.encode(value),
_ => String::new(), _ => String::new(),
}) })
@@ -296,7 +296,7 @@ impl OSD {
pub fn as_date(&self) -> Result<SystemTime, Error> { pub fn as_date(&self) -> Result<SystemTime, Error> {
Ok(match self { Ok(match self {
Self::Date(value) => *value, 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, _ => UNIX_EPOCH,
}) })
} }
@@ -776,7 +776,7 @@ impl OSDReal {
Ok(self.value) Ok(self.value)
} }
pub fn as_string(&self) -> Result<String, Error> { pub fn as_string(&self) -> Result<String, Error> {
Ok(format_real(self.value)) Ok(format_real_for_codec(self.value))
} }
pub fn as_u_integer(&self) -> Result<u32, Error> { pub fn as_u_integer(&self) -> Result<u32, Error> {
Ok(real_to_u32(self.value)) Ok(real_to_u32(self.value))
@@ -795,7 +795,7 @@ impl OSDString {
Ok(string_as_boolean(&self.value)) Ok(string_as_boolean(&self.value))
} }
pub fn as_date(&self) -> Result<SystemTime, Error> { pub fn as_date(&self) -> Result<SystemTime, Error> {
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<i32, Error> { pub fn as_integer(&self) -> Result<i32, Error> {
Ok(string_to_f64(&self.value).map_or(0, floor_to_i32)) 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))) Ok(i64::from(unix_seconds_u32(self.value)))
} }
pub fn as_string(&self) -> Result<String, Error> { pub fn as_string(&self) -> Result<String, Error> {
Ok(format_system_time(self.value)) Ok(format_system_time_for_codec(self.value))
} }
pub fn as_u_integer(&self) -> Result<u32, Error> { pub fn as_u_integer(&self) -> Result<u32, Error> {
Ok(unix_seconds_u32(self.value)) Ok(unix_seconds_u32(self.value))
@@ -1414,7 +1414,7 @@ fn real_to_u64(value: f64) -> u64 {
value.round_ties_even() as 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() { if value.is_nan() {
"NaN".to_owned() "NaN".to_owned()
} else if value == f64::INFINITY { } 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) { let (whole, nanos) = match value.duration_since(UNIX_EPOCH) {
Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()), Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()),
Err(error) => { 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<SystemTime> { pub(crate) fn parse_system_time_for_codec(value: &str) -> Option<SystemTime> {
let value = value.trim(); let value = value.trim();
let (date, time) = value.split_once('T').or_else(|| value.split_once(' '))?; let (date, time) = value.split_once('T').or_else(|| value.split_once(' '))?;
let mut date_parts = date.split('-'); 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) !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 { fn escape_text(value: &str) -> String {
let mut output = String::with_capacity(value.len() + 2); let mut output = String::with_capacity(value.len() + 2);
output.push('"'); output.push('"');
@@ -1597,7 +1620,7 @@ fn json_text_with_defaults(value: &OSD) -> String {
OSD::Undefined => "null".to_owned(), OSD::Undefined => "null".to_owned(),
OSD::Boolean(value) => value.to_string(), OSD::Boolean(value) => value.to_string(),
OSD::Integer(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::String(value) => escape_text(value),
OSD::LlsdXml(_) => "null".to_owned(), OSD::LlsdXml(_) => "null".to_owned(),
OSD::UUID(_) | OSD::Date(_) | OSD::Uri(_) => { OSD::UUID(_) | OSD::Date(_) | OSD::Uri(_) => {

View File

@@ -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<OSD, Error> {
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<OSD, Error> {
deserialize_string(reader.0)
}
pub(crate) fn serialize(value: OSD) -> Result<String, Error> {
serialize_internal(value, false)
}
pub(crate) fn serialize_formatted(value: OSD) -> Result<String, Error> {
serialize_internal(value, true)
}
pub(crate) fn serialize_stream(value: OSD) -> Result<StringWriter, Error> {
Ok(StringWriter(serialize(value)?))
}
pub(crate) fn serialize_stream_formatted(value: OSD) -> Result<StringWriter, Error> {
Ok(StringWriter(serialize_formatted(value)?))
}
fn serialize_internal(value: OSD, formatted: bool) -> Result<String, Error> {
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<Utf16CodeUnit>,
offset: i32,
) -> Result<i32, Error> {
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<i32, Error> {
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<i32, Error> {
peek_and_skip_whitespace(reader)
}
pub(crate) fn get_length_in_brackets(reader: StringReader) -> Result<i32, Error> {
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<String, Error> {
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<String, Error> {
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<String, Error> {
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<u16>,
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<OSD, Error> {
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<OSD, Error> {
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::<i32>()
.map_err(|_| self.error_at(start, "invalid notation LLSD integer"))?;
Ok(OSD::Integer(value))
}
fn parse_real(&mut self) -> Result<OSD, Error> {
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::<f64>()
.map_err(|_| self.error_at(start, "invalid notation LLSD real"))?;
Ok(OSD::Real(value))
}
fn parse_uuid(&mut self) -> Result<OSD, Error> {
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<OSD, Error> {
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<u8> = units
.iter()
.copied()
.map(|unit| {
u8::try_from(unit).map_err(|_| {
self.error_at(start, "invalid notation LLSD raw binary character")
})
})
.collect::<Result<_, _>>()?;
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<OSD, Error> {
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<OSD, Error> {
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<OSD, Error> {
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<OSD, Error> {
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::<OSD>()),
"notation LLSD array allocation limit exceeded",
)?;
Ok(OSD::Array(values))
}
fn parse_map(&mut self, depth: usize) -> Result<OSD, Error> {
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<usize, Error> {
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::<usize>()
.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<String, Error> {
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<u16, Error> {
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<String, Error> {
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<String, Error> {
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<u16> {
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<u16> {
self.units.get(self.position).copied()
}
fn read_unit(&mut self, context: &'static str) -> Result<u16, Error> {
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<String, OSD>,
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<String, OSD>) -> Vec<(&String, &OSD)> {
let mut entries: Vec<_> = values.iter().collect();
entries.sort_unstable_by_key(|(key, _)| *key);
entries
}
fn decode_base16(value: &str) -> Result<Vec<u8>, &'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<Vec<u8>, &'static str> {
let compact: Vec<u8> = 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<u8> {
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",
..
})
));
}
}

View File

@@ -802,11 +802,11 @@ fn notation_deserialize_string() {
for (input, expected) in [ for (input, expected) in [
("''", ""), ("''", ""),
(r#"'test\'\"test'"#, "test'\"test"), (r#"'test\'\"test'"#, "test'\"test"),
("'test \\\\lest'", "test \\\\lest"), ("'test \\\\lest'", "test \\lest"),
("'aa\t la'", "aa\t la"), ("'aa\t la'", "aa\t la"),
("'\\\\'", "\\"), ("'\\\\'", "\\"),
(r#"s(10)"1234567890""#, "1234567890"), (r#"s(10)"1234567890""#, "1234567890"),
(r#"s(5)"\\\""#, "\\\\\\\\\\"), (r#"s(5)"\\\\\""#, "\\\\\\\\\\"),
( (
r#""aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh""#, r#""aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh""#,
"aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh", "aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh",

View File

@@ -39,6 +39,31 @@
"LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUUID::test", "LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUUID::test",
"LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUndef::test", "LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUndef::test",
"LibreMetaverse.Tests/MarketplaceFolderClassifierTests.cs::MarketplaceFolderClassifierTests.ValidateListing_ValidFlags_AreBitmaskComposable::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.Quaternions::test",
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.TestMatrix::test", "LibreMetaverse.Tests/TypeTests.cs::TypeTests.TestMatrix::test",
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.UUIDs::test", "LibreMetaverse.Tests/TypeTests.cs::TypeTests.UUIDs::test",
@@ -46,5 +71,5 @@
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.VectorCasting::test", "LibreMetaverse.Tests/TypeTests.cs::TypeTests.VectorCasting::test",
"LibreMetaverse.Tests/UtilsConversionsTests.cs::UtilsConversionsTests.StringToBytes::test" "LibreMetaverse.Tests/UtilsConversionsTests.cs::UtilsConversionsTests.StringToBytes::test"
], ],
"support_passes": 76 "support_passes": 79
} }

View File

@@ -8611,7 +8611,7 @@
"body_sha256": "a29dc440ccde574c1302c57d3ef4c5404aa4fd419a5f160ba4920ab5a3349cc7", "body_sha256": "a29dc440ccde574c1302c57d3ef4c5404aa4fd419a5f160ba4920ab5a3349cc7",
"categories": [], "categories": [],
"fixture_dependencies": [], "fixture_dependencies": [],
"rust_body_sha256": "0bec6e4d609c3acd083b0e7e4c0588cb2fc3be1dcde3ed685866e559ec567e49", "rust_body_sha256": "780d333102a826359866f70bf895c6dbeaf7489b77f071efd20cac029b7b9fb8",
"status": "translated", "status": "translated",
"rust_file": "tests/compat/tests/structured_data.rs", "rust_file": "tests/compat/tests/structured_data.rs",
"rust_line": 799, "rust_line": 799,

View File

@@ -79,22 +79,38 @@ NATIVE_TYPES = {
} }
NATIVE_MEMBER_BODIES = { 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)": "M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)":
"crate::binary::consume_bytes(stream, consume_bytes)", "crate::binary::consume_bytes(stream, consume_bytes)",
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.Byte[])": "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.Byte[])":
"crate::binary::deserialize_bytes(binary_data)", "crate::binary::deserialize_bytes(binary_data)",
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.IO.Stream)": "M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.IO.Stream)":
"crate::binary::deserialize_stream(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)": "M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)":
"crate::binary::find_byte(stream, to_find)", "crate::binary::find_byte(stream, to_find)",
"M:LibreMetaverse.StructuredData.OSDParser.FindString(System.IO.Stream,System.String)": "M:LibreMetaverse.StructuredData.OSDParser.FindString(System.IO.Stream,System.String)":
"crate::binary::find_string(stream, to_find)", "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)": "M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)":
"crate::binary::host_to_network_int_bytes(int_host_end)", "crate::binary::host_to_network_int_bytes(int_host_end)",
"M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostDouble(System.Byte[])": "M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostDouble(System.Byte[])":
"crate::binary::network_to_host_double(binary_net_end)", "crate::binary::network_to_host_double(binary_net_end)",
"M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostInt(System.Byte[])": "M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostInt(System.Byte[])":
"crate::binary::network_to_host_int(binary_net_end)", "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)": "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD)":
"crate::binary::serialize(osd)", "crate::binary::serialize(osd)",
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD,System.Boolean)": "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD,System.Boolean)":
@@ -103,8 +119,18 @@ NATIVE_MEMBER_BODIES = {
"crate::binary::serialize_stream(data)", "crate::binary::serialize_stream(data)",
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)": "M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)":
"crate::binary::serialize_stream_with_header(data, prepend_header)", "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)": "M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)":
"crate::binary::skip_whitespace(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[])": "M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.Byte[])":
"crate::dispatch::deserialize_bytes(data)", "crate::dispatch::deserialize_bytes(data)",
"M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.IO.Stream)": "M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.IO.Stream)":

View File

@@ -982,6 +982,7 @@ def validate_generated_shims() -> None:
"crate::byte_order::", "crate::byte_order::",
"crate::binary::", "crate::binary::",
"crate::dispatch::", "crate::dispatch::",
"crate::notation::",
) )
): ):
raise ValueError(f"generated shim function does not use the standardized failure: {path.relative_to(ROOT)}") raise ValueError(f"generated shim function does not use the standardized failure: {path.relative_to(ROOT)}")