Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: use new schema validator compiler, add both forms of schema #153

Merged
merged 1 commit into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
# }