@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.
Files changed (82) hide show
  1. package/README.md +183 -2
  2. package/api.ts +22 -4
  3. package/buffer-writer +1 -0
  4. package/cjs/browser-test/common.js +78 -3
  5. package/cjs/browser-test/node-test-large.js +8 -8
  6. package/cjs/browser-test/test-buffer-writer.js +330 -0
  7. package/cjs/browser-test/test-errors.js +57 -34
  8. package/cjs/browser-test/test-indexer.js +12 -0
  9. package/cjs/browser-test/test-reader.js +83 -0
  10. package/cjs/browser-test/test-writer.js +3 -3
  11. package/cjs/lib/buffer-writer.js +161 -0
  12. package/cjs/lib/decoder.js +72 -15
  13. package/cjs/lib/encoder.js +2 -2
  14. package/cjs/lib/header-validator.js +29 -0
  15. package/cjs/lib/reader-browser.js +7 -7
  16. package/cjs/lib/writer-browser.js +1 -1
  17. package/cjs/node-test/common.js +78 -3
  18. package/cjs/node-test/node-test-large.js +8 -8
  19. package/cjs/node-test/test-buffer-writer.js +330 -0
  20. package/cjs/node-test/test-errors.js +57 -34
  21. package/cjs/node-test/test-indexer.js +12 -0
  22. package/cjs/node-test/test-reader.js +83 -0
  23. package/cjs/node-test/test-writer.js +3 -3
  24. package/esm/browser-test/common.js +76 -1
  25. package/esm/browser-test/test-buffer-writer.js +311 -0
  26. package/esm/browser-test/test-errors.js +57 -33
  27. package/esm/browser-test/test-indexer.js +15 -0
  28. package/esm/browser-test/test-reader.js +90 -1
  29. package/esm/browser-test/test-writer.js +3 -3
  30. package/esm/lib/buffer-writer.js +126 -0
  31. package/esm/lib/decoder.js +69 -13
  32. package/esm/lib/header-validator.js +23 -0
  33. package/esm/lib/reader-browser.js +7 -8
  34. package/esm/lib/writer-browser.js +1 -1
  35. package/esm/node-test/common.js +76 -1
  36. package/esm/node-test/test-buffer-writer.js +311 -0
  37. package/esm/node-test/test-errors.js +57 -33
  38. package/esm/node-test/test-indexer.js +15 -0
  39. package/esm/node-test/test-reader.js +90 -1
  40. package/esm/node-test/test-writer.js +3 -3
  41. package/examples/car-to-fixture.js +1 -4
  42. package/examples/dump-index.js +24 -0
  43. package/examples/test-examples.js +33 -0
  44. package/lib/buffer-writer.js +286 -0
  45. package/lib/coding.ts +17 -2
  46. package/lib/decoder.js +130 -14
  47. package/lib/header-validator.js +33 -0
  48. package/lib/header.ipldsch +6 -0
  49. package/lib/reader-browser.js +11 -11
  50. package/lib/writer-browser.js +1 -1
  51. package/package.json +16 -6
  52. package/test/_fixtures_to_js.mjs +24 -0
  53. package/test/common.js +49 -3
  54. package/test/go.carv2 +0 -0
  55. package/test/test-buffer-writer.js +256 -0
  56. package/test/test-errors.js +52 -30
  57. package/test/test-indexer.js +24 -1
  58. package/test/test-reader.js +94 -1
  59. package/test/test-writer.js +3 -3
  60. package/tsconfig.json +3 -1
  61. package/types/api.d.ts +16 -0
  62. package/types/api.d.ts.map +1 -1
  63. package/types/lib/buffer-writer.d.ts +86 -0
  64. package/types/lib/buffer-writer.d.ts.map +1 -0
  65. package/types/lib/coding.d.ts +14 -4
  66. package/types/lib/coding.d.ts.map +1 -1
  67. package/types/lib/decoder.d.ts +38 -2
  68. package/types/lib/decoder.d.ts.map +1 -1
  69. package/types/lib/header-validator.d.ts +2 -0
  70. package/types/lib/header-validator.d.ts.map +1 -0
  71. package/types/lib/reader-browser.d.ts +15 -7
  72. package/types/lib/reader-browser.d.ts.map +1 -1
  73. package/types/test/_fixtures_to_js.d.mts +3 -0
  74. package/types/test/_fixtures_to_js.d.mts.map +1 -0
  75. package/types/test/common.d.ts +13 -0
  76. package/types/test/common.d.ts.map +1 -1
  77. package/types/test/fixtures-expectations.d.ts +63 -0
  78. package/types/test/fixtures-expectations.d.ts.map +1 -0
  79. package/types/test/fixtures.d.ts +3 -0
  80. package/types/test/fixtures.d.ts.map +1 -0
  81. package/types/test/test-buffer-writer.d.ts +2 -0
  82. 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 type CarHeader = { version: number, roots: CID[] }
23
+ export interface CarHeader {
24
+ version: 1,
25
+ roots: CID[]
26
+ }
27
+
28
+ export interface CarV2FixedHeader {
29
+ characteristics: [bigint, bigint],
30
+ dataOffset: number,
31
+ dataSize: number,
32
+ indexOffset: number
33
+ }
34
+
35
+ export interface CarV2Header extends CarV2FixedHeader {
36
+ version: 2,
37
+ roots: CID[],
38
+ }
24
39
 
25
40
  export interface CarDecoder {
26
- header(): Promise<CarHeader>
41
+ header(): Promise<CarHeader|CarV2Header>
27
42
 
28
43
  blocks(): AsyncGenerator<Block>
29
44
 
package/lib/decoder.js CHANGED
@@ -2,6 +2,7 @@ import varint from 'varint'
2
2
  import { CID } from 'multiformats/cid'
3
3
  import * as Digest from 'multiformats/hashes/digest'
4
4
  import { decode as decodeDagCbor } from '@ipld/dag-cbor'
5
+ import { CarHeader as headerValidator } from './header-validator.js'
5
6
 
6
7
  /**
7
8
  * @typedef {import('../api').Block} Block
@@ -9,6 +10,8 @@ import { decode as decodeDagCbor } from '@ipld/dag-cbor'
9
10
  * @typedef {import('../api').BlockIndex} BlockIndex
10
11
  * @typedef {import('./coding').BytesReader} BytesReader
11
12
  * @typedef {import('./coding').CarHeader} CarHeader
13
+ * @typedef {import('./coding').CarV2Header} CarV2Header
14
+ * @typedef {import('./coding').CarV2FixedHeader} CarV2FixedHeader
12
15
  * @typedef {import('./coding').CarDecoder} CarDecoder
13
16
  */
14
17
 
@@ -18,12 +21,17 @@ const CIDV0_BYTES = {
18
21
  DAG_PB: 0x70
19
22
  }
20
23
 
24
+ const V2_HEADER_LENGTH = /* characteristics */ 16 /* v1 offset */ + 8 /* v1 size */ + 8 /* index offset */ + 8
25
+
21
26
  /**
22
27
  * @param {BytesReader} reader
23
28
  * @returns {Promise<number>}
24
29
  */
25
30
  async function readVarint (reader) {
26
31
  const bytes = await reader.upTo(8)
32
+ if (!bytes.length) {
33
+ throw new Error('Unexpected end of data')
34
+ }
27
35
  const i = varint.decode(bytes)
28
36
  reader.seek(varint.decode.bytes)
29
37
  return i
@@ -33,9 +41,40 @@ async function readVarint (reader) {
33
41
 
34
42
  /**
35
43
  * @param {BytesReader} reader
36
- * @returns {Promise<CarHeader>}
44
+ * @returns {Promise<CarV2FixedHeader>}
45
+ */
46
+ async function readV2Header (reader) {
47
+ /** @type {Uint8Array} */
48
+ const bytes = await reader.exactly(V2_HEADER_LENGTH)
49
+ const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
50
+ let offset = 0
51
+ const header = {
52
+ version: 2,
53
+ /** @type {[bigint, bigint]} */
54
+ characteristics: [
55
+ dv.getBigUint64(offset, true),
56
+ dv.getBigUint64(offset += 8, true)
57
+ ],
58
+ dataOffset: Number(dv.getBigUint64(offset += 8, true)),
59
+ dataSize: Number(dv.getBigUint64(offset += 8, true)),
60
+ indexOffset: Number(dv.getBigUint64(offset += 8, true))
61
+ }
62
+ reader.seek(V2_HEADER_LENGTH)
63
+ return header
64
+ /* c8 ignore next 2 */
65
+ // Node.js 12 c8 bug
66
+ }
67
+
68
+ /**
69
+ * Reads header data from a `BytesReader`. The header may either be in the form
70
+ * of a `CarHeader` or `CarV2Header` depending on the CAR being read.
71
+ *
72
+ * @name async decoder.readHeader(reader)
73
+ * @param {BytesReader} reader
74
+ * @param {number} [strictVersion]
75
+ * @returns {Promise<CarHeader|CarV2Header>}
37
76
  */
38
- export async function readHeader (reader) {
77
+ export async function readHeader (reader, strictVersion) {
39
78
  const length = await readVarint(reader)
40
79
  if (length === 0) {
41
80
  throw new Error('Invalid CAR header (zero length)')
@@ -43,22 +82,26 @@ export async function readHeader (reader) {
43
82
  const header = await reader.exactly(length)
44
83
  reader.seek(length)
45
84
  const block = decodeDagCbor(header)
46
- if (block == null || Array.isArray(block) || typeof block !== 'object') {
85
+ if (!headerValidator(block)) {
47
86
  throw new Error('Invalid CAR header format')
48
87
  }
49
- if (block.version !== 1) {
50
- if (typeof block.version === 'string') {
51
- throw new Error(`Invalid CAR version: "${block.version}"`)
52
- }
53
- throw new Error(`Invalid CAR version: ${block.version}`)
88
+ if ((block.version !== 1 && block.version !== 2) || (strictVersion !== undefined && block.version !== strictVersion)) {
89
+ throw new Error(`Invalid CAR version: ${block.version}${strictVersion !== undefined ? ` (expected ${strictVersion})` : ''}`)
54
90
  }
55
- if (!Array.isArray(block.roots)) {
91
+ // we've made 'roots' optional in the schema so we can do the version check
92
+ // before rejecting the block as invalid if there is no version
93
+ const hasRoots = Array.isArray(block.roots)
94
+ if ((block.version === 1 && !hasRoots) || (block.version === 2 && hasRoots)) {
56
95
  throw new Error('Invalid CAR header format')
57
96
  }
58
- if (Object.keys(block).filter((p) => p !== 'roots' && p !== 'version').length) {
59
- throw new Error('Invalid CAR header format')
97
+ if (block.version === 1) {
98
+ return block
60
99
  }
61
- return block
100
+ // version 2
101
+ const v2Header = await readV2Header(reader)
102
+ reader.seek(v2Header.dataOffset - reader.pos)
103
+ const v1Header = await readHeader(reader, 1)
104
+ return Object.assign(v1Header, v2Header)
62
105
  /* c8 ignore next 2 */
63
106
  // Node.js 12 c8 bug
64
107
  }
@@ -112,6 +155,12 @@ async function readCid (reader) {
112
155
  }
113
156
 
114
157
  /**
158
+ * Reads the leading data of an individual block from CAR data from a
159
+ * `BytesReader`. Returns a `BlockHeader` object which contains
160
+ * `{ cid, length, blockLength }` which can be used to either index the block
161
+ * or read the block binary data.
162
+ *
163
+ * @name async decoder.readBlockHead(reader)
115
164
  * @param {BytesReader} reader
116
165
  * @returns {Promise<BlockHeader>}
117
166
  */
@@ -125,7 +174,7 @@ export async function readBlockHead (reader) {
125
174
  }
126
175
  length += (reader.pos - start)
127
176
  const cid = await readCid(reader)
128
- const blockLength = length - (reader.pos - start) // subtract CID length
177
+ const blockLength = length - Number(reader.pos - start) // subtract CID length
129
178
 
130
179
  return { cid, length, blockLength }
131
180
  /* c8 ignore next 2 */
@@ -160,11 +209,25 @@ async function readBlockIndex (reader) {
160
209
  }
161
210
 
162
211
  /**
212
+ * Creates a `CarDecoder` from a `BytesReader`. The `CarDecoder` is as async
213
+ * interface that will consume the bytes from the `BytesReader` to yield a
214
+ * `header()` and either `blocks()` or `blocksIndex()` data.
215
+ *
216
+ * @name decoder.createDecoder(reader)
163
217
  * @param {BytesReader} reader
164
218
  * @returns {CarDecoder}
165
219
  */
166
220
  export function createDecoder (reader) {
167
- const headerPromise = readHeader(reader)
221
+ const headerPromise = (async () => {
222
+ const header = await readHeader(reader)
223
+ if (header.version === 2) {
224
+ const v1length = reader.pos - header.dataOffset
225
+ reader = limitReader(reader, header.dataSize - v1length)
226
+ }
227
+ return header
228
+ /* c8 ignore next 2 */
229
+ // Node.js 12 c8 bug
230
+ })()
168
231
 
169
232
  return {
170
233
  header: () => headerPromise,
@@ -186,6 +249,9 @@ export function createDecoder (reader) {
186
249
  }
187
250
 
188
251
  /**
252
+ * Creates a `BytesReader` from a `Uint8Array`.
253
+ *
254
+ * @name decoder.bytesReader(bytes)
189
255
  * @param {Uint8Array} bytes
190
256
  * @returns {BytesReader}
191
257
  */
@@ -298,6 +364,10 @@ export function chunkReader (readChunk /*, closer */) {
298
364
  }
299
365
 
300
366
  /**
367
+ * Creates a `BytesReader` from an `AsyncIterable<Uint8Array>`, which allows for
368
+ * consumption of CAR data from a streaming source.
369
+ *
370
+ * @name decoder.asyncIterableReader(asyncIterable)
301
371
  * @param {AsyncIterable<Uint8Array>} asyncIterable
302
372
  * @returns {BytesReader}
303
373
  */
@@ -316,3 +386,49 @@ export function asyncIterableReader (asyncIterable) {
316
386
 
317
387
  return chunkReader(readChunk)
318
388
  }
389
+
390
+ /**
391
+ * Wraps a `BytesReader` in a limiting `BytesReader` which limits maximum read
392
+ * to `byteLimit` bytes. It _does not_ update `pos` of the original
393
+ * `BytesReader`.
394
+ *
395
+ * @name decoder.limitReader(reader, byteLimit)
396
+ * @param {BytesReader} reader
397
+ * @param {number} byteLimit
398
+ * @returns {BytesReader}
399
+ */
400
+ export function limitReader (reader, byteLimit) {
401
+ let bytesRead = 0
402
+
403
+ /** @type {BytesReader} */
404
+ return {
405
+ async upTo (length) {
406
+ let bytes = await reader.upTo(length)
407
+ if (bytes.length + bytesRead > byteLimit) {
408
+ bytes = bytes.subarray(0, byteLimit - bytesRead)
409
+ }
410
+ return bytes
411
+ /* c8 ignore next 2 */
412
+ // Node.js 12 c8 bug
413
+ },
414
+
415
+ async exactly (length) {
416
+ const bytes = await reader.exactly(length)
417
+ if (bytes.length + bytesRead > byteLimit) {
418
+ throw new Error('Unexpected end of data')
419
+ }
420
+ return bytes
421
+ /* c8 ignore next 2 */
422
+ // Node.js 12 c8 bug
423
+ },
424
+
425
+ seek (length) {
426
+ bytesRead += length
427
+ reader.seek(length)
428
+ },
429
+
430
+ get pos () {
431
+ return reader.pos
432
+ }
433
+ }
434
+ }
@@ -0,0 +1,33 @@
1
+ /** Auto-generated with ipld-schema-validator@0.0.0-dev at Thu Jun 17 2021 from IPLD Schema:
2
+ *
3
+ * type CarHeader struct {
4
+ * version Int
5
+ * roots optional [&Any]
6
+ * # roots is _not_ optional for CarV1 but we defer that check within code to
7
+ * # gracefully handle the >V1 case where it's just {version:X}
8
+ * }
9
+ *
10
+ */
11
+
12
+ const Kinds = {
13
+ Null: /** @returns {boolean} */ (/** @type {any} */ obj) => obj === null,
14
+ Int: /** @returns {boolean} */ (/** @type {any} */ obj) => Number.isInteger(obj),
15
+ Float: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'number' && Number.isFinite(obj),
16
+ String: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'string',
17
+ Bool: /** @returns {boolean} */ (/** @type {any} */ obj) => typeof obj === 'boolean',
18
+ Bytes: /** @returns {boolean} */ (/** @type {any} */ obj) => obj instanceof Uint8Array,
19
+ Link: /** @returns {boolean} */ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID === obj,
20
+ List: /** @returns {boolean} */ (/** @type {any} */ obj) => Array.isArray(obj),
21
+ Map: /** @returns {boolean} */ (/** @type {any} */ obj) => !Kinds.Null(obj) && typeof obj === 'object' && obj.asCID !== obj && !Kinds.List(obj) && !Kinds.Bytes(obj)
22
+ }
23
+ /** @type {{ [k in string]: (obj:any)=>boolean}} */
24
+ const Types = {
25
+ Int: Kinds.Int,
26
+ 'CarHeader > version': /** @returns {boolean} */ (/** @type {any} */ obj) => Types.Int(obj),
27
+ 'CarHeader > roots (anon) > valueType (anon)': Kinds.Link,
28
+ 'CarHeader > roots (anon)': /** @returns {boolean} */ (/** @type {any} */ obj) => Kinds.List(obj) && Array.prototype.every.call(obj, Types['CarHeader > roots (anon) > valueType (anon)']),
29
+ 'CarHeader > roots': /** @returns {boolean} */ (/** @type {any} */ obj) => Types['CarHeader > roots (anon)'](obj),
30
+ CarHeader: /** @returns {boolean} */ (/** @type {any} */ obj) => { const keys = obj && Object.keys(obj); return Kinds.Map(obj) && ['version'].every((k) => keys.includes(k)) && Object.entries(obj).every(([name, value]) => Types['CarHeader > ' + name] && Types['CarHeader > ' + name](value)) }
31
+ }
32
+
33
+ export const CarHeader = Types.CarHeader
@@ -0,0 +1,6 @@
1
+ type CarHeader struct {
2
+ version Int
3
+ roots optional [&Any]
4
+ # roots is _not_ optional for CarV1 but we defer that check within code to
5
+ # gracefully handle the >V1 case where it's just {version:X}
6
+ }
@@ -5,6 +5,8 @@ import { asyncIterableReader, bytesReader, createDecoder } from './decoder.js'
5
5
  * @typedef {import('../api').Block} Block
6
6
  * @typedef {import('../api').CarReader} CarReaderIface
7
7
  * @typedef {import('./coding').BytesReader} BytesReader
8
+ * @typedef {import('./coding').CarHeader} CarHeader
9
+ * @typedef {import('./coding').CarV2Header} CarV2Header
8
10
  */
9
11
 
10
12
  /**
@@ -26,18 +28,16 @@ import { asyncIterableReader, bytesReader, createDecoder } from './decoder.js'
26
28
  * @class
27
29
  * @implements {CarReaderIface}
28
30
  * @property {number} version The version number of the CAR referenced by this
29
- * reader (should be `1`).
31
+ * reader (should be `1` or `2`).
30
32
  */
31
33
  export class CarReader {
32
34
  /**
33
35
  * @constructs CarReader
34
- * @param {number} version
35
- * @param {CID[]} roots
36
+ * @param {CarHeader|CarV2Header} header
36
37
  * @param {Block[]} blocks
37
38
  */
38
- constructor (version, roots, blocks) {
39
- this._version = version
40
- this._roots = roots
39
+ constructor (header, blocks) {
40
+ this._header = header
41
41
  this._blocks = blocks
42
42
  this._keys = blocks.map((b) => b.cid.toString())
43
43
  }
@@ -48,7 +48,7 @@ export class CarReader {
48
48
  * @instance
49
49
  */
50
50
  get version () {
51
- return this._version
51
+ return this._header.version
52
52
  }
53
53
 
54
54
  /**
@@ -62,7 +62,7 @@ export class CarReader {
62
62
  * @returns {Promise<CID[]>}
63
63
  */
64
64
  async getRoots () {
65
- return this._roots
65
+ return this._header.roots
66
66
  /* c8 ignore next 2 */
67
67
  // Node.js 12 c8 bug
68
68
  }
@@ -190,15 +190,15 @@ export class CarReader {
190
190
  * @param {BytesReader} reader
191
191
  * @returns {Promise<CarReader>}
192
192
  */
193
- async function decodeReaderComplete (reader) {
193
+ export async function decodeReaderComplete (reader) {
194
194
  const decoder = createDecoder(reader)
195
- const { version, roots } = await decoder.header()
195
+ const header = await decoder.header()
196
196
  const blocks = []
197
197
  for await (const block of decoder.blocks()) {
198
198
  blocks.push(block)
199
199
  }
200
200
 
201
- return new CarReader(version, roots, blocks)
201
+ return new CarReader(header, blocks)
202
202
  /* c8 ignore next 2 */
203
203
  // Node.js 12 c8 bug
204
204
  }
@@ -176,7 +176,7 @@ export class CarWriter {
176
176
  const reader = bytesReader(bytes)
177
177
  await readHeader(reader)
178
178
  const newHeader = createHeader(roots)
179
- if (reader.pos !== newHeader.length) {
179
+ if (Number(reader.pos) !== newHeader.length) {
180
180
  throw new Error(`updateRoots() can only overwrite a header of the same length (old header is ${reader.pos} bytes, new header is ${newHeader.length} bytes)`)
181
181
  }
182
182
  bytes.set(newHeader, 0)