use super::lychee_budget::Budget;
use crate::Result;
use rmpv::ValueRef;
use std::collections::BTreeSet;
pub(super) const MAX_SCENE: usize = 16 * 1024 * 1024;
const MAX_DEPTH: usize = 64;
const MAX_NODES: usize = 1_000_000;
const MAX_STRING: usize = 1024 * 1024;
const IDENTIFIER_MASK: &[u8] = b"DragonFruitFTW";
const IDENTIFIER: [u8; 36] = [
0x25, 0x4a, 0x04, 0x02, 0x5e, 0x5f, 0x72, 0x44, 0x58, 0x51, 0x10, 0x76, 0x67, 0x7a, 0x70, 0x10,
0x57, 0x5e, 0x42, 0x56, 0x27, 0x44, 0x42, 0x44, 0x41, 0x7f, 0x64, 0x67, 0x7d, 0x13, 0x52, 0x01,
0x56, 0x0b, 0x23, 0x45,
];
pub(super) fn decode_bytes(encoded: &[u8], budget: &mut Budget) -> Result<Vec<u8>> {
if encoded.len() > MAX_SCENE {
return Err("Lychee scene state exceeds the 16 MiB limit".into());
}
budget.storage(encoded.len(), 1)?;
budget.work(encoded.len())?;
let mut decoded = Vec::new();
decoded.try_reserve_exact(encoded.len())?;
if decoded.capacity() > encoded.len() {
budget.storage(decoded.capacity() - encoded.len(), 1)?;
}
for (index, byte) in encoded.iter().copied().enumerate() {
let offset = index % IDENTIFIER.len();
let identifier = IDENTIFIER[offset] ^ IDENTIFIER_MASK[offset % IDENTIFIER_MASK.len()];
decoded.push(byte.wrapping_sub(identifier));
}
Ok(decoded)
}
pub(super) fn decode<'a>(bytes: &'a [u8], budget: &mut Budget) -> Result<ValueRef<'a>> {
let mut preflight = Preflight {
bytes,
offset: 0,
nodes: 0,
budget,
};
preflight.value(0)?;
if preflight.offset != bytes.len() {
return Err("trailing Lychee MessagePack scene bytes".into());
}
let values = preflight
.nodes
.checked_mul(6)
.ok_or("Lychee metadata allocation overflow")?;
preflight
.budget
.storage(values, std::mem::size_of::<ValueRef<'_>>())?;
let mut remaining = bytes;
let value = rmpv::decode::read_value_ref_with_max_depth(&mut remaining, MAX_DEPTH + 4)?;
if !remaining.is_empty() {
return Err("trailing Lychee MessagePack scene bytes".into());
}
Ok(value)
}
struct Preflight<'a, 'b> {
bytes: &'a [u8],
offset: usize,
nodes: usize,
budget: &'b mut Budget,
}
impl<'a> Preflight<'a, '_> {
fn take(&mut self, length: usize) -> Result<&'a [u8]> {
let end = self
.offset
.checked_add(length)
.ok_or("Lychee MessagePack span overflow")?;
let bytes = self
.bytes
.get(self.offset..end)
.ok_or("truncated Lychee MessagePack scene")?;
self.offset = end;
Ok(bytes)
}
fn byte(&mut self) -> Result<u8> {
Ok(self.take(1)?[0])
}
fn length(&mut self, width: usize) -> Result<usize> {
let value = self
.take(width)?
.iter()
.fold(0_u64, |value, &byte| value * 256 + u64::from(byte));
Ok(usize::try_from(value)?)
}
fn string(&mut self, length: usize) -> Result<&'a str> {
if length > MAX_STRING {
return Err("Lychee MessagePack string exceeds the 1 MiB limit".into());
}
Ok(std::str::from_utf8(self.take(length)?)?)
}
fn value(&mut self, depth: usize) -> Result<Option<&'a str>> {
self.nodes += 1;
if self.nodes > MAX_NODES || depth > MAX_DEPTH {
return Err("Lychee MessagePack node or depth limit exceeded".into());
}
self.budget.work(1)?;
let tag = self.byte()?;
match tag {
0x00..=0x7f | 0xc0 | 0xc2..=0xc3 | 0xe0..=0xff => {}
0x80..=0x8f => self.map(usize::from(tag & 15), depth)?,
0x90..=0x9f => self.array(usize::from(tag & 15), depth)?,
0xa0..=0xbf => return self.string(usize::from(tag & 31)).map(Some),
0xcc | 0xd0 => {
self.take(1)?;
}
0xcd | 0xd1 => {
self.take(2)?;
}
0xca | 0xce | 0xd2 => {
self.take(4)?;
}
0xcb | 0xcf | 0xd3 => {
self.take(8)?;
}
0xc4..=0xc6 => {
let width = 1 << (tag - 0xc4);
let length = self.length(width)?;
self.take(length)?;
}
0xd9..=0xdb => {
let width = 1 << (tag - 0xd9);
let length = self.length(width)?;
return self.string(length).map(Some);
}
0xdc..=0xdd => {
let width = if tag == 0xdc { 2 } else { 4 };
let length = self.length(width)?;
self.array(length, depth)?;
}
0xde..=0xdf => {
let width = if tag == 0xde { 2 } else { 4 };
let length = self.length(width)?;
self.map(length, depth)?;
}
0xc7..=0xc9 => {
let width = 1 << (tag - 0xc7);
let length = self.length(width)?;
self.take(
length
.checked_add(1)
.ok_or("Lychee extension span overflow")?,
)?;
}
0xd4..=0xd8 => {
self.take((1 << (tag - 0xd4)) + 1)?;
}
0xc1 => return Err("invalid Lychee MessagePack reserved marker".into()),
}
Ok(None)
}
fn array(&mut self, count: usize, depth: usize) -> Result<()> {
self.count(count)?;
for _ in 0..count {
self.value(depth + 1)?;
}
Ok(())
}
fn map(&mut self, count: usize, depth: usize) -> Result<()> {
self.count(
count
.checked_mul(2)
.ok_or("Lychee MessagePack map count overflow")?,
)?;
self.budget.storage(count, 128)?;
let mut keys = BTreeSet::new();
for _ in 0..count {
if let Some(key) = self.value(depth + 1)?
&& !keys.insert(key)
{
return Err("duplicate Lychee MessagePack map key".into());
}
self.value(depth + 1)?;
}
Ok(())
}
fn count(&self, count: usize) -> Result<()> {
if count > self.bytes.len() - self.offset || count > MAX_NODES - self.nodes {
return Err("Lychee MessagePack declared count exceeds source or node limits".into());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn malformed_counts_duplicate_keys_trailing_bytes_and_depth_fail_before_value_allocation() {
for bytes in [
[0xdd, 0xff, 0xff, 0xff, 0xff].as_slice(),
&[0xdf, 0xff, 0xff, 0xff, 0xff],
&[0x82, 0xa1, b'a', 1, 0xa1, b'a', 2],
&[0xa1, 0xff],
&[0x90, 0],
&[0xc1],
&[0xd4, 0],
] {
assert!(decode(bytes, &mut Budget::default()).is_err());
}
let mut nested = vec![0x91; MAX_DEPTH + 1];
nested.push(0);
assert!(
decode(&nested, &mut Budget::default())
.unwrap_err()
.to_string()
.contains("depth")
);
assert!(decode(&[0x81, 0xa1, b'a', 0x92, 1, 2], &mut Budget::default()).is_ok());
assert!(decode(&[0x81, 1, 2], &mut Budget::default()).is_ok());
assert!(decode(&[0x81, 0xa1, b'a', 0xd4, 4, 9], &mut Budget::default()).is_ok());
}
#[test]
fn allocation_and_work_preflight_fail_before_upstream_value_construction() {
let mut budget = Budget::default();
budget.storage(512, 1024 * 1024).unwrap();
assert!(
decode(&[0x90], &mut budget)
.unwrap_err()
.to_string()
.contains("allocation")
);
let mut budget = Budget::default();
budget.work(50_000_000).unwrap();
assert!(
decode(&[0x90], &mut budget)
.unwrap_err()
.to_string()
.contains("work")
);
let mut budget = Budget::default();
budget.storage(512, 1024 * 1024).unwrap();
assert!(
decode_bytes(&[0], &mut budget)
.unwrap_err()
.to_string()
.contains("allocation")
);
}
#[test]
fn every_truncated_prefix_and_complete_scalar_arrays_are_checked() {
let bytes = [
0x81, 0xa1, b'x', 0x93, 0xc3, 0xcd, 0, 4, 0xd9, 3, b'a', b'b', b'c',
];
for end in 0..bytes.len() {
assert!(
decode(&bytes[..end], &mut Budget::default()).is_err(),
"prefix {end}"
);
}
assert!(decode(&bytes, &mut Budget::default()).is_ok());
}
}