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