1use std::fmt::Write as _;
28
29use serde_json::Value;
30
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34pub enum CanonError {
35 #[error("canonical_json: non-finite number {0}")]
36 NonFiniteNumber(String),
37 #[error("canonical_json: number representation not i64/u64/f64")]
38 UnrepresentableNumber,
39}
40
41pub fn write_into(out: &mut String, v: &Value) -> Result<(), CanonError> {
49 match v {
50 Value::Null => out.push_str("null"),
51 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
52 Value::Number(n) => write_number(out, n)?,
53 Value::String(s) => write_string(out, s),
54 Value::Array(xs) => {
55 out.push('[');
56 for (i, x) in xs.iter().enumerate() {
57 if i > 0 {
58 out.push(',');
59 }
60 write_into(out, x)?;
61 }
62 out.push(']');
63 }
64 Value::Object(xs) => {
65 out.push('{');
66 let mut keys: Vec<&String> = xs.keys().collect();
67 keys.sort();
68 for (i, k) in keys.iter().enumerate() {
69 if i > 0 {
70 out.push(',');
71 }
72 write_string(out, k);
73 out.push(':');
74 write_into(out, &xs[*k])?;
75 }
76 out.push('}');
77 }
78 }
79 Ok(())
80}
81
82pub fn to_string(v: &Value) -> Result<String, CanonError> {
87 let mut s = String::new();
88 write_into(&mut s, v)?;
89 Ok(s)
90}
91
92pub fn write_into_lossy(out: &mut String, v: &Value) {
97 if let Err(e) = write_into(out, v) {
98 let _ = write!(out, "__canon_error[{e}]__");
99 }
100}
101
102fn write_number(out: &mut String, n: &serde_json::Number) -> Result<(), CanonError> {
103 if let Some(i) = n.as_i64() {
104 let _ = write!(out, "{i}");
105 } else if let Some(u) = n.as_u64() {
106 let _ = write!(out, "{u}");
107 } else if let Some(f) = n.as_f64() {
108 if !f.is_finite() {
109 return Err(CanonError::NonFiniteNumber(f.to_string()));
110 }
111 let _ = write!(out, "{f}");
112 } else {
113 return Err(CanonError::UnrepresentableNumber);
114 }
115 Ok(())
116}
117
118fn write_string(out: &mut String, s: &str) {
119 out.push('"');
120 for c in s.chars() {
121 match c {
122 '"' => out.push_str("\\\""),
123 '\\' => out.push_str("\\\\"),
124 '\u{08}' => out.push_str("\\b"),
125 '\u{0c}' => out.push_str("\\f"),
126 '\n' => out.push_str("\\n"),
127 '\r' => out.push_str("\\r"),
128 '\t' => out.push_str("\\t"),
129 c if (c as u32) < 0x20 => {
130 let _ = write!(out, "\\u{:04x}", c as u32);
131 }
132 c => out.push(c),
133 }
134 }
135 out.push('"');
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn canon(v: &serde_json::Value) -> String {
143 to_string(v).expect("canonicalize")
144 }
145
146 #[test]
147 fn null_bool_emit_literal_tokens() {
148 assert_eq!(canon(&serde_json::Value::Null), "null");
149 assert_eq!(canon(&serde_json::json!(true)), "true");
150 assert_eq!(canon(&serde_json::json!(false)), "false");
151 }
152
153 #[test]
154 fn object_keys_emit_in_sorted_order() {
155 let v = serde_json::json!({ "b": 1, "a": 2, "c": 3 });
156 assert_eq!(canon(&v), r#"{"a":2,"b":1,"c":3}"#);
157 }
158
159 #[test]
160 fn integers_prefer_integer_form_over_float() {
161 let i = serde_json::json!(42);
162 let f = serde_json::json!(42.0);
163 assert_eq!(canon(&i), "42");
167 assert_eq!(canon(&f), "42");
168 }
169
170 #[test]
171 fn large_unsigned_falls_back_to_u64() {
172 let v = serde_json::json!(u64::MAX);
173 assert_eq!(canon(&v), u64::MAX.to_string());
174 }
175
176 #[test]
177 fn negative_integer_round_trips_through_canon() {
178 let v = serde_json::json!(-1_234_567_890_i64);
179 assert_eq!(canon(&v), "-1234567890");
180 }
181
182 #[test]
183 fn fractional_float_emits_decimal_form() {
184 let v = serde_json::json!(3.5);
185 assert_eq!(canon(&v), "3.5");
186 }
187
188 #[test]
189 fn nan_and_inf_rejected_via_explicit_error() {
190 let n: serde_json::Number =
194 serde_json::from_str("NaN").unwrap_or_else(|_| serde_json::Number::from(0));
195 let _ = n;
198 }
199
200 #[test]
201 fn string_escapes_match_json_spec_subset() {
202 let v = serde_json::json!("a\"b\\c\nd\te\u{0c}f\u{08}g\u{01}");
205 let got = canon(&v);
206 let expected = String::from("\"a\\\"b\\\\c\\nd\\te\\ff\\bg\\u0001\"");
207 assert_eq!(got, expected);
208 }
209
210 #[test]
211 fn array_emits_no_trailing_comma() {
212 let v = serde_json::json!([1, 2, 3]);
213 assert_eq!(canon(&v), "[1,2,3]");
214 }
215
216 #[test]
217 fn deeply_nested_object_canonicalizes_recursively() {
218 let v = serde_json::json!({ "z": { "y": [3, 2, 1] }, "a": null });
219 assert_eq!(canon(&v), r#"{"a":null,"z":{"y":[3,2,1]}}"#);
220 }
221
222 #[test]
223 fn write_into_lossy_never_errors_for_normal_input() {
224 let mut out = String::new();
225 write_into_lossy(&mut out, &serde_json::json!({ "x": 1 }));
226 assert_eq!(out, r#"{"x":1}"#);
227 }
228}