//! 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 { 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 { 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( &mut self, serializer: S, depth: u8, ) -> std::result::Result { 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(&self, serializer: S) -> std::result::Result { 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 { 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( bytes: &[u8], serializer: S, ) -> std::result::Result { 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> { 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, AccessError>; fn next(&mut self) -> Option { 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) { (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 { 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) { (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>> { 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>> { 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)); } }