use blake3::Hasher;
#[derive(Clone, Default)]
pub struct ContentHasher {
inner: Hasher,
}
impl ContentHasher {
#[must_use]
pub fn new() -> Self {
Self {
inner: Hasher::new(),
}
}
pub fn update(&mut self, bytes: &[u8]) -> &mut Self {
self.inner.update(bytes);
self
}
pub fn update_framed(&mut self, label: &[u8], value: &[u8]) -> &mut Self {
self.update_length_prefixed(label);
self.update_length_prefixed(value);
self
}
fn update_length_prefixed(&mut self, bytes: &[u8]) -> &mut Self {
self.inner.update(&(bytes.len() as u64).to_le_bytes());
self.inner.update(bytes);
self
}
#[must_use]
pub fn finalize_hex(&self) -> String {
self.inner.finalize().to_hex().to_string()
}
}
#[must_use]
pub fn hash_hex(bytes: &[u8]) -> String {
let mut hasher = ContentHasher::new();
hasher.update(bytes);
hasher.finalize_hex()
}
#[cfg(test)]
mod tests {
use super::{ContentHasher, hash_hex};
#[test]
fn hash_hex_is_stable_and_64_hex_chars() {
let first = hash_hex(b"toven");
let second = hash_hex(b"toven");
assert_eq!(first, second);
assert_eq!(first.len(), 64);
assert!(
first
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
);
}
#[test]
fn distinct_input_yields_distinct_digest() {
assert_ne!(hash_hex(b"a"), hash_hex(b"b"));
}
#[test]
fn finalize_does_not_consume_the_hasher() {
let mut hasher = ContentHasher::new();
hasher.update(b"a");
let after_a = hasher.finalize_hex();
hasher.update(b"b");
let after_ab = hasher.finalize_hex();
assert_ne!(after_a, after_ab);
}
#[test]
fn framing_prevents_field_boundary_collisions() {
let mut left = ContentHasher::new();
left.update_framed(b"ab", b"c");
let mut right = ContentHasher::new();
right.update_framed(b"a", b"bc");
assert_ne!(left.finalize_hex(), right.finalize_hex());
}
#[test]
fn framing_stays_unambiguous_for_arbitrary_bytes() {
let mut left = ContentHasher::new();
left.update_framed(b"a:b", b"c\0d");
let mut right = ContentHasher::new();
right.update_framed(b"a", b"b:c\0d");
assert_ne!(left.finalize_hex(), right.finalize_hex());
let mut embedded = ContentHasher::new();
embedded.update_framed(b"name", b"value\0name\0other");
let mut split = ContentHasher::new();
split.update_framed(b"name", b"value");
split.update_framed(b"name", b"other");
assert_ne!(embedded.finalize_hex(), split.finalize_hex());
}
#[test]
fn raw_concatenation_would_collide_without_framing() {
let mut left = ContentHasher::new();
left.update(b"ab").update(b"c");
let mut right = ContentHasher::new();
right.update(b"a").update(b"bc");
assert_eq!(left.finalize_hex(), right.finalize_hex());
}
}