//! Bounded Binary LLSD codec compatible with `LibreMetaverse`'s marker grammar. #![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 libremetaverse_types::UUID; use libremetaverse_types::compat::{ReadWrite, Uri}; use std::collections::HashMap; use std::io::{Cursor, Read as _, Seek as _, SeekFrom}; use std::time::{Duration, UNIX_EPOCH}; const HEADER: &[u8] = b""; const ALT_HEADER: &[u8] = b""; const MAX_INPUT_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES; const fn is_binary_whitespace(byte: u8) -> bool { matches!(byte, b' ' | b'\t' | b'\n' | b'\r') } pub(crate) fn deserialize_bytes(data: Vec) -> Result { if data.len() > MAX_INPUT_BYTES { return Err(parse_error(0, "binary LLSD input exceeds allocation limit")); } let mut parser = Parser::new(&data); parser.skip_whitespace(); if parser.consume_prefix_ignore_ascii_case(ALT_HEADER) || parser.consume_prefix_ignore_ascii_case(HEADER) { parser.skip_whitespace(); } 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_stream(mut stream: Box) -> Result { let mut data = Vec::new(); (&mut *stream) .take((MAX_INPUT_BYTES + 1) as u64) .read_to_end(&mut data) .map_err(|_| Error::InvalidOperation)?; deserialize_bytes(data) } pub(crate) fn serialize(value: OSD) -> Result, Error> { serialize_with_header(value, true) } pub(crate) fn serialize_with_header(value: OSD, prepend_header: bool) -> Result, Error> { value.validate_limits( OSD::DEFAULT_MAX_DEPTH, OSD::DEFAULT_MAX_NODES, OSD::DEFAULT_MAX_BINARY_BYTES, )?; let mut encoder = Encoder::new(); if prepend_header { encoder.extend(HEADER)?; encoder.push(b'\n')?; } encoder.write_value(&value, 0)?; Ok(encoder.finish()) } pub(crate) fn serialize_stream(value: OSD) -> Result>, Error> { serialize_stream_with_header(value, true) } pub(crate) fn serialize_stream_with_header( value: OSD, prepend_header: bool, ) -> Result>, Error> { let bytes = serialize_with_header(value, prepend_header)?; let position = bytes.len() as u64; let mut cursor = Cursor::new(bytes); cursor.set_position(position); Ok(cursor) } pub(crate) fn consume_bytes( mut stream: Box, consume_bytes: i32, ) -> Result, Error> { let length = usize::try_from(consume_bytes).map_err(|_| Error::Argument)?; if length > MAX_INPUT_BYTES { return Err(Error::Argument); } let mut bytes = vec![0_u8; length]; stream .read_exact(&mut bytes) .map_err(|_| Error::IndexOutOfRange)?; Ok(bytes) } pub(crate) fn find_byte(mut stream: Box, to_find: u8) -> Result { let start = stream .stream_position() .map_err(|_| Error::InvalidOperation)?; let mut byte = [0_u8; 1]; if stream .read(&mut byte) .map_err(|_| Error::InvalidOperation)? == 0 { return Ok(false); } if byte[0] == to_find { Ok(true) } else { stream .seek(SeekFrom::Start(start)) .map_err(|_| Error::InvalidOperation)?; Ok(false) } } pub(crate) fn find_string( mut stream: Box, to_find: String, ) -> Result { let start = stream .stream_position() .map_err(|_| Error::InvalidOperation)?; let expected = to_find.as_bytes(); if expected.len() > MAX_INPUT_BYTES { return Err(Error::Argument); } let mut actual = vec![0_u8; expected.len()]; let matched = stream.read_exact(&mut actual).is_ok() && actual .iter() .zip(expected) .all(|(left, right)| left.eq_ignore_ascii_case(right)); if !matched { stream .seek(SeekFrom::Start(start)) .map_err(|_| Error::InvalidOperation)?; } Ok(matched) } pub(crate) fn skip_whitespace(mut stream: Box) -> Result<(), Error> { loop { let start = stream .stream_position() .map_err(|_| Error::InvalidOperation)?; let mut byte = [0_u8; 1]; if stream .read(&mut byte) .map_err(|_| Error::InvalidOperation)? == 0 { return Ok(()); } if !is_binary_whitespace(byte[0]) { stream .seek(SeekFrom::Start(start)) .map_err(|_| Error::InvalidOperation)?; return Ok(()); } } } pub(crate) fn host_to_network_int_bytes(value: i32) -> Result, Error> { Ok(value.to_be_bytes().to_vec()) } pub(crate) fn network_to_host_int(bytes: Vec) -> Result { let bytes: [u8; 4] = bytes .get(..4) .ok_or(Error::IndexOutOfRange)? .try_into() .map_err(|_| Error::IndexOutOfRange)?; Ok(i32::from_be_bytes(bytes)) } pub(crate) fn network_to_host_double(bytes: Vec) -> Result { let bytes: [u8; 8] = bytes .get(..8) .ok_or(Error::IndexOutOfRange)? .try_into() .map_err(|_| Error::IndexOutOfRange)?; Ok(f64::from_be_bytes(bytes)) } struct Parser<'a> { bytes: &'a [u8], position: usize, nodes: usize, allocated: usize, } impl<'a> Parser<'a> { const fn new(bytes: &'a [u8]) -> Self { Self { bytes, position: 0, nodes: 0, allocated: 0, } } fn parse_value(&mut self, depth: usize) -> Result { if depth > OSD::DEFAULT_MAX_DEPTH { return Err(self.error("binary LLSD nesting depth exceeded")); } self.nodes = self .nodes .checked_add(1) .ok_or_else(|| self.error("binary LLSD node count overflow"))?; if self.nodes > OSD::DEFAULT_MAX_NODES { return Err(self.error("binary LLSD node limit exceeded")); } self.skip_whitespace(); let marker_position = self.position; let marker = self.read_byte("missing binary LLSD value marker")?; match marker { b'!' => Ok(OSD::Undefined), b'1' => Ok(OSD::Boolean(true)), b'0' => Ok(OSD::Boolean(false)), b'i' => Ok(OSD::Integer(i32::from_be_bytes( self.read_array("truncated binary LLSD integer")?, ))), b'r' => Ok(OSD::Real(f64::from_be_bytes( self.read_array("truncated binary LLSD real")?, ))), b'u' => { let bytes = self.read_exact(16, "truncated binary LLSD UUID")?; Ok(OSD::UUID(UUID::new_with_bytes_int32(bytes.to_vec(), 0)?)) } b'b' => { let length = self.read_length("invalid binary LLSD binary length")?; self.add_allocation(length, "binary LLSD binary allocation limit exceeded")?; Ok(OSD::Binary( self.read_exact(length, "truncated binary LLSD binary value")? .to_vec(), )) } b's' => { let bytes = self.read_sized_text("string")?; Ok(OSD::String(String::from_utf8_lossy(bytes).into_owned())) } b'l' => { let bytes = self.read_sized_text("URI")?; let text = String::from_utf8_lossy(bytes).into_owned(); let uri = OSD::String(text.clone()) .as_uri()? .ok_or_else(|| self.error_at(marker_position, "invalid binary LLSD URI"))?; Ok(OSD::Uri(uri)) } b'd' => { let timestamp = f64::from_le_bytes(self.read_array("truncated binary LLSD date")?); Ok(OSD::Date(system_time_from_seconds(timestamp).ok_or_else( || self.error_at(marker_position, "invalid binary LLSD date"), )?)) } b'[' => self.parse_array(depth), b'{' => self.parse_map(depth), _ => Err(self.error_at(marker_position, "unknown binary LLSD type marker")), } } fn parse_array(&mut self, depth: usize) -> Result { let count = self.read_length("invalid binary LLSD array count")?; if count > OSD::DEFAULT_MAX_NODES || count > self.remaining() { return Err(self.error("binary LLSD array count exceeds available input")); } let allocation = count .checked_mul(std::mem::size_of::()) .ok_or_else(|| self.error("binary LLSD array allocation overflow"))?; self.add_allocation(allocation, "binary LLSD array allocation limit exceeded")?; let mut values = Vec::with_capacity(count); for _ in 0..count { values.push(self.parse_value(depth + 1)?); } self.expect_byte(b']', "missing binary LLSD array end marker")?; Ok(OSD::Array(values)) } fn parse_map(&mut self, depth: usize) -> Result { let count = self.read_length("invalid binary LLSD map count")?; if count > OSD::DEFAULT_MAX_NODES || count > self.remaining() { return Err(self.error("binary LLSD map count exceeds available input")); } let allocation = count .checked_mul(std::mem::size_of::<(String, OSD)>()) .ok_or_else(|| self.error("binary LLSD map allocation overflow"))?; self.add_allocation(allocation, "binary LLSD map allocation limit exceeded")?; let mut values = HashMap::with_capacity(count); for _ in 0..count { self.expect_byte(b'k', "missing binary LLSD map key marker")?; let key_bytes = self.read_sized_text("map key")?; let key = String::from_utf8_lossy(key_bytes).into_owned(); let value = self.parse_value(depth + 1)?; values.insert(key, value); } self.expect_byte(b'}', "missing binary LLSD map end marker")?; Ok(OSD::Map(values)) } fn read_sized_text(&mut self, context: &'static str) -> Result<&'a [u8], Error> { let length = self.read_length(match context { "string" => "invalid binary LLSD string length", "URI" => "invalid binary LLSD URI length", _ => "invalid binary LLSD map key length", })?; self.add_allocation(length, "binary LLSD text allocation limit exceeded")?; self.read_exact( length, match context { "string" => "truncated binary LLSD string", "URI" => "truncated binary LLSD URI", _ => "truncated binary LLSD map key", }, ) } fn read_length(&mut self, context: &'static str) -> Result { let position = self.position; let raw = i32::from_be_bytes(self.read_array(context)?); usize::try_from(raw).map_err(|_| self.error_at(position, context)) } fn read_array(&mut self, context: &'static str) -> Result<[u8; N], Error> { self.read_exact(N, context)? .try_into() .map_err(|_| self.error(context)) } fn read_exact(&mut self, length: usize, context: &'static str) -> Result<&'a [u8], Error> { let end = self .position .checked_add(length) .ok_or_else(|| self.error(context))?; let value = self .bytes .get(self.position..end) .ok_or_else(|| self.error(context))?; self.position = end; Ok(value) } fn read_byte(&mut self, context: &'static str) -> Result { let byte = *self .bytes .get(self.position) .ok_or_else(|| self.error(context))?; self.position += 1; Ok(byte) } fn expect_byte(&mut self, expected: u8, context: &'static str) -> Result<(), Error> { let position = self.position; if self.read_byte(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(()) } } fn skip_whitespace(&mut self) { while self .bytes .get(self.position) .is_some_and(|byte| is_binary_whitespace(*byte)) { self.position += 1; } } fn consume_prefix_ignore_ascii_case(&mut self, prefix: &[u8]) -> bool { let Some(candidate) = self.bytes.get(self.position..self.position + prefix.len()) else { return false; }; if candidate.eq_ignore_ascii_case(prefix) { self.position += prefix.len(); true } else { false } } const fn remaining(&self) -> usize { self.bytes.len() - self.position } 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 { bytes: Vec, } impl Encoder { fn new() -> Self { Self { bytes: Vec::with_capacity(128), } } fn finish(self) -> Vec { self.bytes } fn write_value(&mut self, value: &OSD, depth: usize) -> Result<(), Error> { if depth > OSD::DEFAULT_MAX_DEPTH { return Err(parse_error( self.bytes.len(), "binary LLSD nesting depth exceeded", )); } match value { OSD::Undefined => self.push(b'!'), OSD::Boolean(value) => self.push(if *value { b'1' } else { b'0' }), OSD::Integer(value) => { self.push(b'i')?; self.extend(&value.to_be_bytes()) } OSD::Real(value) => { self.push(b'r')?; self.extend(&value.to_be_bytes()) } OSD::UUID(value) => { self.push(b'u')?; self.extend(&value.get_bytes()?) } OSD::String(value) => { self.push(b's')?; self.write_sized(value.as_bytes()) } OSD::Binary(value) => { self.push(b'b')?; self.write_sized(value) } OSD::Date(value) => { self.push(b'd')?; self.extend(&crate::model::unix_seconds_for_codec(*value).to_le_bytes()) } OSD::Uri(Uri(value)) => { self.push(b'l')?; self.write_sized(value.as_bytes()) } OSD::Array(values) => { self.push(b'[')?; self.write_length(values.len())?; for value in values { self.write_value(value, depth + 1)?; } self.push(b']') } OSD::Map(values) => { self.push(b'{')?; self.write_length(values.len())?; let mut entries: Vec<_> = values.iter().collect(); entries.sort_unstable_by_key(|(key, _)| *key); for (key, value) in entries { self.push(b'k')?; self.write_sized(key.as_bytes())?; self.write_value(value, depth + 1)?; } self.push(b'}') } OSD::LlsdXml(_) => Err(parse_error( self.bytes.len(), "LLSD XML fragments have no Binary LLSD marker", )), } } fn write_sized(&mut self, value: &[u8]) -> Result<(), Error> { self.write_length(value.len())?; self.extend(value) } fn write_length(&mut self, length: usize) -> Result<(), Error> { let length = i32::try_from(length).map_err(|_| Error::Argument)?; self.extend(&length.to_be_bytes()) } fn push(&mut self, value: u8) -> Result<(), Error> { if self.bytes.len() >= MAX_INPUT_BYTES { return Err(Error::Argument); } self.bytes.push(value); Ok(()) } fn extend(&mut self, value: &[u8]) -> Result<(), Error> { let length = self .bytes .len() .checked_add(value.len()) .ok_or(Error::Argument)?; if length > MAX_INPUT_BYTES { return Err(Error::Argument); } self.bytes.extend_from_slice(value); Ok(()) } } fn system_time_from_seconds(seconds: f64) -> Option { if !seconds.is_finite() { return None; } if seconds >= 0.0 { UNIX_EPOCH.checked_add(Duration::try_from_secs_f64(seconds).ok()?) } else { UNIX_EPOCH.checked_sub(Duration::try_from_secs_f64(-seconds).ok()?) } } const fn parse_error(position: usize, context: &'static str) -> Error { Error::Parse { position, context } } #[cfg(test)] mod tests { use super::*; #[test] fn every_variant_round_trips_with_header_and_nested_values() { let uuid = UUID::new_with_string("97f4aeca-88a1-42a1-b385-b97b18abb255".into()).unwrap(); let value = OSD::Array(vec![ OSD::Undefined, OSD::Boolean(true), OSD::Integer(-42), OSD::Real(12.5), OSD::String("text 𐄷".into()), OSD::UUID(uuid), OSD::Date(UNIX_EPOCH + Duration::from_millis(1_199_218_231_125)), OSD::Uri(Uri("https://example.test/a".into())), OSD::Binary(vec![0, 1, 255]), OSD::Map(HashMap::from([("nested".into(), OSD::Array(vec![]))])), ]); let encoded = serialize(value.clone()).unwrap(); assert!(encoded.starts_with(b"\n")); assert_eq!(deserialize_bytes(encoded).unwrap(), value); assert_eq!( deserialize_bytes(b" \t\r\n\r\ni\0\0\0\x07".to_vec()).unwrap(), OSD::Integer(7) ); assert!(matches!( deserialize_bytes(b"\x0b!".to_vec()), Err(Error::Parse { position: 0, context: "unknown binary LLSD type marker" }) )); let mut encoded_stream = serialize_stream_with_header(value.clone(), false).unwrap(); assert_eq!( encoded_stream.position(), encoded_stream.get_ref().len() as u64 ); encoded_stream.set_position(0); assert_eq!(deserialize_stream(Box::new(encoded_stream)).unwrap(), value); assert_eq!( host_to_network_int_bytes(-42).unwrap(), (-42_i32).to_be_bytes() ); assert_eq!( network_to_host_int((-42_i32).to_be_bytes().to_vec()).unwrap(), -42 ); assert_eq!( network_to_host_double(12.5_f64.to_be_bytes().to_vec()) .unwrap() .to_bits(), 12.5_f64.to_bits() ); } #[test] fn malformed_seed_corpus_is_rejected_with_positions() { let expected: &[(&str, &str)] = &[ ("empty", "missing binary LLSD value marker"), ("unknown_marker", "unknown binary LLSD type marker"), ("truncated_integer", "truncated binary LLSD integer"), ("truncated_real", "truncated binary LLSD real"), ("truncated_uuid", "truncated binary LLSD UUID"), ( "negative_binary_length", "invalid binary LLSD binary length", ), ( "truncated_binary_length", "invalid binary LLSD binary length", ), ( "negative_string_length", "invalid binary LLSD string length", ), ("negative_array_count", "invalid binary LLSD array count"), ("negative_map_count", "invalid binary LLSD map count"), ( "oversized_string_length", "binary LLSD text allocation limit exceeded", ), ("missing_array_end", "missing binary LLSD array end marker"), ( "missing_map_key_marker", "missing binary LLSD map key marker", ), ("missing_map_end", "missing binary LLSD map end marker"), ("non_finite_date", "invalid binary LLSD date"), ]; let corpus = include_str!("../../../fuzz/corpus/binary_llsd/malformed.hex"); let seeds: HashMap<_, _> = corpus .lines() .map(|line| { let (name, hex) = line.split_once(':').expect("named fuzz seed"); (name, decode_hex(hex)) }) .collect(); assert_eq!(seeds.len(), expected.len()); for (name, expected_context) in expected { let seed = seeds.get(name).expect("fuzz seed named by test"); let Error::Parse { position, context } = deserialize_bytes(seed.clone()).unwrap_err() else { panic!("malformed seed did not return a positional parse error"); }; assert!(position <= seed.len()); assert_eq!(context, *expected_context); } } fn decode_hex(value: &str) -> Vec { assert_eq!(value.len() % 2, 0, "fuzz seed hex must contain byte pairs"); value .as_bytes() .chunks_exact(2) .map(|pair| { let text = std::str::from_utf8(pair).expect("ASCII fuzz seed hex"); u8::from_str_radix(text, 16).expect("valid fuzz seed hex") }) .collect() } #[test] fn excessive_depth_is_rejected_before_stack_growth() { let mut bytes = Vec::new(); for _ in 0..=OSD::DEFAULT_MAX_DEPTH { bytes.extend_from_slice(b"[\0\0\0\x01"); } bytes.push(b'!'); bytes.extend(std::iter::repeat_n(b']', OSD::DEFAULT_MAX_DEPTH + 1)); assert!(matches!( deserialize_bytes(bytes), Err(Error::Parse { context: "binary LLSD nesting depth exceeded", .. }) )); } }