Skip to content

Commit

Permalink
chore: use new schema validator compiler, add both forms of schema
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed Sep 14, 2023
1 parent 8410956 commit cbf7e06
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 85 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
"clean": "aegir clean",
"lint": "aegir lint",
"build": "aegir build",
"build:validator": "npx @ipld/schema to-js src/header.ipldsch > src/header-validator.js",
"release": "aegir release",
"test": "npm run lint && aegir test && npm run test:examples",
"test:node": "aegir test -t node --cov",
Expand Down Expand Up @@ -227,5 +228,8 @@
"ignore": [
"dist"
]
}
},
"eslintIgnore": [
"src/header-validator.js"
]
}
17 changes: 9 additions & 8 deletions src/buffer-decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { CarHeader as headerValidator } from './header-validator.js'
import { CarV1HeaderOrV2Pragma } from './header-validator.js'

/**
* @typedef {import('./api').Block} Block
Expand Down Expand Up @@ -30,22 +30,23 @@ export function readHeader (reader, strictVersion) {
}
const header = reader.exactly(length, true)
const block = decodeDagCbor(header)
if (!headerValidator(block)) {
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})` : ''}`)
}
// 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
const hasRoots = Array.isArray(block.roots)
if ((block.version === 1 && !hasRoots) || (block.version === 2 && hasRoots)) {
throw new Error('Invalid CAR header format')
}
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)
Expand Down
17 changes: 9 additions & 8 deletions src/decoder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { CarHeader as headerValidator } from './header-validator.js'
import { CarV1HeaderOrV2Pragma } from './header-validator.js'

/**
* @typedef {import('./api').Block} Block
Expand Down Expand Up @@ -31,22 +31,23 @@ export async function readHeader (reader, strictVersion) {
}
const header = await reader.exactly(length, true)
const block = decodeDagCbor(header)
if (!headerValidator(block)) {
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})` : ''}`)
}
// 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
const hasRoots = Array.isArray(block.roots)
if ((block.version === 1 && !hasRoots) || (block.version === 2 && hasRoots)) {
throw new Error('Invalid CAR header format')
}
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(await reader.exactly(V2_HEADER_LENGTH, true))
reader.seek(v2Header.dataOffset - reader.pos)
const v1Header = await readHeader(reader, 1)
Expand Down
266 changes: 203 additions & 63 deletions src/header-validator.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,214 @@
/* eslint-disable jsdoc/check-indentation */

/** Auto-generated with ipld-schema-validator@0.0.0-dev at Thu Jun 17 2021 from IPLD Schema:
/** Auto-generated with @ipld/schema@v4.2.0 at Thu Sep 14 2023 from IPLD Schema:
*
* # CarV1HeaderOrV2Pragma is a more relaxed form, and can parse {version:x} where
* # roots are optional. This is typically useful for the {verison:2} CARv2
* # pragma.
*
* 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}
* type CarV1HeaderOrV2Pragma struct {
* roots optional [&Any]
* # roots is _not_ optional for CarV1 but we defer that check within code to
* # gracefully handle the V2 case where it's just {version:X}
* version Int
* }
*
* # CarV1Header is the strict form of the header, and requires roots to be
* # present. This is compatible with the CARv1 specification.
*
* # type CarV1Header struct {
* # roots [&Any]
* # version Int
* # }
*
*/

const Kinds = {
Null: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => obj === null,
Int: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => Number.isInteger(obj),
Float: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => typeof obj === 'number' && Number.isFinite(obj),
String: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => typeof obj === 'string',
Bool: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => typeof obj === 'boolean',
Bytes: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => obj instanceof Uint8Array,
Link: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID === obj,
List: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => Array.isArray(obj),
Map: /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID !== obj && !Kinds.List(obj) && !Kinds.Bytes(obj)
Null: /** @returns {undefined|null} */ (/** @type {any} */ obj) => obj === null ? obj : undefined,
Int: /** @returns {undefined|number} */ (/** @type {any} */ obj) => Number.isInteger(obj) ? obj : undefined,
Float: /** @returns {undefined|number} */ (/** @type {any} */ obj) => typeof obj === 'number' && Number.isFinite(obj) ? obj : undefined,
String: /** @returns {undefined|string} */ (/** @type {any} */ obj) => typeof obj === 'string' ? obj : undefined,
Bool: /** @returns {undefined|boolean} */ (/** @type {any} */ obj) => typeof obj === 'boolean' ? obj : undefined,
Bytes: /** @returns {undefined|Uint8Array} */ (/** @type {any} */ obj) => obj instanceof Uint8Array ? obj : undefined,
Link: /** @returns {undefined|object} */ (/** @type {any} */ obj) => obj !== null && typeof obj === 'object' && obj.asCID === obj ? obj : undefined,
List: /** @returns {undefined|Array<any>} */ (/** @type {any} */ obj) => Array.isArray(obj) ? obj : undefined,
Map: /** @returns {undefined|object} */ (/** @type {any} */ obj) => obj !== null && typeof obj === 'object' && obj.asCID !== obj && !Array.isArray(obj) && !(obj instanceof Uint8Array) ? obj : undefined
}
/** @type {{ [k in string]: (obj:any)=>boolean}} */
/** @type {{ [k in string]: (obj:any)=>undefined|any}} */
const Types = {
'CarV1HeaderOrV2Pragma > roots (anon) > valueType (anon)': Kinds.Link,
'CarV1HeaderOrV2Pragma > roots (anon)': /** @returns {undefined|any} */ (/** @type {any} */ obj) => {
if (Kinds.List(obj) === undefined) {
return undefined
}
for (let i = 0; i < obj.length; i++) {
let v = obj[i]
v = Types['CarV1HeaderOrV2Pragma > roots (anon) > valueType (anon)'](v)
if (v === undefined) {
return undefined
}
if (v !== obj[i]) {
const ret = obj.slice(0, i)
for (let j = i; j < obj.length; j++) {
let v = obj[j]
v = Types['CarV1HeaderOrV2Pragma > roots (anon) > valueType (anon)'](v)
if (v === undefined) {
return undefined
}
ret.push(v)
}
return ret
}
}
return obj
},
Int: Kinds.Int,
'CarHeader > version': /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => Types.Int(obj),
'CarHeader > roots (anon) > valueType (anon)': Kinds.Link,
'CarHeader > roots (anon)': /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => Kinds.List(obj) && Array.prototype.every.call(obj, Types['CarHeader > roots (anon) > valueType (anon)']),
'CarHeader > roots': /**
* @param {any} obj
* @returns {boolean}
*/ (/** @type {any} */ obj) => Types['CarHeader > roots (anon)'](obj),
CarHeader: /**
* @param {any} obj
* @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)) }
CarV1HeaderOrV2Pragma: /** @returns {undefined|any} */ (/** @type {any} */ obj) => {
if (Kinds.Map(obj) === undefined) {
return undefined
}
const entries = Object.entries(obj)
/** @type {{[k in string]: any}} */
let ret = obj
let requiredCount = 1
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i]
switch (key) {
case 'roots':
{
const v = Types['CarV1HeaderOrV2Pragma > roots (anon)'](obj[key])
if (v === undefined) {
return undefined
}
if (v !== value || ret !== obj) {
if (ret === obj) {
/** @type {{[k in string]: any}} */
ret = {}
for (let j = 0; j < i; j++) {
ret[entries[j][0]] = entries[j][1]
}
}
ret.roots = v
}
}
break
case 'version':
{
requiredCount--
const v = Types.Int(obj[key])
if (v === undefined) {
return undefined
}
if (v !== value || ret !== obj) {
if (ret === obj) {
/** @type {{[k in string]: any}} */
ret = {}
for (let j = 0; j < i; j++) {
ret[entries[j][0]] = entries[j][1]
}
}
ret.version = v
}
}
break
default:
return undefined
}
}

if (requiredCount > 0) {
return undefined
}
return ret
}
}
/** @type {{ [k in string]: (obj:any)=>undefined|any}} */
const Reprs = {
'CarV1HeaderOrV2Pragma > roots (anon) > valueType (anon)': Kinds.Link,
'CarV1HeaderOrV2Pragma > roots (anon)': /** @returns {undefined|any} */ (/** @type {any} */ obj) => {
if (Kinds.List(obj) === undefined) {
return undefined
}
for (let i = 0; i < obj.length; i++) {
let v = obj[i]
v = Reprs['CarV1HeaderOrV2Pragma > roots (anon) > valueType (anon)'](v)
if (v === undefined) {
return undefined
}
if (v !== obj[i]) {
const ret = obj.slice(0, i)
for (let j = i; j < obj.length; j++) {
let v = obj[j]
v = Reprs['CarV1HeaderOrV2Pragma > roots (anon) > valueType (anon)'](v)
if (v === undefined) {
return undefined
}
ret.push(v)
}
return ret
}
}
return obj
},
Int: Kinds.Int,
CarV1HeaderOrV2Pragma: /** @returns {undefined|any} */ (/** @type {any} */ obj) => {
if (Kinds.Map(obj) === undefined) {
return undefined
}
const entries = Object.entries(obj)
/** @type {{[k in string]: any}} */
let ret = obj
let requiredCount = 1
for (let i = 0; i < entries.length; i++) {
const [key, value] = entries[i]
switch (key) {
case 'roots':
{
const v = Reprs['CarV1HeaderOrV2Pragma > roots (anon)'](value)
if (v === undefined) {
return undefined
}
if (v !== value || ret !== obj) {
if (ret === obj) {
/** @type {{[k in string]: any}} */
ret = {}
for (let j = 0; j < i; j++) {
ret[entries[j][0]] = entries[j][1]
}
}
ret.roots = v
}
}
break
case 'version':
{
requiredCount--
const v = Reprs.Int(value)
if (v === undefined) {
return undefined
}
if (v !== value || ret !== obj) {
if (ret === obj) {
/** @type {{[k in string]: any}} */
ret = {}
for (let j = 0; j < i; j++) {
ret[entries[j][0]] = entries[j][1]
}
}
ret.version = v
}
}
break
default:
return undefined
}
}
if (requiredCount > 0) {
return undefined
}
return ret
}
}

export const CarHeader = Types.CarHeader
export const CarV1HeaderOrV2Pragma = {
toTyped: Types.CarV1HeaderOrV2Pragma,
toRepresentation: Reprs.CarV1HeaderOrV2Pragma
}
22 changes: 17 additions & 5 deletions src/header.ipldsch
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
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}
# CarV1HeaderOrV2Pragma is a more relaxed form, and can parse {version:x} where
# roots are optional. This is typically useful for the {verison:2} CARv2
# pragma.

type CarV1HeaderOrV2Pragma struct {
roots optional [&Any]
# roots is _not_ optional for CarV1 but we defer that check within code to
# gracefully handle the V2 case where it's just {version:X}
version Int
}

# CarV1Header is the strict form of the header, and requires roots to be
# present. This is compatible with the CARv1 specification.

# type CarV1Header struct {
# roots [&Any]
# version Int
# }

0 comments on commit cbf7e06

Please sign in to comment.