Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
zlib: refuse incomplete-member reset only for gzip
zlib-wrapped deflate still allows reset after flush. The dictionary
test discards the first member and reuses the compressor.

Signed-off-by: Xia Chao <shapirolutts@gmail.com>
  • Loading branch information
xia-chao committed Sep 21, 2026
commit 96593b0f7bcc30155298eea3d6f81d3c407dabdd
15 changes: 8 additions & 7 deletions doc/api/zlib.md
Original file line number Diff line number Diff line change
Expand Up @@ -2198,13 +2198,14 @@ For Zstd streams, cancel the current frame and start a new session while
preserving the configured parameters and dictionary. If `pledgedSrcSize` was
configured for a Zstd compressor, it applies again to the next frame.

Resetting a gzip or zlib-wrapped deflate stream after it has emitted output
for an incomplete member causes the stream to error with
`ERR_ZLIB_INCOMPLETE_FRAME`. Resetting at that point would discard the
member state while the bytes already written out remain at the start of the
output stream, leaving it undecodable. Call `.end()`, or start over with a
new stream, instead. Raw deflate has no wrapper header, so `reset()` after a
flush still concatenates.
Resetting a gzip stream after it has emitted output for an incomplete member
causes the stream to error with `ERR_ZLIB_INCOMPLETE_FRAME`. Resetting at
that point would discard the member state while the bytes already written out
remain at the start of the output stream, leaving it undecodable. Call
`.end()`, or start over with a new gzip stream, instead.
zlib-wrapped deflate may still `reset()` after a flush; callers that reuse
the compressor discard the first output. Raw deflate has no wrapper header,
so `reset()` after a flush still concatenates.

Calling `reset()` while a write is in progress throws an `Error`.

Expand Down
18 changes: 9 additions & 9 deletions src/node_zlib.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1253,16 +1253,16 @@ CompressionError ZlibContext::GetErrorInfo() const {


CompressionError ZlibContext::ResetStream() {
// deflateReset() is deflateEnd + deflateInit: a new stream. Bytes already
// written out cannot be taken back, so refuse reset on wrapper formats
// (gzip / zlib deflate) once an incomplete member has emitted output.
// Unflushed internal state alone is cancelled by deflateReset; raw deflate
// has no wrapper header, so flush+reset still concatenates.
if ((mode_ == GZIP || mode_ == DEFLATE) && !stream_complete_ &&
output_emitted_) {
// deflateReset() is deflateEnd + deflateInit: a new stream. gzip emits a
// wrapper header on the first write; those bytes cannot be taken back, so
// refuse reset once an incomplete gzip member has emitted output.
// zlib-wrapped deflate still allows reset after flush: callers discard the
// first member (test-zlib-dictionary.js). Raw deflate has no wrapper header,
// so flush+reset still concatenates.
if (mode_ == GZIP && !stream_complete_ && output_emitted_) {
return CompressionError(
"Cannot reset a zlib stream with an incomplete member; end the "
"stream or discard the output produced so far",
"Cannot reset a gzip stream with an incomplete member; end the "
"stream or start a new gzip compressor",
"ERR_ZLIB_INCOMPLETE_FRAME",
Z_STREAM_ERROR);
}
Expand Down
156 changes: 86 additions & 70 deletions test/parallel/test-zlib-reset-incomplete-output.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use strict';

// Tests that reset() refuses to run on gzip / zlib-wrapped deflate once an
// incomplete member has already emitted output.
// Tests that reset() refuses to run on gzip once an incomplete member has
// already emitted output.
//
// deflateReset() is equivalent to deflateEnd + deflateInit. Bytes that have
// already been written out cannot be taken back, so the next member would be
// appended to that fragment and gunzip/inflate fail with Z_DATA_ERROR.
// deflateReset() is equivalent to deflateEnd + deflateInit. gzip writes a
// header on the first write, so those bytes cannot be taken back.
//
// gzip and zlib deflate emit a header on the first write, so write-then-reset
// is already unsafe. Raw deflate has no wrapper header: a small write may emit
// zlib-wrapped deflate still allows reset after flush: official
// test-zlib-dictionary.js discards the first member and reuses the
// compressor. Raw deflate has no wrapper header: a small write may emit
// nothing, and flush+reset still concatenates.

require('../common');
Expand All @@ -29,77 +29,93 @@ async function writeHello(stream) {
});
}

for (const [name, create, decompress] of [
['Gzip', zlib.createGzip, zlib.gunzipSync],
['Deflate', zlib.createDeflate, zlib.inflateSync],
]) {
test(`${name} reset throws when write has emitted wrapper output`,
async () => {
const stream = create();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));

await writeHello(stream);
assert.ok(Buffer.concat(chunks).length > 0);

stream.reset();
stream.end(Buffer.from('world'));

await assert.rejects(finished(stream), {
code: 'ERR_ZLIB_INCOMPLETE_FRAME',
});
});

test(`${name} reset throws when flush has emitted incomplete output`,
async () => {
const stream = create();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));
test('Gzip reset throws when write has emitted wrapper output', async () => {
const stream = zlib.createGzip();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));

stream.write(Buffer.from('hello'));
await new Promise((resolve) => stream.flush(resolve));
assert.ok(Buffer.concat(chunks).length > 0);
await writeHello(stream);
assert.ok(Buffer.concat(chunks).length > 0);

stream.reset();
stream.end(Buffer.from('world'));
stream.reset();
stream.end(Buffer.from('world'));

await assert.rejects(finished(stream), {
code: 'ERR_ZLIB_INCOMPLETE_FRAME',
});
});
await assert.rejects(finished(stream), {
code: 'ERR_ZLIB_INCOMPLETE_FRAME',
});
});

test(`${name} flush followed by end still produces a valid stream`,
async () => {
const stream = create();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));

stream.write(Buffer.from('hello'));
await new Promise((resolve) => stream.flush(resolve));
stream.end(Buffer.from('world'));
await finished(stream);

assert.strictEqual(
decompress(Buffer.concat(chunks)).toString(),
'helloworld',
);
});
test('Gzip reset throws when flush has emitted incomplete output', async () => {
const stream = zlib.createGzip();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));

test(`${name} reset before any write still works`, async () => {
const stream = create();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.write(Buffer.from('hello'));
await new Promise((resolve) => stream.flush(resolve));
assert.ok(Buffer.concat(chunks).length > 0);

stream.reset();
stream.end(Buffer.from('hello'));
await finished(stream);
stream.reset();
stream.end(Buffer.from('world'));

assert.strictEqual(
decompress(Buffer.concat(chunks)).toString(),
'hello',
);
await assert.rejects(finished(stream), {
code: 'ERR_ZLIB_INCOMPLETE_FRAME',
});
}
});

test('Gzip flush followed by end still produces a valid stream', async () => {
const stream = zlib.createGzip();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));

stream.write(Buffer.from('hello'));
await new Promise((resolve) => stream.flush(resolve));
stream.end(Buffer.from('world'));
await finished(stream);

assert.strictEqual(
zlib.gunzipSync(Buffer.concat(chunks)).toString(),
'helloworld',
);
});

test('Gzip reset before any write still works', async () => {
const stream = zlib.createGzip();
const chunks = [];
stream.on('data', (chunk) => chunks.push(chunk));

stream.reset();
stream.end(Buffer.from('hello'));
await finished(stream);

assert.strictEqual(
zlib.gunzipSync(Buffer.concat(chunks)).toString(),
'hello',
);
});

test('Deflate reset after flush still works when first output is discarded',
async () => {
const stream = zlib.createDeflate();
const chunks = [];
let take = false;
stream.on('data', (chunk) => {
if (take) {
chunks.push(chunk);
}
});

stream.write(Buffer.from('hello'));
await new Promise((resolve) => stream.flush(resolve));
stream.reset();
take = true;
stream.end(Buffer.from('world'));
await finished(stream);

assert.strictEqual(
zlib.inflateSync(Buffer.concat(chunks)).toString(),
'world',
);
});

test('DeflateRaw reset after write without emitted output still works',
async () => {
Expand Down
Loading