From a6b6c32006802ebf146d259ef041efedfc1315c7 Mon Sep 17 00:00:00 2001 From: maximilliangrand <214999687+maximilliangrand@users.noreply.github.com> Date: Sun, 16 Aug 2026 12:33:48 +0200 Subject: [PATCH] fix(decode): preserve mapKeyConverter in Decoder#clone() Decoder#clone() is used for re-entrant decoding (e.g. an extension codec that decodes nested MessagePack on the same Decoder instance, issue #195). It copied every decoder option except mapKeyConverter, so any map decoded re-entrantly silently fell back to the default key converter instead of the one the caller configured. Copy mapKeyConverter in clone() alongside the other options, and add a regression test that decodes a nested map through an extension codec. --- src/Decoder.ts | 1 + test/reuse-instances-with-extensions.test.ts | 27 ++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Decoder.ts b/src/Decoder.ts index 5b3c884..01e83bd 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -264,6 +264,7 @@ export class Decoder { maxMapLength: this.maxMapLength, maxExtLength: this.maxExtLength, keyDecoder: this.keyDecoder, + mapKeyConverter: this.mapKeyConverter, } as any); } diff --git a/test/reuse-instances-with-extensions.test.ts b/test/reuse-instances-with-extensions.test.ts index f539625..3746a32 100644 --- a/test/reuse-instances-with-extensions.test.ts +++ b/test/reuse-instances-with-extensions.test.ts @@ -45,4 +45,31 @@ describe("reuse instances with extensions", () => { const data = context.decode(buf); deepStrictEqual(data, [BigInt(1), BigInt(2), BigInt(3)]); }); + + it("keeps mapKeyConverter for maps decoded re-entrantly inside an extension", () => { + const MSGPACK_EXT_TYPE_WRAP = 1; + const extensionCodec = new ExtensionCodec(); + const encoder = new Encoder({ extensionCodec }); + const decoder = new Decoder({ extensionCodec, mapKeyConverter: (key) => String(key).toUpperCase() }); + + class Wrapped { + readonly inner: unknown; + constructor(inner: unknown) { + this.inner = inner; + } + } + extensionCodec.register({ + type: MSGPACK_EXT_TYPE_WRAP, + encode: (value) => (value instanceof Wrapped ? encoder.encode(value.inner) : null), + // decode re-enters the same decoder instance, which triggers Decoder#clone() + decode: (data) => new Wrapped(decoder.decode(data)), + }); + + const buf = encoder.encode({ a: 1, nested: new Wrapped({ b: 2 }) }); + const decoded = decoder.decode(buf) as Record; + + // The nested map is decoded on the cloned decoder; it must use the same converter. + deepStrictEqual(Object.keys(decoded), ["A", "NESTED"]); + deepStrictEqual(decoded["NESTED"]!.inner, { B: 2 }); + }); });