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);
+}