use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Hash([u8; 32]);
impl Hash {
pub fn of(bytes: &[u8]) -> Self {
Hash(blake3::hash(bytes).into())
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Hash(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
pub fn from_hex(s: &str) -> Result<Self, HashParseError> {
if s.len() != 64 {
return Err(HashParseError::WrongLength);
}
if !s.bytes().all(is_lower_hex) {
return Err(HashParseError::InvalidChar);
}
let mut buf = [0u8; 32];
hex::decode_to_slice(s, &mut buf).map_err(|_| HashParseError::InvalidChar)?;
Ok(Hash(buf))
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashParseError {
WrongLength,
InvalidChar,
}
impl fmt::Display for HashParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HashParseError::WrongLength => f.write_str("hash must be 64 hex digits"),
HashParseError::InvalidChar => {
f.write_str("hash contains invalid character (must be lowercase hex)")
}
}
}
}
impl std::error::Error for HashParseError {}
fn is_lower_hex(b: u8) -> bool {
matches!(b, b'0'..=b'9' | b'a'..=b'f')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn of_known_value() {
let h = Hash::of(b"");
assert_eq!(
h.to_hex(),
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
);
}
#[test]
fn round_trip_hex() {
let h = Hash::of(b"hello world");
let s = h.to_hex();
assert_eq!(s.len(), 64);
let parsed = Hash::from_hex(&s).unwrap();
assert_eq!(h, parsed);
}
#[test]
fn rejects_uppercase() {
let h = Hash::of(b"x");
let upper = h.to_hex().to_uppercase();
assert_eq!(Hash::from_hex(&upper), Err(HashParseError::InvalidChar));
}
#[test]
fn rejects_wrong_length() {
assert_eq!(Hash::from_hex(""), Err(HashParseError::WrongLength));
assert_eq!(Hash::from_hex("abcd"), Err(HashParseError::WrongLength));
let too_long = "a".repeat(65);
assert_eq!(Hash::from_hex(&too_long), Err(HashParseError::WrongLength));
}
#[test]
fn rejects_non_hex() {
let bad = "g".repeat(64);
assert_eq!(Hash::from_hex(&bad), Err(HashParseError::InvalidChar));
}
#[test]
fn display_is_hex() {
let h = Hash::of(b"abc");
assert_eq!(format!("{h}"), h.to_hex());
}
}