Skip to main content

type_bridge_contract/
codec.rs

1//! Bounded canonical JSON encoding and fail-closed decoding.
2
3use std::fmt;
4
5use serde::de::{DeserializeOwned, MapAccess, SeqAccess, Visitor};
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Value};
8
9use crate::diagnostic::{Diagnostic, DiagnosticCategory};
10use crate::limits::{CANONICAL_CODEC_LIMITS, CodecLimits};
11
12/// A format version owned by a later schema/query/migration envelope.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct FormatVersion(u16);
16
17impl FormatVersion {
18    /// Initial version value for owning formats.
19    pub const V1: Self = Self(1);
20    /// Preserve an unvalidated raw version.
21    pub const fn from_raw(value: u16) -> Self {
22        Self(value)
23    }
24    /// Return the raw number.
25    pub const fn get(self) -> u16 {
26        self.0
27    }
28}
29
30/// Version of the canonical JSON codec itself.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32#[serde(transparent)]
33pub struct CodecVersion(u16);
34
35impl CodecVersion {
36    /// The Phase 1 canonical JSON codec.
37    pub const V1: Self = Self(1);
38    /// Preserve an unvalidated raw version.
39    pub const fn from_raw(value: u16) -> Self {
40        Self(value)
41    }
42    /// Return the raw number.
43    pub const fn get(self) -> u16 {
44        self.0
45    }
46}
47
48/// Require an exact owning-format version before payload construction.
49pub fn ensure_format_version(
50    actual: FormatVersion,
51    supported: FormatVersion,
52) -> Result<(), Diagnostic> {
53    if actual == supported {
54        Ok(())
55    } else {
56        Err(Diagnostic::stable(
57            DiagnosticCategory::InvalidContract,
58            "unsupported_format_version",
59            "contract format version is not supported",
60        )
61        .with_detail("actual", i64::from(actual.get()))
62        .with_detail("supported", i64::from(supported.get())))
63    }
64}
65
66/// Require an exact codec version before payload construction.
67pub fn ensure_codec_version(
68    actual: CodecVersion,
69    supported: CodecVersion,
70) -> Result<(), Diagnostic> {
71    if actual == supported {
72        Ok(())
73    } else {
74        Err(Diagnostic::stable(
75            DiagnosticCategory::InvalidContract,
76            "unsupported_codec_version",
77            "canonical codec version is not supported",
78        )
79        .with_detail("actual", i64::from(actual.get()))
80        .with_detail("supported", i64::from(supported.get())))
81    }
82}
83
84/// Encode one value to compact, key-sorted canonical JSON bytes.
85pub fn to_canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>, Diagnostic> {
86    to_canonical_json_with_limits(value, CANONICAL_CODEC_LIMITS)
87}
88
89/// Encode one value under explicit structural limits.
90pub fn to_canonical_json_with_limits<T: Serialize>(
91    value: &T,
92    limits: CodecLimits,
93) -> Result<Vec<u8>, Diagnostic> {
94    let mut value = serde_json::to_value(value).map_err(|_| {
95        Diagnostic::stable(
96            DiagnosticCategory::InvalidContract,
97            "canonical_json_encode_failed",
98            "value cannot be represented as canonical JSON",
99        )
100    })?;
101    normalize_numbers(&mut value).map_err(|()| {
102        Diagnostic::stable(
103            DiagnosticCategory::InvalidContract,
104            "canonical_json_encode_failed",
105            "value contains a number outside the canonical JSON domain",
106        )
107    })?;
108    sort_object_keys(&mut value);
109    inspect(&value, 1, limits)?;
110    let bytes = serde_json::to_vec(&value).map_err(|_| {
111        Diagnostic::stable(
112            DiagnosticCategory::InvalidContract,
113            "canonical_json_encode_failed",
114            "value cannot be encoded as canonical JSON",
115        )
116    })?;
117    ensure_bytes(bytes.len(), limits)?;
118    Ok(bytes)
119}
120
121/// Decode only exact canonical bytes, checking limits before constructing `T`.
122pub fn from_canonical_json<T>(bytes: &[u8]) -> Result<T, Diagnostic>
123where
124    T: DeserializeOwned + Serialize,
125{
126    from_canonical_json_with_limits(bytes, CANONICAL_CODEC_LIMITS)
127}
128
129/// Decode exact canonical bytes under explicit structural limits.
130pub fn from_canonical_json_with_limits<T>(
131    bytes: &[u8],
132    limits: CodecLimits,
133) -> Result<T, Diagnostic>
134where
135    T: DeserializeOwned + Serialize,
136{
137    ensure_bytes(bytes.len(), limits)?;
138    let mut value = match serde_json::from_slice::<UniqueValue>(bytes) {
139        Ok(value) => value.0,
140        Err(error) if error.to_string().contains(DUPLICATE_KEY_MARKER) => {
141            return Err(Diagnostic::stable(
142                DiagnosticCategory::InvalidContract,
143                "duplicate_canonical_json_key",
144                "canonical JSON objects cannot contain duplicate keys",
145            ));
146        }
147        Err(_) => {
148            return Err(Diagnostic::stable(
149                DiagnosticCategory::InvalidContract,
150                "malformed_canonical_json",
151                "input is not valid canonical JSON",
152            ));
153        }
154    };
155    inspect(&value, 1, limits)?;
156    normalize_numbers(&mut value).map_err(|()| {
157        Diagnostic::stable(
158            DiagnosticCategory::InvalidContract,
159            "malformed_canonical_json",
160            "input is not valid canonical JSON",
161        )
162    })?;
163    sort_object_keys(&mut value);
164    let canonical = serde_json::to_vec(&value).map_err(|_| {
165        Diagnostic::stable(
166            DiagnosticCategory::InvalidContract,
167            "canonical_json_encode_failed",
168            "decoded JSON cannot be re-encoded",
169        )
170    })?;
171    if canonical != bytes {
172        return Err(Diagnostic::stable(
173            DiagnosticCategory::InvalidContract,
174            "non_canonical_json",
175            "input is valid JSON but not the canonical encoding",
176        )
177        .with_detail("actual_bytes", count(bytes.len()))
178        .with_detail("canonical_bytes", count(canonical.len())));
179    }
180    serde_json::from_value(value).map_err(|_| {
181        Diagnostic::stable(
182            DiagnosticCategory::InvalidContract,
183            "invalid_canonical_value",
184            "canonical JSON does not satisfy the requested contract type",
185        )
186    })
187}
188
189const DUPLICATE_KEY_MARKER: &str = "duplicate canonical JSON object key";
190const ARBITRARY_PRECISION_NUMBER_KEY: &str = "$serde_json::private::Number";
191
192/// A JSON value visitor that rejects duplicate object keys before a map can
193/// overwrite them. This preflight is intentionally independent of `T`: every
194/// canonical owning format receives the same strict object-key behavior.
195struct UniqueValue(Value);
196
197impl<'de> Deserialize<'de> for UniqueValue {
198    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
199    where
200        D: serde::Deserializer<'de>,
201    {
202        deserializer.deserialize_any(UniqueValueVisitor)
203    }
204}
205
206struct UniqueValueVisitor;
207
208impl<'de> Visitor<'de> for UniqueValueVisitor {
209    type Value = UniqueValue;
210
211    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
212        formatter.write_str("a JSON value with unique object keys")
213    }
214
215    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
216        Ok(UniqueValue(Value::Bool(value)))
217    }
218
219    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
220        Ok(UniqueValue(Value::Number(value.into())))
221    }
222
223    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
224        Ok(UniqueValue(Value::Number(value.into())))
225    }
226
227    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
228    where
229        E: serde::de::Error,
230    {
231        serde_json::Number::from_f64(value)
232            .map(Value::Number)
233            .map(UniqueValue)
234            .ok_or_else(|| E::custom("non-finite JSON number"))
235    }
236
237    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
238    where
239        E: serde::de::Error,
240    {
241        self.visit_string(value.to_owned())
242    }
243
244    fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
245        Ok(UniqueValue(Value::String(value)))
246    }
247
248    fn visit_none<E>(self) -> Result<Self::Value, E> {
249        Ok(UniqueValue(Value::Null))
250    }
251
252    fn visit_unit<E>(self) -> Result<Self::Value, E> {
253        Ok(UniqueValue(Value::Null))
254    }
255
256    fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
257    where
258        A: SeqAccess<'de>,
259    {
260        let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
261        while let Some(value) = sequence.next_element::<UniqueValue>()? {
262            values.push(value.0);
263        }
264        Ok(UniqueValue(Value::Array(values)))
265    }
266
267    fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error>
268    where
269        A: MapAccess<'de>,
270    {
271        let Some(first_key) = object.next_key::<String>()? else {
272            return Ok(UniqueValue(Value::Object(Map::new())));
273        };
274        if first_key == ARBITRARY_PRECISION_NUMBER_KEY {
275            let spelling = object.next_value::<String>()?;
276            if object.next_key::<String>()?.is_some() {
277                return Err(serde::de::Error::custom(DUPLICATE_KEY_MARKER));
278            }
279            let number = spelling
280                .parse::<serde_json::Number>()
281                .map_err(serde::de::Error::custom)?;
282            return Ok(UniqueValue(Value::Number(number)));
283        }
284
285        let mut values = Map::new();
286        let first_value = object.next_value::<UniqueValue>()?.0;
287        values.insert(first_key, first_value);
288        while let Some(key) = object.next_key::<String>()? {
289            if values.contains_key(&key) {
290                return Err(serde::de::Error::custom(DUPLICATE_KEY_MARKER));
291            }
292            values.insert(key, object.next_value::<UniqueValue>()?.0);
293        }
294        Ok(UniqueValue(Value::Object(values)))
295    }
296}
297
298/// Sort every JSON object lexicographically without relying on
299/// `serde_json::Map`'s backing representation.
300///
301/// Cargo features are additive, so a downstream crate can enable
302/// `serde_json/preserve_order` for the shared dependency even though this
303/// crate does not request it. Re-inserting sorted entries keeps canonical
304/// bytes independent of that feature-unified map backend.
305fn sort_object_keys(value: &mut Value) {
306    match value {
307        Value::Array(values) => {
308            for value in values {
309                sort_object_keys(value);
310            }
311        }
312        Value::Object(values) => {
313            for value in values.values_mut() {
314                sort_object_keys(value);
315            }
316            let mut entries = std::mem::take(values).into_iter().collect::<Vec<_>>();
317            entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
318            values.extend(entries);
319        }
320        Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
321    }
322}
323
324/// Rebuild numbers through the semantic representation used by serde_json's
325/// ordinary backend. With `arbitrary_precision` feature-unified downstream,
326/// parsed numbers otherwise retain raw spellings such as `1e0` and integers
327/// beyond `u64`, making canonical acceptance depend on the Cargo feature graph.
328fn normalize_numbers(value: &mut Value) -> Result<(), ()> {
329    match value {
330        Value::Array(values) => {
331            for value in values {
332                normalize_numbers(value)?;
333            }
334        }
335        Value::Object(values) => {
336            for value in values.values_mut() {
337                normalize_numbers(value)?;
338            }
339        }
340        Value::Number(number) => {
341            let normalized = if let Some(value) = number.as_i64() {
342                value.into()
343            } else if let Some(value) = number.as_u64() {
344                value.into()
345            } else if let Some(value) = number.as_f64() {
346                serde_json::Number::from_f64(value).ok_or(())?
347            } else {
348                return Err(());
349            };
350            *number = normalized;
351        }
352        Value::Null | Value::Bool(_) | Value::String(_) => {}
353    }
354    Ok(())
355}
356
357fn inspect(value: &Value, depth: usize, limits: CodecLimits) -> Result<(), Diagnostic> {
358    if depth > limits.max_depth {
359        return Err(Diagnostic::stable(
360            DiagnosticCategory::ResourceLimit,
361            "canonical_json_too_deep",
362            "canonical JSON exceeds the nesting-depth ceiling",
363        )
364        .with_detail("maximum_depth", count(limits.max_depth)));
365    }
366    match value {
367        Value::String(value) => ensure_string(value.len(), limits),
368        Value::Array(values) => {
369            ensure_collection(values.len(), limits)?;
370            for value in values {
371                inspect(value, depth + 1, limits)?;
372            }
373            Ok(())
374        }
375        Value::Object(values) => {
376            ensure_collection(values.len(), limits)?;
377            for (key, value) in values {
378                ensure_string(key.len(), limits)?;
379                inspect(value, depth + 1, limits)?;
380            }
381            Ok(())
382        }
383        Value::Null | Value::Bool(_) | Value::Number(_) => Ok(()),
384    }
385}
386
387fn ensure_bytes(actual: usize, limits: CodecLimits) -> Result<(), Diagnostic> {
388    if actual <= limits.max_bytes {
389        Ok(())
390    } else {
391        Err(Diagnostic::stable(
392            DiagnosticCategory::ResourceLimit,
393            "canonical_json_too_large",
394            "canonical JSON exceeds the byte ceiling",
395        )
396        .with_detail("actual_bytes", count(actual))
397        .with_detail("maximum_bytes", count(limits.max_bytes)))
398    }
399}
400fn ensure_collection(actual: usize, limits: CodecLimits) -> Result<(), Diagnostic> {
401    if actual <= limits.max_collection_len {
402        Ok(())
403    } else {
404        Err(Diagnostic::stable(
405            DiagnosticCategory::ResourceLimit,
406            "canonical_collection_too_large",
407            "canonical JSON collection exceeds its member ceiling",
408        )
409        .with_detail("actual_items", count(actual))
410        .with_detail("maximum_items", count(limits.max_collection_len)))
411    }
412}
413fn ensure_string(actual: usize, limits: CodecLimits) -> Result<(), Diagnostic> {
414    if actual <= limits.max_string_bytes {
415        Ok(())
416    } else {
417        Err(Diagnostic::stable(
418            DiagnosticCategory::ResourceLimit,
419            "canonical_string_too_large",
420            "canonical JSON string exceeds its byte ceiling",
421        )
422        .with_detail("actual_bytes", count(actual))
423        .with_detail("maximum_bytes", count(limits.max_string_bytes)))
424    }
425}
426fn count(value: usize) -> i64 {
427    i64::try_from(value).unwrap_or(i64::MAX)
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use crate::value::CanonicalValue;
434    use serde::{Deserialize, Serialize};
435    use serde_json::Value;
436
437    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
438    struct OutOfOrderObject {
439        zeta: u8,
440        alpha: OutOfOrderNested,
441    }
442
443    #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
444    struct OutOfOrderNested {
445        zeta: u8,
446        alpha: u8,
447    }
448
449    #[test]
450    fn canonical_object_order_is_independent_of_the_serde_json_map_backend() {
451        let value = OutOfOrderObject {
452            zeta: 3,
453            alpha: OutOfOrderNested { zeta: 2, alpha: 1 },
454        };
455        let canonical = br#"{"alpha":{"alpha":1,"zeta":2},"zeta":3}"#;
456        assert_eq!(to_canonical_json(&value).unwrap(), canonical);
457        assert_eq!(
458            from_canonical_json::<OutOfOrderObject>(canonical).unwrap(),
459            value
460        );
461
462        let insertion_order = br#"{"zeta":3,"alpha":{"zeta":2,"alpha":1}}"#;
463        assert_eq!(
464            from_canonical_json::<OutOfOrderObject>(insertion_order)
465                .unwrap_err()
466                .code()
467                .as_str(),
468            "non_canonical_json"
469        );
470    }
471
472    #[test]
473    fn canonical_decoder_distinguishes_malformed_and_noncanonical_input() {
474        assert_eq!(
475            from_canonical_json::<CanonicalValue>(b"{")
476                .unwrap_err()
477                .code()
478                .as_str(),
479            "malformed_canonical_json"
480        );
481        let spaced = br#"{ "kind":"long","value":"1"}"#;
482        assert_eq!(
483            from_canonical_json::<CanonicalValue>(spaced)
484                .unwrap_err()
485                .code()
486                .as_str(),
487            "non_canonical_json"
488        );
489
490        for noncanonical in [b"1e0" as &[u8], b"1E+0", b"-0"] {
491            for error in [
492                from_canonical_json::<FormatVersion>(noncanonical).unwrap_err(),
493                from_canonical_json::<Value>(noncanonical).unwrap_err(),
494            ] {
495                assert_eq!(error.code().as_str(), "non_canonical_json");
496            }
497        }
498        assert_eq!(
499            from_canonical_json::<Value>(b"01")
500                .unwrap_err()
501                .code()
502                .as_str(),
503            "malformed_canonical_json"
504        );
505    }
506
507    #[test]
508    fn canonical_decoder_rejects_duplicate_keys_at_every_depth() {
509        for duplicate in [
510            br#"{"kind":"long","kind":"long","value":"1"}"# as &[u8],
511            br#"{"outer":{"value":1,"value":2}}"#,
512        ] {
513            assert_eq!(
514                from_canonical_json::<Value>(duplicate)
515                    .unwrap_err()
516                    .code()
517                    .as_str(),
518                "duplicate_canonical_json_key"
519            );
520        }
521    }
522
523    #[test]
524    fn canonical_numbers_are_independent_of_the_serde_json_number_backend() {
525        for canonical in [
526            b"0" as &[u8],
527            b"-1",
528            b"-9223372036854775808",
529            b"18446744073709551615",
530            b"1.0",
531            b"0.0",
532            b"-0.0",
533            b"5e-324",
534        ] {
535            let value = from_canonical_json::<Value>(canonical).unwrap();
536            assert_eq!(to_canonical_json(&value).unwrap(), canonical);
537        }
538
539        for noncanonical in [
540            b"-9223372036854775809" as &[u8],
541            b"18446744073709551616",
542            b"100000000000000000000000000000000000000000000000000",
543            b"4.9406564584124654e-324",
544        ] {
545            assert_eq!(
546                from_canonical_json::<Value>(noncanonical)
547                    .unwrap_err()
548                    .code()
549                    .as_str(),
550                "non_canonical_json"
551            );
552        }
553
554        assert_eq!(to_canonical_json(&1.0_f64).unwrap(), b"1.0");
555        assert_eq!(to_canonical_json(&f64::from_bits(1)).unwrap(), b"5e-324");
556    }
557
558    #[test]
559    fn exact_limits_accept_boundary_and_reject_next_value() {
560        let value = CanonicalValue::String(crate::value::CanonicalString::new("abc").unwrap());
561        let bytes = to_canonical_json(&value).unwrap();
562        let mut limits = CodecLimits::CANONICAL;
563        limits.max_bytes = bytes.len();
564        assert!(from_canonical_json_with_limits::<CanonicalValue>(&bytes, limits).is_ok());
565        limits.max_bytes -= 1;
566        assert_eq!(
567            from_canonical_json_with_limits::<CanonicalValue>(&bytes, limits)
568                .unwrap_err()
569                .code()
570                .as_str(),
571            "canonical_json_too_large"
572        );
573    }
574
575    #[test]
576    fn required_versions_fail_closed() {
577        assert!(serde_json::from_str::<FormatVersion>(r#""1""#).is_err());
578        assert!(serde_json::from_str::<CodecVersion>(r#""1""#).is_err());
579        assert!(ensure_format_version(FormatVersion::V1, FormatVersion::V1).is_ok());
580        assert!(ensure_codec_version(CodecVersion::V1, CodecVersion::V1).is_ok());
581
582        assert_eq!(
583            ensure_format_version(FormatVersion::from_raw(2), FormatVersion::V1)
584                .unwrap_err()
585                .code()
586                .as_str(),
587            "unsupported_format_version",
588        );
589        assert_eq!(
590            ensure_codec_version(CodecVersion::from_raw(2), CodecVersion::V1)
591                .unwrap_err()
592                .code()
593                .as_str(),
594            "unsupported_codec_version",
595        );
596    }
597
598    #[test]
599    fn structural_limits_reject_depth_members_strings_and_keys() {
600        let base = CodecLimits {
601            max_bytes: 128,
602            max_depth: 8,
603            max_collection_len: 8,
604            max_string_bytes: 8,
605        };
606
607        let depth = CodecLimits {
608            max_depth: 2,
609            ..base
610        };
611        assert_eq!(
612            from_canonical_json_with_limits::<Value>(b"[[0]]", depth)
613                .unwrap_err()
614                .code()
615                .as_str(),
616            "canonical_json_too_deep",
617        );
618
619        let members = CodecLimits {
620            max_collection_len: 1,
621            ..base
622        };
623        assert_eq!(
624            from_canonical_json_with_limits::<Value>(b"[0,1]", members)
625                .unwrap_err()
626                .code()
627                .as_str(),
628            "canonical_collection_too_large",
629        );
630
631        let strings = CodecLimits {
632            max_string_bytes: 3,
633            ..base
634        };
635        for bytes in [br#""abcd""# as &[u8], br#"{"abcd":0}"# as &[u8]] {
636            assert_eq!(
637                from_canonical_json_with_limits::<Value>(bytes, strings)
638                    .unwrap_err()
639                    .code()
640                    .as_str(),
641                "canonical_string_too_large",
642            );
643        }
644    }
645}