All checks were successful
Native code generation / deterministic (push) Successful in 11m59s
Imaging and meshing gate / native (push) Successful in 3m55s
JPEG 2000 feature / linux (push) Successful in 2m26s
Native Rust workspace compile / compile (push) Successful in 3m58s
Skia feature / linux (push) Successful in 31m44s
972 lines
33 KiB
Rust
972 lines
33 KiB
Rust
//! 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().is_multiple_of(2) {
|
|
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",
|
|
..
|
|
})
|
|
));
|
|
}
|
|
}
|