use core::fmt;
use core::marker::PhantomData;
#[cfg(feature = "alloc")]
use alloc::boxed::Box;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
use crate::config::{Config, DEFAULT_COLLECTION_LIMIT, DEFAULT_SIZE_LIMIT};
use crate::decoder;
use crate::tags::MAX_DEPTH;
pub const ELEMENT_STRUCTURE_BYTES: u64 = 64;
pub trait DecodeBounded {
const MAX_INPUT: usize;
const MAX_ALLOC: usize;
const MAX_DEPTH: usize;
const MAX_WORK: usize;
const MAX_STRUCTURAL_ELEMENT: usize = usize::MAX;
const STATICALLY_BOUNDED: bool = Self::MAX_ALLOC != usize::MAX;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Budget {
max_input: u64,
max_alloc: u64,
max_depth: usize,
max_work: u64,
element_structure_bytes: u64,
}
impl Budget {
pub const fn new(max_input: u64, max_alloc: u64, max_depth: usize, max_work: u64) -> Self {
Self {
max_input,
max_alloc,
max_depth,
max_work,
element_structure_bytes: ELEMENT_STRUCTURE_BYTES,
}
}
pub const fn from_type<T: DecodeBounded>() -> Self {
Self {
max_input: const_or_default(T::MAX_INPUT, DEFAULT_SIZE_LIMIT),
max_alloc: const_or_default(T::MAX_ALLOC, DEFAULT_SIZE_LIMIT),
max_depth: if T::MAX_DEPTH == usize::MAX {
MAX_DEPTH
} else {
T::MAX_DEPTH
},
max_work: const_or_default(T::MAX_WORK, DEFAULT_SIZE_LIMIT),
element_structure_bytes: ELEMENT_STRUCTURE_BYTES,
}
}
pub const fn from_config(config: Config) -> Self {
let max_input = match config.limit() {
Some(limit) => limit,
None => DEFAULT_SIZE_LIMIT,
};
let max_alloc = match config.collection_limit() {
Some(limit) => limit.saturating_mul(ELEMENT_STRUCTURE_BYTES),
None => DEFAULT_COLLECTION_LIMIT.saturating_mul(ELEMENT_STRUCTURE_BYTES),
};
Self {
max_input,
max_alloc,
max_depth: config.depth_limit(),
max_work: max_input,
element_structure_bytes: ELEMENT_STRUCTURE_BYTES,
}
}
pub const fn with_max_input(mut self, limit: u64) -> Self {
self.max_input = limit;
self
}
pub const fn with_max_alloc(mut self, limit: u64) -> Self {
self.max_alloc = limit;
self
}
pub const fn with_max_depth(mut self, limit: usize) -> Self {
self.max_depth = limit;
self
}
pub const fn with_max_work(mut self, limit: u64) -> Self {
self.max_work = limit;
self
}
pub const fn with_element_structure_bytes(mut self, bytes: u64) -> Self {
self.element_structure_bytes = bytes;
self
}
pub const fn max_input(self) -> u64 {
self.max_input
}
pub const fn max_alloc(self) -> u64 {
self.max_alloc
}
pub const fn max_depth(self) -> usize {
self.max_depth
}
pub const fn max_work(self) -> u64 {
self.max_work
}
pub const fn element_structure_bytes(self) -> u64 {
self.element_structure_bytes
}
}
impl Default for Budget {
fn default() -> Self {
Self {
max_input: DEFAULT_SIZE_LIMIT,
max_alloc: DEFAULT_COLLECTION_LIMIT.saturating_mul(ELEMENT_STRUCTURE_BYTES),
max_depth: MAX_DEPTH,
max_work: DEFAULT_SIZE_LIMIT,
element_structure_bytes: ELEMENT_STRUCTURE_BYTES,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResourceUse {
pub read: u64,
pub alloc_bound: u64,
pub depth_bound: usize,
pub work_bound: u64,
}
#[derive(Debug)]
pub struct Decoded<T> {
pub value: T,
pub use_: ResourceUse,
}
impl<T> Decoded<T> {
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Decoded<U> {
Decoded {
value: f(self.value),
use_: self.use_,
}
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum BudgetExceeded {
Input {
limit: u64,
},
Alloc {
limit: u64,
},
Depth {
limit: usize,
},
Work {
limit: u64,
},
}
#[derive(Debug)]
#[non_exhaustive]
pub enum DecodeError {
Budget(BudgetExceeded),
Codec(crate::Error),
}
impl fmt::Display for BudgetExceeded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Input { limit } => write!(f, "bounded decode: input exceeds {limit} bytes"),
Self::Alloc { limit } => {
write!(
f,
"bounded decode: allocation budget {limit} would be exceeded"
)
}
Self::Depth { limit } => write!(f, "bounded decode: depth exceeds {limit}"),
Self::Work { limit } => write!(f, "bounded decode: work exceeds {limit} units"),
}
}
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Budget(budget) => budget.fmt(f),
Self::Codec(error) => write!(f, "bounded decode failed: {error}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DecodeError {}
impl From<crate::Error> for DecodeError {
fn from(error: crate::Error) -> Self {
Self::Codec(error)
}
}
#[doc(hidden)]
pub const fn const_or_default(value: usize, default: u64) -> u64 {
if value == usize::MAX {
default
} else {
value as u64
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct EnforcedLimits {
pub byte_limit: u64,
pub collection_limit: u64,
pub depth_limit: usize,
}
#[doc(hidden)]
pub const fn derive_enforced_limits(
budget: Budget,
statically_bounded: bool,
per_element_ceiling: u64,
) -> EnforcedLimits {
let byte_limit = if budget.max_input < budget.max_work {
budget.max_input
} else {
budget.max_work
};
let collection_limit = if statically_bounded {
byte_limit
} else {
match budget.max_alloc.checked_div(per_element_ceiling) {
Some(raw) if raw < byte_limit => raw,
_ => byte_limit,
}
};
let depth_limit = if budget.max_depth < MAX_DEPTH {
budget.max_depth
} else {
MAX_DEPTH
};
EnforcedLimits {
byte_limit,
collection_limit,
depth_limit,
}
}
pub fn decode_bounded<'de, T>(input: &'de [u8], budget: Budget) -> Result<Decoded<T>, DecodeError>
where
T: DecodeBounded + for<'a> nextjson::NsonDeserialize<'a>,
{
let input_len = u64::try_from(input.len()).map_err(|_| {
DecodeError::Budget(BudgetExceeded::Input {
limit: budget.max_input,
})
})?;
if input_len > budget.max_input {
return Err(DecodeError::Budget(BudgetExceeded::Input {
limit: budget.max_input,
}));
}
if T::MAX_DEPTH != usize::MAX && T::MAX_DEPTH > budget.max_depth {
return Err(DecodeError::Budget(BudgetExceeded::Depth {
limit: budget.max_depth,
}));
}
if T::MAX_WORK != usize::MAX && T::MAX_WORK as u64 > budget.max_work {
return Err(DecodeError::Budget(BudgetExceeded::Work {
limit: budget.max_work,
}));
}
let structural = if T::MAX_STRUCTURAL_ELEMENT == usize::MAX {
budget.element_structure_bytes()
} else {
T::MAX_STRUCTURAL_ELEMENT as u64
};
let depth_eff = if T::MAX_DEPTH == usize::MAX {
budget.max_depth as u64
} else {
T::MAX_DEPTH as u64
};
let depth_eff = if depth_eff == 0 { 1 } else { depth_eff };
let per_element_ceiling = structural.saturating_mul(depth_eff);
let limits = derive_enforced_limits(budget, T::STATICALLY_BOUNDED, per_element_ceiling);
let config = Config::standard()
.with_limit(limits.byte_limit)
.with_collection_limit(limits.collection_limit)
.with_depth_limit(limits.depth_limit);
let (value, consumed) =
decoder::from_slice_with_consumed(input, config).map_err(DecodeError::Codec)?;
if consumed != input.len() {
return Err(DecodeError::Codec(crate::Error::TrailingBytes {
remaining: input.len() - consumed,
}));
}
let read = consumed as u64;
let depth_bound = T::MAX_DEPTH.min(limits.depth_limit);
let (alloc_bound, work_bound) = if T::STATICALLY_BOUNDED {
(T::MAX_ALLOC as u64, T::MAX_WORK as u64)
} else {
(
limits
.byte_limit
.saturating_add(limits.collection_limit.saturating_mul(per_element_ceiling)),
read,
)
};
Ok(Decoded {
value,
use_: ResourceUse {
read,
alloc_bound,
depth_bound,
work_bound,
},
})
}
macro_rules! primitive {
($($ty:ty => $input:expr),+ $(,)?) => {$(
impl DecodeBounded for $ty {
const MAX_INPUT: usize = $input;
const MAX_ALLOC: usize = 0;
const MAX_DEPTH: usize = 0;
const MAX_WORK: usize = $input + 1;
const MAX_STRUCTURAL_ELEMENT: usize = 0;
}
)+};
}
primitive! {
() => 1, bool => 1, char => 13,
i8 => 9, u8 => 9,
i16 => 9, u16 => 9,
i32 => 9, u32 => 9,
i64 => 10, u64 => 10,
i128 => 18, u128 => 18,
f32 => 5, f64 => 9
}
impl<T: DecodeBounded> DecodeBounded for Option<T> {
const MAX_INPUT: usize = max(1, T::MAX_INPUT);
const MAX_ALLOC: usize = T::MAX_ALLOC;
const MAX_DEPTH: usize = T::MAX_DEPTH;
const MAX_WORK: usize = saturating_add(1, T::MAX_WORK);
const MAX_STRUCTURAL_ELEMENT: usize = T::MAX_STRUCTURAL_ELEMENT;
}
impl<T: DecodeBounded, const N: usize> DecodeBounded for [T; N] {
const MAX_INPUT: usize = saturating_add(saturating_mul(T::MAX_INPUT, N), 2);
const MAX_ALLOC: usize = saturating_mul(T::MAX_ALLOC, N);
const MAX_DEPTH: usize = depth_plus_one(T::MAX_DEPTH);
const MAX_WORK: usize = saturating_add(saturating_mul(T::MAX_WORK, N), 2);
const MAX_STRUCTURAL_ELEMENT: usize = T::MAX_STRUCTURAL_ELEMENT;
}
impl<T> DecodeBounded for PhantomData<T> {
const MAX_INPUT: usize = 0;
const MAX_ALLOC: usize = 0;
const MAX_DEPTH: usize = 0;
const MAX_WORK: usize = 0;
const MAX_STRUCTURAL_ELEMENT: usize = 0;
}
macro_rules! max_depth {
($a:expr) => {
$a
};
($a:expr, $($rest:expr),+) => {
crate::bounded::max($a, max_depth!($($rest),+))
};
}
macro_rules! tuple_bounded {
($($name:ident),+) => {
impl<$($name: DecodeBounded),+> DecodeBounded for ($($name,)+) {
const MAX_INPUT: usize = 2usize $(.saturating_add($name::MAX_INPUT))+;
const MAX_ALLOC: usize = 0usize $(.saturating_add($name::MAX_ALLOC))+;
const MAX_DEPTH: usize = depth_plus_one(max_depth!($($name::MAX_DEPTH),+));
const MAX_WORK: usize = 2usize $(.saturating_add($name::MAX_WORK))+;
const MAX_STRUCTURAL_ELEMENT: usize = max_depth!($($name::MAX_STRUCTURAL_ELEMENT),+);
}
};
}
tuple_bounded!(A);
tuple_bounded!(A, B);
tuple_bounded!(A, B, C);
tuple_bounded!(A, B, C, D);
tuple_bounded!(A, B, C, D, E);
tuple_bounded!(A, B, C, D, E, F);
tuple_bounded!(A, B, C, D, E, F, G);
tuple_bounded!(A, B, C, D, E, F, G, H);
#[cfg(feature = "alloc")]
impl DecodeBounded for String {
const MAX_INPUT: usize = usize::MAX;
const MAX_ALLOC: usize = usize::MAX;
const MAX_DEPTH: usize = 1;
const MAX_WORK: usize = usize::MAX;
const MAX_STRUCTURAL_ELEMENT: usize = 0;
}
#[cfg(feature = "alloc")]
impl<T: DecodeBounded> DecodeBounded for Vec<T> {
const MAX_INPUT: usize = usize::MAX;
const MAX_ALLOC: usize = usize::MAX;
const MAX_DEPTH: usize = depth_plus_one(T::MAX_DEPTH);
const MAX_WORK: usize = usize::MAX;
const MAX_STRUCTURAL_ELEMENT: usize = max(core::mem::size_of::<T>(), T::MAX_STRUCTURAL_ELEMENT);
}
#[cfg(feature = "alloc")]
impl<T: DecodeBounded> DecodeBounded for Box<T> {
const MAX_INPUT: usize = T::MAX_INPUT;
const MAX_ALLOC: usize = saturating_add(T::MAX_ALLOC, core::mem::size_of::<T>());
const MAX_DEPTH: usize = T::MAX_DEPTH;
const MAX_WORK: usize = T::MAX_WORK;
const MAX_STRUCTURAL_ELEMENT: usize = max(core::mem::size_of::<T>(), T::MAX_STRUCTURAL_ELEMENT);
}
impl DecodeBounded for &str {
const MAX_INPUT: usize = usize::MAX;
const MAX_ALLOC: usize = 0;
const MAX_DEPTH: usize = 0;
const MAX_WORK: usize = usize::MAX;
const MAX_STRUCTURAL_ELEMENT: usize = 0;
}
impl<T> DecodeBounded for &[T] {
const MAX_INPUT: usize = usize::MAX;
const MAX_ALLOC: usize = 0;
const MAX_DEPTH: usize = 1;
const MAX_WORK: usize = usize::MAX;
const MAX_STRUCTURAL_ELEMENT: usize = 0;
}
#[doc(hidden)]
pub const fn saturating_add(left: usize, right: usize) -> usize {
left.saturating_add(right)
}
#[doc(hidden)]
pub const fn saturating_mul(left: usize, right: usize) -> usize {
left.saturating_mul(right)
}
#[doc(hidden)]
pub const fn max(left: usize, right: usize) -> usize {
if left > right {
left
} else {
right
}
}
#[doc(hidden)]
pub const fn depth_plus_one(depth: usize) -> usize {
if depth == usize::MAX {
usize::MAX
} else {
depth + 1
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Error;
#[derive(
Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize, crate::DecodeBounded,
)]
struct StaticRecord {
id: u64,
enabled: bool,
coordinates: [i32; 2],
}
#[derive(
Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize, crate::DecodeBounded,
)]
struct DynamicRecord {
id: u64,
name: String,
tags: Vec<u8>,
}
fn static_value() -> StaticRecord {
StaticRecord {
id: 7,
enabled: true,
coordinates: [1, -2],
}
}
#[test]
fn static_type_decode_reports_exact_bounds() {
let value = static_value();
let bytes = crate::options().serialize(&value).unwrap();
let budget = Budget::from_type::<StaticRecord>();
assert_eq!(budget.max_input(), StaticRecord::MAX_INPUT as u64);
assert!(budget.max_input() >= bytes.len() as u64);
assert_eq!(budget.max_alloc(), 0);
assert_eq!(budget.max_depth(), 2);
let decoded = decode_bounded::<StaticRecord>(&bytes, budget).unwrap();
assert_eq!(decoded.value, value);
assert_eq!(decoded.use_.read as usize, bytes.len());
assert_eq!(decoded.use_.alloc_bound, 0);
assert_eq!(decoded.use_.depth_bound, 2);
assert_eq!(decoded.use_.work_bound, StaticRecord::MAX_WORK as u64);
const {
assert!(StaticRecord::STATICALLY_BOUNDED);
};
const {
assert!(!DynamicRecord::STATICALLY_BOUNDED);
};
}
#[test]
fn static_type_input_budget_is_enforced() {
let value = static_value();
let bytes = crate::options().serialize(&value).unwrap();
let budget = Budget::from_type::<StaticRecord>().with_max_input(3);
assert!(matches!(
decode_bounded::<StaticRecord>(&bytes, budget),
Err(DecodeError::Budget(BudgetExceeded::Input { limit: 3 }))
));
let budget = Budget::from_type::<StaticRecord>().with_max_depth(0);
assert!(matches!(
decode_bounded::<StaticRecord>(&bytes, budget),
Err(DecodeError::Budget(BudgetExceeded::Depth { limit: 0 }))
));
}
#[test]
fn dynamic_type_decode_is_budget_checked() {
let value = DynamicRecord {
id: 1,
name: "hello".to_owned(),
tags: vec![1, 2, 3],
};
let bytes = crate::options().serialize(&value).unwrap();
let budget = Budget::default();
let decoded = decode_bounded::<DynamicRecord>(&bytes, budget).unwrap();
assert_eq!(decoded.value, value);
assert_eq!(decoded.use_.read as usize, bytes.len());
assert!(decoded.use_.alloc_bound >= bytes.len() as u64);
assert!(decoded.use_.work_bound >= bytes.len() as u64);
let tight = budget.with_max_input((bytes.len() - 2) as u64);
assert!(matches!(
decode_bounded::<DynamicRecord>(&bytes, tight),
Err(DecodeError::Budget(BudgetExceeded::Input { .. }))
));
}
#[test]
fn dynamic_collections_respect_alloc_budget() {
let value = DynamicRecord {
id: 1,
name: String::new(),
tags: vec![0u8; 100],
};
let bytes = crate::options().serialize(&value).unwrap();
let budget = Budget::default().with_max_alloc(0);
assert!(matches!(
decode_bounded::<DynamicRecord>(&bytes, budget),
Err(DecodeError::Codec(Error::CollectionLimit { limit: 0 }))
));
}
#[test]
fn depth_budget_caps_nesting_for_dynamic_types() {
let value: Vec<Vec<u8>> = vec![vec![1, 2], vec![3]];
let bytes = crate::options().serialize(&value).unwrap();
assert_eq!(<Vec<Vec<u8>> as DecodeBounded>::MAX_DEPTH, 2);
let ok = Budget::default().with_max_depth(2);
let decoded = decode_bounded::<Vec<Vec<u8>>>(&bytes, ok).unwrap();
assert_eq!(decoded.use_.depth_bound, 2);
let too_shallow = Budget::default().with_max_depth(1);
assert!(matches!(
decode_bounded::<Vec<Vec<u8>>>(&bytes, too_shallow),
Err(DecodeError::Budget(BudgetExceeded::Depth { limit: 1 }))
));
}
#[test]
fn trailing_bytes_are_rejected() {
let value = 42u8;
let mut bytes = crate::options().serialize(&value).unwrap();
bytes.push(0);
assert!(matches!(
decode_bounded::<u8>(&bytes, Budget::default()),
Err(DecodeError::Codec(Error::TrailingBytes { remaining: 1 }))
));
}
#[test]
fn budget_from_config_matches_config_limits() {
let config = crate::options()
.with_limit(4096)
.with_collection_limit(128)
.with_depth_limit(8);
let budget = Budget::from_config(config);
assert_eq!(budget.max_input(), 4096);
assert_eq!(budget.max_alloc(), 128 * ELEMENT_STRUCTURE_BYTES);
assert_eq!(budget.max_depth(), 8);
assert_eq!(budget.max_work(), 4096);
}
#[test]
fn algebra_matches_wire_shapes() {
assert_eq!(<() as DecodeBounded>::MAX_INPUT, 1);
assert_eq!(<() as DecodeBounded>::MAX_DEPTH, 0);
assert_eq!(<Option<u8> as DecodeBounded>::MAX_INPUT, 9);
assert_eq!(<Option<u8> as DecodeBounded>::MAX_DEPTH, 0);
assert_eq!(<[u16; 4] as DecodeBounded>::MAX_INPUT, 2 + 4 * 9);
assert_eq!(<[u16; 4] as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(<(u8, bool) as DecodeBounded>::MAX_INPUT, 2 + 9 + 1);
assert_eq!(<(u8, bool) as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(<Vec<u8> as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(<Vec<Vec<u8>> as DecodeBounded>::MAX_DEPTH, 2);
assert_eq!(<String as DecodeBounded>::MAX_INPUT, usize::MAX);
assert_eq!(<String as DecodeBounded>::MAX_ALLOC, usize::MAX);
assert_eq!(<&str as DecodeBounded>::MAX_ALLOC, 0);
assert_eq!(<&str as DecodeBounded>::MAX_INPUT, usize::MAX);
assert_eq!(
<Box<u64> as DecodeBounded>::MAX_ALLOC,
core::mem::size_of::<u64>()
);
assert_eq!(<Box<u64> as DecodeBounded>::MAX_DEPTH, 0);
assert_eq!(<Option<Box<u64>> as DecodeBounded>::MAX_DEPTH, 0);
}
#[test]
fn structural_element_bounds_match_collection_shapes() {
assert_eq!(<String as DecodeBounded>::MAX_STRUCTURAL_ELEMENT, 0);
assert_eq!(
<Vec<u8> as DecodeBounded>::MAX_STRUCTURAL_ELEMENT,
core::mem::size_of::<u8>()
);
assert_eq!(
<Vec<String> as DecodeBounded>::MAX_STRUCTURAL_ELEMENT,
core::mem::size_of::<String>()
);
assert_eq!(
<DynamicRecord as DecodeBounded>::MAX_STRUCTURAL_ELEMENT,
core::mem::size_of::<u8>()
);
assert_eq!(
<Box<u64> as DecodeBounded>::MAX_STRUCTURAL_ELEMENT,
core::mem::size_of::<u64>()
);
assert_eq!(<StaticRecord as DecodeBounded>::MAX_STRUCTURAL_ELEMENT, 0);
}
#[test]
fn string_collection_allocation_is_covered_by_the_bound() {
let value: Vec<String> = (0..200).map(|i| format!("s{i}")).collect();
let bytes = crate::options().serialize(&value).unwrap();
let budget = Budget::default();
let decoded = decode_bounded::<Vec<String>>(&bytes, budget).unwrap();
assert_eq!(decoded.use_.read as usize, bytes.len());
let true_alloc = 200 * core::mem::size_of::<String>() + 600;
assert!(
decoded.use_.alloc_bound as usize >= true_alloc,
"alloc_bound {} must cover the real allocation {}",
decoded.use_.alloc_bound,
true_alloc
);
assert!(
budget.element_structure_bytes()
>= <Vec<String> as DecodeBounded>::MAX_STRUCTURAL_ELEMENT as u64
);
}
fn assert_primitive<T: DecodeBounded>(b: usize) {
assert_eq!(T::MAX_INPUT, b, "B");
assert_eq!(T::MAX_DEPTH, 0, "D");
assert_eq!(T::MAX_ALLOC, 0, "A");
assert_eq!(T::MAX_STRUCTURAL_ELEMENT, 0, "S");
assert_eq!(T::MAX_WORK, b + 1, "W");
assert!(T::STATICALLY_BOUNDED);
}
#[test]
fn primitive_algebra_is_exact() {
assert_primitive::<()>(1);
assert_primitive::<bool>(1);
assert_primitive::<char>(13);
assert_primitive::<i8>(9);
assert_primitive::<u8>(9);
assert_primitive::<i16>(9);
assert_primitive::<u16>(9);
assert_primitive::<i32>(9);
assert_primitive::<u32>(9);
assert_primitive::<i64>(10);
assert_primitive::<u64>(10);
assert_primitive::<i128>(18);
assert_primitive::<u128>(18);
assert_primitive::<f32>(5);
assert_primitive::<f64>(9);
}
#[test]
fn option_array_tuple_algebra_is_exact() {
assert_eq!(<Option<u8> as DecodeBounded>::MAX_INPUT, 9);
assert_eq!(<Option<u8> as DecodeBounded>::MAX_DEPTH, 0);
assert_eq!(<Option<Vec<u8>> as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(
<Option<Vec<u8>> as DecodeBounded>::MAX_STRUCTURAL_ELEMENT,
1
);
assert_eq!(<Option<String> as DecodeBounded>::MAX_ALLOC, usize::MAX);
assert_eq!(<Option<String> as DecodeBounded>::MAX_STRUCTURAL_ELEMENT, 0);
assert_eq!(<[u8; 0] as DecodeBounded>::MAX_INPUT, 2);
assert_eq!(<[u8; 0] as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(<[u8; 3] as DecodeBounded>::MAX_INPUT, 2 + 3 * 9);
assert_eq!(<[u8; 3] as DecodeBounded>::MAX_WORK, 2 + 3 * 10);
assert_eq!(<[[u8; 2]; 3] as DecodeBounded>::MAX_DEPTH, 2);
assert_eq!(<(u8, bool) as DecodeBounded>::MAX_INPUT, 2 + 9 + 1);
assert_eq!(<(u8, bool) as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(
<(u8, u16, u32, u64, u128, i8, i16, i32) as DecodeBounded>::MAX_INPUT,
2 + 9 + 9 + 9 + 10 + 18 + 9 + 9 + 9
);
assert_eq!(<PhantomData<u64> as DecodeBounded>::MAX_INPUT, 0);
assert_eq!(<PhantomData<u64> as DecodeBounded>::MAX_DEPTH, 0);
assert_eq!(<PhantomData<u64> as DecodeBounded>::MAX_ALLOC, 0);
assert_eq!(<PhantomData<u64> as DecodeBounded>::MAX_WORK, 0);
}
#[derive(
Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize, crate::DecodeBounded,
)]
struct NestedRecord {
header: StaticRecord,
items: [u8; 3],
maybe: Option<i64>,
}
#[derive(
Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize, crate::DecodeBounded,
)]
enum StaticEnum {
A,
B(u8),
C { x: i32, y: i32 },
}
#[derive(
Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize, crate::DecodeBounded,
)]
enum ShapeEnum {
Unit,
Newtype(u64),
Tuple(u8, bool),
Named { code: u16, label: String },
}
#[test]
fn derive_algebra_nested_struct_matches_hand_computation() {
assert_eq!(
NestedRecord::MAX_INPUT,
2 + (80 + 15) + (29 + 14) + (10 + 14)
);
assert_eq!(
NestedRecord::MAX_WORK,
2 + (84 + 15) + (32 + 14) + (12 + 14)
);
assert_eq!(NestedRecord::MAX_DEPTH, 3);
assert_eq!(NestedRecord::MAX_ALLOC, 0);
assert_eq!(NestedRecord::MAX_STRUCTURAL_ELEMENT, 0);
const {
assert!(NestedRecord::STATICALLY_BOUNDED);
};
}
#[test]
fn derive_algebra_enums_match_hand_computation() {
assert_eq!(StaticEnum::MAX_INPUT, 52);
assert_eq!(StaticEnum::MAX_WORK, 54);
assert_eq!(StaticEnum::MAX_DEPTH, 2);
assert_eq!(StaticEnum::MAX_ALLOC, 0);
assert_eq!(StaticEnum::MAX_STRUCTURAL_ELEMENT, 0);
const {
assert!(StaticEnum::STATICALLY_BOUNDED);
};
assert_eq!(ShapeEnum::MAX_INPUT, usize::MAX);
assert_eq!(ShapeEnum::MAX_ALLOC, usize::MAX);
assert_eq!(ShapeEnum::MAX_WORK, usize::MAX);
assert_eq!(ShapeEnum::MAX_DEPTH, 3);
assert_eq!(ShapeEnum::MAX_STRUCTURAL_ELEMENT, 0);
const {
assert!(!ShapeEnum::STATICALLY_BOUNDED);
};
}
#[test]
fn decode_bounded_respects_derived_static_algebra() {
let value = NestedRecord {
header: static_value(),
items: [1, 2, 3],
maybe: Some(-5),
};
let bytes = crate::options().serialize(&value).unwrap();
let budget = Budget::from_type::<NestedRecord>();
assert_eq!(budget.max_input(), NestedRecord::MAX_INPUT as u64);
assert_eq!(budget.max_alloc(), 0);
assert_eq!(budget.max_depth(), 3);
let decoded = decode_bounded::<NestedRecord>(&bytes, budget).unwrap();
assert_eq!(decoded.value, value);
assert_eq!(decoded.use_.read as usize, bytes.len());
assert!(decoded.use_.read <= NestedRecord::MAX_INPUT as u64);
assert_eq!(decoded.use_.alloc_bound, 0);
assert_eq!(decoded.use_.depth_bound, 3);
assert!(decoded.use_.work_bound <= NestedRecord::MAX_WORK as u64);
}
#[test]
fn decode_bounded_respects_derived_enum_algebra() {
for value in [
StaticEnum::A,
StaticEnum::B(7),
StaticEnum::C { x: -1, y: 2 },
] {
let bytes = crate::options().serialize(&value).unwrap();
let decoded =
decode_bounded::<StaticEnum>(&bytes, Budget::from_type::<StaticEnum>()).unwrap();
assert_eq!(decoded.value, value);
assert_eq!(decoded.use_.alloc_bound, 0);
assert_eq!(decoded.use_.depth_bound, 2);
assert!(decoded.use_.read <= StaticEnum::MAX_INPUT as u64);
}
for value in [
ShapeEnum::Unit,
ShapeEnum::Newtype(u64::MAX),
ShapeEnum::Tuple(0, true),
ShapeEnum::Named {
code: 9,
label: "x".into(),
},
] {
let bytes = crate::options().serialize(&value).unwrap();
let decoded = decode_bounded::<ShapeEnum>(&bytes, Budget::default()).unwrap();
assert_eq!(decoded.value, value);
assert_eq!(decoded.use_.depth_bound, 3);
}
}
#[test]
fn input_budget_boundary_is_exact() {
let value: Vec<u8> = vec![1, 2, 3];
let bytes = crate::options().serialize(&value).unwrap();
let exact = Budget::default().with_max_input(bytes.len() as u64);
assert!(decode_bounded::<Vec<u8>>(&bytes, exact).is_ok());
let tight = Budget::default().with_max_input(bytes.len() as u64 - 1);
assert!(matches!(
decode_bounded::<Vec<u8>>(&bytes, tight),
Err(DecodeError::Budget(BudgetExceeded::Input { .. }))
));
}
#[test]
fn work_budget_boundary_is_exact() {
let value = static_value();
let bytes = crate::options().serialize(&value).unwrap();
let exact =
Budget::from_type::<StaticRecord>().with_max_work(StaticRecord::MAX_WORK as u64);
assert!(decode_bounded::<StaticRecord>(&bytes, exact).is_ok());
let tight =
Budget::from_type::<StaticRecord>().with_max_work(StaticRecord::MAX_WORK as u64 - 1);
assert!(matches!(
decode_bounded::<StaticRecord>(&bytes, tight),
Err(DecodeError::Budget(BudgetExceeded::Work { .. }))
));
let dyn_value: Vec<u64> = (0..16).collect();
let dyn_bytes = crate::options().serialize(&dyn_value).unwrap();
let len = dyn_bytes.len() as u64;
let exact = Budget::default().with_max_work(len);
let decoded = decode_bounded::<Vec<u64>>(&dyn_bytes, exact).unwrap();
assert_eq!(decoded.use_.read, len);
assert!(decoded.use_.work_bound >= len);
let tight = Budget::default().with_max_work(len - 1);
let result = decode_bounded::<Vec<u64>>(&dyn_bytes, tight);
assert!(
result.is_err(),
"a work budget below the frame size ({len}) must fail; got {:?}",
result.map(|d| d.use_)
);
}
#[test]
fn alloc_budget_boundary_is_exact() {
let value: Vec<u8> = vec![0u8; 100];
let bytes = crate::options().serialize(&value).unwrap();
let tight = Budget::default()
.with_max_alloc(99)
.with_max_input(bytes.len() as u64);
assert!(matches!(
decode_bounded::<Vec<u8>>(&bytes, tight),
Err(DecodeError::Codec(Error::CollectionLimit { limit: 99 }))
));
let exact = Budget::default()
.with_max_alloc(100)
.with_max_input(bytes.len() as u64);
assert!(decode_bounded::<Vec<u8>>(&bytes, exact).is_ok());
}
#[test]
fn element_structure_knob_falls_back_when_structural_unknown() {
let budget = Budget::default()
.with_max_alloc(256)
.with_element_structure_bytes(1);
let limits = derive_enforced_limits(budget, false, budget.element_structure_bytes());
assert_eq!(limits.collection_limit, 256);
let budget = budget.with_element_structure_bytes(64);
let limits = derive_enforced_limits(budget, false, budget.element_structure_bytes());
assert_eq!(limits.collection_limit, 4);
let budget = budget.with_element_structure_bytes(0);
let limits = derive_enforced_limits(budget, false, budget.element_structure_bytes());
assert_eq!(limits.collection_limit, limits.byte_limit);
}
#[test]
fn nested_collection_alloc_bounds_are_sound() {
let value: Vec<Vec<u8>> = vec![vec![1, 2], vec![3, 4, 5]];
let bytes = crate::options().serialize(&value).unwrap();
let decoded = decode_bounded::<Vec<Vec<u8>>>(&bytes, Budget::default()).unwrap();
let true_alloc = 2 * core::mem::size_of::<Vec<u8>>() + 5;
assert!(decoded.use_.alloc_bound as usize >= true_alloc);
assert_eq!(decoded.use_.depth_bound, 2);
let boxed: Vec<Box<u64>> = vec![Box::new(1), Box::new(2)];
let bytes = crate::options().serialize(&boxed).unwrap();
let decoded = decode_bounded::<Vec<Box<u64>>>(&bytes, Budget::default()).unwrap();
let true_alloc = 2 * core::mem::size_of::<Box<u64>>() + 2 * core::mem::size_of::<u64>();
assert!(decoded.use_.alloc_bound as usize >= true_alloc);
}
#[test]
fn decode_bounded_matches_plain_deserialize() {
let values: Vec<DynamicRecord> = (0..20)
.map(|i| DynamicRecord {
id: i,
name: format!("name{i}"),
tags: (0..i % 5).map(|j| j as u8).collect(),
})
.collect();
let bytes = crate::options().serialize(&values).unwrap();
let budget = Budget::default()
.with_max_input(bytes.len() as u64)
.with_max_alloc(1 << 20);
let decoded = decode_bounded::<Vec<DynamicRecord>>(&bytes, budget).unwrap();
assert_eq!(decoded.value, values);
assert_eq!(decoded.use_.read as usize, bytes.len());
let plain: Vec<DynamicRecord> = crate::options().deserialize(&bytes).unwrap();
assert_eq!(plain, values);
}
#[test]
fn budget_builder_setters_and_accessors_roundtrip() {
let budget = Budget::new(1, 2, 3, 4)
.with_max_input(10)
.with_max_alloc(20)
.with_max_depth(30)
.with_max_work(40)
.with_element_structure_bytes(7);
assert_eq!(budget.max_input(), 10);
assert_eq!(budget.max_alloc(), 20);
assert_eq!(budget.max_depth(), 30);
assert_eq!(budget.max_work(), 40);
assert_eq!(budget.element_structure_bytes(), 7);
let default = Budget::default();
assert_eq!(default.max_input(), DEFAULT_SIZE_LIMIT);
assert_eq!(default.max_work(), DEFAULT_SIZE_LIMIT);
assert_eq!(default.max_depth(), MAX_DEPTH);
assert_eq!(default.element_structure_bytes(), ELEMENT_STRUCTURE_BYTES);
assert_eq!(
default.max_alloc(),
DEFAULT_COLLECTION_LIMIT * ELEMENT_STRUCTURE_BYTES
);
}
#[test]
fn deeply_nested_depth_algebra_is_exact() {
assert_eq!(<Vec<u8> as DecodeBounded>::MAX_DEPTH, 1);
assert_eq!(<Vec<Vec<u8>> as DecodeBounded>::MAX_DEPTH, 2);
assert_eq!(<Vec<Vec<Vec<u8>>> as DecodeBounded>::MAX_DEPTH, 3);
assert_eq!(
<Vec<Vec<Vec<Vec<Vec<Vec<u8>>>>>> as DecodeBounded>::MAX_DEPTH,
6
);
assert_eq!(NestedRecord::MAX_DEPTH, 3);
assert_eq!(<Option<StaticRecord> as DecodeBounded>::MAX_DEPTH, 2);
}
#[test]
fn static_boundedness_flags_are_correct() {
const {
assert!(StaticRecord::STATICALLY_BOUNDED);
};
const {
assert!(NestedRecord::STATICALLY_BOUNDED);
};
const {
assert!(StaticEnum::STATICALLY_BOUNDED);
};
const {
assert!(!DynamicRecord::STATICALLY_BOUNDED);
};
const {
assert!(!ShapeEnum::STATICALLY_BOUNDED);
};
const {
assert!(!<Vec<u8>>::STATICALLY_BOUNDED);
};
const {
assert!(<Option<u8>>::STATICALLY_BOUNDED);
};
const {
assert!(<[u8; 4]>::STATICALLY_BOUNDED);
};
}
}