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
117
118
119
120
121
122
123
//! Hand-rolled canonical JSON serializer — port of `CanonicalJson.java` (concept
//! of `canonicalStringify`). Reproduces `JSON.stringify(sortKeys(v), null, 2) +
//! "\n"` byte-for-byte: recursively key-sorted objects, 2-space indent, LF +
//! trailing newline, raw non-ASCII, control chars as `\uXXXX`.
use crate::value::Value;
use std::collections::BTreeMap;
use std::fmt::Write;
/// Serializes `value` to canonical JSON, with a trailing `"\n"`.
pub fn canonical_stringify(value: &Value) -> String {
let mut out = String::new();
write_value(&mut out, value, 0);
out.push('\n');
out
}
fn write_value(out: &mut String, value: &Value, depth: usize) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Int(i) => {
let _ = write!(out, "{i}");
}
Value::Float(d) => write_number(out, *d),
Value::String(s) => write_string(out, s),
Value::List(list) => write_array(out, list, depth),
Value::Map(map) => write_object(out, map, depth),
}
}
fn write_object(out: &mut String, map: &BTreeMap<String, Value>, depth: usize) {
if map.is_empty() {
out.push_str("{}");
return;
}
// The goldens were generated by JS `sort()` / Java `TreeMap`, which order by
// UTF-16 code units — NOT the code-point order `BTreeMap<String, _>` gives.
// The two differ only for keys mixing astral characters (≥ U+10000) with
// U+E000..U+FFFF, but byte-exactness is the whole contract, so re-sort.
let mut entries: Vec<(&String, &Value)> = map.iter().collect();
entries.sort_by(|(a, _), (b, _)| {
a.encode_utf16()
.collect::<Vec<u16>>()
.cmp(&b.encode_utf16().collect::<Vec<u16>>())
});
out.push_str("{\n");
let n = entries.len();
for (i, (key, val)) in entries.into_iter().enumerate() {
indent(out, depth + 1);
write_string(out, key);
out.push_str(": ");
write_value(out, val, depth + 1);
if i + 1 < n {
out.push(',');
}
out.push('\n');
}
indent(out, depth);
out.push('}');
}
fn write_array(out: &mut String, list: &[Value], depth: usize) {
if list.is_empty() {
out.push_str("[]");
return;
}
out.push_str("[\n");
let n = list.len();
for (i, item) in list.iter().enumerate() {
indent(out, depth + 1);
write_value(out, item, depth + 1);
if i + 1 < n {
out.push(',');
}
out.push('\n');
}
indent(out, depth);
out.push(']');
}
fn write_string(out: &mut String, s: &str) {
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{0008}' => out.push_str("\\b"),
'\u{000c}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", c as u32);
}
// Non-ASCII (and all other) characters are emitted raw.
c => out.push(c),
}
}
out.push('"');
}
fn write_number(out: &mut String, d: f64) {
// A finite integral double serializes as an integer (matching Java's
// `(long) d` when `d == Math.rint(d)`, and JS `JSON.stringify`).
//
// Known divergences from JS, none reachable by the corpus (its floats are
// small, e.g. bundle 15's 2.55/2.6): `d as i64` saturates beyond i64::MAX,
// where JS prints e.g. 1e21 as "1e+21"; and Rust's f64 `Display` is the
// shortest round-trip form, which differs from JS's number-to-string for
// some exotic magnitudes. Revisit if a golden ever pins such a value.
if d.is_finite() && d == d.trunc() {
let _ = write!(out, "{}", d as i64);
} else {
let _ = write!(out, "{d}");
}
}
fn indent(out: &mut String, depth: usize) {
for _ in 0..depth {
out.push_str(" ");
}
}