clawft_kernel/workload_pkg/
canonical.rs1use serde_json::Value;
11
12const MAX_SAFE_INT: i64 = (1 << 53) - 1;
14const MAX_DEPTH: usize = 32;
16pub const MAX_CANONICAL_BYTES: usize = 1024 * 1024;
18
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum CanonicalError {
22 #[error("canonical JSON refuses non-integer or out-of-range number {0}")]
24 Number(String),
25 #[error("canonical JSON nesting exceeds {MAX_DEPTH}")]
27 TooDeep,
28 #[error("canonical JSON exceeds {MAX_CANONICAL_BYTES} bytes")]
30 TooLarge,
31}
32
33pub 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 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 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}