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 #[allow(clippy::cast_precision_loss)] if f.abs() > JCS_SAFE_MAX as f64 && f.abs() < 1e21 {
157 return Err(JcsError::UnsafeInteger(ecma_number(f)));
158 }
159 out.push_str(&ecma_number(f));
160 Ok(())
161}
162
163fn ecma_number(f: f64) -> String {
170 if f == 0.0 {
171 return "0".to_owned(); }
173 if f < 0.0 {
174 let mut s = String::with_capacity(24);
175 s.push('-');
176 s.push_str(&ecma_number(-f));
177 return s;
178 }
179 let mut buf = ryu::Buffer::new();
180 let printed = buf.format_finite(f);
181 let (digits, k) = digits_and_k(printed);
182 let digits = digits.as_str();
183 let n = digits.len() as i64;
184
185 if (1..=21).contains(&k) && n <= k {
186 let mut s = String::from(digits);
188 for _ in 0..(k - n) {
189 s.push('0');
190 }
191 s
192 } else if (1..=21).contains(&k) {
193 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
195 let (int_part, frac_part) = digits.split_at(k as usize);
196 format!("{int_part}.{frac_part}")
197 } else if (-5..=0).contains(&k) {
198 let mut s = String::from("0.");
200 for _ in 0..(-k) {
201 s.push('0');
202 }
203 s.push_str(digits);
204 s
205 } else {
206 let exp_val = k - 1;
208 let sign = if exp_val >= 0 { "+" } else { "-" };
209 let (first, rest) = digits.split_at(1);
210 if rest.is_empty() {
211 format!("{first}e{sign}{}", exp_val.abs())
212 } else {
213 format!("{first}.{rest}e{sign}{}", exp_val.abs())
214 }
215 }
216}
217
218fn digits_and_k(printed: &str) -> (String, i64) {
222 let (mantissa, e) = match printed.split_once(['e', 'E']) {
223 Some((m, exp)) => (m, exp.parse::<i64>().unwrap_or(0)),
224 None => (printed, 0),
225 };
226 let (int_part, frac_part) = match mantissa.split_once('.') {
227 Some((i, f)) => (i, f),
228 None => (mantissa, ""),
229 };
230 let int_stripped = int_part.trim_start_matches('0');
231 let (raw, k) = if int_stripped.is_empty() {
232 let lead_zeros = frac_part.len() - frac_part.trim_start_matches('0').len();
233 (
234 frac_part.trim_start_matches('0').to_owned(),
235 e - lead_zeros as i64,
236 )
237 } else {
238 (
239 format!("{int_stripped}{frac_part}"),
240 int_stripped.len() as i64 + e,
241 )
242 };
243 let digits = raw.trim_end_matches('0');
244 if digits.is_empty() {
245 ("0".to_owned(), k)
246 } else {
247 (digits.to_owned(), k)
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
254
255 use super::*;
256 use serde_json::json;
257
258 #[test]
260 fn rfc8785_appendix_b_numbers() {
261 let vectors: &[(u64, &str)] = &[
262 (0x0000000000000000, "0"),
263 (0x8000000000000000, "0"), (0x0000000000000001, "5e-324"),
265 (0x8000000000000001, "-5e-324"),
266 (0x7fefffffffffffff, "1.7976931348623157e+308"),
267 (0xffefffffffffffff, "-1.7976931348623157e+308"),
268 (0x4340000000000000, "9007199254740992"),
269 (0xc340000000000000, "-9007199254740992"),
270 (0x444b1ae4d6e2ef50, "1e+21"),
271 (0x3eb0c6f7a0b5ed8d, "0.000001"),
272 (0x3e7ad7f29abcaf48, "1e-7"),
273 (0x41b3de4355555553, "333333333.3333332"),
274 (0x41b3de4355555554, "333333333.33333325"),
275 (0x41b3de4355555555, "333333333.3333333"),
276 (0x41b3de4355555556, "333333333.3333334"),
277 (0x41b3de4355555557, "333333333.33333343"),
278 (0xbecbf647612f3696, "-0.0000033333333333333333"),
279 (0x43143ff3c1cb0959, "1424953923781206.2"),
280 ];
281 for (bits, expected) in vectors {
282 let f = f64::from_bits(*bits);
283 assert_eq!(&ecma_number(f), expected, "bits {bits:#018x}");
284 }
285 }
286
287 #[test]
288 fn integer_forms() {
289 assert_eq!(ecma_number(1.0), "1");
290 assert_eq!(ecma_number(42.0), "42");
291 assert_eq!(ecma_number(100000.0), "100000");
292 assert_eq!(ecma_number(1e20), "100000000000000000000");
293 assert_eq!(ecma_number(1e21), "1e+21");
294 assert_eq!(ecma_number(-1e21), "-1e+21");
295 }
296
297 #[test]
303 fn integer_form_doubles_beyond_2_53_are_refused() {
304 let refused = [2.993466907830268e18, 9007199254740994.0, -9.999e20];
305 for f in refused {
306 let value = json!([f]);
307 assert!(
308 matches!(canonicalize(&value), Err(JcsError::UnsafeInteger(_))),
309 "expected UnsafeInteger for {f}"
310 );
311 }
312 for f in [9007199254740992.0f64, -9007199254740992.0, 1e21, 1e300, -5e-324] {
314 let first = canonicalize(&json!([f])).unwrap();
315 let reparsed: Value = serde_json::from_str(&first).unwrap();
316 assert_eq!(
317 canonicalize(&reparsed).unwrap(),
318 first,
319 "canonical form of {f} must re-canonicalize identically"
320 );
321 }
322 }
323
324 #[test]
325 fn fraction_forms() {
326 assert_eq!(ecma_number(0.5), "0.5");
327 assert_eq!(ecma_number(0.000001), "0.000001");
328 assert_eq!(ecma_number(0.0000001), "1e-7");
329 assert_eq!(ecma_number(1.5), "1.5");
330 assert_eq!(ecma_number(1.2345678901234567), "1.2345678901234567");
331 }
332
333 #[test]
335 fn rfc8785_key_sorting() {
336 let v: Value = serde_json::from_str(r#"{"\uFB33": 3, "\uD83D\uDE02": 2, "\u20AC": 1}"#).unwrap();
341 let c = canonicalize(&v).unwrap();
342 assert_eq!(c, "{\"\u{20ac}\":1,\"\u{1f602}\":2,\"\u{fb33}\":3}");
343 }
344
345 #[test]
347 fn rfc8785_structure_vector() {
348 let input = r#"{
349 "numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001],
350 "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/",
351 "literals": [null, true, false]
352 }"#;
353 let v: Value = serde_json::from_str(input).unwrap();
354 let c = canonicalize(&v).unwrap();
355 let expected = "{\"literals\":[null,true,false],\"numbers\":[333333333.3333333,1e+30,4.5,0.002,1e-27],\"string\":\"€$\\u000f\\nA'B\\\"\\\\\\\\\\\"/\"}";
356 assert_eq!(c, expected);
357 }
358
359 #[test]
360 fn string_escapes() {
361 let v = json!({"a": "line\nbreak\ttab\u{0001}ctl\"quote\\back"});
362 let c = canonicalize(&v).unwrap();
363 assert_eq!(c, "{\"a\":\"line\\nbreak\\ttab\\u0001ctl\\\"quote\\\\back\"}");
364 }
365
366 #[test]
367 fn unsafe_integers_rejected() {
368 let over = serde_json::Number::from((1u64 << 53) + 1);
369 let v = Value::Number(over);
370 assert_eq!(
371 canonicalize(&v),
372 Err(JcsError::UnsafeInteger(((1u64 << 53) + 1).to_string()))
373 );
374 let under = serde_json::Number::from(-(1i64 << 53) - 1);
375 assert!(canonicalize(&Value::Number(under)).is_err());
376 assert!(canonicalize(&json!(1u64 << 53)).is_ok());
378 assert!(canonicalize(&json!(-(1i64 << 53))).is_ok());
379 }
380
381 #[test]
382 fn insertion_order_does_not_matter() {
383 let a: Value = serde_json::from_str(r#"{"z":1,"a":2,"m":{"y":1,"b":2}}"#).unwrap();
386 let b: Value = serde_json::from_str(r#"{"a":2,"m":{"b":2,"y":1},"z":1}"#).unwrap();
387 assert_eq!(canonicalize(&a).unwrap(), canonicalize(&b).unwrap());
388 }
389
390 #[test]
391 fn canonicalization_is_idempotent() {
392 let v = json!({"b": [1.5, "x", {"k": 1e21}], "a": null});
393 let once = canonicalize(&v).unwrap();
394 let reparsed: Value = serde_json::from_str(&once).unwrap();
395 let twice = canonicalize(&reparsed).unwrap();
396 assert_eq!(once, twice);
397 }
398
399 #[test]
400 fn empty_containers() {
401 assert_eq!(canonicalize(&json!({})).unwrap(), "{}");
402 assert_eq!(canonicalize(&json!([])).unwrap(), "[]");
403 assert_eq!(canonicalize(&json!("")).unwrap(), "\"\"");
404 }
405
406 #[test]
407 fn space_at_boundary_u0020_is_not_escaped() {
408 let v = json!({"a": "a b"});
412 assert_eq!(canonicalize(&v).unwrap(), "{\"a\":\"a b\"}");
413 let v2 = json!({"a": "a\u{001f}b"});
415 assert_eq!(canonicalize(&v2).unwrap(), "{\"a\":\"a\\u001fb\"}");
416 }
417
418 #[test]
419 fn positive_zero_never_prints_with_a_minus_sign() {
420 assert_eq!(ecma_number(0.0), "0");
425 assert_eq!(ecma_number(-0.0), "0");
426 }
427
428 #[test]
442 fn canonicalize_refuses_pathologically_nested_objects() {
443 let mut v = Value::Null;
444 for _ in 0..MAX_NESTED_DEPTH + 10 {
445 let mut map = serde_json::Map::new();
446 map.insert("k".to_owned(), v);
447 v = Value::Object(map);
448 }
449 assert_eq!(canonicalize(&v), Err(JcsError::TooDeep(MAX_NESTED_DEPTH)));
450 }
451
452 #[test]
453 fn canonicalize_refuses_pathologically_nested_arrays() {
454 let mut v = Value::Array(vec![Value::Null]);
455 for _ in 0..MAX_NESTED_DEPTH + 10 {
456 v = Value::Array(vec![v]);
457 }
458 assert_eq!(canonicalize(&v), Err(JcsError::TooDeep(MAX_NESTED_DEPTH)));
459 }
460
461 #[test]
462 fn canonicalize_accepts_depths_up_to_the_cap() {
463 let mut v = Value::Null;
467 for _ in 0..MAX_NESTED_DEPTH {
468 v = Value::Array(vec![v]);
469 }
470 assert!(canonicalize(&v).is_ok());
471 }
472}