From 59057eec03f0fdce834b2c01abe61381b6e7256b Mon Sep 17 00:00:00 2001 From: Pejman Nikram Date: Wed, 19 Feb 2025 22:17:21 +0200 Subject: [PATCH] adding support for nonstandard map key in the decoder fix #255 --- README.md | 25 ++++++++++++++----------- src/Decoder.ts | 21 +++++++++++++++------ test/decodeAsync.test.ts | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 6ba5681..f7c36cc 100644 --- a/README.md +++ b/README.md @@ -145,17 +145,20 @@ NodeJS `Buffer` is also acceptable because it is a subclass of `Uint8Array`. #### `DecoderOptions` -| Name | Type | Default | -| -------------- | -------------- | ----------------------------- | -| extensionCodec | ExtensionCodec | `ExtensionCodec.defaultCodec` | -| context | user-defined | - | -| useBigInt64 | boolean | false | -| rawStrings | boolean | false | -| maxStrLength | number | `4_294_967_295` (UINT32_MAX) | -| maxBinLength | number | `4_294_967_295` (UINT32_MAX) | -| maxArrayLength | number | `4_294_967_295` (UINT32_MAX) | -| maxMapLength | number | `4_294_967_295` (UINT32_MAX) | -| maxExtLength | number | `4_294_967_295` (UINT32_MAX) | +| Name | Type | Default | +| --------------- | ------------------- | ---------------------------------------------- | +| extensionCodec | ExtensionCodec | `ExtensionCodec.defaultCodec` | +| context | user-defined | - | +| useBigInt64 | boolean | false | +| rawStrings | boolean | false | +| maxStrLength | number | `4_294_967_295` (UINT32_MAX) | +| maxBinLength | number | `4_294_967_295` (UINT32_MAX) | +| maxArrayLength | number | `4_294_967_295` (UINT32_MAX) | +| maxMapLength | number | `4_294_967_295` (UINT32_MAX) | +| maxExtLength | number | `4_294_967_295` (UINT32_MAX) | +| mapKeyConverter | MapKeyConverterType | throw exception if key is not string or number | + +`MapKeyConverterType` is defined as `(key: unknown) => string | number`. To skip UTF-8 decoding of strings, `rawStrings` can be set to `true`. In this case, strings are decoded into `Uint8Array`. diff --git a/src/Decoder.ts b/src/Decoder.ts index 0bf74de..e9257a9 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -68,6 +68,13 @@ export type DecoderOptions = Readonly< * `null` is a special value to disable the use of the key decoder at all. */ keyDecoder: KeyDecoder | null; + + /** + * A function to convert decoded map key to a valid JS key type. + * + * Defaults to a function that throws an error if the key is not a string or a number. + */ + mapKeyConverter: (key: unknown) => MapKeyType; }> > & ContextOf; @@ -78,8 +85,11 @@ const STATE_MAP_VALUE = "map_value"; type MapKeyType = string | number; -const isValidMapKeyType = (key: unknown): key is MapKeyType => { - return typeof key === "string" || typeof key === "number"; +const mapKeyConverter = (key: unknown): MapKeyType => { + if (typeof key === "string" || typeof key === "number") { + return key; + } + throw new DecodeError("The type of key must be string or number but " + typeof key); }; type StackMapState = { @@ -213,6 +223,7 @@ export class Decoder { private readonly maxMapLength: number; private readonly maxExtLength: number; private readonly keyDecoder: KeyDecoder | null; + private readonly mapKeyConverter: (key: unknown) => MapKeyType; private totalPos = 0; private pos = 0; @@ -236,6 +247,7 @@ export class Decoder { this.maxMapLength = options?.maxMapLength ?? UINT32_MAX; this.maxExtLength = options?.maxExtLength ?? UINT32_MAX; this.keyDecoder = options?.keyDecoder !== undefined ? options.keyDecoder : sharedCachedKeyDecoder; + this.mapKeyConverter = options?.mapKeyConverter ?? mapKeyConverter; } private clone(): Decoder { @@ -631,14 +643,11 @@ export class Decoder { continue DECODE; } } else if (state.type === STATE_MAP_KEY) { - if (!isValidMapKeyType(object)) { - throw new DecodeError("The type of key must be string or number but " + typeof object); - } if (object === "__proto__") { throw new DecodeError("The key __proto__ is not allowed"); } - state.key = object; + state.key = this.mapKeyConverter(object); state.type = STATE_MAP_VALUE; continue DECODE; } else { diff --git a/test/decodeAsync.test.ts b/test/decodeAsync.test.ts index 9687370..6af6c19 100644 --- a/test/decodeAsync.test.ts +++ b/test/decodeAsync.test.ts @@ -36,6 +36,21 @@ describe("decodeAsync", () => { assert.deepStrictEqual(object, { "foo": "bar" }); }); + it("decodes fixmap {'[1, 2]': 'baz'} with custom map key converter", async () => { + const createStream = async function* () { + yield [0x81]; // fixmap size=1 + yield encode([1, 2]); + yield encode("baz"); + }; + + const object = await decodeAsync(createStream(), { + mapKeyConverter: (key) => JSON.stringify(key), + }); + + const key = JSON.stringify([1, 2]); + assert.deepStrictEqual(object, { [key]: "baz" }); + }); + it("decodes multi-byte integer byte-by-byte", async () => { const createStream = async function* () { yield [0xcd]; // uint 16