agentguard_core/decision/
canonical.rs1use crate::error::Result;
9use serde::Serialize;
10use serde_json::{Map, Value};
11use std::io::Write;
12
13pub fn canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
15 let v = serde_json::to_value(value)?;
16 let mut buf = Vec::new();
17 write_canonical_value(&mut buf, &v)?;
18 Ok(buf)
19}
20
21pub fn write_canonical_value<W: Write>(w: &mut W, v: &Value) -> Result<()> {
27 match v {
28 Value::Null => w.write_all(b"null").map_err(into_err)?,
29 Value::Bool(b) => {
30 let s = if *b { "true" } else { "false" };
31 w.write_all(s.as_bytes()).map_err(into_err)?;
32 }
33 Value::Number(n) => {
34 w.write_all(n.to_string().as_bytes()).map_err(into_err)?;
35 }
36 Value::String(s) => {
37 serde_json::to_writer(w, s)
38 .map_err(|e| Error::Other(format!("serialize string: {}", e)))?;
39 }
40 Value::Array(arr) => {
41 w.write_all(b"[").map_err(into_err)?;
42 for (i, item) in arr.iter().enumerate() {
43 if i > 0 {
44 w.write_all(b",").map_err(into_err)?;
45 }
46 write_canonical_value(w, item)?;
47 }
48 w.write_all(b"]").map_err(into_err)?;
49 }
50 Value::Object(obj) => write_canonical_object(w, obj)?,
51 }
52 Ok(())
53}
54
55fn write_canonical_object<W: Write>(w: &mut W, obj: &Map<String, Value>) -> Result<()> {
56 let mut keys: Vec<&String> = obj.keys().collect();
58 keys.sort();
59
60 w.write_all(b"{").map_err(into_err)?;
61 for (i, k) in keys.iter().enumerate() {
62 if i > 0 {
63 w.write_all(b",").map_err(into_err)?;
64 }
65 serde_json::to_writer(&mut *w, k)
67 .map_err(|e| Error::Other(format!("serialize key: {}", e)))?;
68 w.write_all(b":").map_err(into_err)?;
69 write_canonical_value(w, &obj[*k])?;
70 }
71 w.write_all(b"}").map_err(into_err)?;
72 Ok(())
73}
74
75fn into_err(e: std::io::Error) -> Error {
76 Error::Io(e.to_string())
77}
78
79use crate::error::Error;
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use serde_json::json;
85
86 #[test]
87 fn empty_object_canonical() {
88 let v = json!({});
89 let mut buf = Vec::new();
90 write_canonical_value(&mut buf, &v).unwrap();
91 assert_eq!(buf, b"{}");
92 }
93
94 #[test]
95 fn array_order_preserved() {
96 let v = json!([3, 1, 2]);
97 let mut buf = Vec::new();
98 write_canonical_value(&mut buf, &v).unwrap();
99 assert_eq!(buf, b"[3,1,2]");
100 }
101
102 #[test]
103 fn object_keys_sorted() {
104 let v = json!({"b": 1, "a": 2});
105 let mut buf = Vec::new();
106 write_canonical_value(&mut buf, &v).unwrap();
107 assert_eq!(buf, b"{\"a\":2,\"b\":1}");
108 }
109
110 #[test]
111 fn streaming_matches_allocating() {
112 let v = json!({"a": 1, "b": [1, 2, 3], "c": null, "d": "x"});
115 let allocating = canonical_json(&v).unwrap();
116 let mut streaming = Vec::new();
117 write_canonical_value(&mut streaming, &v).unwrap();
118 assert_eq!(allocating, streaming);
119 }
120
121 #[test]
122 fn nested_keys_sorted_recursively() {
123 let v = json!({"outer": {"z": 1, "a": 2}, "first": 3});
124 let mut buf = Vec::new();
125 write_canonical_value(&mut buf, &v).unwrap();
126 assert!(buf.starts_with(b"{\"first\":3,\"outer\":{\"a\":2,\"z\":1}}"));
128 }
129
130 #[test]
131 fn primitives_render_canonically() {
132 let cases: Vec<(serde_json::Value, Vec<u8>)> = vec![
133 (json!(null), b"null".to_vec()),
134 (json!(true), b"true".to_vec()),
135 (json!(false), b"false".to_vec()),
136 (json!(42), b"42".to_vec()),
137 (json!("hello"), b"\"hello\"".to_vec()),
138 ];
139 for (val, expected) in cases {
140 let mut buf = Vec::new();
141 write_canonical_value(&mut buf, &val).unwrap();
142 assert_eq!(buf, expected, "value {val:?}");
143 }
144 }
145
146 #[cfg(all(test, not(miri)))]
151 mod proptests {
152 use super::*;
153 use proptest::prelude::*;
154
155 proptest! {
156 #[test]
159 fn canonical_is_deterministic(
160 keys in proptest::collection::hash_set("[a-z]{1,4}", 1..6),
161 values in proptest::collection::vec(0i64..100, 1..6)
162 ) {
163 let mut obj = serde_json::Map::new();
164 for (i, k) in keys.iter().enumerate() {
165 obj.insert(k.clone(), serde_json::json!(values.get(i).copied().unwrap_or(0)));
166 }
167 let v1 = canonical_json(&serde_json::Value::Object(obj.clone())).unwrap();
168 let v2 = canonical_json(&serde_json::Value::Object(obj.clone())).unwrap();
169 prop_assert_eq!(v1, v2);
170 }
171
172 #[test]
175 fn hash_chain_pure(
176 payload in "[a-zA-Z0-9]{1,32}"
177 ) {
178 use crate::decision::chain::HashChain;
179 let chain1 = HashChain::new(b"root");
180 let chain2 = HashChain::new(b"root");
181 let h1 = chain1.append(payload.as_bytes());
182 let h2 = chain2.append(payload.as_bytes());
183 prop_assert_eq!(h1, h2);
184 }
185
186 #[test]
189 fn streaming_equals_allocating(
190 keys in proptest::collection::hash_set("[a-z]{1,4}", 1..6),
191 values in proptest::collection::vec(any::<i64>(), 1..6)
192 ) {
193 let mut obj = serde_json::Map::new();
194 for (i, k) in keys.iter().enumerate() {
195 obj.insert(k.clone(), serde_json::json!(values.get(i).copied().unwrap_or(0)));
196 }
197 let v = serde_json::Value::Object(obj);
198 let allocating = canonical_json(&v).unwrap();
199 let mut streaming = Vec::new();
200 write_canonical_value(&mut streaming, &v).unwrap();
201 prop_assert_eq!(allocating, streaming);
202 }
203 }
204 }
205}