From f5251e082e0cdd0c48beafdb5e650975e0b0874a Mon Sep 17 00:00:00 2001 From: Shuyun Yang Date: Fri, 30 Oct 2026 01:42:29 -0700 Subject: [PATCH 1/2] fix: resume array headers across stream chunks --- src/Decoder.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Decoder.ts b/src/Decoder.ts index 5b3c884..79ef188 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -430,13 +430,13 @@ export class Decoder { this.appendBuffer(buffer); - if (isArrayHeaderRequired) { - arrayItemsLeft = this.readArraySize(); - isArrayHeaderRequired = false; - this.complete(); - } - try { + if (isArrayHeaderRequired) { + arrayItemsLeft = this.readArraySize(); + isArrayHeaderRequired = false; + this.complete(); + } + while (true) { yield this.doDecodeSync(); if (--arrayItemsLeft === 0) { @@ -451,6 +451,10 @@ export class Decoder { } this.totalPos += this.pos; } + + if (isArrayHeaderRequired) { + throw new RangeError("Insufficient data in parsing array header"); + } } finally { this.entered = false; } From c60ac58f1428b25868da798bd16558ca4544e92c Mon Sep 17 00:00:00 2001 From: Shuyun Yang Date: Fri, 30 Oct 2026 01:43:14 -0700 Subject: [PATCH 2/2] test: cover fragmented and truncated array headers --- test/decodeArrayStream.test.ts | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/decodeArrayStream.test.ts b/test/decodeArrayStream.test.ts index a8f7ed5..25eebb9 100644 --- a/test/decodeArrayStream.test.ts +++ b/test/decodeArrayStream.test.ts @@ -61,6 +61,54 @@ describe("decodeArrayStream", () => { assert.deepStrictEqual(result, [1, 2, 3]); }); + for (const [name, header] of [ + ["array16", [0xdc, 0, 1]], + ["array32", [0xdd, 0, 0, 0, 1]], + ] as const) { + it(`decodes ${name} split at every header boundary`, async () => { + const object = { nested: [1, "value", null] }; + const encoded = Uint8Array.from([...header, ...encode(object)]); + + for (let split = 1; split < header.length; split++) { + const stream = async function* () { + yield encoded.subarray(0, split); + yield encoded.subarray(split); + }; + const result: Array = []; + for await (const item of decodeArrayStream(stream())) { + result.push(item); + } + assert.deepStrictEqual(result, [object], `split at ${split}`); + } + }); + + it(`rejects ${name} truncated in the header`, async () => { + for (let length = 1; length < header.length; length++) { + const stream = async function* () { + yield header.slice(0, length); + yield new Uint8Array(0); + }; + await assert.rejects(decodeArrayStream(stream()).next(), RangeError); + } + }); + + it(`decodes ${name} byte-by-byte with empty chunks`, async () => { + const object = { nested: [1, "value", null] }; + const encoded = Uint8Array.from([...header, ...encode(object)]); + const stream = async function* () { + for (const byte of encoded) { + yield new Uint8Array(0); + yield Uint8Array.from([0xff, byte, 0xff]).subarray(1, 2); + } + }; + const result: Array = []; + for await (const item of decodeArrayStream(stream())) { + result.push(item); + } + assert.deepStrictEqual(result, [object]); + }); + } + it("decodes objects array", async () => { const objectsArrays: Array = [];