//! Bounded LLSD XML parsing and serialization without entity expansion. #![allow(clippy::missing_errors_doc)] #![allow(clippy::needless_pass_by_value)] #![allow(clippy::unused_self)] use crate::{Error, OSD}; use base64::Engine as _; use libremetaverse_types::UUID; use libremetaverse_types::compat::{ReadWrite, Uri}; use std::collections::HashMap; use std::io::Read as _; use std::time::UNIX_EPOCH; const MAX_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES; pub(crate) fn deserialize_bytes(data: Vec) -> Result { if data.len() > MAX_BYTES { return Err(parse_error(0, "XML LLSD input exceeds allocation limit")); } let text = std::str::from_utf8(data.strip_prefix(b"\xef\xbb\xbf").unwrap_or(&data)) .map_err(|_| parse_error(0, "XML LLSD input is not valid UTF-8"))?; deserialize_text(text) } pub(crate) fn deserialize_string(data: String) -> Result { if data.len() > MAX_BYTES { return Err(parse_error(0, "XML LLSD input exceeds allocation limit")); } deserialize_text(data.strip_prefix('\u{feff}').unwrap_or(&data)) } pub(crate) fn deserialize_stream(mut stream: Box) -> Result { let mut data = Vec::new(); (&mut *stream) .take((MAX_BYTES + 1) as u64) .read_to_end(&mut data) .map_err(|_| Error::InvalidOperation)?; deserialize_bytes(data) } pub(crate) fn deserialize_reader(reader: crate::xml::Reader) -> Result { deserialize_string(reader.0) } fn deserialize_text(text: &str) -> Result { let mut trimmed = text.trim_start(); if trimmed.starts_with("").ok_or_else(|| { parse_error( text.len() - trimmed.len(), "unterminated XML processing instruction", ) })?; trimmed = trimmed[end + 2..].trim_start(); } let first = trimmed .find('<') .ok_or_else(|| parse_error(0, "missing XML LLSD element"))?; let origin = text.len() - trimmed.len() + first; let mut parser = Parser::new(&trimmed[first..], origin); parser.skip_misc()?; let start = parser.read_start()?; let value = if local_name(&start.name) == "llsd" { if start.empty { OSD::Undefined } else { parser.skip_misc()?; if parser.at_end("llsd") { parser.read_end("llsd")?; OSD::Undefined } else { let value = parser.parse_value(0)?; parser.skip_misc()?; parser.read_end("llsd")?; value } } } else { parser.parse_started(start, 0)? }; value.validate_limits( OSD::DEFAULT_MAX_DEPTH, OSD::DEFAULT_MAX_NODES, OSD::DEFAULT_MAX_BINARY_BYTES, )?; Ok(value) } pub(crate) fn serialize_bytes(value: OSD) -> Result, Error> { Ok(serialize_string(value)?.into_bytes()) } pub(crate) fn serialize_string(value: OSD) -> Result { let inner = serialize_inner(value)?; bounded_join("", &inner, "") } pub(crate) fn serialize_inner(value: OSD) -> Result { value.validate_limits( OSD::DEFAULT_MAX_DEPTH, OSD::DEFAULT_MAX_NODES, OSD::DEFAULT_MAX_BINARY_BYTES, )?; let mut encoder = Encoder::new(); encoder.write_value(&value, 0)?; Ok(encoder.output) } pub(crate) fn serialize_element(writer: crate::xml::Writer, value: OSD) -> Result<(), Error> { writer.append(&serialize_inner(value)?) } struct StartTag { name: String, attributes: HashMap, empty: bool, position: usize, } struct Parser<'a> { input: &'a str, position: usize, origin: usize, nodes: usize, allocated: usize, } impl<'a> Parser<'a> { const fn new(input: &'a str, origin: usize) -> Self { Self { input, position: 0, origin, nodes: 0, allocated: 0, } } fn parse_value(&mut self, depth: usize) -> Result { let start = self.read_start()?; self.parse_started(start, depth) } fn parse_started(&mut self, start: StartTag, depth: usize) -> Result { if depth > OSD::DEFAULT_MAX_DEPTH { return Err(self.error("XML LLSD nesting depth exceeded")); } self.nodes = self .nodes .checked_add(1) .ok_or_else(|| self.error("XML LLSD node overflow"))?; if self.nodes > OSD::DEFAULT_MAX_NODES { return Err(self.error("XML LLSD node limit exceeded")); } let name = local_name(&start.name); match name { "map" => self.parse_map(start, depth), "array" => self.parse_array(start, depth), "undef" => { self.finish_empty_or_text(&start, false)?; Ok(OSD::Undefined) } "boolean" => { let text = self.finish_empty_or_text(&start, true)?; Ok(OSD::Boolean(matches!(text.trim(), "1" | "true"))) } "integer" => { let text = self.finish_empty_or_text(&start, true)?; Ok(OSD::Integer(text.trim().parse().unwrap_or(0))) } "real" => { let text = self.finish_empty_or_text(&start, true)?; let value = if text.trim().eq_ignore_ascii_case("nan") { f64::NAN } else { text.trim().parse().unwrap_or(0.0) }; Ok(OSD::Real(value)) } "string" => Ok(OSD::String(self.finish_empty_or_text(&start, true)?)), "uuid" => { let text = self.finish_empty_or_text(&start, true)?; Ok(OSD::UUID( UUID::new_with_string(text.trim().into()).unwrap_or_else(|_| UUID::zero()), )) } "date" => { let text = self.finish_empty_or_text(&start, true)?; Ok(OSD::Date( crate::model::parse_system_time_for_codec(text.trim()).unwrap_or(UNIX_EPOCH), )) } "uri" => Ok(OSD::Uri(Uri(self.finish_empty_or_text(&start, true)?))), "binary" => { if !start.empty && start .attributes .get("encoding") .is_some_and(|value| value != "base64") { return Err( self.error_at(start.position, "unsupported XML LLSD binary encoding") ); } let text = self.finish_empty_or_text(&start, true)?; let compact: Vec<_> = text .bytes() .filter(|byte| !byte.is_ascii_whitespace()) .collect(); let bytes = base64::engine::general_purpose::STANDARD .decode(compact) .map_err(|_| self.error_at(start.position, "invalid XML LLSD base64 value"))?; self.add_allocation(bytes.len(), "XML LLSD binary allocation limit exceeded")?; Ok(OSD::Binary(bytes)) } _ => Err(self.error_at(start.position, "unknown XML LLSD element")), } } fn parse_array(&mut self, start: StartTag, depth: usize) -> Result { if start.empty { return Ok(OSD::Array(Vec::new())); } let mut values = Vec::new(); loop { self.skip_misc()?; if self.at_end("array") { self.read_end("array")?; break; } values.push(self.parse_value(depth + 1)?); self.add_allocation( std::mem::size_of::(), "XML LLSD array allocation limit exceeded", )?; if values.len() > OSD::DEFAULT_MAX_NODES { return Err(self.error("XML LLSD array node limit exceeded")); } } Ok(OSD::Array(values)) } fn parse_map(&mut self, start: StartTag, depth: usize) -> Result { if start.empty { return Ok(OSD::Map(HashMap::new())); } let mut values = HashMap::new(); loop { self.skip_misc()?; if self.at_end("map") { self.read_end("map")?; break; } let key = self.read_start()?; if local_name(&key.name) != "key" { return Err(self.error_at(key.position, "expected XML LLSD map key")); } let key = self.finish_empty_or_text(&key, true)?; self.skip_misc()?; let value = self.parse_value(depth + 1)?; self.add_allocation( std::mem::size_of::<(String, OSD)>(), "XML LLSD map allocation limit exceeded", )?; values.insert(key, value); if values.len() > OSD::DEFAULT_MAX_NODES { return Err(self.error("XML LLSD map node limit exceeded")); } } Ok(OSD::Map(values)) } fn finish_empty_or_text( &mut self, start: &StartTag, allow_text: bool, ) -> Result { if start.empty { return Ok(String::new()); } let text = if allow_text { self.read_text(&start.name)? } else { self.skip_misc()?; String::new() }; self.read_end(&start.name)?; self.add_allocation(text.len(), "XML LLSD text allocation limit exceeded")?; Ok(text) } fn read_text(&mut self, name: &str) -> Result { let mut output = String::new(); loop { if self.at_end(name) { break; } if self.rest().is_empty() { return Err(self.error("unterminated XML LLSD scalar")); } if self.rest().starts_with("") .ok_or_else(|| self.error("unterminated XML comment"))?; self.position += end + 3; Ok(()) } fn skip_ascii_whitespace(&mut self) { while self .rest() .as_bytes() .first() .is_some_and(u8::is_ascii_whitespace) { self.position += 1; } } fn read_name(&mut self) -> Result { let length = self .rest() .bytes() .take_while(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b':' | b'.') }) .count(); if length == 0 { return Err(self.error("invalid XML name")); } let name = self.rest()[..length].to_owned(); self.position += length; Ok(name) } fn expect(&mut self, text: &str, context: &'static str) -> Result<(), Error> { if self.rest().starts_with(text) { self.position += text.len(); Ok(()) } else { Err(self.error(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 > MAX_BYTES { Err(self.error(context)) } else { Ok(()) } } fn rest(&self) -> &'a str { &self.input[self.position..] } const fn absolute_position(&self) -> usize { self.origin + self.position } const fn error(&self, context: &'static str) -> Error { self.error_at(self.absolute_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 write_value(&mut self, value: &OSD, depth: usize) -> Result<(), Error> { if depth > OSD::DEFAULT_MAX_DEPTH { return Err(parse_error( self.output.len(), "XML LLSD nesting depth exceeded", )); } match value { OSD::Undefined => self.push(""), OSD::Boolean(value) => self.scalar("boolean", if *value { "1" } else { "0" }), OSD::Integer(value) => self.scalar("integer", &value.to_string()), OSD::Real(value) => self.scalar("real", &crate::model::format_real_for_codec(*value)), OSD::String(value) => self.scalar("string", value), OSD::UUID(value) => self.scalar("uuid", &value.to_string()), OSD::Date(value) => { self.scalar("date", &crate::model::format_system_time_for_codec(*value)) } OSD::Uri(Uri(value)) => self.scalar("uri", &crate::model::format_uri_for_codec(value)), OSD::Binary(value) => self.scalar_with_attribute( "binary", "encoding", "base64", &base64::engine::general_purpose::STANDARD.encode(value), ), OSD::Array(values) => { self.push("")?; for value in values { self.write_value(value, depth + 1)?; } self.push("") } OSD::Map(values) => { self.push("")?; let mut entries: Vec<_> = values.iter().collect(); entries.sort_unstable_by_key(|(key, _)| *key); for (key, value) in entries { self.scalar("key", key)?; self.write_value(value, depth + 1)?; } self.push("") } OSD::LlsdXml(value) => self.push(value), } } fn scalar(&mut self, name: &str, value: &str) -> Result<(), Error> { self.push(&format!("<{name}>"))?; self.push_escaped(value)?; self.push(&format!("")) } fn scalar_with_attribute( &mut self, name: &str, attribute: &str, attribute_value: &str, value: &str, ) -> Result<(), Error> { self.push(&format!("<{name} {attribute}=\"{attribute_value}\">"))?; self.push(value)?; self.push(&format!("")) } fn push(&mut self, value: &str) -> Result<(), Error> { let length = self .output .len() .checked_add(value.len()) .ok_or(Error::Argument)?; if length > MAX_BYTES { return Err(Error::Argument); } self.output.push_str(value); Ok(()) } fn push_escaped(&mut self, value: &str) -> Result<(), Error> { let mut start = 0; for (index, character) in value.char_indices() { let replacement = match character { '&' => "&", '<' => "<", '>' => ">", _ => continue, }; self.push(&value[start..index])?; self.push(replacement)?; start = index + character.len_utf8(); } self.push(&value[start..]) } } fn decode_entities(value: &str, origin: usize) -> Result { let mut output = String::with_capacity(value.len()); let mut remaining = value; while let Some(index) = remaining.find('&') { output.push_str(&remaining[..index]); let entity_start = origin + value.len() - remaining.len() + index; remaining = &remaining[index + 1..]; let end = remaining .find(';') .ok_or_else(|| parse_error(entity_start, "unterminated XML entity"))?; let entity = &remaining[..end]; let character = match entity { "amp" => '&', "lt" => '<', "gt" => '>', "apos" => '\'', "quot" => '"', entity if entity.starts_with("#x") => char::from_u32( u32::from_str_radix(&entity[2..], 16) .map_err(|_| parse_error(entity_start, "invalid XML character reference"))?, ) .ok_or_else(|| parse_error(entity_start, "invalid XML character reference"))?, entity if entity.starts_with('#') => char::from_u32( entity[1..] .parse() .map_err(|_| parse_error(entity_start, "invalid XML character reference"))?, ) .ok_or_else(|| parse_error(entity_start, "invalid XML character reference"))?, _ => { return Err(parse_error( entity_start, "XML entity expansion is disabled", )); } }; output.push(character); remaining = &remaining[end + 1..]; } output.push_str(remaining); Ok(output) } fn local_name(name: &str) -> &str { name.rsplit(':').next().unwrap_or(name) } fn bounded_join(prefix: &str, value: &str, suffix: &str) -> Result { let length = prefix .len() .checked_add(value.len()) .and_then(|length| length.checked_add(suffix.len())) .ok_or(Error::Argument)?; if length > MAX_BYTES { return Err(Error::Argument); } Ok([prefix, value, suffix].concat()) } 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_and_entities_are_exact() { let value = OSD::Map(HashMap::from([ ("text<&".into(), OSD::String("< > & ' \"".into())), ( "nested".into(), OSD::Array(vec![OSD::Boolean(true), OSD::Binary(vec![0, 255])]), ), ])); let xml = serialize_string(value.clone()).unwrap(); assert_eq!(deserialize_string(xml).unwrap(), value); assert_eq!( deserialize_string("𐄷 &".into()).unwrap(), OSD::String("𐄷 &".into()) ); } #[test] fn empty_binary_ignores_encoding_like_the_reference_reader() { assert_eq!( deserialize_string("".into()).unwrap(), OSD::Binary(Vec::new()) ); } #[test] fn every_serialization_entry_point_is_compact_and_exact() { let value = OSD::Map(HashMap::from([ ("z".into(), OSD::Undefined), ("a<&".into(), OSD::Binary(vec![0, 255])), ])); let inner = "a<&AP8=z"; assert_eq!(serialize_inner(value.clone()).unwrap(), inner); assert_eq!( serialize_string(value.clone()).unwrap(), format!("{inner}") ); assert_eq!( serialize_bytes(value.clone()).unwrap(), format!("{inner}").into_bytes() ); let writer = crate::xml::Writer::default(); serialize_element(writer.clone(), value).unwrap(); assert_eq!(writer.contents(), inner); } #[test] fn reader_stream_comments_and_cdata_match_xml_behavior() { let input = "]]>&"; assert_eq!( deserialize_reader(crate::xml::Reader(input.into())).unwrap(), OSD::String("&".into()) ); let stream: Box = Box::new(std::io::Cursor::new(input.as_bytes().to_vec())); assert_eq!( deserialize_stream(stream).unwrap(), OSD::String("&".into()) ); } #[test] fn hostile_entities_and_malformed_nesting_are_rejected() { for input in [ include_str!("../../../fuzz/corpus/xml_llsd/doctype_entity.xml"), include_str!("../../../fuzz/corpus/xml_llsd/external_entity.xml"), include_str!("../../../fuzz/corpus/xml_llsd/unknown_entity.xml"), "1", "00", ] { assert!(matches!( deserialize_string(input.into()), Err(Error::Parse { .. }) )); } match deserialize_string("1".into()) { Err(Error::Parse { position, context }) => { assert!(position > 6); assert_eq!(context, "nested element in XML LLSD scalar"); } result => panic!("expected positioned XML parse error, got {result:?}"), } let mut too_deep = "".to_owned(); too_deep.push_str(&"".repeat(OSD::DEFAULT_MAX_DEPTH + 2)); too_deep.push_str(""); too_deep.push_str(&"".repeat(OSD::DEFAULT_MAX_DEPTH + 2)); too_deep.push_str(""); assert!(matches!( deserialize_string(too_deep), Err(Error::Parse { context: "XML LLSD nesting depth exceeded", .. }) )); } }