Luigit
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));
    }
}