1use serde_json::Value;
21
22fn strip_meta(value: &Value, strict: bool) -> Value {
27 match value {
28 Value::Object(map) => {
29 let mut out = serde_json::Map::new();
30 for (k, v) in map {
31 if k == "signature" || k == "public_key_id" {
32 continue;
33 }
34 if strict && k == "event_id" {
35 continue;
36 }
37 out.insert(k.clone(), v.clone());
38 }
39 Value::Object(out)
40 }
41 other => other.clone(),
42 }
43}
44
45pub fn canonical(value: &Value, strict: bool) -> Vec<u8> {
50 let stripped = strip_meta(value, strict);
51 serde_json::to_vec(&stripped).expect("canonical serialization is infallible for Value")
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use serde_json::json;
58
59 #[test]
60 fn excludes_signature_and_public_key_id() {
61 let v = json!({"a": 1, "signature": "sig", "public_key_id": "id"});
62 let out = canonical(&v, false);
63 assert!(!std::str::from_utf8(&out).unwrap().contains("signature"));
64 assert!(!std::str::from_utf8(&out).unwrap().contains("public_key_id"));
65 }
66
67 #[test]
68 fn strict_excludes_event_id() {
69 let v = json!({"a": 1, "event_id": "deadbeef"});
70 assert!(
71 !std::str::from_utf8(&canonical(&v, true))
72 .unwrap()
73 .contains("event_id")
74 );
75 assert!(
76 std::str::from_utf8(&canonical(&v, false))
77 .unwrap()
78 .contains("event_id")
79 );
80 }
81
82 #[test]
83 fn keys_are_sorted_lexicographically() {
84 let a = json!({"b": 1, "a": 2, "c": 3});
85 let b = json!({"c": 3, "a": 2, "b": 1});
86 assert_eq!(canonical(&a, false), canonical(&b, false));
87 let s = String::from_utf8(canonical(&a, false)).unwrap();
88 assert_eq!(s, r#"{"a":2,"b":1,"c":3}"#);
89 }
90
91 #[test]
92 fn no_whitespace_in_output() {
93 let v = json!({"x": [1, 2, 3], "y": {"z": "w"}});
94 let s = String::from_utf8(canonical(&v, false)).unwrap();
95 assert!(!s.contains(' '));
96 assert!(!s.contains('\n'));
97 }
98
99 #[test]
100 fn nested_objects_also_sorted() {
101 let v = json!({"outer": {"b": 1, "a": 2}});
102 let s = String::from_utf8(canonical(&v, false)).unwrap();
103 assert_eq!(s, r#"{"outer":{"a":2,"b":1}}"#);
104 }
105
106 #[test]
107 fn non_ascii_passes_through_unescaped() {
108 let v = json!({"name": "Pål"});
109 let s = String::from_utf8(canonical(&v, false)).unwrap();
110 assert!(s.contains("Pål"), "got {s}");
111 }
112}