Commit 0160710ceb2 for nodejs
commit 0160710ceb202396ea46154735af5d3aaf28d38a
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date: Wed Oct 28 09:56:55 2026 -0400
zlib: skip the stream in sync convenience calls without options
`zlib.inflateSync(buffer)` and the other synchronous convenience
methods constructed a Transform stream for every call, even though
only its native handle is used and, without options, the stream is
never exposed. They also allocated a fresh output chunk for every
call.
When no options are passed, drive a native handle created with the
default settings directly, and decompress into a scratch buffer kept
between calls, copying the result out. Calls with options, including
`info`, keep the existing path. Results and errors are unchanged.
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Assisted-by: Grok
PR-URL: https://github.com/nodejs/node/pull/66864
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
diff --git a/lib/zlib.js b/lib/zlib.js
index 3425233cbb6..c084889b698 100644
--- a/lib/zlib.js
+++ b/lib/zlib.js
@@ -31,6 +31,7 @@ const {
ObjectFreeze,
ObjectKeys,
ReflectApply,
+ SafeMap,
Symbol,
SymbolHasInstance,
Uint32Array,
@@ -176,43 +177,128 @@ function zlibBufferOnEnd() {
this.cb(null, buf);
}
-function zlibBufferSync(engine, buffer) {
+function toSyncInput(buffer) {
if (typeof buffer === 'string') {
- buffer = Buffer.from(buffer);
- } else if (!isArrayBufferView(buffer)) {
+ return Buffer.from(buffer);
+ }
+ if (!isArrayBufferView(buffer)) {
if (isAnyArrayBuffer(buffer)) {
- buffer = Buffer.from(buffer);
- } else {
- throw new ERR_INVALID_ARG_TYPE(
- 'buffer',
- ['string', 'Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'],
- buffer,
- );
+ return Buffer.from(buffer);
}
+ throw new ERR_INVALID_ARG_TYPE(
+ 'buffer',
+ ['string', 'Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'],
+ buffer,
+ );
}
+ return buffer;
+}
+
+function zlibBufferSync(engine, buffer) {
+ buffer = toSyncInput(buffer);
buffer = processChunkSync(engine, buffer, engine._finishFlushFlag);
if (engine._info)
return { buffer, engine };
return buffer;
}
+function zlibError(message, errno, code) {
+ if (code === 'ERR_TRAILING_JUNK_AFTER_STREAM_END') {
+ return new ERR_TRAILING_JUNK_AFTER_STREAM_END();
+ }
+ const error = genericNodeError(message, { errno, code });
+ error.errno = errno;
+ error.code = code;
+ return error;
+}
+
function zlibOnError(message, errno, code) {
const self = this[owner_symbol];
// There is no way to cleanly recover.
// Continuing only obscures problems.
-
- let error;
- if (code === 'ERR_TRAILING_JUNK_AFTER_STREAM_END') {
- error = new ERR_TRAILING_JUNK_AFTER_STREAM_END();
- } else {
- error = genericNodeError(message, { errno, code });
- error.errno = errno;
- error.code = code;
- }
+ const error = zlibError(message, errno, code);
self.destroy(error);
self[kError] = error;
}
+// Without options, a synchronous convenience call (zlib.inflateSync(buffer),
+// ...) uses the defaults and its engine is never exposed, so it drives a
+// native handle directly instead of constructing a Transform stream. It also
+// decompresses into a scratch buffer kept between calls and copies the result
+// out, instead of allocating a whole output chunk per call.
+let syncScratchBuffer;
+
+function zlibSyncOnError(message, errno, code) {
+ this[kError] = zlibError(message, errno, code);
+}
+
+function zlibBufferSyncWithDefaults(ctor, buffer) {
+ const chunk = toSyncInput(buffer);
+ const mode = kEngineModes.get(ctor);
+ const writeState = new Uint32Array(2);
+ const handle = createDefaultHandle(mode, writeState);
+ handle.onerror = zlibSyncOnError;
+ // Take the scratch buffer, so that a nested call (e.g. from an async hook)
+ // allocates its own.
+ const scratch = syncScratchBuffer ?? Buffer.allocUnsafeSlow(Z_DEFAULT_CHUNK);
+ syncScratchBuffer = undefined;
+ try {
+ return processSyncWithDefaults(handle, writeState, chunk,
+ defaultFinishFlush(mode), scratch);
+ } finally {
+ handle.close();
+ syncScratchBuffer = scratch;
+ }
+}
+
+// Same as processChunkSync() for an engine created without options.
+function processSyncWithDefaults(handle, writeState, chunk, flushFlag,
+ scratch) {
+ const chunkSize = Z_DEFAULT_CHUNK;
+ let out = scratch;
+ let inOff = 0;
+ let availIn = chunk.byteLength;
+ let buffers = null;
+ let nread = 0;
+
+ while (true) {
+ handle.writeSync(flushFlag, chunk, inOff, availIn, out, 0, chunkSize);
+ if (handle[kError] !== undefined)
+ throw handle[kError];
+
+ const availOutAfter = writeState[0];
+ const availInAfter = writeState[1];
+ const have = chunkSize - availOutAfter;
+ if (have > 0) {
+ nread += have;
+ if (nread > kMaxLength)
+ throw new ERR_BUFFER_TOO_LARGE(kMaxLength);
+ if (out !== scratch) {
+ buffers.push(out.subarray(0, have));
+ } else if (availOutAfter === 0) {
+ buffers = [out];
+ }
+ }
+ if (availOutAfter !== 0)
+ break;
+
+ // The output buffer is full. Continue with the rest of the input.
+ inOff += availIn - availInAfter;
+ availIn = availInAfter;
+ out = Buffer.allocUnsafe(chunkSize);
+ }
+
+ if (nread === 0)
+ return new FastBuffer();
+ if (buffers === null) {
+ // All of the output is in the scratch buffer.
+ const result = Buffer.allocUnsafe(nread);
+ scratch.copy(result, 0, 0, nread);
+ return result;
+ }
+ return Buffer.concat(buffers, nread);
+}
+
const FLUSH_BOUND = [
[ Z_NO_FLUSH, Z_BLOCK ],
[ BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_EMIT_METADATA ],
@@ -794,6 +880,8 @@ class Unzip extends Zlib {
function createConvenienceMethod(ctor, sync, prepareOpts) {
if (sync) {
return function syncBufferWrapper(buffer, opts) {
+ if (opts === undefined)
+ return zlibBufferSyncWithDefaults(ctor, buffer);
return zlibBufferSync(new ctor(opts), buffer);
};
}
@@ -999,6 +1087,61 @@ class ZstdDecompress extends Zstd {
}
}
+const kEngineModes = new SafeMap([
+ [Deflate, DEFLATE],
+ [Inflate, INFLATE],
+ [Gzip, GZIP],
+ [Gunzip, GUNZIP],
+ [DeflateRaw, DEFLATERAW],
+ [InflateRaw, INFLATERAW],
+ [Unzip, UNZIP],
+ [BrotliCompress, BROTLI_ENCODE],
+ [BrotliDecompress, BROTLI_DECODE],
+ [ZstdCompress, ZSTD_COMPRESS],
+ [ZstdDecompress, ZSTD_DECOMPRESS],
+]);
+
+// Creates the native handle that the engine for `mode` creates when its
+// constructor gets no options.
+function createDefaultHandle(mode, writeState) {
+ let handle;
+ switch (mode) {
+ case BROTLI_ENCODE:
+ case BROTLI_DECODE:
+ handle = mode === BROTLI_DECODE ?
+ new binding.BrotliDecoder(mode) : new binding.BrotliEncoder(mode);
+ brotliInitParamsArray.fill(-1);
+ handle.init(brotliInitParamsArray, writeState, processCallback, undefined);
+ return handle;
+ case ZSTD_COMPRESS:
+ zstdInitCParamsArray.fill(-1);
+ handle = new binding.ZstdCompress();
+ handle.init(zstdInitCParamsArray, undefined, writeState, processCallback,
+ undefined, false);
+ return handle;
+ case ZSTD_DECOMPRESS:
+ zstdInitDParamsArray.fill(-1);
+ handle = new binding.ZstdDecompress();
+ handle.init(zstdInitDParamsArray, undefined, writeState, processCallback,
+ undefined, false);
+ return handle;
+ default:
+ handle = new binding.Zlib(mode);
+ handle.init(Z_DEFAULT_WINDOWBITS, Z_DEFAULT_COMPRESSION,
+ Z_DEFAULT_MEMLEVEL, Z_DEFAULT_STRATEGY, writeState,
+ processCallback, undefined, false);
+ return handle;
+ }
+}
+
+function defaultFinishFlush(mode) {
+ if (mode === BROTLI_ENCODE || mode === BROTLI_DECODE)
+ return brotliDefaultOpts.finishFlush;
+ if (mode === ZSTD_COMPRESS || mode === ZSTD_DECOMPRESS)
+ return zstdDefaultOpts.finishFlush;
+ return zlibDefaultOpts.finishFlush;
+}
+
function createProperty(ctor) {
return {
__proto__: null,
diff --git a/test/parallel/test-zlib-sync-without-options.js b/test/parallel/test-zlib-sync-without-options.js
new file mode 100644
index 00000000000..d155f926442
--- /dev/null
+++ b/test/parallel/test-zlib-sync-without-options.js
@@ -0,0 +1,136 @@
+'use strict';
+// Synchronous convenience methods called without options skip the stream and
+// reuse an internal output buffer. Check that they behave exactly like calls
+// that pass an empty options object, which go through the stream.
+
+const common = require('../common');
+const assert = require('assert');
+const async_hooks = require('async_hooks');
+const fs = require('fs');
+const zlib = require('zlib');
+
+const { Z_DEFAULT_CHUNK } = zlib.constants;
+
+const pairs = [
+ ['deflateSync', 'inflateSync'],
+ ['gzipSync', 'gunzipSync'],
+ ['gzipSync', 'unzipSync'],
+ ['deflateRawSync', 'inflateRawSync'],
+ ['brotliCompressSync', 'brotliDecompressSync'],
+ ['zstdCompressSync', 'zstdDecompressSync'],
+];
+
+const random = Buffer.alloc(3 * Z_DEFAULT_CHUNK);
+{
+ let seed = 0x9e3779b9;
+ for (let i = 0; i < random.length; i++) {
+ seed ^= seed << 13;
+ seed ^= seed >>> 17;
+ seed ^= seed << 5;
+ random[i] = seed & 0xff;
+ }
+}
+const text = fs.readFileSync(__filename);
+
+// Outputs that fit in one output buffer, fill it exactly, and span several.
+const inputs = [
+ Buffer.alloc(0),
+ Buffer.from('hello'),
+ text,
+ Buffer.alloc(Z_DEFAULT_CHUNK, 'a'),
+ Buffer.alloc(Z_DEFAULT_CHUNK + 1, 'b'),
+ Buffer.concat([text, random, text]),
+];
+
+for (const [compress, decompress] of pairs) {
+ for (const input of inputs) {
+ const label = `${compress}/${decompress} ${input.length}`;
+ const compressed = zlib[compress](input);
+ assert.ok(Buffer.isBuffer(compressed), label);
+ assert.deepStrictEqual(compressed, zlib[compress](input, {}), label);
+
+ const decompressed = zlib[decompress](compressed);
+ assert.ok(Buffer.isBuffer(decompressed), label);
+ assert.deepStrictEqual(decompressed, input, label);
+ assert.deepStrictEqual(decompressed, zlib[decompress](compressed, {}),
+ label);
+ }
+
+ // Every input type.
+ const expected = zlib[decompress](zlib[compress]('blah'.repeat(8)));
+ for (const view of [
+ 'blah'.repeat(8),
+ ...common.getBufferSources(Buffer.from('blah'.repeat(8))),
+ ]) {
+ assert.deepStrictEqual(zlib[decompress](zlib[compress](view)), expected);
+ }
+}
+
+// Results never share memory with each other.
+{
+ const a = zlib.deflateSync('first');
+ const b = zlib.deflateSync('second');
+ const first = zlib.inflateSync(a);
+ const second = zlib.inflateSync(b);
+ assert.strictEqual(first.toString(), 'first');
+ assert.strictEqual(second.toString(), 'second');
+ second.fill(0);
+ assert.strictEqual(first.toString(), 'first');
+ assert.strictEqual(zlib.inflateSync(a).toString(), 'first');
+}
+
+// Errors are the same as through the stream.
+for (const [method, input] of [
+ ['inflateSync', Buffer.from('not compressed')],
+ ['gunzipSync', Buffer.from('not compressed')],
+ ['unzipSync', Buffer.from('not compressed')],
+ ['inflateRawSync', Buffer.from('not compressed')],
+ ['brotliDecompressSync', Buffer.from('not compressed')],
+ ['zstdDecompressSync', Buffer.from('not compressed')],
+ ['inflateSync', zlib.deflateSync(text).subarray(0, 100)],
+ ['gunzipSync', zlib.gzipSync(text).subarray(0, 100)],
+ ['brotliDecompressSync', zlib.brotliCompressSync(text).subarray(0, 100)],
+ ['zstdDecompressSync', zlib.zstdCompressSync(text).subarray(0, 100)],
+]) {
+ let expected;
+ assert.throws(() => zlib[method](input, {}), (err) => {
+ expected = err;
+ return true;
+ });
+ assert.throws(() => zlib[method](input), (err) => {
+ assert.strictEqual(err.constructor, expected.constructor);
+ assert.strictEqual(err.message, expected.message);
+ assert.strictEqual(err.code, expected.code);
+ assert.strictEqual(err.errno, expected.errno);
+ return true;
+ }, method);
+}
+
+for (const [compress] of pairs) {
+ assert.throws(() => zlib[compress](42), {
+ code: 'ERR_INVALID_ARG_TYPE',
+ name: 'TypeError',
+ });
+}
+
+// A synchronous call made from an async hook while another one is running.
+{
+ const compressed = zlib.deflateSync(text);
+ let nested;
+ const hook = async_hooks.createHook({
+ before() {
+ if (nested === undefined) {
+ nested = null;
+ nested = zlib.inflateSync(compressed);
+ }
+ },
+ }).enable();
+ // The error is reported through a callback into JavaScript, which runs the
+ // before hook.
+ assert.throws(() => zlib.inflateSync(Buffer.from('not compressed')), {
+ code: 'Z_DATA_ERROR',
+ });
+ hook.disable();
+ assert.deepStrictEqual(nested, text);
+ assert.deepStrictEqual(zlib.inflateSync(compressed), text);
+}