UNPKG

@atcute/car

Version:

lightweight DASL CAR and atproto repository decoder for AT Protocol.

172 lines (135 loc) 3.78 kB
import * as CBOR from '@atcute/cbor'; import * as CID from '@atcute/cid'; import * as varint from '@atcute/varint'; import { isCarV1Header, type CarEntry, type CarHeader } from './types.js'; interface SyncByteReader { readonly pos: number; upto(size: number): Uint8Array; exactly(size: number, seek: boolean): Uint8Array; seek(size: number): void; } export interface SyncCarReader { readonly header: CarHeader; readonly roots: CBOR.CidLink[]; /** @deprecated do for..of on the reader directly */ iterate(): Generator<CarEntry>; [Symbol.iterator](): Iterator<CarEntry>; } export const fromUint8Array = (buffer: Uint8Array): SyncCarReader => { const reader = createUint8Reader(buffer); const header = readHeader(reader); return { header, roots: header.data.roots, *iterate() { while (reader.upto(8 + 36).length > 0) { const entryStart = reader.pos; const entrySize = readVarint(reader, 8); const cidStart = reader.pos; const cid = readCid(reader); const bytesStart = reader.pos; const bytesSize = entrySize - (bytesStart - cidStart); const bytes = reader.exactly(bytesSize, true); const cidEnd = bytesStart; const bytesEnd = reader.pos; const entryEnd = bytesEnd; yield { cid, bytes, entryStart, entryEnd, cidStart, cidEnd, bytesStart, bytesEnd, }; } }, [Symbol.iterator](): Iterator<CarEntry> { return this.iterate(); }, }; }; const createUint8Reader = (buf: Uint8Array): SyncByteReader => { let pos = 0; return { get pos() { return pos; }, seek(size) { if (size > buf.length - pos) { throw new RangeError('unexpected end of data'); } pos += size; }, upto(size) { return buf.subarray(pos, pos + size); }, exactly(size, seek) { if (size > buf.length - pos) { throw new RangeError('unexpected end of data'); } const slice = buf.subarray(pos, pos + size); if (seek) { pos += size; } return slice; }, }; }; const readVarint = (reader: SyncByteReader, size: number): number => { const buf = reader.upto(size); if (buf.length === 0) { throw new RangeError(`unexpected end of data`); } const [int, read] = varint.decode(buf); reader.seek(read); return int; }; const readHeader = (reader: SyncByteReader): CarHeader => { const headerStart = reader.pos; const length = readVarint(reader, 8); if (length === 0) { throw new RangeError(`invalid car header; length=0`); } const dataStart = reader.pos; const rawHeader = reader.exactly(length, true); const data = CBOR.decode(rawHeader); if (!isCarV1Header(data)) { throw new TypeError(`expected a car v1 archive`); } const dataEnd = reader.pos; const headerEnd = dataEnd; return { data, headerStart, headerEnd, dataStart, dataEnd }; }; const readCid = (reader: SyncByteReader): CID.Cid => { const head = reader.exactly(4, false); const version = head[0]; const codec = head[1]; const digestType = head[2]; const digestSize = head[3]; if (version !== CID.CID_VERSION) { throw new RangeError(`incorrect cid version (got v${version})`); } if (codec !== CID.CODEC_DCBOR && codec !== CID.CODEC_RAW) { throw new RangeError(`incorrect cid codec (got 0x${codec.toString(16)})`); } if (digestType !== CID.HASH_SHA256) { throw new RangeError(`incorrect cid digest type (got 0x${digestType.toString(16)})`); } if (digestSize !== 32 && digestSize !== 0) { throw new RangeError(`incorrect cid digest size (got ${digestSize})`); } const bytes = reader.exactly(4 + digestSize, true); const digest = bytes.subarray(4, 4 + digestSize); const cid: CID.Cid = { version: version, codec: codec, digest: { codec: digestType, contents: digest, }, bytes: bytes, }; return cid; };