682 lines
23 KiB
Rust
682 lines
23 KiB
Rust
//! 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",
|
|
..
|
|
})
|
|
));
|
|
}
|
|
}
|