-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathbuffer-writer.js
286 lines (262 loc) · 8.47 KB
/
buffer-writer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import * as CBOR from '@ipld/dag-cbor'
import { Token, Type } from 'cborg'
import { tokensToLength } from 'cborg/length'
import varint from 'varint'
/**
* @typedef {import('./api').CID} CID
* @typedef {import('./api').Block} Block
* @typedef {import('./api').CarBufferWriter} Writer
* @typedef {import('./api').CarBufferWriterOptions} Options
* @typedef {import('./coding').CarEncoder} CarEncoder
*/
/**
* A simple CAR writer that writes to a pre-allocated buffer.
*
* @class
* @name CarBufferWriter
* @implements {Writer}
*/
class CarBufferWriter {
/**
* @param {Uint8Array} bytes
* @param {number} headerSize
*/
constructor (bytes, headerSize) {
/** @readonly */
this.bytes = bytes
this.byteOffset = headerSize
/**
* @readonly
* @type {CID[]}
*/
this.roots = []
this.headerSize = headerSize
}
/**
* Add a root to this writer, to be used to create a header when the CAR is
* finalized with {@link CarBufferWriter.close `close()`}
*
* @param {CID} root
* @param {{resize?:boolean}} [options]
* @returns {CarBufferWriter}
*/
addRoot (root, options) {
addRoot(this, root, options)
return this
}
/**
* Write a `Block` (a `{ cid:CID, bytes:Uint8Array }` pair) to the archive.
* Throws if there is not enough capacity.
*
* @param {Block} block - A `{ cid:CID, bytes:Uint8Array }` pair.
* @returns {CarBufferWriter}
*/
write (block) {
addBlock(this, block)
return this
}
/**
* Finalize the CAR and return it as a `Uint8Array`.
*
* @param {object} [options]
* @param {boolean} [options.resize]
* @returns {Uint8Array}
*/
close (options) {
return close(this, options)
}
}
/**
* @param {CarBufferWriter} writer
* @param {CID} root
* @param {{resize?:boolean}} [options]
*/
export const addRoot = (writer, root, options = {}) => {
const { resize = false } = options
const { bytes, headerSize, byteOffset, roots } = writer
writer.roots.push(root)
const size = headerLength(writer)
// If there is not enough space for the new root
if (size > headerSize) {
// Check if we root would fit if we were to resize the head.
if (size - headerSize + byteOffset < bytes.byteLength) {
// If resize is enabled resize head
if (resize) {
resizeHeader(writer, size)
// otherwise remove head and throw an error suggesting to resize
} else {
roots.pop()
throw new RangeError(`Header of size ${headerSize} has no capacity for new root ${root}.
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.`)
}
// If head would not fit even with resize pop new root and throw error
} else {
roots.pop()
throw new RangeError(`Buffer has no capacity for a new root ${root}`)
}
}
}
/**
* Calculates number of bytes required for storing given block in CAR. Useful in
* estimating size of an `ArrayBuffer` for the `CarBufferWriter`.
*
* @name CarBufferWriter.blockLength(Block)
* @param {Block} block
* @returns {number}
*/
export const blockLength = ({ cid, bytes }) => {
const size = cid.bytes.byteLength + bytes.byteLength
return varint.encodingLength(size) + size
}
/**
* @param {CarBufferWriter} writer
* @param {Block} block
*/
export const addBlock = (writer, { cid, bytes }) => {
const byteLength = cid.bytes.byteLength + bytes.byteLength
const size = varint.encode(byteLength)
if (writer.byteOffset + size.length + byteLength > writer.bytes.byteLength) {
throw new RangeError('Buffer has no capacity for this block')
} else {
writeBytes(writer, size)
writeBytes(writer, cid.bytes)
writeBytes(writer, bytes)
}
}
/**
* @param {CarBufferWriter} writer
* @param {object} [options]
* @param {boolean} [options.resize]
*/
export const close = (writer, options = {}) => {
const { resize = false } = options
const { roots, bytes, byteOffset, headerSize } = writer
const headerBytes = CBOR.encode({ version: 1, roots })
const varintBytes = varint.encode(headerBytes.length)
const size = varintBytes.length + headerBytes.byteLength
const offset = headerSize - size
// If header size estimate was accurate we just write header and return
// view into buffer.
if (offset === 0) {
writeHeader(writer, varintBytes, headerBytes)
return bytes.subarray(0, byteOffset)
// If header was overestimated and `{resize: true}` is passed resize header
} else if (resize) {
resizeHeader(writer, size)
writeHeader(writer, varintBytes, headerBytes)
return bytes.subarray(0, writer.byteOffset)
} else {
throw new RangeError(`Header size was overestimated.
You can use close({ resize: true }) to resize header`)
}
}
/**
* @param {CarBufferWriter} writer
* @param {number} byteLength
*/
export const resizeHeader = (writer, byteLength) => {
const { bytes, headerSize } = writer
// Move data section to a new offset
bytes.set(bytes.subarray(headerSize, writer.byteOffset), byteLength)
// Update header size & byteOffset
writer.byteOffset += byteLength - headerSize
writer.headerSize = byteLength
}
/**
* @param {CarBufferWriter} writer
* @param {number[]|Uint8Array} bytes
*/
const writeBytes = (writer, bytes) => {
writer.bytes.set(bytes, writer.byteOffset)
writer.byteOffset += bytes.length
}
/**
* @param {{bytes:Uint8Array}} writer
* @param {number[]} varint
* @param {Uint8Array} header
*/
const writeHeader = ({ bytes }, varint, header) => {
bytes.set(varint)
bytes.set(header, varint.length)
}
const headerPreludeTokens = [
new Token(Type.map, 2),
new Token(Type.string, 'version'),
new Token(Type.uint, 1),
new Token(Type.string, 'roots')
]
const CID_TAG = new Token(Type.tag, 42)
/**
* Calculates header size given the array of byteLength for roots.
*
* @name CarBufferWriter.calculateHeaderLength(rootLengths)
* @param {number[]} rootLengths
* @returns {number}
*/
export const calculateHeaderLength = (rootLengths) => {
const tokens = [...headerPreludeTokens]
tokens.push(new Token(Type.array, rootLengths.length))
for (const rootLength of rootLengths) {
tokens.push(CID_TAG)
tokens.push(new Token(Type.bytes, { length: rootLength + 1 }))
}
const length = tokensToLength(tokens) // no options needed here because we have simple tokens
return varint.encodingLength(length) + length
}
/**
* Calculates header size given the array of roots.
*
* @name CarBufferWriter.headerLength({ roots })
* @param {object} options
* @param {CID[]} options.roots
* @returns {number}
*/
export const headerLength = ({ roots }) =>
calculateHeaderLength(roots.map(cid => cid.bytes.byteLength))
/**
* Estimates header size given a count of the roots and the expected byte length
* of the root CIDs. The default length works for a standard CIDv1 with a
* single-byte multihash code, such as SHA2-256 (i.e. the most common CIDv1).
*
* @name CarBufferWriter.estimateHeaderLength(rootCount[, rootByteLength])
* @param {number} rootCount
* @param {number} [rootByteLength]
* @returns {number}
*/
export const estimateHeaderLength = (rootCount, rootByteLength = 36) =>
calculateHeaderLength(new Array(rootCount).fill(rootByteLength))
/**
* Creates synchronous CAR writer that can be used to encode blocks into a given
* buffer. Optionally you could pass `byteOffset` and `byteLength` to specify a
* range inside buffer to write into. If car file is going to have `roots` you
* need to either pass them under `options.roots` (from which header size will
* be calculated) or provide `options.headerSize` to allocate required space
* in the buffer. You may also provide known `roots` and `headerSize` to
* allocate space for the roots that may not be known ahead of time.
*
* Note: Incorrect `headerSize` may lead to copying bytes inside a buffer
* which will have a negative impact on performance.
*
* @name CarBufferWriter.createWriter(buffer[, options])
* @param {ArrayBuffer} buffer
* @param {object} [options]
* @param {CID[]} [options.roots]
* @param {number} [options.byteOffset]
* @param {number} [options.byteLength]
* @param {number} [options.headerSize]
* @returns {CarBufferWriter}
*/
export const createWriter = (buffer, options = {}) => {
const {
roots = [],
byteOffset = 0,
byteLength = buffer.byteLength,
headerSize = headerLength({ roots })
} = options
const bytes = new Uint8Array(buffer, byteOffset, byteLength)
const writer = new CarBufferWriter(bytes, headerSize)
for (const root of roots) {
writer.addRoot(root)
}
return writer
}