use crate::canonical::decode_varint_le;
use crate::config::{Config, IntEncoding, TrailingBytes};
use crate::error::{Error, Result};
use crate::tags::{
MARKER_U128, MARKER_U16, MARKER_U32, MARKER_U64, TAG_ARRAY, TAG_END, TAG_F32, TAG_F64,
TAG_FALSE, TAG_I128, TAG_I64, TAG_NULL, TAG_OBJECT, TAG_STRING, TAG_TRUE, TAG_U128, TAG_U64,
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Probe {
bytes: usize,
containers: u64,
elements: u64,
depth: usize,
}
impl Probe {
pub const fn bytes(self) -> usize {
self.bytes
}
pub const fn containers(self) -> u64 {
self.containers
}
pub const fn elements(self) -> u64 {
self.elements
}
pub const fn depth(self) -> usize {
self.depth
}
pub const fn fits_input(self, max_bytes: u64) -> bool {
(self.bytes as u64) <= max_bytes
}
pub const fn fits_depth(self, max_depth: usize) -> bool {
self.depth <= max_depth
}
#[cfg(feature = "bounded")]
pub const fn fits_budget(self, budget: crate::bounded::Budget) -> bool {
(self.bytes as u64) <= budget.max_input()
&& (self.bytes as u64) <= budget.max_work()
&& self.depth <= budget.max_depth()
}
}
pub fn probe(config: Config, input: &[u8]) -> Result<Probe> {
let mut walker = Walker {
input,
cursor: 0,
config,
containers: 0,
elements: 0,
depth: 0,
max_depth: 0,
};
walker.value()?;
if config.trailing == TrailingBytes::Reject && walker.cursor != input.len() {
return Err(Error::TrailingBytes {
remaining: input.len() - walker.cursor,
});
}
Ok(Probe {
bytes: walker.cursor,
containers: walker.containers,
elements: walker.elements,
depth: walker.max_depth,
})
}
struct Walker<'a> {
input: &'a [u8],
cursor: usize,
config: Config,
containers: u64,
elements: u64,
depth: usize,
max_depth: usize,
}
impl<'a> Walker<'a> {
fn take(&mut self, len: usize) -> Result<()> {
let end = self.cursor.checked_add(len).ok_or(Error::UnexpectedEnd)?;
if let Some(limit) = self.config.limit {
if end as u64 > limit {
return Err(Error::SizeLimit { limit });
}
}
if end > self.input.len() {
return Err(Error::UnexpectedEnd);
}
self.cursor = end;
Ok(())
}
fn byte(&mut self) -> Result<u8> {
self.take(1)?;
Ok(self.input[self.cursor - 1])
}
fn peek(&self) -> Result<u8> {
self.input
.get(self.cursor)
.copied()
.ok_or(Error::UnexpectedEnd)
}
fn varint(&mut self) -> Result<u128> {
let marker = self.byte()?;
if marker <= 250 {
return Ok(marker as u128);
}
let payload_len = match marker {
MARKER_U16 => 2,
MARKER_U32 => 4,
MARKER_U64 => 8,
MARKER_U128 => 16,
other => return Err(Error::InvalidVarintMarker(other)),
};
let end = self
.cursor
.checked_add(payload_len)
.ok_or(Error::UnexpectedEnd)?;
if let Some(limit) = self.config.limit {
if end as u64 > limit {
return Err(Error::SizeLimit { limit });
}
}
let bytes = self
.input
.get(self.cursor..end)
.ok_or(Error::UnexpectedEnd)?;
self.cursor = end;
match decode_varint_le(marker, bytes) {
Some(value) => Ok(value),
None => Err(Error::NonCanonicalVarint),
}
}
fn number(&mut self, fixed_bytes: usize) -> Result<()> {
if self.config.integers == IntEncoding::Variable && fixed_bytes > 1 {
self.varint().map(|_| ())
} else {
self.take(fixed_bytes)
}
}
fn length(&mut self) -> Result<u64> {
if self.config.integers == IntEncoding::Variable {
u64::try_from(self.varint()?).map_err(|_| Error::IntegerOverflow { target: "u64" })
} else {
self.take(8)?;
Ok(0)
}
}
fn value(&mut self) -> Result<()> {
let tag = self.byte()?;
match tag {
TAG_NULL | TAG_FALSE | TAG_TRUE => Ok(()),
TAG_U64 | TAG_I64 => self.number(8),
TAG_U128 | TAG_I128 => self.number(16),
TAG_F64 => self.take(8),
TAG_F32 => self.take(4),
TAG_STRING => {
let len = usize::try_from(self.length()?)
.map_err(|_| Error::IntegerOverflow { target: "usize" })?;
self.take(len)
}
TAG_ARRAY => self.container(false),
TAG_OBJECT => self.container(true),
TAG_END => Err(Error::Custom(
"unexpected end-of-container terminator".into(),
)),
_ => Err(Error::Custom("invalid value tag".into())),
}
}
fn container(&mut self, is_object: bool) -> Result<()> {
self.containers += 1;
if self.depth >= self.config.depth_limit {
return Err(Error::Custom("decoder nesting depth limit exceeded".into()));
}
self.depth += 1;
if self.depth > self.max_depth {
self.max_depth = self.depth;
}
let mut count = 0u64;
loop {
let tag = self.peek()?;
if tag == TAG_END {
self.take(1)?;
break;
}
count += 1;
if let Some(limit) = self.config.collection_limit {
if count > limit {
return Err(Error::CollectionLimit { limit });
}
}
if is_object {
let key_tag = self.byte()?;
if key_tag != TAG_STRING {
return Err(Error::Custom("invalid object key".into()));
}
let key_len = usize::try_from(self.length()?)
.map_err(|_| Error::IntegerOverflow { target: "usize" })?;
self.take(key_len)?;
}
self.value()?;
}
self.elements += count;
self.depth -= 1;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::borrow::ToOwned;
use alloc::vec;
use alloc::vec::Vec;
fn value_for_scalars() -> Vec<nextjson::Value> {
use nextjson::{Number, Value};
vec![
Value::Null,
Value::Bool(true),
Value::Number(Number::U64(0)),
Value::Number(Number::U64(251)),
Value::Number(Number::U64(u64::MAX)),
Value::Number(Number::I64(-1)),
Value::Number(Number::F64(1.5)),
Value::String("hello".into()),
]
}
#[test]
fn probe_matches_encoder_byte_accounting() {
let config = crate::options();
for value in value_for_scalars() {
let bytes = config.serialize(&value).unwrap();
let probe = probe(config, &bytes).unwrap();
assert_eq!(probe.bytes(), bytes.len(), "byte accounting for {value:?}");
assert_eq!(probe.containers(), 0);
assert_eq!(probe.elements(), 0);
assert_eq!(probe.depth(), 0);
}
}
#[test]
fn probe_reports_nested_shape() {
use nextjson::{Number, Value};
let config = crate::options();
let object = Value::Object(
[("a".to_owned(), Value::Number(Number::U64(3)))]
.into_iter()
.collect(),
);
let frame = config
.serialize(&Value::Array(vec![
Value::Array(vec![
Value::Number(Number::U64(1)),
Value::Number(Number::U64(2)),
]),
object,
]))
.unwrap();
let probe = probe(config, &frame).unwrap();
assert_eq!(probe.bytes(), frame.len());
assert_eq!(probe.containers(), 3); assert_eq!(probe.elements(), 5); assert_eq!(probe.depth(), 2);
}
#[test]
fn probe_acceptance_matches_deserialize() {
let config = crate::options().with_limit(64).with_collection_limit(8);
let frame = config.serialize(&vec![1u64, 2, 3, 4]).unwrap();
for cut in 0..frame.len() {
let decoded = config.deserialize::<Vec<u64>>(&frame[..cut]);
let probed = probe(config, &frame[..cut]);
assert_eq!(
probed.is_ok(),
decoded.is_ok(),
"acceptance drift at cut {cut}"
);
if let Ok(probe) = probed {
assert_eq!(probe.bytes(), frame.len().min(cut));
}
}
assert_eq!(probe(config, &frame).unwrap().bytes(), frame.len());
}
#[test]
fn probe_enforces_limits_like_decode() {
let encoded = crate::options().serialize(&vec![1u64, 2, 3]).unwrap();
let config = crate::options().with_collection_limit(2);
assert!(matches!(
probe(config, &encoded),
Err(Error::CollectionLimit { limit: 2 })
));
assert!(matches!(
config.deserialize::<Vec<u64>>(&encoded),
Err(Error::CollectionLimit { limit: 2 })
));
let deep = crate::options().serialize(&vec![vec![1u64]]).unwrap();
let config = crate::options().with_depth_limit(1);
assert!(probe(config, &deep).is_err());
assert!(config.deserialize::<Vec<Vec<u64>>>(&deep).is_err());
let wide = crate::options().serialize(&vec![1u64; 4]).unwrap();
let config = crate::options().with_limit(8);
assert!(probe(config, &wide).is_err());
assert!(config.deserialize::<Vec<u64>>(&wide).is_err());
}
#[test]
fn probe_rejects_malformed_and_trailing() {
let config = crate::options();
assert!(matches!(
probe(config, &[TAG_U64, MARKER_U16, 5, 0]),
Err(Error::NonCanonicalVarint)
));
assert!(matches!(
probe(config, &[TAG_U64, 255]),
Err(Error::InvalidVarintMarker(255))
));
assert!(matches!(probe(config, &[TAG_END]), Err(Error::Custom(_))));
assert!(matches!(
probe(config, &[TAG_STRING, 5, b'a']),
Err(Error::UnexpectedEnd)
));
let mut frame = config.serialize(&7u64).unwrap();
frame.push(0);
assert!(matches!(
probe(config, &frame),
Err(Error::TrailingBytes { remaining: 1 })
));
assert_eq!(
probe(config.allow_trailing_bytes(), &frame)
.unwrap()
.bytes(),
config.serialize(&7u64).unwrap().len()
);
}
#[test]
fn probe_walks_large_frames_without_allocating() {
let config = crate::options();
let frame = config
.serialize(&(0..10_000u64).collect::<Vec<_>>())
.unwrap();
let probe = probe(config, &frame).unwrap();
assert_eq!(probe.bytes(), frame.len());
assert_eq!(probe.containers(), 1);
assert_eq!(probe.elements(), 10_000);
assert_eq!(probe.depth(), 1);
}
#[cfg(feature = "bounded")]
#[test]
fn probe_integrates_with_budget() {
use crate::bounded::Budget;
let config = crate::options();
let frame = config.serialize(&vec![1u64, 2, 3]).unwrap();
let probe = probe(config, &frame).unwrap();
assert!(probe.fits_budget(Budget::default()));
assert!(probe.fits_budget(Budget::default().with_max_input(frame.len() as u64)));
assert!(!probe.fits_budget(Budget::default().with_max_input(frame.len() as u64 - 1)));
assert!(!probe.fits_budget(Budget::default().with_max_depth(0)));
assert!(probe.fits_input(frame.len() as u64));
assert!(probe.fits_depth(1));
}
}