Implement bounded Notation LLSD codec
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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(_) => {
|
||||||
|
|||||||
971
crates/libremetaverse-structured-data/src/notation.rs
Normal file
971
crates/libremetaverse-structured-data/src/notation.rs
Normal 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",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)":
|
||||||
|
|||||||
@@ -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)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user