Skip to main content

clawft_kernel/workload_pkg/
canonical.rs

1//! Canonical JSON: recursively sorted object keys, compact separators, UTF-8,
2//! no trailing newline, integers only.
3//!
4//! This matches the Cognitum ADR-154 canonical form (`json.dumps(sort_keys=
5//! True, separators=(",", ":"), ensure_ascii=False)`) for the value space
6//! both sides accept, and is independent of whether `serde_json` was built
7//! with `preserve_order`. Floats are refused so that two runtimes can never
8//! disagree about number formatting.
9
10use serde_json::Value;
11
12/// Largest integer magnitude accepted (JavaScript safe-integer range).
13const MAX_SAFE_INT: i64 = (1 << 53) - 1;
14/// Nesting limit.
15const MAX_DEPTH: usize = 32;
16/// Upper bound on canonical output size.
17pub const MAX_CANONICAL_BYTES: usize = 1024 * 1024;
18
19/// Reasons a value has no canonical encoding.
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum CanonicalError {
22    /// Non-integer or out-of-range number.
23    #[error("canonical JSON refuses non-integer or out-of-range number {0}")]
24    Number(String),
25    /// Nesting deeper than the limit.
26    #[error("canonical JSON nesting exceeds {MAX_DEPTH}")]
27    TooDeep,
28    /// Output larger than the limit.
29    #[error("canonical JSON exceeds {MAX_CANONICAL_BYTES} bytes")]
30    TooLarge,
31}
32
33/// Encode `value` canonically.
34pub fn canonical_json(value: &Value) -> Result<Vec<u8>, CanonicalError> {
35    let mut out = Vec::with_capacity(256);
36    write_value(value, &mut out, 0)?;
37    if out.len() > MAX_CANONICAL_BYTES {
38        return Err(CanonicalError::TooLarge);
39    }
40    Ok(out)
41}
42
43fn write_value(value: &Value, out: &mut Vec<u8>, depth: usize) -> Result<(), CanonicalError> {
44    if depth > MAX_DEPTH {
45        return Err(CanonicalError::TooDeep);
46    }
47    if out.len() > MAX_CANONICAL_BYTES {
48        return Err(CanonicalError::TooLarge);
49    }
50    match value {
51        Value::Null => out.extend_from_slice(b"null"),
52        Value::Bool(true) => out.extend_from_slice(b"true"),
53        Value::Bool(false) => out.extend_from_slice(b"false"),
54        Value::Number(n) => {
55            let ok = n
56                .as_i64()
57                .map(|i| (-MAX_SAFE_INT..=MAX_SAFE_INT).contains(&i))
58                .or_else(|| n.as_u64().map(|u| u <= MAX_SAFE_INT as u64))
59                .unwrap_or(false);
60            if !ok {
61                return Err(CanonicalError::Number(n.to_string()));
62            }
63            out.extend_from_slice(n.to_string().as_bytes());
64        }
65        Value::String(s) => write_string(s, out),
66        Value::Array(items) => {
67            out.push(b'[');
68            for (i, item) in items.iter().enumerate() {
69                if i > 0 {
70                    out.push(b',');
71                }
72                write_value(item, out, depth + 1)?;
73            }
74            out.push(b']');
75        }
76        Value::Object(map) => {
77            let mut keys: Vec<&String> = map.keys().collect();
78            // Byte order of UTF-8 equals code-point order, which is what
79            // Python's sort_keys uses.
80            keys.sort();
81            out.push(b'{');
82            for (i, key) in keys.iter().enumerate() {
83                if i > 0 {
84                    out.push(b',');
85                }
86                write_string(key, out);
87                out.push(b':');
88                write_value(&map[key.as_str()], out, depth + 1)?;
89            }
90            out.push(b'}');
91        }
92    }
93    Ok(())
94}
95
96fn write_string(s: &str, out: &mut Vec<u8>) {
97    // serde_json escapes exactly `"`, `\` and control characters, and emits
98    // non-ASCII as raw UTF-8, which is the ensure_ascii=False behaviour.
99    // Serializing a &str cannot fail.
100    let encoded = serde_json::to_string(s).unwrap_or_default();
101    out.extend_from_slice(encoded.as_bytes());
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use serde_json::json;
108
109    #[test]
110    fn sorts_keys_recursively_and_is_compact() {
111        let v = json!({"b": 1, "a": {"z": [true, null], "c": "x"}});
112        let out = canonical_json(&v).unwrap();
113        assert_eq!(out, br#"{"a":{"c":"x","z":[true,null]},"b":1}"#);
114    }
115
116    #[test]
117    fn refuses_floats_and_unsafe_integers() {
118        assert!(matches!(
119            canonical_json(&json!({"x": 1.5})),
120            Err(CanonicalError::Number(_))
121        ));
122        assert!(canonical_json(&json!(9_007_199_254_740_992_u64)).is_err());
123        assert!(canonical_json(&json!(9_007_199_254_740_991_u64)).is_ok());
124    }
125
126    #[test]
127    fn escapes_like_python_ensure_ascii_false() {
128        let v = json!({"k": "q\"b\\s\n\u{e9}/"});
129        let out = String::from_utf8(canonical_json(&v).unwrap()).unwrap();
130        assert_eq!(out, "{\"k\":\"q\\\"b\\\\s\\n\u{e9}/\"}");
131    }
132
133    #[test]
134    fn refuses_excessive_depth() {
135        let mut v = json!(0);
136        for _ in 0..40 {
137            v = json!([v]);
138        }
139        assert_eq!(canonical_json(&v), Err(CanonicalError::TooDeep));
140    }
141}