Implement StructuredData OSD model and containers

This commit is contained in:
2026-08-09 00:10:12 +00:00
parent ae0b7966e2
commit d152e80ef9
15 changed files with 2408 additions and 1456 deletions

View File

@@ -8,6 +8,7 @@ repository.workspace = true
description = "OSD and LLSD shims for the MetaCrate LibreMetaverse rewrite"
[dependencies]
base64 = "0.22"
libremetaverse-types = { path = "../libremetaverse-types" }
[lints]

View File

@@ -0,0 +1,194 @@
//! Bounded wire-format dispatch for the public `OSDParser.Deserialize` overloads.
use crate::{Error, OSD, OSDParser};
use libremetaverse_types::compat::ReadWrite;
use std::io::Read as _;
const PROBE_LENGTH: usize = 20;
const MAX_INPUT_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES;
const BINARY_HEADER: &str = "<? llsd/binary ?>";
const PROTOBUF_HEADER: &str = "<? llsd/protobuf ?>";
const XML_HEADERS: [&str; 3] = ["<llsd>", "<?xml", "<? llsd/xml ?>"];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum WireFormat {
Binary,
Json,
Protobuf,
Xml,
}
pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
if data.len() > MAX_INPUT_BYTES {
return Err(Error::Argument);
}
let format = classify_bytes(&data).ok_or(Error::Argument)?;
let value = match format {
WireFormat::Binary => OSDParser::deserialize_llsd_binary_with_bytes(data)?,
WireFormat::Protobuf => OSDParser::deserialize_llsd_protobuf_with_bytes(data)?,
WireFormat::Xml => OSDParser::deserialize_llsd_xml_with_bytes(data)?,
WireFormat::Json => OSDParser::deserialize_json_with_string(
String::from_utf8(data).map_err(|_| Error::Argument)?,
)?,
};
validate_decoded(value)
}
pub(crate) fn deserialize_string(data: String) -> Result<OSD, Error> {
if data.len() > MAX_INPUT_BYTES {
return Err(Error::Argument);
}
let value = match classify_string(&data) {
WireFormat::Binary => OSDParser::deserialize_llsd_binary_with_bytes(data.into_bytes())?,
WireFormat::Protobuf => OSDParser::deserialize_llsd_protobuf_with_bytes(data.into_bytes())?,
WireFormat::Xml => OSDParser::deserialize_llsd_xml_with_string(data)?,
WireFormat::Json => OSDParser::deserialize_json_with_string(data)?,
};
validate_decoded(value)
}
pub(crate) fn deserialize_stream(mut stream: Box<dyn ReadWrite + Send>) -> Result<OSD, Error> {
let mut data = Vec::new();
(&mut *stream)
.take((MAX_INPUT_BYTES + 1) as u64)
.read_to_end(&mut data)
.map_err(|_| Error::Argument)?;
deserialize_bytes(data)
}
fn validate_decoded(value: OSD) -> Result<OSD, Error> {
value.validate_limits(
OSD::DEFAULT_MAX_DEPTH,
OSD::DEFAULT_MAX_NODES,
OSD::DEFAULT_MAX_BINARY_BYTES,
)?;
Ok(value)
}
fn classify_bytes(data: &[u8]) -> Option<WireFormat> {
let probe = &data[..data.len().min(PROBE_LENGTH)];
let ascii = String::from_utf8_lossy(probe);
let utf8 = ascii.trim_start_matches('\u{feff}').trim_start();
if starts_with_any_ignore_ascii_case(utf8, &XML_HEADERS) {
return Some(WireFormat::Xml);
}
if starts_with_ignore_ascii_case(&ascii, PROTOBUF_HEADER) {
return Some(WireFormat::Protobuf);
}
if starts_with_ignore_ascii_case(&ascii, BINARY_HEADER) {
return Some(WireFormat::Binary);
}
if starts_with_any_ignore_ascii_case(&ascii, &XML_HEADERS) {
return Some(WireFormat::Xml);
}
let first = utf8.as_bytes().first().copied()?;
if matches!(first, b'{' | b'[' | b'"' | b'-' | b'0'..=b'9')
|| utf8.starts_with("true")
|| utf8.starts_with("false")
|| utf8.starts_with("null")
{
Some(WireFormat::Json)
} else {
None
}
}
fn classify_string(data: &str) -> WireFormat {
if starts_with_ignore_ascii_case(data, PROTOBUF_HEADER) {
WireFormat::Protobuf
} else if starts_with_ignore_ascii_case(data, BINARY_HEADER) {
WireFormat::Binary
} else if starts_with_any_ignore_ascii_case(data, &XML_HEADERS) {
WireFormat::Xml
} else {
WireFormat::Json
}
}
fn starts_with_any_ignore_ascii_case(value: &str, prefixes: &[&str]) -> bool {
prefixes
.iter()
.any(|prefix| starts_with_ignore_ascii_case(value, prefix))
}
fn starts_with_ignore_ascii_case(value: &str, prefix: &str) -> bool {
value
.get(..prefix.len())
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn byte_dispatch_recognizes_headers_bom_whitespace_and_json_tokens() {
assert_eq!(
classify_bytes(b"<? llsd/binary ?>i"),
Some(WireFormat::Binary)
);
assert_eq!(
classify_bytes(b"<? LLSD/PROTOBUF ?>"),
Some(WireFormat::Protobuf)
);
assert_eq!(
classify_bytes(b" \n<?xml version='1.0'"),
Some(WireFormat::Xml)
);
assert_eq!(
classify_bytes(b"\xef\xbb\xbf <llsd>"),
Some(WireFormat::Xml)
);
assert_eq!(classify_bytes(b" [1,2]"), Some(WireFormat::Json));
assert_eq!(classify_bytes(b"Unknown conference"), None);
}
#[test]
fn string_dispatch_matches_reference_fallback_to_json() {
assert_eq!(classify_string("<? llsd/binary ?>i"), WireFormat::Binary);
assert_eq!(classify_string("<LLSD>"), WireFormat::Xml);
assert_eq!(classify_string("plain text"), WireFormat::Json);
}
#[test]
fn public_byte_dispatch_rejects_unrecognized_plain_text() {
assert_eq!(
deserialize_bytes(b"Unknown conference".to_vec()),
Err(Error::Argument)
);
let cases: &[(&[u8], &str)] = &[
(
b"<? llsd/binary ?>i\0\0\0\0",
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.Byte[])",
),
(
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)",
),
];
for (input, expected_member) in cases {
assert_eq!(
deserialize_bytes(input.to_vec())
.expect_err("format decoder is intentionally owned by a later issue")
.csharp_member(),
Some(*expected_member)
);
}
let stream = Box::new(std::io::Cursor::new(b"{}".to_vec()));
assert_eq!(
deserialize_stream(stream)
.expect_err("JSON decoder is intentionally owned by issue #36")
.csharp_member(),
Some("M:LibreMetaverse.StructuredData.OSDParser.DeserializeJson(System.String)")
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,9 @@
extern crate self as libremetaverse_structured_data;
mod dispatch;
mod generated;
mod model;
pub mod xml {
/// Project-owned XML element boundary; external XML APIs are not copied.

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,19 @@ impl<T: std::io::Read + std::io::Write> ReadWrite for T {}
#[derive(Clone, Debug)]
pub enum Object {
Undefined,
Boolean(bool),
Integer(i32),
UInteger(u32),
Long(i64),
ULong(u64),
Real(f64),
Color4(crate::Color4),
Date(std::time::SystemTime),
Bytes(Vec<u8>),
Uri(Uri),
Array(Vec<Object>),
Map(std::collections::HashMap<String, Object>),
Matrix4(crate::Matrix4),
Quaternion(crate::Quaternion),
String(String),
@@ -36,6 +48,18 @@ pub enum Object {
impl PartialEq for Object {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Undefined, Self::Undefined) => true,
(Self::Boolean(lhs), Self::Boolean(rhs)) => lhs == rhs,
(Self::Integer(lhs), Self::Integer(rhs)) => lhs == rhs,
(Self::UInteger(lhs), Self::UInteger(rhs)) => lhs == rhs,
(Self::Long(lhs), Self::Long(rhs)) => lhs == rhs,
(Self::ULong(lhs), Self::ULong(rhs)) => lhs == rhs,
(Self::Real(lhs), Self::Real(rhs)) => lhs.to_bits() == rhs.to_bits(),
(Self::Date(lhs), Self::Date(rhs)) => lhs == rhs,
(Self::Bytes(lhs), Self::Bytes(rhs)) => lhs == rhs,
(Self::Uri(lhs), Self::Uri(rhs)) => lhs == rhs,
(Self::Array(lhs), Self::Array(rhs)) => lhs == rhs,
(Self::Map(lhs), Self::Map(rhs)) => lhs == rhs,
(Self::Color4(lhs), Self::Color4(rhs)) => crate::Color4::eq(*lhs, *rhs),
(Self::Matrix4(lhs), Self::Matrix4(rhs)) => crate::Matrix4::eq(*lhs, *rhs),
(Self::Quaternion(lhs), Self::Quaternion(rhs)) => crate::Quaternion::eq(*lhs, *rhs),
@@ -56,6 +80,22 @@ impl std::hash::Hash for Object {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
std::mem::discriminant(self).hash(state);
match self {
Self::Undefined => {}
Self::Boolean(value) => value.hash(state),
Self::Integer(value) => value.hash(state),
Self::UInteger(value) => value.hash(state),
Self::Long(value) => value.hash(state),
Self::ULong(value) => value.hash(state),
Self::Real(value) => value.to_bits().hash(state),
Self::Date(value) => value.hash(state),
Self::Bytes(value) => value.hash(state),
Self::Uri(value) => value.hash(state),
Self::Array(value) => value.hash(state),
Self::Map(value) => {
let mut entries: Vec<_> = value.iter().collect();
entries.sort_unstable_by_key(|(key, _)| *key);
entries.hash(state);
}
Self::Color4(value) => value.get_hash_code().hash(state),
Self::Matrix4(value) => value.get_hash_code().hash(state),
Self::Quaternion(value) => value.get_hash_code().hash(state),