@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.
Files changed (58) 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/test-examples.js +33 -0
  29. package/lib/coding.ts +17 -2
  30. package/lib/decoder.js +130 -14
  31. package/lib/header-validator.js +33 -0
  32. package/lib/header.ipldsch +6 -0
  33. package/lib/reader-browser.js +11 -11
  34. package/lib/writer-browser.js +1 -1
  35. package/package.json +4 -4
  36. package/test/_fixtures_to_js.mjs +24 -0
  37. package/test/common.js +49 -3
  38. package/test/go.carv2 +0 -0
  39. package/test/test-errors.js +52 -30
  40. package/test/test-indexer.js +24 -1
  41. package/test/test-reader.js +94 -1
  42. package/tsconfig.json +2 -1
  43. package/types/lib/coding.d.ts +14 -4
  44. package/types/lib/coding.d.ts.map +1 -1
  45. package/types/lib/decoder.d.ts +38 -2
  46. package/types/lib/decoder.d.ts.map +1 -1
  47. package/types/lib/header-validator.d.ts +2 -0
  48. package/types/lib/header-validator.d.ts.map +1 -0
  49. package/types/lib/reader-browser.d.ts +15 -7
  50. package/types/lib/reader-browser.d.ts.map +1 -1
  51. package/types/test/_fixtures_to_js.d.mts +3 -0
  52. package/types/test/_fixtures_to_js.d.mts.map +1 -0
  53. package/types/test/common.d.ts +13 -0
  54. package/types/test/common.d.ts.map +1 -1
  55. package/types/test/fixtures-expectations.d.ts +63 -0
  56. package/types/test/fixtures-expectations.d.ts.map +1 -0
  57. package/types/test/fixtures.d.ts +3 -0
  58. package/types/test/fixtures.d.ts.map +1 -0
package/README.md CHANGED
@@ -240,6 +240,12 @@ be directly fed to a
240
240
  * [`async CarWriter.createAppender()`](#CarWriter__createAppender)
241
241
  * [`async CarWriter.updateRootsInBytes(bytes, roots)`](#CarWriter__updateRootsInBytes)
242
242
  * [`async CarWriter.updateRootsInFile(fd, roots)`](#CarWriter__updateRootsInFile)
243
+ * [`async decoder.readHeader(reader)`](#async__decoder__readHeader__reader__)
244
+ * [`async decoder.readBlockHead(reader)`](#async__decoder__readBlockHead__reader__)
245
+ * [`decoder.createDecoder(reader)`](#decoder__createDecoder__reader__)
246
+ * [`decoder.bytesReader(bytes)`](#decoder__bytesReader__bytes__)
247
+ * [`decoder.asyncIterableReader(asyncIterable)`](#decoder__asyncIterableReader__asyncIterable__)
248
+ * [`decoder.limitReader(reader, byteLimit)`](#decoder__limitReader__reader____byteLimit__)
243
249
 
244
250
  <a name="CarReader"></a>
245
251
  ### `class CarReader`
@@ -247,7 +253,7 @@ be directly fed to a
247
253
  Properties:
248
254
 
249
255
  * `version` `(number)`: The version number of the CAR referenced by this
250
- reader (should be `1`).
256
+ reader (should be `1` or `2`).
251
257
 
252
258
  Provides blockstore-like access to a CAR.
253
259
 
@@ -748,7 +754,8 @@ upon successful modification.
748
754
 
749
755
  Update the list of roots in the header of an existing CAR file. The first
750
756
  argument must be a file descriptor for CAR file that is open in read and
751
- write mode (not append).
757
+ write mode (not append), e.g. `fs.open` or `fs.promises.open` with `'r+'`
758
+ mode.
752
759
 
753
760
  This operation is an _overwrite_, the total length of the CAR will not be
754
761
  modified. A rejection will occur if the new header will not be the same
@@ -759,6 +766,71 @@ replaced encode as the same length as the new roots.
759
766
  This function is **only available in Node.js** and not a browser
760
767
  environment.
761
768
 
769
+ <a name="async__decoder__readHeader__reader__"></a>
770
+ ### `async decoder.readHeader(reader)`
771
+
772
+ * `reader` `(BytesReader)`
773
+ * `strictVersion` `(number, optional)`
774
+
775
+ * Returns: `Promise<(CarHeader|CarV2Header)>`
776
+
777
+ Reads header data from a `BytesReader`. The header may either be in the form
778
+ of a `CarHeader` or `CarV2Header` depending on the CAR being read.
779
+
780
+ <a name="async__decoder__readBlockHead__reader__"></a>
781
+ ### `async decoder.readBlockHead(reader)`
782
+
783
+ * `reader` `(BytesReader)`
784
+
785
+ * Returns: `Promise<BlockHeader>`
786
+
787
+ Reads the leading data of an individual block from CAR data from a
788
+ `BytesReader`. Returns a `BlockHeader` object which contains
789
+ `{ cid, length, blockLength }` which can be used to either index the block
790
+ or read the block binary data.
791
+
792
+ <a name="decoder__createDecoder__reader__"></a>
793
+ ### `decoder.createDecoder(reader)`
794
+
795
+ * `reader` `(BytesReader)`
796
+
797
+ * Returns: `CarDecoder`
798
+
799
+ Creates a `CarDecoder` from a `BytesReader`. The `CarDecoder` is as async
800
+ interface that will consume the bytes from the `BytesReader` to yield a
801
+ `header()` and either `blocks()` or `blocksIndex()` data.
802
+
803
+ <a name="decoder__bytesReader__bytes__"></a>
804
+ ### `decoder.bytesReader(bytes)`
805
+
806
+ * `bytes` `(Uint8Array)`
807
+
808
+ * Returns: `BytesReader`
809
+
810
+ Creates a `BytesReader` from a `Uint8Array`.
811
+
812
+ <a name="decoder__asyncIterableReader__asyncIterable__"></a>
813
+ ### `decoder.asyncIterableReader(asyncIterable)`
814
+
815
+ * `asyncIterable` `(AsyncIterable<Uint8Array>)`
816
+
817
+ * Returns: `BytesReader`
818
+
819
+ Creates a `BytesReader` from an `AsyncIterable<Uint8Array>`, which allows for
820
+ consumption of CAR data from a streaming source.
821
+
822
+ <a name="decoder__limitReader__reader____byteLimit__"></a>
823
+ ### `decoder.limitReader(reader, byteLimit)`
824
+
825
+ * `reader` `(BytesReader)`
826
+ * `byteLimit` `(number)`
827
+
828
+ * Returns: `BytesReader`
829
+
830
+ Wraps a `BytesReader` in a limiting `BytesReader` which limits maximum read
831
+ to `byteLimit` bytes. It _does not_ update `pos` of the original
832
+ `BytesReader`.
833
+
762
834
  ## License
763
835
 
764
836
  Licensed under either of
@@ -214,12 +214,87 @@ const goCarIndex = [
214
214
  blockLength: 18
215
215
  }
216
216
  ];
217
+ const goCarV2Bytes = multiformats.bytes.fromHex('0aa16776657273696f6e02000000000000000000000000000000003300000000000000c001000000000000f30100000000000038a265726f6f747381d82a5823001220fb16f5083412ef1371d031ed4aa239903d84efdadf1ba3cd678e6475b1a232f86776657273696f6e01511220fb16f5083412ef1371d031ed4aa239903d84efdadf1ba3cd678e6475b1a232f8122d0a221220d9c0d5376d26f1931f7ad52d7acc00fc1090d2edb0808bf61eeb0a152826f6261204f09f8da418a40185011220d9c0d5376d26f1931f7ad52d7acc00fc1090d2edb0808bf61eeb0a152826f62612310a221220d745b7757f5b4593eeab7820306c7bc64eb496a7410a0d07df7a34ffec4b97f1120962617272656c657965183a122e0a2401551220a2e1c40da1ae335d4dffe729eb4d5ca23b74b9e51fc535f4a804a261080c294d1204f09f90a11807581220d745b7757f5b4593eeab7820306c7bc64eb496a7410a0d07df7a34ffec4b97f112340a2401551220b474a99a2705e23cf905a484ec6d14ef58b56bbe62e9292783466ec363b5072d120a666973686d6f6e67657218042801551220b474a99a2705e23cf905a484ec6d14ef58b56bbe62e9292783466ec363b5072d666973682b01551220a2e1c40da1ae335d4dffe729eb4d5ca23b74b9e51fc535f4a804a261080c294d6c6f62737465720100000028000000c800000000000000a2e1c40da1ae335d4dffe729eb4d5ca23b74b9e51fc535f4a804a261080c294d9401000000000000b474a99a2705e23cf905a484ec6d14ef58b56bbe62e9292783466ec363b5072d6b01000000000000d745b7757f5b4593eeab7820306c7bc64eb496a7410a0d07df7a34ffec4b97f11201000000000000d9c0d5376d26f1931f7ad52d7acc00fc1090d2edb0808bf61eeb0a152826f6268b00000000000000fb16f5083412ef1371d031ed4aa239903d84efdadf1ba3cd678e6475b1a232f83900000000000000');
218
+ const goCarV2Roots = [multiformats.CID.parse('QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z')];
219
+ const goCarV2Index = [
220
+ {
221
+ blockLength: 47,
222
+ blockOffset: 143,
223
+ cid: multiformats.CID.parse('QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z'),
224
+ length: 82,
225
+ offset: 108
226
+ },
227
+ {
228
+ blockLength: 99,
229
+ blockOffset: 226,
230
+ cid: multiformats.CID.parse('QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM'),
231
+ length: 135,
232
+ offset: 190
233
+ },
234
+ {
235
+ blockLength: 54,
236
+ blockOffset: 360,
237
+ cid: multiformats.CID.parse('Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE'),
238
+ length: 89,
239
+ offset: 325
240
+ },
241
+ {
242
+ blockLength: 4,
243
+ blockOffset: 451,
244
+ cid: multiformats.CID.parse('bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu'),
245
+ length: 41,
246
+ offset: 414
247
+ },
248
+ {
249
+ blockLength: 7,
250
+ blockOffset: 492,
251
+ cid: multiformats.CID.parse('bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju'),
252
+ length: 44,
253
+ offset: 455
254
+ }
255
+ ];
256
+ const goCarV2Contents = {
257
+ QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z: {
258
+ Links: [{
259
+ Hash: multiformats.CID.parse('QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM'),
260
+ Name: '\uD83C\uDF64',
261
+ Tsize: 164
262
+ }]
263
+ },
264
+ QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM: {
265
+ Links: [
266
+ {
267
+ Hash: multiformats.CID.parse('Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE'),
268
+ Name: 'barreleye',
269
+ Tsize: 58
270
+ },
271
+ {
272
+ Hash: multiformats.CID.parse('bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju'),
273
+ Name: '\uD83D\uDC21',
274
+ Tsize: 7
275
+ }
276
+ ]
277
+ },
278
+ Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE: {
279
+ Links: [{
280
+ Hash: multiformats.CID.parse('bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu'),
281
+ Name: 'fishmonger',
282
+ Tsize: 4
283
+ }]
284
+ },
285
+ bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu: 'fish',
286
+ bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju: 'lobster'
287
+ };
217
288
 
218
289
  exports.assert = assert;
219
290
  exports.carBytes = carBytes;
220
291
  exports.goCarBytes = goCarBytes;
221
292
  exports.goCarIndex = goCarIndex;
222
293
  exports.goCarRoots = goCarRoots;
294
+ exports.goCarV2Bytes = goCarV2Bytes;
295
+ exports.goCarV2Contents = goCarV2Contents;
296
+ exports.goCarV2Index = goCarV2Index;
297
+ exports.goCarV2Roots = goCarV2Roots;
223
298
  exports.makeData = makeData;
224
299
  exports.makeIterable = makeIterable;
225
300
  exports.rndCid = rndCid;
@@ -26,42 +26,65 @@ describe('Misc errors', () => {
26
26
  });
27
27
  });
28
28
  it('bad version', async () => {
29
- const buf2 = multiformats.bytes.fromHex('0aa16776657273696f6e02');
30
- common.assert.strictEqual(multiformats.bytes.toHex(makeHeader({ version: 2 })), '0aa16776657273696f6e02');
31
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 2');
29
+ const buf2 = multiformats.bytes.fromHex('0aa16776657273696f6e03');
30
+ common.assert.strictEqual(multiformats.bytes.toHex(makeHeader({ version: 3 })), '0aa16776657273696f6e03');
31
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 3');
32
32
  });
33
- it('bad header', async () => {
34
- let buf2 = makeHeader({
35
- version: 1,
36
- roots: []
33
+ describe('bad header', async () => {
34
+ it('sanity check', async () => {
35
+ const buf2 = makeHeader({
36
+ version: 1,
37
+ roots: []
38
+ });
39
+ await common.assert.isFulfilled(readerBrowser.CarReader.fromBytes(buf2));
37
40
  });
38
- await common.assert.isFulfilled(readerBrowser.CarReader.fromBytes(buf2));
39
- buf2 = makeHeader({ roots: [] });
40
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR version: undefined');
41
- buf2 = makeHeader({
42
- version: '1',
43
- roots: []
41
+ it('no \'version\' array', async () => {
42
+ const buf2 = makeHeader({ roots: [] });
43
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
44
44
  });
45
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR version: "1"');
46
- buf2 = makeHeader({ version: 1 });
47
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
48
- buf2 = makeHeader({
49
- version: 1,
50
- roots: {}
45
+ it('bad \'version\' type', async () => {
46
+ const buf2 = makeHeader({
47
+ version: '1',
48
+ roots: []
49
+ });
50
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
51
51
  });
52
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
53
- buf2 = makeHeader({
54
- version: 1,
55
- roots: [],
56
- blip: true
52
+ it('no \'roots\' array', async () => {
53
+ const buf2 = makeHeader({ version: 1 });
54
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
55
+ });
56
+ it('bad \'roots\' type', async () => {
57
+ const buf2 = makeHeader({
58
+ version: 1,
59
+ roots: {}
60
+ });
61
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
62
+ });
63
+ it('extraneous properties', async () => {
64
+ const buf2 = makeHeader({
65
+ version: 1,
66
+ roots: [],
67
+ blip: true
68
+ });
69
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
70
+ });
71
+ it('not an object', async () => {
72
+ const buf2 = makeHeader([
73
+ 1,
74
+ []
75
+ ]);
76
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
77
+ });
78
+ it('not an object', async () => {
79
+ const buf2 = makeHeader(null);
80
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
81
+ });
82
+ it('recursive v2 header', async () => {
83
+ const v2Header = common.goCarV2Bytes.slice(0, 51);
84
+ const buf2 = new Uint8Array(51 * 2);
85
+ buf2.set(v2Header, 0);
86
+ buf2.set(v2Header, 51);
87
+ await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 2 (expected 1)');
57
88
  });
58
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
59
- buf2 = makeHeader([
60
- 1,
61
- []
62
- ]);
63
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
64
- buf2 = makeHeader(null);
65
- await common.assert.isRejected(readerBrowser.CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
66
89
  });
67
90
  });
@@ -15,6 +15,18 @@ describe('CarIndexer fromBytes()', () => {
15
15
  }
16
16
  common.assert.deepStrictEqual(indexData, common.goCarIndex);
17
17
  });
18
+ it('v2 complete', async () => {
19
+ const indexer$1 = await indexer.CarIndexer.fromBytes(common.goCarV2Bytes);
20
+ const roots = await indexer$1.getRoots();
21
+ common.assert.strictEqual(roots.length, 1);
22
+ common.assert(common.goCarV2Roots[0].equals(roots[0]));
23
+ common.assert.strictEqual(indexer$1.version, 2);
24
+ const indexData = [];
25
+ for await (const index of indexer$1) {
26
+ indexData.push(index);
27
+ }
28
+ common.assert.deepStrictEqual(indexData, common.goCarV2Index);
29
+ });
18
30
  it('bad argument', async () => {
19
31
  for (const arg of [
20
32
  true,
@@ -2,11 +2,16 @@
2
2
 
3
3
  var readerBrowser = require('../lib/reader-browser.js');
4
4
  var writerBrowser = require('../lib/writer-browser.js');
5
+ var decoder = require('../lib/decoder.js');
5
6
  var Block = require('multiformats/block');
6
7
  var sha2 = require('multiformats/hashes/sha2');
7
8
  var raw = require('multiformats/codecs/raw');
9
+ var base64 = require('multiformats/bases/base64');
10
+ var dagPb = require('@ipld/dag-pb');
8
11
  var common = require('./common.js');
9
12
  var verifyStoreReader = require('./verify-store-reader.js');
13
+ var fixtures = require('./fixtures.js');
14
+ var fixturesExpectations = require('./fixtures-expectations.js');
10
15
 
11
16
  function _interopNamespace(e) {
12
17
  if (e && e.__esModule) return e;
@@ -28,6 +33,7 @@ function _interopNamespace(e) {
28
33
 
29
34
  var Block__namespace = /*#__PURE__*/_interopNamespace(Block);
30
35
  var raw__namespace = /*#__PURE__*/_interopNamespace(raw);
36
+ var dagPb__namespace = /*#__PURE__*/_interopNamespace(dagPb);
31
37
 
32
38
  describe('CarReader fromBytes()', () => {
33
39
  it('complete', async () => {
@@ -66,6 +72,29 @@ describe('CarReader fromBytes()', () => {
66
72
  message: 'Unexpected end of data'
67
73
  });
68
74
  });
75
+ it('v2 complete', async () => {
76
+ const reader = await readerBrowser.CarReader.fromBytes(common.goCarV2Bytes);
77
+ const roots = await reader.getRoots();
78
+ common.assert.strictEqual(roots.length, 1);
79
+ common.assert(common.goCarV2Roots[0].equals(roots[0]));
80
+ common.assert.strictEqual(reader.version, 2);
81
+ for (const {cid} of common.goCarV2Index) {
82
+ const block = await reader.get(cid);
83
+ common.assert.isDefined(block);
84
+ if (block) {
85
+ common.assert(cid.equals(block.cid));
86
+ let content;
87
+ if (cid.code === dagPb__namespace.code) {
88
+ content = dagPb__namespace.decode(block.bytes);
89
+ } else if (cid.code === 85) {
90
+ content = new TextDecoder().decode(block.bytes);
91
+ } else {
92
+ common.assert.fail('Unexpected codec');
93
+ }
94
+ common.assert.deepStrictEqual(content, common.goCarV2Contents[cid.toString()]);
95
+ }
96
+ }
97
+ });
69
98
  it('decode error - trailing null bytes', async () => {
70
99
  const bytes = new Uint8Array(common.carBytes.length + 5);
71
100
  bytes.set(common.carBytes);
@@ -192,4 +221,58 @@ describe('CarReader fromIterable()', () => {
192
221
  message: 'Unexpected end of data'
193
222
  });
194
223
  });
224
+ it('v2 decode error - truncated', async () => {
225
+ const bytes = common.goCarV2Bytes.slice();
226
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
227
+ dv.setBigUint64(35, BigInt(448 - 10), true);
228
+ await common.assert.isRejected(readerBrowser.CarReader.fromIterable(common.makeIterable(bytes, 64)), {
229
+ name: 'Error',
230
+ message: 'Unexpected end of data'
231
+ });
232
+ });
233
+ });
234
+ describe('Shared fixtures', () => {
235
+ describe('Header', () => {
236
+ for (const [name, {
237
+ version: expectedVersion,
238
+ err: expectedError
239
+ }] of Object.entries(fixturesExpectations.expectations)) {
240
+ it(name, async () => {
241
+ const data = base64.base64.baseDecode(fixtures.data[name]);
242
+ let header;
243
+ try {
244
+ header = await decoder.readHeader(decoder.bytesReader(data));
245
+ } catch (err) {
246
+ if (expectedError != null) {
247
+ common.assert.equal(err.message, expectedError);
248
+ return;
249
+ }
250
+ common.assert.ifError(err);
251
+ }
252
+ if (expectedError != null) {
253
+ common.assert.fail(`Expected error: ${ expectedError }`);
254
+ }
255
+ common.assert.isDefined(header, 'did not decode header');
256
+ if (expectedVersion != null && header != null) {
257
+ common.assert.strictEqual(header.version, expectedVersion);
258
+ }
259
+ });
260
+ }
261
+ });
262
+ describe('Contents', () => {
263
+ for (const [name, {cids: expectedCids}] of Object.entries(fixturesExpectations.expectations)) {
264
+ if (expectedCids == null) {
265
+ continue;
266
+ }
267
+ it(name, async () => {
268
+ const data = base64.base64.baseDecode(fixtures.data[name]);
269
+ const reader = await readerBrowser.CarReader.fromBytes(data);
270
+ let i = 0;
271
+ for await (const cid of reader.cids()) {
272
+ common.assert.strictEqual(cid.toString(), expectedCids[i++]);
273
+ }
274
+ common.assert.strictEqual(i, expectedCids.length);
275
+ });
276
+ }
277
+ });
195
278
  });
@@ -6,6 +6,7 @@ var varint = require('varint');
6
6
  var cid = require('multiformats/cid');
7
7
  var Digest = require('multiformats/hashes/digest');
8
8
  var dagCbor = require('@ipld/dag-cbor');
9
+ var headerValidator = require('./header-validator.js');
9
10
 
10
11
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
11
12
 
@@ -35,13 +36,34 @@ const CIDV0_BYTES = {
35
36
  LENGTH: 32,
36
37
  DAG_PB: 112
37
38
  };
39
+ const V2_HEADER_LENGTH = 16 + 8 + 8 + 8;
38
40
  async function readVarint(reader) {
39
41
  const bytes = await reader.upTo(8);
42
+ if (!bytes.length) {
43
+ throw new Error('Unexpected end of data');
44
+ }
40
45
  const i = varint__default["default"].decode(bytes);
41
46
  reader.seek(varint__default["default"].decode.bytes);
42
47
  return i;
43
48
  }
44
- async function readHeader(reader) {
49
+ async function readV2Header(reader) {
50
+ const bytes = await reader.exactly(V2_HEADER_LENGTH);
51
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
52
+ let offset = 0;
53
+ const header = {
54
+ version: 2,
55
+ characteristics: [
56
+ dv.getBigUint64(offset, true),
57
+ dv.getBigUint64(offset += 8, true)
58
+ ],
59
+ dataOffset: Number(dv.getBigUint64(offset += 8, true)),
60
+ dataSize: Number(dv.getBigUint64(offset += 8, true)),
61
+ indexOffset: Number(dv.getBigUint64(offset += 8, true))
62
+ };
63
+ reader.seek(V2_HEADER_LENGTH);
64
+ return header;
65
+ }
66
+ async function readHeader(reader, strictVersion) {
45
67
  const length = await readVarint(reader);
46
68
  if (length === 0) {
47
69
  throw new Error('Invalid CAR header (zero length)');
@@ -49,22 +71,23 @@ async function readHeader(reader) {
49
71
  const header = await reader.exactly(length);
50
72
  reader.seek(length);
51
73
  const block = dagCbor.decode(header);
52
- if (block == null || Array.isArray(block) || typeof block !== 'object') {
74
+ if (!headerValidator.CarHeader(block)) {
53
75
  throw new Error('Invalid CAR header format');
54
76
  }
55
- if (block.version !== 1) {
56
- if (typeof block.version === 'string') {
57
- throw new Error(`Invalid CAR version: "${ block.version }"`);
58
- }
59
- throw new Error(`Invalid CAR version: ${ block.version }`);
77
+ if (block.version !== 1 && block.version !== 2 || strictVersion !== undefined && block.version !== strictVersion) {
78
+ throw new Error(`Invalid CAR version: ${ block.version }${ strictVersion !== undefined ? ` (expected ${ strictVersion })` : '' }`);
60
79
  }
61
- if (!Array.isArray(block.roots)) {
80
+ const hasRoots = Array.isArray(block.roots);
81
+ if (block.version === 1 && !hasRoots || block.version === 2 && hasRoots) {
62
82
  throw new Error('Invalid CAR header format');
63
83
  }
64
- if (Object.keys(block).filter(p => p !== 'roots' && p !== 'version').length) {
65
- throw new Error('Invalid CAR header format');
84
+ if (block.version === 1) {
85
+ return block;
66
86
  }
67
- return block;
87
+ const v2Header = await readV2Header(reader);
88
+ reader.seek(v2Header.dataOffset - reader.pos);
89
+ const v1Header = await readHeader(reader, 1);
90
+ return Object.assign(v1Header, v2Header);
68
91
  }
69
92
  async function readMultihash(reader) {
70
93
  const bytes = await reader.upTo(8);
@@ -102,7 +125,7 @@ async function readBlockHead(reader) {
102
125
  }
103
126
  length += reader.pos - start;
104
127
  const cid = await readCid(reader);
105
- const blockLength = length - (reader.pos - start);
128
+ const blockLength = length - Number(reader.pos - start);
106
129
  return {
107
130
  cid,
108
131
  length,
@@ -132,7 +155,14 @@ async function readBlockIndex(reader) {
132
155
  return index;
133
156
  }
134
157
  function createDecoder(reader) {
135
- const headerPromise = readHeader(reader);
158
+ const headerPromise = (async () => {
159
+ const header = await readHeader(reader);
160
+ if (header.version === 2) {
161
+ const v1length = reader.pos - header.dataOffset;
162
+ reader = limitReader(reader, header.dataSize - v1length);
163
+ }
164
+ return header;
165
+ })();
136
166
  return {
137
167
  header: () => headerPromise,
138
168
  async *blocks() {
@@ -235,10 +265,37 @@ function asyncIterableReader(asyncIterable) {
235
265
  }
236
266
  return chunkReader(readChunk);
237
267
  }
268
+ function limitReader(reader, byteLimit) {
269
+ let bytesRead = 0;
270
+ return {
271
+ async upTo(length) {
272
+ let bytes = await reader.upTo(length);
273
+ if (bytes.length + bytesRead > byteLimit) {
274
+ bytes = bytes.subarray(0, byteLimit - bytesRead);
275
+ }
276
+ return bytes;
277
+ },
278
+ async exactly(length) {
279
+ const bytes = await reader.exactly(length);
280
+ if (bytes.length + bytesRead > byteLimit) {
281
+ throw new Error('Unexpected end of data');
282
+ }
283
+ return bytes;
284
+ },
285
+ seek(length) {
286
+ bytesRead += length;
287
+ reader.seek(length);
288
+ },
289
+ get pos() {
290
+ return reader.pos;
291
+ }
292
+ };
293
+ }
238
294
 
239
295
  exports.asyncIterableReader = asyncIterableReader;
240
296
  exports.bytesReader = bytesReader;
241
297
  exports.chunkReader = chunkReader;
242
298
  exports.createDecoder = createDecoder;
299
+ exports.limitReader = limitReader;
243
300
  exports.readBlockHead = readBlockHead;
244
301
  exports.readHeader = readHeader;
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ const Kinds = {
6
+ Null: obj => obj === null,
7
+ Int: obj => Number.isInteger(obj),
8
+ Float: obj => typeof obj === 'number' && Number.isFinite(obj),
9
+ String: obj => typeof obj === 'string',
10
+ Bool: obj => typeof obj === 'boolean',
11
+ Bytes: obj => obj instanceof Uint8Array,
12
+ Link: obj => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID === obj,
13
+ List: obj => Array.isArray(obj),
14
+ Map: obj => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID !== obj && !Kinds.List(obj) && !Kinds.Bytes(obj)
15
+ };
16
+ const Types = {
17
+ Int: Kinds.Int,
18
+ 'CarHeader > version': obj => Types.Int(obj),
19
+ 'CarHeader > roots (anon) > valueType (anon)': Kinds.Link,
20
+ 'CarHeader > roots (anon)': obj => Kinds.List(obj) && Array.prototype.every.call(obj, Types['CarHeader > roots (anon) > valueType (anon)']),
21
+ 'CarHeader > roots': obj => Types['CarHeader > roots (anon)'](obj),
22
+ CarHeader: obj => {
23
+ const keys = obj && Object.keys(obj);
24
+ return Kinds.Map(obj) && ['version'].every(k => keys.includes(k)) && Object.entries(obj).every(([name, value]) => Types['CarHeader > ' + name] && Types['CarHeader > ' + name](value));
25
+ }
26
+ };
27
+ const CarHeader = Types.CarHeader;
28
+
29
+ exports.CarHeader = CarHeader;
@@ -5,17 +5,16 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var decoder = require('./decoder.js');
6
6
 
7
7
  class CarReader {
8
- constructor(version, roots, blocks) {
9
- this._version = version;
10
- this._roots = roots;
8
+ constructor(header, blocks) {
9
+ this._header = header;
11
10
  this._blocks = blocks;
12
11
  this._keys = blocks.map(b => b.cid.toString());
13
12
  }
14
13
  get version() {
15
- return this._version;
14
+ return this._header.version;
16
15
  }
17
16
  async getRoots() {
18
- return this._roots;
17
+ return this._header.roots;
19
18
  }
20
19
  async has(key) {
21
20
  return this._keys.indexOf(key.toString()) > -1;
@@ -49,14 +48,15 @@ class CarReader {
49
48
  }
50
49
  async function decodeReaderComplete(reader) {
51
50
  const decoder$1 = decoder.createDecoder(reader);
52
- const {version, roots} = await decoder$1.header();
51
+ const header = await decoder$1.header();
53
52
  const blocks = [];
54
53
  for await (const block of decoder$1.blocks()) {
55
54
  blocks.push(block);
56
55
  }
57
- return new CarReader(version, roots, blocks);
56
+ return new CarReader(header, blocks);
58
57
  }
59
58
  const __browser = true;
60
59
 
61
60
  exports.CarReader = CarReader;
62
61
  exports.__browser = __browser;
62
+ exports.decodeReaderComplete = decodeReaderComplete;
@@ -62,7 +62,7 @@ class CarWriter {
62
62
  const reader = decoder.bytesReader(bytes);
63
63
  await decoder.readHeader(reader);
64
64
  const newHeader = encoder.createHeader(roots);
65
- if (reader.pos !== newHeader.length) {
65
+ if (Number(reader.pos) !== newHeader.length) {
66
66
  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)`);
67
67
  }
68
68
  bytes.set(newHeader, 0);