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}