Skip to main content

mur_common/
jcs.rs

1//! RFC 8785 JSON Canonicalization Scheme (JCS).
2//!
3//! Separate from `canonical.rs` — this implements the full JCS spec
4//! including number canonicalization rules that the existing module
5//! explicitly excludes. The two canonical forms serve different contracts.
6
7use serde::Serialize;
8
9/// Serialize a value to RFC 8785 canonical JSON bytes.
10pub fn to_jcs(value: &serde_json::Value) -> Vec<u8> {
11    serde_jcs::to_vec(value).expect("JCS serialization is infallible for valid JSON")
12}
13
14/// Serialize any `Serialize` type to RFC 8785 canonical JSON bytes.
15pub fn to_jcs_for<T: Serialize>(value: &T) -> Vec<u8> {
16    serde_jcs::to_vec(value).expect("JCS serialization should not fail for valid types")
17}
18
19/// Compute SHA-256 of the canonical JSON bytes of a value.
20pub fn jcs_sha256(value: &serde_json::Value) -> String {
21    use sha2::Digest;
22    let bytes = to_jcs(value);
23    format!("{:x}", sha2::Sha256::digest(&bytes))
24}
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29    use serde_json::json;
30
31    #[test]
32    fn scientific_notation_is_expanded() {
33        let v = json!(1e10);
34        let out = String::from_utf8(to_jcs(&v)).unwrap();
35        assert_eq!(out, "10000000000");
36    }
37
38    #[test]
39    fn trailing_zeros_removed() {
40        let v = json!(1.0);
41        let out = String::from_utf8(to_jcs(&v)).unwrap();
42        assert_eq!(out, "1");
43    }
44
45    #[test]
46    fn very_small_number_no_scientific() {
47        let v = json!(0.00001);
48        let out = String::from_utf8(to_jcs(&v)).unwrap();
49        assert!(
50            !out.contains('e') && !out.contains('E'),
51            "expected no scientific notation, got: {out}"
52        );
53    }
54
55    #[test]
56    fn keys_sorted_lexicographically() {
57        let v = json!({"z": 1, "a": 2, "m": 3});
58        let out = String::from_utf8(to_jcs(&v)).unwrap();
59        assert_eq!(out, r#"{"a":2,"m":3,"z":1}"#);
60    }
61
62    #[test]
63    fn nested_keys_sorted_recursively() {
64        let v = json!({"b": {"z": 1, "a": 2}, "a": 1});
65        let out = String::from_utf8(to_jcs(&v)).unwrap();
66        assert_eq!(out, r#"{"a":1,"b":{"a":2,"z":1}}"#);
67    }
68
69    #[test]
70    fn boolean_and_null_preserved() {
71        assert_eq!(String::from_utf8(to_jcs(&json!(true))).unwrap(), "true");
72        assert_eq!(String::from_utf8(to_jcs(&json!(null))).unwrap(), "null");
73    }
74
75    #[test]
76    fn deterministic_across_insertion_order() {
77        let a: serde_json::Value = serde_json::from_str(r#"{"b":1,"a":2}"#).unwrap();
78        let b: serde_json::Value = serde_json::from_str(r#"{"a":2,"b":1}"#).unwrap();
79        assert_eq!(to_jcs(&a), to_jcs(&b));
80    }
81}