@@ -33,10 +33,50 @@ export type DecoderOptions<ContextType = undefined> = Readonly<
3333 *
3434 * This is useful if the strings may contain invalid UTF-8 sequences.
3535 *
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.
3878 */
39- useRawBinaryStrings : boolean ;
79+ supportObjectNumberKeys : boolean ;
4080
4181 /**
4282 * Maximum string length.
@@ -82,18 +122,22 @@ const STATE_ARRAY = "array";
82122const STATE_MAP_KEY = "map_key" ;
83123const STATE_MAP_VALUE = "map_value" ;
84124
85- type MapKeyType = string | number ;
125+ type MapKeyType = string | number | bigint | Uint8Array ;
86126
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+ }
90134
91135type StackMapState = {
92136 type : typeof STATE_MAP_KEY | typeof STATE_MAP_VALUE ;
93137 size : number ;
94138 key : MapKeyType | null ;
95139 readCount : number ;
96- map : Record < string , unknown > ;
140+ map : Record < string , unknown > | Map < MapKeyType , unknown > ;
97141} ;
98142
99143type StackArrayState = {
@@ -107,6 +151,8 @@ class StackPool {
107151 private readonly stack : Array < StackState > = [ ] ;
108152 private stackHeadPosition = - 1 ;
109153
154+ constructor ( private readonly useMap : boolean ) { }
155+
110156 public get length ( ) : number {
111157 return this . stackHeadPosition + 1 ;
112158 }
@@ -130,7 +176,7 @@ class StackPool {
130176 state . type = STATE_MAP_KEY ;
131177 state . readCount = 0 ;
132178 state . size = size ;
133- state . map = { } ;
179+ state . map = this . useMap ? new Map ( ) : { } ;
134180 }
135181
136182 private getUninitializedStateFromPool ( ) {
@@ -213,7 +259,10 @@ export class Decoder<ContextType = undefined> {
213259 private readonly extensionCodec : ExtensionCodecType < ContextType > ;
214260 private readonly context : ContextType ;
215261 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 ;
217266 private readonly maxStrLength : number ;
218267 private readonly maxBinLength : number ;
219268 private readonly maxArrayLength : number ;
@@ -227,20 +276,29 @@ export class Decoder<ContextType = undefined> {
227276 private view = EMPTY_VIEW ;
228277 private bytes = EMPTY_BYTES ;
229278 private headByte = HEAD_BYTE_REQUIRED ;
230- private readonly stack = new StackPool ( ) ;
279+ private readonly stack : StackPool ;
231280
232281 public constructor ( options ?: DecoderOptions < ContextType > ) {
233282 this . extensionCodec = options ?. extensionCodec ?? ( ExtensionCodec . defaultCodec as ExtensionCodecType < ContextType > ) ;
234283 this . context = ( options as { context : ContextType } | undefined ) ?. context as ContextType ; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
235284
236285 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 ;
238290 this . maxStrLength = options ?. maxStrLength ?? UINT32_MAX ;
239291 this . maxBinLength = options ?. maxBinLength ?? UINT32_MAX ;
240292 this . maxArrayLength = options ?. maxArrayLength ?? UINT32_MAX ;
241293 this . maxMapLength = options ?. maxMapLength ?? UINT32_MAX ;
242294 this . maxExtLength = options ?. maxExtLength ?? UINT32_MAX ;
243295 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 ) ;
244302 }
245303
246304 private reinitializeState ( ) {
@@ -404,7 +462,7 @@ export class Decoder<ContextType = undefined> {
404462 this . complete ( ) ;
405463 continue DECODE;
406464 } else {
407- object = { } ;
465+ object = this . useMap ? new Map ( ) : { } ;
408466 }
409467 } else if ( headByte < 0xa0 ) {
410468 // fixarray (1001 xxxx) 0x90 - 0x9f
@@ -571,10 +629,15 @@ export class Decoder<ContextType = undefined> {
571629 continue DECODE;
572630 }
573631 } 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 } ` ) ;
576639 }
577- if ( object === "__proto__" ) {
640+ if ( ! this . useMap && object === "__proto__" ) {
578641 throw new DecodeError ( "The key __proto__ is not allowed" ) ;
579642 }
580643
@@ -584,7 +647,11 @@ export class Decoder<ContextType = undefined> {
584647 } else {
585648 // it must be `state.type === State.MAP_VALUE` here
586649
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+ }
588655 state . readCount ++ ;
589656
590657 if ( state . readCount === state . size ) {
@@ -650,10 +717,10 @@ export class Decoder<ContextType = undefined> {
650717 }
651718
652719 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 ) ;
655722 }
656- return this . decodeBinary ( byteLength , headerOffset ) ;
723+ return this . decodeUtf8String ( byteLength , headerOffset ) ;
657724 }
658725
659726 private decodeUtf8String ( byteLength : number , headerOffset : number ) : string {
0 commit comments