1use std::fmt;
17
18use serde::Serialize;
19use serde::de::DeserializeOwned;
20use serde_json::Value;
21use sha2::{Digest as _, Sha256};
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum CanonicalError {
26 FractionalNumber { path: String },
28 Serialize { message: String },
30 NotRoundTrip,
33}
34
35impl fmt::Display for CanonicalError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 CanonicalError::FractionalNumber { path } => {
39 write!(f, "fractional number at {path} has no canonical form")
40 }
41 CanonicalError::Serialize { message } => write!(f, "cannot serialize: {message}"),
42 CanonicalError::NotRoundTrip => {
43 write!(f, "the value does not round-trip (a non-finite number?)")
44 }
45 }
46 }
47}
48
49impl std::error::Error for CanonicalError {}
50
51pub fn canonical_bytes<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, CanonicalError> {
53 let v = serde_json::to_value(value).map_err(|e| CanonicalError::Serialize {
54 message: e.to_string(),
55 })?;
56 let mut out = Vec::new();
57 write_value(&v, &mut out, &mut String::new(), false)?;
58 Ok(out)
59}
60
61pub fn canonical_value_bytes(value: &Value) -> Result<Vec<u8>, CanonicalError> {
63 let mut out = Vec::new();
64 write_value(value, &mut out, &mut String::new(), false)?;
65 Ok(out)
66}
67
68pub fn record_canonical_bytes<T>(value: &T) -> Result<Vec<u8>, CanonicalError>
75where
76 T: Serialize + DeserializeOwned + PartialEq,
77{
78 let out = record_write(value)?;
79 match serde_json::from_slice::<T>(&out) {
80 Ok(back) if back == *value && record_write(&back)? == out => Ok(out),
81 _ => Err(CanonicalError::NotRoundTrip),
82 }
83}
84
85fn record_write<T: Serialize>(value: &T) -> Result<Vec<u8>, CanonicalError> {
86 let v = serde_json::to_value(value).map_err(|e| CanonicalError::Serialize {
87 message: e.to_string(),
88 })?;
89 let mut out = Vec::new();
90 write_value(&v, &mut out, &mut String::new(), true)?;
91 Ok(out)
92}
93
94pub fn write_binary64(x: f64, out: &mut Vec<u8>) {
100 if x == 0.0 {
101 out.extend_from_slice(if x.is_sign_negative() {
102 b"-0.0"
103 } else {
104 b"0.0"
105 });
106 return;
107 }
108 let sci = format!("{:e}", x.abs());
110 let (mantissa, exp) = sci.split_once('e').unwrap_or((sci.as_str(), "0"));
111 let e: i32 = exp.parse().unwrap_or(0);
112 let digits: Vec<u8> = mantissa.bytes().filter(u8::is_ascii_digit).collect();
113 let n = digits.len() as i32;
114 if x.is_sign_negative() {
115 out.push(b'-');
116 }
117 if (-5..16).contains(&e) {
118 if e >= 0 {
119 let whole = (e + 1) as usize;
120 if n <= e + 1 {
121 out.extend_from_slice(&digits);
122 out.extend(std::iter::repeat_n(b'0', whole - digits.len()));
123 out.extend_from_slice(b".0");
124 } else {
125 out.extend_from_slice(&digits[..whole]);
126 out.push(b'.');
127 out.extend_from_slice(&digits[whole..]);
128 }
129 } else {
130 out.extend_from_slice(b"0.");
131 out.extend(std::iter::repeat_n(b'0', (-e - 1) as usize));
132 out.extend_from_slice(&digits);
133 }
134 } else {
135 out.push(digits[0]);
136 if n > 1 {
137 out.push(b'.');
138 out.extend_from_slice(&digits[1..]);
139 }
140 out.push(b'e');
141 out.push(if e > 0 { b'+' } else { b'-' });
142 out.extend_from_slice(e.unsigned_abs().to_string().as_bytes());
143 }
144}
145
146fn write_str(s: &str, out: &mut Vec<u8>) {
147 out.extend_from_slice(serde_json::to_string(s).unwrap_or_default().as_bytes());
149}
150
151fn write_value(
152 v: &Value,
153 out: &mut Vec<u8>,
154 path: &mut String,
155 fractional: bool,
156) -> Result<(), CanonicalError> {
157 match v {
158 Value::Null => out.extend_from_slice(b"null"),
159 Value::Bool(b) => out.extend_from_slice(if *b { b"true" } else { b"false" }),
160 Value::Number(n) => {
161 if let Some(i) = n.as_i64() {
162 out.extend_from_slice(i.to_string().as_bytes());
163 } else if let Some(u) = n.as_u64() {
164 out.extend_from_slice(u.to_string().as_bytes());
165 } else if let (true, Some(f)) = (fractional, n.as_f64()) {
166 write_binary64(f, out);
167 } else {
168 return Err(CanonicalError::FractionalNumber {
169 path: if path.is_empty() {
170 "/".into()
171 } else {
172 path.clone()
173 },
174 });
175 }
176 }
177 Value::String(s) => write_str(s, out),
178 Value::Array(items) => {
179 out.push(b'[');
180 for (i, item) in items.iter().enumerate() {
181 if i > 0 {
182 out.push(b',');
183 }
184 let len = path.len();
185 path.push('/');
186 path.push_str(&i.to_string());
187 write_value(item, out, path, fractional)?;
188 path.truncate(len);
189 }
190 out.push(b']');
191 }
192 Value::Object(map) => {
193 let keys = sorted_keys(map.keys());
194 out.push(b'{');
195 for (i, k) in keys.into_iter().enumerate() {
196 if i > 0 {
197 out.push(b',');
198 }
199 write_str(k, out);
200 out.push(b':');
201 let len = path.len();
202 path.push('/');
203 path.push_str(k);
204 write_value(&map[k], out, path, fractional)?;
205 path.truncate(len);
206 }
207 out.push(b'}');
208 }
209 }
210 Ok(())
211}
212
213fn sorted_keys<'k>(keys: impl Iterator<Item = &'k String>) -> Vec<&'k String> {
216 let mut keys: Vec<&String> = keys.collect();
217 keys.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
218 keys
219}
220
221pub fn tagged_digest(tag: &str, bytes: &[u8]) -> String {
223 let mut h = Sha256::new();
224 h.update(tag.as_bytes());
225 h.update([0u8]);
226 h.update(bytes);
227 let d = h.finalize();
228 let mut s = String::with_capacity(71);
229 s.push_str("sha256:");
230 for b in d {
231 s.push(char::from_digit(u32::from(b >> 4), 16).unwrap_or('0'));
232 s.push(char::from_digit(u32::from(b & 0xf), 16).unwrap_or('0'));
233 }
234 s
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use serde_json::json;
241
242 #[test]
243 fn keys_are_sorted_by_bytes_and_whitespace_removed() {
244 let v =
245 json!({"b": 1, "a": [true, null, "x"], "B": {"z": -2, "y": 18446744073709551615u64}});
246 assert_eq!(
247 canonical_value_bytes(&v).unwrap(),
248 br#"{"B":{"y":18446744073709551615,"z":-2},"a":[true,null,"x"],"b":1}"#
249 );
250 }
251
252 #[test]
253 fn keys_are_sorted_whatever_the_iteration_order() {
254 let (b, a, upper) = ("b".to_string(), "a".to_string(), "B".to_string());
255 let keys = sorted_keys([&b, &a, &upper].into_iter());
256 assert_eq!(keys, vec![&upper, &a, &b]);
257 }
258
259 #[test]
260 fn a_fractional_number_is_refused_with_its_path() {
261 let v = json!({"a": [1, 2.5]});
262 assert_eq!(
263 canonical_value_bytes(&v),
264 Err(CanonicalError::FractionalNumber {
265 path: "/a/1".into()
266 })
267 );
268 assert!(canonical_value_bytes(&json!({"a": [1, 2]})).is_ok());
270 }
271
272 fn binary64(x: f64) -> String {
273 let mut out = Vec::new();
274 write_binary64(x, &mut out);
275 String::from_utf8(out).unwrap()
276 }
277
278 #[allow(clippy::excessive_precision)]
281 #[test]
282 fn binary64_known_answers_at_the_layout_boundaries() {
283 for (x, want) in [
284 (0.0, "0.0"),
285 (-0.0, "-0.0"),
286 (3.0, "3.0"),
287 (-2.5, "-2.5"),
288 (0.1, "0.1"),
289 (1e-5, "0.00001"),
290 (1.234e-5, "0.00001234"),
291 (9.99e-6, "9.99e-6"),
292 (1e-6, "1e-6"),
293 (1.5e-7, "1.5e-7"),
294 (1e15, "1000000000000000.0"),
295 (9.999999999999998e15, "9999999999999998.0"),
296 (1e16, "1e+16"),
297 (1.5e16, "1.5e+16"),
298 (1e21, "1e+21"),
299 (123456789.123, "123456789.123"),
300 (f64::MAX, "1.7976931348623157e+308"),
301 (-f64::MAX, "-1.7976931348623157e+308"),
302 (f64::MIN_POSITIVE, "2.2250738585072014e-308"),
303 (5e-324, "5e-324"),
304 (934356417220194.25, "934356417220194.3"),
307 (1658206780088562.25, "1658206780088562.3"),
308 (-934356417220194.25, "-934356417220194.3"),
309 ] {
310 assert_eq!(binary64(x), want, "{x:e}");
311 }
312 }
313
314 #[test]
315 fn binary64_round_trips_through_the_correctly_rounded_parser() {
316 let mut seed: u64 = 0x9e37_79b9_7f4a_7c15;
318 for _ in 0..200_000 {
319 seed ^= seed << 13;
320 seed ^= seed >> 7;
321 seed ^= seed << 17;
322 let x = f64::from_bits(seed);
323 if !x.is_finite() {
324 continue;
325 }
326 let text = binary64(x);
327 let back: f64 = serde_json::from_str(&text).unwrap();
328 assert_eq!(back.to_bits(), x.to_bits(), "{text}");
329 }
330 }
331
332 #[test]
333 fn record_form_writes_shortest_round_trip_floats() {
334 let v: Vec<f64> = vec![0.1, 3.0, -0.0, 1e21, 1.5e-7, 5e-324, 2.0f64.sqrt()];
335 assert_eq!(
336 record_canonical_bytes(&v).unwrap(),
337 b"[0.1,3.0,-0.0,1e+21,1.5e-7,5e-324,1.4142135623730951]"
338 );
339 assert!(canonical_bytes(&v).is_err());
341 let ints = json!({"b": [1, -2], "a": "x"});
343 assert_eq!(
344 record_canonical_bytes(&ints).unwrap(),
345 canonical_bytes(&ints).unwrap()
346 );
347 }
348
349 #[test]
350 fn record_form_refuses_non_finite_numbers() {
351 for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
352 assert_eq!(
353 record_canonical_bytes(&vec![1.0, bad]),
354 Err(CanonicalError::NotRoundTrip),
355 "{bad}"
356 );
357 }
358 assert!(record_canonical_bytes(&vec![1.0, f64::MAX]).is_ok());
360 }
361
362 #[test]
363 fn strings_use_json_escaping() {
364 let v = json!("a\"b\\c\n\u{1}é");
365 assert_eq!(
366 canonical_value_bytes(&v).unwrap(),
367 "\"a\\\"b\\\\c\\n\\u0001é\"".as_bytes()
368 );
369 }
370
371 #[test]
372 fn digests_are_domain_separated_and_stable() {
373 let a = tagged_digest("rustev.plan/1", b"{}");
374 let b = tagged_digest("rustev.definition/1", b"{}");
375 assert_ne!(a, b);
376 assert_eq!(a.len(), 71);
377 assert!(a.starts_with("sha256:"));
378 assert_eq!(
380 tagged_digest("x", b""),
381 "sha256:14f825b2bbc32dd8d196367fa8776873069c12a8954d8da7513aa7704ddd09eb"
382 );
383 }
384}