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 parameter_digest(params: &Value) -> String {
hex::encode(Sha256::digest(canonicalize(params).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::String(_) | Value::Number(_) | Value::Bool(_) | Value::Null => {
out.push_str(&serde_json::to_string(v).expect("scalar serializes"));
}
}
}
#[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!(parameter_digest(&a), parameter_digest(&b));
}
#[test]
fn different_values_yield_different_digests() {
assert_ne!(
parameter_digest(&json!({"amount": 100})),
parameter_digest(&json!({"amount": 101})),
);
assert_ne!(
parameter_digest(&json!({"amount": 100})),
parameter_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!(
parameter_digest(&json!([1, 2, 3])),
parameter_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}"#);
}
}