//! 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 = ""; const PROTOBUF_HEADER: &str = ""; const XML_HEADERS: [&str; 3] = ["", ""]; #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum WireFormat { Binary, Json, Protobuf, Xml, } pub(crate) fn deserialize_bytes(data: Vec) -> Result { 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 { 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) -> Result { 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 { value.validate_limits( OSD::DEFAULT_MAX_DEPTH, OSD::DEFAULT_MAX_NODES, OSD::DEFAULT_MAX_BINARY_BYTES, )?; Ok(value) } fn classify_bytes(data: &[u8]) -> Option { 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::*; use std::collections::HashMap; #[test] fn byte_dispatch_recognizes_headers_bom_whitespace_and_json_tokens() { assert_eq!( classify_bytes(b"i"), Some(WireFormat::Binary) ); assert_eq!( classify_bytes(b""), Some(WireFormat::Protobuf) ); assert_eq!( classify_bytes(b" \n"), 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("i"), WireFormat::Binary); assert_eq!(classify_string(""), 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) ); assert_eq!( deserialize_bytes(b"i\0\0\0\0".to_vec()), Ok(OSD::Integer(0)) ); assert_eq!( deserialize_bytes(b"".to_vec()), Ok(OSD::Undefined) ); assert_eq!( deserialize_bytes(b"".to_vec()), Ok(OSD::Undefined) ); assert_eq!( deserialize_bytes(b"{}".to_vec()), Ok(OSD::Map(HashMap::new())) ); let stream = Box::new(std::io::Cursor::new(b"{}".to_vec())); assert_eq!(deserialize_stream(stream), Ok(OSD::Map(HashMap::new()))); } }