use serde::Serialize;
use sha2::{Digest, Sha256};
pub fn digest_bytes(domain: &str, bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(domain.as_bytes());
hasher.update([0]);
hasher.update(bytes);
let digest = hasher.finalize();
format!("sha256:{digest:x}")
}
pub fn digest_json<T: Serialize>(domain: &str, value: &T) -> Result<String, serde_json::Error> {
let bytes = serde_json::to_vec(value)?;
Ok(digest_bytes(domain, &bytes))
}
pub fn validate_digest(value: &str) -> Result<(), InvalidDigest> {
let Some(hex) = value.strip_prefix("sha256:") else {
return Err(InvalidDigest);
};
if hex.len() != 64
|| !hex
.bytes()
.all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
{
return Err(InvalidDigest);
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InvalidDigest;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_separation_changes_digest() {
let left = digest_bytes("a.v1", b"payload");
let right = digest_bytes("b.v1", b"payload");
assert_ne!(left, right);
assert!(left.starts_with("sha256:"));
assert_eq!(left.len(), "sha256:".len() + 64);
}
#[test]
fn validate_digest_accepts_canonical_form() {
let digest = digest_bytes("a.v1", b"payload");
assert!(validate_digest(&digest).is_ok());
assert!(validate_digest(&digest.to_ascii_uppercase()).is_err());
assert!(validate_digest("not-a-digest").is_err());
}
#[test]
fn digest_json_round_trips_structured_values() {
#[derive(Serialize)]
struct Payload<'a> {
name: &'a str,
count: u32,
}
let digest = digest_json(
"test.domain",
&Payload {
name: "alpha",
count: 3,
},
)
.unwrap();
assert!(digest.starts_with("sha256:"));
assert_eq!(digest.len(), "sha256:".len() + 64);
assert_eq!(
digest,
digest_json(
"test.domain",
&Payload {
name: "alpha",
count: 3
}
)
.unwrap()
);
assert_ne!(
digest,
digest_json(
"other.domain",
&Payload {
name: "alpha",
count: 3
}
)
.unwrap()
);
}
#[test]
fn validate_digest_rejects_malformed_hex() {
let digest = digest_bytes("a.v1", b"payload");
let short = format!("sha256:{}", "a".repeat(63));
let long = format!("sha256:{}", "a".repeat(65));
let non_hex = format!("sha256:{}", "g".repeat(64));
assert!(validate_digest(&digest).is_ok());
assert!(validate_digest(&short).is_err());
assert!(validate_digest(&long).is_err());
assert!(validate_digest(&non_hex).is_err());
assert!(validate_digest("sha256:").is_err());
}
}