Skip to content
Open
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
25 changes: 20 additions & 5 deletions lib/zlib.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,11 @@ function zlibBufferOnData(chunk) {

function zlibBufferOnError(err) {
this.removeAllListeners('end');
this.cb(err);
this.buffers = null;
this._outBuffer = null;
const cb = this.cb;
this.cb = null;
cb(err);
}

function zlibBufferOnEnd() {
Expand All @@ -167,13 +171,19 @@ function zlibBufferOnEnd() {
buf = new FastBuffer();
} else {
const bufs = this.buffers;
buf = (bufs.length === 1 ? bufs[0] : Buffer.concat(bufs, this.nread));
buf = (bufs.length === 1 ?
(bufs[0].byteLength === bufs[0].buffer.byteLength ? bufs[0] : Buffer.from(bufs[0])) :
Buffer.concat(bufs, this.nread));
}
this.buffers = null;
this._outBuffer = null;
const cb = this.cb;
this.cb = null;
this.close();
if (this._info)
this.cb(null, { buffer: buf, engine: this });
cb(null, { buffer: buf, engine: this });
else
this.cb(null, buf);
cb(null, buf);
}

function zlibBufferSync(engine, buffer) {
Expand Down Expand Up @@ -497,7 +507,9 @@ function processChunkSync(self, chunk, flushFlag) {
if (nread === 0)
return new FastBuffer();

return (buffers.length === 1 ? buffers[0] : Buffer.concat(buffers, nread));
return (buffers.length === 1 ?
(buffers[0].byteLength === buffers[0].buffer.byteLength ? buffers[0] : Buffer.from(buffers[0])) :
Buffer.concat(buffers, nread));
}

function processChunk(self, chunk, flushFlag, cb) {
Expand Down Expand Up @@ -627,6 +639,9 @@ function _close(engine) {
// Caller may invoke .close after a zlib error (which will null _handle)
engine._handle?.close();
engine._handle = null;
engine._outBuffer = null;
engine._writeState = null;
engine.buffers = null;
}

const zlibDefaultOpts = {
Expand Down
68 changes: 68 additions & 0 deletions test/parallel/test-zlib-one-shot-memory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const zlib = require('zlib');

// Test that one-shot convenience methods (async and sync) do not retain
// oversized unpooled backing ArrayBuffers (such as the 16KB default chunkSize)
// when output size is small, and that internal stream references are cleared.

const smallPayload = Buffer.from('hello world'.repeat(10));

// Test async convenience methods with info: true to inspect engine cleanup
for (const method of [
'gzip',
'deflate',
'deflateRaw',
'brotliCompress',
'zstdCompress',
]) {
zlib[method](smallPayload, { info: true, chunkSize: 16384 }, common.mustCall((err, res) => {

Check failure on line 20 in test/parallel/test-zlib-one-shot-memory.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Please use common.mustSucceed instead of common.mustCall with assert.ifError
assert.ifError(err);
assert.ok(Buffer.isBuffer(res.buffer));

// The backing ArrayBuffer should not retain the 16KB unpooled chunk
assert.ok(
res.buffer.buffer.byteLength < 16384,
`${method} result backing ArrayBuffer should not retain 16KB chunk padding`
);

// Engine stream should have released internal buffer references
assert.strictEqual(res.engine._outBuffer, null);
assert.strictEqual(res.engine.buffers, null);
assert.strictEqual(res.engine.cb, null);
assert.strictEqual(res.engine._writeState, null);
}));

// Test sync convenience method
const syncMethod = `${method}Sync`;
const syncResult = zlib[syncMethod](smallPayload, { chunkSize: 16384 });
assert.ok(Buffer.isBuffer(syncResult));
assert.ok(

Check failure on line 41 in test/parallel/test-zlib-one-shot-memory.js

View workflow job for this annotation

GitHub Actions / test-macOS

--- stderr --- node:internal/assert/utils:146 throw error; ^ AssertionError [ERR_ASSERTION]: gzipSync result backing ArrayBuffer should not retain 16KB chunk padding at Object.<anonymous> (/Users/runner/work/node/node/node/test/parallel/test-zlib-one-shot-memory.js:41:10) at Module._compile (node:internal/modules/cjs/loader:1924:14) at Object..js (node:internal/modules/cjs/loader:2064:10) at Module.load (node:internal/modules/cjs/loader:1646:32) at Module._load (node:internal/modules/cjs/loader:1438:12) at wrapModuleLoad (node:internal/modules/cjs/loader:261:19) at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5) at node:internal/main/run_main_module:33:47 { generatedMessage: false, code: 'ERR_ASSERTION', actual: false, expected: true, operator: '==', diff: 'simple' } Node.js v27.0.0-pre Command: out/Release/node /Users/runner/work/node/node/node/test/parallel/test-zlib-one-shot-memory.js
syncResult.buffer.byteLength < 16384,
`${syncMethod} result backing ArrayBuffer should not retain 16KB chunk padding`
);
}

// Test decompress convenience methods
zlib.gzip(smallPayload, common.mustCall((err, compressed) => {

Check failure on line 48 in test/parallel/test-zlib-one-shot-memory.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Please use common.mustSucceed instead of common.mustCall with assert.ifError
assert.ifError(err);
zlib.gunzip(compressed, { info: true, chunkSize: 16384 }, common.mustCall((err, res) => {

Check failure on line 50 in test/parallel/test-zlib-one-shot-memory.js

View workflow job for this annotation

GitHub Actions / lint-js-and-md

Please use common.mustSucceed instead of common.mustCall with assert.ifError
assert.ifError(err);
assert.strictEqual(res.buffer.toString(), smallPayload.toString());
assert.ok(
res.buffer.buffer.byteLength < 16384,
'gunzip result backing ArrayBuffer should not retain 16KB chunk padding'
);
assert.strictEqual(res.engine._outBuffer, null);
assert.strictEqual(res.engine.buffers, null);
}));
}));

// Test error path cleans up references
{
const invalidGzip = Buffer.from([0x1f, 0x8b, 0x00, 0x00]);
zlib.gunzip(invalidGzip, { chunkSize: 16384 }, common.mustCall((err) => {
assert.ok(err);
}));
}
Loading