use std::collections::BTreeMap;
use serde::Serialize;
use serde_json::Value;
pub fn to_canonical_bytes<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, String> {
let v = serde_json::to_value(value)
.map_err(|e| format!("canonical: serialize to value failed: {e}"))?;
let canon = canonicalize(v)?;
serde_json::to_vec(&canon)
.map_err(|e| format!("canonical: serialize canonical form failed: {e}"))
}
pub fn to_canonical_string<T: Serialize + ?Sized>(value: &T) -> Result<String, String> {
let bytes = to_canonical_bytes(value)?;
String::from_utf8(bytes).map_err(|e| format!("canonical: invalid utf-8: {e}"))
}
fn canonicalize(value: Value) -> Result<Value, String> {
match value {
Value::Object(map) => {
let mut sorted: BTreeMap<String, Value> = BTreeMap::new();
for (k, v) in map {
sorted.insert(k, canonicalize(v)?);
}
let mut out = serde_json::Map::with_capacity(sorted.len());
for (k, v) in sorted {
out.insert(k, v);
}
Ok(Value::Object(out))
}
Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(canonicalize(item)?);
}
Ok(Value::Array(out))
}
Value::Number(ref n) => {
if let Some(f) = n.as_f64()
&& !f.is_finite()
{
return Err("canonical: non-finite float in input".to_string());
}
Ok(value)
}
other => Ok(other),
}
}
pub fn sha256_canonical<T: Serialize + ?Sized>(value: &T) -> Result<String, String> {
use sha2::{Digest, Sha256};
let bytes = to_canonical_bytes(value)?;
Ok(hex::encode(Sha256::digest(&bytes)))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn object_keys_sort_at_every_depth() {
let unordered = json!({
"z": 1,
"a": {
"y": 2,
"b": 3,
},
"m": [{"q": 4, "p": 5}],
});
let canon = to_canonical_string(&unordered).unwrap();
assert_eq!(canon, r#"{"a":{"b":3,"y":2},"m":[{"p":5,"q":4}],"z":1}"#);
}
#[test]
fn whitespace_is_stripped() {
let v = json!({"key": "value"});
let canon = to_canonical_string(&v).unwrap();
assert!(!canon.contains(' '));
assert!(!canon.contains('\n'));
}
#[test]
fn array_order_is_preserved() {
let v = json!([3, 1, 2]);
let canon = to_canonical_string(&v).unwrap();
assert_eq!(canon, "[3,1,2]");
}
#[test]
fn unicode_strings_pass_through() {
let v = json!({"text": "amyloid-β"});
let canon = to_canonical_string(&v).unwrap();
assert!(canon.contains("amyloid-β"));
}
#[test]
fn same_logical_content_produces_same_bytes() {
let a = json!({"x": 1, "y": 2});
let b = json!({"y": 2, "x": 1});
let bytes_a = to_canonical_bytes(&a).unwrap();
let bytes_b = to_canonical_bytes(&b).unwrap();
assert_eq!(bytes_a, bytes_b);
}
#[test]
fn sha256_canonical_is_stable() {
let a = json!({"hello": "world"});
let h1 = sha256_canonical(&a).unwrap();
let h2 = sha256_canonical(&a).unwrap();
assert_eq!(h1, h2);
assert_eq!(h1.len(), 64);
}
}