UNPKG

@atcute/car

Version:

lightweight DASL CAR (content-addressable archives) codec for AT Protocol.

152 lines (123 loc) 3.69 kB
import * as CBOR from '@atcute/cbor'; import type { CidLink } from '@atcute/cid'; import * as CID from '@atcute/cid'; import * as varint from '@atcute/varint'; import { isCarV1Header, type CarEntry, type CarHeader } from './types.ts'; export interface SyncCarReader { readonly header: CarHeader; readonly roots: CidLink[]; /** @deprecated do for..of on the reader directly */ iterate(): IterableIterator<CarEntry>; [Symbol.iterator](): Iterator<CarEntry>; } export const fromUint8Array = (buffer: Uint8Array): SyncCarReader => { const { header, nextOffset: headerOffset } = readHeader(buffer, 0); let pos = headerOffset; return { header, roots: header.data.roots, iterate(): IterableIterator<CarEntry> { return { next(): IteratorResult<CarEntry> { if (pos >= buffer.length) { return { done: true, value: undefined, }; } const entryStart = pos; const { value: entryLength, nextOffset: lengthOffset } = varint.decode(buffer, pos, 8); pos = lengthOffset; const cidStart = pos; const { cid, nextOffset: cidOffset } = readCid(buffer, pos); pos = cidOffset; const bytesStart = pos; const bytesSize = entryLength - (bytesStart - cidStart); if (bytesSize < 0 || bytesStart + bytesSize > buffer.length) { throw new RangeError('unexpected end of data'); } const bytesEnd = bytesStart + bytesSize; const bytes = buffer.subarray(bytesStart, bytesEnd); pos = bytesEnd; const cidEnd = bytesStart; const entryEnd = bytesEnd; return { done: false, value: { cid, bytes, entryStart, entryEnd, cidStart, cidEnd, bytesStart, bytesEnd, }, }; }, [Symbol.iterator]() { return this; }, }; }, [Symbol.iterator](): Iterator<CarEntry> { return this.iterate(); }, }; }; const readHeader = (source: Uint8Array, offset: number): { header: CarHeader; nextOffset: number } => { const headerStart = offset; const { value: length, nextOffset: lengthOffset } = varint.decode(source, offset, 8); if (length === 0) { throw new RangeError(`invalid car header; length=0`); } const dataStart = lengthOffset; const dataEnd = dataStart + length; if (dataEnd > source.length) { throw new RangeError('unexpected end of data'); } const data = CBOR.decode(source.subarray(dataStart, dataEnd)); if (!isCarV1Header(data)) { throw new TypeError(`expected a car v1 archive`); } const headerEnd = dataEnd; return { header: { data, headerStart, headerEnd, dataStart, dataEnd }, nextOffset: dataEnd, }; }; const readCid = (source: Uint8Array, offset: number): { cid: CID.Cid; nextOffset: number } => { const cidEnd = offset + 36; if (cidEnd > source.length) { throw new RangeError('unexpected end of data'); } const bytes = source.subarray(offset, cidEnd); const version = bytes[0]; const codec = bytes[1]; const digestType = bytes[2]; const digestSize = bytes[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) { throw new RangeError(`incorrect cid digest size (got ${digestSize})`); } return { cid: { version: version, codec: codec, digest: { codec: digestType, contents: bytes.subarray(4, 36), }, bytes: bytes, }, nextOffset: cidEnd, }; };