OverviewFilesCommitsIssuesDocsReleasesPeers

atproto-repo / lib / car.js

73 lines · javascript ref:
// 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 }
}