nodejs/node · #66864

zlib: skip the stream in sync convenience calls without options

anonrig · merged Oct 28, 20262 files · 298 + / 19 −
lib/zlib.js162 + / 19 −
@@ -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,
test/parallel/test-zlib-sync-without-options.jsadded136 + / 0 −
@@ -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);+}