Skip to main content

a3s_code_core/
content_digest.rs

1//! Shared domain-separated SHA-256 digests.
2//!
3//! These helpers are used by the evaluation substrate and by non-evaluation
4//! harness paths (store WAL, orchestration checkpoints, Flow decisions).
5//! Keeping them outside `evaluation` lets Advanced evaluation stay optional
6//! without forcing digest callers into that module graph.
7
8use serde::Serialize;
9use sha2::{Digest, Sha256};
10
11/// Domain-separated SHA-256 digest encoded as `sha256:<hex>`.
12pub fn digest_bytes(domain: &str, bytes: &[u8]) -> String {
13    let mut hasher = Sha256::new();
14    hasher.update(domain.as_bytes());
15    hasher.update([0]);
16    hasher.update(bytes);
17    let digest = hasher.finalize();
18    format!("sha256:{digest:x}")
19}
20
21/// JSON-serialize `value` then digest the bytes under `domain`.
22pub fn digest_json<T: Serialize>(domain: &str, value: &T) -> Result<String, serde_json::Error> {
23    let bytes = serde_json::to_vec(value)?;
24    Ok(digest_bytes(domain, &bytes))
25}
26
27/// Reject digests that are not lowercase `sha256:` + 64 hex characters.
28pub fn validate_digest(value: &str) -> Result<(), InvalidDigest> {
29    let Some(hex) = value.strip_prefix("sha256:") else {
30        return Err(InvalidDigest);
31    };
32    if hex.len() != 64
33        || !hex
34            .bytes()
35            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
36    {
37        return Err(InvalidDigest);
38    }
39    Ok(())
40}
41
42/// Marker error for an ill-formed `sha256:<hex>` digest string.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct InvalidDigest;
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn domain_separation_changes_digest() {
52        let left = digest_bytes("a.v1", b"payload");
53        let right = digest_bytes("b.v1", b"payload");
54        assert_ne!(left, right);
55        assert!(left.starts_with("sha256:"));
56        assert_eq!(left.len(), "sha256:".len() + 64);
57    }
58
59    #[test]
60    fn validate_digest_accepts_canonical_form() {
61        let digest = digest_bytes("a.v1", b"payload");
62        assert!(validate_digest(&digest).is_ok());
63        assert!(validate_digest(&digest.to_ascii_uppercase()).is_err());
64        assert!(validate_digest("not-a-digest").is_err());
65    }
66}