1use crate::error::{SignatifError, SignatifResult};
18use serde_json::Value;
19
20pub fn canonicalize(value: &Value) -> SignatifResult<String> {
27 let mut out = String::new();
28 write_value(value, &mut out)?;
29 Ok(out)
30}
31
32pub fn canonical_hash(value: &Value) -> SignatifResult<[u8; 32]> {
39 use sha2::{Digest, Sha256};
40 let canon = canonicalize(value)?;
41 Ok(Sha256::digest(canon.as_bytes()).into())
42}
43
44fn write_value(v: &Value, out: &mut String) -> SignatifResult<()> {
45 match v {
46 Value::Null => out.push_str("null"),
47 Value::Bool(true) => out.push_str("true"),
48 Value::Bool(false) => out.push_str("false"),
49 Value::Number(n) => write_number(n, out)?,
50 Value::String(s) => write_string(s, out),
51 Value::Array(items) => {
52 out.push('[');
53 for (i, item) in items.iter().enumerate() {
54 if i > 0 {
55 out.push(',');
56 }
57 write_value(item, out)?;
58 }
59 out.push(']');
60 }
61 Value::Object(map) => {
62 let mut keys: Vec<&String> = map.keys().collect();
64 keys.sort_by(|a, b| utf16_cmp(a, b));
65 out.push('{');
66 for (i, key) in keys.iter().enumerate() {
67 if i > 0 {
68 out.push(',');
69 }
70 write_string(key, out);
71 out.push(':');
72 write_value(&map[*key], out)?;
73 }
74 out.push('}');
75 }
76 }
77 Ok(())
78}
79
80fn utf16_cmp(a: &str, b: &str) -> std::cmp::Ordering {
81 let mut ai = a.encode_utf16();
82 let mut bi = b.encode_utf16();
83 loop {
84 match (ai.next(), bi.next()) {
85 (None, None) => return std::cmp::Ordering::Equal,
86 (None, Some(_)) => return std::cmp::Ordering::Less,
87 (Some(_), None) => return std::cmp::Ordering::Greater,
88 (Some(x), Some(y)) => match x.cmp(&y) {
89 std::cmp::Ordering::Equal => continue,
90 other => return other,
91 },
92 }
93 }
94}
95
96fn write_number(n: &serde_json::Number, out: &mut String) -> SignatifResult<()> {
97 if let Some(i) = n.as_i64() {
98 out.push_str(&i.to_string());
99 return Ok(());
100 }
101 if let Some(u) = n.as_u64() {
102 out.push_str(&u.to_string());
103 return Ok(());
104 }
105 let f = n
106 .as_f64()
107 .ok_or_else(|| SignatifError::Encoding("number is not finite".into()))?;
108 if !f.is_finite() {
109 return Err(SignatifError::Encoding(
110 "NaN and Infinity are not allowed".into(),
111 ));
112 }
113 if f == 0.0 {
114 out.push('0');
116 return Ok(());
117 }
118 out.push_str(&serde_json::to_string(&Value::from(f)).expect("finite f64 serializes"));
121 Ok(())
122}
123
124fn write_string(s: &str, out: &mut String) {
125 out.push('"');
126 for c in s.chars() {
127 match c {
128 '"' => out.push_str("\\\""),
129 '\\' => out.push_str("\\\\"),
130 '\u{08}' => out.push_str("\\b"),
131 '\u{09}' => out.push_str("\\t"),
132 '\u{0a}' => out.push_str("\\n"),
133 '\u{0c}' => out.push_str("\\f"),
134 '\u{0d}' => out.push_str("\\r"),
135 c if (c as u32) < 0x20 => {
136 out.push_str(&format!("\\u{:04x}", c as u32));
137 }
138 c => out.push(c),
139 }
140 }
141 out.push('"');
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use serde_json::json;
148
149 #[test]
150 fn control_chars_escaped_lowercase_hex() {
151 assert_eq!(
152 canonicalize(&json!("\u{0007}\u{001f}")).unwrap(),
153 "\"\\u0007\\u001f\""
154 );
155 }
156
157 #[test]
158 fn verbatim_characters_pass_through() {
159 assert_eq!(canonicalize(&json!("€ßé")).unwrap(), "\"€ßé\"");
161 }
162
163 #[test]
164 fn sorts_keys_by_utf16() {
165 let v = json!({"b": 1, "a": 2});
166 assert_eq!(canonicalize(&v).unwrap(), r#"{"a":2,"b":1}"#);
167 }
168
169 #[test]
170 fn utf16_ordering_beats_codepoint_ordering() {
171 let v = json!({"\u{10000}": 1, "\u{FF21}": 2});
176 let c = canonicalize(&v).unwrap();
177 let surrogate_pos = c.find('\u{10000}').expect("surrogate char present");
178 let fullwidth_pos = c.find('\u{FF21}').expect("fullwidth char present");
179 assert!(surrogate_pos < fullwidth_pos, "got {c}");
180 }
181
182 #[test]
183 fn deterministic_nested_structures() {
184 let a = json!({"z": [1, 2.5, true, null], "a": {"y": "x"}});
185 let b = json!({"a": {"y": "x"}, "z": [1, 2.5, true, null]});
186 assert_eq!(canonicalize(&a).unwrap(), canonicalize(&b).unwrap());
187 }
188
189 #[test]
190 fn negative_zero_normalizes() {
191 let v: Value = serde_json::from_str("-0.0").unwrap();
192 assert_eq!(canonicalize(&v).unwrap(), "0");
193 }
194
195 #[test]
196 fn floats_shortest_round_trip() {
197 assert_eq!(canonicalize(&json!(2.5)).unwrap(), "2.5");
198 assert_eq!(canonicalize(&json!(1e30)).unwrap(), "1e+30");
199 }
200
201 #[test]
202 fn canonical_hash_is_sha256_of_canonical_bytes() {
203 use sha2::{Digest, Sha256};
204 let h = canonical_hash(&json!({"a":1})).unwrap();
205 let expect: [u8; 32] = Sha256::digest(b"{\"a\":1}").into();
206 assert_eq!(h, expect);
207 }
208}