@ipld/car
Version:
Content Addressable aRchive format reader and writer
214 lines (192 loc) • 6.26 kB
JavaScript
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
}
}
}