use core::fmt;
pub const HASH_LEN: usize = 32;
pub const HEX_LEN: usize = 64;
pub type Hash = [u8; HASH_LEN];
pub const ZERO: Hash = [0u8; HASH_LEN];
#[must_use]
pub fn hash(data: &[u8]) -> Hash {
let h = blake3::hash(data);
*h.as_bytes()
}
#[derive(Debug, Default, Clone)]
pub struct Hasher {
inner: blake3::Hasher,
}
impl Hasher {
#[must_use]
pub fn new() -> Self {
Self {
inner: blake3::Hasher::new(),
}
}
pub fn update(&mut self, data: &[u8]) -> &mut Self {
self.inner.update(data);
self
}
#[must_use]
pub fn finalize(&self) -> Hash {
*self.inner.finalize().as_bytes()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum FromHexError {
#[error("hex digest must be {} chars, got {actual}", HEX_LEN)]
InvalidLength { actual: usize },
#[error("hex digest contained a non-hex byte")]
InvalidChar,
}
#[must_use]
pub fn to_hex_bytes(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
const HEX: &[u8; 16] = b"0123456789abcdef";
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
#[must_use]
pub fn to_hex(h: &Hash) -> String {
to_hex_bytes(h)
}
#[must_use]
pub fn domain_digest(domain: &[u8], body: &[u8]) -> Hash {
let mut h = blake3::Hasher::new();
let domain_len = u16::try_from(domain.len()).expect("domain <= u16::MAX");
h.update(&domain_len.to_le_bytes());
h.update(domain);
h.update(body);
*h.finalize().as_bytes()
}
pub fn from_hex(s: &str) -> Result<Hash, FromHexError> {
let bytes = s.as_bytes();
if bytes.len() != HEX_LEN {
return Err(FromHexError::InvalidLength {
actual: bytes.len(),
});
}
let mut out = [0u8; HASH_LEN];
for i in 0..HASH_LEN {
let hi = hex_nibble(bytes[i * 2])?;
let lo = hex_nibble(bytes[i * 2 + 1])?;
out[i] = (hi << 4) | lo;
}
Ok(out)
}
fn hex_nibble(b: u8) -> Result<u8, FromHexError> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(10 + (b - b'a')),
b'A'..=b'F' => Ok(10 + (b - b'A')),
_ => Err(FromHexError::InvalidChar),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObjectPath {
pub dir: [u8; 2],
pub file: [u8; 62],
}
impl fmt::Display for ObjectPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}/{}",
core::str::from_utf8(&self.dir).expect("ascii hex"),
core::str::from_utf8(&self.file).expect("ascii hex"),
)
}
}
#[must_use]
pub fn object_path(h: &Hash) -> ObjectPath {
let hex = to_hex(h);
let bytes = hex.as_bytes();
let mut dir = [0u8; 2];
let mut file = [0u8; 62];
dir.copy_from_slice(&bytes[..2]);
file.copy_from_slice(&bytes[2..]);
ObjectPath { dir, file }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_vector_hello() {
let h = hash(b"hello");
assert_eq!(
to_hex(&h),
"ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f"
);
}
#[test]
fn to_hex_bytes_matches_to_hex_for_32_byte_slice() {
let h = hash(b"any");
assert_eq!(to_hex_bytes(h.as_slice()), to_hex(&h));
}
#[test]
fn to_hex_bytes_handles_arbitrary_length() {
assert_eq!(to_hex_bytes(b""), "");
assert_eq!(to_hex_bytes(&[0x00, 0xff]), "00ff");
assert_eq!(to_hex_bytes(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
}
#[test]
fn incremental_matches_oneshot() {
let oneshot = hash(b"hello world");
let mut h = Hasher::new();
h.update(b"hello ").update(b"world");
assert_eq!(oneshot, h.finalize());
}
#[test]
fn from_hex_roundtrip() {
let h = hash(b"test");
let hex = to_hex(&h);
let parsed = from_hex(&hex).unwrap();
assert_eq!(h, parsed);
}
#[test]
fn from_hex_accepts_mixed_case() {
let lower = "ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f";
let upper = lower.to_ascii_uppercase();
assert_eq!(from_hex(lower).unwrap(), from_hex(&upper).unwrap());
}
#[test]
fn from_hex_rejects_too_short() {
assert!(matches!(
from_hex("abcdef"),
Err(FromHexError::InvalidLength { .. })
));
}
#[test]
fn from_hex_rejects_bad_char() {
let bad: String = "gg".chars().chain("00".repeat(31).chars()).collect();
assert_eq!(from_hex(&bad), Err(FromHexError::InvalidChar));
}
#[test]
fn to_hex_of_zero_is_all_zeros() {
assert_eq!(to_hex(&ZERO), "0".repeat(HEX_LEN));
}
#[test]
fn object_path_splits_correctly() {
let h = hash(b"test");
let path = object_path(&h);
let hex = to_hex(&h);
assert_eq!(&path.dir, &hex.as_bytes()[..2]);
assert_eq!(&path.file[..], &hex.as_bytes()[2..]);
}
}