Skip to main content

type_bridge_contract/
codec.rs

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