1use crate::cache::CacheKey;
18use crate::error::{Result, SomaError};
19use ciborium::value::Value as Cbor;
20use serde::Serialize;
21
22pub fn canonical_bytes<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>> {
27 let mut raw = Vec::new();
28 ciborium::ser::into_writer(value, &mut raw)
29 .map_err(|e| SomaError::Cache(format!("not canonically serializable: {e}")))?;
30 let decoded: Cbor = ciborium::de::from_reader(raw.as_slice())
31 .map_err(|e| SomaError::Cache(format!("canonical re-decode failed: {e}")))?;
32 let canon = canonicalize(decoded)?;
33 encode(&canon)
34}
35
36pub fn hash_canonical<T: Serialize + ?Sized>(value: &T) -> Result<CacheKey> {
38 Ok(CacheKey::hash_data(&canonical_bytes(value)?))
39}
40
41fn encode(v: &Cbor) -> Result<Vec<u8>> {
42 let mut buf = Vec::new();
43 ciborium::ser::into_writer(v, &mut buf)
44 .map_err(|e| SomaError::Cache(format!("canonical encode failed: {e}")))?;
45 Ok(buf)
46}
47
48fn canonicalize(v: Cbor) -> Result<Cbor> {
49 Ok(match v {
50 Cbor::Float(f) => Cbor::Float(canonical_float(f)),
51 Cbor::Array(items) => {
52 Cbor::Array(items.into_iter().map(canonicalize).collect::<Result<_>>()?)
53 }
54 Cbor::Map(entries) => {
55 let mut encoded: Vec<(Vec<u8>, Cbor, Cbor)> = Vec::with_capacity(entries.len());
56 for (key, value) in entries {
57 let key = canonicalize(key)?;
58 let value = canonicalize(value)?;
59 let key_bytes = encode(&key)?;
60 encoded.push((key_bytes, key, value));
61 }
62 encoded.sort_by(|a, b| a.0.cmp(&b.0));
63 for pair in encoded.windows(2) {
64 if pair[0].0 == pair[1].0 {
65 return Err(SomaError::Cache(
66 "duplicate map key in canonical encoding".into(),
67 ));
68 }
69 }
70 Cbor::Map(encoded.into_iter().map(|(_, k, v)| (k, v)).collect())
71 }
72 Cbor::Tag(tag, inner) => Cbor::Tag(tag, Box::new(canonicalize(*inner)?)),
73 other => other,
74 })
75}
76
77fn canonical_float(f: f64) -> f64 {
79 if f.is_nan() {
80 f64::NAN
81 } else if f == 0.0 {
82 0.0
83 } else {
84 f
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use std::collections::HashMap;
92
93 #[test]
94 fn hashmap_encoding_is_order_independent() {
95 let mut reference: Option<Vec<u8>> = None;
98 for i in 0..100 {
99 let mut map = HashMap::new();
100 let keys = ["alpha", "beta", "gamma", "delta", "epsilon"];
102 for (j, _k) in keys.iter().enumerate() {
103 let idx = (i + j) % keys.len();
104 map.insert(keys[idx].to_string(), idx as i64);
105 }
106 let bytes = canonical_bytes(&map).unwrap();
107 match &reference {
108 None => reference = Some(bytes),
109 Some(r) => assert_eq!(r, &bytes, "iteration {i} diverged"),
110 }
111 }
112 }
113
114 #[test]
115 fn nested_structures_canonicalize() {
116 #[derive(serde::Serialize)]
117 struct Config {
118 name: String,
119 params: HashMap<String, f64>,
120 layers: Vec<u32>,
121 }
122 let mut params = HashMap::new();
123 params.insert("lr".into(), 0.001);
124 params.insert("momentum".into(), 0.9);
125 let a = Config {
126 name: "m".into(),
127 params: params.clone(),
128 layers: vec![64, 32],
129 };
130 let b = Config {
131 name: "m".into(),
132 params,
133 layers: vec![64, 32],
134 };
135 assert_eq!(hash_canonical(&a).unwrap(), hash_canonical(&b).unwrap());
136 }
137
138 #[test]
139 fn negative_zero_normalizes() {
140 assert_eq!(
141 canonical_bytes(&(-0.0f64)).unwrap(),
142 canonical_bytes(&0.0f64).unwrap()
143 );
144 }
145
146 #[test]
147 fn nan_payloads_collapse_to_one_encoding() {
148 let quiet = f64::NAN;
149 let payload = f64::from_bits(0x7ff8_0000_0000_0001);
150 assert!(payload.is_nan());
151 assert_eq!(
152 canonical_bytes(&quiet).unwrap(),
153 canonical_bytes(&payload).unwrap()
154 );
155 }
156
157 #[test]
158 fn distinct_values_distinct_hashes() {
159 assert_ne!(
160 hash_canonical(&vec![1.0f64, 2.0]).unwrap(),
161 hash_canonical(&vec![2.0f64, 1.0]).unwrap()
162 );
163 assert_ne!(hash_canonical(&"a").unwrap(), hash_canonical(&"b").unwrap());
164 }
165
166 #[test]
167 fn float_and_int_do_not_collide() {
168 assert_ne!(
170 canonical_bytes(&1u64).unwrap(),
171 canonical_bytes(&1.0f64).unwrap()
172 );
173 }
174}