1use std::fmt;
11use std::fmt::Write as _;
12
13use serde_json::{Number, Value};
14
15const MAX_SAFE_INTEGER: u64 = (1 << 53) - 1;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum CanonicalJsonError {
23 NotAnInteger(String),
25 IntegerOutOfRange(String),
27}
28
29impl fmt::Display for CanonicalJsonError {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 CanonicalJsonError::NotAnInteger(n) => {
33 write!(
34 f,
35 "{n} is not an integer; the transcript records integers only"
36 )
37 }
38 CanonicalJsonError::IntegerOutOfRange(n) => {
39 write!(f, "{n} is beyond the integer range the transcript records")
40 }
41 }
42 }
43}
44
45impl std::error::Error for CanonicalJsonError {}
46
47pub fn canonical_json(value: &Value) -> Result<String, CanonicalJsonError> {
68 let mut out = String::new();
69 write_value(value, &mut out)?;
70 Ok(out)
71}
72
73pub fn check_numbers(value: &Value) -> Result<(), CanonicalJsonError> {
83 match value {
84 Value::Null | Value::Bool(_) | Value::String(_) => Ok(()),
85 Value::Number(n) => check_number(n),
86 Value::Array(items) => items.iter().try_for_each(check_numbers),
87 Value::Object(members) => members.values().try_for_each(check_numbers),
88 }
89}
90
91fn check_number(n: &Number) -> Result<(), CanonicalJsonError> {
92 let magnitude = if let Some(i) = n.as_i64() {
93 i.unsigned_abs()
94 } else if let Some(u) = n.as_u64() {
95 u
96 } else {
97 return Err(CanonicalJsonError::NotAnInteger(n.to_string()));
98 };
99 if magnitude > MAX_SAFE_INTEGER {
100 return Err(CanonicalJsonError::IntegerOutOfRange(n.to_string()));
101 }
102 Ok(())
103}
104
105fn write_value(value: &Value, out: &mut String) -> Result<(), CanonicalJsonError> {
106 match value {
107 Value::Null => out.push_str("null"),
108 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
109 Value::Number(n) => {
110 check_number(n)?;
111 out.push_str(&n.to_string());
112 }
113 Value::String(s) => write_string(s, out),
114 Value::Array(items) => {
115 out.push('[');
116 for (i, item) in items.iter().enumerate() {
117 if i > 0 {
118 out.push(',');
119 }
120 write_value(item, out)?;
121 }
122 out.push(']');
123 }
124 Value::Object(map) => {
125 let mut members: Vec<(&String, &Value)> = map.iter().collect();
129 members.sort_by(|a, b| a.0.encode_utf16().cmp(b.0.encode_utf16()));
130 out.push('{');
131 for (i, (name, member)) in members.into_iter().enumerate() {
132 if i > 0 {
133 out.push(',');
134 }
135 write_string(name, out);
136 out.push(':');
137 write_value(member, out)?;
138 }
139 out.push('}');
140 }
141 }
142 Ok(())
143}
144
145fn write_string(s: &str, out: &mut String) {
149 out.push('"');
150 for c in s.chars() {
151 match c {
152 '"' => out.push_str("\\\""),
153 '\\' => out.push_str("\\\\"),
154 '\u{08}' => out.push_str("\\b"),
155 '\t' => out.push_str("\\t"),
156 '\n' => out.push_str("\\n"),
157 '\u{0C}' => out.push_str("\\f"),
158 '\r' => out.push_str("\\r"),
159 c if c < ' ' => {
160 let _ = write!(out, "\\u{:04x}", u32::from(c));
161 }
162 c => out.push(c),
163 }
164 }
165 out.push('"');
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use serde_json::json;
172
173 #[test]
174 fn members_are_ordered_by_utf16_code_units() {
175 let value: Value = serde_json::from_str(
177 r#"{"\u20ac":"Euro Sign","\r":"Carriage Return","\ufb33":"Hebrew Letter Dalet With Dagesh","1":"One","\ud83d\ude00":"Emoji: Grinning Face","\u0080":"Control","\u00f6":"Latin Small Letter O With Diaeresis"}"#,
178 )
179 .expect("parse");
180 let expected = concat!(
183 r#"{"\r":"Carriage Return","1":"One","#,
184 "\"\u{80}\":\"Control\",",
185 "\"\u{f6}\":\"Latin Small Letter O With Diaeresis\",",
186 "\"\u{20ac}\":\"Euro Sign\",",
187 "\"\u{1f600}\":\"Emoji: Grinning Face\",",
188 "\"\u{fb33}\":\"Hebrew Letter Dalet With Dagesh\"}",
189 );
190 assert_eq!(canonical_json(&value).expect("canonical"), expected);
191 }
192
193 #[test]
194 fn strings_use_the_rfc_escapes() {
195 let value: Value =
197 serde_json::from_str(r#""\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/""#)
198 .expect("parse");
199 assert_eq!(
200 canonical_json(&value).expect("canonical"),
201 r#""€$\u000f\nA'B\"\\\\\"/""#
202 );
203 }
204
205 #[test]
206 fn nesting_and_literals() {
207 let value = json!({ "b": [true, null, -3, {"z": 1, "a": "x"}], "a": 0 });
208 assert_eq!(
209 canonical_json(&value).expect("canonical"),
210 r#"{"a":0,"b":[true,null,-3,{"a":"x","z":1}]}"#
211 );
212 }
213
214 #[test]
215 fn floats_and_large_integers_are_refused() {
216 assert!(matches!(
217 canonical_json(&json!(1.5)),
218 Err(CanonicalJsonError::NotAnInteger(_))
219 ));
220 assert!(matches!(
221 canonical_json(&json!(1_u64 << 53)),
222 Err(CanonicalJsonError::IntegerOutOfRange(_))
223 ));
224 assert!(canonical_json(&json!(MAX_SAFE_INTEGER)).is_ok());
225 assert!(canonical_json(&json!(-(1_i64 << 53) + 1)).is_ok());
226 }
227
228 #[test]
229 fn check_numbers_walks_nested_values() {
230 assert!(check_numbers(&json!({ "a": [1, "x", { "b": -2 }] })).is_ok());
231 assert!(matches!(
232 check_numbers(&json!({ "a": [1, { "b": 0.5 }] })),
233 Err(CanonicalJsonError::NotAnInteger(_))
234 ));
235 assert!(matches!(
236 check_numbers(&json!([u64::MAX])),
237 Err(CanonicalJsonError::IntegerOutOfRange(_))
238 ));
239 }
240}