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, §ion);
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}