1use core::fmt;
4
5#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct Digest(pub [u8; 32]);
8
9impl Digest {
10 #[must_use]
15 pub fn hash_bytes(bytes: &[u8]) -> Self {
16 Self(*blake3::hash(bytes).as_bytes())
17 }
18
19 #[must_use]
21 pub fn to_hex(self) -> String {
22 const HEX: &[u8; 16] = b"0123456789abcdef";
23 let mut result = String::with_capacity(64);
24 for byte in self.0 {
25 result.push(char::from(HEX[usize::from(byte >> 4)]));
26 result.push(char::from(HEX[usize::from(byte & 15)]));
27 }
28 result
29 }
30}
31
32impl fmt::Debug for Digest {
33 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34 write!(formatter, "Digest({self})")
35 }
36}
37
38impl fmt::Display for Digest {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 formatter.write_str(&self.to_hex())
41 }
42}
43
44pub trait CanonicalWrite {
55 fn canonical_write(&self, out: &mut StateHasher);
57}
58
59#[derive(Default)]
71pub struct StateHasher {
72 inner: blake3::Hasher,
73}
74
75impl StateHasher {
76 #[must_use]
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn u8(&mut self, value: u8) {
84 self.inner.update(&[value]);
85 }
86 pub fn u16(&mut self, value: u16) {
88 self.inner.update(&value.to_le_bytes());
89 }
90 pub fn u32(&mut self, value: u32) {
92 self.inner.update(&value.to_le_bytes());
93 }
94 pub fn u64(&mut self, value: u64) {
96 self.inner.update(&value.to_le_bytes());
97 }
98 pub fn i32(&mut self, value: i32) {
100 self.inner.update(&value.to_le_bytes());
101 }
102 pub fn i64(&mut self, value: i64) {
104 self.inner.update(&value.to_le_bytes());
105 }
106 pub fn bool(&mut self, value: bool) {
108 self.u8(u8::from(value));
109 }
110
111 pub fn bytes(&mut self, bytes: &[u8]) {
113 self.length(bytes.len());
114 self.inner.update(bytes);
115 }
116
117 pub fn length(&mut self, length: usize) {
123 self.u32(u32::try_from(length).expect("canonical collection length exceeds u32"));
124 }
125
126 pub fn write<T: CanonicalWrite + ?Sized>(&mut self, value: &T) {
128 value.canonical_write(self);
129 }
130
131 pub fn sequence<T: CanonicalWrite>(&mut self, values: &[T]) {
133 self.length(values.len());
134 for value in values {
135 self.write(value);
136 }
137 }
138
139 #[must_use]
141 pub fn finish(self) -> Digest {
142 Digest(*self.inner.finalize().as_bytes())
143 }
144}
145
146macro_rules! canonical_integer {
147 ($type:ty, $method:ident) => {
148 impl CanonicalWrite for $type {
149 fn canonical_write(&self, out: &mut StateHasher) {
150 out.$method(*self);
151 }
152 }
153 };
154}
155
156canonical_integer!(u8, u8);
157canonical_integer!(u16, u16);
158canonical_integer!(u32, u32);
159canonical_integer!(u64, u64);
160canonical_integer!(i32, i32);
161canonical_integer!(i64, i64);
162
163impl CanonicalWrite for bool {
164 fn canonical_write(&self, out: &mut StateHasher) {
165 out.bool(*self);
166 }
167}
168
169#[must_use]
171pub fn digest_of<T: CanonicalWrite + ?Sized>(value: &T) -> Digest {
172 let mut hasher = StateHasher::new();
173 hasher.write(value);
174 hasher.finish()
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use wasm_bindgen_test::wasm_bindgen_test;
181
182 #[wasm_bindgen_test(unsupported = test)]
186 fn bool_writes_through_the_trait_are_canonical() {
187 let mut direct = StateHasher::new();
188 direct.bool(true);
189 assert_eq!(digest_of(&true), direct.finish());
190 assert_ne!(digest_of(&true), digest_of(&false));
191 }
192
193 #[wasm_bindgen_test(unsupported = test)]
194 fn length_prefixes_and_order_are_unambiguous() {
195 let mut split_a = StateHasher::new();
196 split_a.bytes(b"ab");
197 split_a.bytes(b"c");
198 let mut split_b = StateHasher::new();
199 split_b.bytes(b"a");
200 split_b.bytes(b"bc");
201 assert_ne!(split_a.finish(), split_b.finish());
202 let mut first = StateHasher::new();
203 first.sequence(&[1_u32, 2]);
204 let mut second = StateHasher::new();
205 second.sequence(&[2_u32, 1]);
206 assert_ne!(first.finish(), second.finish());
207 }
208
209 #[wasm_bindgen_test(unsupported = test)]
210 fn canonical_digest_is_frozen() {
211 let mut hasher = StateHasher::new();
212 hasher.u16(0x1234);
213 hasher.i32(-2);
214 hasher.bool(true);
215 hasher.bytes(b"sgl");
216 assert_eq!(
217 hasher.finish().to_hex(),
218 "e18ef0865fe504e976275cc32bb1b8bbcbf812fd92b8e1e21be43c29a979444a"
219 );
220 }
221
222 #[wasm_bindgen_test(unsupported = test)]
223 fn raw_content_digest_is_exact_blake3() {
224 assert_eq!(
225 Digest::hash_bytes(b"abc").to_hex(),
226 "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"
227 );
228 }
229}