Skip to main content

didbot_data/
cid.rs

1//! Content identifiers, decoded rather than glanced at.
2//!
3//! `didbot_schema::format::cid` already checks CID *strings*, and it
4//! deliberately does not decode: a client can write any CID into a record and
5//! this server dereferences none of them, so the lenient alphabet-and-length
6//! rule atproto specifies is the right check at that boundary. This type is
7//! for the other direction. To encode a link into DAG-CBOR you need the CID's
8//! bytes, and to know that two links are the same link you need to compare
9//! them; neither is answerable from the string.
10//!
11//! What atproto permits is a narrow slice of what a CID can be, and all of it
12//! is enforced here, because a value this crate cannot re-encode byte for byte
13//! is a value it must not accept:
14//!
15//! * version 1. CIDv0 is a valid IPFS identifier, is base58 with no multibase
16//!   prefix at all, and is not permitted in atproto.
17//! * the multibase is base32, lowercase, unpadded — the `b` prefix. Another
18//!   multibase would decode to the same bytes and print back differently.
19//! * the multihash is sha-256, 32 bytes.
20//! * the codec is DAG-CBOR or raw. Everything atproto links to is one or the
21//!   other: a record or a blob.
22
23use std::fmt::Write as _;
24
25use sha2::{Digest, Sha256};
26
27use crate::encoding::{base32_decode, base32_encode, DecodeError};
28
29/// The multicodec for DAG-CBOR, which is what a link to a record carries.
30pub const DAG_CBOR: u64 = 0x71;
31
32/// The multicodec for raw bytes, which is what a link to a blob carries.
33pub const RAW: u64 = 0x55;
34
35/// The multihash code for sha-256.
36const SHA2_256: u64 = 0x12;
37
38/// How long a sha-256 digest is.
39const DIGEST_LENGTH: u64 = 32;
40
41/// The multibase prefix for lowercase unpadded base32.
42const BASE32_PREFIX: char = 'b';
43
44/// Why a string or a byte sequence is not a CID this crate can use.
45#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
46pub enum CidError {
47    /// Empty, so there is not even a multibase prefix.
48    #[error("a cid cannot be empty")]
49    Empty,
50    /// A multibase other than lowercase base32.
51    #[error("multibase {0:?} is not the lowercase base32 atproto requires")]
52    Multibase(char),
53    /// The base32 body did not decode.
54    #[error("cid is not valid base32: {0}")]
55    NotBase32(#[from] DecodeError),
56    /// A varint that ran off the end of the input, or one too long to be a
57    /// `u64`.
58    #[error("truncated or oversized varint in cid")]
59    BadVarint,
60    /// A CID version other than 1, which includes every CIDv0.
61    #[error("cid version {0} is not supported; atproto requires version 1")]
62    Version(u64),
63    /// A codec other than DAG-CBOR or raw.
64    #[error("cid codec {0:#x} is neither dag-cbor nor raw")]
65    Codec(u64),
66    /// A multihash that is not a 32-byte sha-256.
67    #[error("cid multihash {code:#x}/{length} is not a 32-byte sha-256")]
68    Multihash {
69        /// The multihash function code that was found.
70        code: u64,
71        /// The digest length that was found.
72        length: u64,
73    },
74    /// Bytes after the digest, or fewer than the digest claimed.
75    #[error("cid digest is {found} bytes, not the {expected} its multihash declares")]
76    DigestLength {
77        /// How many bytes actually followed the multihash header.
78        found: usize,
79        /// How many the header said there would be.
80        expected: usize,
81    },
82}
83
84/// A content identifier, held as the bytes it hashes and prints from.
85///
86/// The binary form is the canonical one and the string is derived from it, not
87/// the other way around: a CID that came in as a string and goes out as one
88/// goes out identical, because there is only one string for these bytes.
89#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct Cid {
91    /// Version, codec and multihash, concatenated — the multibase payload.
92    bytes: Vec<u8>,
93    /// The codec, parsed out so a caller can ask without re-reading varints.
94    codec: u64,
95}
96
97impl Cid {
98    /// The CID of a DAG-CBOR block: sha-256 over exactly these bytes.
99    ///
100    /// This is the whole reason the encoder has to be canonical. The identity
101    /// of a record is the hash of its encoding, so two encoders that disagree
102    /// about map order or integer width give the same record two identities,
103    /// and every other implementation would be right to say ours is the wrong
104    /// one.
105    pub fn of_dag_cbor(block: &[u8]) -> Self {
106        Self::of_block(DAG_CBOR, block)
107    }
108
109    /// The CID of a raw block, which is what a blob is referenced by.
110    pub fn of_raw(block: &[u8]) -> Self {
111        Self::of_block(RAW, block)
112    }
113
114    /// Builds a CID over `block` under `codec`.
115    fn of_block(codec: u64, block: &[u8]) -> Self {
116        let digest = Sha256::digest(block);
117        let mut bytes = Vec::with_capacity(4 + digest.len());
118        write_varint(&mut bytes, 1);
119        write_varint(&mut bytes, codec);
120        write_varint(&mut bytes, SHA2_256);
121        write_varint(&mut bytes, DIGEST_LENGTH);
122        bytes.extend_from_slice(&digest);
123        Self { bytes, codec }
124    }
125
126    /// Parses the string form: `b` followed by lowercase base32.
127    ///
128    /// ```
129    /// use didbot_data::Cid;
130    /// let cid = Cid::parse("bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a").unwrap();
131    /// assert_eq!(cid.to_string(), "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a");
132    /// assert!(Cid::parse("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR").is_err());
133    /// ```
134    pub fn parse(text: &str) -> Result<Self, CidError> {
135        let mut chars = text.chars();
136        let prefix = chars.next().ok_or(CidError::Empty)?;
137        if prefix != BASE32_PREFIX {
138            return Err(CidError::Multibase(prefix));
139        }
140        Self::from_bytes(&base32_decode(chars.as_str())?)
141    }
142
143    /// Parses the binary form, which is what a DAG-CBOR link tag carries.
144    pub fn from_bytes(bytes: &[u8]) -> Result<Self, CidError> {
145        let mut rest = bytes;
146        let version = read_varint(&mut rest)?;
147        if version != 1 {
148            return Err(CidError::Version(version));
149        }
150        let codec = read_varint(&mut rest)?;
151        if codec != DAG_CBOR && codec != RAW {
152            return Err(CidError::Codec(codec));
153        }
154        let code = read_varint(&mut rest)?;
155        let length = read_varint(&mut rest)?;
156        if code != SHA2_256 || length != DIGEST_LENGTH {
157            return Err(CidError::Multihash { code, length });
158        }
159        if rest.len() as u64 != length {
160            return Err(CidError::DigestLength {
161                found: rest.len(),
162                expected: length as usize,
163            });
164        }
165        Ok(Self {
166            bytes: bytes.to_vec(),
167            codec,
168        })
169    }
170
171    /// Reads a CID off the front of a byte stream, leaving the rest.
172    ///
173    /// A CID is self-delimiting — the multihash declares its own digest
174    /// length — which is what lets a CAR file put one immediately before a
175    /// block with no length prefix between them. [`Cid::from_bytes`] is the
176    /// whole-slice form and refuses a trailing byte; this one is the streaming
177    /// form and hands the trailing bytes back.
178    ///
179    /// ```
180    /// use didbot_data::Cid;
181    ///
182    /// let cid = Cid::of_raw(b"a block");
183    /// let mut framed = cid.as_bytes().to_vec();
184    /// framed.extend_from_slice(b"a block");
185    /// let mut rest = framed.as_slice();
186    /// assert_eq!(Cid::read(&mut rest).unwrap(), cid);
187    /// assert_eq!(rest, b"a block");
188    /// ```
189    pub fn read(input: &mut &[u8]) -> Result<Self, CidError> {
190        let whole = *input;
191        let mut rest: &[u8] = whole;
192        let version = read_varint(&mut rest)?;
193        if version != 1 {
194            return Err(CidError::Version(version));
195        }
196        let codec = read_varint(&mut rest)?;
197        if codec != DAG_CBOR && codec != RAW {
198            return Err(CidError::Codec(codec));
199        }
200        let code = read_varint(&mut rest)?;
201        let length = read_varint(&mut rest)?;
202        if code != SHA2_256 || length != DIGEST_LENGTH {
203            return Err(CidError::Multihash { code, length });
204        }
205        let digest = usize::try_from(length).unwrap_or(usize::MAX);
206        if rest.len() < digest {
207            return Err(CidError::DigestLength {
208                found: rest.len(),
209                expected: digest,
210            });
211        }
212        let taken = whole.len() - rest.len() + digest;
213        *input = &whole[taken..];
214        Ok(Self {
215            bytes: whole[..taken].to_vec(),
216            codec,
217        })
218    }
219
220    /// The CID these bytes have under `codec`, which is how a reader checks
221    /// that a block is the one it was handed under a name.
222    ///
223    /// The codec comes from the name being checked rather than from the
224    /// caller's expectation: a block is either the bytes its CID says it is
225    /// or it is not, and which codec that CID declares is part of the claim.
226    #[must_use]
227    pub fn of_block_under(codec: u64, block: &[u8]) -> Self {
228        Self::of_block(codec, block)
229    }
230
231    /// The binary form: version, codec and multihash, with no multibase
232    /// prefix.
233    pub fn as_bytes(&self) -> &[u8] {
234        &self.bytes
235    }
236
237    /// Which of the two permitted codecs this CID names.
238    pub fn codec(&self) -> u64 {
239        self.codec
240    }
241}
242
243/// A raw-block CID computed over bytes that arrive a chunk at a time.
244///
245/// [`Cid::of_raw`] needs the whole block in hand, which is the one thing a
246/// blob upload must never have: a personal data server that buffers a body
247/// before it hashes it is holding the whole file in memory to learn its name
248/// (a known defect in jacquard's `uploadBlob`). This is the same
249/// hash, fed incrementally, so the bytes can go to their destination as they
250/// arrive and the name falls out at the end.
251///
252/// It counts as it hashes, because the size is the other half of a blob
253/// reference and a caller that tracked it separately could disagree with the
254/// bytes that were actually hashed.
255///
256/// ```
257/// use didbot_data::{Cid, RawHasher};
258/// let mut hasher = RawHasher::new();
259/// hasher.update(b"quernstone");
260/// hasher.update(b"-farthingale");
261/// let (cid, size) = hasher.finish();
262/// assert_eq!(cid, Cid::of_raw(b"quernstone-farthingale"));
263/// assert_eq!(size, 22);
264/// ```
265#[derive(Debug, Clone, Default)]
266pub struct RawHasher {
267    digest: Sha256,
268    size: u64,
269}
270
271impl RawHasher {
272    /// A hasher over nothing yet.
273    pub fn new() -> Self {
274        Self::default()
275    }
276
277    /// Folds in the next chunk.
278    pub fn update(&mut self, chunk: &[u8]) {
279        self.digest.update(chunk);
280        self.size = self.size.saturating_add(chunk.len() as u64);
281    }
282
283    /// How many bytes have been folded in so far.
284    ///
285    /// Readable mid-stream so a caller enforcing a size limit can stop at the
286    /// byte that breaks it rather than after the body has been written out.
287    pub fn size(&self) -> u64 {
288        self.size
289    }
290
291    /// The CID of everything folded in, and how much that was.
292    pub fn finish(self) -> (Cid, u64) {
293        let digest = self.digest.finalize();
294        let mut bytes = Vec::with_capacity(4 + digest.len());
295        write_varint(&mut bytes, 1);
296        write_varint(&mut bytes, RAW);
297        write_varint(&mut bytes, SHA2_256);
298        write_varint(&mut bytes, DIGEST_LENGTH);
299        bytes.extend_from_slice(&digest);
300        (Cid { bytes, codec: RAW }, self.size)
301    }
302}
303
304impl std::fmt::Display for Cid {
305    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306        f.write_char(BASE32_PREFIX)?;
307        f.write_str(&base32_encode(&self.bytes))
308    }
309}
310
311/// Appends an unsigned LEB128 varint, the multiformats encoding.
312fn write_varint(out: &mut Vec<u8>, mut value: u64) {
313    while value >= 0x80 {
314        out.push((value as u8) | 0x80);
315        value >>= 7;
316    }
317    out.push(value as u8);
318}
319
320/// Reads an unsigned LEB128 varint, advancing `input` past it.
321///
322/// Nine continuation bytes is already more than a `u64` holds, so the length
323/// cap is what stops a hostile string from being read forever.
324///
325/// The encoding must also be minimal, which multiformats requires of an
326/// unsigned varint and which is not a tidiness here. A [`Cid`] keeps the bytes
327/// it was read from and prints from them, so a version written `81 00` rather
328/// than `01` is a second byte sequence, a second string and a second `Cid`
329/// that compares unequal — for one block, with one digest. The rule is that
330/// the last group of a multi-byte varint has to carry something, so a
331/// terminating zero group after a continuation byte is refused.
332fn read_varint(input: &mut &[u8]) -> Result<u64, CidError> {
333    let mut value: u64 = 0;
334    for index in 0..10 {
335        let byte = *input.first().ok_or(CidError::BadVarint)?;
336        *input = &input[1..];
337        let payload = u64::from(byte & 0x7f);
338        value |= payload
339            .checked_shl(index * 7)
340            .filter(|shifted| shifted >> (index * 7) == payload)
341            .ok_or(CidError::BadVarint)?;
342        if byte & 0x80 == 0 {
343            if index > 0 && payload == 0 {
344                return Err(CidError::BadVarint);
345            }
346            return Ok(value);
347        }
348    }
349    Err(CidError::BadVarint)
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    /// The `$link` in the upstream data-model fixtures.
357    const FIXTURE: &str = "bafyreidfayvfuwqa7qlnopdjiqrxzs6blmoeu4rujcjtnci5beludirz2a";
358
359    #[test]
360    fn a_parsed_cid_prints_back_identically() {
361        assert_eq!(Cid::parse(FIXTURE).unwrap().to_string(), FIXTURE);
362    }
363
364    #[test]
365    fn the_binary_form_opens_with_version_and_codec() {
366        let cid = Cid::parse(FIXTURE).unwrap();
367        assert_eq!(&cid.as_bytes()[..4], &[0x01, 0x71, 0x12, 0x20]);
368        assert_eq!(cid.codec(), DAG_CBOR);
369    }
370
371    #[test]
372    fn a_raw_blob_reference_parses_and_keeps_its_codec() {
373        let cid =
374            Cid::parse("bafkreiccldh766hwcnuxnf2wh6jgzepf2nlu2lvcllt63eww5p6chi4ity").unwrap();
375        assert_eq!(cid.codec(), RAW);
376    }
377
378    #[test]
379    fn hashing_a_block_agrees_with_the_string_form() {
380        // The empty DAG-CBOR map, `a0`, has a CID every implementation agrees
381        // on; it is the identity of an empty record.
382        assert_eq!(
383            Cid::of_dag_cbor(&[0xa0]).to_string(),
384            "bafyreigbtj4x7ip5legnfznufuopl4sg4knzc2cof6duas4b3q2fy6swua"
385        );
386    }
387
388    #[test]
389    fn a_cid_v0_is_refused_before_anything_is_decoded() {
390        assert_eq!(
391            Cid::parse("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"),
392            Err(CidError::Multibase('Q'))
393        );
394    }
395
396    #[test]
397    fn the_bogus_cid_from_the_vectors_is_refused() {
398        assert!(Cid::parse(".").is_err());
399    }
400
401    #[test]
402    fn a_truncated_digest_is_refused() {
403        let mut bytes = Cid::parse(FIXTURE).unwrap().as_bytes().to_vec();
404        bytes.pop();
405        assert_eq!(
406            Cid::from_bytes(&bytes),
407            Err(CidError::DigestLength {
408                found: 31,
409                expected: 32
410            })
411        );
412    }
413
414    #[test]
415    fn an_unsupported_codec_is_refused() {
416        // 0x70 is dag-pb, which is what a CIDv0 would have been.
417        let cid = Cid::of_dag_cbor(&[0xa0]);
418        let mut bytes = cid.as_bytes().to_vec();
419        bytes[1] = 0x70;
420        assert_eq!(Cid::from_bytes(&bytes), Err(CidError::Codec(0x70)));
421    }
422
423    #[test]
424    fn varints_round_trip_across_the_whole_range() {
425        for value in [0u64, 1, 0x7f, 0x80, 0x3fff, 0x4000, u64::MAX] {
426            let mut bytes = Vec::new();
427            write_varint(&mut bytes, value);
428            let mut input = bytes.as_slice();
429            assert_eq!(read_varint(&mut input), Ok(value));
430            assert!(input.is_empty());
431        }
432    }
433
434    /// A varint written wider than it needs gives one block two names.
435    ///
436    /// Asserted as the round trip rather than as an error kind: a `Cid` keeps
437    /// the bytes it read and prints from them, so anything `from_bytes`
438    /// accepts must serialise back to what it was handed. The named error
439    /// comes after, so a regression fails on the property and not on the
440    /// spelling of the refusal.
441    #[test]
442    fn a_padded_varint_would_be_a_second_name_for_one_block() {
443        let canonical = Cid::of_dag_cbor(&[0xa0]);
444        // Version 1 written `81 00`; codec, multihash and digest unchanged.
445        let mut padded = vec![0x81, 0x00];
446        padded.extend_from_slice(&canonical.as_bytes()[1..]);
447        if let Ok(read) = Cid::from_bytes(&padded) {
448            assert_eq!(
449                read.to_string(),
450                canonical.to_string(),
451                "{read} is a second name for the block {canonical} names"
452            );
453        }
454        assert_eq!(Cid::from_bytes(&padded), Err(CidError::BadVarint));
455
456        // The same padding in the codec, which a reader checks by value and
457        // is least likely to check the width of.
458        let mut padded_codec = vec![0x01, 0xf1, 0x00];
459        padded_codec.extend_from_slice(&canonical.as_bytes()[2..]);
460        assert_eq!(Cid::from_bytes(&padded_codec), Err(CidError::BadVarint));
461
462        // The canonical spelling still reads, so the rule refuses a form
463        // rather than the field.
464        assert_eq!(Cid::from_bytes(canonical.as_bytes()), Ok(canonical));
465    }
466
467    /// The same refusal reaches a link inside a block, which is where a
468    /// padded CID would actually arrive: a peer's record or tree node.
469    #[test]
470    fn a_link_carrying_a_padded_cid_is_not_a_link() {
471        let canonical = Cid::of_dag_cbor(&[0xa0]);
472        let mut padded = vec![0x81, 0x00];
473        padded.extend_from_slice(&canonical.as_bytes()[1..]);
474        // Tag 42 over a byte string of the identity prefix and those bytes.
475        let mut block = vec![0xd8, 0x2a, 0x58, (padded.len() + 1) as u8, 0x00];
476        block.extend_from_slice(&padded);
477        assert!(
478            crate::dag_cbor::decode(&block).is_err(),
479            "a block linking through a padded cid decoded"
480        );
481    }
482
483    #[test]
484    fn a_varint_with_no_end_is_refused_rather_than_read_forever() {
485        let mut input: &[u8] = &[0x80; 12];
486        assert_eq!(read_varint(&mut input), Err(CidError::BadVarint));
487    }
488}