Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 162 additions & 19 deletions lib/zlib.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const {
ObjectFreeze,
ObjectKeys,
ReflectApply,
SafeMap,
Symbol,
SymbolHasInstance,
Uint32Array,
Expand Down Expand Up @@ -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 ],
Expand Down Expand Up @@ -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);
};
}
Expand Down Expand Up @@ -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,
Expand Down
136 changes: 136 additions & 0 deletions test/parallel/test-zlib-sync-without-options.js
Original file line number Diff line number Diff line change
@@ -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);
}
Loading