1use std::fmt::Write;
22
23use serde_json::{Map, Value};
24
25#[derive(Debug, thiserror::Error)]
26pub enum CanonicalJsonError {
27 #[error("cannot canonicalize non-finite number: {0}")]
28 NonFinite(f64),
29}
30
31pub fn canonicalize(value: &Value) -> Result<String, CanonicalJsonError> {
32 let mut out = String::new();
33 encode(value, &mut out)?;
34 Ok(out)
35}
36
37fn encode(v: &Value, out: &mut String) -> Result<(), CanonicalJsonError> {
38 match v {
39 Value::Null => out.push_str("null"),
40 Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
41 Value::Number(n) => {
42 if let Some(i) = n.as_i64() {
43 write!(out, "{}", i).unwrap();
44 } else if let Some(u) = n.as_u64() {
45 write!(out, "{}", u).unwrap();
46 } else if let Some(f) = n.as_f64() {
47 if !f.is_finite() {
48 return Err(CanonicalJsonError::NonFinite(f));
49 }
50 if f == 0.0 {
51 out.push('0');
52 } else if f.fract() == 0.0 && f.abs() < 1e16 {
53 write!(out, "{}", f as i64).unwrap();
54 } else {
55 write!(out, "{}", f).unwrap();
56 }
57 }
58 }
59 Value::String(s) => write_json_string(&nfc(s), out),
60 Value::Array(xs) => {
61 out.push('[');
62 for (i, x) in xs.iter().enumerate() {
63 if i > 0 {
64 out.push(',');
65 }
66 encode(x, out)?;
67 }
68 out.push(']');
69 }
70 Value::Object(map) => {
71 out.push('{');
72 let mut entries: Vec<(String, &Value)> = map.iter().map(|(k, v)| (nfc(k), v)).collect();
73 entries.sort_by(|a, b| a.0.cmp(&b.0));
74 for (i, (k, v)) in entries.iter().enumerate() {
75 if i > 0 {
76 out.push(',');
77 }
78 write_json_string(k, out);
79 out.push(':');
80 encode(v, out)?;
81 }
82 out.push('}');
83 }
84 }
85 Ok(())
86}
87
88fn nfc(s: &str) -> String {
89 use unicode_normalization::UnicodeNormalization;
90 UnicodeNormalization::nfc(s).collect()
91}
92
93fn write_json_string(s: &str, out: &mut String) {
94 out.push('"');
95 for c in s.chars() {
96 match c {
97 '"' => out.push_str("\\\""),
98 '\\' => out.push_str("\\\\"),
99 '\n' => out.push_str("\\n"),
100 '\r' => out.push_str("\\r"),
101 '\t' => out.push_str("\\t"),
102 '\u{08}' => out.push_str("\\b"),
103 '\u{0C}' => out.push_str("\\f"),
104 c if (c as u32) < 0x20 => {
105 write!(out, "\\u{:04x}", c as u32).unwrap();
106 }
107 c => out.push(c),
108 }
109 }
110 out.push('"');
111}
112
113pub fn canonicalize_map(map: &Map<String, Value>) -> Result<String, CanonicalJsonError> {
115 canonicalize(&Value::Object(map.clone()))
116}