1use std::fmt;
16
17use blake3::Hash as Blake3Hash;
18
19#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
21pub enum HashError {
22 #[error("invalid hash length: expected 64 hex chars, got {0}")]
24 InvalidLength(usize),
25 #[error("invalid hex in hash")]
27 InvalidHex,
28}
29
30#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
36pub struct Hash(
37 pub [u8; 32],
39);
40
41impl Hash {
42 #[must_use]
44 pub fn from_data(data: &[u8]) -> Self {
45 Self(*blake3::hash(data).as_bytes())
46 }
47
48 pub fn from_hex(hex: &str) -> Result<Self, HashError> {
50 if hex.len() != 64 {
51 return Err(HashError::InvalidLength(hex.len()));
52 }
53 let mut bytes = [0u8; 32];
54 hex.as_bytes()
55 .chunks_exact(2)
56 .zip(bytes.iter_mut())
57 .try_for_each(|(chunk, byte)| {
58 *byte = u8::from_str_radix(
59 std::str::from_utf8(chunk).map_err(|_| HashError::InvalidHex)?,
60 16,
61 )
62 .map_err(|_| HashError::InvalidHex)?;
63 Ok::<_, HashError>(())
64 })?;
65 Ok(Self(bytes))
66 }
67
68 #[must_use]
70 pub fn to_hex(&self) -> String {
71 const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
72 let mut s = String::with_capacity(64);
73 for byte in &self.0 {
74 s.push(HEX_CHARS[usize::from(byte >> 4)] as char);
75 s.push(HEX_CHARS[usize::from(byte & 0x0f)] as char);
76 }
77 s
78 }
79
80 pub const ZERO: Self = Self([0u8; 32]);
82
83 #[must_use]
92 pub fn bucket(&self) -> u8 {
93 self.0[0]
94 }
95
96 #[must_use]
98 pub fn as_blake3(&self) -> Blake3Hash {
99 Blake3Hash::from_bytes(self.0)
100 }
101}
102
103impl fmt::Debug for Hash {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "Hash({})", self.to_hex())
106 }
107}
108
109impl fmt::Display for Hash {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 let hex = self.to_hex();
113 write!(f, "{}…", &hex[..12])
114 }
115}
116
117impl From<Blake3Hash> for Hash {
118 fn from(h: Blake3Hash) -> Self {
119 Self(*h.as_bytes())
120 }
121}
122
123impl From<[u8; 32]> for Hash {
124 fn from(bytes: [u8; 32]) -> Self {
125 Self(bytes)
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 const EMPTY_BLAKE3_HEX: &str =
135 "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262";
136
137 #[test]
138 fn hex_roundtrip() -> Result<(), HashError> {
139 let h = Hash::from_data(b"hello");
140 let parsed = Hash::from_hex(&h.to_hex())?;
141 assert_eq!(h, parsed);
142 Ok(())
143 }
144
145 #[test]
146 fn empty_string_vector() -> Result<(), HashError> {
147 assert_eq!(Hash::from_data(b"").to_hex(), EMPTY_BLAKE3_HEX);
148 assert_eq!(Hash::from_hex(EMPTY_BLAKE3_HEX)?, Hash::from_data(b""));
149 Ok(())
150 }
151
152 #[test]
153 fn rejects_bad_length() {
154 assert_eq!(Hash::from_hex("abc"), Err(HashError::InvalidLength(3)));
155 assert_eq!(
156 Hash::from_hex(&"a".repeat(63)),
157 Err(HashError::InvalidLength(63))
158 );
159 }
160
161 #[test]
162 fn rejects_bad_hex() {
163 let mut bad = "a".repeat(63);
164 bad.push('g');
165 assert_eq!(Hash::from_hex(&bad), Err(HashError::InvalidHex));
166 }
167
168 #[test]
169 fn display_is_short_form() -> Result<(), HashError> {
170 let h = Hash::from_hex(EMPTY_BLAKE3_HEX)?;
171 assert_eq!(format!("{h}"), "af1349b9f5f9…");
172 Ok(())
173 }
174
175 #[test]
176 fn debug_is_full_hex() -> Result<(), HashError> {
177 let h = Hash::from_hex(EMPTY_BLAKE3_HEX)?;
178 assert_eq!(format!("{h:?}"), format!("Hash({EMPTY_BLAKE3_HEX})"));
179 Ok(())
180 }
181
182 #[test]
183 fn zero_sentinel() {
184 assert_eq!(Hash::ZERO.0, [0u8; 32]);
185 }
186
187 #[test]
188 fn ordering_is_byte_lexicographic() {
189 let mut a = Hash::ZERO;
190 a.0[0] = 0x00;
191 let mut b = Hash::ZERO;
192 b.0[0] = 0x01;
193 assert!(a < b);
194 }
195}