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 {
46 fn canonical_write(&self, out: &mut StateHasher);
48}
49
50#[derive(Default)]
52pub struct StateHasher {
53 inner: blake3::Hasher,
54}
55
56impl StateHasher {
57 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn u8(&mut self, value: u8) {
65 self.inner.update(&[value]);
66 }
67 pub fn u16(&mut self, value: u16) {
69 self.inner.update(&value.to_le_bytes());
70 }
71 pub fn u32(&mut self, value: u32) {
73 self.inner.update(&value.to_le_bytes());
74 }
75 pub fn u64(&mut self, value: u64) {
77 self.inner.update(&value.to_le_bytes());
78 }
79 pub fn i32(&mut self, value: i32) {
81 self.inner.update(&value.to_le_bytes());
82 }
83 pub fn i64(&mut self, value: i64) {
85 self.inner.update(&value.to_le_bytes());
86 }
87 pub fn bool(&mut self, value: bool) {
89 self.u8(u8::from(value));
90 }
91
92 pub fn bytes(&mut self, bytes: &[u8]) {
94 self.length(bytes.len());
95 self.inner.update(bytes);
96 }
97
98 pub fn length(&mut self, length: usize) {
104 self.u32(u32::try_from(length).expect("canonical collection length exceeds u32"));
105 }
106
107 pub fn write<T: CanonicalWrite + ?Sized>(&mut self, value: &T) {
109 value.canonical_write(self);
110 }
111
112 pub fn sequence<T: CanonicalWrite>(&mut self, values: &[T]) {
114 self.length(values.len());
115 for value in values {
116 self.write(value);
117 }
118 }
119
120 #[must_use]
122 pub fn finish(self) -> Digest {
123 Digest(*self.inner.finalize().as_bytes())
124 }
125}
126
127macro_rules! canonical_integer {
128 ($type:ty, $method:ident) => {
129 impl CanonicalWrite for $type {
130 fn canonical_write(&self, out: &mut StateHasher) {
131 out.$method(*self);
132 }
133 }
134 };
135}
136
137canonical_integer!(u8, u8);
138canonical_integer!(u16, u16);
139canonical_integer!(u32, u32);
140canonical_integer!(u64, u64);
141canonical_integer!(i32, i32);
142canonical_integer!(i64, i64);
143
144impl CanonicalWrite for bool {
145 fn canonical_write(&self, out: &mut StateHasher) {
146 out.bool(*self);
147 }
148}
149
150#[must_use]
152pub fn digest_of<T: CanonicalWrite + ?Sized>(value: &T) -> Digest {
153 let mut hasher = StateHasher::new();
154 hasher.write(value);
155 hasher.finish()
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use wasm_bindgen_test::wasm_bindgen_test;
162
163 #[wasm_bindgen_test(unsupported = test)]
167 fn bool_writes_through_the_trait_are_canonical() {
168 let mut direct = StateHasher::new();
169 direct.bool(true);
170 assert_eq!(digest_of(&true), direct.finish());
171 assert_ne!(digest_of(&true), digest_of(&false));
172 }
173
174 #[wasm_bindgen_test(unsupported = test)]
175 fn length_prefixes_and_order_are_unambiguous() {
176 let mut split_a = StateHasher::new();
177 split_a.bytes(b"ab");
178 split_a.bytes(b"c");
179 let mut split_b = StateHasher::new();
180 split_b.bytes(b"a");
181 split_b.bytes(b"bc");
182 assert_ne!(split_a.finish(), split_b.finish());
183 let mut first = StateHasher::new();
184 first.sequence(&[1_u32, 2]);
185 let mut second = StateHasher::new();
186 second.sequence(&[2_u32, 1]);
187 assert_ne!(first.finish(), second.finish());
188 }
189
190 #[wasm_bindgen_test(unsupported = test)]
191 fn canonical_digest_is_frozen() {
192 let mut hasher = StateHasher::new();
193 hasher.u16(0x1234);
194 hasher.i32(-2);
195 hasher.bool(true);
196 hasher.bytes(b"sgl");
197 assert_eq!(
198 hasher.finish().to_hex(),
199 "e18ef0865fe504e976275cc32bb1b8bbcbf812fd92b8e1e21be43c29a979444a"
200 );
201 }
202
203 #[wasm_bindgen_test(unsupported = test)]
204 fn raw_content_digest_is_exact_blake3() {
205 assert_eq!(
206 Digest::hash_bytes(b"abc").to_hex(),
207 "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"
208 );
209 }
210}