Implement StructuredData OSD model and containers
This commit is contained in:
@@ -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]
|
||||
|
||||
194
crates/libremetaverse-structured-data/src/dispatch.rs
Normal file
194
crates/libremetaverse-structured-data/src/dispatch.rs
Normal 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
@@ -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.
|
||||
|
||||
1855
crates/libremetaverse-structured-data/src/model.rs
Normal file
1855
crates/libremetaverse-structured-data/src/model.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user