UNPKG

@ipld/car

Version:

Content Addressable aRchive format reader and writer

214 lines (192 loc) 6.26 kB
import { decode as decodeDagCbor } from '@ipld/dag-cbor' import { CID } from 'multiformats/cid' import * as Digest from 'multiformats/hashes/digest' import { CIDV0_BYTES, decodeV2Header, decodeVarint, getMultihashLength, V2_HEADER_LENGTH } from './decoder-common.js' import { CarV1HeaderOrV2Pragma } from './header-validator.js' /** * @typedef {import('./api').Block} Block * @typedef {import('./api').BlockHeader} BlockHeader * @typedef {import('./api').BlockIndex} BlockIndex * @typedef {import('./coding').BytesBufferReader} BytesBufferReader * @typedef {import('./coding').CarHeader} CarHeader * @typedef {import('./coding').CarV2Header} CarV2Header * @typedef {import('./coding').CarV2FixedHeader} CarV2FixedHeader */ /** * Reads header data from a `BytesReader`. The header may either be in the form * of a `CarHeader` or `CarV2Header` depending on the CAR being read. * * @name decoder.readHeader(reader) * @param {BytesBufferReader} reader * @param {number} [strictVersion] * @returns {CarHeader | CarV2Header} */ export function readHeader (reader, strictVersion) { const length = decodeVarint(reader.upTo(8), reader) if (length === 0) { throw new Error('Invalid CAR header (zero length)') } const header = reader.exactly(length, true) const block = decodeDagCbor(header) if (CarV1HeaderOrV2Pragma.toTyped(block) === undefined) { throw new Error('Invalid CAR header format') } if ((block.version !== 1 && block.version !== 2) || (strictVersion !== undefined && block.version !== strictVersion)) { throw new Error(`Invalid CAR version: ${block.version}${strictVersion !== undefined ? ` (expected ${strictVersion})` : ''}`) } if (block.version === 1) { // CarV1HeaderOrV2Pragma makes roots optional, let's make it mandatory if (!Array.isArray(block.roots)) { throw new Error('Invalid CAR header format') } return block } // version 2 if (block.roots !== undefined) { throw new Error('Invalid CAR header format') } const v2Header = decodeV2Header(reader.exactly(V2_HEADER_LENGTH, true)) reader.seek(v2Header.dataOffset - reader.pos) const v1Header = readHeader(reader, 1) return Object.assign(v1Header, v2Header) } /** * Reads CID sync * * @param {BytesBufferReader} reader * @returns {CID} */ function readCid (reader) { const first = reader.exactly(2, false) if (first[0] === CIDV0_BYTES.SHA2_256 && first[1] === CIDV0_BYTES.LENGTH) { // cidv0 32-byte sha2-256 const bytes = reader.exactly(34, true) const multihash = Digest.decode(bytes) return CID.create(0, CIDV0_BYTES.DAG_PB, multihash) } const version = decodeVarint(reader.upTo(8), reader) if (version !== 1) { throw new Error(`Unexpected CID version (${version})`) } const codec = decodeVarint(reader.upTo(8), reader) const bytes = reader.exactly(getMultihashLength(reader.upTo(8)), true) const multihash = Digest.decode(bytes) return CID.create(version, codec, multihash) } /** * Reads the leading data of an individual block from CAR data from a * `BytesBufferReader`. Returns a `BlockHeader` object which contains * `{ cid, length, blockLength }` which can be used to either index the block * or read the block binary data. * * @name async decoder.readBlockHead(reader) * @param {BytesBufferReader} reader * @returns {BlockHeader} */ export function readBlockHead (reader) { // length includes a CID + Binary, where CID has a variable length // we have to deal with const start = reader.pos let length = decodeVarint(reader.upTo(8), reader) if (length === 0) { throw new Error('Invalid CAR section (zero length)') } length += (reader.pos - start) const cid = readCid(reader) const blockLength = length - Number(reader.pos - start) // subtract CID length return { cid, length, blockLength } } /** * Returns Car header and blocks from a Uint8Array * * @param {Uint8Array} bytes * @returns {{ header : CarHeader | CarV2Header , blocks: Block[]}} */ export function fromBytes (bytes) { let reader = bytesReader(bytes) const header = readHeader(reader) if (header.version === 2) { const v1length = reader.pos - header.dataOffset reader = limitReader(reader, header.dataSize - v1length) } const blocks = [] while (reader.upTo(8).length > 0) { const { cid, blockLength } = readBlockHead(reader) blocks.push({ cid, bytes: reader.exactly(blockLength, true) }) } return { header, blocks } } /** * Creates a `BytesBufferReader` from a `Uint8Array`. * * @name decoder.bytesReader(bytes) * @param {Uint8Array} bytes * @returns {BytesBufferReader} */ export function bytesReader (bytes) { let pos = 0 /** @type {BytesBufferReader} */ return { upTo (length) { return bytes.subarray(pos, pos + Math.min(length, bytes.length - pos)) }, exactly (length, seek = false) { if (length > bytes.length - pos) { throw new Error('Unexpected end of data') } const out = bytes.subarray(pos, pos + length) if (seek) { pos += length } return out }, seek (length) { pos += length }, get pos () { return pos } } } /** * Wraps a `BytesBufferReader` in a limiting `BytesBufferReader` which limits maximum read * to `byteLimit` bytes. It _does not_ update `pos` of the original * `BytesBufferReader`. * * @name decoder.limitReader(reader, byteLimit) * @param {BytesBufferReader} reader * @param {number} byteLimit * @returns {BytesBufferReader} */ export function limitReader (reader, byteLimit) { let bytesRead = 0 /** @type {BytesBufferReader} */ return { upTo (length) { let bytes = reader.upTo(length) if (bytes.length + bytesRead > byteLimit) { bytes = bytes.subarray(0, byteLimit - bytesRead) } return bytes }, exactly (length, seek = false) { const bytes = reader.exactly(length, seek) if (bytes.length + bytesRead > byteLimit) { throw new Error('Unexpected end of data') } if (seek) { bytesRead += length } return bytes }, seek (length) { bytesRead += length reader.seek(length) }, get pos () { return reader.pos } } }