@ipld/car 3.2.4 → 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.
- package/README.md +74 -2
- package/cjs/browser-test/common.js +75 -0
- package/cjs/browser-test/test-errors.js +55 -32
- package/cjs/browser-test/test-indexer.js +12 -0
- package/cjs/browser-test/test-reader.js +83 -0
- package/cjs/lib/decoder.js +70 -13
- package/cjs/lib/header-validator.js +29 -0
- package/cjs/lib/reader-browser.js +7 -7
- package/cjs/lib/writer-browser.js +1 -1
- package/cjs/node-test/common.js +75 -0
- package/cjs/node-test/test-errors.js +55 -32
- package/cjs/node-test/test-indexer.js +12 -0
- package/cjs/node-test/test-reader.js +83 -0
- package/esm/browser-test/common.js +76 -1
- package/esm/browser-test/test-errors.js +57 -33
- package/esm/browser-test/test-indexer.js +15 -0
- package/esm/browser-test/test-reader.js +90 -1
- package/esm/lib/decoder.js +69 -13
- package/esm/lib/header-validator.js +23 -0
- package/esm/lib/reader-browser.js +7 -8
- package/esm/lib/writer-browser.js +1 -1
- package/esm/node-test/common.js +76 -1
- package/esm/node-test/test-errors.js +57 -33
- package/esm/node-test/test-indexer.js +15 -0
- package/esm/node-test/test-reader.js +90 -1
- package/examples/car-to-fixture.js +1 -4
- package/examples/dump-index.js +24 -0
- package/examples/test-examples.js +33 -0
- package/lib/coding.ts +17 -2
- package/lib/decoder.js +130 -14
- package/lib/header-validator.js +33 -0
- package/lib/header.ipldsch +6 -0
- package/lib/reader-browser.js +11 -11
- package/lib/writer-browser.js +1 -1
- package/package.json +4 -4
- package/test/_fixtures_to_js.mjs +24 -0
- package/test/common.js +49 -3
- package/test/go.carv2 +0 -0
- package/test/test-errors.js +52 -30
- package/test/test-indexer.js +24 -1
- package/test/test-reader.js +94 -1
- package/tsconfig.json +2 -1
- package/types/lib/coding.d.ts +14 -4
- package/types/lib/coding.d.ts.map +1 -1
- package/types/lib/decoder.d.ts +38 -2
- package/types/lib/decoder.d.ts.map +1 -1
- package/types/lib/header-validator.d.ts +2 -0
- package/types/lib/header-validator.d.ts.map +1 -0
- package/types/lib/reader-browser.d.ts +15 -7
- package/types/lib/reader-browser.d.ts.map +1 -1
- package/types/test/_fixtures_to_js.d.mts +3 -0
- package/types/test/_fixtures_to_js.d.mts.map +1 -0
- package/types/test/common.d.ts +13 -0
- package/types/test/common.d.ts.map +1 -1
- package/types/test/fixtures-expectations.d.ts +63 -0
- package/types/test/fixtures-expectations.d.ts.map +1 -0
- package/types/test/fixtures.d.ts +3 -0
- package/types/test/fixtures.d.ts.map +1 -0
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<
|
|
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 (
|
|
85
|
+
if (!headerValidator(block)) {
|
|
47
86
|
throw new Error('Invalid CAR header format')
|
|
48
87
|
}
|
|
49
|
-
if (block.version !== 1) {
|
|
50
|
-
|
|
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
|
-
|
|
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 (
|
|
59
|
-
|
|
97
|
+
if (block.version === 1) {
|
|
98
|
+
return block
|
|
60
99
|
}
|
|
61
|
-
|
|
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 =
|
|
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
|
package/lib/reader-browser.js
CHANGED
|
@@ -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 {
|
|
35
|
-
* @param {CID[]} roots
|
|
36
|
+
* @param {CarHeader|CarV2Header} header
|
|
36
37
|
* @param {Block[]} blocks
|
|
37
38
|
*/
|
|
38
|
-
constructor (
|
|
39
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
|
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(
|
|
201
|
+
return new CarReader(header, blocks)
|
|
202
202
|
/* c8 ignore next 2 */
|
|
203
203
|
// Node.js 12 c8 bug
|
|
204
204
|
}
|
package/lib/writer-browser.js
CHANGED
|
@@ -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
|
+
"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",
|
|
@@ -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
|
package/test/test-errors.js
CHANGED
|
@@ -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('
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version:
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
})
|
package/test/test-indexer.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
/* eslint-env mocha */
|
|
2
2
|
|
|
3
3
|
import { CarIndexer } from '@ipld/car/indexer'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
goCarBytes,
|
|
6
|
+
goCarIndex,
|
|
7
|
+
goCarV2Bytes,
|
|
8
|
+
goCarV2Roots,
|
|
9
|
+
goCarV2Index,
|
|
10
|
+
makeIterable,
|
|
11
|
+
assert
|
|
12
|
+
} from './common.js'
|
|
5
13
|
import { verifyRoots } from './verify-store-reader.js'
|
|
6
14
|
|
|
7
15
|
describe('CarIndexer fromBytes()', () => {
|
|
@@ -18,6 +26,21 @@ describe('CarIndexer fromBytes()', () => {
|
|
|
18
26
|
assert.deepStrictEqual(indexData, goCarIndex)
|
|
19
27
|
})
|
|
20
28
|
|
|
29
|
+
it('v2 complete', async () => {
|
|
30
|
+
const indexer = await CarIndexer.fromBytes(goCarV2Bytes)
|
|
31
|
+
const roots = await indexer.getRoots()
|
|
32
|
+
assert.strictEqual(roots.length, 1)
|
|
33
|
+
assert(goCarV2Roots[0].equals(roots[0]))
|
|
34
|
+
assert.strictEqual(indexer.version, 2)
|
|
35
|
+
|
|
36
|
+
const indexData = []
|
|
37
|
+
for await (const index of indexer) {
|
|
38
|
+
indexData.push(index)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
assert.deepStrictEqual(indexData, goCarV2Index)
|
|
42
|
+
})
|
|
43
|
+
|
|
21
44
|
it('bad argument', async () => {
|
|
22
45
|
for (const arg of [true, false, null, undefined, 'string', 100, { obj: 'nope' }]) {
|
|
23
46
|
// @ts-ignore
|