// Minimal CAR v1 codec — bare-safe, dependency-light replacement for @ipld/car.
//
// CAR v1 is a trivial container: varint(headerLen) ++ dag-cbor({version:1, roots:[…]})
// then, per block, varint(len) ++ CIDbytes ++ blockBytes. We only need whole-buffer
// read/write (no streaming, no file I/O), so this is ~40 lines of pure JS over
// multiformats' CID (byte-exact with @ipld/car — verified by the repo tests + the
// @atproto/repo conformance reader). Drops @ipld/car (which imports `fs`/streams and
// won't load under Bare).
//
// ⚠️ LIMITATIONS vs @ipld/car (tracked in memory: bare-car-limitations). Fine for our
// current use (whole-buffer, CARv1, OUR-OWN trusted output); add the feature before the
// gate it guards:
// - No streaming — whole CAR is buffered in memory. GATE: large repos (getRepo of a
// big repo could be GBs). Add an async-iterator read/write path.
// - No untrusted-input hardening — readCar trusts varints/lengths/CIDs. GATE: reading
// UNTRUSTED CARs (com.atproto.repo.importRepo, ingress bridge). Add bounds/overflow/
// truncation checks + duplicate-CID handling before then.
// - CARv1 only (no CARv2/index). atproto is CARv1, so no gate today.
// Fully-featured path: grow this file at each gate, OR extract a standalone bare-native
// `bare-car` package (the holepunch ecosystem lacks one — a real contribution).
import { CID } from 'multiformats/cid'
import * as dagCbor from '@ipld/dag-cbor'
function varintEncode (n) {
const out = []
while (n >= 0x80) { out.push((n & 0x7f) | 0x80); n >>>= 7 }
out.push(n)
return Uint8Array.from(out)
}
function varintDecode (bytes, offset) {
let n = 0; let shift = 0; let i = offset
for (;;) {
const b = bytes[i++]
n |= (b & 0x7f) << shift
if ((b & 0x80) === 0) break
shift += 7
}
return [n >>> 0, i]
}
// Serialise { root, blocks } to CAR bytes. `blocks` is an iterable of { cid, bytes }.
export async function writeCar (rootCid, blocks) {
const header = dagCbor.encode({ version: 1, roots: [rootCid] })
const parts = [Buffer.from(varintEncode(header.length)), Buffer.from(header)]
for (const { cid, bytes } of blocks) {
const cidBytes = cid.bytes
const body = new Uint8Array(cidBytes.length + bytes.length)
body.set(cidBytes, 0)
body.set(bytes, cidBytes.length)
parts.push(Buffer.from(varintEncode(body.length)), Buffer.from(body))
}
return Buffer.concat(parts)
}
// Read a CAR into { roots, blocks } where blocks maps cid-string -> { cid, bytes }.
export async function readCar (carBytes) {
const bytes = carBytes instanceof Uint8Array ? carBytes : new Uint8Array(carBytes)
let [hlen, off] = varintDecode(bytes, 0)
const header = dagCbor.decode(bytes.subarray(off, off + hlen))
off += hlen
const blocks = new Map()
while (off < bytes.length) {
const [blen, bodyStart] = varintDecode(bytes, off)
const end = bodyStart + blen
const [cid, rest] = CID.decodeFirst(bytes.subarray(bodyStart, end))
blocks.set(cid.toString(), { cid, bytes: rest })
off = end
}
return { roots: header.roots, blocks }
}