a3s_code_core/
content_digest.rs1use serde::Serialize;
9use sha2::{Digest, Sha256};
10
11pub 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
21pub 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
27pub 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#[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}