use serde_json::Value;
fn strip_meta(value: &Value, strict: bool) -> Value {
match value {
Value::Object(map) => {
let mut out = serde_json::Map::new();
for (k, v) in map {
if k == "signature" || k == "public_key_id" {
continue;
}
if strict && k == "event_id" {
continue;
}
out.insert(k.clone(), v.clone());
}
Value::Object(out)
}
other => other.clone(),
}
}
pub fn canonical(value: &Value, strict: bool) -> Vec<u8> {
let stripped = strip_meta(value, strict);
serde_json::to_vec(&stripped).expect("canonical serialization is infallible for Value")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn excludes_signature_and_public_key_id() {
let v = json!({"a": 1, "signature": "sig", "public_key_id": "id"});
let out = canonical(&v, false);
assert!(!std::str::from_utf8(&out).unwrap().contains("signature"));
assert!(!std::str::from_utf8(&out).unwrap().contains("public_key_id"));
}
#[test]
fn strict_excludes_event_id() {
let v = json!({"a": 1, "event_id": "deadbeef"});
assert!(
!std::str::from_utf8(&canonical(&v, true))
.unwrap()
.contains("event_id")
);
assert!(
std::str::from_utf8(&canonical(&v, false))
.unwrap()
.contains("event_id")
);
}
#[test]
fn keys_are_sorted_lexicographically() {
let a = json!({"b": 1, "a": 2, "c": 3});
let b = json!({"c": 3, "a": 2, "b": 1});
assert_eq!(canonical(&a, false), canonical(&b, false));
let s = String::from_utf8(canonical(&a, false)).unwrap();
assert_eq!(s, r#"{"a":2,"b":1,"c":3}"#);
}
#[test]
fn no_whitespace_in_output() {
let v = json!({"x": [1, 2, 3], "y": {"z": "w"}});
let s = String::from_utf8(canonical(&v, false)).unwrap();
assert!(!s.contains(' '));
assert!(!s.contains('\n'));
}
#[test]
fn nested_objects_also_sorted() {
let v = json!({"outer": {"b": 1, "a": 2}});
let s = String::from_utf8(canonical(&v, false)).unwrap();
assert_eq!(s, r#"{"outer":{"a":2,"b":1}}"#);
}
#[test]
fn non_ascii_passes_through_unescaped() {
let v = json!({"name": "Pål"});
let s = String::from_utf8(canonical(&v, false)).unwrap();
assert!(s.contains("Pål"), "got {s}");
}
}