use serde_json::Value;
use sha2::{Digest, Sha256};
pub fn canonicalize(v: &Value) -> String {
let mut out = String::new();
write_canonical(v, &mut out);
out
}
pub fn digest(v: &Value) -> String {
hex::encode(Sha256::digest(canonicalize(v).as_bytes()))
}
fn write_canonical(v: &Value, out: &mut String) {
match v {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
out.push('{');
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serde_json::to_string(k).expect("string key serializes"));
out.push(':');
write_canonical(&map[*k], out);
}
out.push('}');
}
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
Value::Number(n) => write_number(n, out),
Value::String(_) | Value::Bool(_) | Value::Null => {
out.push_str(&serde_json::to_string(v).expect("scalar serializes"));
}
}
}
fn write_number(n: &serde_json::Number, out: &mut String) {
if !n.is_f64() {
out.push_str(&n.to_string());
return;
}
out.push_str(&ecmascript_number(
n.as_f64().expect("is_f64 was just checked"),
));
}
fn ecmascript_number(x: f64) -> String {
if x == 0.0 {
return "0".to_string(); }
if x < 0.0 {
return format!("-{}", ecmascript_number(-x));
}
let mut buf = ryu::Buffer::new();
let printed = buf.format_finite(x);
let (mantissa, exponent) = match printed.split_once('e') {
Some((m, e)) => (m, e.parse::<i32>().expect("Ryu writes an integer exponent")),
None => (printed, 0),
};
let (int_digits, frac_digits) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let written = format!("{int_digits}{frac_digits}");
let leading = written.len() - written.trim_start_matches('0').len();
let digits = written[leading..].trim_end_matches('0');
let n = exponent + int_digits.len() as i32 - leading as i32;
let k = digits.len() as i32;
if k <= n && n <= 21 {
format!("{digits}{}", "0".repeat((n - k) as usize))
} else if 0 < n && n <= 21 {
format!("{}.{}", &digits[..n as usize], &digits[n as usize..])
} else if -6 < n && n <= 0 {
format!("0.{}{digits}", "0".repeat(-n as usize))
} else {
let sign = if n > 0 { '+' } else { '-' };
let magnitude = (n - 1).abs();
if k == 1 {
format!("{digits}e{sign}{magnitude}")
} else {
format!("{}.{}e{sign}{magnitude}", &digits[..1], &digits[1..])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn key_order_does_not_change_the_canonical_form() {
let a = json!({"b": 1, "a": 2, "c": 3});
let b = json!({"c": 3, "a": 2, "b": 1});
assert_eq!(canonicalize(&a), r#"{"a":2,"b":1,"c":3}"#);
assert_eq!(canonicalize(&a), canonicalize(&b));
assert_eq!(digest(&a), digest(&b));
}
#[test]
fn different_values_yield_different_digests() {
assert_ne!(
digest(&json!({"amount": 100})),
digest(&json!({"amount": 101})),
);
assert_ne!(
digest(&json!({"amount": 100})),
digest(&json!({"amount": "100"})),
);
}
#[test]
fn nested_objects_are_sorted_but_array_order_is_preserved() {
let v = json!([{"b": 1, "a": 2}, {"d": 4, "c": 3}]);
assert_eq!(canonicalize(&v), r#"[{"a":2,"b":1},{"c":3,"d":4}]"#);
assert_ne!(digest(&json!([1, 2, 3])), digest(&json!([3, 2, 1])),);
}
#[test]
fn keys_are_ordered_by_utf16_code_unit_not_utf8_bytes() {
let v = json!({"\u{FFFF}": 1, "\u{10000}": 2});
let c = canonicalize(&v);
let sup = c.find('\u{10000}').unwrap();
let bmp = c.find('\u{FFFF}').unwrap();
assert!(
sup < bmp,
"U+10000 must sort before U+FFFF (UTF-16 order): {c}"
);
}
#[test]
fn strings_are_escaped_and_round_trip_to_the_same_value() {
let v = json!({"k": "a\"b\\c\n\u{0001}"});
let c = canonicalize(&v);
assert!(c.contains("\\\""), "quote escaped: {c}");
assert!(c.contains("\\\\"), "backslash escaped: {c}");
assert!(c.contains("\\u0001"), "control char is lowercase \\u: {c}");
assert_eq!(serde_json::from_str::<Value>(&c).unwrap(), v);
}
#[test]
fn no_insignificant_whitespace() {
let v = json!({"a": [1, {"x": true}], "b": null});
assert_eq!(canonicalize(&v), r#"{"a":[1,{"x":true}],"b":null}"#);
}
#[test]
fn numbers_serialize_as_ecmascript_prints_them() {
for (x, want) in [
(3.0, "3"),
(-3.0, "-3"),
(0.0, "0"),
(-0.0, "0"),
(1.5, "1.5"),
(0.1, "0.1"),
(1e15, "1000000000000000"),
(1e20, "100000000000000000000"),
(1e21, "1e+21"),
(1e-6, "0.000001"),
(1e-7, "1e-7"),
(f64::MAX, "1.7976931348623157e+308"),
(5e-324, "5e-324"),
] {
assert_eq!(ecmascript_number(x), want, "for {x:?}");
assert_eq!(canonicalize(&json!(x)), want, "canonicalized {x:?}");
}
}
#[test]
fn integral_floats_and_integers_share_a_digest() {
let written_as_integer: Value = serde_json::from_str(r#"{"qty":3}"#).unwrap();
let written_as_float: Value = serde_json::from_str(r#"{"qty":3.0}"#).unwrap();
let written_in_exponent: Value = serde_json::from_str(r#"{"qty":3e0}"#).unwrap();
assert_eq!(canonicalize(&written_as_integer), r#"{"qty":3}"#);
assert_eq!(digest(&written_as_integer), digest(&written_as_float));
assert_eq!(digest(&written_as_integer), digest(&written_in_exponent));
}
#[test]
fn integers_beyond_the_interoperable_range_keep_their_digits() {
let a: Value = serde_json::from_str(r#"{"id":9007199254740993}"#).unwrap();
let b: Value = serde_json::from_str(r#"{"id":9007199254740992}"#).unwrap();
assert_eq!(canonicalize(&a), r#"{"id":9007199254740993}"#);
assert_ne!(digest(&a), digest(&b));
}
}