use serde_json::Value;
use sha2::{Digest, Sha256};
#[must_use]
pub fn canonical_json(value: &Value) -> String {
let mut out = String::new();
write_canonical(value, &mut out);
out
}
fn write_canonical(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Number(number) => out.push_str(&number.to_string()),
Value::String(text) => {
out.push_str(
&serde_json::to_string(text).expect("serializing a string to JSON cannot fail"),
);
}
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
out.push('{');
for (index, key) in keys.iter().enumerate() {
if index > 0 {
out.push(',');
}
out.push_str(
&serde_json::to_string(key).expect("serializing a string to JSON cannot fail"),
);
out.push(':');
write_canonical(&map[key.as_str()], out);
}
out.push('}');
}
}
}
#[must_use]
pub fn sha256_hex(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let mut out = String::with_capacity(digest.len() * 2);
for byte in digest {
out.push_str(&format!("{byte:02x}"));
}
out
}
#[must_use]
pub fn hash_value(value: &Value) -> String {
format!("sha256:{}", sha256_hex(canonical_json(value).as_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn canonical_json_sorts_object_keys_recursively() {
let a: Value =
serde_json::from_str(r#"{"b": {"z": 1, "a": 2}, "a": [3, {"y": 4, "x": 5}]}"#).unwrap();
let b: Value =
serde_json::from_str(r#"{"a": [3, {"x": 5, "y": 4}], "b": {"a": 2, "z": 1}}"#).unwrap();
assert_eq!(canonical_json(&a), canonical_json(&b));
assert_eq!(
canonical_json(&a),
r#"{"a":[3,{"x":5,"y":4}],"b":{"a":2,"z":1}}"#
);
}
#[test]
fn canonical_json_escapes_strings() {
let value = json!({"text": "line\none \"two\""});
assert_eq!(canonical_json(&value), r#"{"text":"line\none \"two\""}"#);
}
#[test]
fn hash_value_is_prefixed_and_stable() {
let hash = hash_value(&json!({"a": 1}));
assert!(hash.starts_with("sha256:"));
assert_eq!(hash, hash_value(&json!({"a": 1})));
assert_ne!(hash, hash_value(&json!({"a": 2})));
}
}