1use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7
8pub const DIGEST_LEN: usize = 32;
10
11#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct Digest(#[serde(with = "serde_bytes_32")] [u8; DIGEST_LEN]);
20
21impl Digest {
22 pub fn of(bytes: &[u8]) -> Self {
28 Self(*blake3::hash(bytes).as_bytes())
29 }
30
31 pub const fn from_bytes(bytes: [u8; DIGEST_LEN]) -> Self {
33 Self(bytes)
34 }
35
36 pub const fn as_bytes(&self) -> &[u8; DIGEST_LEN] {
38 &self.0
39 }
40
41 pub fn to_hex(&self) -> String {
43 use fmt::Write as _;
44 self.0.iter().fold(String::with_capacity(64), |mut s, b| {
45 let _ = write!(s, "{b:02x}");
46 s
47 })
48 }
49
50 pub fn short(&self, n: usize) -> String {
54 let hex = self.to_hex();
55 hex[..n.min(hex.len())].to_string()
56 }
57}
58
59#[derive(Debug, thiserror::Error)]
61pub enum DigestParseError {
62 #[error("expected {expected} hex characters, found {found}")]
64 Length {
65 expected: usize,
67 found: usize,
69 },
70
71 #[error("invalid hex character {ch:?} at position {index}")]
73 Char {
74 ch: char,
76 index: usize,
78 },
79}
80
81impl FromStr for Digest {
82 type Err = DigestParseError;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 if s.len() != DIGEST_LEN * 2 {
86 return Err(DigestParseError::Length {
87 expected: DIGEST_LEN * 2,
88 found: s.len(),
89 });
90 }
91 let mut out = [0u8; DIGEST_LEN];
92 for (i, byte) in out.iter_mut().enumerate() {
93 let pair = &s[i * 2..i * 2 + 2];
94 *byte = u8::from_str_radix(pair, 16).map_err(|_| {
95 let (index, ch) = pair
96 .char_indices()
97 .find(|(_, c)| !c.is_ascii_hexdigit())
98 .map(|(j, c)| (i * 2 + j, c))
99 .unwrap_or((i * 2, pair.chars().next().unwrap_or('?')));
100 DigestParseError::Char { ch, index }
101 })?;
102 }
103 Ok(Self(out))
104 }
105}
106
107impl fmt::Display for Digest {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 f.write_str(&self.to_hex())
110 }
111}
112
113impl fmt::Debug for Digest {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(f, "Digest({}…)", self.short(12))
117 }
118}
119
120mod serde_bytes_32 {
138 use super::DIGEST_LEN;
139 use serde::de::{Error as _, Unexpected};
140 use serde::{Deserialize, Deserializer, Serializer};
141
142 pub(super) fn serialize<S: Serializer>(
143 bytes: &[u8; DIGEST_LEN],
144 s: S,
145 ) -> Result<S::Ok, S::Error> {
146 if s.is_human_readable() {
147 return s.serialize_str(&super::Digest(*bytes).to_hex());
148 }
149 s.serialize_bytes(bytes)
150 }
151
152 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
153 d: D,
154 ) -> Result<[u8; DIGEST_LEN], D::Error> {
155 if d.is_human_readable() {
156 let hex = String::deserialize(d)?;
159 return hex
160 .parse::<super::Digest>()
161 .map(|digest| *digest.as_bytes())
162 .map_err(|_| D::Error::invalid_value(Unexpected::Str(&hex), &"64 hex characters"));
163 }
164
165 let raw = serde_bytes_helper::ByteBuf::deserialize(d)?;
166 <[u8; DIGEST_LEN]>::try_from(raw.0.as_slice())
167 .map_err(|_| D::Error::invalid_value(Unexpected::Bytes(&raw.0), &"exactly 32 bytes"))
168 }
169
170 mod serde_bytes_helper {
172 use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor};
173 use std::fmt;
174
175 pub(super) struct ByteBuf(pub(super) Vec<u8>);
176
177 impl<'de> Deserialize<'de> for ByteBuf {
178 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
179 struct V;
180 impl<'de> Visitor<'de> for V {
181 type Value = ByteBuf;
182 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.write_str("a byte string")
184 }
185 fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<ByteBuf, E> {
186 Ok(ByteBuf(v.to_vec()))
187 }
188 fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<ByteBuf, E> {
189 Ok(ByteBuf(v))
190 }
191 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<ByteBuf, A::Error> {
192 let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(32));
193 while let Some(b) = seq.next_element::<u8>()? {
194 out.push(b);
195 }
196 Ok(ByteBuf(out))
197 }
198 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<ByteBuf, E> {
199 Err(E::custom(format!("expected bytes, found string {v:?}")))
200 }
201 }
202 d.deserialize_bytes(V)
203 }
204 }
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn hex_is_lowercase_and_64_chars() {
214 let d = Digest::of(b"boundgraph");
215 let hex = d.to_hex();
216 assert_eq!(hex.len(), 64);
217 assert_eq!(hex, hex.to_lowercase());
218 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
219 }
220
221 #[test]
222 fn hex_round_trips() {
223 let d = Digest::of(b"boundgraph");
224 assert_eq!(Digest::from_str(&d.to_hex()).unwrap(), d);
225 }
226
227 #[test]
234 fn the_canonical_encoding_of_a_digest_is_fixed() {
235 let digest = Digest::of(b"boundgraph");
236 let encoded = crate::to_canonical_cbor(&digest).unwrap();
237
238 assert_eq!(encoded.len(), 34, "a two-byte header and 32 bytes");
239 assert_eq!(encoded[0], 0x58, "byte string, one-byte length");
240 assert_eq!(encoded[1], 0x20, "32 bytes long");
241 assert_eq!(&encoded[2..], digest.as_bytes());
242
243 let back: Digest = crate::from_canonical_cbor(&encoded).unwrap();
244 assert_eq!(back, digest);
245 }
246
247 #[test]
251 fn a_digest_reads_as_hex_in_a_human_readable_format() {
252 let digest = Digest::of(b"boundgraph");
253 let json = serde_json::to_string(&digest).unwrap();
254
255 assert_eq!(json, format!("\"{}\"", digest.to_hex()));
256 assert!(
257 json.contains(&digest.to_hex()),
258 "so `grep <digest>` finds the line that mentions it"
259 );
260
261 let back: Digest = serde_json::from_str(&json).unwrap();
262 assert_eq!(back, digest);
263 }
264
265 #[test]
266 fn a_malformed_hex_digest_is_refused_rather_than_truncated() {
267 for bad in ["\"\"", "\"nothex\"", "\"ab\"", "\"zz00\""] {
268 assert!(
269 serde_json::from_str::<Digest>(bad).is_err(),
270 "{bad} must not parse as a digest"
271 );
272 }
273 }
274
275 #[test]
276 fn hashing_is_stable_and_input_sensitive() {
277 assert_eq!(Digest::of(b"a"), Digest::of(b"a"));
278 assert_ne!(Digest::of(b"a"), Digest::of(b"b"));
279 }
280
281 #[test]
282 fn rejects_bad_hex() {
283 assert!(matches!(
284 Digest::from_str("abc"),
285 Err(DigestParseError::Length { .. })
286 ));
287 let bad = "z".repeat(64);
288 assert!(matches!(
289 Digest::from_str(&bad),
290 Err(DigestParseError::Char { .. })
291 ));
292 }
293
294 #[test]
295 fn debug_is_abbreviated_but_display_is_not() {
296 let d = Digest::of(b"boundgraph");
297 assert!(format!("{d:?}").len() < d.to_string().len());
298 assert_eq!(d.to_string(), d.to_hex());
299 }
300}