@ipld/car 3.2.3 → 4.1.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 +183 -2
- package/api.ts +22 -4
- package/buffer-writer +1 -0
- package/cjs/browser-test/common.js +78 -3
- package/cjs/browser-test/node-test-large.js +8 -8
- package/cjs/browser-test/test-buffer-writer.js +330 -0
- package/cjs/browser-test/test-errors.js +57 -34
- package/cjs/browser-test/test-indexer.js +12 -0
- package/cjs/browser-test/test-reader.js +83 -0
- package/cjs/lib/buffer-writer.js +161 -0
- package/cjs/lib/decoder.js +72 -15
- package/cjs/lib/encoder.js +2 -2
- 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 +78 -3
- package/cjs/node-test/node-test-large.js +8 -8
- package/cjs/node-test/test-buffer-writer.js +330 -0
- package/cjs/node-test/test-errors.js +57 -34
- 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-buffer-writer.js +311 -0
- 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/buffer-writer.js +126 -0
- 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-buffer-writer.js +311 -0
- 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/buffer-writer.js +286 -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 +17 -7
- package/test/_fixtures_to_js.mjs +24 -0
- package/test/common.js +49 -3
- package/test/go.carv2 +0 -0
- package/test/test-buffer-writer.js +256 -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 +3 -1
- package/types/api.d.ts +16 -0
- package/types/api.d.ts.map +1 -1
- package/types/lib/buffer-writer.d.ts +86 -0
- package/types/lib/buffer-writer.d.ts.map +1 -0
- 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/types/test/test-buffer-writer.d.ts +2 -0
- package/types/test/test-buffer-writer.d.ts.map +1 -0
|
@@ -4,7 +4,8 @@ import { encode as vEncode } from 'varint';
|
|
|
4
4
|
import { CarReader } from '../lib/reader.js';
|
|
5
5
|
import {
|
|
6
6
|
carBytes,
|
|
7
|
-
assert
|
|
7
|
+
assert,
|
|
8
|
+
goCarV2Bytes
|
|
8
9
|
} from './common.js';
|
|
9
10
|
function makeHeader(block) {
|
|
10
11
|
const u = cbEncode(block);
|
|
@@ -26,42 +27,65 @@ describe('Misc errors', () => {
|
|
|
26
27
|
});
|
|
27
28
|
});
|
|
28
29
|
it('bad version', async () => {
|
|
29
|
-
const buf2 = bytes.fromHex('
|
|
30
|
-
assert.strictEqual(bytes.toHex(makeHeader({ version:
|
|
31
|
-
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version:
|
|
30
|
+
const buf2 = bytes.fromHex('0aa16776657273696f6e03');
|
|
31
|
+
assert.strictEqual(bytes.toHex(makeHeader({ version: 3 })), '0aa16776657273696f6e03');
|
|
32
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 3');
|
|
32
33
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
describe('bad header', async () => {
|
|
35
|
+
it('sanity check', async () => {
|
|
36
|
+
const buf2 = makeHeader({
|
|
37
|
+
version: 1,
|
|
38
|
+
roots: []
|
|
39
|
+
});
|
|
40
|
+
await assert.isFulfilled(CarReader.fromBytes(buf2));
|
|
37
41
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
buf2 = makeHeader({
|
|
42
|
-
version: '1',
|
|
43
|
-
roots: []
|
|
42
|
+
it('no \'version\' array', async () => {
|
|
43
|
+
const buf2 = makeHeader({ roots: [] });
|
|
44
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
44
45
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
it('bad \'version\' type', async () => {
|
|
47
|
+
const buf2 = makeHeader({
|
|
48
|
+
version: '1',
|
|
49
|
+
roots: []
|
|
50
|
+
});
|
|
51
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
51
52
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
it('no \'roots\' array', async () => {
|
|
54
|
+
const buf2 = makeHeader({ version: 1 });
|
|
55
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
56
|
+
});
|
|
57
|
+
it('bad \'roots\' type', async () => {
|
|
58
|
+
const buf2 = makeHeader({
|
|
59
|
+
version: 1,
|
|
60
|
+
roots: {}
|
|
61
|
+
});
|
|
62
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
63
|
+
});
|
|
64
|
+
it('extraneous properties', async () => {
|
|
65
|
+
const buf2 = makeHeader({
|
|
66
|
+
version: 1,
|
|
67
|
+
roots: [],
|
|
68
|
+
blip: true
|
|
69
|
+
});
|
|
70
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
71
|
+
});
|
|
72
|
+
it('not an object', async () => {
|
|
73
|
+
const buf2 = makeHeader([
|
|
74
|
+
1,
|
|
75
|
+
[]
|
|
76
|
+
]);
|
|
77
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
78
|
+
});
|
|
79
|
+
it('not an object', async () => {
|
|
80
|
+
const buf2 = makeHeader(null);
|
|
81
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
82
|
+
});
|
|
83
|
+
it('recursive v2 header', async () => {
|
|
84
|
+
const v2Header = goCarV2Bytes.slice(0, 51);
|
|
85
|
+
const buf2 = new Uint8Array(51 * 2);
|
|
86
|
+
buf2.set(v2Header, 0);
|
|
87
|
+
buf2.set(v2Header, 51);
|
|
88
|
+
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR version: 2 (expected 1)');
|
|
57
89
|
});
|
|
58
|
-
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
59
|
-
buf2 = makeHeader([
|
|
60
|
-
1,
|
|
61
|
-
[]
|
|
62
|
-
]);
|
|
63
|
-
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
64
|
-
buf2 = makeHeader(null);
|
|
65
|
-
await assert.isRejected(CarReader.fromBytes(buf2), Error, 'Invalid CAR header format');
|
|
66
90
|
});
|
|
67
91
|
});
|
|
@@ -2,6 +2,9 @@ import { CarIndexer } from '../lib/indexer.js';
|
|
|
2
2
|
import {
|
|
3
3
|
goCarBytes,
|
|
4
4
|
goCarIndex,
|
|
5
|
+
goCarV2Bytes,
|
|
6
|
+
goCarV2Roots,
|
|
7
|
+
goCarV2Index,
|
|
5
8
|
makeIterable,
|
|
6
9
|
assert
|
|
7
10
|
} from './common.js';
|
|
@@ -17,6 +20,18 @@ describe('CarIndexer fromBytes()', () => {
|
|
|
17
20
|
}
|
|
18
21
|
assert.deepStrictEqual(indexData, goCarIndex);
|
|
19
22
|
});
|
|
23
|
+
it('v2 complete', async () => {
|
|
24
|
+
const indexer = await CarIndexer.fromBytes(goCarV2Bytes);
|
|
25
|
+
const roots = await indexer.getRoots();
|
|
26
|
+
assert.strictEqual(roots.length, 1);
|
|
27
|
+
assert(goCarV2Roots[0].equals(roots[0]));
|
|
28
|
+
assert.strictEqual(indexer.version, 2);
|
|
29
|
+
const indexData = [];
|
|
30
|
+
for await (const index of indexer) {
|
|
31
|
+
indexData.push(index);
|
|
32
|
+
}
|
|
33
|
+
assert.deepStrictEqual(indexData, goCarV2Index);
|
|
34
|
+
});
|
|
20
35
|
it('bad argument', async () => {
|
|
21
36
|
for (const arg of [
|
|
22
37
|
true,
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { CarReader } from '../lib/reader.js';
|
|
2
2
|
import { CarWriter } from '../lib/writer.js';
|
|
3
|
+
import {
|
|
4
|
+
bytesReader,
|
|
5
|
+
readHeader
|
|
6
|
+
} from '../lib/decoder.js';
|
|
3
7
|
import * as Block from 'multiformats/block';
|
|
4
8
|
import { sha256 } from 'multiformats/hashes/sha2';
|
|
5
9
|
import * as raw from 'multiformats/codecs/raw';
|
|
10
|
+
import { base64 } from 'multiformats/bases/base64';
|
|
11
|
+
import * as dagPb from '@ipld/dag-pb';
|
|
6
12
|
import {
|
|
7
13
|
carBytes,
|
|
8
14
|
makeIterable,
|
|
9
|
-
assert
|
|
15
|
+
assert,
|
|
16
|
+
goCarV2Bytes,
|
|
17
|
+
goCarV2Roots,
|
|
18
|
+
goCarV2Index,
|
|
19
|
+
goCarV2Contents
|
|
10
20
|
} from './common.js';
|
|
11
21
|
import {
|
|
12
22
|
verifyRoots,
|
|
@@ -15,6 +25,8 @@ import {
|
|
|
15
25
|
verifyBlocks,
|
|
16
26
|
verifyCids
|
|
17
27
|
} from './verify-store-reader.js';
|
|
28
|
+
import { data as fixtures } from './fixtures.js';
|
|
29
|
+
import { expectations as fixtureExpectations } from './fixtures-expectations.js';
|
|
18
30
|
describe('CarReader fromBytes()', () => {
|
|
19
31
|
it('complete', async () => {
|
|
20
32
|
const reader = await CarReader.fromBytes(carBytes);
|
|
@@ -52,6 +64,29 @@ describe('CarReader fromBytes()', () => {
|
|
|
52
64
|
message: 'Unexpected end of data'
|
|
53
65
|
});
|
|
54
66
|
});
|
|
67
|
+
it('v2 complete', async () => {
|
|
68
|
+
const reader = await CarReader.fromBytes(goCarV2Bytes);
|
|
69
|
+
const roots = await reader.getRoots();
|
|
70
|
+
assert.strictEqual(roots.length, 1);
|
|
71
|
+
assert(goCarV2Roots[0].equals(roots[0]));
|
|
72
|
+
assert.strictEqual(reader.version, 2);
|
|
73
|
+
for (const {cid} of goCarV2Index) {
|
|
74
|
+
const block = await reader.get(cid);
|
|
75
|
+
assert.isDefined(block);
|
|
76
|
+
if (block) {
|
|
77
|
+
assert(cid.equals(block.cid));
|
|
78
|
+
let content;
|
|
79
|
+
if (cid.code === dagPb.code) {
|
|
80
|
+
content = dagPb.decode(block.bytes);
|
|
81
|
+
} else if (cid.code === 85) {
|
|
82
|
+
content = new TextDecoder().decode(block.bytes);
|
|
83
|
+
} else {
|
|
84
|
+
assert.fail('Unexpected codec');
|
|
85
|
+
}
|
|
86
|
+
assert.deepStrictEqual(content, goCarV2Contents[cid.toString()]);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
55
90
|
it('decode error - trailing null bytes', async () => {
|
|
56
91
|
const bytes = new Uint8Array(carBytes.length + 5);
|
|
57
92
|
bytes.set(carBytes);
|
|
@@ -178,4 +213,58 @@ describe('CarReader fromIterable()', () => {
|
|
|
178
213
|
message: 'Unexpected end of data'
|
|
179
214
|
});
|
|
180
215
|
});
|
|
216
|
+
it('v2 decode error - truncated', async () => {
|
|
217
|
+
const bytes = goCarV2Bytes.slice();
|
|
218
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
219
|
+
dv.setBigUint64(35, BigInt(448 - 10), true);
|
|
220
|
+
await assert.isRejected(CarReader.fromIterable(makeIterable(bytes, 64)), {
|
|
221
|
+
name: 'Error',
|
|
222
|
+
message: 'Unexpected end of data'
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('Shared fixtures', () => {
|
|
227
|
+
describe('Header', () => {
|
|
228
|
+
for (const [name, {
|
|
229
|
+
version: expectedVersion,
|
|
230
|
+
err: expectedError
|
|
231
|
+
}] of Object.entries(fixtureExpectations)) {
|
|
232
|
+
it(name, async () => {
|
|
233
|
+
const data = base64.baseDecode(fixtures[name]);
|
|
234
|
+
let header;
|
|
235
|
+
try {
|
|
236
|
+
header = await readHeader(bytesReader(data));
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (expectedError != null) {
|
|
239
|
+
assert.equal(err.message, expectedError);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
assert.ifError(err);
|
|
243
|
+
}
|
|
244
|
+
if (expectedError != null) {
|
|
245
|
+
assert.fail(`Expected error: ${ expectedError }`);
|
|
246
|
+
}
|
|
247
|
+
assert.isDefined(header, 'did not decode header');
|
|
248
|
+
if (expectedVersion != null && header != null) {
|
|
249
|
+
assert.strictEqual(header.version, expectedVersion);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
describe('Contents', () => {
|
|
255
|
+
for (const [name, {cids: expectedCids}] of Object.entries(fixtureExpectations)) {
|
|
256
|
+
if (expectedCids == null) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
it(name, async () => {
|
|
260
|
+
const data = base64.baseDecode(fixtures[name]);
|
|
261
|
+
const reader = await CarReader.fromBytes(data);
|
|
262
|
+
let i = 0;
|
|
263
|
+
for await (const cid of reader.cids()) {
|
|
264
|
+
assert.strictEqual(cid.toString(), expectedCids[i++]);
|
|
265
|
+
}
|
|
266
|
+
assert.strictEqual(i, expectedCids.length);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
});
|
|
181
270
|
});
|
|
@@ -42,10 +42,7 @@ async function run () {
|
|
|
42
42
|
const indexer = await CarIndexer.fromBytes(bytes)
|
|
43
43
|
const reader = await CarReader.fromBytes(bytes)
|
|
44
44
|
const fixture = {
|
|
45
|
-
header:
|
|
46
|
-
roots: await reader.getRoots(),
|
|
47
|
-
version: reader.version
|
|
48
|
-
},
|
|
45
|
+
header: reader._header, // a little naughty but we need gory details
|
|
49
46
|
blocks: []
|
|
50
47
|
}
|
|
51
48
|
let i = 0
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Take a .car file and dump its index in DAG-JSON format, one line per block
|
|
4
|
+
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import { CarIndexer } from '@ipld/car/indexer'
|
|
7
|
+
import * as dagJson from '@ipld/dag-json'
|
|
8
|
+
|
|
9
|
+
if (!process.argv[2]) {
|
|
10
|
+
console.log('Usage: dump-index.js <path/to/car>')
|
|
11
|
+
process.exit(1)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function run () {
|
|
15
|
+
const indexer = await CarIndexer.fromIterable(fs.createReadStream(process.argv[2]))
|
|
16
|
+
for await (const blockIndex of indexer) {
|
|
17
|
+
console.log(new TextDecoder().decode(dagJson.encode(blockIndex)))
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
run().catch((err) => {
|
|
22
|
+
console.error(err)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
})
|
|
@@ -76,6 +76,39 @@ Blocks:
|
|
|
76
76
|
assert.strictEqual(stdout, '{"blocks":[{"blockLength":55,"blockOffset":137,"cid":{"/":"bafyreihyrpefhacm6kkp4ql6j6udakdit7g3dmkzfriqfykhjw6cad5lrm"},"content":{"link":{"/":"QmNX6Tffavsya4xgBi2VJQnSuqy9GsxongxZZ9uZBqp16d"},"name":"blip"},"length":92,"offset":100},{"blockLength":97,"blockOffset":228,"cid":{"/":"QmNX6Tffavsya4xgBi2VJQnSuqy9GsxongxZZ9uZBqp16d"},"content":{"Links":[{"Hash":{"/":"bafkreifw7plhl6mofk6sfvhnfh64qmkq73oeqwl6sloru6rehaoujituke"},"Name":"bear","Tsize":4},{"Hash":{"/":"QmWXZxVQ9yZfhQxLD35eDR8LiMRsYtHxYqTFCBbJoiJVys"},"Name":"second","Tsize":149}]},"length":133,"offset":192},{"blockLength":4,"blockOffset":362,"cid":{"/":"bafkreifw7plhl6mofk6sfvhnfh64qmkq73oeqwl6sloru6rehaoujituke"},"content":{"/":{"bytes":"Y2NjYw"}},"length":41,"offset":325},{"blockLength":94,"blockOffset":402,"cid":{"/":"QmWXZxVQ9yZfhQxLD35eDR8LiMRsYtHxYqTFCBbJoiJVys"},"content":{"Links":[{"Hash":{"/":"bafkreiebzrnroamgos2adnbpgw5apo3z4iishhbdx77gldnbk57d4zdio4"},"Name":"dog","Tsize":4},{"Hash":{"/":"QmdwjhxpxzcMsR3qUuj7vUL8pbA7MgR3GAxWi2GLHjsKCT"},"Name":"first","Tsize":51}]},"length":130,"offset":366},{"blockLength":4,"blockOffset":533,"cid":{"/":"bafkreiebzrnroamgos2adnbpgw5apo3z4iishhbdx77gldnbk57d4zdio4"},"content":{"/":{"bytes":"YmJiYg"}},"length":41,"offset":496},{"blockLength":47,"blockOffset":572,"cid":{"/":"QmdwjhxpxzcMsR3qUuj7vUL8pbA7MgR3GAxWi2GLHjsKCT"},"content":{"Links":[{"Hash":{"/":"bafkreidbxzk2ryxwwtqxem4l3xyyjvw35yu4tcct4cqeqxwo47zhxgxqwq"},"Name":"cat","Tsize":4}]},"length":82,"offset":537},{"blockLength":4,"blockOffset":656,"cid":{"/":"bafkreidbxzk2ryxwwtqxem4l3xyyjvw35yu4tcct4cqeqxwo47zhxgxqwq"},"content":{"/":{"bytes":"YWFhYQ"}},"length":41,"offset":619},{"blockLength":18,"blockOffset":697,"cid":{"/":"bafyreidj5idub6mapiupjwjsyyxhyhedxycv4vihfsicm2vt46o7morwlm"},"content":{"link":null,"name":"limbo"},"length":55,"offset":660}],"header":{"roots":[{"/":"bafyreihyrpefhacm6kkp4ql6j6udakdit7g3dmkzfriqfykhjw6cad5lrm"},{"/":"bafyreidj5idub6mapiupjwjsyyxhyhedxycv4vihfsicm2vt46o7morwlm"}],"version":1}}\n')
|
|
77
77
|
console.log('\u001b[32m✔\u001b[39m [example] car-to-fixture ../test/go.car')
|
|
78
78
|
})
|
|
79
|
+
}).then(async () => {
|
|
80
|
+
await runExample('dump-index', ['example.car']).then(({ stdout, stderr }) => {
|
|
81
|
+
assert.strictEqual(stderr, '')
|
|
82
|
+
assert.strictEqual(stdout, '{"blockLength":24,"blockOffset":96,"cid":{"/":"bafkreihwkf6mtnjobdqrkiksr7qhp6tiiqywux64aylunbvmfhzeql2coa"},"length":61,"offset":59}\n')
|
|
83
|
+
console.log('\u001b[32m✔\u001b[39m [example] dump-index example.car')
|
|
84
|
+
})
|
|
85
|
+
}).then(async () => {
|
|
86
|
+
await runExample('dump-index', ['../test/go.car']).then(({ stdout, stderr }) => {
|
|
87
|
+
assert.strictEqual(stderr, '')
|
|
88
|
+
assert.strictEqual(stdout,
|
|
89
|
+
`{"blockLength":55,"blockOffset":137,"cid":{"/":"bafyreihyrpefhacm6kkp4ql6j6udakdit7g3dmkzfriqfykhjw6cad5lrm"},"length":92,"offset":100}
|
|
90
|
+
{"blockLength":97,"blockOffset":228,"cid":{"/":"QmNX6Tffavsya4xgBi2VJQnSuqy9GsxongxZZ9uZBqp16d"},"length":133,"offset":192}
|
|
91
|
+
{"blockLength":4,"blockOffset":362,"cid":{"/":"bafkreifw7plhl6mofk6sfvhnfh64qmkq73oeqwl6sloru6rehaoujituke"},"length":41,"offset":325}
|
|
92
|
+
{"blockLength":94,"blockOffset":402,"cid":{"/":"QmWXZxVQ9yZfhQxLD35eDR8LiMRsYtHxYqTFCBbJoiJVys"},"length":130,"offset":366}
|
|
93
|
+
{"blockLength":4,"blockOffset":533,"cid":{"/":"bafkreiebzrnroamgos2adnbpgw5apo3z4iishhbdx77gldnbk57d4zdio4"},"length":41,"offset":496}
|
|
94
|
+
{"blockLength":47,"blockOffset":572,"cid":{"/":"QmdwjhxpxzcMsR3qUuj7vUL8pbA7MgR3GAxWi2GLHjsKCT"},"length":82,"offset":537}
|
|
95
|
+
{"blockLength":4,"blockOffset":656,"cid":{"/":"bafkreidbxzk2ryxwwtqxem4l3xyyjvw35yu4tcct4cqeqxwo47zhxgxqwq"},"length":41,"offset":619}
|
|
96
|
+
{"blockLength":18,"blockOffset":697,"cid":{"/":"bafyreidj5idub6mapiupjwjsyyxhyhedxycv4vihfsicm2vt46o7morwlm"},"length":55,"offset":660}
|
|
97
|
+
`)
|
|
98
|
+
console.log('\u001b[32m✔\u001b[39m [example] dump-index ../test/go.carv2')
|
|
99
|
+
})
|
|
100
|
+
}).then(async () => {
|
|
101
|
+
await runExample('dump-index', ['../test/go.carv2']).then(({ stdout, stderr }) => {
|
|
102
|
+
assert.strictEqual(stderr, '')
|
|
103
|
+
assert.strictEqual(stdout,
|
|
104
|
+
`{"blockLength":47,"blockOffset":143,"cid":{"/":"QmfEoLyB5NndqeKieExd1rtJzTduQUPEV8TwAYcUiy3H5Z"},"length":82,"offset":108}
|
|
105
|
+
{"blockLength":99,"blockOffset":226,"cid":{"/":"QmczfirA7VEH7YVvKPTPoU69XM3qY4DC39nnTsWd4K3SkM"},"length":135,"offset":190}
|
|
106
|
+
{"blockLength":54,"blockOffset":360,"cid":{"/":"Qmcpz2FHJD7VAhg1fxFXdYJKePtkx1BsHuCrAgWVnaHMTE"},"length":89,"offset":325}
|
|
107
|
+
{"blockLength":4,"blockOffset":451,"cid":{"/":"bafkreifuosuzujyf4i6psbneqtwg2fhplc2wxptc5euspa2gn3bwhnihfu"},"length":41,"offset":414}
|
|
108
|
+
{"blockLength":7,"blockOffset":492,"cid":{"/":"bafkreifc4hca3inognou377hfhvu2xfchn2ltzi7yu27jkaeujqqqdbjju"},"length":44,"offset":455}
|
|
109
|
+
`)
|
|
110
|
+
console.log('\u001b[32m✔\u001b[39m [example] dump-index ../test/go.carv2')
|
|
111
|
+
})
|
|
79
112
|
}).catch((err) => {
|
|
80
113
|
console.error(err.stack)
|
|
81
114
|
process.exit(1)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import varint from 'varint'
|
|
2
|
+
import { Token, Type } from 'cborg'
|
|
3
|
+
import { tokensToLength } from 'cborg/length'
|
|
4
|
+
import * as CBOR from '@ipld/dag-cbor'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {import('../api').CID} CID
|
|
8
|
+
* @typedef {import('../api').Block} Block
|
|
9
|
+
* @typedef {import('../api').CarBufferWriter} Writer
|
|
10
|
+
* @typedef {import('../api').CarBufferWriterOptions} Options
|
|
11
|
+
* @typedef {import('./coding').CarEncoder} CarEncoder
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A simple CAR writer that writes to a pre-allocated buffer.
|
|
16
|
+
*
|
|
17
|
+
* @class
|
|
18
|
+
* @name CarBufferWriter
|
|
19
|
+
* @implements {Writer}
|
|
20
|
+
*/
|
|
21
|
+
class CarBufferWriter {
|
|
22
|
+
/**
|
|
23
|
+
* @param {Uint8Array} bytes
|
|
24
|
+
* @param {number} headerSize
|
|
25
|
+
*/
|
|
26
|
+
constructor (bytes, headerSize) {
|
|
27
|
+
/** @readonly */
|
|
28
|
+
this.bytes = bytes
|
|
29
|
+
this.byteOffset = headerSize
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @readonly
|
|
33
|
+
* @type {CID[]}
|
|
34
|
+
*/
|
|
35
|
+
this.roots = []
|
|
36
|
+
this.headerSize = headerSize
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Add a root to this writer, to be used to create a header when the CAR is
|
|
41
|
+
* finalized with {@link CarBufferWriter.close `close()`}
|
|
42
|
+
*
|
|
43
|
+
* @param {CID} root
|
|
44
|
+
* @param {{resize?:boolean}} [options]
|
|
45
|
+
* @returns {CarBufferWriter}
|
|
46
|
+
*/
|
|
47
|
+
addRoot (root, options) {
|
|
48
|
+
addRoot(this, root, options)
|
|
49
|
+
return this
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Write a `Block` (a `{ cid:CID, bytes:Uint8Array }` pair) to the archive.
|
|
54
|
+
* Throws if there is not enough capacity.
|
|
55
|
+
*
|
|
56
|
+
* @param {Block} block A `{ cid:CID, bytes:Uint8Array }` pair.
|
|
57
|
+
* @returns {CarBufferWriter}
|
|
58
|
+
*/
|
|
59
|
+
write (block) {
|
|
60
|
+
addBlock(this, block)
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Finalize the CAR and return it as a `Uint8Array`.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} [options]
|
|
68
|
+
* @param {boolean} [options.resize]
|
|
69
|
+
* @returns {Uint8Array}
|
|
70
|
+
*/
|
|
71
|
+
close (options) {
|
|
72
|
+
return close(this, options)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {CarBufferWriter} writer
|
|
78
|
+
* @param {CID} root
|
|
79
|
+
* @param {{resize?:boolean}} [options]
|
|
80
|
+
*/
|
|
81
|
+
export const addRoot = (writer, root, { resize = false } = {}) => {
|
|
82
|
+
const { bytes, headerSize, byteOffset, roots } = writer
|
|
83
|
+
writer.roots.push(root)
|
|
84
|
+
const size = headerLength(writer)
|
|
85
|
+
// If there is not enough space for the new root
|
|
86
|
+
if (size > headerSize) {
|
|
87
|
+
// Check if we root would fit if we were to resize the head.
|
|
88
|
+
if (size - headerSize + byteOffset < bytes.byteLength) {
|
|
89
|
+
// If resize is enabled resize head
|
|
90
|
+
if (resize) {
|
|
91
|
+
resizeHeader(writer, size)
|
|
92
|
+
// otherwise remove head and throw an error suggesting to resize
|
|
93
|
+
} else {
|
|
94
|
+
roots.pop()
|
|
95
|
+
throw new RangeError(`Header of size ${headerSize} has no capacity for new root ${root}.
|
|
96
|
+
However there is a space in the buffer and you could call addRoot(root, { resize: root }) to resize header to make a space for this root.`)
|
|
97
|
+
}
|
|
98
|
+
// If head would not fit even with resize pop new root and throw error
|
|
99
|
+
} else {
|
|
100
|
+
roots.pop()
|
|
101
|
+
throw new RangeError(`Buffer has no capacity for a new root ${root}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Calculates number of bytes required for storing given block in CAR. Useful in
|
|
108
|
+
* estimating size of an `ArrayBuffer` for the `CarBufferWriter`.
|
|
109
|
+
*
|
|
110
|
+
* @name CarBufferWriter.blockLength(Block)
|
|
111
|
+
* @param {Block} block
|
|
112
|
+
* @returns {number}
|
|
113
|
+
*/
|
|
114
|
+
export const blockLength = ({ cid, bytes }) => {
|
|
115
|
+
const size = cid.bytes.byteLength + bytes.byteLength
|
|
116
|
+
return varint.encodingLength(size) + size
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {CarBufferWriter} writer
|
|
121
|
+
* @param {Block} block
|
|
122
|
+
*/
|
|
123
|
+
export const addBlock = (writer, { cid, bytes }) => {
|
|
124
|
+
const byteLength = cid.bytes.byteLength + bytes.byteLength
|
|
125
|
+
const size = varint.encode(byteLength)
|
|
126
|
+
if (writer.byteOffset + size.length + byteLength > writer.bytes.byteLength) {
|
|
127
|
+
throw new RangeError('Buffer has no capacity for this block')
|
|
128
|
+
} else {
|
|
129
|
+
writeBytes(writer, size)
|
|
130
|
+
writeBytes(writer, cid.bytes)
|
|
131
|
+
writeBytes(writer, bytes)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {CarBufferWriter} writer
|
|
137
|
+
* @param {object} [options]
|
|
138
|
+
* @param {boolean} [options.resize]
|
|
139
|
+
*/
|
|
140
|
+
export const close = (writer, { resize = false } = {}) => {
|
|
141
|
+
const { roots, bytes, byteOffset, headerSize } = writer
|
|
142
|
+
|
|
143
|
+
const headerBytes = CBOR.encode({ version: 1, roots })
|
|
144
|
+
const varintBytes = varint.encode(headerBytes.length)
|
|
145
|
+
|
|
146
|
+
const size = varintBytes.length + headerBytes.byteLength
|
|
147
|
+
const offset = headerSize - size
|
|
148
|
+
|
|
149
|
+
// If header size estimate was accurate we just write header and return
|
|
150
|
+
// view into buffer.
|
|
151
|
+
if (offset === 0) {
|
|
152
|
+
writeHeader(writer, varintBytes, headerBytes)
|
|
153
|
+
return bytes.subarray(0, byteOffset)
|
|
154
|
+
// If header was overestimated and `{resize: true}` is passed resize header
|
|
155
|
+
} else if (resize) {
|
|
156
|
+
resizeHeader(writer, size)
|
|
157
|
+
writeHeader(writer, varintBytes, headerBytes)
|
|
158
|
+
return bytes.subarray(0, writer.byteOffset)
|
|
159
|
+
} else {
|
|
160
|
+
throw new RangeError(`Header size was overestimated.
|
|
161
|
+
You can use close({ resize: true }) to resize header`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @param {CarBufferWriter} writer
|
|
167
|
+
* @param {number} byteLength
|
|
168
|
+
*/
|
|
169
|
+
export const resizeHeader = (writer, byteLength) => {
|
|
170
|
+
const { bytes, headerSize } = writer
|
|
171
|
+
// Move data section to a new offset
|
|
172
|
+
bytes.set(bytes.subarray(headerSize, writer.byteOffset), byteLength)
|
|
173
|
+
// Update header size & byteOffset
|
|
174
|
+
writer.byteOffset += byteLength - headerSize
|
|
175
|
+
writer.headerSize = byteLength
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {CarBufferWriter} writer
|
|
180
|
+
* @param {number[]|Uint8Array} bytes
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
const writeBytes = (writer, bytes) => {
|
|
184
|
+
writer.bytes.set(bytes, writer.byteOffset)
|
|
185
|
+
writer.byteOffset += bytes.length
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* @param {{bytes:Uint8Array}} writer
|
|
189
|
+
* @param {number[]} varint
|
|
190
|
+
* @param {Uint8Array} header
|
|
191
|
+
*/
|
|
192
|
+
const writeHeader = ({ bytes }, varint, header) => {
|
|
193
|
+
bytes.set(varint)
|
|
194
|
+
bytes.set(header, varint.length)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const headerPreludeTokens = [
|
|
198
|
+
new Token(Type.map, 2),
|
|
199
|
+
new Token(Type.string, 'version'),
|
|
200
|
+
new Token(Type.uint, 1),
|
|
201
|
+
new Token(Type.string, 'roots')
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
const CID_TAG = new Token(Type.tag, 42)
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Calculates header size given the array of byteLength for roots.
|
|
208
|
+
*
|
|
209
|
+
* @name CarBufferWriter.calculateHeaderLength(rootLengths)
|
|
210
|
+
* @param {number[]} rootLengths
|
|
211
|
+
* @returns {number}
|
|
212
|
+
*/
|
|
213
|
+
export const calculateHeaderLength = (rootLengths) => {
|
|
214
|
+
const tokens = [...headerPreludeTokens]
|
|
215
|
+
tokens.push(new Token(Type.array, rootLengths.length))
|
|
216
|
+
for (const rootLength of rootLengths) {
|
|
217
|
+
tokens.push(CID_TAG)
|
|
218
|
+
tokens.push(new Token(Type.bytes, { length: rootLength + 1 }))
|
|
219
|
+
}
|
|
220
|
+
const length = tokensToLength(tokens) // no options needed here because we have simple tokens
|
|
221
|
+
return varint.encodingLength(length) + length
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Calculates header size given the array of roots.
|
|
226
|
+
*
|
|
227
|
+
* @name CarBufferWriter.headerLength({ roots })
|
|
228
|
+
* @param {object} options
|
|
229
|
+
* @param {CID[]} options.roots
|
|
230
|
+
* @returns {number}
|
|
231
|
+
*/
|
|
232
|
+
export const headerLength = ({ roots }) =>
|
|
233
|
+
calculateHeaderLength(roots.map(cid => cid.bytes.byteLength))
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Estimates header size given a count of the roots and the expected byte length
|
|
237
|
+
* of the root CIDs. The default length works for a standard CIDv1 with a
|
|
238
|
+
* single-byte multihash code, such as SHA2-256 (i.e. the most common CIDv1).
|
|
239
|
+
*
|
|
240
|
+
* @name CarBufferWriter.estimateHeaderLength(rootCount[, rootByteLength])
|
|
241
|
+
* @param {number} rootCount
|
|
242
|
+
* @param {number} [rootByteLength]
|
|
243
|
+
* @returns {number}
|
|
244
|
+
*/
|
|
245
|
+
export const estimateHeaderLength = (rootCount, rootByteLength = 36) =>
|
|
246
|
+
calculateHeaderLength(new Array(rootCount).fill(rootByteLength))
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Creates synchronous CAR writer that can be used to encode blocks into a given
|
|
250
|
+
* buffer. Optionally you could pass `byteOffset` and `byteLength` to specify a
|
|
251
|
+
* range inside buffer to write into. If car file is going to have `roots` you
|
|
252
|
+
* need to either pass them under `options.roots` (from which header size will
|
|
253
|
+
* be calculated) or provide `options.headerSize` to allocate required space
|
|
254
|
+
* in the buffer. You may also provide known `roots` and `headerSize` to
|
|
255
|
+
* allocate space for the roots that may not be known ahead of time.
|
|
256
|
+
*
|
|
257
|
+
* Note: Incorrect `headerSize` may lead to copying bytes inside a buffer
|
|
258
|
+
* which will have a negative impact on performance.
|
|
259
|
+
*
|
|
260
|
+
* @name CarBufferWriter.createWriter(buffer[, options])
|
|
261
|
+
* @param {ArrayBuffer} buffer
|
|
262
|
+
* @param {object} [options]
|
|
263
|
+
* @param {CID[]} [options.roots]
|
|
264
|
+
* @param {number} [options.byteOffset]
|
|
265
|
+
* @param {number} [options.byteLength]
|
|
266
|
+
* @param {number} [options.headerSize]
|
|
267
|
+
* @returns {CarBufferWriter}
|
|
268
|
+
*/
|
|
269
|
+
export const createWriter = (
|
|
270
|
+
buffer,
|
|
271
|
+
{
|
|
272
|
+
roots = [],
|
|
273
|
+
byteOffset = 0,
|
|
274
|
+
byteLength = buffer.byteLength,
|
|
275
|
+
headerSize = headerLength({ roots })
|
|
276
|
+
} = {}
|
|
277
|
+
) => {
|
|
278
|
+
const bytes = new Uint8Array(buffer, byteOffset, byteLength)
|
|
279
|
+
|
|
280
|
+
const writer = new CarBufferWriter(bytes, headerSize)
|
|
281
|
+
for (const root of roots) {
|
|
282
|
+
writer.addRoot(root)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return writer
|
|
286
|
+
}
|
package/lib/coding.ts
CHANGED
|
@@ -20,10 +20,25 @@ export interface IteratorChannel<T> {
|
|
|
20
20
|
iterator: AsyncIterator<T>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export
|
|
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
|
|