cbor-x 1.6.7 → 1.6.9

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 CHANGED
@@ -52,6 +52,10 @@ receivingStream.on('data', (data) => {
52
52
  ```
53
53
  The `EncoderStream` and `DecoderStream` instances will have also the record structure extension enabled by default (see below).
54
54
 
55
+ When a chunk ends inside a value, `DecoderStream` keeps the bytes of that value until the chunks that complete it arrive. The `maxIncompleteBufferSize` option limits how many bytes it keeps for one incomplete value (default 64 MB, `0x4000000`); past that, the stream emits a "Maximum incomplete buffer size exceeded" error. A value larger than this limit still decodes when it arrives in a single chunk, so raise the limit for streams that carry larger values. If the input ends inside a value, the stream emits an "Unexpected end of CBOR data" error with `incomplete: true`. `decodeIter` takes the same option and reports an incomplete final value the same way.
56
+
57
+ Incomplete values are scanned incrementally without constructing objects, then decoded when their complete bytes arrive. This keeps decoding work linear in the size of a value split across many chunks. All built-in tags and custom extensions that consume exactly one tagged CBOR item are supported. An undocumented custom `handlesRead` decoder that consumes multiple sibling items cannot be framed on fragmented input; it throws an error naming the tag. Whole-buffer decoding of these extensions is unchanged.
58
+
55
59
  ### Iterables
56
60
  In addition to using CBOR with streams, CBOR can also encode to an iterable that can be iterated as a sequence of binary chunks with `encodeAsIterable`, which facilitates progressive encoding:
57
61
  ```JavaScript
package/decode.js CHANGED
@@ -13,7 +13,6 @@ const RECORD_INLINE_ID = 0xdfff // temporary first-come first-serve tag // propo
13
13
  const BUNDLED_STRINGS_ID = 0xdff9
14
14
  const PACKED_TABLE_TAG_ID = 51
15
15
  const PACKED_REFERENCE_TAG_ID = 6
16
- const STOP_CODE = {}
17
16
  let maxArraySize = 112810000 // This is the maximum array size in V8. We would potentially detect and set it higher
18
17
  // for JSC, but this is pretty large and should be sufficient for most use cases
19
18
  let maxMapSize = 16810000 // JavaScript has a fixed maximum map size of about 16710000, but JS itself enforces this,
@@ -125,16 +124,11 @@ export class Decoder {
125
124
  // re-entrant execution, save the state and restore it after we do this decode
126
125
  return saveState(() => {
127
126
  clearSource()
128
- try {
129
- return this ? this.decode(source, end) : Decoder.prototype.decode.call(defaultOptions, source, end)
130
- } catch (error) {
131
- if (error && typeof error == 'object')
132
- nestedDecodeErrors.add(error)
133
- throw error
134
- }
127
+ return this ? this.decode(source, end) : Decoder.prototype.decode.call(defaultOptions, source, end)
135
128
  })
136
129
  }
137
- srcEnd = end > -1 ? end : source.length
130
+ // past the view, the JS string readers would decode the missing bytes as zeros instead of failing as truncated
131
+ srcEnd = end > -1 && end < source.length ? end : source.length
138
132
  position = 0
139
133
  stringPosition = 0
140
134
  srcStringEnd = 0
@@ -174,18 +168,28 @@ export class Decoder {
174
168
  return checkedRead()
175
169
  }
176
170
  decodeMultiple(source, forEach) {
171
+ if (src) {
172
+ // Keep the complete nested sequence inside its own decoder state.
173
+ return saveState(() => {
174
+ clearSource()
175
+ return this ? this.decodeMultiple(source, forEach) : defaultDecoder.decodeMultiple(source, forEach)
176
+ })
177
+ }
177
178
  let values, lastPosition = 0
178
179
  try {
179
180
  let size = source.length
180
181
  sequentialMode = true
181
182
  let value = this ? this.decode(source, size) : defaultDecoder.decode(source, size)
182
183
  if (forEach) {
184
+ let end = position
183
185
  if (forEach(value) === false) {
184
186
  return
185
187
  }
186
- while(position < size) {
187
- lastPosition = position
188
- if (forEach(checkedRead()) === false) {
188
+ while(end < size) {
189
+ lastPosition = end
190
+ value = checkedRead()
191
+ end = position
192
+ if (forEach(value) === false) {
189
193
  return
190
194
  }
191
195
  }
@@ -199,8 +203,10 @@ export class Decoder {
199
203
  return values
200
204
  }
201
205
  } catch(error) {
202
- error.lastPosition = lastPosition
203
- error.values = values
206
+ if (error && (typeof error == 'object' || typeof error == 'function')) {
207
+ error.lastPosition = lastPosition
208
+ error.values = values
209
+ }
204
210
  throw error
205
211
  } finally {
206
212
  sequentialMode = false
@@ -212,8 +218,10 @@ export function getPosition() {
212
218
  return position
213
219
  }
214
220
  const nestedDecodeErrors = new WeakSet()
221
+ let arrayAllocationBudget
215
222
  export function checkedRead() {
216
223
  try {
224
+ arrayAllocationBudget = srcEnd - position
217
225
  let result = read()
218
226
  if (bundledStrings) {
219
227
  if (position >= bundledStrings.postBundlePosition)
@@ -255,9 +263,21 @@ export function checkedRead() {
255
263
  }
256
264
  }
257
265
 
258
- function endOfCBORError() {
266
+ function readBreak() {
267
+ if (position < srcEnd && src[position] === 0xff) {
268
+ position++
269
+ return true
270
+ }
271
+ return false
272
+ }
273
+ // neededEnd, from a length check, is a position that the source must extend to before the item can decode. It is a
274
+ // lower bound for any completion of the source: every byte read before the check was within the source, so a longer
275
+ // source reaches the same check with the same length. Streams wait for it instead of decoding on every chunk.
276
+ function endOfCBORError(neededEnd) {
259
277
  let error = new Error('Unexpected end of CBOR data')
260
278
  error.incomplete = true
279
+ if (neededEnd)
280
+ error.neededEnd = neededEnd
261
281
  return error
262
282
  }
263
283
 
@@ -309,6 +329,9 @@ export function read() {
309
329
  } else if (currentDecoder.int64AsNumber) {
310
330
  token = dataView.getUint32(position) * 0x100000000
311
331
  token += dataView.getUint32(position + 4)
332
+ position += 8
333
+ if (majorType === 1) return -1 - token
334
+ break
312
335
  } else token = dataView.getBigUint64(position)
313
336
  position += 8
314
337
  break
@@ -320,10 +343,10 @@ export function read() {
320
343
  throw new Error('Indefinite length not supported for byte or text strings')
321
344
  case 4: // array
322
345
  let array = []
323
- let value, i = 0
324
- while ((value = read()) != STOP_CODE) {
346
+ let i = 0
347
+ while (!readBreak()) {
325
348
  if (i >= maxArraySize) throw new Error(`Array length exceeds ${maxArraySize}`)
326
- array[i++] = value
349
+ array[i++] = read()
327
350
  }
328
351
  return majorType == 4 ? array : majorType == 3 ? array.join('') : Buffer.concat(array)
329
352
  case 5: // map
@@ -332,13 +355,15 @@ export function read() {
332
355
  let object = {}
333
356
  let i = 0;
334
357
  if (currentDecoder.keyMap) {
335
- while((key = read()) != STOP_CODE) {
358
+ while (!readBreak()) {
359
+ key = read()
336
360
  if (i++ >= objectSizeLimit) throw new Error(`Property count exceeds ${objectSizeLimit}`)
337
361
  object[safeKey(currentDecoder.decodeKey(key))] = read()
338
362
  }
339
363
  }
340
364
  else {
341
- while ((key = read()) != STOP_CODE) {
365
+ while (!readBreak()) {
366
+ key = read()
342
367
  if (i++ >= objectSizeLimit) throw new Error(`Property count exceeds ${objectSizeLimit}`)
343
368
  object[safeKey(key)] = read()
344
369
  }
@@ -352,7 +377,8 @@ export function read() {
352
377
  let map = new Map()
353
378
  if (currentDecoder.keyMap) {
354
379
  let i = 0;
355
- while((key = read()) != STOP_CODE) {
380
+ while (!readBreak()) {
381
+ key = read()
356
382
  if (i++ >= maxMapSize) {
357
383
  throw new Error(`Map size exceeds ${maxMapSize}`);
358
384
  }
@@ -361,7 +387,8 @@ export function read() {
361
387
  }
362
388
  else {
363
389
  let i = 0;
364
- while ((key = read()) != STOP_CODE) {
390
+ while (!readBreak()) {
391
+ key = read()
365
392
  if (i++ >= maxMapSize) {
366
393
  throw new Error(`Map size exceeds ${maxMapSize}`);
367
394
  }
@@ -371,7 +398,8 @@ export function read() {
371
398
  return map
372
399
  }
373
400
  case 7:
374
- return STOP_CODE
401
+ // readBreak consumes every break that ends an indefinite-length item
402
+ throw new Error('Unexpected break stop code outside of an indefinite-length item')
375
403
  default:
376
404
  throw new Error('Invalid major type for indefinite length ' + majorType)
377
405
  }
@@ -385,13 +413,13 @@ export function read() {
385
413
  case 1: // negative int
386
414
  return ~token
387
415
  case 2: // buffer
388
- if (token > srcEnd - position) throw endOfCBORError()
416
+ if (token > srcEnd - position) throw endOfCBORError(position + token)
389
417
  return readBin(token)
390
418
  case 3: // string
391
419
  if (srcStringEnd >= position) { // already extracted, which only covers complete strings
392
420
  return srcString.slice(position - srcStringStart, (position += token) - srcStringStart)
393
421
  }
394
- if (token > srcEnd - position) throw endOfCBORError()
422
+ if (token > srcEnd - position) throw endOfCBORError(position + token)
395
423
  if (srcStringEnd == 0 && srcEnd < 140 && token < 32) {
396
424
  // for small blocks, avoiding the overhead of the extract call is helpful
397
425
  let string = token < 16 ? shortStringInJS(token) : longStringInJS(token)
@@ -403,7 +431,9 @@ export function read() {
403
431
  if (token > maxArraySize) throw new Error(`Array length exceeds ${maxArraySize}`)
404
432
  // every element occupies at least one byte, so a length beyond what remains in the source can
405
433
  // never be satisfied; check before allocating so a tiny header can not force a huge allocation
406
- if (token > srcEnd - position) throw endOfCBORError()
434
+ // nested arrays each pass the check above yet still preallocate, so also spend from a
435
+ // per-decode budget of the remaining bytes; the summed lengths of any valid tree stay under it
436
+ if (token > srcEnd - position || (arrayAllocationBudget -= token) < 0) throw endOfCBORError(position + token)
407
437
  let array = new Array(token)
408
438
  //if (currentDecoder.keyMap) for (let i = 0; i < token; i++) array[i] = currentDecoder.decodeKey(read())
409
439
  //else
@@ -412,7 +442,7 @@ export function read() {
412
442
  case 5: // map
413
443
  if (currentDecoder.mapsAsObjects) {
414
444
  if (token > objectSizeLimit) throw new Error(`Property count exceeds ${objectSizeLimit}`)
415
- if (token > (srcEnd - position) / 2) throw endOfCBORError() // each entry needs at least two bytes
445
+ if (token > (srcEnd - position) / 2) throw endOfCBORError(position + 2 * token) // each entry needs at least two bytes
416
446
  let object = {}
417
447
  if (currentDecoder.keyMap) for (let i = 0; i < token; i++) object[safeKey(currentDecoder.decodeKey(read()))] = read()
418
448
  else for (let i = 0; i < token; i++) object[safeKey(read())] = read()
@@ -423,7 +453,7 @@ export function read() {
423
453
  restoreMapsAsObject = false
424
454
  }
425
455
  if (token > maxMapSize) throw new Error(`Map size exceeds ${maxMapSize}`)
426
- if (token > (srcEnd - position) / 2) throw endOfCBORError()
456
+ if (token > (srcEnd - position) / 2) throw endOfCBORError(position + 2 * token)
427
457
  let map = new Map()
428
458
  if (currentDecoder.keyMap) for (let i = 0; i < token; i++) map.set(currentDecoder.decodeKey(read()),read())
429
459
  else for (let i = 0; i < token; i++) map.set(read(), read())
@@ -1016,6 +1046,7 @@ currentExtensions[LEGACY_RECORD_INLINE_ID] = (data) => {
1016
1046
  }
1017
1047
  return object
1018
1048
  }
1049
+ const legacyRecordDefinition = currentExtensions[LEGACY_RECORD_INLINE_ID]
1019
1050
  currentExtensions[14] = (value) => {
1020
1051
  if (bundledStrings)
1021
1052
  return bundledStrings[0].slice(bundledStrings.position0, bundledStrings.position0 += value)
@@ -1288,6 +1319,9 @@ function loadShared() {
1288
1319
  }
1289
1320
 
1290
1321
  function saveState(callback) {
1322
+ let savedFragmentEnd = fragmentEnd
1323
+ let savedFragmentReader = fragmentReadString
1324
+ let savedReadFixedString = readFixedString
1291
1325
  let savedSrcEnd = srcEnd
1292
1326
  let savedPosition = position
1293
1327
  let savedStringPosition = stringPosition
@@ -1297,6 +1331,7 @@ function saveState(callback) {
1297
1331
  let savedStrings = strings
1298
1332
  let savedReferenceMap = referenceMap
1299
1333
  let savedBundledStrings = bundledStrings
1334
+ let savedPackedValues = packedValues
1300
1335
 
1301
1336
  // TODO: We may need to revisit this if we do more external calls to user code (since it could be slow)
1302
1337
  let savedSrc = new Uint8Array(src.slice(0, srcEnd)) // we copy the data in case it changes while external data is processed
@@ -1304,10 +1339,23 @@ function saveState(callback) {
1304
1339
  let savedDecoder = currentDecoder
1305
1340
  let savedSequentialMode = sequentialMode
1306
1341
  let savedRestoreMapsAsObject = restoreMapsAsObject
1342
+ let savedArrayAllocationBudget = arrayAllocationBudget
1343
+ fragmentEnd = 0
1344
+ if (fragmentReadString) readFixedString = fragmentReadString
1345
+ fragmentReadString = null
1307
1346
  restoreMapsAsObject = false
1308
1347
  try {
1309
1348
  return callback()
1349
+ } catch (error) {
1350
+ // the callback decodes its own source (a shared structures blob, or an extension's embedded data), so a
1351
+ // truncation there cannot be completed by more of the outer input; checkedRead() clears the flag
1352
+ if (error && (typeof error == 'object' || typeof error == 'function'))
1353
+ nestedDecodeErrors.add(error)
1354
+ throw error
1310
1355
  } finally {
1356
+ fragmentEnd = savedFragmentEnd
1357
+ fragmentReadString = savedFragmentReader
1358
+ readFixedString = savedReadFixedString
1311
1359
  srcEnd = savedSrcEnd
1312
1360
  position = savedPosition
1313
1361
  stringPosition = savedStringPosition
@@ -1317,11 +1365,13 @@ function saveState(callback) {
1317
1365
  strings = savedStrings
1318
1366
  referenceMap = savedReferenceMap
1319
1367
  bundledStrings = savedBundledStrings
1368
+ packedValues = savedPackedValues
1320
1369
  src = savedSrc
1321
1370
  sequentialMode = savedSequentialMode
1322
1371
  currentStructures = savedStructures
1323
1372
  currentDecoder = savedDecoder
1324
1373
  restoreMapsAsObject = savedRestoreMapsAsObject
1374
+ arrayAllocationBudget = savedArrayAllocationBudget
1325
1375
  dataView = new DataView(src.buffer, src.byteOffset, src.byteLength)
1326
1376
  }
1327
1377
  }
@@ -1331,8 +1381,87 @@ export function clearSource() {
1331
1381
  currentStructures = null
1332
1382
  }
1333
1383
 
1384
+ const registeredExtensions = new Map()
1385
+ let fragmentEnd = 0
1386
+ let fragmentReadString
1387
+
1388
+ const builtinScanTags = new Map([[LEGACY_RECORD_INLINE_ID, legacyRecordDefinition], [259, currentExtensions[259]]])
1389
+ function isBuiltinScanTag(tag) {
1390
+ return builtinScanTags.has(tag) && currentExtensions[tag] === builtinScanTags.get(tag)
1391
+ }
1392
+
1393
+ function getScanTag(tag, decoder) {
1394
+ return tag >= BUNDLED_STRINGS_ID && decoder.structures && decoder.structures[tag & 0x1fff]
1395
+ }
1396
+
1397
+ function checkScanContainer(major, length, mapsAsObjects) {
1398
+ if (major === 4) {
1399
+ if (length > maxArraySize) throw new Error(`Array length exceeds ${maxArraySize}`)
1400
+ } else if (mapsAsObjects) {
1401
+ if (length > objectSizeLimit) throw new Error(`Property count exceeds ${objectSizeLimit}`)
1402
+ } else if (length > maxMapSize) throw new Error(`Map size exceeds ${maxMapSize}`)
1403
+ }
1404
+
1405
+ function decodeScanned(decoder, source, end) {
1406
+ const boundStrings = isNativeAccelerationEnabled && end < source.length
1407
+ if (!registeredExtensions.size && !boundStrings) return decoder.decodeMultiple(source)
1408
+ const savedEnd = fragmentEnd
1409
+ const savedReader = readFixedString
1410
+ const savedFragmentReader = fragmentReadString
1411
+ const originals = []
1412
+ fragmentEnd = end
1413
+ fragmentReadString = savedReader
1414
+ try {
1415
+ if (boundStrings) readFixedString = (length) => {
1416
+ if (!fragmentEnd || position >= fragmentEnd) return savedReader(length)
1417
+ const savedSrcEnd = srcEnd
1418
+ srcEnd = fragmentEnd
1419
+ try { return savedReader(length) } finally { srcEnd = savedSrcEnd }
1420
+ }
1421
+ for (const [tag, extension] of registeredExtensions) {
1422
+ if (!extension.handlesRead) continue
1423
+ const original = currentExtensions[tag]
1424
+ const guarded = (read) => {
1425
+ if (!fragmentEnd || position >= fragmentEnd) return extension(read)
1426
+ let calls = 0, framingError
1427
+ const value = extension(() => {
1428
+ if (calls++) {
1429
+ framingError = new Error('Tag ' + tag + ': fragmented decoding cannot frame multi-item handlesRead extensions')
1430
+ throw framingError
1431
+ }
1432
+ return read()
1433
+ })
1434
+ if (framingError) throw framingError
1435
+ return value
1436
+ }
1437
+ guarded.handlesRead = true
1438
+ originals.push([tag, original, guarded])
1439
+ currentExtensions[tag] = guarded
1440
+ }
1441
+ return decoder.decodeMultiple(source)
1442
+ } finally {
1443
+ fragmentEnd = savedEnd
1444
+ fragmentReadString = savedFragmentReader
1445
+ readFixedString = savedReader
1446
+ for (const [tag, original, guarded] of originals) {
1447
+ if (currentExtensions[tag] === guarded) currentExtensions[tag] = original
1448
+ }
1449
+ }
1450
+ }
1451
+
1452
+ // Internal streaming hooks; not part of the public decoder API.
1453
+ export const _stream = {
1454
+ getTag: getScanTag,
1455
+ isBuiltinTag: isBuiltinScanTag,
1456
+ checkContainer: checkScanContainer,
1457
+ decode: decodeScanned,
1458
+ get readThreshold() { return inlineObjectReadThreshold }
1459
+ }
1460
+
1334
1461
  export function addExtension(extension) {
1335
1462
  currentExtensions[extension.tag] = extension.decode
1463
+ if (typeof extension.decode === 'function') registeredExtensions.set(extension.tag, extension.decode)
1464
+ else registeredExtensions.delete(extension.tag)
1336
1465
  }
1337
1466
 
1338
1467
  export function setSizeLimits(limits) {