Implement bounded XML LLSD codec
This commit is contained in:
@@ -160,15 +160,15 @@ mod tests {
|
||||
deserialize_bytes(b"<? llsd/binary ?>i\0\0\0\0".to_vec()),
|
||||
Ok(OSD::Integer(0))
|
||||
);
|
||||
assert_eq!(
|
||||
deserialize_bytes(b"<llsd><undef /></llsd>".to_vec()),
|
||||
Ok(OSD::Undefined)
|
||||
);
|
||||
let cases: &[(&[u8], &str)] = &[
|
||||
(
|
||||
b"<? llsd/protobuf ?>",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])",
|
||||
),
|
||||
(
|
||||
b"<llsd><undef /></llsd>",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Byte[])",
|
||||
),
|
||||
(
|
||||
b"{}",
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeJson(System.String)",
|
||||
|
||||
@@ -329,33 +329,25 @@ impl OSDParser {
|
||||
pub fn deserialize_llsd_xml_with_bytes(
|
||||
xml_data: Vec<u8>,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Byte[])",
|
||||
)
|
||||
crate::xml_codec::deserialize_bytes(xml_data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.IO.Stream)`.
|
||||
pub fn deserialize_llsd_xml_with_stream(
|
||||
xml_stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.IO.Stream)",
|
||||
)
|
||||
crate::xml_codec::deserialize_stream(xml_stream)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.String)`.
|
||||
pub fn deserialize_llsd_xml_with_string(
|
||||
xml_data: String,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.String)",
|
||||
)
|
||||
crate::xml_codec::deserialize_string(xml_data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Xml.XmlReader)`.
|
||||
pub fn deserialize_llsd_xml_with_xml_reader(
|
||||
xml_data: libremetaverse_structured_data::xml::Reader,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Xml.XmlReader)",
|
||||
)
|
||||
crate::xml_codec::deserialize_reader(xml_data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)`.
|
||||
pub fn escape_character(
|
||||
@@ -454,9 +446,7 @@ impl OSDParser {
|
||||
pub fn serialize_llsd_inner_xml_string(
|
||||
data: libremetaverse_structured_data::OSD,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDInnerXmlString(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::xml_codec::serialize_inner(data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_notation(
|
||||
@@ -495,26 +485,20 @@ impl OSDParser {
|
||||
pub fn serialize_llsd_xml_bytes(
|
||||
data: libremetaverse_structured_data::OSD,
|
||||
) -> Result<Vec<u8>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlBytes(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::xml_codec::serialize_bytes(data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlElement(System.Xml.XmlWriter,LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_xml_element(
|
||||
writer: libremetaverse_structured_data::xml::Writer,
|
||||
data: libremetaverse_structured_data::OSD,
|
||||
) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlElement(System.Xml.XmlWriter,LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::xml_codec::serialize_element(writer, data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlString(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_xml_string(
|
||||
data: libremetaverse_structured_data::OSD,
|
||||
) -> Result<String, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlString(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::xml_codec::serialize_string(data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)`.
|
||||
pub fn skip_white_space(
|
||||
|
||||
@@ -7,16 +7,44 @@ mod dispatch;
|
||||
mod generated;
|
||||
mod model;
|
||||
mod notation;
|
||||
mod xml_codec;
|
||||
|
||||
pub mod xml {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Project-owned XML element boundary; external XML APIs are not copied.
|
||||
pub struct Element;
|
||||
|
||||
/// Native XML reader boundary; external XML APIs are not copied.
|
||||
pub struct Reader;
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Reader(pub String);
|
||||
|
||||
/// Native XML writer boundary; external XML APIs are not copied.
|
||||
pub struct Writer;
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Writer(pub Arc<Mutex<String>>);
|
||||
|
||||
impl Writer {
|
||||
#[must_use]
|
||||
pub fn contents(&self) -> String {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn append(&self, value: &str) -> Result<(), crate::Error> {
|
||||
let mut output = self.0.lock().map_err(|_| crate::Error::InvalidOperation)?;
|
||||
let length = output
|
||||
.len()
|
||||
.checked_add(value.len())
|
||||
.ok_or(crate::Error::Argument)?;
|
||||
if length > crate::OSD::DEFAULT_MAX_BINARY_BYTES {
|
||||
return Err(crate::Error::Argument);
|
||||
}
|
||||
output.push_str(value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use generated::*;
|
||||
|
||||
@@ -1556,7 +1556,7 @@ 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 {
|
||||
pub(crate) fn format_uri_for_codec(value: &str) -> String {
|
||||
if !value.contains("://") {
|
||||
return value.to_owned();
|
||||
}
|
||||
|
||||
759
crates/libremetaverse-structured-data/src/xml_codec.rs
Normal file
759
crates/libremetaverse-structured-data/src/xml_codec.rs
Normal file
@@ -0,0 +1,759 @@
|
||||
//! 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<u8>) -> Result<OSD, Error> {
|
||||
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<OSD, Error> {
|
||||
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<dyn ReadWrite + Send>) -> Result<OSD, Error> {
|
||||
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<OSD, Error> {
|
||||
deserialize_string(reader.0)
|
||||
}
|
||||
|
||||
fn deserialize_text(text: &str) -> Result<OSD, Error> {
|
||||
let mut trimmed = text.trim_start();
|
||||
if trimmed.starts_with("<?") {
|
||||
let end = trimmed.find("?>").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<Vec<u8>, Error> {
|
||||
Ok(serialize_string(value)?.into_bytes())
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_string(value: OSD) -> Result<String, Error> {
|
||||
let inner = serialize_inner(value)?;
|
||||
bounded_join("<llsd>", &inner, "</llsd>")
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_inner(value: OSD) -> Result<String, Error> {
|
||||
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<String, String>,
|
||||
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<OSD, Error> {
|
||||
let start = self.read_start()?;
|
||||
self.parse_started(start, depth)
|
||||
}
|
||||
|
||||
fn parse_started(&mut self, start: StartTag, depth: usize) -> Result<OSD, Error> {
|
||||
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<OSD, Error> {
|
||||
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::<OSD>(),
|
||||
"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<OSD, Error> {
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
let mut output = String::new();
|
||||
loop {
|
||||
if self.at_end(name) {
|
||||
break;
|
||||
}
|
||||
if self.rest().starts_with("<!--") {
|
||||
self.skip_comment()?;
|
||||
continue;
|
||||
}
|
||||
if self.rest().starts_with("<?") {
|
||||
self.skip_pi()?;
|
||||
continue;
|
||||
}
|
||||
if self.rest().starts_with("<![CDATA[") {
|
||||
self.position += 9;
|
||||
let end = self
|
||||
.rest()
|
||||
.find("]]>")
|
||||
.ok_or_else(|| self.error("unterminated XML CDATA"))?;
|
||||
output.push_str(&self.rest()[..end]);
|
||||
self.position += end + 3;
|
||||
continue;
|
||||
}
|
||||
if self.rest().starts_with('<') {
|
||||
return Err(self.error("nested element in XML LLSD scalar"));
|
||||
}
|
||||
let end = self.rest().find('<').unwrap_or(self.rest().len());
|
||||
output.push_str(&decode_entities(
|
||||
&self.rest()[..end],
|
||||
self.absolute_position(),
|
||||
)?);
|
||||
self.position += end;
|
||||
if output.len() > MAX_BYTES {
|
||||
return Err(self.error("XML LLSD text allocation limit exceeded"));
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn read_start(&mut self) -> Result<StartTag, Error> {
|
||||
self.skip_misc()?;
|
||||
let position = self.absolute_position();
|
||||
self.expect("<", "expected XML start element")?;
|
||||
if self.rest().starts_with('/')
|
||||
|| self.rest().starts_with('!')
|
||||
|| self.rest().starts_with('?')
|
||||
{
|
||||
return Err(self.error_at(position, "expected XML start element"));
|
||||
}
|
||||
let name = self.read_name()?;
|
||||
let mut attributes = HashMap::new();
|
||||
loop {
|
||||
self.skip_ascii_whitespace();
|
||||
if self.rest().starts_with("/>") {
|
||||
self.position += 2;
|
||||
return Ok(StartTag {
|
||||
name,
|
||||
attributes,
|
||||
empty: true,
|
||||
position,
|
||||
});
|
||||
}
|
||||
if self.rest().starts_with('>') {
|
||||
self.position += 1;
|
||||
return Ok(StartTag {
|
||||
name,
|
||||
attributes,
|
||||
empty: false,
|
||||
position,
|
||||
});
|
||||
}
|
||||
let attribute = self.read_name()?;
|
||||
self.skip_ascii_whitespace();
|
||||
self.expect("=", "expected XML attribute equals")?;
|
||||
self.skip_ascii_whitespace();
|
||||
let quote = self
|
||||
.rest()
|
||||
.as_bytes()
|
||||
.first()
|
||||
.copied()
|
||||
.filter(|byte| matches!(byte, b'\'' | b'"'))
|
||||
.ok_or_else(|| self.error("expected quoted XML attribute"))?;
|
||||
self.position += 1;
|
||||
let end = self
|
||||
.rest()
|
||||
.find(char::from(quote))
|
||||
.ok_or_else(|| self.error("unterminated XML attribute"))?;
|
||||
let value = decode_entities(&self.rest()[..end], self.absolute_position())?;
|
||||
self.position += end + 1;
|
||||
attributes.insert(local_name(&attribute).to_owned(), value);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_end(&mut self, expected: &str) -> Result<(), Error> {
|
||||
self.expect("</", "expected XML end element")?;
|
||||
let name = self.read_name()?;
|
||||
self.skip_ascii_whitespace();
|
||||
self.expect(">", "unterminated XML end element")?;
|
||||
if local_name(&name) != local_name(expected) {
|
||||
return Err(self.error("mismatched XML LLSD end element"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn at_end(&self, name: &str) -> bool {
|
||||
self.rest()
|
||||
.strip_prefix("</")
|
||||
.and_then(|rest| {
|
||||
rest.split(|c: char| c == '>' || c.is_ascii_whitespace())
|
||||
.next()
|
||||
})
|
||||
.is_some_and(|found| local_name(found) == local_name(name))
|
||||
}
|
||||
|
||||
fn skip_misc(&mut self) -> Result<(), Error> {
|
||||
loop {
|
||||
self.skip_ascii_whitespace();
|
||||
if self.rest().starts_with("<?") {
|
||||
self.skip_pi()?;
|
||||
} else if self.rest().starts_with("<!--") {
|
||||
self.skip_comment()?;
|
||||
} else if self
|
||||
.rest()
|
||||
.get(..9)
|
||||
.is_some_and(|value| value.eq_ignore_ascii_case("<!DOCTYPE"))
|
||||
{
|
||||
return Err(self.error("XML DTD and entity declarations are disabled"));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn skip_pi(&mut self) -> Result<(), Error> {
|
||||
let end = self
|
||||
.rest()
|
||||
.find("?>")
|
||||
.ok_or_else(|| self.error("unterminated XML processing instruction"))?;
|
||||
self.position += end + 2;
|
||||
Ok(())
|
||||
}
|
||||
fn skip_comment(&mut self) -> Result<(), Error> {
|
||||
let end = self
|
||||
.rest()
|
||||
.find("-->")
|
||||
.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<String, Error> {
|
||||
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("<undef />"),
|
||||
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("<array>")?;
|
||||
for value in values {
|
||||
self.write_value(value, depth + 1)?;
|
||||
}
|
||||
self.push("</array>")
|
||||
}
|
||||
OSD::Map(values) => {
|
||||
self.push("<map>")?;
|
||||
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("</map>")
|
||||
}
|
||||
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!("</{name}>"))
|
||||
}
|
||||
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!("</{name}>"))
|
||||
}
|
||||
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<String, Error> {
|
||||
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<String, Error> {
|
||||
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("<llsd><string>𐄷 &</string></llsd>".into()).unwrap(),
|
||||
OSD::String("𐄷 &".into())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn empty_binary_ignores_encoding_like_the_reference_reader() {
|
||||
assert_eq!(
|
||||
deserialize_string("<llsd><binary encoding='legacy' /></llsd>".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 = "<map><key>a<&</key><binary encoding=\"base64\">AP8=</binary><key>z</key><undef /></map>";
|
||||
assert_eq!(serialize_inner(value.clone()).unwrap(), inner);
|
||||
assert_eq!(
|
||||
serialize_string(value.clone()).unwrap(),
|
||||
format!("<llsd>{inner}</llsd>")
|
||||
);
|
||||
assert_eq!(
|
||||
serialize_bytes(value.clone()).unwrap(),
|
||||
format!("<llsd>{inner}</llsd>").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 = "<?xml version='1.0'?><llsd><!-- ignored --><string><![CDATA[<raw>]]>&</string></llsd>";
|
||||
assert_eq!(
|
||||
deserialize_reader(crate::xml::Reader(input.into())).unwrap(),
|
||||
OSD::String("<raw>&".into())
|
||||
);
|
||||
let stream: Box<dyn ReadWrite + Send> =
|
||||
Box::new(std::io::Cursor::new(input.as_bytes().to_vec()));
|
||||
assert_eq!(
|
||||
deserialize_stream(stream).unwrap(),
|
||||
OSD::String("<raw>&".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"),
|
||||
"<llsd><array><integer>1</array></llsd>",
|
||||
"<llsd><binary encoding='base16'>00</binary></llsd>",
|
||||
] {
|
||||
assert!(matches!(
|
||||
deserialize_string(input.into()),
|
||||
Err(Error::Parse { .. })
|
||||
));
|
||||
}
|
||||
|
||||
match deserialize_string("<llsd><integer>1</real></llsd>".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 = "<llsd>".to_owned();
|
||||
too_deep.push_str(&"<array>".repeat(OSD::DEFAULT_MAX_DEPTH + 2));
|
||||
too_deep.push_str("<undef />");
|
||||
too_deep.push_str(&"</array>".repeat(OSD::DEFAULT_MAX_DEPTH + 2));
|
||||
too_deep.push_str("</llsd>");
|
||||
assert!(matches!(
|
||||
deserialize_string(too_deep),
|
||||
Err(Error::Parse {
|
||||
context: "XML LLSD nesting depth exceeded",
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user