Skip to main content

mnem_core/id/
multihash.rs

1//! Self-describing hashes per the [multihash] spec.
2//!
3//! Every content hash in mnem is a `Multihash` - the algorithm code is part
4//! of the hash value, so the default hash algorithm can change in a future
5//! mnem version without invalidating existing content. //!
6//! [multihash]: https://github.com/multiformats/multihash
7
8use core::fmt;
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::IdError;
13
14// Re-export the underlying crate's error so callers don't need a direct
15// `multihash` dependency.
16pub use multihash::Error as MultihashError;
17
18/// Multihash algorithm code for SHA-256 (32-byte digest). Default for mnem/0.1.
19pub const HASH_SHA2_256: u64 = 0x12;
20
21/// Multihash algorithm code for BLAKE3-256 (32-byte digest).
22///
23/// Available when `multihash-codetable/blake3` is enabled in the dep graph,
24/// which mnem-core does by default. BLAKE3 is a candidate default for a
25/// future mnem format version
26pub const HASH_BLAKE3_256: u64 = 0x1e;
27
28/// A content hash tagged with its algorithm code.
29///
30/// Internally wraps `multihash::Multihash<64>` - 64 bytes of buffer, enough
31/// for any algorithm on mnem's allow-list (SHA-256, BLAKE3-256 today;
32/// SHA-512, BLAKE3-512, etc. if later expanded).
33///
34/// # Equality
35///
36/// Two `Multihash` values are equal only if they have the same algorithm
37/// code AND the same digest bytes. A SHA-256 and a BLAKE3-256 of the same
38/// input never compare equal even if their digest bytes collide.
39#[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct Multihash(multihash::Multihash<64>);
42
43impl Multihash {
44    /// Compute a SHA-256 multihash of `bytes`.
45    ///
46    /// This is the default content-hash algorithm for mnem/0.1.
47    #[must_use]
48    pub fn sha2_256(bytes: &[u8]) -> Self {
49        use multihash_codetable::{Code, MultihashDigest};
50        Self(Code::Sha2_256.digest(bytes))
51    }
52
53    /// Compute a BLAKE3-256 multihash of `bytes`.
54    ///
55    /// Produces a 32-byte digest with algorithm code `0x1e`. Faster than
56    /// SHA-256 in most conditions; accepted by mnem but not the default
57    /// for mnem/0.1 .
58    #[must_use]
59    pub fn blake3_256(bytes: &[u8]) -> Self {
60        use multihash_codetable::{Code, MultihashDigest};
61        Self(Code::Blake3_256.digest(bytes))
62    }
63
64    /// Wrap a raw algorithm code + digest bytes into a `Multihash`.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`IdError::Multihash`] if `digest.len() > 64` or if the
69    /// digest length is otherwise invalid for the declared code.
70    pub fn wrap(code: u64, digest: &[u8]) -> Result<Self, IdError> {
71        multihash::Multihash::<64>::wrap(code, digest)
72            .map(Self)
73            .map_err(|source| IdError::Multihash { source })
74    }
75
76    /// The algorithm code byte. See [`HASH_SHA2_256`] and friends.
77    #[must_use]
78    pub const fn code(&self) -> u64 {
79        self.0.code()
80    }
81
82    /// Digest length in bytes.
83    #[must_use]
84    pub const fn size(&self) -> u8 {
85        self.0.size()
86    }
87
88    /// Borrow the digest bytes.
89    #[must_use]
90    pub fn digest(&self) -> &[u8] {
91        self.0.digest()
92    }
93
94    /// Serialize to the multihash wire format: `<code: varint> <size: varint> <digest>`.
95    #[must_use]
96    pub fn to_bytes(&self) -> Vec<u8> {
97        self.0.to_bytes()
98    }
99
100    /// Parse a multihash from its wire format.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`IdError::Multihash`] if the bytes are malformed.
105    pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdError> {
106        multihash::Multihash::<64>::from_bytes(bytes)
107            .map(Self)
108            .map_err(|source| IdError::Multihash { source })
109    }
110
111    /// Consume and return the underlying `multihash::Multihash<64>`.
112    /// Crate-internal interop with [`crate::id::cid`].
113    #[must_use]
114    pub(crate) const fn into_inner(self) -> multihash::Multihash<64> {
115        self.0
116    }
117}
118
119impl fmt::Debug for Multihash {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(
122            f,
123            "multihash(code=0x{:x}, size={}, digest=",
124            self.code(),
125            self.size()
126        )?;
127        for b in self.digest().iter().take(4) {
128            write!(f, "{b:02x}")?;
129        }
130        f.write_str("…)")
131    }
132}
133
134impl From<multihash::Multihash<64>> for Multihash {
135    fn from(m: multihash::Multihash<64>) -> Self {
136        Self(m)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn sha2_256_deterministic() {
146        let a = Multihash::sha2_256(b"hello");
147        let b = Multihash::sha2_256(b"hello");
148        assert_eq!(a, b);
149        assert_eq!(a.code(), HASH_SHA2_256);
150        assert_eq!(a.size(), 32);
151        assert_eq!(a.digest().len(), 32);
152    }
153
154    #[test]
155    fn different_inputs_different_hashes() {
156        let a = Multihash::sha2_256(b"hello");
157        let b = Multihash::sha2_256(b"world");
158        assert_ne!(a, b);
159    }
160
161    #[test]
162    fn different_algos_distinct_even_on_empty() {
163        let sha = Multihash::sha2_256(&[]);
164        let blake = Multihash::blake3_256(&[]);
165        assert_ne!(
166            sha, blake,
167            "sha2-256 and blake3 of empty must not compare equal"
168        );
169        assert_ne!(sha.code(), blake.code());
170    }
171
172    #[test]
173    fn wire_round_trip() {
174        let original = Multihash::sha2_256(b"round-trip me");
175        let bytes = original.to_bytes();
176        let decoded = Multihash::from_bytes(&bytes).expect("decode");
177        assert_eq!(original, decoded);
178        // Header: 0x12 (sha2-256) then 0x20 (32) then 32 bytes digest
179        assert_eq!(bytes[0], 0x12);
180        assert_eq!(bytes[1], 0x20);
181        assert_eq!(bytes.len(), 34);
182    }
183
184    #[test]
185    fn wrap_roundtrip() {
186        let digest = [0xabu8; 32];
187        let m = Multihash::wrap(HASH_SHA2_256, &digest).expect("wrap");
188        assert_eq!(m.code(), HASH_SHA2_256);
189        assert_eq!(m.digest(), &digest);
190    }
191}