1use serde_json::Value;
18use std::fmt::Write as _;
19
20#[derive(Debug, thiserror::Error, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum JcsError {
24 #[error("integer {0} outside ±2^53; JCS would lose precision")]
26 UnsafeInteger(String),
27 #[error("non-finite number cannot be canonicalized")]
29 NonFinite,
30 #[error("value nesting exceeds {0}; JCS refuses to recurse further")]
42 TooDeep(usize),
43}
44
45const MAX_NESTED_DEPTH: usize = 128;
49
50pub fn canonicalize(value: &Value) -> Result<String, JcsError> {
52 let mut out = String::with_capacity(512);
55 write_value(value, &mut out, 0)?;
56 Ok(out)
57}
58
59fn write_value(value: &Value, out: &mut String, depth: usize) -> Result<(), JcsError> {
60 if depth > MAX_NESTED_DEPTH {
61 return Err(JcsError::TooDeep(MAX_NESTED_DEPTH));
62 }
63 match value {
64 Value::Null => out.push_str("null"),
65 Value::Bool(true) => out.push_str("true"),
66 Value::Bool(false) => out.push_str("false"),
67 Value::Number(n) => write_number(n, out)?,
68 Value::String(s) => write_string(s, out),
69 Value::Array(items) => {
70 out.push('[');
71 for (i, item) in items.iter().enumerate() {
72 if i > 0 {
73 out.push(',');
74 }
75 write_value(item, out, depth + 1)?;
76 }
77 out.push(']');
78 }
79 Value::Object(map) => {
80 let mut keys: Vec<&String> = map.keys().collect();
82 keys.sort_by(|a, b| utf16_cmp(a, b));
83 out.push('{');
84 for (i, key) in keys.iter().enumerate() {
85 if i > 0 {
86 out.push(',');
87 }
88 write_string(key, out);
89 out.push(':');
90 if let Some(v) = map.get(*key) {
91 write_value(v, out, depth + 1)?;
92 }
93 }
94 out.push('}');
95 }
96 }
97 Ok(())
98}
99
100fn utf16_cmp(a: &str, b: &str) -> std::cmp::Ordering {
102 a.encode_utf16().cmp(b.encode_utf16())
103}
104
105fn write_string(s: &str, out: &mut String) {
106 out.push('"');
107 for ch in s.chars() {
108 match ch {
109 '"' => out.push_str("\\\""),
110 '\\' => out.push_str("\\\\"),
111 '\u{0008}' => out.push_str("\\b"),
112 '\u{000C}' => out.push_str("\\f"),
113 '\n' => out.push_str("\\n"),
114 '\r' => out.push_str("\\r"),
115 '\t' => out.push_str("\\t"),
116 c if (c as u32) < 0x20 => {
117 let _ = write!(out, "\\u{:04x}", c as u32);
118 }
119 c => out.push(c),
120 }
121 }
122 out.push('"');
123}
124
125use av_core::error::JCS_SAFE_MAX;
126
127fn write_number(n: &serde_json::Number, out: &mut String) -> Result<(), JcsError> {
128 if let Some(u) = n.as_u64() {
129 if u > JCS_SAFE_MAX {
130 return Err(JcsError::UnsafeInteger(u.to_string()));
131 }
132 let _ = write!(out, "{u}");
133 return Ok(());
134 }
135 if let Some(i) = n.as_i64() {
136 if i < -(JCS_SAFE_MAX as i64) {
137 return Err(JcsError::UnsafeInteger(i.to_string()));
138 }
139 let _ = write!(out, "{i}");
140 return Ok(());
141 }
142 let f = n.as_f64().ok_or(JcsError::NonFinite)?;
143 if !f.is_finite() {
144 return Err(JcsError::NonFinite);
145 }
146 out.push_str(&ecma_number(f));
147 Ok(())
148}
149
150fn ecma_number(f: f64) -> String {
157 if f == 0.0 {
158 return "0".to_owned(); }
160 if f < 0.0 {
161 let mut s = String::with_capacity(24);
162 s.push('-');
163 s.push_str(&ecma_number(-f));
164 return s;
165 }
166 let mut buf = ryu::Buffer::new();
167 let printed = buf.format_finite(f);
168 let (digits, k) = digits_and_k(printed);
169 let digits = digits.as_str();
170 let n = digits.len() as i64;
171
172 if (1..=21).contains(&k) && n <= k {
173 let mut s = String::from(digits);
175 for _ in 0..(k - n) {
176 s.push('0');
177 }
178 s
179 } else if (1..=21).contains(&k) {
180 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
182 let (int_part, frac_part) = digits.split_at(k as usize);
183 format!("{int_part}.{frac_part}")
184 } else if (-5..=0).contains(&k) {
185 let mut s = String::from("0.");
187 for _ in 0..(-k) {
188 s.push('0');
189 }
190 s.push_str(digits);
191 s
192 } else {
193 let exp_val = k - 1;
195 let sign = if exp_val >= 0 { "+" } else { "-" };
196 let (first, rest) = digits.split_at(1);
197 if rest.is_empty() {
198 format!("{first}e{sign}{}", exp_val.abs())
199 } else {
200 format!("{first}.{rest}e{sign}{}", exp_val.abs())
201 }
202 }
203}
204
205fn digits_and_k(printed: &str) -> (String, i64) {
209 let (mantissa, e) = match printed.split_once(['e', 'E']) {
210 Some((m, exp)) => (m, exp.parse::<i64>().unwrap_or(0)),
211 None => (printed, 0),
212 };
213 let (int_part, frac_part) = match mantissa.split_once('.') {
214 Some((i, f)) => (i, f),
215 None => (mantissa, ""),
216 };
217 let int_stripped = int_part.trim_start_matches('0');
218 let (raw, k) = if int_stripped.is_empty() {
219 let lead_zeros = frac_part.len() - frac_part.trim_start_matches('0').len();
220 (
221 frac_part.trim_start_matches('0').to_owned(),
222 e - lead_zeros as i64,
223 )
224 } else {
225 (
226 format!("{int_stripped}{frac_part}"),
227 int_stripped.len() as i64 + e,
228 )
229 };
230 let digits = raw.trim_end_matches('0');
231 if digits.is_empty() {
232 ("0".to_owned(), k)
233 } else {
234 (digits.to_owned(), k)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
241
242 use super::*;
243 use serde_json::json;
244
245 #[test]
247 fn rfc8785_appendix_b_numbers() {
248 let vectors: &[(u64, &str)] = &[
249 (0x0000000000000000, "0"),
250 (0x8000000000000000, "0"), (0x0000000000000001, "5e-324"),
252 (0x8000000000000001, "-5e-324"),
253 (0x7fefffffffffffff, "1.7976931348623157e+308"),
254 (0xffefffffffffffff, "-1.7976931348623157e+308"),
255 (0x4340000000000000, "9007199254740992"),
256 (0xc340000000000000, "-9007199254740992"),
257 (0x444b1ae4d6e2ef50, "1e+21"),
258 (0x3eb0c6f7a0b5ed8d, "0.000001"),
259 (0x3e7ad7f29abcaf48, "1e-7"),
260 (0x41b3de4355555553, "333333333.3333332"),
261 (0x41b3de4355555554, "333333333.33333325"),
262 (0x41b3de4355555555, "333333333.3333333"),
263 (0x41b3de4355555556, "333333333.3333334"),
264 (0x41b3de4355555557, "333333333.33333343"),
265 (0xbecbf647612f3696, "-0.0000033333333333333333"),
266 (0x43143ff3c1cb0959, "1424953923781206.2"),
267 ];
268 for (bits, expected) in vectors {
269 let f = f64::from_bits(*bits);
270 assert_eq!(&ecma_number(f), expected, "bits {bits:#018x}");
271 }
272 }
273
274 #[test]
275 fn integer_forms() {
276 assert_eq!(ecma_number(1.0), "1");
277 assert_eq!(ecma_number(42.0), "42");
278 assert_eq!(ecma_number(100000.0), "100000");
279 assert_eq!(ecma_number(1e20), "100000000000000000000");
280 assert_eq!(ecma_number(1e21), "1e+21");
281 assert_eq!(ecma_number(-1e21), "-1e+21");
282 }
283
284 #[test]
285 fn fraction_forms() {
286 assert_eq!(ecma_number(0.5), "0.5");
287 assert_eq!(ecma_number(0.000001), "0.000001");
288 assert_eq!(ecma_number(0.0000001), "1e-7");
289 assert_eq!(ecma_number(1.5), "1.5");
290 assert_eq!(ecma_number(1.2345678901234567), "1.2345678901234567");
291 }
292
293 #[test]
295 fn rfc8785_key_sorting() {
296 let v: Value = serde_json::from_str(r#"{"\uFB33": 3, "\uD83D\uDE02": 2, "\u20AC": 1}"#).unwrap();
301 let c = canonicalize(&v).unwrap();
302 assert_eq!(c, "{\"\u{20ac}\":1,\"\u{1f602}\":2,\"\u{fb33}\":3}");
303 }
304
305 #[test]
307 fn rfc8785_structure_vector() {
308 let input = r#"{
309 "numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
310 "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
311 "literals": [null, true, false]
312 }"#;
313 let v: Value = serde_json::from_str(input).unwrap();
314 let c = canonicalize(&v).unwrap();
315 let expected = "{\"literals\":[null,true,false],\"numbers\":[333333333.3333333,1e+30,4.5,0.002,1e-27],\"string\":\"€$\\u000f\\nA'B\\\"\\\\\\\\\\\"/\"}";
316 assert_eq!(c, expected);
317 }
318
319 #[test]
320 fn string_escapes() {
321 let v = json!({"a": "line\nbreak\ttab\u{0001}ctl\"quote\\back"});
322 let c = canonicalize(&v).unwrap();
323 assert_eq!(c, "{\"a\":\"line\\nbreak\\ttab\\u0001ctl\\\"quote\\\\back\"}");
324 }
325
326 #[test]
327 fn unsafe_integers_rejected() {
328 let over = serde_json::Number::from((1u64 << 53) + 1);
329 let v = Value::Number(over);
330 assert_eq!(
331 canonicalize(&v),
332 Err(JcsError::UnsafeInteger(((1u64 << 53) + 1).to_string()))
333 );
334 let under = serde_json::Number::from(-(1i64 << 53) - 1);
335 assert!(canonicalize(&Value::Number(under)).is_err());
336 assert!(canonicalize(&json!(1u64 << 53)).is_ok());
338 assert!(canonicalize(&json!(-(1i64 << 53))).is_ok());
339 }
340
341 #[test]
342 fn insertion_order_does_not_matter() {
343 let a: Value = serde_json::from_str(r#"{"z":1,"a":2,"m":{"y":1,"b":2}}"#).unwrap();
346 let b: Value = serde_json::from_str(r#"{"a":2,"m":{"b":2,"y":1},"z":1}"#).unwrap();
347 assert_eq!(canonicalize(&a).unwrap(), canonicalize(&b).unwrap());
348 }
349
350 #[test]
351 fn canonicalization_is_idempotent() {
352 let v = json!({"b": [1.5, "x", {"k": 1e21}], "a": null});
353 let once = canonicalize(&v).unwrap();
354 let reparsed: Value = serde_json::from_str(&once).unwrap();
355 let twice = canonicalize(&reparsed).unwrap();
356 assert_eq!(once, twice);
357 }
358
359 #[test]
360 fn empty_containers() {
361 assert_eq!(canonicalize(&json!({})).unwrap(), "{}");
362 assert_eq!(canonicalize(&json!([])).unwrap(), "[]");
363 assert_eq!(canonicalize(&json!("")).unwrap(), "\"\"");
364 }
365
366 #[test]
367 fn space_at_boundary_u0020_is_not_escaped() {
368 let v = json!({"a": "a b"});
372 assert_eq!(canonicalize(&v).unwrap(), "{\"a\":\"a b\"}");
373 let v2 = json!({"a": "a\u{001f}b"});
375 assert_eq!(canonicalize(&v2).unwrap(), "{\"a\":\"a\\u001fb\"}");
376 }
377
378 #[test]
379 fn positive_zero_never_prints_with_a_minus_sign() {
380 assert_eq!(ecma_number(0.0), "0");
385 assert_eq!(ecma_number(-0.0), "0");
386 }
387
388 #[test]
402 fn canonicalize_refuses_pathologically_nested_objects() {
403 let mut v = Value::Null;
404 for _ in 0..MAX_NESTED_DEPTH + 10 {
405 let mut map = serde_json::Map::new();
406 map.insert("k".to_owned(), v);
407 v = Value::Object(map);
408 }
409 assert_eq!(canonicalize(&v), Err(JcsError::TooDeep(MAX_NESTED_DEPTH)));
410 }
411
412 #[test]
413 fn canonicalize_refuses_pathologically_nested_arrays() {
414 let mut v = Value::Array(vec![Value::Null]);
415 for _ in 0..MAX_NESTED_DEPTH + 10 {
416 v = Value::Array(vec![v]);
417 }
418 assert_eq!(canonicalize(&v), Err(JcsError::TooDeep(MAX_NESTED_DEPTH)));
419 }
420
421 #[test]
422 fn canonicalize_accepts_depths_up_to_the_cap() {
423 let mut v = Value::Null;
427 for _ in 0..MAX_NESTED_DEPTH {
428 v = Value::Array(vec![v]);
429 }
430 assert!(canonicalize(&v).is_ok());
431 }
432}