Skip to main content

agent_scroll/
canonical.rs

1use serde_json::{Number, Value};
2use sha2::{Digest, Sha256};
3
4use crate::error::Error;
5
6/// RFC 8785 requires all JSON numbers be formatted via ECMA-262 ToString of f64.
7/// `serde_jcs` preserves u64/i64 above 2^53, so we coerce those to f64 first to
8/// match the precision-loss behaviour of the TypeScript reference.
9fn normalize_numbers(value: Value) -> Value {
10    const SAFE: u64 = 1u64 << 53;
11    match value {
12        Value::Object(map) => Value::Object(
13            map.into_iter()
14                .map(|(k, v)| (k, normalize_numbers(v)))
15                .collect(),
16        ),
17        Value::Array(arr) => Value::Array(arr.into_iter().map(normalize_numbers).collect()),
18        Value::Number(n) => {
19            if let Some(u) = n.as_u64() {
20                if u > SAFE {
21                    return Number::from_f64(u as f64)
22                        .map(Value::Number)
23                        .unwrap_or(Value::Number(n));
24                }
25            } else if let Some(i) = n.as_i64() {
26                if i.unsigned_abs() > SAFE {
27                    return Number::from_f64(i as f64)
28                        .map(Value::Number)
29                        .unwrap_or(Value::Number(n));
30                }
31            }
32            Value::Number(n)
33        }
34        other => other,
35    }
36}
37
38pub fn canonical(value: &Value) -> Result<Vec<u8>, Error> {
39    let normalized = normalize_numbers(value.clone());
40    serde_jcs::to_string(&normalized)
41        .map(|s| s.into_bytes())
42        .map_err(|e| Error::Invalid(format!("jcs: {e}")))
43}
44
45pub fn hash_canonical(value: &Value) -> Result<String, Error> {
46    let bytes = canonical(value)?;
47    let digest = Sha256::digest(&bytes);
48    Ok(format!("sha256:{}", hex_lower(&digest)))
49}
50
51fn hex_lower(bytes: &[u8]) -> String {
52    let mut s = String::with_capacity(bytes.len() * 2);
53    for b in bytes {
54        s.push_str(&format!("{:02x}", b));
55    }
56    s
57}