Implement bounded Binary LLSD codec
This commit is contained in:
681
crates/libremetaverse-structured-data/src/binary.rs
Normal file
681
crates/libremetaverse-structured-data/src/binary.rs
Normal file
@@ -0,0 +1,681 @@
|
||||
//! Bounded Binary LLSD codec compatible with `LibreMetaverse`'s marker grammar.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Public Result shapes are fixed by the mapped API.
|
||||
#![allow(clippy::needless_pass_by_value)] // Owned arguments mirror mapped C# value parameters.
|
||||
#![allow(clippy::unnecessary_wraps)] // Result return shapes are fixed by the mapped API.
|
||||
#![allow(clippy::unused_self)] // Parser error helpers retain local cursor context call sites.
|
||||
|
||||
use crate::{Error, OSD};
|
||||
use libremetaverse_types::UUID;
|
||||
use libremetaverse_types::compat::{ReadWrite, Uri};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read as _, Seek as _, SeekFrom};
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
const HEADER: &[u8] = b"<?llsd/binary?>";
|
||||
const ALT_HEADER: &[u8] = b"<? llsd/binary ?>";
|
||||
const MAX_INPUT_BYTES: usize = OSD::DEFAULT_MAX_BINARY_BYTES;
|
||||
|
||||
const fn is_binary_whitespace(byte: u8) -> bool {
|
||||
matches!(byte, b' ' | b'\t' | b'\n' | b'\r')
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_bytes(data: Vec<u8>) -> Result<OSD, Error> {
|
||||
if data.len() > MAX_INPUT_BYTES {
|
||||
return Err(parse_error(0, "binary LLSD input exceeds allocation limit"));
|
||||
}
|
||||
let mut parser = Parser::new(&data);
|
||||
parser.skip_whitespace();
|
||||
if parser.consume_prefix_ignore_ascii_case(ALT_HEADER)
|
||||
|| parser.consume_prefix_ignore_ascii_case(HEADER)
|
||||
{
|
||||
parser.skip_whitespace();
|
||||
}
|
||||
let value = parser.parse_value(0)?;
|
||||
value.validate_limits(
|
||||
OSD::DEFAULT_MAX_DEPTH,
|
||||
OSD::DEFAULT_MAX_NODES,
|
||||
OSD::DEFAULT_MAX_BINARY_BYTES,
|
||||
)?;
|
||||
Ok(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::InvalidOperation)?;
|
||||
deserialize_bytes(data)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize(value: OSD) -> Result<Vec<u8>, Error> {
|
||||
serialize_with_header(value, true)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_with_header(value: OSD, prepend_header: bool) -> Result<Vec<u8>, Error> {
|
||||
value.validate_limits(
|
||||
OSD::DEFAULT_MAX_DEPTH,
|
||||
OSD::DEFAULT_MAX_NODES,
|
||||
OSD::DEFAULT_MAX_BINARY_BYTES,
|
||||
)?;
|
||||
let mut encoder = Encoder::new();
|
||||
if prepend_header {
|
||||
encoder.extend(HEADER)?;
|
||||
encoder.push(b'\n')?;
|
||||
}
|
||||
encoder.write_value(&value, 0)?;
|
||||
Ok(encoder.finish())
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_stream(value: OSD) -> Result<Cursor<Vec<u8>>, Error> {
|
||||
serialize_stream_with_header(value, true)
|
||||
}
|
||||
|
||||
pub(crate) fn serialize_stream_with_header(
|
||||
value: OSD,
|
||||
prepend_header: bool,
|
||||
) -> Result<Cursor<Vec<u8>>, Error> {
|
||||
let bytes = serialize_with_header(value, prepend_header)?;
|
||||
let position = bytes.len() as u64;
|
||||
let mut cursor = Cursor::new(bytes);
|
||||
cursor.set_position(position);
|
||||
Ok(cursor)
|
||||
}
|
||||
|
||||
pub(crate) fn consume_bytes(
|
||||
mut stream: Box<dyn ReadWrite + Send>,
|
||||
consume_bytes: i32,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
let length = usize::try_from(consume_bytes).map_err(|_| Error::Argument)?;
|
||||
if length > MAX_INPUT_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut bytes = vec![0_u8; length];
|
||||
stream
|
||||
.read_exact(&mut bytes)
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn find_byte(mut stream: Box<dyn ReadWrite + Send>, to_find: u8) -> Result<bool, Error> {
|
||||
let start = stream
|
||||
.stream_position()
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let mut byte = [0_u8; 1];
|
||||
if stream
|
||||
.read(&mut byte)
|
||||
.map_err(|_| Error::InvalidOperation)?
|
||||
== 0
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if byte[0] == to_find {
|
||||
Ok(true)
|
||||
} else {
|
||||
stream
|
||||
.seek(SeekFrom::Start(start))
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn find_string(
|
||||
mut stream: Box<dyn ReadWrite + Send>,
|
||||
to_find: String,
|
||||
) -> Result<bool, Error> {
|
||||
let start = stream
|
||||
.stream_position()
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let expected = to_find.as_bytes();
|
||||
if expected.len() > MAX_INPUT_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
let mut actual = vec![0_u8; expected.len()];
|
||||
let matched = stream.read_exact(&mut actual).is_ok()
|
||||
&& actual
|
||||
.iter()
|
||||
.zip(expected)
|
||||
.all(|(left, right)| left.eq_ignore_ascii_case(right));
|
||||
if !matched {
|
||||
stream
|
||||
.seek(SeekFrom::Start(start))
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
}
|
||||
Ok(matched)
|
||||
}
|
||||
|
||||
pub(crate) fn skip_whitespace(mut stream: Box<dyn ReadWrite + Send>) -> Result<(), Error> {
|
||||
loop {
|
||||
let start = stream
|
||||
.stream_position()
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
let mut byte = [0_u8; 1];
|
||||
if stream
|
||||
.read(&mut byte)
|
||||
.map_err(|_| Error::InvalidOperation)?
|
||||
== 0
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
if !is_binary_whitespace(byte[0]) {
|
||||
stream
|
||||
.seek(SeekFrom::Start(start))
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn host_to_network_int_bytes(value: i32) -> Result<Vec<u8>, Error> {
|
||||
Ok(value.to_be_bytes().to_vec())
|
||||
}
|
||||
|
||||
pub(crate) fn network_to_host_int(bytes: Vec<u8>) -> Result<i32, Error> {
|
||||
let bytes: [u8; 4] = bytes
|
||||
.get(..4)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(i32::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
pub(crate) fn network_to_host_double(bytes: Vec<u8>) -> Result<f64, Error> {
|
||||
let bytes: [u8; 8] = bytes
|
||||
.get(..8)
|
||||
.ok_or(Error::IndexOutOfRange)?
|
||||
.try_into()
|
||||
.map_err(|_| Error::IndexOutOfRange)?;
|
||||
Ok(f64::from_be_bytes(bytes))
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
bytes: &'a [u8],
|
||||
position: usize,
|
||||
nodes: usize,
|
||||
allocated: usize,
|
||||
}
|
||||
|
||||
impl<'a> Parser<'a> {
|
||||
const fn new(bytes: &'a [u8]) -> Self {
|
||||
Self {
|
||||
bytes,
|
||||
position: 0,
|
||||
nodes: 0,
|
||||
allocated: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_value(&mut self, depth: usize) -> Result<OSD, Error> {
|
||||
if depth > OSD::DEFAULT_MAX_DEPTH {
|
||||
return Err(self.error("binary LLSD nesting depth exceeded"));
|
||||
}
|
||||
self.nodes = self
|
||||
.nodes
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| self.error("binary LLSD node count overflow"))?;
|
||||
if self.nodes > OSD::DEFAULT_MAX_NODES {
|
||||
return Err(self.error("binary LLSD node limit exceeded"));
|
||||
}
|
||||
self.skip_whitespace();
|
||||
let marker_position = self.position;
|
||||
let marker = self.read_byte("missing binary LLSD value marker")?;
|
||||
match marker {
|
||||
b'!' => Ok(OSD::Undefined),
|
||||
b'1' => Ok(OSD::Boolean(true)),
|
||||
b'0' => Ok(OSD::Boolean(false)),
|
||||
b'i' => Ok(OSD::Integer(i32::from_be_bytes(
|
||||
self.read_array("truncated binary LLSD integer")?,
|
||||
))),
|
||||
b'r' => Ok(OSD::Real(f64::from_be_bytes(
|
||||
self.read_array("truncated binary LLSD real")?,
|
||||
))),
|
||||
b'u' => {
|
||||
let bytes = self.read_exact(16, "truncated binary LLSD UUID")?;
|
||||
Ok(OSD::UUID(UUID::new_with_bytes_int32(bytes.to_vec(), 0)?))
|
||||
}
|
||||
b'b' => {
|
||||
let length = self.read_length("invalid binary LLSD binary length")?;
|
||||
self.add_allocation(length, "binary LLSD binary allocation limit exceeded")?;
|
||||
Ok(OSD::Binary(
|
||||
self.read_exact(length, "truncated binary LLSD binary value")?
|
||||
.to_vec(),
|
||||
))
|
||||
}
|
||||
b's' => {
|
||||
let bytes = self.read_sized_text("string")?;
|
||||
Ok(OSD::String(String::from_utf8_lossy(bytes).into_owned()))
|
||||
}
|
||||
b'l' => {
|
||||
let bytes = self.read_sized_text("URI")?;
|
||||
let text = String::from_utf8_lossy(bytes).into_owned();
|
||||
let uri = OSD::String(text.clone())
|
||||
.as_uri()?
|
||||
.ok_or_else(|| self.error_at(marker_position, "invalid binary LLSD URI"))?;
|
||||
Ok(OSD::Uri(uri))
|
||||
}
|
||||
b'd' => {
|
||||
let timestamp = f64::from_le_bytes(self.read_array("truncated binary LLSD date")?);
|
||||
Ok(OSD::Date(system_time_from_seconds(timestamp).ok_or_else(
|
||||
|| self.error_at(marker_position, "invalid binary LLSD date"),
|
||||
)?))
|
||||
}
|
||||
b'[' => self.parse_array(depth),
|
||||
b'{' => self.parse_map(depth),
|
||||
_ => Err(self.error_at(marker_position, "unknown binary LLSD type marker")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_array(&mut self, depth: usize) -> Result<OSD, Error> {
|
||||
let count = self.read_length("invalid binary LLSD array count")?;
|
||||
if count > OSD::DEFAULT_MAX_NODES || count > self.remaining() {
|
||||
return Err(self.error("binary LLSD array count exceeds available input"));
|
||||
}
|
||||
let allocation = count
|
||||
.checked_mul(std::mem::size_of::<OSD>())
|
||||
.ok_or_else(|| self.error("binary LLSD array allocation overflow"))?;
|
||||
self.add_allocation(allocation, "binary LLSD array allocation limit exceeded")?;
|
||||
let mut values = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
values.push(self.parse_value(depth + 1)?);
|
||||
}
|
||||
self.expect_byte(b']', "missing binary LLSD array end marker")?;
|
||||
Ok(OSD::Array(values))
|
||||
}
|
||||
|
||||
fn parse_map(&mut self, depth: usize) -> Result<OSD, Error> {
|
||||
let count = self.read_length("invalid binary LLSD map count")?;
|
||||
if count > OSD::DEFAULT_MAX_NODES || count > self.remaining() {
|
||||
return Err(self.error("binary LLSD map count exceeds available input"));
|
||||
}
|
||||
let allocation = count
|
||||
.checked_mul(std::mem::size_of::<(String, OSD)>())
|
||||
.ok_or_else(|| self.error("binary LLSD map allocation overflow"))?;
|
||||
self.add_allocation(allocation, "binary LLSD map allocation limit exceeded")?;
|
||||
let mut values = HashMap::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
self.expect_byte(b'k', "missing binary LLSD map key marker")?;
|
||||
let key_bytes = self.read_sized_text("map key")?;
|
||||
let key = String::from_utf8_lossy(key_bytes).into_owned();
|
||||
let value = self.parse_value(depth + 1)?;
|
||||
values.insert(key, value);
|
||||
}
|
||||
self.expect_byte(b'}', "missing binary LLSD map end marker")?;
|
||||
Ok(OSD::Map(values))
|
||||
}
|
||||
|
||||
fn read_sized_text(&mut self, context: &'static str) -> Result<&'a [u8], Error> {
|
||||
let length = self.read_length(match context {
|
||||
"string" => "invalid binary LLSD string length",
|
||||
"URI" => "invalid binary LLSD URI length",
|
||||
_ => "invalid binary LLSD map key length",
|
||||
})?;
|
||||
self.add_allocation(length, "binary LLSD text allocation limit exceeded")?;
|
||||
self.read_exact(
|
||||
length,
|
||||
match context {
|
||||
"string" => "truncated binary LLSD string",
|
||||
"URI" => "truncated binary LLSD URI",
|
||||
_ => "truncated binary LLSD map key",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn read_length(&mut self, context: &'static str) -> Result<usize, Error> {
|
||||
let position = self.position;
|
||||
let raw = i32::from_be_bytes(self.read_array(context)?);
|
||||
usize::try_from(raw).map_err(|_| self.error_at(position, context))
|
||||
}
|
||||
|
||||
fn read_array<const N: usize>(&mut self, context: &'static str) -> Result<[u8; N], Error> {
|
||||
self.read_exact(N, context)?
|
||||
.try_into()
|
||||
.map_err(|_| self.error(context))
|
||||
}
|
||||
|
||||
fn read_exact(&mut self, length: usize, context: &'static str) -> Result<&'a [u8], Error> {
|
||||
let end = self
|
||||
.position
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| self.error(context))?;
|
||||
let value = self
|
||||
.bytes
|
||||
.get(self.position..end)
|
||||
.ok_or_else(|| self.error(context))?;
|
||||
self.position = end;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn read_byte(&mut self, context: &'static str) -> Result<u8, Error> {
|
||||
let byte = *self
|
||||
.bytes
|
||||
.get(self.position)
|
||||
.ok_or_else(|| self.error(context))?;
|
||||
self.position += 1;
|
||||
Ok(byte)
|
||||
}
|
||||
|
||||
fn expect_byte(&mut self, expected: u8, context: &'static str) -> Result<(), Error> {
|
||||
let position = self.position;
|
||||
if self.read_byte(context)? == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(self.error_at(position, 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 > OSD::DEFAULT_MAX_BINARY_BYTES {
|
||||
Err(self.error(context))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
while self
|
||||
.bytes
|
||||
.get(self.position)
|
||||
.is_some_and(|byte| is_binary_whitespace(*byte))
|
||||
{
|
||||
self.position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_prefix_ignore_ascii_case(&mut self, prefix: &[u8]) -> bool {
|
||||
let Some(candidate) = self.bytes.get(self.position..self.position + prefix.len()) else {
|
||||
return false;
|
||||
};
|
||||
if candidate.eq_ignore_ascii_case(prefix) {
|
||||
self.position += prefix.len();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
const fn remaining(&self) -> usize {
|
||||
self.bytes.len() - self.position
|
||||
}
|
||||
|
||||
const fn error(&self, context: &'static str) -> Error {
|
||||
self.error_at(self.position, context)
|
||||
}
|
||||
|
||||
const fn error_at(&self, position: usize, context: &'static str) -> Error {
|
||||
parse_error(position, context)
|
||||
}
|
||||
}
|
||||
|
||||
struct Encoder {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
bytes: Vec::with_capacity(128),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(self) -> Vec<u8> {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
fn write_value(&mut self, value: &OSD, depth: usize) -> Result<(), Error> {
|
||||
if depth > OSD::DEFAULT_MAX_DEPTH {
|
||||
return Err(parse_error(
|
||||
self.bytes.len(),
|
||||
"binary LLSD nesting depth exceeded",
|
||||
));
|
||||
}
|
||||
match value {
|
||||
OSD::Undefined => self.push(b'!'),
|
||||
OSD::Boolean(value) => self.push(if *value { b'1' } else { b'0' }),
|
||||
OSD::Integer(value) => {
|
||||
self.push(b'i')?;
|
||||
self.extend(&value.to_be_bytes())
|
||||
}
|
||||
OSD::Real(value) => {
|
||||
self.push(b'r')?;
|
||||
self.extend(&value.to_be_bytes())
|
||||
}
|
||||
OSD::UUID(value) => {
|
||||
self.push(b'u')?;
|
||||
self.extend(&value.get_bytes()?)
|
||||
}
|
||||
OSD::String(value) => {
|
||||
self.push(b's')?;
|
||||
self.write_sized(value.as_bytes())
|
||||
}
|
||||
OSD::Binary(value) => {
|
||||
self.push(b'b')?;
|
||||
self.write_sized(value)
|
||||
}
|
||||
OSD::Date(value) => {
|
||||
self.push(b'd')?;
|
||||
self.extend(&crate::model::unix_seconds_for_codec(*value).to_le_bytes())
|
||||
}
|
||||
OSD::Uri(Uri(value)) => {
|
||||
self.push(b'l')?;
|
||||
self.write_sized(value.as_bytes())
|
||||
}
|
||||
OSD::Array(values) => {
|
||||
self.push(b'[')?;
|
||||
self.write_length(values.len())?;
|
||||
for value in values {
|
||||
self.write_value(value, depth + 1)?;
|
||||
}
|
||||
self.push(b']')
|
||||
}
|
||||
OSD::Map(values) => {
|
||||
self.push(b'{')?;
|
||||
self.write_length(values.len())?;
|
||||
let mut entries: Vec<_> = values.iter().collect();
|
||||
entries.sort_unstable_by_key(|(key, _)| *key);
|
||||
for (key, value) in entries {
|
||||
self.push(b'k')?;
|
||||
self.write_sized(key.as_bytes())?;
|
||||
self.write_value(value, depth + 1)?;
|
||||
}
|
||||
self.push(b'}')
|
||||
}
|
||||
OSD::LlsdXml(_) => Err(parse_error(
|
||||
self.bytes.len(),
|
||||
"LLSD XML fragments have no Binary LLSD marker",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_sized(&mut self, value: &[u8]) -> Result<(), Error> {
|
||||
self.write_length(value.len())?;
|
||||
self.extend(value)
|
||||
}
|
||||
|
||||
fn write_length(&mut self, length: usize) -> Result<(), Error> {
|
||||
let length = i32::try_from(length).map_err(|_| Error::Argument)?;
|
||||
self.extend(&length.to_be_bytes())
|
||||
}
|
||||
|
||||
fn push(&mut self, value: u8) -> Result<(), Error> {
|
||||
if self.bytes.len() >= MAX_INPUT_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
self.bytes.push(value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extend(&mut self, value: &[u8]) -> Result<(), Error> {
|
||||
let length = self
|
||||
.bytes
|
||||
.len()
|
||||
.checked_add(value.len())
|
||||
.ok_or(Error::Argument)?;
|
||||
if length > MAX_INPUT_BYTES {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
self.bytes.extend_from_slice(value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn system_time_from_seconds(seconds: f64) -> Option<std::time::SystemTime> {
|
||||
if !seconds.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if seconds >= 0.0 {
|
||||
UNIX_EPOCH.checked_add(Duration::try_from_secs_f64(seconds).ok()?)
|
||||
} else {
|
||||
UNIX_EPOCH.checked_sub(Duration::try_from_secs_f64(-seconds).ok()?)
|
||||
}
|
||||
}
|
||||
|
||||
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_with_header_and_nested_values() {
|
||||
let uuid = UUID::new_with_string("97f4aeca-88a1-42a1-b385-b97b18abb255".into()).unwrap();
|
||||
let value = OSD::Array(vec![
|
||||
OSD::Undefined,
|
||||
OSD::Boolean(true),
|
||||
OSD::Integer(-42),
|
||||
OSD::Real(12.5),
|
||||
OSD::String("text 𐄷".into()),
|
||||
OSD::UUID(uuid),
|
||||
OSD::Date(UNIX_EPOCH + Duration::from_millis(1_199_218_231_125)),
|
||||
OSD::Uri(Uri("https://example.test/a".into())),
|
||||
OSD::Binary(vec![0, 1, 255]),
|
||||
OSD::Map(HashMap::from([("nested".into(), OSD::Array(vec![]))])),
|
||||
]);
|
||||
let encoded = serialize(value.clone()).unwrap();
|
||||
assert!(encoded.starts_with(b"<?llsd/binary?>\n"));
|
||||
assert_eq!(deserialize_bytes(encoded).unwrap(), value);
|
||||
assert_eq!(
|
||||
deserialize_bytes(b" \t\r\n<? LlSd/BiNaRy ?>\r\ni\0\0\0\x07".to_vec()).unwrap(),
|
||||
OSD::Integer(7)
|
||||
);
|
||||
assert!(matches!(
|
||||
deserialize_bytes(b"\x0b!".to_vec()),
|
||||
Err(Error::Parse {
|
||||
position: 0,
|
||||
context: "unknown binary LLSD type marker"
|
||||
})
|
||||
));
|
||||
|
||||
let mut encoded_stream = serialize_stream_with_header(value.clone(), false).unwrap();
|
||||
assert_eq!(
|
||||
encoded_stream.position(),
|
||||
encoded_stream.get_ref().len() as u64
|
||||
);
|
||||
encoded_stream.set_position(0);
|
||||
assert_eq!(deserialize_stream(Box::new(encoded_stream)).unwrap(), value);
|
||||
|
||||
assert_eq!(
|
||||
host_to_network_int_bytes(-42).unwrap(),
|
||||
(-42_i32).to_be_bytes()
|
||||
);
|
||||
assert_eq!(
|
||||
network_to_host_int((-42_i32).to_be_bytes().to_vec()).unwrap(),
|
||||
-42
|
||||
);
|
||||
assert_eq!(
|
||||
network_to_host_double(12.5_f64.to_be_bytes().to_vec())
|
||||
.unwrap()
|
||||
.to_bits(),
|
||||
12.5_f64.to_bits()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_seed_corpus_is_rejected_with_positions() {
|
||||
let expected: &[(&str, &str)] = &[
|
||||
("empty", "missing binary LLSD value marker"),
|
||||
("unknown_marker", "unknown binary LLSD type marker"),
|
||||
("truncated_integer", "truncated binary LLSD integer"),
|
||||
("truncated_real", "truncated binary LLSD real"),
|
||||
("truncated_uuid", "truncated binary LLSD UUID"),
|
||||
(
|
||||
"negative_binary_length",
|
||||
"invalid binary LLSD binary length",
|
||||
),
|
||||
(
|
||||
"truncated_binary_length",
|
||||
"invalid binary LLSD binary length",
|
||||
),
|
||||
(
|
||||
"negative_string_length",
|
||||
"invalid binary LLSD string length",
|
||||
),
|
||||
("negative_array_count", "invalid binary LLSD array count"),
|
||||
("negative_map_count", "invalid binary LLSD map count"),
|
||||
(
|
||||
"oversized_string_length",
|
||||
"binary LLSD text allocation limit exceeded",
|
||||
),
|
||||
("missing_array_end", "missing binary LLSD array end marker"),
|
||||
(
|
||||
"missing_map_key_marker",
|
||||
"missing binary LLSD map key marker",
|
||||
),
|
||||
("missing_map_end", "missing binary LLSD map end marker"),
|
||||
("non_finite_date", "invalid binary LLSD date"),
|
||||
];
|
||||
let corpus = include_str!("../../../fuzz/corpus/binary_llsd/malformed.hex");
|
||||
let seeds: HashMap<_, _> = corpus
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let (name, hex) = line.split_once(':').expect("named fuzz seed");
|
||||
(name, decode_hex(hex))
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(seeds.len(), expected.len());
|
||||
for (name, expected_context) in expected {
|
||||
let seed = seeds.get(name).expect("fuzz seed named by test");
|
||||
let Error::Parse { position, context } = deserialize_bytes(seed.clone()).unwrap_err()
|
||||
else {
|
||||
panic!("malformed seed did not return a positional parse error");
|
||||
};
|
||||
assert!(position <= seed.len());
|
||||
assert_eq!(context, *expected_context);
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_hex(value: &str) -> Vec<u8> {
|
||||
assert_eq!(value.len() % 2, 0, "fuzz seed hex must contain byte pairs");
|
||||
value
|
||||
.as_bytes()
|
||||
.chunks_exact(2)
|
||||
.map(|pair| {
|
||||
let text = std::str::from_utf8(pair).expect("ASCII fuzz seed hex");
|
||||
u8::from_str_radix(text, 16).expect("valid fuzz seed hex")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excessive_depth_is_rejected_before_stack_growth() {
|
||||
let mut bytes = Vec::new();
|
||||
for _ in 0..=OSD::DEFAULT_MAX_DEPTH {
|
||||
bytes.extend_from_slice(b"[\0\0\0\x01");
|
||||
}
|
||||
bytes.push(b'!');
|
||||
bytes.extend(std::iter::repeat_n(b']', OSD::DEFAULT_MAX_DEPTH + 1));
|
||||
assert!(matches!(
|
||||
deserialize_bytes(bytes),
|
||||
Err(Error::Parse {
|
||||
context: "binary LLSD nesting depth exceeded",
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -156,11 +156,11 @@ mod tests {
|
||||
deserialize_bytes(b"Unknown conference".to_vec()),
|
||||
Err(Error::Argument)
|
||||
);
|
||||
assert_eq!(
|
||||
deserialize_bytes(b"<? llsd/binary ?>i\0\0\0\0".to_vec()),
|
||||
Ok(OSD::Integer(0))
|
||||
);
|
||||
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[])",
|
||||
|
||||
@@ -251,9 +251,7 @@ impl OSDParser {
|
||||
stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||
consume_bytes: i32,
|
||||
) -> Result<Vec<u8>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.ConsumeBytes(System.IO.Stream,System.Int32)",
|
||||
)
|
||||
crate::binary::consume_bytes(stream, consume_bytes)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.Deserialize(System.Byte[])`.
|
||||
pub fn deserialize_with_bytes(
|
||||
@@ -293,17 +291,13 @@ impl OSDParser {
|
||||
pub fn deserialize_llsd_binary_with_bytes(
|
||||
binary_data: Vec<u8>,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.Byte[])",
|
||||
)
|
||||
crate::binary::deserialize_bytes(binary_data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.IO.Stream)`.
|
||||
pub fn deserialize_llsd_binary_with_stream(
|
||||
stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||
) -> Result<libremetaverse_structured_data::OSD, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDBinary(System.IO.Stream)",
|
||||
)
|
||||
crate::binary::deserialize_stream(stream)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.DeserializeLLSDNotation(System.IO.StringReader)`.
|
||||
pub fn deserialize_llsd_notation_with_string_reader(
|
||||
@@ -383,18 +377,14 @@ impl OSDParser {
|
||||
stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||
to_find: u8,
|
||||
) -> Result<bool, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.FindByte(System.IO.Stream,System.Byte)",
|
||||
)
|
||||
crate::binary::find_byte(stream, to_find)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.FindString(System.IO.Stream,System.String)`.
|
||||
pub fn find_string(
|
||||
stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||
to_find: String,
|
||||
) -> Result<bool, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.FindString(System.IO.Stream,System.String)",
|
||||
)
|
||||
crate::binary::find_string(stream, to_find)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.GetLengthInBrackets(System.IO.StringReader)`.
|
||||
pub fn get_length_in_brackets(
|
||||
@@ -415,21 +405,15 @@ impl OSDParser {
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)`.
|
||||
pub fn host_to_network_int_bytes(int_host_end: i32) -> Result<Vec<u8>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.HostToNetworkIntBytes(System.Int32)",
|
||||
)
|
||||
crate::binary::host_to_network_int_bytes(int_host_end)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostDouble(System.Byte[])`.
|
||||
pub fn network_to_host_double(binary_net_end: Vec<u8>) -> Result<f64, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostDouble(System.Byte[])",
|
||||
)
|
||||
crate::binary::network_to_host_double(binary_net_end)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostInt(System.Byte[])`.
|
||||
pub fn network_to_host_int(binary_net_end: Vec<u8>) -> Result<i32, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.NetworkToHostInt(System.Byte[])",
|
||||
)
|
||||
crate::binary::network_to_host_int(binary_net_end)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.PeekAndSkipWhitespace(System.IO.StringReader)`.
|
||||
pub fn peek_and_skip_whitespace(
|
||||
@@ -460,35 +444,27 @@ impl OSDParser {
|
||||
pub fn serialize_llsd_binary_with_osd(
|
||||
osd: libremetaverse_structured_data::OSD,
|
||||
) -> Result<Vec<u8>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::binary::serialize(osd)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD,System.Boolean)`.
|
||||
pub fn serialize_llsd_binary_with_osd_boolean(
|
||||
osd: libremetaverse_structured_data::OSD,
|
||||
prepend_header: bool,
|
||||
) -> Result<Vec<u8>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinary(LibreMetaverse.StructuredData.OSD,System.Boolean)",
|
||||
)
|
||||
crate::binary::serialize_with_header(osd, prepend_header)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_binary_stream_with_osd(
|
||||
data: libremetaverse_structured_data::OSD,
|
||||
) -> Result<std::io::Cursor<Vec<u8>>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD)",
|
||||
)
|
||||
crate::binary::serialize_stream(data)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)`.
|
||||
pub fn serialize_llsd_binary_stream_with_osd_boolean(
|
||||
data: libremetaverse_structured_data::OSD,
|
||||
prepend_header: bool,
|
||||
) -> Result<std::io::Cursor<Vec<u8>>, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDBinaryStream(LibreMetaverse.StructuredData.OSD,System.Boolean)",
|
||||
)
|
||||
crate::binary::serialize_stream_with_header(data, prepend_header)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.SerializeLLSDInnerXmlString(LibreMetaverse.StructuredData.OSD)`.
|
||||
pub fn serialize_llsd_inner_xml_string(
|
||||
@@ -568,9 +544,7 @@ impl OSDParser {
|
||||
pub fn skip_white_space(
|
||||
stream: Box<dyn libremetaverse_types::compat::ReadWrite + Send>,
|
||||
) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.StructuredData.OSDParser.SkipWhiteSpace(System.IO.Stream)",
|
||||
)
|
||||
crate::binary::skip_whitespace(stream)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.StructuredData.OSDParser.UnescapeCharacter(System.String,System.Char)`.
|
||||
pub fn unescape_character(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
extern crate self as libremetaverse_structured_data;
|
||||
|
||||
mod binary;
|
||||
mod dispatch;
|
||||
mod generated;
|
||||
mod model;
|
||||
|
||||
@@ -188,7 +188,7 @@ impl OSD {
|
||||
Self::Real(value) => value.to_be_bytes().to_vec(),
|
||||
Self::String(value) | Self::LlsdXml(value) => value.as_bytes().to_vec(),
|
||||
Self::UUID(value) => value.get_bytes()?,
|
||||
Self::Date(value) => unix_seconds_f64(*value).to_le_bytes().to_vec(),
|
||||
Self::Date(value) => unix_seconds_for_codec(*value).to_le_bytes().to_vec(),
|
||||
Self::Uri(value) => value.0.as_bytes().to_vec(),
|
||||
Self::Binary(value) => value.clone(),
|
||||
Self::Array(values) => values
|
||||
@@ -842,7 +842,7 @@ impl OSDUUID {
|
||||
scalar_wrapper!(OSDDate, SystemTime, Date, Date);
|
||||
impl OSDDate {
|
||||
pub fn as_binary(&self) -> Result<Vec<u8>, Error> {
|
||||
Ok(unix_seconds_f64(self.value).to_le_bytes().to_vec())
|
||||
Ok(unix_seconds_for_codec(self.value).to_le_bytes().to_vec())
|
||||
}
|
||||
pub const fn as_date(&self) -> Result<SystemTime, Error> {
|
||||
Ok(self.value)
|
||||
@@ -1425,7 +1425,7 @@ fn format_real(value: f64) -> String {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
fn unix_seconds_f64(value: SystemTime) -> f64 {
|
||||
pub(crate) fn unix_seconds_for_codec(value: SystemTime) -> f64 {
|
||||
match value.duration_since(UNIX_EPOCH) {
|
||||
Ok(duration) => duration.as_secs_f64(),
|
||||
Err(error) => -error.duration().as_secs_f64(),
|
||||
|
||||
@@ -16,9 +16,9 @@ use std::sync::{
|
||||
|
||||
pub trait Collection<T> {}
|
||||
|
||||
pub trait ReadWrite: std::io::Read + std::io::Write {}
|
||||
pub trait ReadWrite: std::io::Read + std::io::Write + std::io::Seek {}
|
||||
|
||||
impl<T: std::io::Read + std::io::Write> ReadWrite for T {}
|
||||
impl<T: std::io::Read + std::io::Write + std::io::Seek> ReadWrite for T {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Object {
|
||||
|
||||
@@ -55,6 +55,13 @@ pub enum Error {
|
||||
InvalidOperation,
|
||||
/// An index or destination buffer boundary was exceeded.
|
||||
IndexOutOfRange,
|
||||
/// Structured input was malformed at a byte or character position.
|
||||
Parse {
|
||||
/// Zero-based offset at which validation failed.
|
||||
position: usize,
|
||||
/// Static parser context suitable for diagnostics and fuzz triage.
|
||||
context: &'static str,
|
||||
},
|
||||
/// An operation observed a requested cancellation.
|
||||
Cancelled,
|
||||
/// An HTTP request completed with an unsuccessful response.
|
||||
@@ -73,6 +80,7 @@ impl Error {
|
||||
| Self::Argument
|
||||
| Self::InvalidOperation
|
||||
| Self::IndexOutOfRange
|
||||
| Self::Parse { .. }
|
||||
| Self::Cancelled
|
||||
| Self::HttpRequest
|
||||
| Self::Socket => None,
|
||||
@@ -96,6 +104,9 @@ impl fmt::Display for Error {
|
||||
formatter.write_str("operation was invalid for the current state")
|
||||
}
|
||||
Self::IndexOutOfRange => formatter.write_str("index was out of range"),
|
||||
Self::Parse { position, context } => {
|
||||
write!(formatter, "parse error at offset {position}: {context}")
|
||||
}
|
||||
Self::Cancelled => formatter.write_str("operation was cancelled"),
|
||||
Self::HttpRequest => formatter.write_str("HTTP request failed"),
|
||||
Self::Socket => formatter.write_str("socket operation failed"),
|
||||
|
||||
Reference in New Issue
Block a user