@ipld/car 3.2.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +74 -2
  2. package/cjs/browser-test/common.js +75 -0
  3. package/cjs/browser-test/test-errors.js +55 -32
  4. package/cjs/browser-test/test-indexer.js +12 -0
  5. package/cjs/browser-test/test-reader.js +83 -0
  6. package/cjs/lib/decoder.js +70 -13
  7. package/cjs/lib/header-validator.js +29 -0
  8. package/cjs/lib/reader-browser.js +7 -7
  9. package/cjs/lib/writer-browser.js +1 -1
  10. package/cjs/node-test/common.js +75 -0
  11. package/cjs/node-test/test-errors.js +55 -32
  12. package/cjs/node-test/test-indexer.js +12 -0
  13. package/cjs/node-test/test-reader.js +83 -0
  14. package/esm/browser-test/common.js +76 -1
  15. package/esm/browser-test/test-errors.js +57 -33
  16. package/esm/browser-test/test-indexer.js +15 -0
  17. package/esm/browser-test/test-reader.js +90 -1
  18. package/esm/lib/decoder.js +69 -13
  19. package/esm/lib/header-validator.js +23 -0
  20. package/esm/lib/reader-browser.js +7 -8
  21. package/esm/lib/writer-browser.js +1 -1
  22. package/esm/node-test/common.js +76 -1
  23. package/esm/node-test/test-errors.js +57 -33
  24. package/esm/node-test/test-indexer.js +15 -0
  25. package/esm/node-test/test-reader.js +90 -1
  26. package/examples/car-to-fixture.js +1 -4
  27. package/examples/dump-index.js +24 -0
  28. package/examples/package.json +1 -1
  29. package/examples/test-examples.js +33 -0
  30. package/lib/coding.ts +17 -2
  31. package/lib/decoder.js +130 -14
  32. package/lib/header-validator.js +33 -0
  33. package/lib/header.ipldsch +6 -0
  34. package/lib/reader-browser.js +11 -11
  35. package/lib/writer-browser.js +1 -1
  36. package/package.json +7 -7
  37. package/test/_fixtures_to_js.mjs +24 -0
  38. package/test/common.js +49 -3
  39. package/test/go.carv2 +0 -0
  40. package/test/test-errors.js +52 -30
  41. package/test/test-indexer.js +24 -1
  42. package/test/test-reader.js +94 -1
  43. package/tsconfig.json +2 -1
  44. package/types/lib/coding.d.ts +14 -4
  45. package/types/lib/coding.d.ts.map +1 -1
  46. package/types/lib/decoder.d.ts +38 -2
  47. package/types/lib/decoder.d.ts.map +1 -1
  48. package/types/lib/header-validator.d.ts +2 -0
  49. package/types/lib/header-validator.d.ts.map +1 -0
  50. package/types/lib/reader-browser.d.ts +15 -7
  51. package/types/lib/reader-browser.d.ts.map +1 -1
  52. package/types/test/_fixtures_to_js.d.mts +3 -0
  53. package/types/test/_fixtures_to_js.d.mts.map +1 -0
  54. package/types/test/common.d.ts +13 -0
  55. package/types/test/common.d.ts.map +1 -1
  56. package/types/test/fixtures-expectations.d.ts +63 -0
  57. package/types/test/fixtures-expectations.d.ts.map +1 -0
  58. package/types/test/fixtures.d.ts +3 -0
  59. package/types/test/fixtures.d.ts.map +1 -0
package/lib/coding.ts CHANGED
@@ -20,10 +20,25 @@ export interface IteratorChannel<T> {
20
20
  iterator: AsyncIterator<T>
21
21
  }
22
22
 
23
- export type CarHeader = { version: number, roots: CID[] }
23
+ export interface CarHeader {
24
+ version: 1,
25
+ roots: CID[]
26
+ }
27
+
28
+ export interface CarV2FixedHeader {
29
+ characteristics: [bigint, bigint],
30
+ dataOffset: number,
31
+ dataSize: number,
32
+ indexOffset: number
33
+ }
34
+
35
+ export interface CarV2Header extends CarV2FixedHeader {
36
+ version: 2,
37
+ roots: CID[],
38
+ }
24
39
 
25
40
  export interface CarDecoder {
26
- header(): Promise<CarHeader>
41
+ header(): Promise<CarHeader|CarV2Header>
27
42
 
28
43
  blocks(): AsyncGenerator<Block>
29
44
 
package/lib/decoder.js CHANGED
@@ -2,6 +2,7 @@ import varint from 'varint'
2
2
  import { CID } from 'multiformats/cid'
3
3
  import * as Digest from 'multiformats/hashes/digest'
4
4
  import { decode as decodeDagCbor } from '@ipld/dag-cbor'
5
+ import { CarHeader as headerValidator } from './header-validator.js'
5
6
 
6
7
  /**
7
8
  * @typedef {import('../api').Block} Block
@@ -9,6 +10,8 @@ import { decode as decodeDagCbor } from '@ipld/dag-cbor'
9
10
  * @typedef {import('../api').BlockIndex} BlockIndex
10
11
  * @typedef {import('./coding').BytesReader} BytesReader
11
12
  * @typedef {import('./coding').CarHeader} CarHeader
13
+ * @typedef {import('./coding').CarV2Header} CarV2Header
14
+ * @typedef {import('./coding').CarV2FixedHeader} CarV2FixedHeader
12
15
  * @typedef {import('./coding').CarDecoder} CarDecoder
13
16
  */
14
17
 
@@ -18,12 +21,17 @@ const CIDV0_BYTES = {
18
21
  DAG_PB: 0x70
19
22
  }
20
23
 
24
+ const V2_HEADER_LENGTH = /* characteristics */ 16 /* v1 offset */ + 8 /* v1 size */ + 8 /* index offset */ + 8
25
+
21
26
  /**
22
27
  * @param {BytesReader} reader
23
28
  * @returns {Promise<number>}
24
29
  */
25
30
  async function readVarint (reader) {
26
31
  const bytes = await reader.upTo(8)
32
+ if (!bytes.length) {
33
+ throw new Error('Unexpected end of data')
34
+ }
27
35
  const i = varint.decode(bytes)
28
36
  reader.seek(varint.decode.bytes)
29
37
  return i
@@ -33,9 +41,40 @@ async function readVarint (reader) {
33
41
 
34
42
  /**
35
43
  * @param {BytesReader} reader
36
- * @returns {Promise<CarHeader>}
44
+ * @returns {Promise<CarV2FixedHeader>}
45
+ */
46
+ async function readV2Header (reader) {
47
+ /** @type {Uint8Array} */
48
+ const bytes = await reader.exactly(V2_HEADER_LENGTH)
49
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
50
+ let offset = 0
51
+ const header = {
52
+ version: 2,
53
+ /** @type {[bigint, bigint]} */
54
+ characteristics: [
55
+ dv.getBigUint64(offset, true),
56
+ dv.getBigUint64(offset += 8, true)
57
+ ],
58
+ dataOffset: Number(dv.getBigUint64(offset += 8, true)),
59
+ dataSize: Number(dv.getBigUint64(offset += 8, true)),
60
+ indexOffset: Number(dv.getBigUint64(offset += 8, true))
61
+ }
62
+ reader.seek(V2_HEADER_LENGTH)
63
+ return header
64
+ /* c8 ignore next 2 */
65
+ // Node.js 12 c8 bug
66
+ }
67
+
68
+ /**
69
+ * Reads header data from a `BytesReader`. The header may either be in the form
70
+ * of a `CarHeader` or `CarV2Header` depending on the CAR being read.
71
+ *
72
+ * @name async decoder.readHeader(reader)
73
+ * @param {BytesReader} reader
74
+ * @param {number} [strictVersion]
75
+ * @returns {Promise<CarHeader|CarV2Header>}
37
76
  */
38
- export async function readHeader (reader) {
77
+ export async function readHeader (reader, strictVersion) {
39
78
  const length = await readVarint(reader)
40
79
  if (length === 0) {
41
80
  throw new Error('Invalid CAR header (zero length)')
@@ -43,22 +82,26 @@ export async function readHeader (reader) {
43
82
  const header = await reader.exactly(length)
44
83
  reader.seek(length)
45
84
  const block = decodeDagCbor(header)
46
- if (block == null || Array.isArray(block) || typeof block !== 'object') {
85
+ if (!headerValidator(block)) {
47
86
  throw new Error('Invalid CAR header format')
48
87
  }
49
- if (block.version !== 1) {
50
- if (typeof block.version === 'string') {
51
- throw new Error(`Invalid CAR version: "${block.version}"`)
52
- }
53
- throw new Error(`Invalid CAR version: ${block.version}`)
88
+ if ((block.version !== 1 && block.version !== 2) || (strictVersion !== undefined && block.version !== strictVersion)) {
89
+ throw new Error(`Invalid CAR version: ${block.version}${strictVersion !== undefined ? ` (expected ${strictVersion})` : ''}`)
54
90
  }
55
- if (!Array.isArray(block.roots)) {
91
+ // we've made 'roots' optional in the schema so we can do the version check
92
+ // before rejecting the block as invalid if there is no version
93
+ const hasRoots = Array.isArray(block.roots)
94
+ if ((block.version === 1 && !hasRoots) || (block.version === 2 && hasRoots)) {
56
95
  throw new Error('Invalid CAR header format')
57
96
  }
58
- if (Object.keys(block).filter((p) => p !== 'roots' && p !== 'version').length) {
59
- throw new Error('Invalid CAR header format')
97
+ if (block.version === 1) {
98
+ return block
60
99
  }
61
- return block
100
+ // version 2
101
+ const v2Header = await readV2Header(reader)
102
+ reader.seek(v2Header.dataOffset - reader.pos)
103
+ const v1Header = await readHeader(reader, 1)
104
+ return Object.assign(v1Header, v2Header)
62
105
  /* c8 ignore next 2 */
63
106
  // Node.js 12 c8 bug
64
107
  }
@@ -112,6 +155,12 @@ async function readCid (reader) {
112
155
  }
113
156
 
114
157
  /**
158
+ * Reads the leading data of an individual block from CAR data from a
159
+ * `BytesReader`. Returns a `BlockHeader` object which contains
160
+ * `{ cid, length, blockLength }` which can be used to either index the block
161
+ * or read the block binary data.
162
+ *
163
+ * @name async decoder.readBlockHead(reader)
115
164
  * @param {BytesReader} reader
116
165
  * @returns {Promise<BlockHeader>}
117
166
  */
@@ -125,7 +174,7 @@ export async function readBlockHead (reader) {
125
174
  }
126
175
  length += (reader.pos - start)
127
176
  const cid = await readCid(reader)
128
- const blockLength = length - (reader.pos - start) // subtract CID length
177
+ const blockLength = length - Number(reader.pos - start) // subtract CID length
129
178
 
130
179
  return { cid, length, blockLength }
131
180
  /* c8 ignore next 2 */
@@ -160,11 +209,25 @@ async function readBlockIndex (reader) {
160
209
  }
161
210
 
162
211
  /**
212
+ * Creates a `CarDecoder` from a `BytesReader`. The `CarDecoder` is as async
213
+ * interface that will consume the bytes from the `BytesReader` to yield a
214
+ * `header()` and either `blocks()` or `blocksIndex()` data.
215
+ *
216
+ * @name decoder.createDecoder(reader)
163
217
  * @param {BytesReader} reader
164
218
  * @returns {CarDecoder}
165
219
  */
166
220
  export function createDecoder (reader) {
167
- const headerPromise = readHeader(reader)
221
+ const headerPromise = (async () => {
222
+ const header = await readHeader(reader)
223
+ if (header.version === 2) {
224
+ const v1length = reader.pos - header.dataOffset
225
+ reader = limitReader(reader, header.dataSize - v1length)
226
+ }
227
+ return header
228
+ /* c8 ignore next 2 */
229
+ // Node.js 12 c8 bug
230
+ })()
168
231
 
169
232
  return {
170
233
  header: () => headerPromise,
@@ -186,6 +249,9 @@ export function createDecoder (reader) {
186
249
  }
187
250
 
188
251
  /**
252
+ * Creates a `BytesReader` from a `Uint8Array`.
253
+ *
254
+ * @name decoder.bytesReader(bytes)
189
255
  * @param {Uint8Array} bytes
190
256
  * @returns {BytesReader}
191
257
  */
@@ -298,6 +364,10 @@ export function chunkReader (readChunk /*, closer */) {
298
364
  }
299
365
 
300
366
  /**
367
+ * Creates a `BytesReader` from an `AsyncIterable<Uint8Array>`, which allows for
368
+ * consumption of CAR data from a streaming source.
369
+ *
370
+ * @name decoder.asyncIterableReader(asyncIterable)
301
371
  * @param {AsyncIterable<Uint8Array>} asyncIterable
302
372
  * @returns {BytesReader}
303
373
  */
@@ -316,3 +386,49 @@ export function asyncIterableReader (asyncIterable) {
316
386
 
317
387
  return chunkReader(readChunk)
318
388
  }
389
+
390
+ /**
391
+ * Wraps a `BytesReader` in a limiting `BytesReader` which limits maximum read
392
+ * to `byteLimit` bytes. It _does not_ update `pos` of the original
393
+ * `BytesReader`.
394
+ *
395
+ * @name decoder.limitReader(reader, byteLimit)
396
+ * @param {BytesReader} reader
397
+ * @param {number} byteLimit
398
+ * @returns {BytesReader}
399
+ */
400
+ export function limitReader (reader, byteLimit) {
401
+ let bytesRead = 0
402
+
403
+ /** @type {BytesReader} */
404
+ return {
405
+ async upTo (length) {
406
+ let bytes = await reader.upTo(length)
407
+ if (bytes.length + bytesRead > byteLimit) {
408
+ bytes = bytes.subarray(0, byteLimit - bytesRead)
409
+ }
410
+ return bytes
411
+ /* c8 ignore next 2 */
412
+ // Node.js 12 c8 bug
413
+ },
414
+
415
+ async exactly (length) {
416
+ const bytes = await reader.exactly(length)
417
+ if (bytes.length + bytesRead > byteLimit) {
418
+ throw new Error('Unexpected end of data')
419
+ }
420
+ return bytes
421
+ /* c8 ignore next 2 */
422
+ // Node.js 12 c8 bug
423
+ },
424
+
425
+ seek (length) {
426
+ bytesRead += length
427
+ reader.seek(length)
428
+ },
429
+
430
+ get pos () {
431
+ return reader.pos
432
+ }
433
+ }
434
+ }
@@ -0,0 +1,33 @@
1
+ /** Auto-generated with ipld-schema-validator@0.0.0-dev at Thu Jun 17 2021 from IPLD Schema:
2
+ *
3
+ * type CarHeader struct {
4
+ * version Int
5
+ * roots optional [&Any]
6
+ * # roots is _not_ optional for CarV1 but we defer that check within code to
7
+ * # gracefully handle the >V1 case where it's just {version:X}
8
+ * }
9
+ *
10
+ */
11
+
12
+ const Kinds = {
13
+ Null: /** @returns {boolean} */ (/** @type {any} */ obj) => obj === null,
14
+ Int: /** @returns {boolean} */ (/** @type {any} */ obj) => Number.isInteger(obj),
15
+ Float: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'number' && Number.isFinite(obj),
16
+ String: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'string',
17
+ Bool: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'boolean',
18
+ Bytes: /** @returns {boolean} */ (/** @type {any} */ obj) => obj instanceof Uint8Array,
19
+ Link: /** @returns {boolean} */ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID === obj,
20
+ List: /** @returns {boolean} */ (/** @type {any} */ obj) => Array.isArray(obj),
21
+ Map: /** @returns {boolean} */ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID !== obj && !Kinds.List(obj) && !Kinds.Bytes(obj)
22
+ }
23
+ /** @type {{ [k in string]: (obj:any)=>boolean}} */
24
+ const Types = {
25
+ Int: Kinds.Int,
26
+ 'CarHeader > version': /** @returns {boolean} */ (/** @type {any} */ obj) => Types.Int(obj),
27
+ 'CarHeader > roots (anon) > valueType (anon)': Kinds.Link,
28
+ 'CarHeader > roots (anon)': /** @returns {boolean} */ (/** @type {any} */ obj) => Kinds.List(obj) && Array.prototype.every.call(obj, Types['CarHeader > roots (anon) > valueType (anon)']),
29
+ 'CarHeader > roots': /** @returns {boolean} */ (/** @type {any} */ obj) => Types['CarHeader > roots (anon)'](obj),
30
+ 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)) }
31
+ }
32
+
33
+ export const CarHeader = Types.CarHeader
@@ -0,0 +1,6 @@
1
+ type CarHeader struct {
2
+ version Int
3
+ roots optional [&Any]
4
+ # roots is _not_ optional for CarV1 but we defer that check within code to
5
+ # gracefully handle the >V1 case where it's just {version:X}
6
+ }
@@ -5,6 +5,8 @@ import { asyncIterableReader, bytesReader, createDecoder } from './decoder.js'
5
5
  * @typedef {import('../api').Block} Block
6
6
  * @typedef {import('../api').CarReader} CarReaderIface
7
7
  * @typedef {import('./coding').BytesReader} BytesReader
8
+ * @typedef {import('./coding').CarHeader} CarHeader
9
+ * @typedef {import('./coding').CarV2Header} CarV2Header
8
10
  */
9
11
 
10
12
  /**
@@ -26,18 +28,16 @@ import { asyncIterableReader, bytesReader, createDecoder } from './decoder.js'
26
28
  * @class
27
29
  * @implements {CarReaderIface}
28
30
  * @property {number} version The version number of the CAR referenced by this
29
- * reader (should be `1`).
31
+ * reader (should be `1` or `2`).
30
32
  */
31
33
  export class CarReader {
32
34
  /**
33
35
  * @constructs CarReader
34
- * @param {number} version
35
- * @param {CID[]} roots
36
+ * @param {CarHeader|CarV2Header} header
36
37
  * @param {Block[]} blocks
37
38
  */
38
- constructor (version, roots, blocks) {
39
- this._version = version
40
- this._roots = roots
39
+ constructor (header, blocks) {
40
+ this._header = header
41
41
  this._blocks = blocks
42
42
  this._keys = blocks.map((b) => b.cid.toString())
43
43
  }
@@ -48,7 +48,7 @@ export class CarReader {
48
48
  * @instance
49
49
  */
50
50
  get version () {
51
- return this._version
51
+ return this._header.version
52
52
  }
53
53
 
54
54
  /**
@@ -62,7 +62,7 @@ export class CarReader {
62
62
  * @returns {Promise<CID[]>}
63
63
  */
64
64
  async getRoots () {
65
- return this._roots
65
+ return this._header.roots
66
66
  /* c8 ignore next 2 */
67
67
  // Node.js 12 c8 bug
68
68
  }
@@ -190,15 +190,15 @@ export class CarReader {
190
190
  * @param {BytesReader} reader
191
191
  * @returns {Promise<CarReader>}
192
192
  */
193
- async function decodeReaderComplete (reader) {
193
+ export async function decodeReaderComplete (reader) {
194
194
  const decoder = createDecoder(reader)
195
- const { version, roots } = await decoder.header()
195
+ const header = await decoder.header()
196
196
  const blocks = []
197
197
  for await (const block of decoder.blocks()) {
198
198
  blocks.push(block)
199
199
  }
200
200
 
201
- return new CarReader(version, roots, blocks)
201
+ return new CarReader(header, blocks)
202
202
  /* c8 ignore next 2 */
203
203
  // Node.js 12 c8 bug
204
204
  }
@@ -176,7 +176,7 @@ export class CarWriter {
176
176
  const reader = bytesReader(bytes)
177
177
  await readHeader(reader)
178
178
  const newHeader = createHeader(roots)
179
- if (reader.pos !== newHeader.length) {
179
+ if (Number(reader.pos) !== newHeader.length) {
180
180
  throw new Error(`updateRoots() can only overwrite a header of the same length (old header is ${reader.pos} bytes, new header is ${newHeader.length} bytes)`)
181
181
  }
182
182
  bytes.set(newHeader, 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ipld/car",
3
- "version": "3.2.2",
3
+ "version": "4.0.0",
4
4
  "description": "Content Addressable aRchive format reader and writer",
5
5
  "main": "./cjs/car.js",
6
6
  "types": "./types/car.d.ts",
@@ -8,17 +8,17 @@
8
8
  "lint": "standard",
9
9
  "build": "npm run build:js && npm run build:types",
10
10
  "build:js": "ipjs build --tests --main && npm run build:copy",
11
- "build:copy": "mkdir -p dist/examples/ && cp -a tsconfig.json *.js *.ts lib test dist/ && cp examples/*.* dist/examples/",
11
+ "build:copy": "mkdir -p dist/examples/ && cp -a tsconfig.json .npmignore *.js *.ts lib test dist/ && cp examples/*.* dist/examples/ && rm -rf dist/test/fixtures/",
12
12
  "build:types": "tsc --build && mv types dist",
13
13
  "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",
14
14
  "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",
15
- "test:node": "c8 --check-coverage --branches 100 --functions 100 --lines 100 mocha test/test-*.js test/node-test-*.js",
15
+ "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",
16
16
  "test:cjs:browser": "polendina --page --worker --serviceworker --cleanup dist/cjs/browser-test/test-*.js",
17
17
  "test:esm:browser": "polendina --page --worker --serviceworker --cleanup dist/esm/browser-test/test-*.js",
18
18
  "test": "npm run lint && npm run test:node && npm run test:cjs && npm run test --prefix examples/",
19
19
  "test:ci": "npm run lint && npm run test:node && npm run test:esm && npm run test:cjs && npm run test --prefix examples/",
20
20
  "coverage": "c8 --reporter=html --reporter=text mocha test/test-*.js && npx st -d coverage -p 8888",
21
- "docs": "jsdoc4readme --readme --description-only lib/reader*.js lib/indexed-reader.js lib/iterator.js lib/indexer.js lib/writer*.js"
21
+ "docs": "jsdoc4readme --readme --description-only lib/reader*.js lib/indexed-reader.js lib/iterator.js lib/indexer.js lib/writer*.js lib/decoder.js"
22
22
  },
23
23
  "keywords": [
24
24
  "car",
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "dependencies": {
69
- "@ipld/dag-cbor": "^6.0.15",
69
+ "@ipld/dag-cbor": "^7.0.0",
70
70
  "multiformats": "^9.5.4",
71
71
  "varint": "^6.0.0"
72
72
  },
@@ -75,7 +75,7 @@
75
75
  "@types/chai": "^4.3.0",
76
76
  "@types/chai-as-promised": "^7.1.4",
77
77
  "@types/mocha": "^9.0.0",
78
- "@types/node": "^16.11.12",
78
+ "@types/node": "^17.0.0",
79
79
  "@types/varint": "^6.0.0",
80
80
  "@typescript-eslint/eslint-plugin": "^5.6.0",
81
81
  "@typescript-eslint/parser": "^5.6.0",
@@ -88,7 +88,7 @@
88
88
  "mocha": "^9.1.3",
89
89
  "polendina": "~2.0.1",
90
90
  "standard": "^16.0.4",
91
- "typescript": "~4.5.2"
91
+ "typescript": "~4.6.2"
92
92
  },
93
93
  "standard": {
94
94
  "ignore": [
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readdir, readFile, writeFile } from 'fs/promises'
4
+ import { dirname, join } from 'path'
5
+
6
+ async function main () {
7
+ const thisdir = dirname(new URL(import.meta.url).pathname)
8
+ const outfile = join(thisdir, 'fixtures.js')
9
+ const fixturesdir = join(thisdir, 'fixtures')
10
+ const files = await readdir(fixturesdir)
11
+ let content = '/** @type {Record<string, string>} */\nexport const data = {\n'
12
+ for (const f of files) {
13
+ content += ` '${f}': '`
14
+ content += (await readFile(join(fixturesdir, f))).toString('base64')
15
+ content += '\',\n'
16
+ }
17
+ content += ' _: \'\'\n}\n'
18
+ await writeFile(join(outfile), content, 'utf8')
19
+ }
20
+
21
+ main().catch((err) => {
22
+ console.error(err)
23
+ process.exit(1)
24
+ })
package/test/common.js CHANGED
@@ -132,14 +132,13 @@ function makeIterable (data, chunkSize) {
132
132
  }
133
133
 
134
134
  const carBytes = bytes.fromHex('63a265726f6f747382d82a58250001711220f88bc853804cf294fe417e4fa83028689fcdb1b1592c5102e1474dbc200fab8bd82a5825000171122069ea0740f9807a28f4d932c62e7c1c83be055e55072c90266ab3e79df63a365b6776657273696f6e01280155122061be55a8e2f6b4e172338bddf184d6dbee29c98853e0a0485ecee7f27b9af0b461616161280155122081cc5b17018674b401b42f35ba07bb79e211239c23bffe658da1577e3e646877626262622801551220b6fbd675f98e2abd22d4ed29fdc83150fedc48597e92dd1a7a24381d44a2745163636363511220e7dc486e97e6ebe5cdabab3e392bdad128b6e09acc94bb4e2aa2af7b986d24d0122d0a240155122061be55a8e2f6b4e172338bddf184d6dbee29c98853e0a0485ecee7f27b9af0b4120363617418048001122079a982de3c9907953d4d323cee1d0fb1ed8f45f8ef02870c0cb9e09246bd530a122d0a240155122081cc5b17018674b401b42f35ba07bb79e211239c23bffe658da1577e3e6468771203646f671804122d0a221220e7dc486e97e6ebe5cdabab3e392bdad128b6e09acc94bb4e2aa2af7b986d24d01205666972737418338301122002acecc5de2438ea4126a3010ecb1f8a599c8eff22fff1a1dcffe999b27fd3de122e0a2401551220b6fbd675f98e2abd22d4ed29fdc83150fedc48597e92dd1a7a24381d44a274511204626561721804122f0a22122079a982de3c9907953d4d323cee1d0fb1ed8f45f8ef02870c0cb9e09246bd530a12067365636f6e641895015b01711220f88bc853804cf294fe417e4fa83028689fcdb1b1592c5102e1474dbc200fab8ba2646c696e6bd82a582300122002acecc5de2438ea4126a3010ecb1f8a599c8eff22fff1a1dcffe999b27fd3de646e616d6564626c6970360171122069ea0740f9807a28f4d932c62e7c1c83be055e55072c90266ab3e79df63a365ba2646c696e6bf6646e616d65656c696d626f')
135
+
135
136
  // go.car is written as a graph, not by the allBlocks ordering here, so ordering is slightly out
136
137
  const goCarBytes = bytes.fromHex('63a265726f6f747382d82a58250001711220f88bc853804cf294fe417e4fa83028689fcdb1b1592c5102e1474dbc200fab8bd82a5825000171122069ea0740f9807a28f4d932c62e7c1c83be055e55072c90266ab3e79df63a365b6776657273696f6e015b01711220f88bc853804cf294fe417e4fa83028689fcdb1b1592c5102e1474dbc200fab8ba2646c696e6bd82a582300122002acecc5de2438ea4126a3010ecb1f8a599c8eff22fff1a1dcffe999b27fd3de646e616d6564626c69708301122002acecc5de2438ea4126a3010ecb1f8a599c8eff22fff1a1dcffe999b27fd3de122e0a2401551220b6fbd675f98e2abd22d4ed29fdc83150fedc48597e92dd1a7a24381d44a274511204626561721804122f0a22122079a982de3c9907953d4d323cee1d0fb1ed8f45f8ef02870c0cb9e09246bd530a12067365636f6e641895012801551220b6fbd675f98e2abd22d4ed29fdc83150fedc48597e92dd1a7a24381d44a27451636363638001122079a982de3c9907953d4d323cee1d0fb1ed8f45f8ef02870c0cb9e09246bd530a122d0a240155122081cc5b17018674b401b42f35ba07bb79e211239c23bffe658da1577e3e6468771203646f671804122d0a221220e7dc486e97e6ebe5cdabab3e392bdad128b6e09acc94bb4e2aa2af7b986d24d0120566697273741833280155122081cc5b17018674b401b42f35ba07bb79e211239c23bffe658da1577e3e64687762626262511220e7dc486e97e6ebe5cdabab3e392bdad128b6e09acc94bb4e2aa2af7b986d24d0122d0a240155122061be55a8e2f6b4e172338bddf184d6dbee29c98853e0a0485ecee7f27b9af0b412036361741804280155122061be55a8e2f6b4e172338bddf184d6dbee29c98853e0a0485ecee7f27b9af0b461616161360171122069ea0740f9807a28f4d932c62e7c1c83be055e55072c90266ab3e79df63a365ba2646c696e6bf6646e616d65656c696d626f')
137
-
138
138
  const goCarRoots = [
139
139
  CID.parse('bafyreihyrpefhacm6kkp4ql6j6udakdit7g3dmkzfriqfykhjw6cad5lrm'),
140
140
  CID.parse('bafyreidj5idub6mapiupjwjsyyxhyhedxycv4vihfsicm2vt46o7morwlm')
141
141
  ]
142
-
143
142
  const goCarIndex = [
144
143
  { cid: CID.parse('bafyreihyrpefhacm6kkp4ql6j6udakdit7g3dmkzfriqfykhjw6cad5lrm'), offset: 100, length: 92, blockOffset: 137, blockLength: 55 },
145
144
  { cid: CID.parse('QmNX6Tffavsya4xgBi2VJQnSuqy9GsxongxZZ9uZBqp16d'), offset: 192, length: 133, blockOffset: 228, blockLength: 97 },
@@ -151,6 +150,49 @@ const goCarIndex = [
151
150
  { cid: CID.parse('bafyreidj5idub6mapiupjwjsyyxhyhedxycv4vihfsicm2vt46o7morwlm'), offset: 660, length: 55, blockOffset: 697, blockLength: 18 }
152
151
  ]
153
152
 
153
+ const goCarV2Bytes = bytes.fromHex('0aa16776657273696f6e02000000000000000000000000000000003300000000000000c001000000000000f30100000000000038a265726f6f747381d82a5823001220fb16f5083412ef1371d031ed4aa239903d84efdadf1ba3cd678e6475b1a232f86776657273696f6e01511220fb16f5083412ef1371d031ed4aa239903d84efdadf1ba3cd678e6475b1a232f8122d0a221220d9c0d5376d26f1931f7ad52d7acc00fc1090d2edb0808bf61eeb0a152826f6261204f09f8da418a40185011220d9c0d5376d26f1931f7ad52d7acc00fc1090d2edb0808bf61eeb0a152826f62612310a221220d745b7757f5b4593eeab7820306c7bc64eb496a7410a0d07df7a34ffec4b97f1120962617272656c657965183a122e0a2401551220a2e1c40da1ae335d4dffe729eb4d5ca23b74b9e51fc535f4a804a261080c294d1204f09f90a11807581220d745b7757f5b4593eeab7820306c7bc64eb496a7410a0d07df7a34ffec4b97f112340a2401551220b474a99a2705e23cf905a484ec6d14ef58b56bbe62e9292783466ec363b5072d120a666973686d6f6e67657218042801551220b474a99a2705e23cf905a484ec6d14ef58b56bbe62e9292783466ec363b5072d666973682b01551220a2e1c40da1ae335d4dffe729eb4d5ca23b74b9e51fc535f4a804a261080c294d6c6f62737465720100000028000000c800000000000000a2e1c40da1ae335d4dffe729eb4d5ca23b74b9e51fc535f4a804a261080c294d9401000000000000b474a99a2705e23cf905a484ec6d14ef58b56bbe62e9292783466ec363b5072d6b01000000000000d745b7757f5b4593eeab7820306c7bc64eb496a7410a0d07df7a34ffec4b97f11201000000000000d9c0d5376d26f1931f7ad52d7acc00fc1090d2edb0808bf61eeb0a152826f6268b00000000000000fb16f5083412ef1371d031ed4aa239903d84efdadf1ba3cd678e6475b1a232f83900000000000000')
154
+ const goCarV2Roots = [CID.parse('QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z')]
155
+ const goCarV2Index = [
156
+ { blockLength: 47, blockOffset: 143, cid: CID.parse('QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z'), length: 82, offset: 108 },
157
+ { blockLength: 99, blockOffset: 226, cid: CID.parse('QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM'), length: 135, offset: 190 },
158
+ { blockLength: 54, blockOffset: 360, cid: CID.parse('Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE'), length: 89, offset: 325 },
159
+ { blockLength: 4, blockOffset: 451, cid: CID.parse('bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu'), length: 41, offset: 414 },
160
+ { blockLength: 7, blockOffset: 492, cid: CID.parse('bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju'), length: 44, offset: 455 }
161
+ ]
162
+ /** @type {{[k in string]: any}} */
163
+ const goCarV2Contents = {
164
+ QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z: {
165
+ Links: [{
166
+ Hash: CID.parse('QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM'),
167
+ Name: '🍤',
168
+ Tsize: 164
169
+ }]
170
+ },
171
+ QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM: {
172
+ Links: [
173
+ {
174
+ Hash: CID.parse('Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE'),
175
+ Name: 'barreleye',
176
+ Tsize: 58
177
+ },
178
+ {
179
+ Hash: CID.parse('bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju'),
180
+ Name: '🐡',
181
+ Tsize: 7
182
+ }
183
+ ]
184
+ },
185
+ Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE: {
186
+ Links: [{
187
+ Hash: CID.parse('bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu'),
188
+ Name: 'fishmonger',
189
+ Tsize: 4
190
+ }]
191
+ },
192
+ bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu: 'fish',
193
+ bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju: 'lobster'
194
+ }
195
+
154
196
  export {
155
197
  toBlock,
156
198
  assert,
@@ -160,5 +202,9 @@ export {
160
202
  carBytes,
161
203
  goCarBytes,
162
204
  goCarRoots,
163
- goCarIndex
205
+ goCarIndex,
206
+ goCarV2Bytes,
207
+ goCarV2Roots,
208
+ goCarV2Index,
209
+ goCarV2Contents
164
210
  }
package/test/go.carv2 ADDED
Binary file
@@ -3,7 +3,7 @@ import { bytes } from 'multiformats'
3
3
  import { encode as cbEncode } from '@ipld/dag-cbor'
4
4
  import { encode as vEncode } from 'varint'
5
5
  import { CarReader } from '@ipld/car/reader'
6
- import { carBytes, assert } from './common.js'
6
+ import { carBytes, assert, goCarV2Bytes } from './common.js'
7
7
 
8
8
  /**
9
9
  * @param {any} block
@@ -34,43 +34,65 @@ describe('Misc errors', () => {
34
34
 
35
35
  it('bad version', async () => {
36
36
  // quick sanity check that makeHeader() works properly!
37
- const buf2 = bytes.fromHex('0aa16776657273696f6e02')
38
- // {version:2} - fixed string, likely to be used by CARv2 to escape header parsing rules
39
- assert.strictEqual(bytes.toHex(makeHeader({ version: 2 })), '0aa16776657273696f6e02')
40
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 2')
37
+ const buf2 = bytes.fromHex('0aa16776657273696f6e03')
38
+ assert.strictEqual(bytes.toHex(makeHeader({ version: 3 })), '0aa16776657273696f6e03')
39
+ // {version:3}
40
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 3')
41
41
  })
42
42
 
43
- it('bad header', async () => {
44
- // sanity check, this should be fine
45
- let buf2 = makeHeader({ version: 1, roots: [] })
46
- await assert.isFulfilled(CarReader.fromBytes(buf2))
43
+ describe('bad header', async () => {
44
+ it('sanity check', async () => {
45
+ // sanity check, this should be fine
46
+ const buf2 = makeHeader({ version: 1, roots: [] })
47
+ await assert.isFulfilled(CarReader.fromBytes(buf2))
48
+ })
47
49
 
48
- // no 'version' array
49
- buf2 = makeHeader({ roots: [] })
50
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: undefined')
50
+ it('no \'version\' array', async () => {
51
+ const buf2 = makeHeader({ roots: [] })
52
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
53
+ })
51
54
 
52
- // bad 'version' type
53
- buf2 = makeHeader({ version: '1', roots: [] })
54
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: "1"')
55
+ it('bad \'version\' type', async () => {
56
+ const buf2 = makeHeader({ version: '1', roots: [] })
57
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
58
+ })
59
+
60
+ it('no \'roots\' array', async () => {
61
+ const buf2 = makeHeader({ version: 1 })
62
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
63
+ })
55
64
 
56
- // no 'roots' array
57
- buf2 = makeHeader({ version: 1 })
58
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
65
+ it('bad \'roots\' type', async () => {
66
+ const buf2 = makeHeader({ version: 1, roots: {} })
67
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
68
+ })
59
69
 
60
- // bad 'roots' type
61
- buf2 = makeHeader({ version: 1, roots: {} })
62
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
70
+ it('extraneous properties', async () => {
71
+ const buf2 = makeHeader({ version: 1, roots: [], blip: true })
72
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
73
+ })
63
74
 
64
- // extraneous properties
65
- buf2 = makeHeader({ version: 1, roots: [], blip: true })
66
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
75
+ it('not an object', async () => {
76
+ const buf2 = makeHeader([1, []])
77
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
78
+ })
67
79
 
68
- // not an object
69
- buf2 = makeHeader([1, []])
70
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
80
+ it('not an object', async () => {
81
+ const buf2 = makeHeader(null)
82
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
83
+ })
71
84
 
72
- // not an object
73
- buf2 = makeHeader(null)
74
- await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format')
85
+ it('recursive v2 header', async () => {
86
+ // first 51 bytes are the carv2 header:
87
+ // 11b prefix, 16b characteristics, 8b data offset, 8b data size, 8b index offset
88
+ const v2Header = goCarV2Bytes.slice(0, 51)
89
+ // parser should expect to get a carv1 header at the data offset, but it uses the same
90
+ // code to check the carv2 header so let's make sure it doesn't allow recursive carv2
91
+ // headers
92
+ const buf2 = new Uint8Array(51 * 2)
93
+ buf2.set(v2Header, 0)
94
+ buf2.set(v2Header, 51)
95
+ await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 2 (expected 1)')
96
+ })
75
97
  })
76
98
  })