Implement bounded XML LLSD codec
This commit is contained in:
@@ -90,5 +90,12 @@ reference escape rules and accepted scalar spellings, base64, base16 and
|
|||||||
length-prefixed binary forms, and both compact and formatted output. Malformed
|
length-prefixed binary forms, and both compact and formatted output. Malformed
|
||||||
notation reports UTF-16 offsets with parser context, while input, output,
|
notation reports UTF-16 offsets with parser context, while input, output,
|
||||||
nesting, node, and aggregate allocation limits apply to untrusted text.
|
nesting, node, and aggregate allocation limits apply to untrusted text.
|
||||||
|
The native XML LLSD codec covers the wrapper and inner-element APIs, all scalar
|
||||||
|
and container elements, namespace-local names, XML declarations, comments,
|
||||||
|
CDATA, entity decoding, and the reference's nonstandard Linden processing
|
||||||
|
instruction handling. Serialization is compact and deterministic for maps.
|
||||||
|
DTD declarations and named entity expansion are disabled; input, output,
|
||||||
|
nesting, node, and aggregate allocation limits are enforced with byte-positioned
|
||||||
|
parse errors.
|
||||||
The controlled audit aggregates every expected failure by standardized C#
|
The controlled audit aggregates every expected failure by standardized C#
|
||||||
member ID and rejects unrelated fixture, assertion, compile, or symbol errors.
|
member ID and rejects unrelated fixture, assertion, compile, or symbol errors.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
|
|||||||
| `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim |
|
| `LibreMetaverse.RLV` | 28 | 499 | callable failure-only shim |
|
||||||
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
|
| `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | callable failure-only shim |
|
||||||
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim |
|
| `LibreMetaverse.Rendering.Simple` | 1 | 6 | callable failure-only shim |
|
||||||
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 15 types / 281 members; remaining surface is callable failure-only shims |
|
| `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 15 types / 289 members; remaining surface is callable failure-only shims |
|
||||||
| `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |
|
| `LibreMetaverse.Types` | 45 | 942 | native implementation: 45 types / 942 members; no generated shims remain |
|
||||||
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |
|
| `LibreMetaverse.Utilities` | 3 | 13 | callable failure-only shim |
|
||||||
| `LibreMetaverse.Voice.Vivox` | 64 | 531 | callable failure-only shim |
|
| `LibreMetaverse.Voice.Vivox` | 64 | 531 | callable failure-only shim |
|
||||||
|
|||||||
@@ -160,15 +160,15 @@ mod tests {
|
|||||||
deserialize_bytes(b"<? llsd/binary ?>i\0\0\0\0".to_vec()),
|
deserialize_bytes(b"<? llsd/binary ?>i\0\0\0\0".to_vec()),
|
||||||
Ok(OSD::Integer(0))
|
Ok(OSD::Integer(0))
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
deserialize_bytes(b"<llsd><undef /></llsd>".to_vec()),
|
||||||
|
Ok(OSD::Undefined)
|
||||||
|
);
|
||||||
let cases: &[(&[u8], &str)] = &[
|
let cases: &[(&[u8], &str)] = &[
|
||||||
(
|
(
|
||||||
b"<? llsd/protobuf ?>",
|
b"<? llsd/protobuf ?>",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])",
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDProtobuf(System.Byte[])",
|
||||||
),
|
),
|
||||||
(
|
|
||||||
b"<llsd><undef /></llsd>",
|
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Byte[])",
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
b"{}",
|
b"{}",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeJson(System.String)",
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeJson(System.String)",
|
||||||
|
|||||||
@@ -329,33 +329,25 @@ impl OSDParser {
|
|||||||
pub fn deserialize_llsd_xml_with_bytes(
|
pub fn deserialize_llsd_xml_with_bytes(
|
||||||
xml_data: Vec<u8>,
|
xml_data: Vec<u8>,
|
||||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::deserialize_bytes(xml_data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Byte[])",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.IO.Stream)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.IO.Stream)`.
|
||||||
pub fn deserialize_llsd_xml_with_stream(
|
pub fn deserialize_llsd_xml_with_stream(
|
||||||
xml_stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
xml_stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::deserialize_stream(xml_stream)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.IO.Stream)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.String)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.String)`.
|
||||||
pub fn deserialize_llsd_xml_with_string(
|
pub fn deserialize_llsd_xml_with_string(
|
||||||
xml_data: String,
|
xml_data: String,
|
||||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::deserialize_string(xml_data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.String)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Xml.XmlReader)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Xml.XmlReader)`.
|
||||||
pub fn deserialize_llsd_xml_with_xml_reader(
|
pub fn deserialize_llsd_xml_with_xml_reader(
|
||||||
xml_data: libremetaverse_structured_data::xml::Reader,
|
xml_data: libremetaverse_structured_data::xml::Reader,
|
||||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::deserialize_reader(xml_data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Xml.XmlReader)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)`.
|
||||||
pub fn escape_character(
|
pub fn escape_character(
|
||||||
@@ -454,9 +446,7 @@ impl OSDParser {
|
|||||||
pub fn serialize_llsd_inner_xml_string(
|
pub fn serialize_llsd_inner_xml_string(
|
||||||
data: libremetaverse_structured_data::OSD,
|
data: libremetaverse_structured_data::OSD,
|
||||||
) -> Result<String, crate::Error> {
|
) -> Result<String, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::serialize_inner(data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDInnerXmlString(LibreMetaverse.StructuredData.OSD)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)`.
|
||||||
pub fn serialize_llsd_notation(
|
pub fn serialize_llsd_notation(
|
||||||
@@ -495,26 +485,20 @@ impl OSDParser {
|
|||||||
pub fn serialize_llsd_xml_bytes(
|
pub fn serialize_llsd_xml_bytes(
|
||||||
data: libremetaverse_structured_data::OSD,
|
data: libremetaverse_structured_data::OSD,
|
||||||
) -> Result<Vec<u8>, crate::Error> {
|
) -> Result<Vec<u8>, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::serialize_bytes(data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlBytes(LibreMetaverse.StructuredData.OSD)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlElement(System.Xml.XmlWriter,LibreMetaverse.StructuredData.OSD)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlElement(System.Xml.XmlWriter,LibreMetaverse.StructuredData.OSD)`.
|
||||||
pub fn serialize_llsd_xml_element(
|
pub fn serialize_llsd_xml_element(
|
||||||
writer: libremetaverse_structured_data::xml::Writer,
|
writer: libremetaverse_structured_data::xml::Writer,
|
||||||
data: libremetaverse_structured_data::OSD,
|
data: libremetaverse_structured_data::OSD,
|
||||||
) -> Result<(), crate::Error> {
|
) -> Result<(), crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::serialize_element(writer, data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlElement(System.Xml.XmlWriter,LibreMetaverse.StructuredData.OSD)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlString(LibreMetaverse.StructuredData.OSD)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlString(LibreMetaverse.StructuredData.OSD)`.
|
||||||
pub fn serialize_llsd_xml_string(
|
pub fn serialize_llsd_xml_string(
|
||||||
data: libremetaverse_structured_data::OSD,
|
data: libremetaverse_structured_data::OSD,
|
||||||
) -> Result<String, crate::Error> {
|
) -> Result<String, crate::Error> {
|
||||||
libremetaverse_types::not_implemented(
|
crate::xml_codec::serialize_string(data)
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlString(LibreMetaverse.StructuredData.OSD)",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)`.
|
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)`.
|
||||||
pub fn skip_white_space(
|
pub fn skip_white_space(
|
||||||
|
|||||||
@@ -7,16 +7,44 @@ mod dispatch;
|
|||||||
mod generated;
|
mod generated;
|
||||||
mod model;
|
mod model;
|
||||||
mod notation;
|
mod notation;
|
||||||
|
mod xml_codec;
|
||||||
|
|
||||||
pub mod xml {
|
pub mod xml {
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
/// Project-owned XML element boundary; external XML APIs are not copied.
|
/// Project-owned XML element boundary; external XML APIs are not copied.
|
||||||
pub struct Element;
|
pub struct Element;
|
||||||
|
|
||||||
/// Native XML reader boundary; external XML APIs are not copied.
|
/// 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.
|
/// 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::*;
|
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)
|
!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("://") {
|
if !value.contains("://") {
|
||||||
return value.to_owned();
|
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",
|
||||||
|
..
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
6
fuzz/corpus/xml_llsd/README.md
Normal file
6
fuzz/corpus/xml_llsd/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# XML LLSD security regression corpus
|
||||||
|
|
||||||
|
These inputs exercise the XML LLSD parser's entity boundary. DTD declarations,
|
||||||
|
external entities, and undeclared named entities must be rejected without file
|
||||||
|
or network access and without entity expansion. The structured-data unit tests
|
||||||
|
load every fixture directly.
|
||||||
1
fuzz/corpus/xml_llsd/doctype_entity.xml
Normal file
1
fuzz/corpus/xml_llsd/doctype_entity.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE llsd [<!ENTITY x "expanded">]><llsd><string>&x;</string></llsd>
|
||||||
1
fuzz/corpus/xml_llsd/external_entity.xml
Normal file
1
fuzz/corpus/xml_llsd/external_entity.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE llsd [<!ENTITY x SYSTEM "file:///etc/passwd">]><llsd><string>&x;</string></llsd>
|
||||||
1
fuzz/corpus/xml_llsd/unknown_entity.xml
Normal file
1
fuzz/corpus/xml_llsd/unknown_entity.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<llsd><string>&unknown;</string></llsd>
|
||||||
@@ -69,7 +69,22 @@
|
|||||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.UUIDs::test",
|
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.UUIDs::test",
|
||||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.Vector3ApproxEquals::test",
|
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.Vector3ApproxEquals::test",
|
||||||
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.VectorCasting::test",
|
"LibreMetaverse.Tests/TypeTests.cs::TypeTests.VectorCasting::test",
|
||||||
"LibreMetaverse.Tests/UtilsConversionsTests.cs::UtilsConversionsTests.StringToBytes::test"
|
"LibreMetaverse.Tests/UtilsConversionsTests.cs::UtilsConversionsTests.StringToBytes::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeBinary::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeBoolean::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeDates::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeIntegers::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeLLSDSample::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeNestedContainers::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeNoDTD::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeReals::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeSillyPI::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeSillyPI_LowercasePI::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeSillyPI_NoWhitespaceAfterPI::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeStrings::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeURI::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
|
||||||
|
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
|
||||||
],
|
],
|
||||||
"support_passes": 79
|
"support_passes": 84
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,14 @@ NATIVE_MEMBER_BODIES = {
|
|||||||
"crate::notation::deserialize_reader(reader)",
|
"crate::notation::deserialize_reader(reader)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)":
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.String)":
|
||||||
"crate::notation::deserialize_string(notation_data)",
|
"crate::notation::deserialize_string(notation_data)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Byte[])":
|
||||||
|
"crate::xml_codec::deserialize_bytes(xml_data)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.IO.Stream)":
|
||||||
|
"crate::xml_codec::deserialize_stream(xml_stream)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.String)":
|
||||||
|
"crate::xml_codec::deserialize_string(xml_data)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDXml(System.Xml.XmlReader)":
|
||||||
|
"crate::xml_codec::deserialize_reader(xml_data)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)":
|
"M:LibreMetaverse.StructuredData.OSDParser.EscapeCharacter(System.String,System.Char)":
|
||||||
"crate::notation::escape_character(s, c)",
|
"crate::notation::escape_character(s, c)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)":
|
"M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)":
|
||||||
@@ -119,6 +127,8 @@ NATIVE_MEMBER_BODIES = {
|
|||||||
"crate::binary::serialize_stream(data)",
|
"crate::binary::serialize_stream(data)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)":
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)":
|
||||||
"crate::binary::serialize_stream_with_header(data, prepend_header)",
|
"crate::binary::serialize_stream_with_header(data, prepend_header)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDInnerXmlString(LibreMetaverse.StructuredData.OSD)":
|
||||||
|
"crate::xml_codec::serialize_inner(data)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)":
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotation(LibreMetaverse.StructuredData.OSD)":
|
||||||
"crate::notation::serialize(osd)",
|
"crate::notation::serialize(osd)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)":
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationFormatted(LibreMetaverse.StructuredData.OSD)":
|
||||||
@@ -127,6 +137,12 @@ NATIVE_MEMBER_BODIES = {
|
|||||||
"crate::notation::serialize_stream(osd)",
|
"crate::notation::serialize_stream(osd)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)":
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDNotationStreamFormatted(LibreMetaverse.StructuredData.OSD)":
|
||||||
"crate::notation::serialize_stream_formatted(osd)",
|
"crate::notation::serialize_stream_formatted(osd)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlBytes(LibreMetaverse.StructuredData.OSD)":
|
||||||
|
"crate::xml_codec::serialize_bytes(data)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlElement(System.Xml.XmlWriter,LibreMetaverse.StructuredData.OSD)":
|
||||||
|
"crate::xml_codec::serialize_element(writer, data)",
|
||||||
|
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDXmlString(LibreMetaverse.StructuredData.OSD)":
|
||||||
|
"crate::xml_codec::serialize_string(data)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)":
|
"M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)":
|
||||||
"crate::binary::skip_whitespace(stream)",
|
"crate::binary::skip_whitespace(stream)",
|
||||||
"M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)":
|
"M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)":
|
||||||
|
|||||||
@@ -983,6 +983,7 @@ def validate_generated_shims() -> None:
|
|||||||
"crate::binary::",
|
"crate::binary::",
|
||||||
"crate::dispatch::",
|
"crate::dispatch::",
|
||||||
"crate::notation::",
|
"crate::notation::",
|
||||||
|
"crate::xml_codec::",
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
raise ValueError(f"generated shim function does not use the standardized failure: {path.relative_to(ROOT)}")
|
raise ValueError(f"generated shim function does not use the standardized failure: {path.relative_to(ROOT)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user