Implement JSON and Protobuf OSD codecs
This commit is contained in:
467
crates/libremetaverse-structured-data/src/json_codec.rs
Normal file
467
crates/libremetaverse-structured-data/src/json_codec.rs
Normal file
@@ -0,0 +1,467 @@
|
||||
//! Bounded JSON parsing with explicit `LibreMetaverse` OSD coercions.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)]
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
|
||||
use crate::{Error, OSD};
|
||||
use libremetaverse_types::compat::{ReadWrite, Uri};
|
||||
use serde::de::{self, DeserializeSeed, Error as _, MapAccess, SeqAccess, Visitor};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::io::Read as _;
|
||||
use std::rc::Rc;
|
||||
|
||||
const MAX_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES;
|
||||
|
||||
pub(crate) fn deserialize_string(json: String) -> Result<OSD, Error> {
|
||||
if json.len() > MAX_BYTES {
|
||||
return Err(parse_error(0, "JSON OSD input exceeds allocation limit"));
|
||||
}
|
||||
let json = json.strip_prefix('\u{feff}').unwrap_or(&json);
|
||||
let limits = Limits::default();
|
||||
let mut deserializer = serde_json::Deserializer::from_str(json);
|
||||
let value = Seed { depth: 0, limits }
|
||||
.deserialize(&mut deserializer)
|
||||
.map_err(|error| json_error(json, &error))?;
|
||||
deserializer
|
||||
.end()
|
||||
.map_err(|error| json_error(json, &error))?;
|
||||
value.validate_limits(
|
||||
OSD::DEFAULT_MAX_DEPTH,
|
||||
OSD::DEFAULT_MAX_NODES,
|
||||
OSD::DEFAULT_MAX_BINARY_BYTES,
|
||||
)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_stream(mut json: Box<dyn ReadWrite + Send>) -> Result<OSD, Error> {
|
||||
let mut bytes = Vec::new();
|
||||
(&mut *json)
|
||||
.take((MAX_BYTES + 1) as u64)
|
||||
.read_to_end(&mut bytes)
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
if bytes.len() > MAX_BYTES {
|
||||
return Err(parse_error(0, "JSON OSD input exceeds allocation limit"));
|
||||
}
|
||||
let text = String::from_utf8(bytes)
|
||||
.map_err(|_| parse_error(0, "JSON OSD input is not valid UTF-8"))?;
|
||||
deserialize_string(text)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize(osd: OSD, preserve_defaults: Option<bool>) -> Result<String, Error> {
|
||||
osd.validate_limits(
|
||||
OSD::DEFAULT_MAX_DEPTH,
|
||||
OSD::DEFAULT_MAX_NODES,
|
||||
OSD::DEFAULT_MAX_BINARY_BYTES,
|
||||
)?;
|
||||
let mut encoder = Encoder::new();
|
||||
encoder.write_value(&osd, preserve_defaults.unwrap_or(false), 0)?;
|
||||
Ok(encoder.output)
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Limits {
|
||||
nodes: Rc<Cell<usize>>,
|
||||
allocated: Rc<Cell<usize>>,
|
||||
}
|
||||
|
||||
impl Limits {
|
||||
fn add_node<E: de::Error>(&self, depth: usize) -> Result<(), E> {
|
||||
if depth > OSD::DEFAULT_MAX_DEPTH {
|
||||
return Err(E::custom("JSON OSD nesting depth exceeded"));
|
||||
}
|
||||
let nodes = self
|
||||
.nodes
|
||||
.get()
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| E::custom("JSON OSD node count overflow"))?;
|
||||
if nodes > OSD::DEFAULT_MAX_NODES {
|
||||
return Err(E::custom("JSON OSD node limit exceeded"));
|
||||
}
|
||||
self.nodes.set(nodes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_allocation<E: de::Error>(&self, amount: usize) -> Result<(), E> {
|
||||
let allocated = self
|
||||
.allocated
|
||||
.get()
|
||||
.checked_add(amount)
|
||||
.ok_or_else(|| E::custom("JSON OSD allocation overflow"))?;
|
||||
if allocated > MAX_BYTES {
|
||||
return Err(E::custom("JSON OSD allocation limit exceeded"));
|
||||
}
|
||||
self.allocated.set(allocated);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Seed {
|
||||
depth: usize,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl<'de> DeserializeSeed<'de> for Seed {
|
||||
type Value = OSD;
|
||||
|
||||
fn deserialize<D: serde::Deserializer<'de>>(self, deserializer: D) -> Result<OSD, D::Error> {
|
||||
self.limits.add_node(self.depth)?;
|
||||
deserializer.deserialize_any(OsdVisitor {
|
||||
depth: self.depth,
|
||||
limits: self.limits,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct OsdVisitor {
|
||||
depth: usize,
|
||||
limits: Limits,
|
||||
}
|
||||
|
||||
impl<'de> Visitor<'de> for OsdVisitor {
|
||||
type Value = OSD;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a JSON value convertible to OSD")
|
||||
}
|
||||
|
||||
fn visit_bool<E: de::Error>(self, value: bool) -> Result<OSD, E> {
|
||||
Ok(OSD::Boolean(value))
|
||||
}
|
||||
|
||||
fn visit_i64<E: de::Error>(self, value: i64) -> Result<OSD, E> {
|
||||
if let Ok(value) = i32::try_from(value) {
|
||||
Ok(OSD::Integer(value))
|
||||
} else {
|
||||
self.limits.add_allocation(8)?;
|
||||
Ok(OSD::Binary(value.to_be_bytes().to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
fn visit_u64<E: de::Error>(self, value: u64) -> Result<OSD, E> {
|
||||
if let Ok(value) = i32::try_from(value) {
|
||||
Ok(OSD::Integer(value))
|
||||
} else if let Ok(value) = i64::try_from(value) {
|
||||
self.limits.add_allocation(8)?;
|
||||
Ok(OSD::Binary(value.to_be_bytes().to_vec()))
|
||||
} else {
|
||||
Ok(OSD::Real(value as f64))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_f64<E: de::Error>(self, value: f64) -> Result<OSD, E> {
|
||||
if value.is_finite() {
|
||||
Ok(OSD::Real(value))
|
||||
} else {
|
||||
Err(E::custom(
|
||||
"JSON OSD number is outside the finite double range",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<OSD, E> {
|
||||
self.limits.add_allocation(value.len())?;
|
||||
Ok(if value.is_empty() {
|
||||
OSD::Undefined
|
||||
} else {
|
||||
OSD::String(value.to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_string<E: de::Error>(self, value: String) -> Result<OSD, E> {
|
||||
self.limits.add_allocation(value.len())?;
|
||||
Ok(if value.is_empty() {
|
||||
OSD::Undefined
|
||||
} else {
|
||||
OSD::String(value)
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_none<E: de::Error>(self) -> Result<OSD, E> {
|
||||
Ok(OSD::Undefined)
|
||||
}
|
||||
|
||||
fn visit_unit<E: de::Error>(self) -> Result<OSD, E> {
|
||||
Ok(OSD::Undefined)
|
||||
}
|
||||
|
||||
fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<OSD, A::Error> {
|
||||
let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0).min(1024));
|
||||
while let Some(value) = sequence.next_element_seed(Seed {
|
||||
depth: self.depth + 1,
|
||||
limits: self.limits.clone(),
|
||||
})? {
|
||||
self.limits
|
||||
.add_allocation::<A::Error>(std::mem::size_of::<OSD>())?;
|
||||
values.push(value);
|
||||
}
|
||||
Ok(OSD::Array(values))
|
||||
}
|
||||
|
||||
fn visit_map<A: MapAccess<'de>>(self, mut object: A) -> Result<OSD, A::Error> {
|
||||
let mut values = HashMap::with_capacity(object.size_hint().unwrap_or(0).min(1024));
|
||||
while let Some(key) = object.next_key::<String>()? {
|
||||
self.limits
|
||||
.add_allocation::<A::Error>(key.len() + std::mem::size_of::<(String, OSD)>())?;
|
||||
if values.contains_key(&key) {
|
||||
return Err(A::Error::custom("duplicate JSON OSD object property"));
|
||||
}
|
||||
let value = object.next_value_seed(Seed {
|
||||
depth: self.depth + 1,
|
||||
limits: self.limits.clone(),
|
||||
})?;
|
||||
values.insert(key, value);
|
||||
}
|
||||
Ok(OSD::Map(values))
|
||||
}
|
||||
}
|
||||
|
||||
struct Encoder {
|
||||
output: String,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
output: String::with_capacity(128),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_value(&mut self, value: &OSD, preserve: bool, depth: usize) -> Result<(), Error> {
|
||||
if depth > OSD::DEFAULT_MAX_DEPTH {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
if !preserve && is_default(value) {
|
||||
return self.push("null");
|
||||
}
|
||||
match value {
|
||||
OSD::Undefined | OSD::LlsdXml(_) => self.push("null"),
|
||||
OSD::Boolean(value) => self.push(if *value { "true" } else { "false" }),
|
||||
OSD::Integer(value) => self.push(&value.to_string()),
|
||||
OSD::Real(value) => {
|
||||
let number = serde_json::Number::from_f64(*value).ok_or(Error::Argument)?;
|
||||
self.push(&number.to_string())
|
||||
}
|
||||
OSD::String(value) => self.write_string(value),
|
||||
OSD::UUID(value) => self.write_string(&value.to_string()),
|
||||
OSD::Date(value) => {
|
||||
self.write_string(&crate::model::format_system_time_for_codec(*value))
|
||||
}
|
||||
OSD::Uri(Uri(value)) => self.write_string(&crate::model::format_uri_for_codec(value)),
|
||||
OSD::Binary(values) => {
|
||||
self.push("[")?;
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if index != 0 {
|
||||
self.push(",")?;
|
||||
}
|
||||
self.push(&value.to_string())?;
|
||||
}
|
||||
self.push("]")
|
||||
}
|
||||
OSD::Array(values) => {
|
||||
self.push("[")?;
|
||||
for (index, value) in values.iter().enumerate() {
|
||||
if index != 0 {
|
||||
self.push(",")?;
|
||||
}
|
||||
self.write_value(value, preserve, depth + 1)?;
|
||||
}
|
||||
self.push("]")
|
||||
}
|
||||
OSD::Map(values) => {
|
||||
self.push("{")?;
|
||||
let mut entries: Vec<_> = values.iter().collect();
|
||||
entries.sort_unstable_by_key(|(key, _)| *key);
|
||||
let mut written = 0;
|
||||
for (key, value) in entries {
|
||||
if !preserve && is_default(value) {
|
||||
continue;
|
||||
}
|
||||
if written != 0 {
|
||||
self.push(",")?;
|
||||
}
|
||||
self.write_string(key)?;
|
||||
self.push(":")?;
|
||||
self.write_value(value, preserve, depth + 1)?;
|
||||
written += 1;
|
||||
}
|
||||
self.push("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_string(&mut self, value: &str) -> Result<(), Error> {
|
||||
self.push("\"")?;
|
||||
let mut start = 0;
|
||||
for (index, character) in value.char_indices() {
|
||||
let escape = match character {
|
||||
'\u{08}' => Some("\\b"),
|
||||
'\t' => Some("\\t"),
|
||||
'\n' => Some("\\n"),
|
||||
'\u{0c}' => Some("\\f"),
|
||||
'\r' => Some("\\r"),
|
||||
'"' => Some("\\u0022"),
|
||||
'&' => Some("\\u0026"),
|
||||
'\'' => Some("\\u0027"),
|
||||
'+' => Some("\\u002B"),
|
||||
'<' => Some("\\u003C"),
|
||||
'>' => Some("\\u003E"),
|
||||
'\\' => Some("\\\\"),
|
||||
character if character < ' ' || !character.is_ascii() => None,
|
||||
_ => continue,
|
||||
};
|
||||
self.push(&value[start..index])?;
|
||||
if let Some(escape) = escape {
|
||||
self.push(escape)?;
|
||||
} else {
|
||||
let mut units = [0_u16; 2];
|
||||
for unit in character.encode_utf16(&mut units) {
|
||||
self.push(&format!("\\u{unit:04X}"))?;
|
||||
}
|
||||
}
|
||||
start = index + character.len_utf8();
|
||||
}
|
||||
self.push(&value[start..])?;
|
||||
self.push("\"")
|
||||
}
|
||||
|
||||
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 is_default(value: &OSD) -> bool {
|
||||
match value {
|
||||
OSD::Undefined | OSD::LlsdXml(_) => true,
|
||||
OSD::Boolean(value) => !value,
|
||||
OSD::Integer(value) => *value == 0,
|
||||
OSD::Real(value) => *value == 0.0,
|
||||
OSD::String(value) | OSD::Uri(Uri(value)) => value.is_empty(),
|
||||
OSD::UUID(value) => value.equals_with_uuid(libremetaverse_types::UUID::zero()),
|
||||
OSD::Binary(value) => value.is_empty(),
|
||||
OSD::Date(_) | OSD::Array(_) | OSD::Map(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn json_error(input: &str, error: &serde_json::Error) -> Error {
|
||||
let line = error.line().max(1);
|
||||
let column = error.column().max(1);
|
||||
let line_start = input
|
||||
.match_indices('\n')
|
||||
.take(line.saturating_sub(1))
|
||||
.last()
|
||||
.map_or(0, |(index, _)| index + 1);
|
||||
parse_error(
|
||||
line_start.saturating_add(column - 1).min(input.len()),
|
||||
"malformed or resource-limited JSON OSD",
|
||||
)
|
||||
}
|
||||
|
||||
const fn parse_error(position: usize, context: &'static str) -> Error {
|
||||
Error::Parse { position, context }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use libremetaverse_types::UUID;
|
||||
|
||||
#[test]
|
||||
fn explicit_json_coercions_and_default_policy_match_reference() {
|
||||
assert_eq!(deserialize_string("\"\"".into()).unwrap(), OSD::Undefined);
|
||||
assert_eq!(deserialize_string("null".into()).unwrap(), OSD::Undefined);
|
||||
let stream: Box<dyn ReadWrite + Send> =
|
||||
Box::new(std::io::Cursor::new(b"{\"stream\":true}".to_vec()));
|
||||
assert_eq!(
|
||||
deserialize_stream(stream).unwrap(),
|
||||
OSD::Map(HashMap::from([("stream".into(), OSD::Boolean(true))]))
|
||||
);
|
||||
assert_eq!(
|
||||
deserialize_string("2147483648".into()).unwrap(),
|
||||
OSD::Binary(2_147_483_648_i64.to_be_bytes().to_vec())
|
||||
);
|
||||
let value = OSD::Map(HashMap::from([
|
||||
("false".into(), OSD::Boolean(false)),
|
||||
("true".into(), OSD::Boolean(true)),
|
||||
("zero".into(), OSD::Integer(0)),
|
||||
(
|
||||
"array".into(),
|
||||
OSD::Array(vec![OSD::Integer(0), OSD::String("x".into())]),
|
||||
),
|
||||
]));
|
||||
assert_eq!(
|
||||
serialize(value.clone(), None).unwrap(),
|
||||
"{\"array\":[null,\"x\"],\"true\":true}"
|
||||
);
|
||||
assert_eq!(
|
||||
serialize(value, Some(true)).unwrap(),
|
||||
include_str!("../../../tests/fixtures/structured_data/json_reference.json").trim()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_strings_use_system_text_json_compatible_escaping() {
|
||||
let value = OSD::String("<&+\"'\\ ж 𠮟".into());
|
||||
assert_eq!(
|
||||
serialize(value.clone(), Some(true)).unwrap(),
|
||||
"\"\\u003C\\u0026\\u002B\\u0022\\u0027\\\\ \\u0436 \\uD842\\uDF9F\""
|
||||
);
|
||||
assert_eq!(
|
||||
deserialize_string(serialize(value.clone(), Some(true)).unwrap()).unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_duplicate_and_deep_json_are_positioned_errors() {
|
||||
for input in ["{\"x\":1,\"x\":2}", "[1,", "1 trailing"] {
|
||||
assert!(matches!(
|
||||
deserialize_string(input.into()),
|
||||
Err(Error::Parse { .. })
|
||||
));
|
||||
}
|
||||
let too_deep = format!(
|
||||
"{}null{}",
|
||||
"[".repeat(OSD::DEFAULT_MAX_DEPTH + 2),
|
||||
"]".repeat(OSD::DEFAULT_MAX_DEPTH + 2)
|
||||
);
|
||||
assert!(matches!(
|
||||
deserialize_string(too_deep),
|
||||
Err(Error::Parse { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_round_trip_preserves_representable_osd_values() {
|
||||
let value = OSD::Map(HashMap::from([
|
||||
("boolean".into(), OSD::Boolean(false)),
|
||||
("integer".into(), OSD::Integer(-4)),
|
||||
("real".into(), OSD::Real(1.5)),
|
||||
("string".into(), OSD::String("text".into())),
|
||||
("uuid".into(), OSD::UUID(UUID::zero())),
|
||||
]));
|
||||
let decoded = deserialize_string(serialize(value, Some(true)).unwrap()).unwrap();
|
||||
let OSD::Map(decoded) = decoded else {
|
||||
panic!("expected JSON object");
|
||||
};
|
||||
assert_eq!(decoded["boolean"], OSD::Boolean(false));
|
||||
assert_eq!(decoded["integer"], OSD::Integer(-4));
|
||||
assert_eq!(decoded["real"], OSD::Real(1.5));
|
||||
assert_eq!(decoded["string"], OSD::String("text".into()));
|
||||
assert_eq!(
|
||||
decoded["uuid"],
|
||||
OSD::String("00000000-0000-0000-0000-000000000000".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user