diff --git a/lib/decoder.js b/lib/decoder.js index 2a4d2e0..dcfc5b3 100644 --- a/lib/decoder.js +++ b/lib/decoder.js @@ -2,6 +2,7 @@ import varint from 'varint' import { CID } from 'multiformats/cid' import * as Digest from 'multiformats/hashes/digest' import { decode as decodeDagCbor } from '@ipld/dag-cbor' +import { CarHeader as headerValidator } from './header-validator.js' /** * @typedef {import('../api').Block} Block @@ -33,9 +34,10 @@ async function readVarint (reader) { /** * @param {BytesReader} reader + * @param {number} [strictVersion=1] a strict version to expect in the header, or -1 to skip version checking entirely * @returns {Promise} */ -export async function readHeader (reader) { +export async function readHeader (reader, strictVersion = 1) { const length = await readVarint(reader) if (length === 0) { throw new Error('Invalid CAR header (zero length)') @@ -43,19 +45,17 @@ export async function readHeader (reader) { const header = await reader.exactly(length) reader.seek(length) const block = decodeDagCbor(header) - if (block == null || Array.isArray(block) || typeof block !== 'object') { + if (!headerValidator(block)) { throw new Error('Invalid CAR header format') } - if (block.version !== 1) { - if (typeof block.version === 'string') { - throw new Error(`Invalid CAR version: "${block.version}"`) + if (strictVersion !== -1) { + if (block.version !== strictVersion) { + throw new Error(`Invalid CAR version: ${block.version}`) } - throw new Error(`Invalid CAR version: ${block.version}`) } if (!Array.isArray(block.roots)) { - throw new Error('Invalid CAR header format') - } - if (Object.keys(block).filter((p) => p !== 'roots' && p !== 'version').length) { + // we've made 'roots' optional in the schema so we can do the version check + // before rejecting the block as invalid if there is no version throw new Error('Invalid CAR header format') } return block diff --git a/lib/header-validator.js b/lib/header-validator.js new file mode 100644 index 0000000..634e64b --- /dev/null +++ b/lib/header-validator.js @@ -0,0 +1,33 @@ +/** Auto-generated with ipld-schema-validator@0.0.0-dev at Thu Jun 17 2021 from IPLD Schema: + * + * type CarHeader struct { + * version Int + * roots optional [&Any] + * # roots is _not_ optional for CarV1 but we defer that check within code to + * # gracefully handle the >V1 case where it's just {version:X} + * } + * + */ + +const Kinds = { + Null: /** @returns {boolean} */ (/** @type {any} */ obj) => obj === null, + Int: /** @returns {boolean} */ (/** @type {any} */ obj) => Number.isInteger(obj), + Float: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'number' && Number.isFinite(obj), + String: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'string', + Bool: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'boolean', + Bytes: /** @returns {boolean} */ (/** @type {any} */ obj) => obj instanceof Uint8Array, + Link: /** @returns {boolean} */ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID === obj, + List: /** @returns {boolean} */ (/** @type {any} */ obj) => Array.isArray(obj), + Map: /** @returns {boolean} */ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID !== obj && !Kinds.List(obj) && !Kinds.Bytes(obj) +} +/** @type {{ [k in string]: (obj:any)=>boolean}} */ +const Types = { + Int: Kinds.Int, + 'CarHeader > version': /** @returns {boolean} */ (/** @type {any} */ obj) => Types.Int(obj), + 'CarHeader > roots (anon) > valueType (anon)': Kinds.Link, + 'CarHeader > roots (anon)': /** @returns {boolean} */ (/** @type {any} */ obj) => Kinds.List(obj) && Array.prototype.every.call(obj, Types['CarHeader > roots (anon) > valueType (anon)']), + 'CarHeader > roots': /** @returns {boolean} */ (/** @type {any} */ obj) => Types['CarHeader > roots (anon)'](obj), + CarHeader: /** @returns {boolean} */ (/** @type {any} */ obj) => { const keys = obj && Object.keys(obj); return Kinds.Map(obj) && ['version'].every((k) => keys.includes(k)) && Object.entries(obj).every(([name, value]) => Types['CarHeader > ' + name] && Types['CarHeader > ' + name](value)) } +} + +export const CarHeader = Types.CarHeader diff --git a/lib/header.ipldsch b/lib/header.ipldsch new file mode 100644 index 0000000..0f10a23 --- /dev/null +++ b/lib/header.ipldsch @@ -0,0 +1,6 @@ +type CarHeader struct { + version Int + roots optional [&Any] + # roots is _not_ optional for CarV1 but we defer that check within code to + # gracefully handle the >V1 case where it's just {version:X} +} diff --git a/package.json b/package.json index e2d6c56..46d7c32 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "build:types": "tsc --build && mv types dist", "test:cjs": "rm -rf dist && npm run build && cp test/go.car dist/cjs/node-test/ && mocha dist/cjs/node-test/test-*.js && mocha dist/cjs/node-test/node-test-*.js && npm run test:cjs:browser", "test:esm": "rm -rf dist && npm run build && cp test/go.car dist/esm/node-test/ && mocha dist/esm/node-test/test-*.js && mocha dist/esm/node-test/node-test-*.js && npm run test:esm:browser", - "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/test-*.js test/node-test-*.js", + "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --exclude lib/header-validator.js --exclude test/ mocha test/test-*.js test/node-test-*.js", "test:cjs:browser": "polendina --page --worker --serviceworker --cleanup dist/cjs/browser-test/test-*.js", "test:esm:browser": "polendina --page --worker --serviceworker --cleanup dist/esm/browser-test/test-*.js", "test": "npm run lint && npm run test:node && npm run test:cjs && npm run test --prefix examples/", @@ -211,4 +211,4 @@ "@semantic-release/git" ] } -} +} \ No newline at end of file diff --git a/test/test-errors.js b/test/test-errors.js index 0cfa4d6..7594985 100644 --- a/test/test-errors.js +++ b/test/test-errors.js @@ -40,37 +40,46 @@ describe('Misc errors', () => { await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 2') }) - it('bad header', async () => { - // sanity check, this should be fine - let buf2 = makeHeader({ version: 1, roots: [] }) - await assert.isFulfilled(CarReader.fromBytes(buf2)) + describe('bad header', async () => { + it('sanity check', async () => { + // sanity check, this should be fine + const buf2 = makeHeader({ version: 1, roots: [] }) + await assert.isFulfilled(CarReader.fromBytes(buf2)) + }) - // no 'version' array - buf2 = makeHeader({ roots: [] }) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: undefined') + it('no \'version\' array', async () => { + const buf2 = makeHeader({ roots: [] }) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) - // bad 'version' type - buf2 = makeHeader({ version: '1', roots: [] }) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: "1"') + it('bad \'version\' type', async () => { + const buf2 = makeHeader({ version: '1', roots: [] }) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) - // no 'roots' array - buf2 = makeHeader({ version: 1 }) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + it('no \'roots\' array', async () => { + const buf2 = makeHeader({ version: 1 }) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) - // bad 'roots' type - buf2 = makeHeader({ version: 1, roots: {} }) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + it('bad \'roots\' type', async () => { + const buf2 = makeHeader({ version: 1, roots: {} }) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) - // extraneous properties - buf2 = makeHeader({ version: 1, roots: [], blip: true }) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + it('extraneous properties', async () => { + const buf2 = makeHeader({ version: 1, roots: [], blip: true }) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) - // not an object - buf2 = makeHeader([1, []]) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + it('not an object', async () => { + const buf2 = makeHeader([1, []]) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) - // not an object - buf2 = makeHeader(null) - await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + it('not an object', async () => { + const buf2 = makeHeader(null) + await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format') + }) }) })