@evolu/common 8.6.1 → 8.7.0

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/src/Buffer.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  /**
2
2
  * Binary data handling and byte array utilities.
3
3
  *
4
+ * Buffer-based decoding functions intentionally throw errors instead of
5
+ * returning {@link Result}. This is a deliberate micro-optimization for Evolu
6
+ * Protocol's hot paths: Result is inexpensive, but returning decoded values
7
+ * directly avoids its success-case allocation, and using `Error` objects
8
+ * preserves stack traces. In the future, we will try returning `Error` objects
9
+ * in `Result` values to measure the real-world performance impact.
10
+ *
4
11
  * @module
5
12
  */
6
13
 
7
14
  import type { Result } from "./Result.ts";
8
- import { NonNegativeInt, zeroNonNegativeInt } from "./Type.ts";
15
+ import type { JsonValue, NonNegativeInt } from "./Type.ts";
9
16
  export {
10
17
  bytesToHex,
11
18
  bytesToUtf8,
@@ -41,12 +48,7 @@ export class BufferError extends Error {
41
48
  * reused within functions by leveraging `reset` to clear contents while
42
49
  * preserving capacity, or `truncate` to adjust the length to a specific size,
43
50
  * reducing the need for new allocations. Pass Buffers to `encode*` functions to
44
- * append serialized data and use `decode*` functions to extract data. Both
45
- * `shift` and `shiftN` throw an {@link BufferError} with message "Buffer parse
46
- * ended prematurely" on failure, as do higher-level `decode*` functions,
47
- * providing stack traces for debugging instead of using {@link Result}. This
48
- * avoids allocation overhead in success cases and leverages exceptions'
49
- * diagnostic benefits.
51
+ * append serialized data and use `decode*` functions to extract data.
50
52
  *
51
53
  * ### Example
52
54
  *
@@ -101,7 +103,7 @@ export interface Buffer {
101
103
 
102
104
  /**
103
105
  * Appends binary data to the buffer, resizing if necessary. Throws if
104
- * `arg.length` is not a non-negative integer.
106
+ * `arg.length` is not a non-negative safe integer.
105
107
  */
106
108
  extend: (arg: Uint8Array | ArrayLike<number>) => void;
107
109
 
@@ -144,18 +146,26 @@ export interface Buffer {
144
146
  export const createBuffer = (
145
147
  arrayLike?: Uint8Array | ArrayLike<number>,
146
148
  ): Buffer => {
149
+ const initialLength = arrayLike?.length ?? 0;
150
+ assertNonNegativeInt(initialLength, "arrayLike.length");
151
+
147
152
  let value = arrayLike
148
153
  ? new globalThis.Uint8Array(arrayLike)
149
154
  : new globalThis.Uint8Array(512);
150
- let length = NonNegativeInt.orThrow(arrayLike ? arrayLike.length : 0);
155
+ let length = initialLength;
151
156
 
152
157
  const buffer: Buffer = {
153
- getCapacity: () => NonNegativeInt.orThrow(value.length),
158
+ getCapacity: () => value.length as NonNegativeInt,
154
159
 
155
160
  getLength: () => length,
156
161
 
157
162
  extend: (arg) => {
158
- const targetSize = length + arg.length;
163
+ const argLength = arg.length;
164
+ assertNonNegativeInt(argLength, "arg.length");
165
+
166
+ const targetSize = length + argLength;
167
+ assertNonNegativeInt(targetSize, "Buffer length");
168
+
159
169
  if (value.length < targetSize) {
160
170
  const oldValue = value;
161
171
  const newCapacity = Math.max(value.length * 2, targetSize);
@@ -163,26 +173,22 @@ export const createBuffer = (
163
173
  value.set(oldValue);
164
174
  }
165
175
  value.set(arg, length);
166
- length = NonNegativeInt.orThrow(length + arg.length);
176
+ length = targetSize;
167
177
  },
168
178
 
169
179
  shift: () => {
170
- if (length === 0) {
171
- throw new BufferError("Buffer parse ended prematurely");
172
- }
180
+ assertBufferHasRemainingBytes(length, 1);
173
181
  const first = value[0];
174
182
  value = value.subarray(1);
175
183
  length--;
176
- return NonNegativeInt.orThrow(first);
184
+ return first as NonNegativeInt;
177
185
  },
178
186
 
179
187
  shiftN: (n) => {
180
- if (length < n) {
181
- throw new BufferError("Buffer parse ended prematurely");
182
- }
188
+ assertBufferHasRemainingBytes(length, n);
183
189
  const subarray = value.subarray(0, n);
184
190
  value = value.subarray(n);
185
- length = NonNegativeInt.orThrow(length - n);
191
+ length = (length - n) as NonNegativeInt;
186
192
  return subarray;
187
193
  },
188
194
 
@@ -196,11 +202,949 @@ export const createBuffer = (
196
202
  },
197
203
 
198
204
  reset: () => {
199
- length = zeroNonNegativeInt;
205
+ length = 0 as NonNegativeInt;
200
206
  },
201
207
 
202
- unwrap: () => value.subarray(0, length),
208
+ unwrap: () => (value.length === length ? value : value.subarray(0, length)),
203
209
  };
204
210
 
205
211
  return buffer;
206
212
  };
213
+
214
+ // Inspired by msgpackr 2.0.5, licensed under the MIT License.
215
+ // This implementation is specialized for Evolu's JsonValue domain.
216
+ //
217
+ // MIT License
218
+ //
219
+ // Copyright (c) 2020 Kris Zyp
220
+ //
221
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
222
+ // of this software and associated documentation files (the "Software"), to deal
223
+ // in the Software without restriction, including without limitation the rights
224
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
225
+ // copies of the Software, and to permit persons to whom the Software is
226
+ // furnished to do so, subject to the following conditions:
227
+ //
228
+ // The above copyright notice and this permission notice shall be included in all
229
+ // copies or substantial portions of the Software.
230
+ //
231
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
232
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
233
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
234
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
235
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
236
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
237
+ // SOFTWARE.
238
+
239
+ interface JsonKeyCacheEntry {
240
+ readonly bytes: Uint8Array;
241
+ readonly value: string;
242
+ }
243
+
244
+ const jsonKeyCacheSize = 4096;
245
+ const maxCachedJsonKeyByteLength = 32;
246
+ const maxJsonNestingDepth = 1_000;
247
+ let jsonEncoderTarget = new globalThis.Uint8Array(8192);
248
+ let jsonEncoderTargetView = new globalThis.DataView(jsonEncoderTarget.buffer);
249
+ let jsonEncoderPosition = 0;
250
+ let jsonEncoderDepth = 0;
251
+ let jsonEncoderIsActive = false;
252
+ const emptyJsonDecoderSource: Uint8Array = new globalThis.Uint8Array(0);
253
+ const emptyJsonDecoderView: DataView = new globalThis.DataView(
254
+ emptyJsonDecoderSource.buffer,
255
+ );
256
+ let jsonDecoderSource: Uint8Array = emptyJsonDecoderSource;
257
+ let jsonDecoderView: DataView = emptyJsonDecoderView;
258
+ let jsonDecoderPosition = 0;
259
+ let jsonDecoderDepth = 0;
260
+ const jsonStringFromCharCode = globalThis.String.fromCharCode;
261
+ // Cache only short keys and use a fixed table to bound retained memory.
262
+ const jsonKeyCache: Array<JsonKeyCacheEntry | undefined> =
263
+ globalThis.Array.from({ length: jsonKeyCacheSize }, () => undefined);
264
+
265
+ /**
266
+ * Encodes a {@link JsonValue} using the MessagePack format.
267
+ *
268
+ * ### Example
269
+ *
270
+ * ```ts
271
+ * import {
272
+ * assertEqual,
273
+ * createBuffer,
274
+ * encodeJsonValue,
275
+ * JsonValue,
276
+ * } from "@evolu/common";
277
+ *
278
+ * const buffer = createBuffer();
279
+ * const value = JsonValue.orThrow({ name: "Ada" });
280
+ *
281
+ * encodeJsonValue(buffer, value);
282
+ *
283
+ * assertEqual(
284
+ * buffer.unwrap(),
285
+ * new Uint8Array([
286
+ * 0x81, 0xa4, 0x6e, 0x61, 0x6d, 0x65, 0xa3, 0x41, 0x64, 0x61,
287
+ * ]),
288
+ * );
289
+ * ```
290
+ *
291
+ * Encoding is artificially limited to 1,000 nested arrays or objects to keep
292
+ * recursive encoding and decoding safe and symmetric. JSON data should not
293
+ * require such depth; flatten or split deeply nested data, or use a
294
+ * purpose-built serialization format.
295
+ */
296
+ export const encodeJsonValue = (buffer: Buffer, value: JsonValue): void => {
297
+ if (jsonEncoderIsActive) {
298
+ throw new BufferError("Reentrant JSON encoding is not supported.");
299
+ }
300
+ jsonEncoderIsActive = true;
301
+ jsonEncoderPosition = 0;
302
+
303
+ try {
304
+ encodeJsonValueToTarget(value);
305
+ buffer.extend(jsonEncoderTarget.subarray(0, jsonEncoderPosition));
306
+ } finally {
307
+ jsonEncoderPosition = 0;
308
+ jsonEncoderDepth = 0;
309
+ jsonEncoderIsActive = false;
310
+ }
311
+ };
312
+
313
+ /**
314
+ * Decodes a {@link JsonValue} using the MessagePack format.
315
+ *
316
+ * Throws a {@link BufferError} without modifying the Buffer if the encoded value
317
+ * is malformed, truncated, unsupported, outside the JsonValue domain, or
318
+ * exceeds 1,000 nested arrays or objects.
319
+ *
320
+ * ### Example
321
+ *
322
+ * ```ts
323
+ * import {
324
+ * assertEqual,
325
+ * createBuffer,
326
+ * decodeJsonValue,
327
+ * } from "@evolu/common";
328
+ *
329
+ * const buffer = createBuffer([
330
+ * 0x81, 0xa4, 0x6e, 0x61, 0x6d, 0x65, 0xa3, 0x41, 0x64, 0x61,
331
+ * ]);
332
+ *
333
+ * assertEqual(decodeJsonValue(buffer), { name: "Ada" });
334
+ * assertEqual(buffer.unwrap(), new Uint8Array());
335
+ * ```
336
+ */
337
+ export const decodeJsonValue = (buffer: Buffer): JsonValue => {
338
+ const source = buffer.unwrap();
339
+ jsonDecoderSource = source;
340
+ jsonDecoderView = new globalThis.DataView(
341
+ source.buffer,
342
+ source.byteOffset,
343
+ source.byteLength,
344
+ );
345
+ jsonDecoderPosition = 0;
346
+
347
+ try {
348
+ const value = decodeJsonValueFromSource();
349
+ buffer.shiftN(jsonDecoderPosition as NonNegativeInt);
350
+ return value;
351
+ } catch (error) {
352
+ if (error instanceof BufferError) throw error;
353
+
354
+ throw new BufferError("Invalid MessagePack data");
355
+ } finally {
356
+ jsonDecoderSource = emptyJsonDecoderSource;
357
+ jsonDecoderView = emptyJsonDecoderView;
358
+ jsonDecoderPosition = 0;
359
+ jsonDecoderDepth = 0;
360
+ }
361
+ };
362
+
363
+ const encodeJsonValueToTarget = (value: JsonValue): void => {
364
+ if (value === null) {
365
+ writeJsonEncoderByte(0xc0);
366
+ return;
367
+ }
368
+
369
+ // oxlint-disable-next-line typescript/switch-exhaustiveness-check -- JsonValue excludes the additional runtime types reported by tsgolint.
370
+ switch (typeof value) {
371
+ case "string":
372
+ encodeJsonStringToTarget(value);
373
+ return;
374
+ case "number": {
375
+ if (globalThis.Object.is(value, -0)) {
376
+ ensureJsonEncoderCapacity(9);
377
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xcb;
378
+ jsonEncoderTargetView.setFloat64(jsonEncoderPosition, value);
379
+ jsonEncoderPosition += 8;
380
+ return;
381
+ }
382
+
383
+ if (value >>> 0 === value) {
384
+ if (value < 0x80) {
385
+ writeJsonEncoderByte(value);
386
+ } else if (value < 0x100) {
387
+ ensureJsonEncoderCapacity(2);
388
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xcc;
389
+ jsonEncoderTarget[jsonEncoderPosition++] = value;
390
+ } else if (value < 0x10000) {
391
+ ensureJsonEncoderCapacity(3);
392
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xcd;
393
+ jsonEncoderTargetView.setUint16(jsonEncoderPosition, value);
394
+ jsonEncoderPosition += 2;
395
+ } else {
396
+ ensureJsonEncoderCapacity(5);
397
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xce;
398
+ jsonEncoderTargetView.setUint32(jsonEncoderPosition, value);
399
+ jsonEncoderPosition += 4;
400
+ }
401
+ return;
402
+ }
403
+
404
+ if (
405
+ globalThis.Number.isInteger(value) &&
406
+ value >= -0x80000000 &&
407
+ value < 0
408
+ ) {
409
+ if (value >= -0x20) {
410
+ writeJsonEncoderByte(0x100 + value);
411
+ } else if (value >= -0x80) {
412
+ ensureJsonEncoderCapacity(2);
413
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xd0;
414
+ jsonEncoderTargetView.setInt8(jsonEncoderPosition++, value);
415
+ } else if (value >= -0x8000) {
416
+ ensureJsonEncoderCapacity(3);
417
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xd1;
418
+ jsonEncoderTargetView.setInt16(jsonEncoderPosition, value);
419
+ jsonEncoderPosition += 2;
420
+ } else {
421
+ ensureJsonEncoderCapacity(5);
422
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xd2;
423
+ jsonEncoderTargetView.setInt32(jsonEncoderPosition, value);
424
+ jsonEncoderPosition += 4;
425
+ }
426
+ return;
427
+ }
428
+
429
+ ensureJsonEncoderCapacity(9);
430
+ jsonEncoderTarget[jsonEncoderPosition++] = 0xcb;
431
+ jsonEncoderTargetView.setFloat64(jsonEncoderPosition, value);
432
+ jsonEncoderPosition += 8;
433
+ return;
434
+ }
435
+ case "boolean":
436
+ writeJsonEncoderByte(value ? 0xc3 : 0xc2);
437
+ return;
438
+ case "object": {
439
+ if (jsonEncoderDepth >= maxJsonNestingDepth) {
440
+ throw new BufferError(
441
+ `JSON nesting exceeds the maximum depth of ${maxJsonNestingDepth}.`,
442
+ );
443
+ }
444
+ jsonEncoderDepth++;
445
+
446
+ if (globalThis.Array.isArray(value)) {
447
+ const array = value as ReadonlyArray<JsonValue>;
448
+ const length = array.length;
449
+ writeJsonCollectionHeader(length, 0x90, 0xdc, 0xdd);
450
+
451
+ for (const item of array) encodeJsonValueToTarget(item);
452
+ jsonEncoderDepth--;
453
+ return;
454
+ }
455
+
456
+ const object = value as Readonly<Record<string, JsonValue>>;
457
+ const keys = globalThis.Object.keys(object);
458
+ writeJsonCollectionHeader(keys.length, 0x80, 0xde, 0xdf);
459
+
460
+ for (const key of keys) {
461
+ encodeJsonStringToTarget(key);
462
+ encodeJsonValueToTarget(object[key]);
463
+ }
464
+ jsonEncoderDepth--;
465
+ }
466
+ }
467
+ };
468
+
469
+ const encodeJsonStringToTarget = (value: string): void => {
470
+ const valueLength = value.length;
471
+ const headerLength =
472
+ valueLength < 0x20
473
+ ? 1
474
+ : valueLength < 0x100
475
+ ? 2
476
+ : valueLength < 0x10000
477
+ ? 3
478
+ : 5;
479
+ ensureJsonEncoderCapacity(5 + valueLength * 3);
480
+
481
+ const headerPosition = jsonEncoderPosition;
482
+ jsonEncoderPosition += headerLength;
483
+
484
+ for (let index = 0; index < valueLength; index++) {
485
+ let first = value.charCodeAt(index);
486
+
487
+ if (first < 0x80) {
488
+ jsonEncoderTarget[jsonEncoderPosition++] = first;
489
+ } else if (first < 0x800) {
490
+ jsonEncoderTarget[jsonEncoderPosition++] = (first >> 6) | 0xc0;
491
+ jsonEncoderTarget[jsonEncoderPosition++] = (first & 0x3f) | 0x80;
492
+ } else if (
493
+ (first & 0xfc00) === 0xd800 &&
494
+ (value.charCodeAt(index + 1) & 0xfc00) === 0xdc00
495
+ ) {
496
+ const second = value.charCodeAt(++index);
497
+ first = 0x10000 + ((first & 0x03ff) << 10) + (second & 0x03ff);
498
+ jsonEncoderTarget[jsonEncoderPosition++] = (first >> 18) | 0xf0;
499
+ jsonEncoderTarget[jsonEncoderPosition++] = ((first >> 12) & 0x3f) | 0x80;
500
+ jsonEncoderTarget[jsonEncoderPosition++] = ((first >> 6) & 0x3f) | 0x80;
501
+ jsonEncoderTarget[jsonEncoderPosition++] = (first & 0x3f) | 0x80;
502
+ } else {
503
+ jsonEncoderTarget[jsonEncoderPosition++] = (first >> 12) | 0xe0;
504
+ jsonEncoderTarget[jsonEncoderPosition++] = ((first >> 6) & 0x3f) | 0x80;
505
+ jsonEncoderTarget[jsonEncoderPosition++] = (first & 0x3f) | 0x80;
506
+ }
507
+ }
508
+
509
+ const byteLength = jsonEncoderPosition - headerPosition - headerLength;
510
+ assertMessagePackLength(byteLength, "String byte length");
511
+
512
+ if (byteLength < 0x20) {
513
+ jsonEncoderTarget[headerPosition] = 0xa0 | byteLength;
514
+ return;
515
+ }
516
+
517
+ if (byteLength < 0x100) {
518
+ if (headerLength === 1) {
519
+ jsonEncoderTarget.copyWithin(
520
+ headerPosition + 2,
521
+ headerPosition + 1,
522
+ jsonEncoderPosition,
523
+ );
524
+ jsonEncoderPosition++;
525
+ }
526
+ jsonEncoderTarget[headerPosition] = 0xd9;
527
+ jsonEncoderTarget[headerPosition + 1] = byteLength;
528
+ return;
529
+ }
530
+
531
+ if (byteLength < 0x10000) {
532
+ if (headerLength < 3) {
533
+ const additionalHeaderLength = 3 - headerLength;
534
+ jsonEncoderTarget.copyWithin(
535
+ headerPosition + 3,
536
+ headerPosition + headerLength,
537
+ jsonEncoderPosition,
538
+ );
539
+ jsonEncoderPosition += additionalHeaderLength;
540
+ }
541
+ jsonEncoderTarget[headerPosition] = 0xda;
542
+ jsonEncoderTargetView.setUint16(headerPosition + 1, byteLength);
543
+ return;
544
+ }
545
+
546
+ if (headerLength < 5) {
547
+ const additionalHeaderLength = 5 - headerLength;
548
+ jsonEncoderTarget.copyWithin(
549
+ headerPosition + 5,
550
+ headerPosition + headerLength,
551
+ jsonEncoderPosition,
552
+ );
553
+ jsonEncoderPosition += additionalHeaderLength;
554
+ }
555
+ jsonEncoderTarget[headerPosition] = 0xdb;
556
+ jsonEncoderTargetView.setUint32(headerPosition + 1, byteLength);
557
+ };
558
+
559
+ const writeJsonCollectionHeader = (
560
+ length: number,
561
+ fixedMarker: number,
562
+ marker16: number,
563
+ marker32: number,
564
+ ): void => {
565
+ assertMessagePackLength(length, "Collection length");
566
+
567
+ if (length < 0x10) {
568
+ writeJsonEncoderByte(fixedMarker | length);
569
+ } else if (length < 0x10000) {
570
+ ensureJsonEncoderCapacity(3);
571
+ jsonEncoderTarget[jsonEncoderPosition++] = marker16;
572
+ jsonEncoderTargetView.setUint16(jsonEncoderPosition, length);
573
+ jsonEncoderPosition += 2;
574
+ } else {
575
+ ensureJsonEncoderCapacity(5);
576
+ jsonEncoderTarget[jsonEncoderPosition++] = marker32;
577
+ jsonEncoderTargetView.setUint32(jsonEncoderPosition, length);
578
+ jsonEncoderPosition += 4;
579
+ }
580
+ };
581
+
582
+ const writeJsonEncoderByte = (value: number): void => {
583
+ ensureJsonEncoderCapacity(1);
584
+ jsonEncoderTarget[jsonEncoderPosition++] = value;
585
+ };
586
+
587
+ const ensureJsonEncoderCapacity = (additionalLength: number): void => {
588
+ const requiredLength = jsonEncoderPosition + additionalLength;
589
+ assertNonNegativeInt(requiredLength, "Encoded JSON value length");
590
+
591
+ if (requiredLength <= jsonEncoderTarget.length) return;
592
+
593
+ const newCapacity = globalThis.Math.max(
594
+ jsonEncoderTarget.length * 2,
595
+ requiredLength,
596
+ );
597
+ assertNonNegativeInt(newCapacity, "JSON encoder capacity");
598
+
599
+ const oldTarget = jsonEncoderTarget;
600
+ jsonEncoderTarget = new globalThis.Uint8Array(newCapacity);
601
+ jsonEncoderTarget.set(oldTarget.subarray(0, jsonEncoderPosition));
602
+ jsonEncoderTargetView = new globalThis.DataView(jsonEncoderTarget.buffer);
603
+ };
604
+
605
+ const assertMessagePackLength = (length: number, name: string): void => {
606
+ if (length > 0xffffffff) {
607
+ throw new BufferError(`${name} exceeds the MessagePack uint32 limit.`);
608
+ }
609
+ };
610
+
611
+ const decodeJsonValueFromSource = (): JsonValue => {
612
+ const marker = readJsonDecoderByte();
613
+
614
+ if (marker <= 0x7f) return marker as JsonValue;
615
+ if (marker <= 0x8f) return decodeJsonMap(marker - 0x80);
616
+ if (marker <= 0x9f) return decodeJsonArray(marker - 0x90);
617
+ if (marker <= 0xbf) return decodeJsonString(marker - 0xa0);
618
+ if (marker >= 0xe0) return (marker - 0x100) as JsonValue;
619
+
620
+ switch (marker) {
621
+ case 0xc0:
622
+ return null;
623
+ case 0xc2:
624
+ return false;
625
+ case 0xc3:
626
+ return true;
627
+ case 0xca:
628
+ return decodeJsonFloat(4);
629
+ case 0xcb:
630
+ return decodeJsonFloat(8);
631
+ case 0xcc:
632
+ return readJsonDecoderByte() as JsonValue;
633
+ case 0xcd:
634
+ return readJsonUint16() as JsonValue;
635
+ case 0xce:
636
+ return readJsonUint32() as JsonValue;
637
+ case 0xd0: {
638
+ assertJsonDecoderHasRemainingBytes(1);
639
+ return jsonDecoderView.getInt8(jsonDecoderPosition++) as JsonValue;
640
+ }
641
+ case 0xd1: {
642
+ assertJsonDecoderHasRemainingBytes(2);
643
+ const value = jsonDecoderView.getInt16(jsonDecoderPosition);
644
+ jsonDecoderPosition += 2;
645
+ return value as JsonValue;
646
+ }
647
+ case 0xd2: {
648
+ assertJsonDecoderHasRemainingBytes(4);
649
+ const value = jsonDecoderView.getInt32(jsonDecoderPosition);
650
+ jsonDecoderPosition += 4;
651
+ return value as JsonValue;
652
+ }
653
+ case 0xd9:
654
+ return decodeJsonString(readJsonDecoderByte());
655
+ case 0xda:
656
+ return decodeJsonString(readJsonUint16());
657
+ case 0xdb:
658
+ return decodeJsonString(readJsonUint32());
659
+ case 0xdc:
660
+ return decodeJsonArray(readJsonUint16());
661
+ case 0xdd:
662
+ return decodeJsonArray(readJsonUint32());
663
+ case 0xde:
664
+ return decodeJsonMap(readJsonUint16());
665
+ case 0xdf:
666
+ return decodeJsonMap(readJsonUint32());
667
+ default:
668
+ throw new BufferError(
669
+ `Unsupported MessagePack marker 0x${marker.toString(16).padStart(2, "0")}.`,
670
+ );
671
+ }
672
+ };
673
+
674
+ const decodeJsonFloat = (byteLength: 4 | 8): JsonValue => {
675
+ assertJsonDecoderHasRemainingBytes(byteLength);
676
+ const value =
677
+ byteLength === 4
678
+ ? jsonDecoderView.getFloat32(jsonDecoderPosition)
679
+ : jsonDecoderView.getFloat64(jsonDecoderPosition);
680
+ jsonDecoderPosition += byteLength;
681
+
682
+ if (!globalThis.Number.isFinite(value)) {
683
+ throw new BufferError("A decoded JSON number must be finite.");
684
+ }
685
+ return value as JsonValue;
686
+ };
687
+
688
+ const decodeJsonString = (byteLength: number): string => {
689
+ assertJsonDecoderHasRemainingBytes(byteLength);
690
+
691
+ shortAscii: if (byteLength < 16) {
692
+ if (byteLength === 0) return "";
693
+
694
+ const start = jsonDecoderPosition;
695
+ const first = jsonDecoderSource[jsonDecoderPosition++];
696
+
697
+ if ((first & 0x80) !== 0) {
698
+ jsonDecoderPosition = start;
699
+ break shortAscii;
700
+ }
701
+ if (byteLength === 1) return jsonStringFromCharCode(first);
702
+
703
+ const second = jsonDecoderSource[jsonDecoderPosition++];
704
+ if ((second & 0x80) !== 0) {
705
+ jsonDecoderPosition = start;
706
+ break shortAscii;
707
+ }
708
+ if (byteLength === 2) return jsonStringFromCharCode(first, second);
709
+
710
+ const third = jsonDecoderSource[jsonDecoderPosition++];
711
+ if ((third & 0x80) !== 0) {
712
+ jsonDecoderPosition = start;
713
+ break shortAscii;
714
+ }
715
+ if (byteLength === 3) return jsonStringFromCharCode(first, second, third);
716
+
717
+ const fourth = jsonDecoderSource[jsonDecoderPosition++];
718
+ if ((fourth & 0x80) !== 0) {
719
+ jsonDecoderPosition = start;
720
+ break shortAscii;
721
+ }
722
+ if (byteLength === 4) {
723
+ return jsonStringFromCharCode(first, second, third, fourth);
724
+ }
725
+
726
+ const fifth = jsonDecoderSource[jsonDecoderPosition++];
727
+ if ((fifth & 0x80) !== 0) {
728
+ jsonDecoderPosition = start;
729
+ break shortAscii;
730
+ }
731
+ if (byteLength === 5) {
732
+ return jsonStringFromCharCode(first, second, third, fourth, fifth);
733
+ }
734
+
735
+ const sixth = jsonDecoderSource[jsonDecoderPosition++];
736
+ if ((sixth & 0x80) !== 0) {
737
+ jsonDecoderPosition = start;
738
+ break shortAscii;
739
+ }
740
+ if (byteLength === 6) {
741
+ return jsonStringFromCharCode(first, second, third, fourth, fifth, sixth);
742
+ }
743
+
744
+ const seventh = jsonDecoderSource[jsonDecoderPosition++];
745
+ if ((seventh & 0x80) !== 0) {
746
+ jsonDecoderPosition = start;
747
+ break shortAscii;
748
+ }
749
+ if (byteLength === 7) {
750
+ return jsonStringFromCharCode(
751
+ first,
752
+ second,
753
+ third,
754
+ fourth,
755
+ fifth,
756
+ sixth,
757
+ seventh,
758
+ );
759
+ }
760
+
761
+ const eighth = jsonDecoderSource[jsonDecoderPosition++];
762
+ if ((eighth & 0x80) !== 0) {
763
+ jsonDecoderPosition = start;
764
+ break shortAscii;
765
+ }
766
+ if (byteLength === 8) {
767
+ return jsonStringFromCharCode(
768
+ first,
769
+ second,
770
+ third,
771
+ fourth,
772
+ fifth,
773
+ sixth,
774
+ seventh,
775
+ eighth,
776
+ );
777
+ }
778
+
779
+ const ninth = jsonDecoderSource[jsonDecoderPosition++];
780
+ if ((ninth & 0x80) !== 0) {
781
+ jsonDecoderPosition = start;
782
+ break shortAscii;
783
+ }
784
+ if (byteLength === 9) {
785
+ return jsonStringFromCharCode(
786
+ first,
787
+ second,
788
+ third,
789
+ fourth,
790
+ fifth,
791
+ sixth,
792
+ seventh,
793
+ eighth,
794
+ ninth,
795
+ );
796
+ }
797
+
798
+ const tenth = jsonDecoderSource[jsonDecoderPosition++];
799
+ if ((tenth & 0x80) !== 0) {
800
+ jsonDecoderPosition = start;
801
+ break shortAscii;
802
+ }
803
+ if (byteLength === 10) {
804
+ return jsonStringFromCharCode(
805
+ first,
806
+ second,
807
+ third,
808
+ fourth,
809
+ fifth,
810
+ sixth,
811
+ seventh,
812
+ eighth,
813
+ ninth,
814
+ tenth,
815
+ );
816
+ }
817
+
818
+ const eleventh = jsonDecoderSource[jsonDecoderPosition++];
819
+ if ((eleventh & 0x80) !== 0) {
820
+ jsonDecoderPosition = start;
821
+ break shortAscii;
822
+ }
823
+ if (byteLength === 11) {
824
+ return jsonStringFromCharCode(
825
+ first,
826
+ second,
827
+ third,
828
+ fourth,
829
+ fifth,
830
+ sixth,
831
+ seventh,
832
+ eighth,
833
+ ninth,
834
+ tenth,
835
+ eleventh,
836
+ );
837
+ }
838
+
839
+ const twelfth = jsonDecoderSource[jsonDecoderPosition++];
840
+ if ((twelfth & 0x80) !== 0) {
841
+ jsonDecoderPosition = start;
842
+ break shortAscii;
843
+ }
844
+ if (byteLength === 12) {
845
+ return jsonStringFromCharCode(
846
+ first,
847
+ second,
848
+ third,
849
+ fourth,
850
+ fifth,
851
+ sixth,
852
+ seventh,
853
+ eighth,
854
+ ninth,
855
+ tenth,
856
+ eleventh,
857
+ twelfth,
858
+ );
859
+ }
860
+
861
+ const thirteenth = jsonDecoderSource[jsonDecoderPosition++];
862
+ if ((thirteenth & 0x80) !== 0) {
863
+ jsonDecoderPosition = start;
864
+ break shortAscii;
865
+ }
866
+ if (byteLength === 13) {
867
+ return jsonStringFromCharCode(
868
+ first,
869
+ second,
870
+ third,
871
+ fourth,
872
+ fifth,
873
+ sixth,
874
+ seventh,
875
+ eighth,
876
+ ninth,
877
+ tenth,
878
+ eleventh,
879
+ twelfth,
880
+ thirteenth,
881
+ );
882
+ }
883
+
884
+ const fourteenth = jsonDecoderSource[jsonDecoderPosition++];
885
+ if ((fourteenth & 0x80) !== 0) {
886
+ jsonDecoderPosition = start;
887
+ break shortAscii;
888
+ }
889
+ if (byteLength === 14) {
890
+ return jsonStringFromCharCode(
891
+ first,
892
+ second,
893
+ third,
894
+ fourth,
895
+ fifth,
896
+ sixth,
897
+ seventh,
898
+ eighth,
899
+ ninth,
900
+ tenth,
901
+ eleventh,
902
+ twelfth,
903
+ thirteenth,
904
+ fourteenth,
905
+ );
906
+ }
907
+
908
+ const fifteenth = jsonDecoderSource[jsonDecoderPosition++];
909
+ if ((fifteenth & 0x80) !== 0) {
910
+ jsonDecoderPosition = start;
911
+ break shortAscii;
912
+ }
913
+ return jsonStringFromCharCode(
914
+ first,
915
+ second,
916
+ third,
917
+ fourth,
918
+ fifth,
919
+ sixth,
920
+ seventh,
921
+ eighth,
922
+ ninth,
923
+ tenth,
924
+ eleventh,
925
+ twelfth,
926
+ thirteenth,
927
+ fourteenth,
928
+ fifteenth,
929
+ );
930
+ }
931
+
932
+ const end = jsonDecoderPosition + byteLength;
933
+ const units: Array<number> = [];
934
+ let result = "";
935
+
936
+ while (jsonDecoderPosition < end) {
937
+ const first = jsonDecoderSource[jsonDecoderPosition++];
938
+
939
+ if (first < 0x80) {
940
+ units.push(first);
941
+ } else if (first >= 0xc2 && first <= 0xdf) {
942
+ assertJsonStringHasRemainingBytes(end, 1);
943
+ const second = readJsonContinuationByte();
944
+ units.push(((first & 0x1f) << 6) | second);
945
+ } else if (first >= 0xe0 && first <= 0xef) {
946
+ assertJsonStringHasRemainingBytes(end, 2);
947
+ const secondByte = jsonDecoderSource[jsonDecoderPosition];
948
+ if (first === 0xe0 && secondByte < 0xa0) {
949
+ throw new BufferError("Invalid UTF-8 string encoding.");
950
+ }
951
+ const second = readJsonContinuationByte();
952
+ const third = readJsonContinuationByte();
953
+ units.push(((first & 0x0f) << 12) | (second << 6) | third);
954
+ } else if (first >= 0xf0 && first <= 0xf4) {
955
+ assertJsonStringHasRemainingBytes(end, 3);
956
+ const secondByte = jsonDecoderSource[jsonDecoderPosition];
957
+ if (
958
+ (first === 0xf0 && secondByte < 0x90) ||
959
+ (first === 0xf4 && secondByte > 0x8f)
960
+ ) {
961
+ throw new BufferError("Invalid UTF-8 string encoding.");
962
+ }
963
+ const second = readJsonContinuationByte();
964
+ const third = readJsonContinuationByte();
965
+ const fourth = readJsonContinuationByte();
966
+ const codePoint =
967
+ ((first & 0x07) << 18) | (second << 12) | (third << 6) | fourth;
968
+ const pair = codePoint - 0x10000;
969
+ units.push(0xd800 | (pair >> 10), 0xdc00 | (pair & 0x3ff));
970
+ } else {
971
+ throw new BufferError("Invalid UTF-8 string encoding.");
972
+ }
973
+
974
+ if (units.length >= 0x1000) {
975
+ result += jsonStringFromCharCode(...units);
976
+ units.length = 0;
977
+ }
978
+ }
979
+
980
+ if (units.length > 0) {
981
+ result += jsonStringFromCharCode(...units);
982
+ }
983
+ return result;
984
+ };
985
+
986
+ const decodeJsonArray = (length: number): JsonValue => {
987
+ if (jsonDecoderDepth >= maxJsonNestingDepth) {
988
+ throw new BufferError(
989
+ `JSON nesting exceeds the maximum depth of ${maxJsonNestingDepth}.`,
990
+ );
991
+ }
992
+ if (length > jsonDecoderSource.length - jsonDecoderPosition) {
993
+ throw new BufferError("Buffer parse ended prematurely");
994
+ }
995
+
996
+ // oxlint-disable-next-line unicorn/no-new-array -- Preallocation is intentional in this decoding hot path.
997
+ const value = new Array<JsonValue>(length);
998
+ jsonDecoderDepth++;
999
+ for (let index = 0; index < length; index++) {
1000
+ value[index] = decodeJsonValueFromSource();
1001
+ }
1002
+ jsonDecoderDepth--;
1003
+ return value;
1004
+ };
1005
+
1006
+ const decodeJsonMap = (length: number): JsonValue => {
1007
+ if (jsonDecoderDepth >= maxJsonNestingDepth) {
1008
+ throw new BufferError(
1009
+ `JSON nesting exceeds the maximum depth of ${maxJsonNestingDepth}.`,
1010
+ );
1011
+ }
1012
+ if (length > (jsonDecoderSource.length - jsonDecoderPosition) / 2) {
1013
+ throw new BufferError("Buffer parse ended prematurely");
1014
+ }
1015
+
1016
+ const value: Record<string, JsonValue> = {};
1017
+ jsonDecoderDepth++;
1018
+ for (let index = 0; index < length; index++) {
1019
+ const marker = readJsonDecoderByte();
1020
+ let key: string;
1021
+
1022
+ if (marker >= 0xa0 && marker <= 0xbf) {
1023
+ key = decodeCachedJsonKey(marker - 0xa0);
1024
+ } else if (marker === 0xd9) {
1025
+ key = decodeCachedJsonKey(readJsonDecoderByte());
1026
+ } else if (marker === 0xda) {
1027
+ key = decodeCachedJsonKey(readJsonUint16());
1028
+ } else if (marker === 0xdb) {
1029
+ key = decodeCachedJsonKey(readJsonUint32());
1030
+ } else {
1031
+ jsonDecoderPosition--;
1032
+ decodeJsonValueFromSource();
1033
+ throw new BufferError("A decoded JSON object key must be a string.");
1034
+ }
1035
+
1036
+ const entryValue = decodeJsonValueFromSource();
1037
+
1038
+ if (key === "__proto__") {
1039
+ globalThis.Object.defineProperty(value, key, {
1040
+ value: entryValue,
1041
+ configurable: true,
1042
+ enumerable: true,
1043
+ writable: true,
1044
+ });
1045
+ } else {
1046
+ value[key] = entryValue;
1047
+ }
1048
+ }
1049
+ jsonDecoderDepth--;
1050
+ return value;
1051
+ };
1052
+
1053
+ const decodeCachedJsonKey = (byteLength: number): string => {
1054
+ if (byteLength > maxCachedJsonKeyByteLength) {
1055
+ return decodeJsonString(byteLength);
1056
+ }
1057
+
1058
+ assertJsonDecoderHasRemainingBytes(byteLength);
1059
+ const start = jsonDecoderPosition;
1060
+ const end = start + byteLength;
1061
+ const firstBytes =
1062
+ byteLength > 1
1063
+ ? jsonDecoderView.getUint16(start)
1064
+ : byteLength === 1
1065
+ ? jsonDecoderSource[start]
1066
+ : 0;
1067
+ const cacheIndex = ((byteLength << 5) ^ firstBytes) & (jsonKeyCacheSize - 1);
1068
+ const entry = jsonKeyCache[cacheIndex];
1069
+
1070
+ if (entry?.bytes.length === byteLength) {
1071
+ let index = 0;
1072
+ while (
1073
+ index < byteLength &&
1074
+ entry.bytes[index] === jsonDecoderSource[start + index]
1075
+ ) {
1076
+ index++;
1077
+ }
1078
+ if (index === byteLength) {
1079
+ jsonDecoderPosition = end;
1080
+ return entry.value;
1081
+ }
1082
+ }
1083
+
1084
+ const value = decodeJsonString(byteLength);
1085
+ jsonKeyCache[cacheIndex] = {
1086
+ bytes: jsonDecoderSource.slice(start, end),
1087
+ value,
1088
+ };
1089
+ return value;
1090
+ };
1091
+
1092
+ const readJsonContinuationByte = (): number => {
1093
+ const byte = jsonDecoderSource[jsonDecoderPosition++];
1094
+ if ((byte & 0xc0) !== 0x80) {
1095
+ throw new BufferError("Invalid UTF-8 string encoding.");
1096
+ }
1097
+ return byte & 0x3f;
1098
+ };
1099
+
1100
+ const assertJsonStringHasRemainingBytes = (
1101
+ end: number,
1102
+ requiredBytes: number,
1103
+ ): void => {
1104
+ if (end - jsonDecoderPosition < requiredBytes) {
1105
+ throw new BufferError("Invalid UTF-8 string encoding.");
1106
+ }
1107
+ };
1108
+
1109
+ const readJsonDecoderByte = (): number => {
1110
+ assertJsonDecoderHasRemainingBytes(1);
1111
+ return jsonDecoderSource[jsonDecoderPosition++];
1112
+ };
1113
+
1114
+ const readJsonUint16 = (): number => {
1115
+ assertJsonDecoderHasRemainingBytes(2);
1116
+ const value = jsonDecoderView.getUint16(jsonDecoderPosition);
1117
+ jsonDecoderPosition += 2;
1118
+ return value;
1119
+ };
1120
+
1121
+ const readJsonUint32 = (): number => {
1122
+ assertJsonDecoderHasRemainingBytes(4);
1123
+ const value = jsonDecoderView.getUint32(jsonDecoderPosition);
1124
+ jsonDecoderPosition += 4;
1125
+ return value;
1126
+ };
1127
+
1128
+ const assertJsonDecoderHasRemainingBytes = (requiredBytes: number): void => {
1129
+ if (jsonDecoderSource.length - jsonDecoderPosition < requiredBytes) {
1130
+ throw new BufferError("Buffer parse ended prematurely");
1131
+ }
1132
+ };
1133
+
1134
+ const assertNonNegativeInt: (
1135
+ value: number,
1136
+ name: string,
1137
+ ) => asserts value is NonNegativeInt = (value, name) => {
1138
+ if (!globalThis.Number.isSafeInteger(value) || value < 0) {
1139
+ throw new BufferError(`${name} must be a non-negative safe integer.`);
1140
+ }
1141
+ };
1142
+
1143
+ const assertBufferHasRemainingBytes = (
1144
+ remainingBytes: number,
1145
+ requiredBytes: number,
1146
+ ): void => {
1147
+ if (remainingBytes < requiredBytes) {
1148
+ throw new BufferError("Buffer parse ended prematurely");
1149
+ }
1150
+ };