cbor-x 1.6.8 → 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/encode.js CHANGED
@@ -572,6 +572,11 @@ export class Encoder extends Decoder {
572
572
  return encode(json)
573
573
  }
574
574
 
575
+ // not in extensions, so class instances that reach writeObject do not pay another instanceof
576
+ if (constructor === DataView) {
577
+ writeBuffer(new Uint8Array(value.buffer, value.byteOffset, value.byteLength), makeRoom)
578
+ return
579
+ }
575
580
  // no extension found, write as a plain object
576
581
  writeObject(value)
577
582
  }
@@ -666,8 +671,22 @@ export class Encoder extends Decoder {
666
671
  size++
667
672
  }
668
673
  }
669
- target[objectOffset++ + start] = size >> 8
670
- target[objectOffset + start] = size & 0xff
674
+ if (size < 0x10000) {
675
+ // fits in a map-16 (uint16) count: back-patch the reserved 2 bytes
676
+ target[objectOffset++ + start] = size >> 8
677
+ target[objectOffset + start] = size & 0xff
678
+ } else {
679
+ // too many entries for a map-16 count: widen the already-written header
680
+ // to map-32, mirroring writeArrayHeader's 0x9a/setUint32 path. Shift the
681
+ // encoded entries 2 bytes forward to make room for the wider (uint32) count.
682
+ if (position + 2 > safeEnd)
683
+ makeRoom(position + 2)
684
+ let headerPosition = objectOffset + start
685
+ target.copyWithin(headerPosition + 4, headerPosition + 2, position)
686
+ position += 2
687
+ target[headerPosition - 1] = 0xba // map 32
688
+ targetView.setUint32(headerPosition, size)
689
+ }
671
690
  } :
672
691
  (object, skipValues) => {
673
692
  let nextTransition, transition = structures.transitions || (structures.transitions = Object.create(null))
package/index.d.ts CHANGED
@@ -66,7 +66,7 @@ import { Transform, Readable } from 'stream'
66
66
 
67
67
  export as namespace CBOR;
68
68
  export class DecoderStream extends Transform {
69
- constructor(options?: Options | { highWaterMark: number, emitClose: boolean, allowHalfOpen: boolean })
69
+ constructor(options?: Options & { maxIncompleteBufferSize?: number } | { highWaterMark: number, emitClose: boolean, allowHalfOpen: boolean, maxIncompleteBufferSize?: number })
70
70
  }
71
71
  export class EncoderStream extends Transform {
72
72
  constructor(options?: Options | { highWaterMark: number, emitClose: boolean, allowHalfOpen: boolean })
package/iterators.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Encoder } from './encode.js'
2
- import { Decoder } from './decode.js'
2
+ import { Decoder, _stream } from './decode.js'
3
+ import { StructureScanner } from './scan.js'
3
4
 
4
5
  /**
5
6
  * Given an Iterable first argument, returns an Iterable where each value is encoded as a Buffer
@@ -38,7 +39,7 @@ async function * encodeIterAsync (objectIterator, options) {
38
39
  * Given an Iterable/Iterator input which yields buffers, returns an IterableIterator which yields sync decoded objects
39
40
  * Or, given an Async Iterable/Iterator which yields promises resolving in buffers, returns an AsyncIterableIterator.
40
41
  * @param {Iterable|Iterator|AsyncIterable|AsyncIterableIterator} bufferIterator
41
- * @param {object} [options] - Decoder options
42
+ * @param {object} [options] - Decoder options, and maxIncompleteBufferSize
42
43
  * @returns {IterableIterator|Promise.<AsyncIterableIterator}
43
44
  */
44
45
  export function decodeIter (bufferIterator, options = {}) {
@@ -47,26 +48,15 @@ export function decodeIter (bufferIterator, options = {}) {
47
48
  }
48
49
 
49
50
  const decoder = new Decoder(options)
50
- let incomplete, fatalError
51
+ const chunkDecoder = new ChunkDecoder(decoder, options.maxIncompleteBufferSize)
52
+ let fatalError
51
53
  const parser = (chunk) => {
52
- let yields
53
- // if there's incomplete data from previous chunk, concatinate and try again
54
- if (incomplete) {
55
- chunk = Buffer.concat([incomplete, chunk])
56
- incomplete = undefined
57
- }
58
-
59
54
  try {
60
- yields = decoder.decodeMultiple(chunk)
61
- } catch (err) {
62
- yields = err.values
63
- if (err.incomplete) {
64
- incomplete = chunk.slice(err.lastPosition)
65
- } else {
66
- fatalError = err
67
- }
55
+ return chunkDecoder.buffer ? chunkDecoder.decodeAfterIncomplete(chunk) : decoder.decodeMultiple(chunk)
56
+ } catch (error) {
57
+ fatalError = chunkDecoder.keepIncomplete(error, chunk)
58
+ return (error && error.values) || []
68
59
  }
69
- return yields || []
70
60
  }
71
61
 
72
62
  if (typeof bufferIterator[Symbol.iterator] === 'function') {
@@ -75,6 +65,8 @@ export function decodeIter (bufferIterator, options = {}) {
75
65
  yield * parser(value)
76
66
  if (fatalError) throw fatalError
77
67
  }
68
+ const endError = chunkDecoder.end()
69
+ if (endError) throw endError
78
70
  })()
79
71
  } else if (typeof bufferIterator[Symbol.asyncIterator] === 'function') {
80
72
  return (async function * iter () {
@@ -82,6 +74,118 @@ export function decodeIter (bufferIterator, options = {}) {
82
74
  yield * parser(value)
83
75
  if (fatalError) throw fatalError
84
76
  }
77
+ const endError = chunkDecoder.end()
78
+ if (endError) throw endError
85
79
  })()
86
80
  }
87
81
  }
82
+
83
+ /**
84
+ * Keeps the bytes of a value that a chunk of a stream ends inside of, for DecoderStream and decodeIter, and decodes
85
+ * them again once enough have arrived to possibly complete it. Callers decode a chunk with decodeAfterIncomplete if
86
+ * buffer is set, or else directly with decodeMultiple, and pass any error to keepIncomplete, which returns the error
87
+ * to report, if any. end returns an error if the input ended inside a value.
88
+ */
89
+ export class ChunkDecoder {
90
+ constructor (decoder, maxIncompleteBufferSize) {
91
+ this.decoder = decoder
92
+ if (maxIncompleteBufferSize === undefined) maxIncompleteBufferSize = 0x4000000
93
+ if (typeof maxIncompleteBufferSize !== 'number' || !(maxIncompleteBufferSize >= 0)) {
94
+ throw new TypeError('maxIncompleteBufferSize must be a non-negative number')
95
+ }
96
+ this.maxIncompleteBufferSize = Math.floor(maxIncompleteBufferSize)
97
+ // buffer[0, bufferEnd) is the incomplete value; no decoded value is a view of buffer while it is kept here
98
+ this.buffer = null
99
+ this.bufferEnd = 0
100
+ this.appends = 0
101
+ this.scanner = null
102
+ this.scanned = false
103
+ this.resetScanner = false
104
+ this.neededLength = 0 // a lower bound on the incomplete value's length
105
+ }
106
+
107
+ decodeAfterIncomplete (chunk) {
108
+ if (!(ArrayBuffer.isView(chunk) && chunk.BYTES_PER_ELEMENT === 1)) {
109
+ throw new TypeError('Source must be a Uint8Array or Buffer')
110
+ }
111
+ const length = this.bufferEnd + chunk.length
112
+ if (length < this.neededLength && length > this.maxIncompleteBufferSize) throw maxIncompleteBufferSizeError()
113
+ if (length > this.buffer.length) {
114
+ // Values usually complete within two appends, which are sized exactly. After that the buffer grows by at least
115
+ // the bytes already kept, so that they are copied a bounded number of times however small the chunks are.
116
+ const size = this.appends < 2 ? length
117
+ : Math.max(length, Math.min(length + this.bufferEnd, this.maxIncompleteBufferSize))
118
+ const buffer = Buffer.allocUnsafe(size)
119
+ if (size > length) buffer.fill(0, length) // decoded values are views of this buffer, so no uninitialized memory
120
+ buffer.set(this.buffer.subarray(0, this.bufferEnd))
121
+ this.buffer = buffer
122
+ }
123
+ this.buffer.set(chunk, this.bufferEnd)
124
+ this.bufferEnd = length
125
+ this.appends++
126
+ if (length < this.neededLength) return []
127
+ if (!this.scanner) this.scanner = new StructureScanner(this.decoder)
128
+ if (this.resetScanner) {
129
+ this.scanner.reset()
130
+ this.resetScanner = false
131
+ }
132
+ if (!this.scanner.scan(this.buffer, length)) {
133
+ this.neededLength = this.scanner.neededLength
134
+ if (length > this.maxIncompleteBufferSize) throw maxIncompleteBufferSizeError()
135
+ return []
136
+ }
137
+ this.scanned = true
138
+ const values = _stream.decode(this.decoder, this.buffer.subarray(0, length), this.scanner.position)
139
+ this.scanned = false
140
+ this.buffer = null
141
+ return values
142
+ }
143
+
144
+ keepIncomplete (error, chunk) {
145
+ // an extension may throw any value, and a falsy one would read as success
146
+ if (!error) return this.fail(new Error('Decoding failed: ' + error))
147
+ if (!error.incomplete) return this.fail(error)
148
+ if (this.scanned && error.lastPosition === 0) {
149
+ error.incomplete = false
150
+ return this.fail(error)
151
+ }
152
+ const source = this.buffer ? this.buffer.subarray(0, this.bufferEnd) : chunk
153
+ const length = source.length - error.lastPosition
154
+ if (length > this.maxIncompleteBufferSize) return this.fail(maxIncompleteBufferSizeError())
155
+ if (!this.buffer || error.lastPosition > 0) {
156
+ if (length === 0) return
157
+ try {
158
+ // copied, so that the remainder does not keep in memory the chunk, or the buffer that the values before it
159
+ // are views of
160
+ this.buffer = Buffer.from(source.subarray(error.lastPosition))
161
+ } catch (copyError) {
162
+ return this.fail(copyError)
163
+ }
164
+ this.bufferEnd = length
165
+ this.appends = 0
166
+ }
167
+ this.scanned = false
168
+ this.resetScanner = true
169
+ this.neededLength = Math.max(length + 1, error.neededEnd ? error.neededEnd - error.lastPosition : 0)
170
+ }
171
+
172
+ end () {
173
+ if (this.buffer) {
174
+ const error = this.fail(new Error('Unexpected end of CBOR data'))
175
+ error.incomplete = true
176
+ return error
177
+ }
178
+ }
179
+
180
+ fail (error) {
181
+ this.buffer = null
182
+ this.scanner = null
183
+ this.scanned = false
184
+ this.resetScanner = false
185
+ return error
186
+ }
187
+ }
188
+
189
+ function maxIncompleteBufferSizeError () {
190
+ return new Error('Maximum incomplete buffer size exceeded')
191
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cbor-x",
3
3
  "author": "Kris Zyp",
4
- "version": "1.6.8",
4
+ "version": "1.6.9",
5
5
  "description": "Ultra-fast and conformant CBOR (RFC 8949) implementation with support for numerous tag extensions including records and structured cloning",
6
6
  "license": "MIT",
7
7
  "types": "./index.d.ts",
@@ -75,7 +75,7 @@
75
75
  "node:buffer": false
76
76
  },
77
77
  "optionalDependencies": {
78
- "cbor-extract": "^2.2.2"
78
+ "cbor-extract": "^2.2.3"
79
79
  },
80
80
  "devDependencies": {
81
81
  "@rollup/plugin-json": "^5.0.1",
package/scan.js ADDED
@@ -0,0 +1,253 @@
1
+ import { _stream } from './decode.js'
2
+
3
+ // Positions are relative to the retained value, including bundle pointers. Frames and their storage survive appends.
4
+ export class StructureScanner {
5
+ constructor (decoder) {
6
+ this.decoder = decoder
7
+ this.frames = []
8
+ this.records = new Map()
9
+ this.readers = new Map()
10
+ this.reset()
11
+ }
12
+
13
+ reset () {
14
+ this.position = 0
15
+ this.depth = 0
16
+ this.neededLength = 0
17
+ this.postBundlePosition = 0
18
+ this.headers = 0
19
+ this.mapsAsObjects = !!this.decoder.mapsAsObjects
20
+ this.restoreMap = false
21
+ this.records.clear()
22
+ this.readers.clear()
23
+ this.push('items', 1)
24
+ }
25
+
26
+ push (kind, remaining) {
27
+ if (this.depth === 16384) throw new RangeError('Maximum CBOR nesting depth exceeded')
28
+ const frame = this.frames[this.depth] || (this.frames[this.depth] = {})
29
+ frame.kind = kind
30
+ frame.remaining = remaining
31
+ frame.stage = 0
32
+ frame.id = undefined
33
+ frame.restoreMap = false
34
+ frame.legacy = false
35
+ frame.definition = undefined
36
+ frame.limitMajor = 0
37
+ frame.count = 0
38
+ this.depth++
39
+ return frame
40
+ }
41
+
42
+ header (source, end, lengthOnly = false) {
43
+ const start = this.position
44
+ if (start >= end) {
45
+ this.neededLength = start + 1
46
+ return false
47
+ }
48
+ const byte = source[start]
49
+ const major = byte >> 5
50
+ const info = byte & 31
51
+ const width = info < 24 || (lengthOnly && info > 26) ? 0 : info === 24 ? 1 : info === 25 ? 2 : info === 26 ? 4 : info === 27 ? 8 : 0
52
+ if (!lengthOnly && info >= 28 && info <= 30) throw new Error('Unknown token ' + info)
53
+ if (start + 1 + width > end) {
54
+ this.neededLength = start + 1 + width
55
+ return false
56
+ }
57
+ let value = info
58
+ if (width) {
59
+ value = 0
60
+ for (let i = start + 1; i <= start + width; i++) value = value * 256 + source[i]
61
+ if (width === 8 && major > 1 && major !== 7 && value > 0xffffffff) {
62
+ throw new Error('JavaScript does not support arrays, maps, or strings with length over 4294967295')
63
+ }
64
+ }
65
+ this.major = major
66
+ this.info = info
67
+ this.argument = value
68
+ this.position = start + 1 + width
69
+ this.headers++
70
+ return true
71
+ }
72
+
73
+ scan (source, end) {
74
+ while (this.depth) {
75
+ if (this.position > end) {
76
+ this.neededLength = this.position
77
+ return false
78
+ }
79
+ const frame = this.frames[this.depth - 1]
80
+ if (frame.kind === 'bundle') {
81
+ if (frame.stage === 0) {
82
+ if (!this.header(source, end, true)) return false
83
+ frame.remaining = Math.max(0, this.argument - 2)
84
+ frame.stage = 1
85
+ }
86
+ if (frame.stage === 1) {
87
+ const offsetStart = this.position
88
+ if (!this.header(source, end)) return false
89
+ if (this.major > 1 || this.info === 31) throw new Error('Invalid bundled string offset')
90
+ frame.target = offsetStart + (this.major === 1 ? -1 - this.argument : this.argument)
91
+ if (!Number.isSafeInteger(frame.target) || frame.target < 0) throw new Error('Invalid bundled string offset')
92
+ frame.stage = 2
93
+ }
94
+ if (frame.stage === 2) {
95
+ if (frame.remaining) {
96
+ if (!this.header(source, end, true)) return false
97
+ this.position += this.argument
98
+ frame.remaining--
99
+ continue
100
+ }
101
+ frame.resume = this.position
102
+ this.position = frame.target
103
+ frame.remaining = 2
104
+ frame.stage = 3
105
+ continue
106
+ }
107
+ if (frame.stage === 3) {
108
+ if (frame.remaining) {
109
+ if (!this.header(source, end, true)) return false
110
+ this.position += this.argument
111
+ frame.remaining--
112
+ continue
113
+ }
114
+ this.postBundlePosition = this.position
115
+ this.position = frame.resume
116
+ frame.kind = 'items'
117
+ frame.remaining = 1
118
+ continue
119
+ }
120
+ }
121
+ if (frame.kind === 'record') {
122
+ if (!this.header(source, end, true)) return false
123
+ if (this.info > 26) throw new Error('Expected array header, but got ' + source[this.position - 1])
124
+ frame.kind = 'items'
125
+ frame.remaining = this.argument
126
+ const reader = frame.reader
127
+ if (reader.compiled.has(this.argument)) frame.remaining = Math.min(this.argument, reader.length)
128
+ else if (reader.slowReads++ >= _stream.readThreshold) {
129
+ reader.compiled.add(this.argument)
130
+ frame.remaining = Math.min(this.argument, reader.length)
131
+ }
132
+ continue
133
+ }
134
+ if (frame.kind === 'definition' && frame.stage === 0) {
135
+ const start = this.position
136
+ if (!this.header(source, end, !frame.legacy)) return false
137
+ if (frame.legacy && this.major !== 4) {
138
+ this.position = start
139
+ this.headers--
140
+ frame.kind = 'items'
141
+ frame.remaining = 1
142
+ continue
143
+ }
144
+ frame.remaining = frame.legacy ? (this.info === 31 ? -1 : this.argument) : Math.max(2, this.argument)
145
+ if (frame.legacy) {
146
+ if (frame.remaining >= 0) _stream.checkContainer(4, frame.remaining)
147
+ else frame.limitMajor = 4
148
+ frame.index = 0
149
+ }
150
+ frame.total = frame.remaining
151
+ frame.stage = 1
152
+ continue
153
+ }
154
+ if (frame.remaining === 0) {
155
+ if (frame.definition) this.records.set(frame.id - 0xe000, frame.definition)
156
+ if (frame.restoreMap && this.restoreMap) {
157
+ this.mapsAsObjects = true
158
+ this.restoreMap = false
159
+ }
160
+ this.depth--
161
+ continue
162
+ }
163
+ if (!this.header(source, end)) return false
164
+ let { major, argument, info } = this
165
+ if (frame.kind === 'definition') {
166
+ const index = frame.legacy ? frame.index++ : frame.total - frame.remaining
167
+ if (index === 0 && major === 0) frame.id = argument
168
+ if ((frame.multiple ? index >= 1 && index < frame.total - 1 : index === 1) && major === 4 && info !== 31 && frame.id !== undefined) {
169
+ const definition = { length: argument, slowReads: 0, compiled: new Set() }
170
+ if (frame.legacy) frame.definition = definition
171
+ else this.records.set(frame.id - 0xe000 + (frame.multiple ? index - 1 : 0), definition)
172
+ }
173
+ }
174
+ if (major === 7 && info === 31 && (frame.remaining === -1 || frame.remaining === -2)) {
175
+ if (frame.definition) this.records.set(frame.id - 0xe000, frame.definition)
176
+ this.depth--
177
+ continue
178
+ }
179
+ if (frame.limitMajor && (frame.remaining === -1 || frame.remaining === -2)) {
180
+ _stream.checkContainer(frame.limitMajor, ++frame.count, frame.mapsAsObjects)
181
+ }
182
+ if (frame.remaining > 0) frame.remaining--
183
+ else if (frame.remaining === -2) frame.remaining = -3
184
+ else if (frame.remaining === -3) frame.remaining = -2
185
+ if (info === 31) {
186
+ if (major === 2 || major === 3) throw new Error('Indefinite length not supported for byte or text strings')
187
+ if (major === 4 || major === 5) {
188
+ const container = this.push('items', major === 4 ? -1 : -2)
189
+ container.limitMajor = major
190
+ container.mapsAsObjects = this.mapsAsObjects
191
+ if (major === 5 && this.restoreMap) {
192
+ this.mapsAsObjects = true
193
+ this.restoreMap = false
194
+ }
195
+ }
196
+ else if (major === 7) throw new Error('Unexpected break stop code outside of an indefinite-length item')
197
+ else throw new Error('Invalid major type for indefinite length ' + major)
198
+ continue
199
+ }
200
+ switch (major) {
201
+ case 2:
202
+ case 3:
203
+ this.position += argument
204
+ break
205
+ case 4:
206
+ case 5:
207
+ _stream.checkContainer(major, argument, this.mapsAsObjects)
208
+ if (major === 5 && this.restoreMap) {
209
+ this.mapsAsObjects = true
210
+ this.restoreMap = false
211
+ }
212
+ if (argument) this.push('items', major === 5 ? argument * 2 : argument)
213
+ break
214
+ case 6: {
215
+ const structure = _stream.getTag(argument, this.decoder)
216
+ let reader = this.records.get(argument & 0x1fff)
217
+ if (argument >= 0xdff9 && (structure || reader)) {
218
+ if (!reader) {
219
+ reader = this.readers.get(structure)
220
+ if (!reader) {
221
+ reader = { length: structure.length, slowReads: structure.slowReads || 0, compiled: new Set() }
222
+ for (let compiled = structure.compiledReader; compiled; compiled = compiled.next) reader.compiled.add(compiled.propertyCount)
223
+ this.readers.set(structure, reader)
224
+ }
225
+ }
226
+ this.push('record').reader = reader
227
+ } else if (argument === 0xdff9) {
228
+ this.push('bundle')
229
+ } else if (argument === 0xdfff || argument === 0xdffe) {
230
+ this.push('definition').multiple = argument === 0xdffe
231
+ } else if (argument === 259 && _stream.isBuiltinTag(argument) && this.mapsAsObjects) {
232
+ this.push('items', 1).restoreMap = true
233
+ this.mapsAsObjects = false
234
+ this.restoreMap = true
235
+ } else if (argument === 105 && _stream.isBuiltinTag(argument)) {
236
+ const definition = this.push('definition')
237
+ definition.multiple = false
238
+ definition.legacy = true
239
+ } else {
240
+ this.push('items', 1)
241
+ }
242
+ break
243
+ }
244
+ }
245
+ }
246
+ if (this.postBundlePosition) {
247
+ if (this.position >= this.postBundlePosition) throw new Error('Unexpected bundle position')
248
+ this.position = this.postBundlePosition
249
+ }
250
+ if (this.frames.length > 256) this.frames.length = 256
251
+ return true
252
+ }
253
+ }
package/stream.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Transform } from 'stream'
2
2
  import { Encoder } from './encode.js'
3
3
  import { checkedRead, getPosition, Decoder, clearSource } from './decode.js'
4
+ import { ChunkDecoder } from './iterators.js'
4
5
  var DEFAULT_OPTIONS = {objectMode: true}
5
6
 
6
7
  export class EncoderStream extends Transform {
@@ -30,21 +31,16 @@ export class DecoderStream extends Transform {
30
31
  super(options)
31
32
  options.structures = []
32
33
  this.decoder = options.decoder || new Decoder(options)
34
+ this.chunkDecoder = new ChunkDecoder(this.decoder, options.maxIncompleteBufferSize)
33
35
  }
34
36
  _transform(chunk, encoding, callback) {
35
- if (this.incompleteBuffer) {
36
- chunk = Buffer.concat([this.incompleteBuffer, chunk])
37
- this.incompleteBuffer = null
38
- }
39
37
  let values, fatalError
40
38
  try {
41
- values = this.decoder.decodeMultiple(chunk)
39
+ // decodeMultiple is called directly, since another call level here measurably slows small writes
40
+ values = this.chunkDecoder.buffer ? this.chunkDecoder.decodeAfterIncomplete(chunk) : this.decoder.decodeMultiple(chunk)
42
41
  } catch(error) {
43
- values = error.values
44
- if (error.incomplete)
45
- this.incompleteBuffer = chunk.slice(error.lastPosition)
46
- else
47
- fatalError = error
42
+ values = error && error.values
43
+ fatalError = this.chunkDecoder.keepIncomplete(error, chunk)
48
44
  }
49
45
  for (let value of values || []) {
50
46
  if (value === null)
@@ -53,6 +49,9 @@ export class DecoderStream extends Transform {
53
49
  }
54
50
  callback(fatalError)
55
51
  }
52
+ _flush(callback) {
53
+ callback(this.chunkDecoder.end())
54
+ }
56
55
  getNullValue() {
57
56
  return Symbol.for(null)
58
57
  }