Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/ipc/workers/nativeLzma2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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")
}
Expand Down
27 changes: 27 additions & 0 deletions tests/ipc/nativeLzma2Adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Uint8Array> {
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)
Expand Down
Loading