//! CAR files (CARv1): a header naming the roots, then blocks, each its //! CID and bytes. How a repo is exported, imported, and sent down the //! firehose a commit at a time. use crate::cid::{Cid, DAG_CBOR}; use crate::drisl::{self, Value}; use std::collections::BTreeMap; use std::fmt; #[derive(Debug, Clone, PartialEq, Eq)] pub struct CarError(pub String); impl fmt::Display for CarError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(&self.0) } } impl std::error::Error for CarError {} fn err(what: impl Into) -> Result { Err(CarError(what.into())) } /// A CAR file read: its roots, and its blocks by CID. #[derive(Debug, Clone, PartialEq, Default)] pub struct Car { pub roots: Vec, pub blocks: BTreeMap>, } fn varint(mut n: u64, out: &mut Vec) { while n >= 0x80 { out.push((n as u8) | 0x80); n >>= 7; } out.push(n as u8); } /// A CAR file of `roots` and `blocks`, the blocks in the order given. pub fn write<'a>(roots: &[Cid], blocks: impl IntoIterator) -> Vec { let header = drisl::encode(&Value::Map(BTreeMap::from([ ( "roots".into(), Value::Array(roots.iter().map(|c| Value::Link(*c)).collect()), ), ("version".into(), Value::Integer(1)), ]))); let mut out = Vec::new(); varint(header.len() as u64, &mut out); out.extend(header); for (cid, bytes) in blocks { let cid = cid.to_bytes(); varint((cid.len() + bytes.len()) as u64, &mut out); out.extend(cid); out.extend(bytes); } out } struct Reader<'a> { bytes: &'a [u8], at: usize, } impl Reader<'_> { fn varint(&mut self) -> Result { let mut n: u64 = 0; for shift in (0..64).step_by(7) { let Some(&b) = self.bytes.get(self.at) else { return err("the file ends in the middle of a length"); }; self.at += 1; n |= u64::from(b & 0x7f) << shift; if b & 0x80 == 0 { // Past what this machine can hold, `take` refuses it. return Ok(usize::try_from(n).unwrap_or(usize::MAX)); } } err("a length too long") } fn take(&mut self, n: usize) -> Result<&[u8], CarError> { if n > self.bytes.len() - self.at { return err("the file ends in the middle of a block"); } let start = self.at; self.at += n; Ok(&self.bytes[start..self.at]) } } /// The CAR file `bytes` hold. Each block's CID is checked against its /// bytes; a block whose CID is not one atproto blesses is refused. pub fn read(bytes: &[u8]) -> Result { let mut r = Reader { bytes, at: 0 }; let len = r.varint()?; let header = drisl::decode(r.take(len)?).or_else(|e| err(format!("the header: {e}")))?; let Value::Map(header) = header else { return err("the header is not a map"); }; let (Some(Value::Integer(1)), Some(Value::Array(roots))) = (header.get("version"), header.get("roots")) else { return err("the header is not version 1 with roots"); }; let roots = roots .iter() .map(|r| match r { Value::Link(c) => Ok(*c), _ => err("a root is not a link"), }) .collect::>()?; let mut car = Car { roots, blocks: BTreeMap::new(), }; while r.at < bytes.len() { let len = r.varint()?; let block = r.take(len)?; // A blessed CID is 36 bytes: version, codec, hash and its length. let Some((cid, data)) = block.split_at_checked(36) else { return err("a block too short for its CID"); }; let cid = Cid::from_bytes(cid).or_else(|e| err(format!("a block's CID: {e}")))?; // Cid::from_bytes allows only these two codecs. let hashed = if cid.codec() == DAG_CBOR { Cid::of_cbor(data) } else { Cid::of_raw(data) }; if hashed != cid { return err(format!("block {cid} does not hash to its CID")); } car.blocks.insert(cid, data.to_vec()); } Ok(car) }