diff --git a/src/ipc/workers/nativeLzma2.ts b/src/ipc/workers/nativeLzma2.ts index 83523439..713998fc 100644 --- a/src/ipc/workers/nativeLzma2.ts +++ b/src/ipc/workers/nativeLzma2.ts @@ -89,9 +89,16 @@ class NativeLzma2Decoder implements Lzma2DecoderPort { } } - private queue(bytes: Uint8Array): void { + /** + * `owned` says the bytes are ours to keep. The library documents `update()` + * as handing back a zero-copy view, so that one is copied before a later call + * can write over it. `finish()` resolves to the decoded tail and leaves a + * spent decoder behind, so nothing can rewrite it, and on a solid block the + * copy would be a second copy of the entire block. + */ + private queue(bytes: Uint8Array, owned = false): void { if (bytes.length === 0) return - this.pending.push(Uint8Array.from(bytes)) + this.pending.push(owned ? bytes : Uint8Array.from(bytes)) this.pendingBytes += bytes.length } @@ -128,7 +135,7 @@ class NativeLzma2Decoder implements Lzma2DecoderPort { if (control === 0) { try { this.queue(this.decoder.update(Uint8Array.of(0))) - this.queue(await this.decoder.finish()) + this.queue(await this.decoder.finish(), true) } catch { throw new NativeLzma2Error("the native LZMA2 decoder rejected the stream") } diff --git a/tests/ipc/nativeLzma2Adapter.test.ts b/tests/ipc/nativeLzma2Adapter.test.ts index 75db172b..55b5cd05 100644 --- a/tests/ipc/nativeLzma2Adapter.test.ts +++ b/tests/ipc/nativeLzma2Adapter.test.ts @@ -48,6 +48,33 @@ describe("native LZMA2 adapter", () => { expect(received).toEqual(Uint8Array.from([0xa0, 0, 0, 0, 1, 0x11, 0x22])) }) + it("assembles output correctly when the decoder reuses its update buffer", async () => { + // The library returns update() output as a zero-copy view, so a later call + // may write over bytes the adapter is still holding. finish() hands back the + // tail of a spent decoder, which is why that one is queued without a copy. + const reused = new Uint8Array(4) + let call = 0 + class ReusingDecompressor { + update(): Uint8Array { + reused.fill(++call) + return reused + } + + async finish(): Promise { + reused.fill(0xff) + return Uint8Array.of(0xaa, 0xbb) + } + } + + const pieces: Uint8Array[] = [] + const factory = createNativeLzma2DecoderFactory(ReusingDecompressor) + const decoder = factory(0, (bytes) => pieces.push(bytes.slice())) + const input = inputOver(Uint8Array.from([0xa0, 0, 0, 0, 1, 0x11, 0x22, 0xa0, 0, 0, 0, 1, 0x33, 0x44, 0])) + while (!decoder.finished) await decoder.decodeChunk(input) + + expect(Buffer.concat(pieces.map((piece) => Buffer.from(piece)))).toEqual(Buffer.from([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 0xaa, 0xbb])) + }) + it("drains buffered native output in bounded pieces", async () => { const expected = new Uint8Array(5 * 1024 * 1024 + 123) expected.fill(7)