nodejs/node · #66864
zlib: skip the stream in sync convenience calls without options
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);+}