diff --git a/lib/zlib.js b/lib/zlib.js index c86fe467a23..a793fe07cb6 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); +}