Skip to main content

dasl/
cid.rs

1//! CIDs (Content IDs) are identifiers used for addressing resources by their contents, essentially a hash with limited metadata.
2//!
3//! [Spec](https://dasl.ing/cid.html)
4
5use std::{fmt::Display, str::FromStr};
6
7use sha2::Digest;
8use thiserror::Error;
9
10use crate::base32::BASE32_LOWER;
11
12mod serde;
13
14pub(crate) use self::serde::{BytesToCidVisitor, CID_SERDE_PRIVATE_IDENTIFIER};
15
16const CID_VERSION: u8 = 1;
17const PREFIX_LEN: usize = 4;
18/// Length of a known hash
19const HASH_LEN: u8 = 32;
20const DATA_LEN: usize = PREFIX_LEN + HASH_LEN as usize;
21const HASH_CODE_SHA2_256: u8 = 0x12;
22const HASH_CODE_BLAKE3: u8 = 0x1e;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
25pub struct Cid {
26    // - 1 byte CID version
27    // - 1 byte Codec
28    // - 1 byte hash type
29    // - 1 byte Length
30    // - 32 bytes hash
31    data: [u8; DATA_LEN],
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
35#[non_exhaustive]
36#[repr(u8)]
37pub enum Codec {
38    Raw = 0x55,
39    Drisl = 0x71,
40}
41
42#[derive(Debug, Error)]
43pub enum ParseCodecError {
44    #[error("Unknown codec: 0x{_0:X}")]
45    UnknownCodec(u8),
46}
47
48impl TryFrom<u8> for Codec {
49    type Error = ParseCodecError;
50
51    fn try_from(value: u8) -> Result<Self, Self::Error> {
52        match value {
53            0x55 => Ok(Self::Raw),
54            0x71 => Ok(Self::Drisl),
55            _ => Err(ParseCodecError::UnknownCodec(value)),
56        }
57    }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
61#[non_exhaustive]
62#[repr(u8)]
63pub enum Multihash {
64    Sha2256 = 0x12,
65    Blake3 = 0x1e,
66}
67
68impl TryFrom<u8> for Multihash {
69    type Error = MultihashParseError;
70
71    fn try_from(value: u8) -> Result<Self, Self::Error> {
72        match value {
73            HASH_CODE_SHA2_256 => Ok(Self::Sha2256),
74            HASH_CODE_BLAKE3 => Ok(Self::Blake3),
75            _ => Err(MultihashParseError::UnknownHash(value)),
76        }
77    }
78}
79
80#[derive(Debug, Error)]
81pub enum CidParseError {
82    #[error("Invalid encoding")]
83    InvalidEncoding,
84    #[error("Too short")]
85    TooShort,
86    #[error("Invalid CID version: {_0}")]
87    InvalidCidVersion(u8),
88    #[error("Invalid codec: {_0}")]
89    InvalidCodec(ParseCodecError),
90    #[error("Invalid multihash: {_0}")]
91    InvalidMultihash(MultihashParseError),
92}
93
94impl From<ParseCodecError> for CidParseError {
95    fn from(err: ParseCodecError) -> Self {
96        Self::InvalidCodec(err)
97    }
98}
99
100impl From<MultihashParseError> for CidParseError {
101    fn from(err: MultihashParseError) -> Self {
102        Self::InvalidMultihash(err)
103    }
104}
105
106impl FromStr for Cid {
107    type Err = CidParseError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        if !s.starts_with('b') {
111            return Err(CidParseError::InvalidEncoding);
112        }
113
114        // skip base encoding prefix
115        let without_prefix = &s.as_bytes()[1..];
116        let bytes = BASE32_LOWER
117            .decode(without_prefix)
118            .map_err(|_e| CidParseError::InvalidEncoding)?;
119
120        Cid::from_bytes_raw(&bytes)
121    }
122}
123
124impl Cid {
125    /// Returns the `Multihash` of this `CID`.
126    pub fn hash(&self) -> &[u8] {
127        match self.data[3] {
128            0 => &[][..], // empty hash
129            HASH_LEN => &self.data[PREFIX_LEN..],
130            _ => unreachable!("invalid construction"),
131        }
132    }
133
134    pub fn multihash_type(&self) -> Multihash {
135        Multihash::try_from(self.data[2]).expect("invalid construction")
136    }
137
138    /// Returns the `Codec` of this `CID`.
139    pub fn codec(&self) -> Codec {
140        Codec::try_from(self.data[1]).expect("invalid construction")
141    }
142
143    /// Tries to decode a `CID` from binary encoding.
144    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CidParseError> {
145        if bytes.is_empty() {
146            return Err(CidParseError::TooShort);
147        }
148        if bytes[0] != 0x0 {
149            return Err(CidParseError::InvalidEncoding);
150        }
151        Self::from_bytes_raw(&bytes[1..])
152    }
153
154    /// Tries to decode a `CID` from its raw binary components.
155    pub fn from_bytes_raw(bytes: &[u8]) -> Result<Self, CidParseError> {
156        const MIN_LEN: usize = 3;
157
158        if bytes.len() < MIN_LEN {
159            return Err(CidParseError::TooShort);
160        }
161        if bytes.len() > DATA_LEN {
162            return Err(MultihashParseError::InvalidLength(bytes.len()).into());
163        }
164
165        if bytes[0] != CID_VERSION {
166            return Err(CidParseError::InvalidCidVersion(bytes[0]));
167        }
168        let mut data = [0u8; DATA_LEN];
169        let _codec = Codec::try_from(bytes[1])?;
170        let _multihash = Multihash::try_from(bytes[2])?;
171
172        let len = bytes[3];
173        match len {
174            0 => {
175                if bytes.len() > 4 {
176                    return Err(MultihashParseError::InvalidLength(bytes.len()).into());
177                }
178                data[..PREFIX_LEN].copy_from_slice(&bytes[..PREFIX_LEN]);
179            }
180            HASH_LEN => {
181                if bytes.len() != DATA_LEN {
182                    return Err(MultihashParseError::InvalidLength(bytes.len()).into());
183                }
184                data.copy_from_slice(bytes);
185            }
186            _ => return Err(MultihashParseError::InvalidLengthPrefix.into()),
187        }
188
189        Ok(Cid { data })
190    }
191
192    /// Encode the `CID` in its raw binary format.
193    pub fn as_bytes(&self) -> &[u8] {
194        match self.data[3] {
195            0 => &self.data[..PREFIX_LEN],
196            HASH_LEN => &self.data,
197            _ => unreachable!("invalid construction"),
198        }
199    }
200
201    pub fn digest_sha2(codec: Codec, data: impl AsRef<[u8]>) -> Self {
202        let hash = sha2::Sha256::digest(data);
203        let mut data = [0u8; DATA_LEN];
204        data[0] = CID_VERSION;
205        data[1] = codec as u8;
206        data[2] = HASH_CODE_SHA2_256;
207        data[3] = HASH_LEN;
208        data[PREFIX_LEN..].copy_from_slice(&hash);
209        Self { data }
210    }
211
212    pub fn digest_blake3(codec: Codec, data: impl AsRef<[u8]>) -> Self {
213        let hash = blake3::hash(data.as_ref());
214        let mut data = [0u8; DATA_LEN];
215        data[0] = CID_VERSION;
216        data[1] = codec as u8;
217        data[2] = HASH_CODE_BLAKE3;
218        data[3] = HASH_LEN;
219        data[PREFIX_LEN..].copy_from_slice(hash.as_bytes());
220        Self { data }
221    }
222
223    pub fn empty_sha2_256(codec: Codec) -> Self {
224        let mut data = [0u8; DATA_LEN];
225        data[0] = CID_VERSION;
226        data[1] = codec as u8;
227        data[2] = HASH_CODE_SHA2_256;
228        data[3] = 0;
229        Self { data }
230    }
231
232    pub fn empty_blake3(codec: Codec) -> Self {
233        let mut data = [0u8; DATA_LEN];
234        data[0] = CID_VERSION;
235        data[1] = codec as u8;
236        data[2] = HASH_CODE_BLAKE3;
237        data[3] = 0;
238        Self { data }
239    }
240}
241
242impl Display for Cid {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        write!(f, "b")?;
245        let out = self.as_bytes();
246        BASE32_LOWER.encode_write(out, f)?;
247
248        Ok(())
249    }
250}
251
252#[derive(Debug, Error)]
253#[non_exhaustive]
254pub enum MultihashParseError {
255    #[error("Invalid length: {_0}")]
256    InvalidLength(usize),
257    #[error("Unknown hash: {_0:x}")]
258    UnknownHash(u8),
259    #[error("Invalid length prefix")]
260    InvalidLengthPrefix,
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_base_sha2_256() {
269        // Sha2 256: "foo"
270        let cid_str = "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy";
271        let parsed: Cid = cid_str.parse().unwrap();
272        assert_eq!(parsed.codec(), Codec::Raw);
273        assert!(matches!(parsed.multihash_type(), Multihash::Sha2256));
274
275        let cid_str_back = parsed.to_string();
276        assert_eq!(cid_str_back, cid_str);
277    }
278
279    #[test]
280    fn test_base_blake3() {
281        // Blake3: "foo"
282        let cid_str = "bafkr4iae4c5tt4yldi76xcpvg3etxykqkvec352im5fqbutolj2xo5yc5e";
283        let parsed: Cid = cid_str.parse().unwrap();
284        assert_eq!(parsed.codec(), Codec::Raw);
285        assert!(matches!(parsed.multihash_type(), Multihash::Blake3));
286
287        let cid_str_back = parsed.to_string();
288        assert_eq!(cid_str_back, cid_str);
289    }
290
291    #[test]
292    fn test_digest_sha2_256() {
293        let cid_str = "bafkreibme22gw2h7y2h7tg2fhqotaqjucnbc24deqo72b6mkl2egezxhvy";
294        assert_eq!(Cid::digest_sha2(Codec::Raw, b"foo").to_string(), cid_str);
295    }
296
297    #[test]
298    fn test_digest_blake3() {
299        let cid_str = "bafkr4iae4c5tt4yldi76xcpvg3etxykqkvec352im5fqbutolj2xo5yc5e";
300        assert_eq!(Cid::digest_blake3(Codec::Raw, b"foo").to_string(), cid_str);
301    }
302}