Skip to main content

didbot_repo/
car.rs

1//! CARv1: the file a repository is handed over as.
2//!
3//! A CAR file is a header and then a pile of blocks, each one prefixed by its
4//! own length. It carries no index, no ordering rule and no compression — it
5//! is a way of putting content-addressed blocks in a stream and nothing more,
6//! and what makes it usable is that every block names itself, so a reader can
7//! start from the root the header declares and follow links.
8//!
9//! # The framing, in full
10//!
11//! ```text
12//! varint(len(header))  header                 header is DAG-CBOR
13//! varint(len(cid) + len(block))  cid  block   repeated, in any order
14//! ```
15//!
16//! The varint is unsigned LEB128, the same one multiformats uses everywhere
17//! else. The header is `{"roots": [<link>], "version": 1}` — `version` is the
18//! CAR format's own version and has nothing to do with the repository version
19//! in a commit, which is 3. Two different numbers called version in one file
20//! is a genuine trap and the reason this is spelled out here.
21//!
22//! # Reading one back
23//!
24//! [`read`] is the other direction, and it arrived with the Merkle search
25//! tree conformance suite: the trees those vectors describe are handed over
26//! as CAR files, so checking a diff against them means reading one. It hands
27//! back the roots the header declares and the blocks as they were framed,
28//! and it checks the one thing a CAR file can be checked for from the inside
29//! — that every block hashes to the CID it is filed under. Nothing above it
30//! has to trust the sender's labelling.
31//!
32//! What it does not do is walk. A CAR is a bag of blocks in no required
33//! order, and which of them matter depends on what the root is — a commit, a
34//! tree node, a record — so following links is the caller's job.
35
36use std::collections::BTreeMap;
37
38use didbot_data::dag_cbor::CborError;
39use didbot_data::{dag_cbor, Cid, CidError, Value};
40
41/// The CAR container format version this writes. Not a repository version.
42const CAR_VERSION: i64 = 1;
43
44/// Writes a CARv1 stream with one root.
45///
46/// See [`write_roots`] for the general form; this is the shape a repository
47/// export takes, where the one root is the signed commit.
48///
49/// Blocks are written in the order the iterator hands them over. Nothing in
50/// the format cares — a reader indexes by CID as it goes — but a caller that
51/// wants a byte-identical export from identical state should hand over a
52/// stable order, which is what an ordered map gives for free.
53///
54/// A block whose CID does not name it is not detected here. The writer is
55/// handed pairs and trusts them; the check belongs where the pair is made,
56/// and every producer in this crate makes it by hashing.
57///
58/// ```
59/// use didbot_data::Cid;
60/// use didbot_repo::car;
61///
62/// let block = b"not a real block".to_vec();
63/// let cid = Cid::of_raw(&block);
64/// let car = car::write(&cid, [(cid.clone(), block)]);
65/// assert!(car.len() > 60);
66/// ```
67#[must_use]
68pub fn write(root: &Cid, blocks: impl IntoIterator<Item = (Cid, Vec<u8>)>) -> Vec<u8> {
69    write_roots(std::slice::from_ref(root), blocks)
70}
71
72/// Writes a CARv1 stream with however many roots it has, including none.
73///
74/// A file with no roots is a bag of blocks and says so: it declares nothing to
75/// start walking from, because the caller asked for particular blocks by name
76/// rather than for a structure. That is what `com.atproto.sync.getBlocks`
77/// returns and it is what the reference implementation writes for it, so it is
78/// what this writes rather than inventing a root the caller did not ask for.
79#[must_use]
80pub fn write_roots(roots: &[Cid], blocks: impl IntoIterator<Item = (Cid, Vec<u8>)>) -> Vec<u8> {
81    let mut out = Vec::new();
82    write_section(&mut out, &header(roots));
83    for (cid, block) in blocks {
84        let mut section = Vec::with_capacity(cid.as_bytes().len() + block.len());
85        section.extend_from_slice(cid.as_bytes());
86        section.extend_from_slice(&block);
87        write_section(&mut out, &section);
88    }
89    out
90}
91
92/// How many bytes a block of `len` bytes filed under `cid` takes in a CAR
93/// file: its length prefix, its CID and the block itself.
94///
95/// What a caller sizing a file before writing it counts, section by section.
96#[must_use]
97pub fn section_len(cid: &Cid, len: usize) -> usize {
98    let payload = cid.as_bytes().len() + len;
99    let mut prefix = Vec::with_capacity(10);
100    write_varint(&mut prefix, payload as u64);
101    prefix.len() + payload
102}
103
104/// Why a byte sequence is not a CAR file this crate can read.
105#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
106pub enum CarError {
107    /// A varint that ran off the end, or one too long to be a `u64`.
108    #[error("truncated or oversized varint at byte {0}")]
109    BadVarint(usize),
110    /// A section that claims more bytes than the file has left.
111    #[error("the section at byte {at} claims {claimed} bytes and {left} remain")]
112    Truncated {
113        /// Where the section's length prefix was.
114        at: usize,
115        /// How long it said it was.
116        claimed: u64,
117        /// How much file was left after the prefix.
118        left: usize,
119    },
120    /// The file is empty, so it has not even a header.
121    #[error("a car file cannot be empty")]
122    Empty,
123    /// The header block did not decode as DAG-CBOR.
124    #[error("the car header is not dag-cbor: {0}")]
125    Header(#[from] CborError),
126    /// The header decoded and was not the map a CAR header is.
127    #[error("the car header is not a v1 header: {0}")]
128    NotAHeader(String),
129    /// A block's CID did not parse.
130    #[error("the block at byte {at} is filed under something that is not a cid: {err}")]
131    BlockCid {
132        /// Where the section began.
133        at: usize,
134        /// Why the CID was refused.
135        err: CidError,
136    },
137    /// A block that does not hash to the CID it is filed under.
138    #[error("the block filed under {claimed} at byte {at} hashes to {actual}")]
139    Mislabelled {
140        /// Where the section began.
141        at: usize,
142        /// The CID the file claimed.
143        claimed: Box<Cid>,
144        /// The CID the bytes actually have.
145        actual: Box<Cid>,
146    },
147    /// The same CID filed twice with different bytes, which cannot happen to
148    /// a hash and means one of the two is not what it says.
149    #[error("{0} appears twice in the file")]
150    Duplicate(Box<Cid>),
151}
152
153/// A CAR file, read apart.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct Car {
156    /// The roots the header declares, in the order it declares them.
157    ///
158    /// A file may declare none, and one that declares several is legal. What
159    /// a root *means* is the caller's to know: a repository export roots at a
160    /// commit, and the Merkle search tree vectors root at a tree node.
161    pub roots: Vec<Cid>,
162    /// Every block in the file, by CID.
163    ///
164    /// Ordered, because a `BTreeMap` keyed by CID is the order
165    /// [`write()`] wants to hand them back in, so a file that is read and
166    /// rewritten comes out canonically ordered whatever order it arrived in.
167    pub blocks: BTreeMap<Cid, Vec<u8>>,
168}
169
170/// Reads a CARv1 file.
171///
172/// Every block is hashed and checked against the CID it is filed under. That
173/// is not an optional strictness: a CAR file is a claim that these bytes are
174/// those names, and a reader that skips the check turns a corrupted download
175/// into a repository that verifies against itself and against nothing else.
176/// The cost is one sha-256 per block, which is the same hash the sender paid
177/// to name it.
178///
179/// The version field is checked to be 1. There is no CARv2 support here and
180/// the failure for one is a header that says so rather than a parse that
181/// wanders into the index.
182///
183/// ```
184/// use didbot_data::Cid;
185/// use didbot_repo::car;
186///
187/// let block = b"not a real block".to_vec();
188/// let cid = Cid::of_raw(&block);
189/// let bytes = car::write(&cid, [(cid.clone(), block.clone())]);
190/// let read = car::read(&bytes).expect("a car this crate wrote");
191/// assert_eq!(read.roots, vec![cid.clone()]);
192/// assert_eq!(read.blocks[&cid], block);
193/// ```
194pub fn read(bytes: &[u8]) -> Result<Car, CarError> {
195    let mut reader = Reader {
196        bytes,
197        at: 0,
198        section_start: 0,
199    };
200    let header = reader.section()?.ok_or(CarError::Empty)?;
201    let roots = parse_header(&dag_cbor::decode(header)?)?;
202
203    let mut blocks = BTreeMap::new();
204    while let Some((at, section)) = reader.section()?.map(|s| (reader.section_start, s)) {
205        let (cid, block) = split_block(at, section)?;
206        let actual = Cid::of_block_under(cid.codec(), block);
207        if actual != cid {
208            return Err(CarError::Mislabelled {
209                at,
210                claimed: Box::new(cid),
211                actual: Box::new(actual),
212            });
213        }
214        if blocks.insert(cid.clone(), block.to_vec()).is_some() {
215            return Err(CarError::Duplicate(Box::new(cid)));
216        }
217    }
218    Ok(Car { roots, blocks })
219}
220
221/// Splits one block section into the CID it is filed under and its bytes.
222///
223/// The CID is not length-prefixed: it is self-describing, and where it ends
224/// is where the block begins. [`Cid::read`] is what knows that.
225fn split_block(at: usize, section: &[u8]) -> Result<(Cid, &[u8]), CarError> {
226    let mut rest = section;
227    let cid = Cid::read(&mut rest).map_err(|err| CarError::BlockCid { at, err })?;
228    Ok((cid, rest))
229}
230
231/// Reads the roots out of a decoded header, and refuses everything else.
232fn parse_header(header: &Value) -> Result<Vec<Cid>, CarError> {
233    let refuse = |why: &str| CarError::NotAHeader(why.to_owned());
234    let Value::Map(fields) = header else {
235        return Err(refuse("it is not a map"));
236    };
237    match fields.get("version") {
238        Some(Value::Integer(CAR_VERSION)) => {}
239        Some(Value::Integer(other)) => {
240            return Err(CarError::NotAHeader(format!(
241                "it declares version {other}, and this reads version 1"
242            )))
243        }
244        _ => return Err(refuse("it declares no version")),
245    }
246    let Some(Value::List(roots)) = fields.get("roots") else {
247        return Err(refuse("it has no roots list"));
248    };
249    roots
250        .iter()
251        .map(|root| match root {
252            Value::Link(cid) => Ok(cid.clone()),
253            _ => Err(refuse("a root is not a link")),
254        })
255        .collect()
256}
257
258/// A cursor over the length-prefixed sections a CAR file is made of.
259struct Reader<'a> {
260    /// The whole file.
261    bytes: &'a [u8],
262    /// How far in the cursor is.
263    at: usize,
264    /// Where the section most recently returned began, for error messages.
265    section_start: usize,
266}
267
268impl<'a> Reader<'a> {
269    /// Reads the next section, or `None` at the end of the file.
270    fn section(&mut self) -> Result<Option<&'a [u8]>, CarError> {
271        if self.at >= self.bytes.len() {
272            return Ok(None);
273        }
274        self.section_start = self.at;
275        let length = self.varint()?;
276        let left = self.bytes.len() - self.at;
277        let claimed = usize::try_from(length).unwrap_or(usize::MAX);
278        if claimed > left {
279            return Err(CarError::Truncated {
280                at: self.section_start,
281                claimed: length,
282                left,
283            });
284        }
285        let section = &self.bytes[self.at..self.at + claimed];
286        self.at += claimed;
287        Ok(Some(section))
288    }
289
290    /// Reads an unsigned LEB128 varint, the inverse of [`write_varint`].
291    fn varint(&mut self) -> Result<u64, CarError> {
292        let mut value: u64 = 0;
293        let mut shift = 0;
294        loop {
295            let byte = *self
296                .bytes
297                .get(self.at)
298                .ok_or(CarError::BadVarint(self.section_start))?;
299            self.at += 1;
300            // 64 bits is ten seven-bit groups, and the tenth may only carry
301            // the one bit that is left. Anything else is a varint that does
302            // not fit in what it is being read into.
303            if shift > 63 || (shift == 63 && byte > 1) {
304                return Err(CarError::BadVarint(self.section_start));
305            }
306            value |= u64::from(byte & 0x7f) << shift;
307            if byte & 0x80 == 0 {
308                return Ok(value);
309            }
310            shift += 7;
311        }
312    }
313}
314
315/// The DAG-CBOR header block: which roots, and which CAR version.
316fn header(roots: &[Cid]) -> Vec<u8> {
317    dag_cbor::encode(&Value::Map(std::collections::BTreeMap::from([
318        (
319            "roots".to_owned(),
320            Value::List(roots.iter().cloned().map(Value::Link).collect()),
321        ),
322        ("version".to_owned(), Value::Integer(CAR_VERSION)),
323    ])))
324}
325
326/// Appends one length-prefixed section.
327fn write_section(out: &mut Vec<u8>, payload: &[u8]) {
328    write_varint(out, payload.len() as u64);
329    out.extend_from_slice(payload);
330}
331
332/// Appends an unsigned LEB128 varint.
333///
334/// The same encoding `didbot_data::Cid` uses inside a CID, written
335/// again here rather than shared, because the two are the same shape for
336/// unrelated reasons: multiformats chose it for CIDs and CAR chose it for
337/// section lengths, and a change to either must not move the other.
338fn write_varint(out: &mut Vec<u8>, mut value: u64) {
339    while value >= 0x80 {
340        out.push((value as u8) | 0x80);
341        value >>= 7;
342    }
343    out.push(value as u8);
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349
350    #[test]
351    fn a_varint_is_shortest_form() {
352        let mut out = Vec::new();
353        write_varint(&mut out, 0);
354        assert_eq!(out, vec![0x00]);
355        out.clear();
356        write_varint(&mut out, 127);
357        assert_eq!(out, vec![0x7f]);
358        out.clear();
359        write_varint(&mut out, 128);
360        assert_eq!(out, vec![0x80, 0x01]);
361        out.clear();
362        write_varint(&mut out, 300);
363        assert_eq!(out, vec![0xac, 0x02]);
364    }
365
366    /// A section's size, counted before writing, is what writing it adds:
367    /// across the one-byte and two-byte length prefixes, and a large block.
368    #[test]
369    fn a_section_len_is_what_the_section_adds_to_the_file() {
370        let root = Cid::of_dag_cbor(b"a commit");
371        let empty = write(&root, []).len();
372        for len in [0, 1, 91, 92, 5_000, 2_000_000] {
373            let block = vec![7u8; len];
374            let cid = Cid::of_raw(&block);
375            let file = write(&root, [(cid.clone(), block)]);
376            assert_eq!(
377                file.len() - empty,
378                section_len(&cid, len),
379                "a {len}-byte block"
380            );
381        }
382    }
383
384    /// The header is the same six-key-free map every CAR file starts with, so
385    /// its bytes are fixed except for the root. Asserting the prefix catches a
386    /// map that lost its ordering or grew a field.
387    #[test]
388    fn the_header_declares_one_root_and_version_one() {
389        let root = Cid::of_dag_cbor(b"a commit");
390        let bytes = header(std::slice::from_ref(&root));
391        // {"roots": [...], "version": 1}: a two-key map, "roots" first.
392        assert_eq!(&bytes[..7], &[0xa2, 0x65, b'r', b'o', b'o', b't', b's']);
393        assert_eq!(&bytes[bytes.len() - 9..], b"\x67version\x01".as_slice());
394    }
395
396    #[test]
397    fn a_car_with_no_blocks_is_a_header_alone() {
398        let root = Cid::of_dag_cbor(b"a commit");
399        let bytes = write(&root, []);
400        assert_eq!(bytes.len(), header(std::slice::from_ref(&root)).len() + 1);
401    }
402
403    /// A rootless header is the same map with an empty list, which is a
404    /// different length and therefore a different file. Asserted so that a
405    /// change to `write_roots` cannot quietly start naming a root.
406    #[test]
407    fn a_rootless_car_declares_no_roots() {
408        let block = b"a block somebody asked for by name".to_vec();
409        let cid = Cid::of_raw(&block);
410        let bytes = write_roots(&[], [(cid, block)]);
411        // 0x80 is an empty CBOR array, where a one-element one is 0x81.
412        assert_eq!(&bytes[1..8], &[0xa2, 0x65, b'r', b'o', b'o', b't', b's']);
413        assert_eq!(bytes[8], 0x80);
414    }
415}