use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::agentlog::canonical;
pub const ID_PREFIX: &str = "sha256:";
pub const HEX_LEN: usize = 64;
pub fn content_id(payload: &Value) -> String {
let bytes = canonical::to_bytes(payload);
let digest = Sha256::digest(&bytes);
let mut out = String::with_capacity(ID_PREFIX.len() + HEX_LEN);
out.push_str(ID_PREFIX);
for byte in digest {
out.push(nibble(byte >> 4));
out.push(nibble(byte & 0xF));
}
out
}
pub fn is_valid(s: &str) -> bool {
if !s.starts_with(ID_PREFIX) {
return false;
}
let hex = &s[ID_PREFIX.len()..];
hex.len() == HEX_LEN && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
fn nibble(n: u8) -> char {
debug_assert!(n < 16);
match n {
0..=9 => (b'0' + n) as char,
_ => (b'a' + (n - 10)) as char,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn spec_5_6_known_vector() {
let payload = json!({"hello": "world"});
assert_eq!(
content_id(&payload),
"sha256:93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588"
);
}
#[test]
fn id_is_prefixed_and_64_hex_chars() {
let id = content_id(&json!(null));
assert!(id.starts_with("sha256:"));
let hex = &id[ID_PREFIX.len()..];
assert_eq!(hex.len(), HEX_LEN);
assert!(hex
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()));
}
#[test]
fn determinism_across_calls() {
let p = json!({"model": "claude-opus-4-7", "temperature": 0.2});
assert_eq!(content_id(&p), content_id(&p));
}
#[test]
fn equivalent_payloads_hash_equal() {
let a = json!({"a": 1, "b": 2});
let b = json!({"b": 2, "a": 1});
assert_eq!(content_id(&a), content_id(&b));
}
#[test]
fn nfc_equivalence_produces_equal_id() {
let decomposed = json!({"key": "e\u{0301}clair"});
let precomposed = json!({"key": "\u{00e9}clair"});
assert_eq!(content_id(&decomposed), content_id(&precomposed));
}
#[test]
fn distinct_payloads_hash_different() {
let a = json!({"a": 1});
let b = json!({"a": 2});
assert_ne!(content_id(&a), content_id(&b));
}
#[test]
fn is_valid_accepts_well_formed_id() {
assert!(is_valid(
"sha256:93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588"
));
}
#[test]
fn is_valid_rejects_wrong_prefix() {
assert!(!is_valid(
"md5:93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588"
));
assert!(!is_valid(
"93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588"
));
}
#[test]
fn is_valid_rejects_wrong_length() {
assert!(!is_valid("sha256:abcd"));
assert!(!is_valid(&format!("sha256:{}", "a".repeat(63))));
assert!(!is_valid(&format!("sha256:{}", "a".repeat(65))));
}
#[test]
fn is_valid_rejects_uppercase_hex() {
assert!(!is_valid(
"sha256:93A23971A914E5EACBF0A8D25154CDA309C3C1C72FBB9914D47C60F3CB681588"
));
}
}