Skip to main content

tf_types/
canonical.rs

1//! Deterministic JSON serialization compatible with the TypeScript
2//! implementation in `tools/tf-types-ts/src/core/canonical.ts`.
3//!
4//! Rules:
5//!   * Object keys are sorted by UTF-8 byte order of their NFC-normalized
6//!     form. (Rust's `String::cmp` compares underlying UTF-8 bytes; the
7//!     TS implementation uses an explicit UTF-8 byte comparator instead
8//!     of JS's UTF-16 code-unit `<`.)
9//!   * All string values are NFC-normalized.
10//!   * Finite integers emit as integers (no `.0`); finite non-integer numbers
11//!     emit via Rust's shortest round-trip representation, matching the
12//!     `serde_json` defaults which in turn match JavaScript's `String(n)`.
13//!   * `-0` is emitted as `0`.
14//!   * `NaN`, `±Infinity` are rejected.
15//!   * No whitespace anywhere in the output.
16//!
17//! Byte-for-byte parity with the TypeScript implementation is enforced by
18//! `conformance/canonical-vectors.yaml` and
19//! `conformance/cross-language-signature-vectors.yaml`.
20
21use std::fmt::Write;
22
23use serde_json::{Map, Value};
24
25#[derive(Debug, thiserror::Error)]
26pub enum CanonicalJsonError {
27    #[error("cannot canonicalize non-finite number: {0}")]
28    NonFinite(f64),
29}
30
31pub fn canonicalize(value: &Value) -> Result<String, CanonicalJsonError> {
32    let mut out = String::new();
33    encode(value, &mut out)?;
34    Ok(out)
35}
36
37fn encode(v: &Value, out: &mut String) -> Result<(), CanonicalJsonError> {
38    match v {
39        Value::Null => out.push_str("null"),
40        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
41        Value::Number(n) => {
42            if let Some(i) = n.as_i64() {
43                write!(out, "{}", i).unwrap();
44            } else if let Some(u) = n.as_u64() {
45                write!(out, "{}", u).unwrap();
46            } else if let Some(f) = n.as_f64() {
47                if !f.is_finite() {
48                    return Err(CanonicalJsonError::NonFinite(f));
49                }
50                if f == 0.0 {
51                    out.push('0');
52                } else if f.fract() == 0.0 && f.abs() < 1e16 {
53                    write!(out, "{}", f as i64).unwrap();
54                } else {
55                    write!(out, "{}", f).unwrap();
56                }
57            }
58        }
59        Value::String(s) => write_json_string(&nfc(s), out),
60        Value::Array(xs) => {
61            out.push('[');
62            for (i, x) in xs.iter().enumerate() {
63                if i > 0 {
64                    out.push(',');
65                }
66                encode(x, out)?;
67            }
68            out.push(']');
69        }
70        Value::Object(map) => {
71            out.push('{');
72            let mut entries: Vec<(String, &Value)> = map.iter().map(|(k, v)| (nfc(k), v)).collect();
73            entries.sort_by(|a, b| a.0.cmp(&b.0));
74            for (i, (k, v)) in entries.iter().enumerate() {
75                if i > 0 {
76                    out.push(',');
77                }
78                write_json_string(k, out);
79                out.push(':');
80                encode(v, out)?;
81            }
82            out.push('}');
83        }
84    }
85    Ok(())
86}
87
88fn nfc(s: &str) -> String {
89    use unicode_normalization::UnicodeNormalization;
90    UnicodeNormalization::nfc(s).collect()
91}
92
93fn write_json_string(s: &str, out: &mut String) {
94    out.push('"');
95    for c in s.chars() {
96        match c {
97            '"' => out.push_str("\\\""),
98            '\\' => out.push_str("\\\\"),
99            '\n' => out.push_str("\\n"),
100            '\r' => out.push_str("\\r"),
101            '\t' => out.push_str("\\t"),
102            '\u{08}' => out.push_str("\\b"),
103            '\u{0C}' => out.push_str("\\f"),
104            c if (c as u32) < 0x20 => {
105                write!(out, "\\u{:04x}", c as u32).unwrap();
106            }
107            c => out.push(c),
108        }
109    }
110    out.push('"');
111}
112
113/// Convenience for &serde_json::Map.
114pub fn canonicalize_map(map: &Map<String, Value>) -> Result<String, CanonicalJsonError> {
115    canonicalize(&Value::Object(map.clone()))
116}