iris_format/digest.rs
1//! Content digests.
2//!
3//! Every section carries one, the footer commits to all of them, and the trailer commits to the
4//! footer. That chain is what makes it possible to say this is the same dataset without comparing
5//! whole files, and it is what lets a host swap in a native decoder it already trusts for a
6//! sandboxed one it does not: the substitution is keyed on the digest of the decoder module, so
7//! there is nothing to guess about which decoder a dataset actually asked for.
8
9use core::fmt;
10
11use crate::layout::DIGEST_SIZE;
12
13/// A BLAKE3 hash of some part of a container.
14///
15/// BLAKE3 rather than SHA-256 because this gets computed over whole datasets on the write path and
16/// over whole sections on any read that verifies, and the difference is large enough to change
17/// whether verification is on by default.
18#[derive(Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
19pub struct Digest(pub [u8; DIGEST_SIZE]);
20
21impl Digest {
22 /// The digest of some bytes.
23 #[must_use]
24 pub fn of(bytes: &[u8]) -> Self {
25 Self(*blake3::hash(bytes).as_bytes())
26 }
27
28 /// The raw bytes.
29 #[must_use]
30 pub const fn as_bytes(&self) -> &[u8; DIGEST_SIZE] {
31 &self.0
32 }
33
34 /// The first sixteen hex characters, which is what belongs in a log line.
35 ///
36 /// Never use this to decide whether two things are the same. It is here because a full digest
37 /// in an error message is noise a reader skips, and a short one is something they read.
38 #[must_use]
39 pub fn short(&self) -> String {
40 let full = self.to_string();
41 full[..16].to_owned()
42 }
43}
44
45impl fmt::Display for Digest {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 for byte in self.0 {
48 write!(f, "{byte:02x}")?;
49 }
50 Ok(())
51 }
52}
53
54impl fmt::Debug for Digest {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "Digest({})", self.short())
57 }
58}