@ipld/car 3.2.4 → 4.1.1
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/browser-test/test-writer.js +3 -3
- 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/cjs/node-test/test-writer.js +3 -3
- 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/browser-test/test-writer.js +3 -3
- 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/esm/node-test/test-writer.js +3 -3
- 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 +16 -6
- 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/test/test-writer.js +3 -3
- 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
|
@@ -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
|
|
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)
|