@@ -33,10 +33,50 @@ export type DecoderOptions<ContextType = undefined> = Readonly<
33
33
*
34
34
* This is useful if the strings may contain invalid UTF-8 sequences.
35
35
*
36
- * Note that this option only applies to string values, not map keys. Additionally, when
37
- * enabled, raw string length is limited by the maxBinLength option.
36
+ * When enabled, raw string length is limited by the maxBinLength option.
37
+ *
38
+ * Note that this option only applies to string values, not map keys. See `rawBinaryStringKeys`
39
+ * for map keys.
40
+ */
41
+ rawBinaryStringValues : boolean ;
42
+
43
+ /**
44
+ * By default, map keys will be decoded as UTF-8 strings. However, if this option is true, map
45
+ * keys will be returned as Uint8Arrays without additional decoding.
46
+ *
47
+ * Requires `useMap` to be true, since plain objects do not support binary keys.
48
+ *
49
+ * When enabled, raw string length is limited by the maxBinLength option.
50
+ *
51
+ * Note that this option only applies to map keys, not string values. See `rawBinaryStringValues`
52
+ * for string values.
53
+ */
54
+ rawBinaryStringKeys : boolean ;
55
+
56
+ /**
57
+ * If true, the decoder will use the Map object to store map values. If false, it will use plain
58
+ * objects. Defaults to false.
59
+ *
60
+ * Besides the type of container, the main difference is that Map objects support a wider range
61
+ * of key types. Plain objects only support string keys (though you can enable
62
+ * `supportObjectNumberKeys` to coerce number keys to strings), while Map objects support
63
+ * strings, numbers, bigints, and Uint8Arrays.
64
+ */
65
+ useMap : boolean ;
66
+
67
+ /**
68
+ * If true, the decoder will support decoding numbers as map keys on plain objects. Defaults to
69
+ * false.
70
+ *
71
+ * Note that any numbers used as object keys will be converted to strings, so there is a risk of
72
+ * key collision as well as the inability to re-encode the object to the same representation.
73
+ *
74
+ * This option is ignored if `useMap` is true.
75
+ *
76
+ * This is useful for backwards compatibility before `useMap` was introduced. Consider instead
77
+ * using `useMap` for new code.
38
78
*/
39
- useRawBinaryStrings : boolean ;
79
+ supportObjectNumberKeys : boolean ;
40
80
41
81
/**
42
82
* Maximum string length.
@@ -82,18 +122,22 @@ const STATE_ARRAY = "array";
82
122
const STATE_MAP_KEY = "map_key" ;
83
123
const STATE_MAP_VALUE = "map_value" ;
84
124
85
- type MapKeyType = string | number ;
125
+ type MapKeyType = string | number | bigint | Uint8Array ;
86
126
87
- const isValidMapKeyType = ( key : unknown ) : key is MapKeyType => {
88
- return typeof key === "string" || typeof key === "number" ;
89
- } ;
127
+ function isValidMapKeyType ( key : unknown , useMap : boolean , supportObjectNumberKeys : boolean ) : key is MapKeyType {
128
+ if ( useMap ) {
129
+ return typeof key === "string" || typeof key === "number" || typeof key === "bigint" || key instanceof Uint8Array ;
130
+ }
131
+ // Plain objects support a more limited set of key types
132
+ return typeof key === "string" || ( supportObjectNumberKeys && typeof key === "number" ) ;
133
+ }
90
134
91
135
type StackMapState = {
92
136
type : typeof STATE_MAP_KEY | typeof STATE_MAP_VALUE ;
93
137
size : number ;
94
138
key : MapKeyType | null ;
95
139
readCount : number ;
96
- map : Record < string , unknown > ;
140
+ map : Record < string , unknown > | Map < MapKeyType , unknown > ;
97
141
} ;
98
142
99
143
type StackArrayState = {
@@ -107,6 +151,8 @@ class StackPool {
107
151
private readonly stack : Array < StackState > = [ ] ;
108
152
private stackHeadPosition = - 1 ;
109
153
154
+ constructor ( private readonly useMap : boolean ) { }
155
+
110
156
public get length ( ) : number {
111
157
return this . stackHeadPosition + 1 ;
112
158
}
@@ -130,7 +176,7 @@ class StackPool {
130
176
state . type = STATE_MAP_KEY ;
131
177
state . readCount = 0 ;
132
178
state . size = size ;
133
- state . map = { } ;
179
+ state . map = this . useMap ? new Map ( ) : { } ;
134
180
}
135
181
136
182
private getUninitializedStateFromPool ( ) {
@@ -213,7 +259,10 @@ export class Decoder<ContextType = undefined> {
213
259
private readonly extensionCodec : ExtensionCodecType < ContextType > ;
214
260
private readonly context : ContextType ;
215
261
private readonly intMode : IntMode ;
216
- private readonly useRawBinaryStrings : boolean ;
262
+ private readonly rawBinaryStringValues : boolean ;
263
+ private readonly rawBinaryStringKeys : boolean ;
264
+ private readonly useMap : boolean ;
265
+ private readonly supportObjectNumberKeys : boolean ;
217
266
private readonly maxStrLength : number ;
218
267
private readonly maxBinLength : number ;
219
268
private readonly maxArrayLength : number ;
@@ -227,20 +276,29 @@ export class Decoder<ContextType = undefined> {
227
276
private view = EMPTY_VIEW ;
228
277
private bytes = EMPTY_BYTES ;
229
278
private headByte = HEAD_BYTE_REQUIRED ;
230
- private readonly stack = new StackPool ( ) ;
279
+ private readonly stack : StackPool ;
231
280
232
281
public constructor ( options ?: DecoderOptions < ContextType > ) {
233
282
this . extensionCodec = options ?. extensionCodec ?? ( ExtensionCodec . defaultCodec as ExtensionCodecType < ContextType > ) ;
234
283
this . context = ( options as { context : ContextType } | undefined ) ?. context as ContextType ; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
235
284
236
285
this . intMode = options ?. intMode ?? ( options ?. useBigInt64 ? IntMode . AS_ENCODED : IntMode . UNSAFE_NUMBER ) ;
237
- this . useRawBinaryStrings = options ?. useRawBinaryStrings ?? false ;
286
+ this . rawBinaryStringValues = options ?. rawBinaryStringValues ?? false ;
287
+ this . rawBinaryStringKeys = options ?. rawBinaryStringKeys ?? false ;
288
+ this . useMap = options ?. useMap ?? false ;
289
+ this . supportObjectNumberKeys = options ?. supportObjectNumberKeys ?? false ;
238
290
this . maxStrLength = options ?. maxStrLength ?? UINT32_MAX ;
239
291
this . maxBinLength = options ?. maxBinLength ?? UINT32_MAX ;
240
292
this . maxArrayLength = options ?. maxArrayLength ?? UINT32_MAX ;
241
293
this . maxMapLength = options ?. maxMapLength ?? UINT32_MAX ;
242
294
this . maxExtLength = options ?. maxExtLength ?? UINT32_MAX ;
243
295
this . keyDecoder = options ?. keyDecoder !== undefined ? options . keyDecoder : sharedCachedKeyDecoder ;
296
+
297
+ if ( this . rawBinaryStringKeys && ! this . useMap ) {
298
+ throw new Error ( "rawBinaryStringKeys is only supported when useMap is true" ) ;
299
+ }
300
+
301
+ this . stack = new StackPool ( this . useMap ) ;
244
302
}
245
303
246
304
private reinitializeState ( ) {
@@ -404,7 +462,7 @@ export class Decoder<ContextType = undefined> {
404
462
this . complete ( ) ;
405
463
continue DECODE;
406
464
} else {
407
- object = { } ;
465
+ object = this . useMap ? new Map ( ) : { } ;
408
466
}
409
467
} else if ( headByte < 0xa0 ) {
410
468
// fixarray (1001 xxxx) 0x90 - 0x9f
@@ -571,10 +629,15 @@ export class Decoder<ContextType = undefined> {
571
629
continue DECODE;
572
630
}
573
631
} else if ( state . type === STATE_MAP_KEY ) {
574
- if ( ! isValidMapKeyType ( object ) ) {
575
- throw new DecodeError ( "The type of key must be string or number but " + typeof object ) ;
632
+ if ( ! isValidMapKeyType ( object , this . useMap , this . supportObjectNumberKeys ) ) {
633
+ const acceptableTypes = this . useMap
634
+ ? "string, number, bigint, or Uint8Array"
635
+ : this . supportObjectNumberKeys
636
+ ? "string or number"
637
+ : "string" ;
638
+ throw new DecodeError ( `The type of key must be ${ acceptableTypes } but got ${ typeof object } ` ) ;
576
639
}
577
- if ( object === "__proto__" ) {
640
+ if ( ! this . useMap && object === "__proto__" ) {
578
641
throw new DecodeError ( "The key __proto__ is not allowed" ) ;
579
642
}
580
643
@@ -584,7 +647,11 @@ export class Decoder<ContextType = undefined> {
584
647
} else {
585
648
// it must be `state.type === State.MAP_VALUE` here
586
649
587
- state . map [ state . key ! ] = object ;
650
+ if ( this . useMap ) {
651
+ ( state . map as Map < MapKeyType , unknown > ) . set ( state . key ! , object ) ;
652
+ } else {
653
+ ( state . map as Record < string , unknown > ) [ state . key as string ] = object ;
654
+ }
588
655
state . readCount ++ ;
589
656
590
657
if ( state . readCount === state . size ) {
@@ -650,10 +717,10 @@ export class Decoder<ContextType = undefined> {
650
717
}
651
718
652
719
private decodeString ( byteLength : number , headerOffset : number ) : string | Uint8Array {
653
- if ( ! this . useRawBinaryStrings || this . stateIsMapKey ( ) ) {
654
- return this . decodeUtf8String ( byteLength , headerOffset ) ;
720
+ if ( this . stateIsMapKey ( ) ? this . rawBinaryStringKeys : this . rawBinaryStringValues ) {
721
+ return this . decodeBinary ( byteLength , headerOffset ) ;
655
722
}
656
- return this . decodeBinary ( byteLength , headerOffset ) ;
723
+ return this . decodeUtf8String ( byteLength , headerOffset ) ;
657
724
}
658
725
659
726
private decodeUtf8String ( byteLength : number , headerOffset : number ) : string {
0 commit comments