Skip to main content

uor_addr/json/
value.rs

1//! JSON typed input (ADR-023 amended by ADR-060) with JCS-RFC8785 +
2//! Unicode NFC canonical-form byte output.
3//!
4//! JSON canonicalization is **not** a streaming transform: JCS-RFC8785
5//! §3.2.3 sorts object members lexicographically by key, which needs
6//! per-object storage. The realization therefore materializes the
7//! canonical form once, in an `alloc` buffer ([`canonicalize`]), with
8//! **no** width / depth / count ceilings: string widths, number widths,
9//! object-key counts, array-element counts, and total size are unbounded.
10//! The handle then flows through the pipeline as a zero-copy
11//! [`TermValue::Borrowed`] carrier over those canonical bytes, and ψ₉
12//! folds them through the σ-axis. The single retained bound is
13//! [`MAX_JSON_DEPTH`](crate::json::shapes::bounds::MAX_JSON_DEPTH), a
14//! native-stack overflow guard on the recursive parser/canonicalizer.
15//!
16//! [`JsonValue`] (the owned parsed value, `alloc`-gated) holds the
17//! structurally-tagged byte form and backs the [`JsonValueRef`] navigator
18//! used by the schema-pinned descendants; [`JsonCarrier`] is the borrowed
19//! model-input handle the pipeline binds.
20//!
21//! # Tagged byte layout
22//!
23//! ```text
24//! JsonValue ::= Tag(1 byte) Payload
25//!   Tag = 0x00 Null         — no payload
26//!   Tag = 0x01 BoolFalse    — no payload
27//!   Tag = 0x02 BoolTrue     — no payload
28//!   Tag = 0x03 Number       — u32 BE length || N bytes (canonical ASCII)
29//!   Tag = 0x04 String       — u32 BE length || N bytes (UTF-8, NFC)
30//!   Tag = 0x05 Array        — u32 BE count  || count × JsonValue
31//!   Tag = 0x06 Object       — u32 BE count  || count × (u32 BE keylen || key || JsonValue)
32//! ```
33//!
34//! All multi-byte length / count fields are big-endian. Strings and
35//! object keys are NFC-normalized at parse time, so the canonical-form
36//! emitter is purely structural — it sorts object entries by NFC byte
37//! order and emits JCS syntax around already-canonical content.
38
39use prism::operation::TermValue;
40use prism::pipeline::{
41    ConstrainedTypeShape, ConstraintRef, IntoBindingValue, PartitionProductFields,
42};
43
44// ─── Tag byte constants (consumed only by the alloc-gated parser /
45//     canonicalizer / navigator) ──────────────────────────────────────────
46
47#[cfg(feature = "alloc")]
48pub(crate) const TAG_NULL: u8 = 0x00;
49#[cfg(feature = "alloc")]
50pub(crate) const TAG_FALSE: u8 = 0x01;
51#[cfg(feature = "alloc")]
52pub(crate) const TAG_TRUE: u8 = 0x02;
53#[cfg(feature = "alloc")]
54pub(crate) const TAG_NUMBER: u8 = 0x03;
55#[cfg(feature = "alloc")]
56pub(crate) const TAG_STRING: u8 = 0x04;
57#[cfg(feature = "alloc")]
58pub(crate) const TAG_ARRAY: u8 = 0x05;
59#[cfg(feature = "alloc")]
60pub(crate) const TAG_OBJECT: u8 = 0x06;
61
62// ─── ShapeViolation IRIs (alloc-gated parser) ───────────────────────────
63
64#[cfg(feature = "alloc")]
65const INVALID_JSON_VIOLATION: prism::pipeline::ShapeViolation = prism::pipeline::ShapeViolation {
66    shape_iri: "https://uor.foundation/addr/JsonValue",
67    constraint_iri: "https://uor.foundation/addr/JsonValue/validUtf8Json",
68    property_iri: "https://uor.foundation/addr/inputBytes",
69    expected_range: "https://uor.foundation/addr/ValidUtf8Json",
70    min_count: 0,
71    max_count: 1,
72    kind: prism::pipeline::ViolationKind::ValueCheck,
73};
74
75#[cfg(feature = "alloc")]
76const DEPTH_BOUND_VIOLATION: prism::pipeline::ShapeViolation = prism::pipeline::ShapeViolation {
77    shape_iri: "https://uor.foundation/addr/JsonValue",
78    constraint_iri: "https://uor.foundation/addr/JsonValue/depthBound",
79    property_iri: "https://uor.foundation/addr/JsonValue/depth",
80    expected_range: "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
81    min_count: 0,
82    max_count: crate::json::shapes::bounds::MAX_JSON_DEPTH as u32,
83    kind: prism::pipeline::ViolationKind::CardinalityViolation,
84};
85
86// ─── JsonCarrier — the borrowed model-input handle (no_alloc) ───────────
87
88/// Borrowed canonical-JSON input handle (ADR-060 borrowed carrier). A
89/// thin, `Copy` borrow of canonical bytes produced by [`canonicalize`];
90/// `as_binding_value` returns the `Borrowed` carrier zero-copy.
91#[derive(Clone, Copy, Debug)]
92pub struct JsonCarrier<'a>(&'a [u8]);
93
94impl<'a> JsonCarrier<'a> {
95    /// Wrap a canonical-JSON byte slice as a model input handle.
96    #[must_use]
97    pub fn new(canonical_bytes: &'a [u8]) -> Self {
98        Self(canonical_bytes)
99    }
100
101    /// Borrow the canonical-JSON bytes.
102    #[must_use]
103    pub fn canonical_bytes(&self) -> &'a [u8] {
104        self.0
105    }
106}
107
108impl ConstrainedTypeShape for JsonCarrier<'_> {
109    const IRI: &'static str = "https://uor.foundation/addr/JsonValue";
110    const SITE_COUNT: usize = 1;
111    const CONSTRAINTS: &'static [ConstraintRef] = &[];
112    const CYCLE_SIZE: u64 = u64::MAX;
113}
114
115impl prism::uor_foundation::pipeline::__sdk_seal::Sealed for JsonCarrier<'_> {}
116
117impl<'a> IntoBindingValue<'a> for JsonCarrier<'a> {
118    fn as_binding_value<const INLINE_BYTES: usize>(&self) -> TermValue<'a, INLINE_BYTES> {
119        TermValue::borrowed(self.0)
120    }
121}
122
123impl PartitionProductFields for JsonCarrier<'_> {
124    const FIELDS: &'static [(u32, u32)] = &[];
125    const FIELD_NAMES: &'static [&'static str] = &[];
126}
127
128// ═════════════════════════════════════════════════════════════════════
129// alloc-gated parser, canonicalizer, owned value, and navigator
130// ═════════════════════════════════════════════════════════════════════
131
132#[cfg(feature = "alloc")]
133pub use alloc_impl::{canonicalize, ArrayIter, JsonValue, JsonValueRef, ObjectIter};
134
135#[cfg(feature = "alloc")]
136mod alloc_impl {
137    use super::*;
138    use crate::canonical::nfc;
139    use crate::json::shapes::bounds::MAX_JSON_DEPTH;
140    use alloc::collections::BTreeSet;
141    use alloc::vec::Vec;
142    use prism::pipeline::ShapeViolation;
143
144    // ─── JsonValue — the owned parsed value ─────────────────────────────
145
146    /// Owned parsed JSON value, holding the structurally-tagged byte form
147    /// documented in the [module header](super). Backs the
148    /// [`JsonValueRef`] navigator. There is no width / depth / count
149    /// ceiling. **`alloc`-gated** — the pipeline binds the borrowed
150    /// [`JsonCarrier`] handle.
151    #[derive(Clone, PartialEq, Eq)]
152    pub struct JsonValue {
153        pub(crate) bytes: Vec<u8>,
154    }
155
156    impl core::fmt::Debug for JsonValue {
157        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
158            f.debug_struct("JsonValue")
159                .field("len", &self.bytes.len())
160                .finish_non_exhaustive()
161        }
162    }
163
164    impl JsonValue {
165        /// Parse raw JSON bytes into a typed `JsonValue` (RFC 8259 syntax,
166        /// escape decoding, UAX #15 NFC normalization, JCS number
167        /// canonicalization).
168        ///
169        /// # Errors
170        ///
171        /// - `validUtf8Json` — input is not valid UTF-8 JSON, or an object
172        ///   repeats a member name (names are compared after escape
173        ///   decoding and NFC normalization).
174        /// - `depthBound` — nesting exceeds the [`MAX_JSON_DEPTH`]
175        ///   stack-safety bound.
176        pub fn parse(raw: &[u8]) -> Result<Self, ShapeViolation> {
177            let mut value = Self { bytes: Vec::new() };
178            let mut p = Parser::new(raw);
179            p.skip_ws();
180            parse_value(&mut p, &mut value, 0)?;
181            p.skip_ws();
182            if !p.is_eof() {
183                return Err(INVALID_JSON_VIOLATION);
184            }
185            Ok(value)
186        }
187
188        /// Borrow the structurally-tagged byte serialization (the runtime
189        /// form; **not** the canonical bytes ψ₉ hashes — derive those via
190        /// [`canonicalize`]).
191        #[must_use]
192        pub fn tagged_bytes(&self) -> &[u8] {
193            &self.bytes
194        }
195
196        fn push_byte(&mut self, b: u8) {
197            self.bytes.push(b);
198        }
199
200        fn push_u32_be(&mut self, v: u32) {
201            self.bytes.extend_from_slice(&v.to_be_bytes());
202        }
203
204        fn extend(&mut self, data: &[u8]) {
205            self.bytes.extend_from_slice(data);
206        }
207
208        fn patch_u32_be(&mut self, offset: usize, v: u32) {
209            self.bytes[offset..offset + 4].copy_from_slice(&v.to_be_bytes());
210        }
211    }
212
213    /// Parse + emit the JCS-RFC8785 + Unicode NFC canonical-form bytes.
214    ///
215    /// # Errors
216    ///
217    /// Surfaces any [`ShapeViolation`] [`JsonValue::parse`] would emit.
218    pub fn canonicalize(raw: &[u8]) -> Result<Vec<u8>, ShapeViolation> {
219        let value = JsonValue::parse(raw)?;
220        let mut out = Vec::new();
221        let mut pos = 0;
222        emit_value(value.tagged_bytes(), &mut pos, &mut out)?;
223        Ok(out)
224    }
225
226    // ─── Tokenizer ──────────────────────────────────────────────────────
227
228    struct Parser<'a> {
229        input: &'a [u8],
230        pos: usize,
231    }
232
233    impl<'a> Parser<'a> {
234        fn new(input: &'a [u8]) -> Self {
235            Self { input, pos: 0 }
236        }
237        fn is_eof(&self) -> bool {
238            self.pos >= self.input.len()
239        }
240        fn peek(&self) -> Result<u8, ShapeViolation> {
241            if self.is_eof() {
242                return Err(INVALID_JSON_VIOLATION);
243            }
244            Ok(self.input[self.pos])
245        }
246        fn bump(&mut self) -> Result<u8, ShapeViolation> {
247            let b = self.peek()?;
248            self.pos += 1;
249            Ok(b)
250        }
251        fn skip_ws(&mut self) {
252            while self.pos < self.input.len() {
253                match self.input[self.pos] {
254                    b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
255                    _ => break,
256                }
257            }
258        }
259        fn expect(&mut self, byte: u8) -> Result<(), ShapeViolation> {
260            if self.bump()? != byte {
261                return Err(INVALID_JSON_VIOLATION);
262            }
263            Ok(())
264        }
265        fn expect_lit(&mut self, lit: &[u8]) -> Result<(), ShapeViolation> {
266            if self.pos + lit.len() > self.input.len()
267                || &self.input[self.pos..self.pos + lit.len()] != lit
268            {
269                return Err(INVALID_JSON_VIOLATION);
270            }
271            self.pos += lit.len();
272            Ok(())
273        }
274    }
275
276    fn parse_value(
277        p: &mut Parser<'_>,
278        out: &mut JsonValue,
279        depth: usize,
280    ) -> Result<(), ShapeViolation> {
281        if depth > MAX_JSON_DEPTH {
282            return Err(DEPTH_BOUND_VIOLATION);
283        }
284        p.skip_ws();
285        match p.peek()? {
286            b'n' => {
287                p.expect_lit(b"null")?;
288                out.push_byte(TAG_NULL);
289                Ok(())
290            }
291            b't' => {
292                p.expect_lit(b"true")?;
293                out.push_byte(TAG_TRUE);
294                Ok(())
295            }
296            b'f' => {
297                p.expect_lit(b"false")?;
298                out.push_byte(TAG_FALSE);
299                Ok(())
300            }
301            b'"' => parse_string(p, out),
302            b'-' | b'0'..=b'9' => parse_number(p, out),
303            b'[' => parse_array(p, out, depth + 1),
304            b'{' => parse_object(p, out, depth + 1),
305            _ => Err(INVALID_JSON_VIOLATION),
306        }
307    }
308
309    fn parse_array(
310        p: &mut Parser<'_>,
311        out: &mut JsonValue,
312        depth: usize,
313    ) -> Result<(), ShapeViolation> {
314        p.expect(b'[')?;
315        out.push_byte(TAG_ARRAY);
316        let count_pos = out.bytes.len();
317        out.push_u32_be(0);
318        let mut count: u32 = 0;
319        p.skip_ws();
320        if p.peek()? == b']' {
321            p.pos += 1;
322            return Ok(());
323        }
324        loop {
325            parse_value(p, out, depth)?;
326            count += 1;
327            p.skip_ws();
328            match p.bump()? {
329                b',' => {
330                    p.skip_ws();
331                    continue;
332                }
333                b']' => break,
334                _ => return Err(INVALID_JSON_VIOLATION),
335            }
336        }
337        out.patch_u32_be(count_pos, count);
338        Ok(())
339    }
340
341    fn parse_object(
342        p: &mut Parser<'_>,
343        out: &mut JsonValue,
344        depth: usize,
345    ) -> Result<(), ShapeViolation> {
346        p.expect(b'{')?;
347        out.push_byte(TAG_OBJECT);
348        let count_pos = out.bytes.len();
349        out.push_u32_be(0);
350        let mut count: u32 = 0;
351        // Member names already seen in this object. JCS input must be
352        // I-JSON (RFC 8785 §3.1, RFC 7493 §2.3), which forbids duplicate
353        // names; names that only become equal after NFC normalization are
354        // duplicates too (uor-jcs-nfc SPEC.md §3.4, §4.3). Keys come out of
355        // `decode_string_into_nfc` already normalized, so comparing the
356        // bytes covers escape-spelled and NFC-equivalent names.
357        let mut seen: BTreeSet<Vec<u8>> = BTreeSet::new();
358        p.skip_ws();
359        if p.peek()? == b'}' {
360            p.pos += 1;
361            return Ok(());
362        }
363        loop {
364            p.skip_ws();
365            if p.peek()? != b'"' {
366                return Err(INVALID_JSON_VIOLATION);
367            }
368            let key = decode_string_into_nfc(p)?;
369            if seen.contains(&key) {
370                return Err(INVALID_JSON_VIOLATION);
371            }
372            out.push_u32_be(key.len() as u32);
373            out.extend(&key);
374            seen.insert(key);
375            p.skip_ws();
376            p.expect(b':')?;
377            p.skip_ws();
378            parse_value(p, out, depth)?;
379            count += 1;
380            p.skip_ws();
381            match p.bump()? {
382                b',' => continue,
383                b'}' => break,
384                _ => return Err(INVALID_JSON_VIOLATION),
385            }
386        }
387        out.patch_u32_be(count_pos, count);
388        Ok(())
389    }
390
391    fn parse_string(p: &mut Parser<'_>, out: &mut JsonValue) -> Result<(), ShapeViolation> {
392        let s = decode_string_into_nfc(p)?;
393        out.push_byte(TAG_STRING);
394        out.push_u32_be(s.len() as u32);
395        out.extend(&s);
396        Ok(())
397    }
398
399    /// Decode a JSON string literal at the cursor (escape handling +
400    /// NFC normalization), returning the NFC-normalized UTF-8 bytes.
401    fn decode_string_into_nfc(p: &mut Parser<'_>) -> Result<Vec<u8>, ShapeViolation> {
402        p.expect(b'"')?;
403        let mut stage1 = Vec::new();
404        loop {
405            if p.is_eof() {
406                return Err(INVALID_JSON_VIOLATION);
407            }
408            let b = p.input[p.pos];
409            match b {
410                b'"' => {
411                    p.pos += 1;
412                    break;
413                }
414                b'\\' => {
415                    p.pos += 1;
416                    let esc = p.bump()?;
417                    match esc {
418                        b'"' => stage1.push(b'"'),
419                        b'\\' => stage1.push(b'\\'),
420                        b'/' => stage1.push(b'/'),
421                        b'b' => stage1.push(0x08),
422                        b'f' => stage1.push(0x0C),
423                        b'n' => stage1.push(0x0A),
424                        b'r' => stage1.push(0x0D),
425                        b't' => stage1.push(0x09),
426                        b'u' => {
427                            let cp = decode_u_escape(p)?;
428                            let c = char::from_u32(cp).ok_or(INVALID_JSON_VIOLATION)?;
429                            let mut tmp = [0u8; 4];
430                            stage1.extend_from_slice(c.encode_utf8(&mut tmp).as_bytes());
431                        }
432                        _ => return Err(INVALID_JSON_VIOLATION),
433                    }
434                }
435                // Unescaped control characters forbidden by RFC 8259 §7.
436                0x00..=0x1F => return Err(INVALID_JSON_VIOLATION),
437                _ => {
438                    stage1.push(b);
439                    p.pos += 1;
440                }
441            }
442        }
443        normalize_nfc(&stage1)
444    }
445
446    /// NFC-normalize `stage1` into an owned buffer, growing the output
447    /// allocation until it fits (UAX #15 NFC expansion is bounded, so the
448    /// loop runs at most a couple of iterations).
449    fn normalize_nfc(stage1: &[u8]) -> Result<Vec<u8>, ShapeViolation> {
450        let mut cap = stage1.len().saturating_mul(3).max(64);
451        loop {
452            let mut buf = alloc::vec![0u8; cap];
453            match nfc::normalize_into(stage1, &mut buf) {
454                Ok(n) => {
455                    buf.truncate(n);
456                    return Ok(buf);
457                }
458                Err(nfc::NfcError::OutputOverflow) => {
459                    cap = cap.saturating_mul(2);
460                }
461                Err(_) => return Err(INVALID_JSON_VIOLATION),
462            }
463        }
464    }
465
466    fn decode_u_escape(p: &mut Parser<'_>) -> Result<u32, ShapeViolation> {
467        let high = decode_hex4(p)?;
468        if (0xD800..=0xDBFF).contains(&high) {
469            if p.input.get(p.pos..p.pos + 2) != Some(b"\\u") {
470                return Err(INVALID_JSON_VIOLATION);
471            }
472            p.pos += 2;
473            let low = decode_hex4(p)?;
474            if !(0xDC00..=0xDFFF).contains(&low) {
475                return Err(INVALID_JSON_VIOLATION);
476            }
477            Ok(0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00))
478        } else if (0xDC00..=0xDFFF).contains(&high) {
479            Err(INVALID_JSON_VIOLATION)
480        } else {
481            Ok(high)
482        }
483    }
484
485    fn decode_hex4(p: &mut Parser<'_>) -> Result<u32, ShapeViolation> {
486        if p.pos + 4 > p.input.len() {
487            return Err(INVALID_JSON_VIOLATION);
488        }
489        let mut v: u32 = 0;
490        for _ in 0..4 {
491            let d = p.input[p.pos];
492            p.pos += 1;
493            let nibble = match d {
494                b'0'..=b'9' => (d - b'0') as u32,
495                b'a'..=b'f' => 10 + (d - b'a') as u32,
496                b'A'..=b'F' => 10 + (d - b'A') as u32,
497                _ => return Err(INVALID_JSON_VIOLATION),
498            };
499            v = (v << 4) | nibble;
500        }
501        Ok(v)
502    }
503
504    fn parse_number(p: &mut Parser<'_>, out: &mut JsonValue) -> Result<(), ShapeViolation> {
505        let start = p.pos;
506        let mut has_decimal = false;
507        let mut has_exponent = false;
508        if p.peek()? == b'-' {
509            p.pos += 1;
510        }
511        match p.peek()? {
512            b'0' => p.pos += 1,
513            b'1'..=b'9' => {
514                p.pos += 1;
515                while let Ok(b) = p.peek() {
516                    if b.is_ascii_digit() {
517                        p.pos += 1;
518                    } else {
519                        break;
520                    }
521                }
522            }
523            _ => return Err(INVALID_JSON_VIOLATION),
524        }
525        if p.peek().ok() == Some(b'.') {
526            has_decimal = true;
527            p.pos += 1;
528            let frac_start = p.pos;
529            while let Ok(b) = p.peek() {
530                if b.is_ascii_digit() {
531                    p.pos += 1;
532                } else {
533                    break;
534                }
535            }
536            if p.pos == frac_start {
537                return Err(INVALID_JSON_VIOLATION);
538            }
539        }
540        if let Ok(b) = p.peek() {
541            if b == b'e' || b == b'E' {
542                has_exponent = true;
543                p.pos += 1;
544                if let Ok(s) = p.peek() {
545                    if s == b'+' || s == b'-' {
546                        p.pos += 1;
547                    }
548                }
549                let exp_start = p.pos;
550                while let Ok(d) = p.peek() {
551                    if d.is_ascii_digit() {
552                        p.pos += 1;
553                    } else {
554                        break;
555                    }
556                }
557                if p.pos == exp_start {
558                    return Err(INVALID_JSON_VIOLATION);
559                }
560            }
561        }
562        let raw = &p.input[start..p.pos];
563        let canon = canonicalize_number(raw, has_decimal || has_exponent)?;
564        out.push_byte(TAG_NUMBER);
565        out.push_u32_be(canon.len() as u32);
566        out.extend(&canon);
567        Ok(())
568    }
569
570    /// Canonicalize a JSON number per JCS-RFC8785 §3.2.2.3 (ECMA-262
571    /// 7.1.12.1): integer-syntax literals pass through verbatim (RFC 8259
572    /// forbids leading zeros / explicit `+`, so they are already in
573    /// ECMA-262 ToString form), float-syntax literals (and `-0`) route
574    /// through `f64` + `ryu` shortest-round-trip.
575    fn canonicalize_number(raw: &[u8], is_float_syntax: bool) -> Result<Vec<u8>, ShapeViolation> {
576        let is_negative_zero = raw == b"-0";
577        if is_float_syntax || is_negative_zero {
578            let s = core::str::from_utf8(raw).map_err(|_| INVALID_JSON_VIOLATION)?;
579            let v: f64 = s.parse().map_err(|_| INVALID_JSON_VIOLATION)?;
580            let mut ryu_buf = ryu::Buffer::new();
581            Ok(ryu_buf.format(v).as_bytes().to_vec())
582        } else {
583            Ok(raw.to_vec())
584        }
585    }
586
587    // ─── JCS canonicalizer (tagged bytes → canonical bytes) ─────────────
588
589    fn read_byte(tagged: &[u8], pos: &mut usize) -> Result<u8, ShapeViolation> {
590        if *pos >= tagged.len() {
591            return Err(INVALID_JSON_VIOLATION);
592        }
593        let b = tagged[*pos];
594        *pos += 1;
595        Ok(b)
596    }
597
598    fn read_u32_be(tagged: &[u8], pos: &mut usize) -> Result<u32, ShapeViolation> {
599        if *pos + 4 > tagged.len() {
600            return Err(INVALID_JSON_VIOLATION);
601        }
602        let v = u32::from_be_bytes([
603            tagged[*pos],
604            tagged[*pos + 1],
605            tagged[*pos + 2],
606            tagged[*pos + 3],
607        ]);
608        *pos += 4;
609        Ok(v)
610    }
611
612    fn read_slice<'a>(
613        tagged: &'a [u8],
614        pos: &mut usize,
615        len: usize,
616    ) -> Result<&'a [u8], ShapeViolation> {
617        if *pos + len > tagged.len() {
618            return Err(INVALID_JSON_VIOLATION);
619        }
620        let s = &tagged[*pos..*pos + len];
621        *pos += len;
622        Ok(s)
623    }
624
625    fn emit_value(tagged: &[u8], pos: &mut usize, out: &mut Vec<u8>) -> Result<(), ShapeViolation> {
626        let tag = read_byte(tagged, pos)?;
627        match tag {
628            TAG_NULL => {
629                out.extend_from_slice(b"null");
630                Ok(())
631            }
632            TAG_FALSE => {
633                out.extend_from_slice(b"false");
634                Ok(())
635            }
636            TAG_TRUE => {
637                out.extend_from_slice(b"true");
638                Ok(())
639            }
640            TAG_NUMBER => {
641                let len = read_u32_be(tagged, pos)? as usize;
642                let bytes = read_slice(tagged, pos, len)?;
643                out.extend_from_slice(bytes);
644                Ok(())
645            }
646            TAG_STRING => {
647                let len = read_u32_be(tagged, pos)? as usize;
648                let bytes = read_slice(tagged, pos, len)?;
649                emit_json_string(bytes, out);
650                Ok(())
651            }
652            TAG_ARRAY => {
653                let count = read_u32_be(tagged, pos)? as usize;
654                out.push(b'[');
655                for i in 0..count {
656                    if i > 0 {
657                        out.push(b',');
658                    }
659                    emit_value(tagged, pos, out)?;
660                }
661                out.push(b']');
662                Ok(())
663            }
664            TAG_OBJECT => emit_object(tagged, pos, out),
665            _ => Err(INVALID_JSON_VIOLATION),
666        }
667    }
668
669    fn emit_object(
670        tagged: &[u8],
671        pos: &mut usize,
672        out: &mut Vec<u8>,
673    ) -> Result<(), ShapeViolation> {
674        let count = read_u32_be(tagged, pos)? as usize;
675        // Collect each member's entry offset (start of its u32 keylen),
676        // then stable-sort by NFC key bytes (== lexicographic, strings are
677        // pre-normalized at parse time).
678        let mut entries: Vec<usize> = Vec::with_capacity(count);
679        for _ in 0..count {
680            entries.push(*pos);
681            let key_len = read_u32_be(tagged, pos)? as usize;
682            *pos += key_len;
683            if *pos > tagged.len() {
684                return Err(INVALID_JSON_VIOLATION);
685            }
686            skip_value(tagged, pos)?;
687        }
688        entries.sort_by(|&a, &b| entry_key(a, tagged).cmp(entry_key(b, tagged)));
689        out.push(b'{');
690        for (i, &entry_off) in entries.iter().enumerate() {
691            if i > 0 {
692                out.push(b',');
693            }
694            let mut p = entry_off;
695            let key_len = read_u32_be(tagged, &mut p)? as usize;
696            let key_bytes = read_slice(tagged, &mut p, key_len)?;
697            emit_json_string(key_bytes, out);
698            out.push(b':');
699            emit_value(tagged, &mut p, out)?;
700        }
701        out.push(b'}');
702        Ok(())
703    }
704
705    fn entry_key(off: usize, tagged: &[u8]) -> &[u8] {
706        if off + 4 > tagged.len() {
707            return &[];
708        }
709        let key_len = u32::from_be_bytes([
710            tagged[off],
711            tagged[off + 1],
712            tagged[off + 2],
713            tagged[off + 3],
714        ]) as usize;
715        let start = off + 4;
716        if start + key_len > tagged.len() {
717            return &[];
718        }
719        &tagged[start..start + key_len]
720    }
721
722    fn skip_value(tagged: &[u8], pos: &mut usize) -> Result<(), ShapeViolation> {
723        let tag = read_byte(tagged, pos)?;
724        match tag {
725            TAG_NULL | TAG_FALSE | TAG_TRUE => Ok(()),
726            TAG_NUMBER | TAG_STRING => {
727                let len = read_u32_be(tagged, pos)? as usize;
728                *pos += len;
729                if *pos > tagged.len() {
730                    Err(INVALID_JSON_VIOLATION)
731                } else {
732                    Ok(())
733                }
734            }
735            TAG_ARRAY => {
736                let count = read_u32_be(tagged, pos)? as usize;
737                for _ in 0..count {
738                    skip_value(tagged, pos)?;
739                }
740                Ok(())
741            }
742            TAG_OBJECT => {
743                let count = read_u32_be(tagged, pos)? as usize;
744                for _ in 0..count {
745                    let key_len = read_u32_be(tagged, pos)? as usize;
746                    *pos += key_len;
747                    if *pos > tagged.len() {
748                        return Err(INVALID_JSON_VIOLATION);
749                    }
750                    skip_value(tagged, pos)?;
751                }
752                Ok(())
753            }
754            _ => Err(INVALID_JSON_VIOLATION),
755        }
756    }
757
758    /// Emit `bytes` as a JCS-compliant JSON string literal.
759    fn emit_json_string(bytes: &[u8], out: &mut Vec<u8>) {
760        out.push(b'"');
761        for &b in bytes {
762            match b {
763                b'"' => out.extend_from_slice(b"\\\""),
764                b'\\' => out.extend_from_slice(b"\\\\"),
765                0x08 => out.extend_from_slice(b"\\b"),
766                0x09 => out.extend_from_slice(b"\\t"),
767                0x0A => out.extend_from_slice(b"\\n"),
768                0x0C => out.extend_from_slice(b"\\f"),
769                0x0D => out.extend_from_slice(b"\\r"),
770                0x00..=0x1F => {
771                    out.extend_from_slice(b"\\u00");
772                    out.push(nibble_hex(b >> 4));
773                    out.push(nibble_hex(b & 0x0f));
774                }
775                _ => out.push(b),
776            }
777        }
778        out.push(b'"');
779    }
780
781    fn nibble_hex(n: u8) -> u8 {
782        match n {
783            0..=9 => b'0' + n,
784            10..=15 => b'a' + (n - 10),
785            _ => b'0',
786        }
787    }
788
789    // ─── JsonValueRef — tagged-byte navigator for schema admission ──────
790
791    /// Zero-copy view into a tagged-byte JSON value (or sub-value), used
792    /// by the schema-pinned descendants to validate JSON-LD admission
793    /// predicates. Keys and string values are NFC-normalized; numbers
794    /// carry their canonical ASCII text.
795    #[derive(Clone, Copy)]
796    pub struct JsonValueRef<'a> {
797        tagged: &'a [u8],
798        offset: usize,
799    }
800
801    impl<'a> JsonValueRef<'a> {
802        /// Root navigator over a parsed [`JsonValue`].
803        pub fn root(value: &'a JsonValue) -> Self {
804            Self {
805                tagged: value.tagged_bytes(),
806                offset: 0,
807            }
808        }
809
810        /// Tag byte at this position.
811        pub fn tag(&self) -> u8 {
812            self.tagged[self.offset]
813        }
814        pub fn is_null(&self) -> bool {
815            self.tag() == TAG_NULL
816        }
817        pub fn is_bool(&self) -> bool {
818            matches!(self.tag(), TAG_FALSE | TAG_TRUE)
819        }
820        pub fn is_number(&self) -> bool {
821            self.tag() == TAG_NUMBER
822        }
823        pub fn is_string(&self) -> bool {
824            self.tag() == TAG_STRING
825        }
826        pub fn is_array(&self) -> bool {
827            self.tag() == TAG_ARRAY
828        }
829        pub fn is_object(&self) -> bool {
830            self.tag() == TAG_OBJECT
831        }
832
833        pub fn as_bool(&self) -> Option<bool> {
834            match self.tag() {
835                TAG_FALSE => Some(false),
836                TAG_TRUE => Some(true),
837                _ => None,
838            }
839        }
840
841        /// Borrow the NFC-normalized UTF-8 content of a string value.
842        pub fn as_str(&self) -> Option<&'a [u8]> {
843            if !self.is_string() {
844                return None;
845            }
846            let mut p = self.offset + 1;
847            let len = read_u32_be(self.tagged, &mut p).ok()? as usize;
848            Some(&self.tagged[p..p + len])
849        }
850
851        /// Borrow the canonical ASCII text of a number value.
852        pub fn as_number_str(&self) -> Option<&'a [u8]> {
853            if !self.is_number() {
854                return None;
855            }
856            let mut p = self.offset + 1;
857            let len = read_u32_be(self.tagged, &mut p).ok()? as usize;
858            Some(&self.tagged[p..p + len])
859        }
860
861        /// Look up an object entry by its NFC key bytes.
862        pub fn get(&self, key: &[u8]) -> Option<JsonValueRef<'a>> {
863            let mut iter = self.iter_object()?;
864            iter.find_map(|(k, v)| if k == key { Some(v) } else { None })
865        }
866
867        /// Iterate object entries `(key_bytes, value_ref)` in tagged-form
868        /// (input) order.
869        pub fn iter_object(&self) -> Option<ObjectIter<'a>> {
870            if !self.is_object() {
871                return None;
872            }
873            let mut p = self.offset + 1;
874            let count = read_u32_be(self.tagged, &mut p).ok()? as usize;
875            Some(ObjectIter {
876                tagged: self.tagged,
877                pos: p,
878                remaining: count,
879            })
880        }
881
882        /// Iterate array elements.
883        pub fn iter_array(&self) -> Option<ArrayIter<'a>> {
884            if !self.is_array() {
885                return None;
886            }
887            let mut p = self.offset + 1;
888            let count = read_u32_be(self.tagged, &mut p).ok()? as usize;
889            Some(ArrayIter {
890                tagged: self.tagged,
891                pos: p,
892                remaining: count,
893            })
894        }
895    }
896
897    /// Iterator over an object's `(key_bytes, value)` entries.
898    pub struct ObjectIter<'a> {
899        tagged: &'a [u8],
900        pos: usize,
901        remaining: usize,
902    }
903
904    impl<'a> Iterator for ObjectIter<'a> {
905        type Item = (&'a [u8], JsonValueRef<'a>);
906        fn next(&mut self) -> Option<Self::Item> {
907            if self.remaining == 0 {
908                return None;
909            }
910            let key_len = read_u32_be(self.tagged, &mut self.pos).ok()? as usize;
911            let key_end = self.pos + key_len;
912            let key = &self.tagged[self.pos..key_end];
913            self.pos = key_end;
914            let value_offset = self.pos;
915            self.pos = skip_to_end(self.tagged, self.pos).ok()?;
916            self.remaining -= 1;
917            Some((
918                key,
919                JsonValueRef {
920                    tagged: self.tagged,
921                    offset: value_offset,
922                },
923            ))
924        }
925    }
926
927    /// Iterator over an array's elements.
928    pub struct ArrayIter<'a> {
929        tagged: &'a [u8],
930        pos: usize,
931        remaining: usize,
932    }
933
934    impl<'a> Iterator for ArrayIter<'a> {
935        type Item = JsonValueRef<'a>;
936        fn next(&mut self) -> Option<Self::Item> {
937            if self.remaining == 0 {
938                return None;
939            }
940            let value_offset = self.pos;
941            self.pos = skip_to_end(self.tagged, self.pos).ok()?;
942            self.remaining -= 1;
943            Some(JsonValueRef {
944                tagged: self.tagged,
945                offset: value_offset,
946            })
947        }
948    }
949
950    fn skip_to_end(tagged: &[u8], pos: usize) -> Result<usize, ShapeViolation> {
951        let mut p = pos;
952        let tag = read_byte(tagged, &mut p)?;
953        match tag {
954            TAG_NULL | TAG_FALSE | TAG_TRUE => Ok(p),
955            TAG_NUMBER | TAG_STRING => {
956                let len = read_u32_be(tagged, &mut p)? as usize;
957                Ok(p + len)
958            }
959            TAG_ARRAY => {
960                let count = read_u32_be(tagged, &mut p)? as usize;
961                for _ in 0..count {
962                    p = skip_to_end(tagged, p)?;
963                }
964                Ok(p)
965            }
966            TAG_OBJECT => {
967                let count = read_u32_be(tagged, &mut p)? as usize;
968                for _ in 0..count {
969                    let key_len = read_u32_be(tagged, &mut p)? as usize;
970                    p += key_len;
971                    p = skip_to_end(tagged, p)?;
972                }
973                Ok(p)
974            }
975            _ => Err(INVALID_JSON_VIOLATION),
976        }
977    }
978
979    #[cfg(test)]
980    mod tests {
981        use super::*;
982
983        #[test]
984        fn parses_simple_object() {
985            let v = JsonValue::parse(br#"{"foo":"bar"}"#).expect("valid");
986            assert_eq!(v.bytes[0], TAG_OBJECT);
987        }
988
989        #[test]
990        fn rejects_invalid_json() {
991            let err = JsonValue::parse(b"not json").expect_err("must reject");
992            assert_eq!(err.shape_iri, INVALID_JSON_VIOLATION.shape_iri);
993        }
994
995        #[test]
996        fn rejects_overdeep_recursion() {
997            use alloc::string::String;
998            let mut s = String::new();
999            for _ in 0..(MAX_JSON_DEPTH + 2) {
1000                s.push('[');
1001            }
1002            for _ in 0..(MAX_JSON_DEPTH + 2) {
1003                s.push(']');
1004            }
1005            let err = JsonValue::parse(s.as_bytes()).expect_err("must reject");
1006            assert_eq!(err.constraint_iri, DEPTH_BOUND_VIOLATION.constraint_iri);
1007        }
1008
1009        #[test]
1010        fn accepts_unbounded_string_width() {
1011            use alloc::format;
1012            use alloc::string::String;
1013            let big: String = "a".repeat(200_000);
1014            let raw = format!("\"{big}\"");
1015            let canon = canonicalize(raw.as_bytes()).expect("unbounded string admitted");
1016            assert_eq!(canon.len(), big.len() + 2);
1017        }
1018
1019        const CANONICAL_FIXTURES: &[(&[u8], &[u8])] = &[
1020            (br#"{"foo":"bar"}"#, br#"{"foo":"bar"}"#),
1021            (br#"{"b": 1, "a": 2}"#, br#"{"a":2,"b":1}"#),
1022            (
1023                br#"{"nested": {"deep": {"value": "found"}}}"#,
1024                br#"{"nested":{"deep":{"value":"found"}}}"#,
1025            ),
1026            (
1027                br#"{"int": 42, "bool": true, "null_val": null}"#,
1028                br#"{"bool":true,"int":42,"null_val":null}"#,
1029            ),
1030            (b"[1, 2, 3]", b"[1,2,3]"),
1031            (br#"["a", "b", "c"]"#, br#"["a","b","c"]"#),
1032        ];
1033
1034        #[test]
1035        fn canonicalizer_matches_reference_for_inline_fixtures() {
1036            for (raw, expected) in CANONICAL_FIXTURES {
1037                let canon = canonicalize(raw).expect("valid");
1038                assert_eq!(canon, *expected, "raw={raw:?}");
1039            }
1040        }
1041
1042        #[test]
1043        fn canonicalizer_collapses_unicode_decomposed_to_composed() {
1044            let decomposed = "{\"name\": \"cafe\u{0301}\"}".as_bytes();
1045            let composed = "{\"name\":\"caf\u{00E9}\"}".as_bytes();
1046            assert_eq!(
1047                canonicalize(decomposed).expect("valid"),
1048                canonicalize(composed).expect("valid")
1049            );
1050        }
1051
1052        #[test]
1053        fn canonicalize_is_idempotent_on_its_own_output() {
1054            for (raw, _expected) in CANONICAL_FIXTURES {
1055                let once = canonicalize(raw).expect("valid");
1056                let twice = canonicalize(&once).expect("re-canonicalises");
1057                assert_eq!(once, twice, "idempotence broken for {raw:?}");
1058            }
1059        }
1060    }
1061}