msgpackr 1.11.11 → 1.11.13

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "msgpackr",
3
3
  "author": "Kris Zyp",
4
- "version": "1.11.11",
4
+ "version": "1.11.13",
5
5
  "description": "Ultra-fast MessagePack implementation with extensions for records and structured cloning",
6
6
  "license": "MIT",
7
7
  "types": "./index.d.ts",
package/stream.js CHANGED
@@ -25,6 +25,7 @@ export class UnpackrStream extends Transform {
25
25
  options.objectMode = true
26
26
  super(options)
27
27
  options.structures = []
28
+ this.maxIncompleteBufferSize = options.maxIncompleteBufferSize !== undefined ? options.maxIncompleteBufferSize : 0x4000000
28
29
  this.unpackr = options.unpackr || new Unpackr(options)
29
30
  }
30
31
  _transform(chunk, encoding, callback) {
@@ -37,19 +38,23 @@ export class UnpackrStream extends Transform {
37
38
  values = this.unpackr.unpackMultiple(chunk)
38
39
  } catch(error) {
39
40
  if (error.incomplete) {
40
- this.incompleteBuffer = chunk.slice(error.lastPosition)
41
+ let incompleteBuffer = chunk.slice(error.lastPosition)
42
+ if (incompleteBuffer.length > this.maxIncompleteBufferSize) {
43
+ this.incompleteBuffer = null
44
+ return callback(new Error('Maximum incomplete buffer size exceeded'))
45
+ }
46
+ this.incompleteBuffer = incompleteBuffer
41
47
  values = error.values
48
+ } else {
49
+ return callback(error)
42
50
  }
43
- else
44
- throw error
45
- } finally {
46
- for (let value of values || []) {
47
- if (value === null)
48
- value = this.getNullValue()
49
- this.push(value)
50
- }
51
51
  }
52
- if (callback) callback()
52
+ for (let value of values || []) {
53
+ if (value === null)
54
+ value = this.getNullValue()
55
+ this.push(value)
56
+ }
57
+ callback()
53
58
  }
54
59
  getNullValue() {
55
60
  return Symbol.for(null)
package/struct.js CHANGED
@@ -69,9 +69,16 @@ const encodeUtf8 = hasNodeBuffer ? function(target, string, position) {
69
69
  const TYPE = Symbol('type');
70
70
  const PARENT = Symbol('parent');
71
71
  setWriteStructSlots(writeStruct, prepareStructures);
72
- function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr) {
72
+ function writeStruct(object, target, encodingStart, position, structures, makeRoom, pack, packr, structureKnown) {
73
73
  let typedStructs = packr.typedStructs || (packr.typedStructs = []);
74
74
  // note that we rely on pack.js to load stored structures before we get to this point
75
+ // structureKnown is set only on the internal layout-retry below: attempt 1 already minted
76
+ // this record's structure, so the retry re-encodes a known shape and must not re-apply the
77
+ // cap (which could otherwise bail after attempt 1 already packed refs → corrupt fallback).
78
+ // `frozen` is a local (from this instance's typedStructs) — never a shared global — so a
79
+ // re-entrant encode on another instance (e.g. via an enumerable getter) can't flip it.
80
+ const cap = packr.maxOwnStructures ?? Infinity;
81
+ const frozen = !structureKnown && typedStructs.length >= cap;
75
82
  let targetView = target.dataView;
76
83
  let refsStartPosition = (typedStructs.lastStringStart || 100) + position;
77
84
  let safeEnd = target.length - 10;
@@ -102,9 +109,12 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
102
109
  let usedAscii0;
103
110
  let keyIndex = 0;
104
111
  for (let key in object) {
105
- let value = object[key];
106
112
  let nextTransition = transition[key];
113
+ // Resolve the key transition BEFORE reading the value: when frozen and the key is new we
114
+ // bail here, so an enumerable getter isn't invoked during this (failed) struct attempt and
115
+ // then again by the plain fallback (which would double-read a side-effecting accessor).
107
116
  if (!nextTransition) {
117
+ if (frozen) return 0;
108
118
  transition[key] = nextTransition = {
109
119
  key,
110
120
  parent: transition,
@@ -119,6 +129,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
119
129
  date64: null
120
130
  };
121
131
  }
132
+ let value = object[key];
122
133
  if (position > safeEnd) {
123
134
  target = makeRoom(position);
124
135
  targetView = target.dataView;
@@ -136,10 +147,10 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
136
147
  if (nextId < 200 || !nextTransition.num64) {
137
148
  if (number >> 0 === number && number < 0x20000000 && number > -0x1f000000) {
138
149
  if (number < 0xf6 && number >= 0 && (nextTransition.num8 && !(nextId > 200 && nextTransition.num32) || number < 0x20 && !nextTransition.num32)) {
139
- transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1);
150
+ transition = nextTransition.num8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
140
151
  target[position++] = number;
141
152
  } else {
142
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
153
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
143
154
  targetView.setUint32(position, number, true);
144
155
  position += 4;
145
156
  }
@@ -150,14 +161,14 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
150
161
  let xShifted
151
162
  // this checks for rounding of numbers that were encoded in 32-bit float to nearest significant decimal digit that could be preserved
152
163
  if (((xShifted = number * mult10[((target[position + 3] & 0x7f) << 1) | (target[position + 2] >> 7)]) >> 0) === xShifted) {
153
- transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4);
164
+ transition = nextTransition.num32 || createTypeTransition(nextTransition, NUMBER, 4, frozen);
154
165
  position += 4;
155
166
  break;
156
167
  }
157
168
  }
158
169
  }
159
170
  }
160
- transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8);
171
+ transition = nextTransition.num64 || createTypeTransition(nextTransition, NUMBER, 8, frozen);
161
172
  targetView.setFloat64(position, number, true);
162
173
  position += 8;
163
174
  break;
@@ -223,21 +234,21 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
223
234
  nextTransition.string8 = transition;
224
235
  pack(null, 0, true); // special call to notify that structures have been updated
225
236
  } else {
226
- transition = createTypeTransition(nextTransition, UTF8, 1);
237
+ transition = createTypeTransition(nextTransition, UTF8, 1, frozen);
227
238
  }
228
239
  }
229
240
  } else if (refOffset === 0 && !usedAscii0) {
230
241
  usedAscii0 = true;
231
- transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0);
242
+ transition = nextTransition.ascii0 || createTypeTransition(nextTransition, ASCII, 0, frozen);
232
243
  break; // don't increment position
233
244
  }// else ascii:
234
245
  else if (!(transition = nextTransition.ascii8) && !(typedStructs.length > 10 && (transition = nextTransition.string8)))
235
- transition = createTypeTransition(nextTransition, ASCII, 1);
246
+ transition = createTypeTransition(nextTransition, ASCII, 1, frozen);
236
247
  target[position++] = refOffset;
237
248
  } else {
238
249
  // TODO: Enable ascii16 at some point, but get the logic right
239
250
  //if (isNotAscii)
240
- transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2);
251
+ transition = nextTransition.string16 || createTypeTransition(nextTransition, UTF8, 2, frozen);
241
252
  //else
242
253
  //transition = nextTransition.ascii16 || createTypeTransition(nextTransition, ASCII, 2);
243
254
  targetView.setUint16(position, refOffset, true);
@@ -247,7 +258,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
247
258
  case 'object':
248
259
  if (value) {
249
260
  if (value.constructor === Date) {
250
- transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8);
261
+ transition = nextTransition.date64 || createTypeTransition(nextTransition, DATE, 8, frozen);
251
262
  targetView.setFloat64(position, value.getTime(), true);
252
263
  position += 8;
253
264
  } else {
@@ -263,7 +274,7 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
263
274
  }
264
275
  break;
265
276
  case 'boolean':
266
- transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1);
277
+ transition = nextTransition.num8 || nextTransition.ascii8 || createTypeTransition(nextTransition, NUMBER, 1, frozen);
267
278
  target[position++] = value ? 0xf9 : 0xf8; // match CBOR with these
268
279
  break;
269
280
  case 'undefined':
@@ -276,9 +287,41 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
276
287
  default:
277
288
  queuedReferences.push(key, value, keyIndex);
278
289
  }
290
+ if (transition === undefined) return 0; // frozen: structure cap reached
279
291
  keyIndex++;
280
292
  }
281
293
 
294
+ // Cap enforcement for queued (nested-object / null) references. pack() advances msgpackr's
295
+ // shared write position and we cannot cleanly bail afterward, so preflight the whole queued
296
+ // chain through EXISTING transitions first: if the cap is reached and any field would need a
297
+ // new structure, fall back to plain encoding now (return 0) — before touching the shared
298
+ // position. Uses a FRESH length read (not the entry-time `frozen`): a getter invoked while
299
+ // reading values above may have minted on this same instance since entry.
300
+ if (!structureKnown && queuedReferences.length > 0 && typedStructs.length >= cap) {
301
+ let t = transition;
302
+ for (let i = 0, l = queuedReferences.length; i < l; i += 3) {
303
+ // A non-null (object/Date) ref is pack()ed into the shared buffer, advancing
304
+ // msgpackr's write position. Its structure variant (object16 vs object32) depends on
305
+ // the runtime ref-section offset (inline strings + earlier refs), which we can't know
306
+ // before packing — and we can't bail after a pack without corrupting the fallback. So
307
+ // under the cap, any record with a packing ref falls back to plain encoding now,
308
+ // before any pack(). null/undefined refs don't pack, so they're walked normally.
309
+ if (queuedReferences[i + 1] != null) return 0;
310
+ const nt = t[queuedReferences[i]];
311
+ if (!nt) return 0;
312
+ const next = nt.object16; // null/undefined ref → OBJECT_DATA size 2
313
+ if (!next) return 0;
314
+ t = next;
315
+ }
316
+ if (t[RECORD_SYMBOL] == null) return 0; // exact structure not yet minted
317
+ }
318
+
319
+ // Past the preflight the chain is known, so no minting happens — except a rare offset
320
+ // divergence (a known shape whose ref section now crosses 0xff00 and needs object32 where
321
+ // the preflight matched object16). Once a ref is packed we can no longer bail, so we finish
322
+ // via the unfrozen forceTypeTransition: a bounded, self-converging overshoot for that one
323
+ // record. packedRef keeps the record-id mint from bailing after a pack.
324
+ let packedRef = false;
282
325
  for (let i = 0, l = queuedReferences.length; i < l;) {
283
326
  let key = queuedReferences[i++];
284
327
  let value = queuedReferences[i++];
@@ -300,15 +343,6 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
300
343
  }
301
344
  let newPosition;
302
345
  if (value) {
303
- /*if (typeof value === 'string') { // TODO: we could re-enable long strings
304
- if (position + value.length * 3 > safeEnd) {
305
- target = makeRoom(position + value.length * 3);
306
- position -= start;
307
- targetView = target.dataView;
308
- start = 0;
309
- }
310
- newPosition = position + target.utf8Write(value, position, 0xffffffff);
311
- } else { */
312
346
  let size;
313
347
  refOffset = refPosition - refsStartPosition;
314
348
  if (refOffset < 0xff00) {
@@ -318,15 +352,15 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
318
352
  else if ((transition = nextTransition.object32))
319
353
  size = 4;
320
354
  else {
321
- transition = createTypeTransition(nextTransition, OBJECT_DATA, 2);
355
+ transition = forceTypeTransition(nextTransition, OBJECT_DATA, 2);
322
356
  size = 2;
323
357
  }
324
358
  } else {
325
- transition = nextTransition.object32 || createTypeTransition(nextTransition, OBJECT_DATA, 4);
359
+ transition = nextTransition.object32 || forceTypeTransition(nextTransition, OBJECT_DATA, 4);
326
360
  size = 4;
327
361
  }
328
362
  newPosition = pack(value, refPosition);
329
- //}
363
+ packedRef = true;
330
364
  if (typeof newPosition === 'object') {
331
365
  // re-allocated
332
366
  refPosition = newPosition.position;
@@ -346,16 +380,19 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
346
380
  position += 4;
347
381
  }
348
382
  } else { // null or undefined
349
- transition = nextTransition.object16 || createTypeTransition(nextTransition, OBJECT_DATA, 2);
383
+ transition = nextTransition.object16 || forceTypeTransition(nextTransition, OBJECT_DATA, 2);
350
384
  targetView.setInt16(position, value === null ? -10 : -9, true);
351
385
  position += 2;
352
386
  }
353
387
  keyIndex++;
354
388
  }
355
389
 
356
-
357
390
  let recordId = transition[RECORD_SYMBOL];
358
391
  if (recordId == null) {
392
+ // Flat records (no queued refs) reach here without packing, so the cap is enforced
393
+ // cleanly. Records that packed nested refs already passed the preflight; either way
394
+ // bailing now after refs were packed would corrupt the fallback.
395
+ if (!packedRef && typedStructs.length >= cap) return 0;
359
396
  recordId = packr.typedStructs.length;
360
397
  let structure = [];
361
398
  let nextTransition = transition;
@@ -409,7 +446,11 @@ function writeStruct(object, target, encodingStart, position, structures, makeRo
409
446
  if (refsStartPosition === refPosition)
410
447
  return position; // no refs
411
448
  typedStructs.lastStringStart = position - start;
412
- return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr);
449
+ // Fixed section overflowed our estimate retry with the corrected size. The structure
450
+ // is already minted at this point, so pass structureKnown=true to skip the cap check
451
+ // (otherwise a record that became frozen during attempt 1 would bail mid-retry, after
452
+ // refs were already packed, and corrupt the fallback).
453
+ return writeStruct(object, target, encodingStart, start, structures, makeRoom, pack, packr, true);
413
454
  }
414
455
  return refPosition;
415
456
  }
@@ -441,9 +482,36 @@ function anyType(transition, position, targetView, value) {
441
482
  // TODO: can we do an "any" type where we defer the decision?
442
483
  return;
443
484
  }
444
- function createTypeTransition(transition, type, size) {
485
+ // When the typed-structure dictionary reaches maxOwnStructures we stop minting new
486
+ // structures/transitions. typedStructs is append-only and pinned on the long-lived
487
+ // encoder (records reference structures by recordId), so an unbounded shape space —
488
+ // e.g. a wide, sparsely/variably-populated schema — would otherwise grow the
489
+ // dictionary + transition trie without limit. `frozen` is passed in (derived from the
490
+ // encoding instance's own typedStructs.length, never a shared global) so a re-entrant
491
+ // encode on another instance can't flip it; while frozen, a missing transition returns
492
+ // undefined so the caller bails and the record falls back to plain encoding.
493
+ function createTypeTransition(transition, type, size, frozen) {
494
+ let typeName = TYPE_NAMES[type] + (size << 3);
495
+ let newTransition = transition[typeName];
496
+ if (newTransition) return newTransition;
497
+ if (frozen) return undefined;
498
+ newTransition = transition[typeName] = Object.create(null);
499
+ newTransition.__type = type;
500
+ newTransition.__size = size;
501
+ newTransition.__parent = transition;
502
+ return newTransition;
503
+ }
504
+
505
+ // Unfrozen variant: always mints. Used in the queued-ref loop once a nested value has
506
+ // already been pack()ed — at that point pack() has advanced msgpackr's shared write
507
+ // position, so bailing with `return 0` would corrupt the fallback. We must finish the
508
+ // encode instead, even if that means minting a (bounded) handful of structures past the
509
+ // cap. The cap is still enforced up front via the preflight, before the first pack().
510
+ function forceTypeTransition(transition, type, size) {
445
511
  let typeName = TYPE_NAMES[type] + (size << 3);
446
- let newTransition = transition[typeName] || (transition[typeName] = Object.create(null));
512
+ let newTransition = transition[typeName];
513
+ if (newTransition) return newTransition;
514
+ newTransition = transition[typeName] = Object.create(null);
447
515
  newTransition.__type = type;
448
516
  newTransition.__size = size;
449
517
  newTransition.__parent = transition;
@@ -477,7 +545,8 @@ function onLoadedStructures(sharedData) {
477
545
  date64: null,
478
546
  };
479
547
  }
480
- transition = createTypeTransition(nextTransition, type, size);
548
+ // Replaying persisted structures is never subject to the cap — always mint.
549
+ transition = createTypeTransition(nextTransition, type, size, false);
481
550
  }
482
551
  transition[RECORD_SYMBOL] = i;
483
552
  }