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
|
||||
same byte, depth, node, and allocation bounds; map keys are encoded in sorted
|
||||
order to make output stable despite the model's intentionally unordered maps.
|
||||
The bounded Notation LLSD codec preserves UTF-16 length-prefix semantics, the
|
||||
reference escape rules and accepted scalar spellings, base64, base16 and
|
||||
length-prefixed binary forms, and both compact and formatted output. Malformed
|
||||
notation reports UTF-16 offsets with parser context, while input, output,
|
||||
nesting, node, and aggregate allocation limits apply to untrusted text.
|
||||
The controlled audit aggregates every expected failure by standardized C#
|
||||
member ID and rejects unrelated fixture, assertion, compile, or symbol errors.
|
||||
|
||||
@@ -12,7 +12,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
||||
| `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim |
|
||||
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
|
||||
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim |
|
||||
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 15 types / 268 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 15 types / 281 members; remaining surface is callable failure-only shims |
|
||||
| `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |
|
||||
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |
|
||||
| `LibreMetaverse.Voice.Vivox` | 64 | 531 | callable failure-only shim |
|
||||
|
||||
@@ -242,9 +242,7 @@ impl OSDParser {
|
||||
buffer: Vec<libremetaverse_types::compat::Utf16CodeUnit>,
|
||||
offset: i32,
|
||||
) -> Result<i32, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.BufferCharactersEqual(System.IO.StringReader,System.Char[],System.Int32)",
|
||||
)
|
||||
crate::notation::buffer_characters_equal(reader, buffer, offset)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)`.
|
||||
pub fn consume_bytes(
|
||||
@@ -303,17 +301,13 @@ impl OSDParser {
|
||||
pub fn deserialize_llsd_notation_with_string_reader(
|
||||
reader: libremetaverse_types::compat::StringReader,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.IO.StringReader)",
|
||||
)
|
||||
crate::notation::deserialize_reader(reader)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)`.
|
||||
pub fn deserialize_llsd_notation_with_string(
|
||||
notation_data: String,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)",
|
||||
)
|
||||
crate::notation::deserialize_string(notation_data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])`.
|
||||
pub fn deserialize_llsd_protobuf_with_bytes(
|
||||
@@ -368,9 +362,7 @@ impl OSDParser {
|
||||
s: String,
|
||||
c: libremetaverse_types::compat::Utf16CodeUnit,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)",
|
||||
)
|
||||
crate::notation::escape_character(s, c)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)`.
|
||||
pub fn find_byte(
|
||||
@@ -390,18 +382,14 @@ impl OSDParser {
|
||||
pub fn get_length_in_brackets(
|
||||
reader: libremetaverse_types::compat::StringReader,
|
||||
) -> Result<i32, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.GetLengthInBrackets(System.IO.StringReader)",
|
||||
)
|
||||
crate::notation::get_length_in_brackets(reader)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)`.
|
||||
pub fn get_string_delimited_by(
|
||||
reader: libremetaverse_types::compat::StringReader,
|
||||
delimiter: libremetaverse_types::compat::Utf16CodeUnit,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)",
|
||||
)
|
||||
crate::notation::get_string_delimited_by(reader, delimiter)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)`.
|
||||
pub fn host_to_network_int_bytes(int_host_end: i32) -> Result<Vec<u8>, crate::Error> {
|
||||
@@ -419,17 +407,13 @@ impl OSDParser {
|
||||
pub fn peek_and_skip_whitespace(
|
||||
reader: libremetaverse_types::compat::StringReader,
|
||||
) -> Result<i32, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.PeekAndSkipWhitespace(System.IO.StringReader)",
|
||||
)
|
||||
crate::notation::peek_and_skip_whitespace(reader)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)`.
|
||||
pub fn read_and_skip_whitespace(
|
||||
reader: libremetaverse_types::compat::StringReader,
|
||||
) -> Result<i32, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)",
|
||||
)
|
||||
crate::notation::read_and_skip_whitespace(reader)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeJsonString(LibreMetaverse.StructuredData.OSD,System.Boolean)`.
|
||||
pub fn serialize_json_string(
|
||||
@@ -478,33 +462,25 @@ impl OSDParser {
|
||||
pub fn serialize_llsd_notation(
|
||||
osd: libremetaverse_structured_data::OSD,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::notation::serialize(osd)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_notation_formatted(
|
||||
osd: libremetaverse_structured_data::OSD,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::notation::serialize_formatted(osd)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_notation_stream(
|
||||
osd: libremetaverse_structured_data::OSD,
|
||||
) -> Result<libremetaverse_types::compat::StringWriter, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::notation::serialize_stream(osd)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_notation_stream_formatted(
|
||||
osd: libremetaverse_structured_data::OSD,
|
||||
) -> Result<libremetaverse_types::compat::StringWriter, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::notation::serialize_stream_formatted(osd)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDProtobuf(LibreMetaverse.StructuredData.OSD,System.Boolean)`.
|
||||
pub fn serialize_llsd_protobuf(
|
||||
@@ -551,9 +527,7 @@ impl OSDParser {
|
||||
s: String,
|
||||
c: libremetaverse_types::compat::Utf16CodeUnit,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)",
|
||||
)
|
||||
crate::notation::unescape_character(s, c)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ mod binary;
|
||||
mod dispatch;
|
||||
mod generated;
|
||||
mod model;
|
||||
mod notation;
|
||||
|
||||
pub mod xml {
|
||||
/// Project-owned XML element boundary; external XML APIs are not copied.
|
||||
|
||||
@@ -273,11 +273,11 @@ impl OSD {
|
||||
Ok(match self {
|
||||
Self::Boolean(value) => if *value { "1" } else { "0" }.to_owned(),
|
||||
Self::Integer(value) => value.to_string(),
|
||||
Self::Real(value) => format_real(*value),
|
||||
Self::Real(value) => format_real_for_codec(*value),
|
||||
Self::String(value) | Self::LlsdXml(value) => value.clone(),
|
||||
Self::UUID(value) => value.to_string(),
|
||||
Self::Date(value) => format_system_time(*value),
|
||||
Self::Uri(value) => value.0.clone(),
|
||||
Self::Date(value) => format_system_time_for_codec(*value),
|
||||
Self::Uri(value) => format_uri_for_codec(&value.0),
|
||||
Self::Binary(value) => base64::engine::general_purpose::STANDARD.encode(value),
|
||||
_ => String::new(),
|
||||
})
|
||||
@@ -296,7 +296,7 @@ impl OSD {
|
||||
pub fn as_date(&self) -> Result<SystemTime, Error> {
|
||||
Ok(match self {
|
||||
Self::Date(value) => *value,
|
||||
Self::String(value) => parse_system_time(value).unwrap_or(UNIX_EPOCH),
|
||||
Self::String(value) => parse_system_time_for_codec(value).unwrap_or(UNIX_EPOCH),
|
||||
_ => UNIX_EPOCH,
|
||||
})
|
||||
}
|
||||
@@ -776,7 +776,7 @@ impl OSDReal {
|
||||
Ok(self.value)
|
||||
}
|
||||
pub fn as_string(&self) -> Result<String, Error> {
|
||||
Ok(format_real(self.value))
|
||||
Ok(format_real_for_codec(self.value))
|
||||
}
|
||||
pub fn as_u_integer(&self) -> Result<u32, Error> {
|
||||
Ok(real_to_u32(self.value))
|
||||
@@ -795,7 +795,7 @@ impl OSDString {
|
||||
Ok(string_as_boolean(&self.value))
|
||||
}
|
||||
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> {
|
||||
Ok(string_to_f64(&self.value).map_or(0, floor_to_i32))
|
||||
@@ -854,7 +854,7 @@ impl OSDDate {
|
||||
Ok(i64::from(unix_seconds_u32(self.value)))
|
||||
}
|
||||
pub fn as_string(&self) -> Result<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> {
|
||||
Ok(unix_seconds_u32(self.value))
|
||||
@@ -1414,7 +1414,7 @@ fn real_to_u64(value: f64) -> u64 {
|
||||
value.round_ties_even() as u64
|
||||
}
|
||||
}
|
||||
fn format_real(value: f64) -> String {
|
||||
pub(crate) fn format_real_for_codec(value: f64) -> String {
|
||||
if value.is_nan() {
|
||||
"NaN".to_owned()
|
||||
} else if value == f64::INFINITY {
|
||||
@@ -1438,7 +1438,7 @@ fn unix_seconds_u32(value: SystemTime) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_system_time(value: SystemTime) -> String {
|
||||
pub(crate) fn format_system_time_for_codec(value: SystemTime) -> String {
|
||||
let (whole, nanos) = match value.duration_since(UNIX_EPOCH) {
|
||||
Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()),
|
||||
Err(error) => {
|
||||
@@ -1485,7 +1485,7 @@ fn civil_from_unix(seconds: i64) -> (i32, u32, u32, u32, u32, u32) {
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_system_time(value: &str) -> Option<SystemTime> {
|
||||
pub(crate) fn parse_system_time_for_codec(value: &str) -> Option<SystemTime> {
|
||||
let value = value.trim();
|
||||
let (date, time) = value.split_once('T').or_else(|| value.split_once(' '))?;
|
||||
let mut date_parts = date.split('-');
|
||||
@@ -1556,6 +1556,29 @@ fn valid_uri_reference(value: &str) -> bool {
|
||||
!value.chars().any(char::is_whitespace) && !value.chars().any(char::is_control)
|
||||
}
|
||||
|
||||
fn format_uri_for_codec(value: &str) -> String {
|
||||
if !value.contains("://") {
|
||||
return value.to_owned();
|
||||
}
|
||||
let mut output = String::with_capacity(value.len());
|
||||
for character in value.chars() {
|
||||
match character {
|
||||
' ' => output.push_str("%20"),
|
||||
'"' => output.push_str("%22"),
|
||||
'<' => output.push_str("%3C"),
|
||||
'>' => output.push_str("%3E"),
|
||||
character if character.is_control() => {
|
||||
let mut bytes = [0_u8; 4];
|
||||
for byte in character.encode_utf8(&mut bytes).as_bytes() {
|
||||
write!(output, "%{byte:02X}").expect("writing to String is infallible");
|
||||
}
|
||||
}
|
||||
character => output.push(character),
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn escape_text(value: &str) -> String {
|
||||
let mut output = String::with_capacity(value.len() + 2);
|
||||
output.push('"');
|
||||
@@ -1597,7 +1620,7 @@ fn json_text_with_defaults(value: &OSD) -> String {
|
||||
OSD::Undefined => "null".to_owned(),
|
||||
OSD::Boolean(value) => value.to_string(),
|
||||
OSD::Integer(value) => value.to_string(),
|
||||
OSD::Real(value) => format_real(*value),
|
||||
OSD::Real(value) => format_real_for_codec(*value),
|
||||
OSD::String(value) => escape_text(value),
|
||||
OSD::LlsdXml(_) => "null".to_owned(),
|
||||
OSD::UUID(_) | OSD::Date(_) | OSD::Uri(_) => {
|
||||
|
||||
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 [
|
||||
("''", ""),
|
||||
(r#"'test\'\"test'"#, "test'\"test"),
|
||||
("'test \\\\lest'", "test \\\\lest"),
|
||||
("'test \\\\lest'", "test \\lest"),
|
||||
("'aa\t la'", "aa\t la"),
|
||||
("'\\\\'", "\\"),
|
||||
(r#"s(10)"1234567890""#, "1234567890"),
|
||||
(r#"s(5)"\\\""#, "\\\\\\\\\\"),
|
||||
(r#"s(5)"\\\\\""#, "\\\\\\\\\\"),
|
||||
(
|
||||
r#""aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh""#,
|
||||
"aouAOUhsdjklfghskldjfghqeiurtzwieortzaslxfjkgh",
|
||||
|
||||
@@ -39,6 +39,31 @@
|
||||
"LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUUID::test",
|
||||
"LibreMetaverse.Tests/BinaryLLSDTests.cs::BinarySDTests.SerializeUndef::test",
|
||||
"LibreMetaverse.Tests/MarketplaceFolderClassifierTests.cs::MarketplaceFolderClassifierTests.ValidateListing_ValidFlags_AreBitmaskComposable::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeArray::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeBoolean::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeDate::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeInteger::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeMap::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeReal::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeRealWorldExamples::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeString::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeURI::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeUUID::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.DeserializeUndef::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.HelperFunctions::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeArray::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeBinary::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeBoolean::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeDate::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeFormattedTest::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeInteger::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeMap::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeReal::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeString::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeURI::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeUUID::test",
|
||||
"LibreMetaverse.Tests/NotationLLSDTests.cs::NotationSDTests.SerializeUndef::test",
|
||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.LLSDTerseParsing::test",
|
||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.Quaternions::test",
|
||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.TestMatrix::test",
|
||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.UUIDs::test",
|
||||
@@ -46,5 +71,5 @@
|
||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.VectorCasting::test",
|
||||
"LibreMetaverse.Tests/UtilsConversionsTests.cs::UtilsConversionsTests.StringToBytes::test"
|
||||
],
|
||||
"support_passes": 76
|
||||
"support_passes": 79
|
||||
}
|
||||
|
||||
@@ -8611,7 +8611,7 @@
|
||||
"body_sha256": "a29dc440ccde574c1302c57d3ef4c5404aa4fd419a5f160ba4920ab5a3349cc7",
|
||||
"categories": [],
|
||||
"fixture_dependencies": [],
|
||||
"rust_body_sha256": "0bec6e4d609c3acd083b0e7e4c0588cb2fc3be1dcde3ed685866e559ec567e49",
|
||||
"rust_body_sha256": "780d333102a826359866f70bf895c6dbeaf7489b77f071efd20cac029b7b9fb8",
|
||||
"status": "translated",
|
||||
"rust_file": "tests/compat/tests/structured_data.rs",
|
||||
"rust_line": 799,
|
||||
|
||||
@@ -79,22 +79,38 @@ NATIVE_TYPES = {
|
||||
}
|
||||
|
||||
NATIVE_MEMBER_BODIES = {
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.BufferCharactersEqual(System.IO.StringReader,System.Char[],System.Int32)":
|
||||
"crate::notation::buffer_characters_equal(reader, buffer, offset)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)":
|
||||
"crate::binary::consume_bytes(stream, consume_bytes)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.Byte[])":
|
||||
"crate::binary::deserialize_bytes(binary_data)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.IO.Stream)":
|
||||
"crate::binary::deserialize_stream(stream)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.IO.StringReader)":
|
||||
"crate::notation::deserialize_reader(reader)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)":
|
||||
"crate::notation::deserialize_string(notation_data)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)":
|
||||
"crate::notation::escape_character(s, c)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)":
|
||||
"crate::binary::find_byte(stream, to_find)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.FindString(System.IO.Stream,System.String)":
|
||||
"crate::binary::find_string(stream, to_find)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.GetLengthInBrackets(System.IO.StringReader)":
|
||||
"crate::notation::get_length_in_brackets(reader)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.GetStringDelimitedBy(System.IO.StringReader,System.Char)":
|
||||
"crate::notation::get_string_delimited_by(reader, delimiter)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)":
|
||||
"crate::binary::host_to_network_int_bytes(int_host_end)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostDouble(System.Byte[])":
|
||||
"crate::binary::network_to_host_double(binary_net_end)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostInt(System.Byte[])":
|
||||
"crate::binary::network_to_host_int(binary_net_end)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.PeekAndSkipWhitespace(System.IO.StringReader)":
|
||||
"crate::notation::peek_and_skip_whitespace(reader)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.ReadAndSkipWhitespace(System.IO.StringReader)":
|
||||
"crate::notation::read_and_skip_whitespace(reader)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::binary::serialize(osd)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD,System.Boolean)":
|
||||
@@ -103,8 +119,18 @@ NATIVE_MEMBER_BODIES = {
|
||||
"crate::binary::serialize_stream(data)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)":
|
||||
"crate::binary::serialize_stream_with_header(data, prepend_header)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::notation::serialize(osd)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::notation::serialize_formatted(osd)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStream(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::notation::serialize_stream(osd)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)":
|
||||
"crate::notation::serialize_stream_formatted(osd)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)":
|
||||
"crate::binary::skip_whitespace(stream)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)":
|
||||
"crate::notation::unescape_character(s, c)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.Byte[])":
|
||||
"crate::dispatch::deserialize_bytes(data)",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.IO.Stream)":
|
||||
|
||||
@@ -982,6 +982,7 @@ def validate_generated_shims() -> None:
|
||||
"crate::byte_order::",
|
||||
"crate::binary::",
|
||||
"crate::dispatch::",
|
||||
"crate::notation::",
|
||||
)
|
||||
):
|
||||
raise ValueError(f"generated shim function does not use the standardized failure: {path.relative_to(ROOT)}")
|
||||
|
||||
Reference in New Issue
Block a user