repositories / smith
smith
There are many coding harnesses - but this one is fast
owned by admin
smith-core/src/json_scan.rs
Raw//! Borrowed, bounded canonical CBOR traversal for arbitrary JSON values
//! (`SMH-SPEC-SPEC0001`, Sessions).
//! Rejects non-JSON types, non-finite numbers, non-canonical encodings,
//! overdeep containers and trailing bytes.
use crate::cbor::{Decoder, Header};
use crate::content_codec::JsonView;
use serde::ser::{Error as _, SerializeMap, SerializeSeq};
use serde::{Serialize, Serializer};
use smith::error::{Result, SmithError};
use smith::structured::{AccessError, Kind};
/// Maximum nesting for arbitrary JSON containers.
pub const JSON_DEPTH_MAX: u8 = 64;
/// Canonical CBOR text-map key order: encoded length, then UTF-8 bytes.
pub fn key_order(a: &str, b: &str) -> std::cmp::Ordering {
a.len()
.cmp(&b.len())
.then_with(|| a.as_bytes().cmp(b.as_bytes()))
}
fn invalid() -> SmithError {
SmithError::Session {
code: smith::error::SessionCode::EntryDecode,
message: "invalid, non-canonical, truncated or overdeep JSON CBOR".into(),
}
}
struct Cursor<'a> {
bytes: &'a [u8],
}
impl<'a> Cursor<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let (front, tail) = self.bytes.split_at_checked(n).ok_or_else(invalid)?;
self.bytes = tail;
Ok(front)
}
fn header(&mut self) -> Result<(Header, &'a [u8])> {
let mut decoder = Decoder::from(self.bytes);
let header = decoder.pull().map_err(|_| invalid())?;
let raw = self.take(decoder.offset())?;
Ok((header, raw))
}
fn text(&mut self, len: usize) -> Result<&'a str> {
std::str::from_utf8(self.take(len)?).map_err(|_| invalid())
}
fn map_key(&mut self, previous: Option<&str>) -> Result<&'a str> {
let (header, raw) = self.header()?;
let Header::Text(Some(len)) = header else {
return Err(invalid());
};
canonical(raw, len as u64)?;
let key = self.text(len)?;
if previous.is_some_and(|last| key_order(last, key).is_ge()) {
return Err(invalid());
}
Ok(key)
}
fn item_span(&mut self, depth: u8) -> Result<&'a [u8]> {
let start = self.bytes;
self.item(depth, None)?;
Ok(&start[..start.len() - self.bytes.len()])
}
fn item(&mut self, depth: u8, pattern: Option<&str>) -> Result<bool> {
let (header, raw) = self.header()?;
match header {
Header::Positive(value) => {
canonical(raw, value)?;
Ok(false)
}
Header::Negative(value) => {
canonical(raw, value)?;
i64::try_from(value).map_err(|_| invalid())?;
Ok(false)
}
Header::Text(Some(len)) => {
canonical(raw, len as u64)?;
let text = self.text(len)?;
Ok(pattern.is_some_and(|needle| text.contains(needle)))
}
Header::Array(Some(len)) | Header::Map(Some(len)) => {
canonical(raw, len as u64)?;
self.items(depth, pattern, len, matches!(header, Header::Map(..)))
}
Header::Float(value) => {
canonical_float(raw, value)?;
Ok(false)
}
Header::Simple(20..=22) if raw == [0xf4] || raw == [0xf5] || raw == [0xf6] => Ok(false),
_ => Err(invalid()),
}
}
fn items(&mut self, depth: u8, pattern: Option<&str>, len: usize, map: bool) -> Result<bool> {
if depth >= JSON_DEPTH_MAX || len > self.bytes.len() / if map { 2 } else { 1 } {
return Err(invalid());
}
let mut found = false;
let mut previous = None;
for _ in 0..len {
if map {
previous = Some(self.map_key(previous)?);
}
found |= self.item(depth + 1, pattern)?;
}
Ok(found)
}
fn serialize_item<S: Serializer>(
&mut self,
serializer: S,
depth: u8,
) -> std::result::Result<S::Ok, S::Error> {
let (header, raw) = self
.header()
.map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
match header {
Header::Positive(value) => {
canonical(raw, value).map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
serializer.serialize_u64(value)
}
Header::Negative(value) => {
canonical(raw, value).map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
let value =
i64::try_from(value).map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
serializer.serialize_i64(-1 - value)
}
Header::Text(Some(len)) => {
canonical(raw, len as u64).map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
serializer.serialize_str(
self.text(len)
.map_err(|_| S::Error::custom("invalid JSON CBOR"))?,
)
}
Header::Array(Some(len)) | Header::Map(Some(len)) => {
canonical(raw, len as u64).map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
if depth >= 64
|| len
> self.bytes.len()
/ if matches!(header, Header::Map(..)) {
2
} else {
1
}
{
return Err(S::Error::custom("invalid JSON CBOR"));
}
if matches!(header, Header::Array(..)) {
let mut sequence = serializer.serialize_seq(Some(len))?;
for _ in 0..len {
let value = self
.item_span(depth + 1)
.map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
sequence.serialize_element(&Value(value))?;
}
sequence.end()
} else {
let mut map = serializer.serialize_map(Some(len))?;
let mut previous = None;
for _ in 0..len {
let key = self
.map_key(previous)
.map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
previous = Some(key);
let value = self
.item_span(depth + 1)
.map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
map.serialize_entry(key, &Value(value))?;
}
map.end()
}
}
Header::Float(value) => {
canonical_float(raw, value).map_err(|_| S::Error::custom("invalid JSON CBOR"))?;
serializer.serialize_f64(value)
}
Header::Simple(20) if raw == [0xf4] => serializer.serialize_bool(false),
Header::Simple(21) if raw == [0xf5] => serializer.serialize_bool(true),
Header::Simple(22) if raw == [0xf6] => serializer.serialize_none(),
_ => Err(S::Error::custom("invalid JSON CBOR")),
}
}
}
fn canonical(raw: &[u8], value: u64) -> Result<()> {
let canonical = match raw.first().copied().map(|byte| byte & 31) {
Some(0..=23) => value <= 23,
Some(24) => value > 23 && u8::try_from(value).is_ok(),
Some(25) => value > u64::from(u8::MAX) && u16::try_from(value).is_ok(),
Some(26) => value > u64::from(u16::MAX) && u32::try_from(value).is_ok(),
Some(27) => value > u64::from(u32::MAX),
_ => false,
};
canonical.then_some(()).ok_or_else(invalid)
}
fn canonical_float(raw: &[u8], value: f64) -> Result<()> {
#[expect(
clippy::cast_possible_truncation,
reason = "canonical CBOR uses f32 only when this lossy conversion round-trips"
)]
let narrow = value as f32;
let canonical = match raw.first().copied().map(|byte| byte & 31) {
Some(25) => true,
Some(26) => !is_f16(value),
Some(27) => f64::from(narrow).to_bits() != value.to_bits(),
_ => false,
};
(canonical && value.is_finite())
.then_some(())
.ok_or_else(invalid)
}
pub fn is_f16(value: f64) -> bool {
if value == 0.0 {
return true;
}
let bits = value.to_bits();
let exponent = (bits >> 52) & 0x7ff;
if (1009..=1038).contains(&exponent) && bits & ((1_u64 << 42) - 1) == 0 {
return true;
}
let scaled = value.abs() * 16_777_216.0;
scaled <= 1023.0 && scaled.fract() == 0.0
}
struct Value<'a>(&'a [u8]);
impl Serialize for Value<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
serialize(self.0, serializer)
}
}
/// Validate one canonical JSON value without materializing it; optionally search string values.
///
/// # Errors
/// Returns `ENTRY_DECODE` on invalid CBOR, non-canonical or JSON-incompatible values,
/// excess depth, or trailing bytes.
pub fn scan(bytes: &[u8], pattern: Option<&str>) -> Result<bool> {
let mut cursor = Cursor { bytes };
let found = cursor.item(0, pattern)?;
if !cursor.bytes.is_empty() {
return Err(invalid());
}
Ok(found)
}
/// Serialize one validated canonical JSON CBOR value without materializing it.
pub fn serialize<S: Serializer>(
bytes: &[u8],
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
let mut cursor = Cursor { bytes };
let value = cursor.serialize_item(serializer, 0)?;
if cursor.bytes.is_empty() {
Ok(value)
} else {
Err(S::Error::custom("invalid JSON CBOR"))
}
}
/// Inspect a scalar or container tag in a validated resident span.
pub fn kind(bytes: &[u8]) -> Result<Kind<'_>> {
let mut cursor = Cursor { bytes };
match cursor.header()?.0 {
Header::Positive(value) => Ok(Kind::Unsigned(value)),
Header::Negative(value) => Ok(Kind::Signed(
-1 - i64::try_from(value).map_err(|_| invalid())?,
)),
Header::Float(value) => Ok(Kind::Float(value)),
Header::Text(Some(len)) => Ok(Kind::String(cursor.text(len)?)),
Header::Array(Some(_)) => Ok(Kind::Array),
Header::Map(Some(_)) => Ok(Kind::Object),
Header::Simple(20) => Ok(Kind::Bool(false)),
Header::Simple(21) => Ok(Kind::Bool(true)),
Header::Simple(22) => Ok(Kind::Null),
_ => Err(invalid()),
}
}
/// Fallible, exact-size traversal of a borrowed CBOR array.
pub struct Elements<'a> {
cursor: Cursor<'a>,
remaining: usize,
}
impl<'a> Iterator for Elements<'a> {
type Item = std::result::Result<JsonView<'a>, AccessError>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
Some(
self.cursor
.item_span(0)
.map(JsonView)
.map_err(|_| AccessError::Malformed),
)
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl ExactSizeIterator for Elements<'_> {}
/// Fallible, exact-size traversal of borrowed CBOR object fields.
pub struct Fields<'a> {
cursor: Cursor<'a>,
previous: Option<&'a str>,
remaining: usize,
}
impl<'a> Iterator for Fields<'a> {
type Item = std::result::Result<(&'a str, JsonView<'a>), AccessError>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining == 0 {
return None;
}
self.remaining -= 1;
Some(
(|| {
let key = self.cursor.map_key(self.previous)?;
self.previous = Some(key);
Ok((key, JsonView(self.cursor.item_span(0)?)))
})()
.map_err(|_: SmithError| AccessError::Malformed),
)
}
fn size_hint(&self) -> (usize, Option<usize>) {
(self.remaining, Some(self.remaining))
}
}
impl ExactSizeIterator for Fields<'_> {}
/// Start array traversal bounded by the resident span's byte length.
pub fn elements(bytes: &[u8]) -> Result<Option<Elements<'_>>> {
let mut cursor = Cursor { bytes };
let Header::Array(Some(remaining)) = cursor.header()?.0 else {
return Ok(None);
};
if remaining > cursor.bytes.len() {
return Err(invalid());
}
Ok(Some(Elements { cursor, remaining }))
}
/// Start object traversal bounded by the resident span's byte length.
pub fn fields(bytes: &[u8]) -> Result<Option<Fields<'_>>> {
let mut cursor = Cursor { bytes };
let Header::Map(Some(remaining)) = cursor.header()?.0 else {
return Ok(None);
};
if remaining > cursor.bytes.len() / 2 {
return Err(invalid());
}
Ok(Some(Fields {
cursor,
previous: None,
remaining,
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn traversal_reports_truncated_elements_with_exact_remaining_count() {
use smith::structured::StructuredValue;
let mut array = JsonView(&[0x82, 0xf6, 0xff]).elements().unwrap().unwrap();
assert_eq!(array.len(), 2);
assert_eq!(array.next().unwrap().unwrap().kind().unwrap(), Kind::Null);
assert_eq!(array.len(), 1);
assert!(matches!(array.next(), Some(Err(AccessError::Malformed))));
assert_eq!(array.len(), 0);
assert!(array.next().is_none());
}
#[test]
fn field_lookup_propagates_invalid_fields_and_rejects_unbounded_counts() {
use smith::structured::StructuredValue;
let bad = JsonView(&[0xa2, 0x61, b'a', 1, 0x61, b'b', 0xff]);
assert!(matches!(bad.field("b"), Err(AccessError::Malformed)));
assert!(
JsonView(&[0x9a, 0xff, 0xff, 0xff, 0xff])
.elements()
.is_err()
);
assert!(JsonView(&[0xba, 0xff, 0xff, 0xff, 0xff]).fields().is_err());
let mut duplicate = JsonView(&[0xa2, 0x61, b'a', 1, 0x61, b'a', 2])
.fields()
.unwrap()
.unwrap();
assert_eq!(duplicate.next().unwrap().unwrap().0, "a");
assert!(matches!(
duplicate.next(),
Some(Err(AccessError::Malformed))
));
assert_eq!(duplicate.len(), 0);
}
#[test]
fn rejects_duplicate_map_keys() {
assert!(scan(&[0xa2, 0x61, b'a', 1, 0x61, b'a', 2], None).is_err());
}
#[test]
fn rejects_map_keys_outside_encoder_order() {
assert!(scan(&[0xa2, 0x62, b'a', b'a', 1, 0x61, b'b', 2], None).is_err());
}
#[test]
fn rejects_nonminimal_numeric_encodings() {
assert!(scan(&[0x18, 23], None).is_err());
assert!(scan(&[0xfa, 0x3f, 0x80, 0, 0], None).is_err());
}
#[test]
fn rejects_malformed_numeric_values_and_excess_depth() {
assert!(scan(&[0x1a, 0], None).is_err());
assert!(
scan(
&[0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
None
)
.is_err()
);
assert!(scan(&[0xfb, 0x3f, 0xf0, 0, 0, 0, 0, 0, 0], None).is_err());
assert!(scan(&[0xf8, 20], None).is_err());
let mut overdeep = vec![0x81; 65];
overdeep.push(0xf6);
assert!(scan(&overdeep, None).is_err());
}
#[test]
fn accepts_canonical_map_when_keys_follow_session_order() {
assert!(scan(&[0xa2, 0x61, b'b', 1, 0x62, b'a', b'a', 2], None).is_ok());
}
#[test]
fn preserves_integer_float_and_negative_zero_kinds_when_streaming() {
let bytes = [
0x84, // array(4)
0x20, // -1
0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // u64::MAX
0xf9, 0x80, 0x00, // -0.0
0xfb, 0x3f, 0xf1, 0x99, 0x99, 0x99, 0x99, 0x99, 0x9a, // 1.1
];
assert!(scan(&bytes, None).is_ok());
let value = serialize(&bytes, serde_json::value::Serializer).unwrap();
let values = value.as_array().unwrap();
assert_eq!(values[0].as_i64(), Some(-1));
assert_eq!(values[1].as_u64(), Some(u64::MAX));
assert!(values[2].as_f64().unwrap().is_sign_negative());
assert_eq!(values[3].as_f64(), Some(1.1));
}
}