Skip to content

Commit

Permalink
chore: add header validation via schema, allow for !=1 version
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Mar 4, 2022
1 parent ae56ce5 commit d260597
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 36 deletions.
18 changes: 9 additions & 9 deletions lib/decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -33,29 +34,28 @@ 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<CarHeader>}
*/
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)')
}
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
Expand Down
33 changes: 33 additions & 0 deletions lib/header-validator.js
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions lib/header.ipldsch
Original file line number Diff line number Diff line change
@@ -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}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down Expand Up @@ -211,4 +211,4 @@
"@semantic-release/git"
]
}
}
}
59 changes: 34 additions & 25 deletions test/test-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
})

0 comments on commit d260597

Please sign in to comment.