Skip to content

Commit af015be

Browse files
committed
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
1 parent 2052d0d commit af015be

2 files changed

Lines changed: 298 additions & 19 deletions

File tree

‎lib/zlib.js‎

Lines changed: 162 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const {
3131
ObjectFreeze,
3232
ObjectKeys,
3333
ReflectApply,
34+
SafeMap,
3435
Symbol,
3536
SymbolHasInstance,
3637
Uint32Array,
@@ -176,43 +177,128 @@ function zlibBufferOnEnd() {
176177
this.cb(null, buf);
177178
}
178179

179-
function zlibBufferSync(engine, buffer) {
180+
function toSyncInput(buffer) {
180181
if (typeof buffer === 'string') {
181-
buffer = Buffer.from(buffer);
182-
} else if (!isArrayBufferView(buffer)) {
182+
return Buffer.from(buffer);
183+
}
184+
if (!isArrayBufferView(buffer)) {
183185
if (isAnyArrayBuffer(buffer)) {
184-
buffer = Buffer.from(buffer);
185-
} else {
186-
throw new ERR_INVALID_ARG_TYPE(
187-
'buffer',
188-
['string', 'Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'],
189-
buffer,
190-
);
186+
return Buffer.from(buffer);
191187
}
188+
throw new ERR_INVALID_ARG_TYPE(
189+
'buffer',
190+
['string', 'Buffer', 'TypedArray', 'DataView', 'ArrayBuffer'],
191+
buffer,
192+
);
192193
}
194+
return buffer;
195+
}
196+
197+
function zlibBufferSync(engine, buffer) {
198+
buffer = toSyncInput(buffer);
193199
buffer = processChunkSync(engine, buffer, engine._finishFlushFlag);
194200
if (engine._info)
195201
return { buffer, engine };
196202
return buffer;
197203
}
198204

205+
function zlibError(message, errno, code) {
206+
if (code === 'ERR_TRAILING_JUNK_AFTER_STREAM_END') {
207+
return new ERR_TRAILING_JUNK_AFTER_STREAM_END();
208+
}
209+
const error = genericNodeError(message, { errno, code });
210+
error.errno = errno;
211+
error.code = code;
212+
return error;
213+
}
214+
199215
function zlibOnError(message, errno, code) {
200216
const self = this[owner_symbol];
201217
// There is no way to cleanly recover.
202218
// Continuing only obscures problems.
203-
204-
let error;
205-
if (code === 'ERR_TRAILING_JUNK_AFTER_STREAM_END') {
206-
error = new ERR_TRAILING_JUNK_AFTER_STREAM_END();
207-
} else {
208-
error = genericNodeError(message, { errno, code });
209-
error.errno = errno;
210-
error.code = code;
211-
}
219+
const error = zlibError(message, errno, code);
212220
self.destroy(error);
213221
self[kError] = error;
214222
}
215223

224+
// Without options, a synchronous convenience call (zlib.inflateSync(buffer),
225+
// ...) uses the defaults and its engine is never exposed, so it drives a
226+
// native handle directly instead of constructing a Transform stream. It also
227+
// decompresses into a scratch buffer kept between calls and copies the result
228+
// out, instead of allocating a whole output chunk per call.
229+
let syncScratchBuffer;
230+
231+
function zlibSyncOnError(message, errno, code) {
232+
this[kError] = zlibError(message, errno, code);
233+
}
234+
235+
function zlibBufferSyncWithDefaults(ctor, buffer) {
236+
const chunk = toSyncInput(buffer);
237+
const mode = kEngineModes.get(ctor);
238+
const writeState = new Uint32Array(2);
239+
const handle = createDefaultHandle(mode, writeState);
240+
handle.onerror = zlibSyncOnError;
241+
// Take the scratch buffer, so that a nested call (e.g. from an async hook)
242+
// allocates its own.
243+
const scratch = syncScratchBuffer ?? Buffer.allocUnsafeSlow(Z_DEFAULT_CHUNK);
244+
syncScratchBuffer = undefined;
245+
try {
246+
return processSyncWithDefaults(handle, writeState, chunk,
247+
defaultFinishFlush(mode), scratch);
248+
} finally {
249+
handle.close();
250+
syncScratchBuffer = scratch;
251+
}
252+
}
253+
254+
// Same as processChunkSync() for an engine created without options.
255+
function processSyncWithDefaults(handle, writeState, chunk, flushFlag,
256+
scratch) {
257+
const chunkSize = Z_DEFAULT_CHUNK;
258+
let out = scratch;
259+
let inOff = 0;
260+
let availIn = chunk.byteLength;
261+
let buffers = null;
262+
let nread = 0;
263+
264+
while (true) {
265+
handle.writeSync(flushFlag, chunk, inOff, availIn, out, 0, chunkSize);
266+
if (handle[kError] !== undefined)
267+
throw handle[kError];
268+
269+
const availOutAfter = writeState[0];
270+
const availInAfter = writeState[1];
271+
const have = chunkSize - availOutAfter;
272+
if (have > 0) {
273+
nread += have;
274+
if (nread > kMaxLength)
275+
throw new ERR_BUFFER_TOO_LARGE(kMaxLength);
276+
if (out !== scratch) {
277+
buffers.push(out.subarray(0, have));
278+
} else if (availOutAfter === 0) {
279+
buffers = [out];
280+
}
281+
}
282+
if (availOutAfter !== 0)
283+
break;
284+
285+
// The output buffer is full. Continue with the rest of the input.
286+
inOff += availIn - availInAfter;
287+
availIn = availInAfter;
288+
out = Buffer.allocUnsafe(chunkSize);
289+
}
290+
291+
if (nread === 0)
292+
return new FastBuffer();
293+
if (buffers === null) {
294+
// All of the output is in the scratch buffer.
295+
const result = Buffer.allocUnsafe(nread);
296+
scratch.copy(result, 0, 0, nread);
297+
return result;
298+
}
299+
return Buffer.concat(buffers, nread);
300+
}
301+
216302
const FLUSH_BOUND = [
217303
[ Z_NO_FLUSH, Z_BLOCK ],
218304
[ BROTLI_OPERATION_PROCESS, BROTLI_OPERATION_EMIT_METADATA ],
@@ -794,6 +880,8 @@ class Unzip extends Zlib {
794880
function createConvenienceMethod(ctor, sync, prepareOpts) {
795881
if (sync) {
796882
return function syncBufferWrapper(buffer, opts) {
883+
if (opts === undefined)
884+
return zlibBufferSyncWithDefaults(ctor, buffer);
797885
return zlibBufferSync(new ctor(opts), buffer);
798886
};
799887
}
@@ -999,6 +1087,61 @@ class ZstdDecompress extends Zstd {
9991087
}
10001088
}
10011089

1090+
const kEngineModes = new SafeMap([
1091+
[Deflate, DEFLATE],
1092+
[Inflate, INFLATE],
1093+
[Gzip, GZIP],
1094+
[Gunzip, GUNZIP],
1095+
[DeflateRaw, DEFLATERAW],
1096+
[InflateRaw, INFLATERAW],
1097+
[Unzip, UNZIP],
1098+
[BrotliCompress, BROTLI_ENCODE],
1099+
[BrotliDecompress, BROTLI_DECODE],
1100+
[ZstdCompress, ZSTD_COMPRESS],
1101+
[ZstdDecompress, ZSTD_DECOMPRESS],
1102+
]);
1103+
1104+
// Creates the native handle that the engine for `mode` creates when its
1105+
// constructor gets no options.
1106+
function createDefaultHandle(mode, writeState) {
1107+
let handle;
1108+
switch (mode) {
1109+
case BROTLI_ENCODE:
1110+
case BROTLI_DECODE:
1111+
handle = mode === BROTLI_DECODE ?
1112+
new binding.BrotliDecoder(mode) : new binding.BrotliEncoder(mode);
1113+
brotliInitParamsArray.fill(-1);
1114+
handle.init(brotliInitParamsArray, writeState, processCallback, undefined);
1115+
return handle;
1116+
case ZSTD_COMPRESS:
1117+
zstdInitCParamsArray.fill(-1);
1118+
handle = new binding.ZstdCompress();
1119+
handle.init(zstdInitCParamsArray, undefined, writeState, processCallback,
1120+
undefined, false);
1121+
return handle;
1122+
case ZSTD_DECOMPRESS:
1123+
zstdInitDParamsArray.fill(-1);
1124+
handle = new binding.ZstdDecompress();
1125+
handle.init(zstdInitDParamsArray, undefined, writeState, processCallback,
1126+
undefined, false);
1127+
return handle;
1128+
default:
1129+
handle = new binding.Zlib(mode);
1130+
handle.init(Z_DEFAULT_WINDOWBITS, Z_DEFAULT_COMPRESSION,
1131+
Z_DEFAULT_MEMLEVEL, Z_DEFAULT_STRATEGY, writeState,
1132+
processCallback, undefined, false);
1133+
return handle;
1134+
}
1135+
}
1136+
1137+
function defaultFinishFlush(mode) {
1138+
if (mode === BROTLI_ENCODE || mode === BROTLI_DECODE)
1139+
return brotliDefaultOpts.finishFlush;
1140+
if (mode === ZSTD_COMPRESS || mode === ZSTD_DECOMPRESS)
1141+
return zstdDefaultOpts.finishFlush;
1142+
return zlibDefaultOpts.finishFlush;
1143+
}
1144+
10021145
function createProperty(ctor) {
10031146
return {
10041147
__proto__: null,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
'use strict';
2+
// Synchronous convenience methods called without options skip the stream and
3+
// reuse an internal output buffer. Check that they behave exactly like calls
4+
// that pass an empty options object, which go through the stream.
5+
6+
const common = require('../common');
7+
const assert = require('assert');
8+
const async_hooks = require('async_hooks');
9+
const fs = require('fs');
10+
const zlib = require('zlib');
11+
12+
const { Z_DEFAULT_CHUNK } = zlib.constants;
13+
14+
const pairs = [
15+
['deflateSync', 'inflateSync'],
16+
['gzipSync', 'gunzipSync'],
17+
['gzipSync', 'unzipSync'],
18+
['deflateRawSync', 'inflateRawSync'],
19+
['brotliCompressSync', 'brotliDecompressSync'],
20+
['zstdCompressSync', 'zstdDecompressSync'],
21+
];
22+
23+
const random = Buffer.alloc(3 * Z_DEFAULT_CHUNK);
24+
{
25+
let seed = 0x9e3779b9;
26+
for (let i = 0; i < random.length; i++) {
27+
seed ^= seed << 13;
28+
seed ^= seed >>> 17;
29+
seed ^= seed << 5;
30+
random[i] = seed & 0xff;
31+
}
32+
}
33+
const text = fs.readFileSync(__filename);
34+
35+
// Outputs that fit in one output buffer, fill it exactly, and span several.
36+
const inputs = [
37+
Buffer.alloc(0),
38+
Buffer.from('hello'),
39+
text,
40+
Buffer.alloc(Z_DEFAULT_CHUNK, 'a'),
41+
Buffer.alloc(Z_DEFAULT_CHUNK + 1, 'b'),
42+
Buffer.concat([text, random, text]),
43+
];
44+
45+
for (const [compress, decompress] of pairs) {
46+
for (const input of inputs) {
47+
const label = `${compress}/${decompress} ${input.length}`;
48+
const compressed = zlib[compress](input);
49+
assert.ok(Buffer.isBuffer(compressed), label);
50+
assert.deepStrictEqual(compressed, zlib[compress](input, {}), label);
51+
52+
const decompressed = zlib[decompress](compressed);
53+
assert.ok(Buffer.isBuffer(decompressed), label);
54+
assert.deepStrictEqual(decompressed, input, label);
55+
assert.deepStrictEqual(decompressed, zlib[decompress](compressed, {}),
56+
label);
57+
}
58+
59+
// Every input type.
60+
const expected = zlib[decompress](zlib[compress]('blah'.repeat(8)));
61+
for (const view of [
62+
'blah'.repeat(8),
63+
...common.getBufferSources(Buffer.from('blah'.repeat(8))),
64+
]) {
65+
assert.deepStrictEqual(zlib[decompress](zlib[compress](view)), expected);
66+
}
67+
}
68+
69+
// Results never share memory with each other.
70+
{
71+
const a = zlib.deflateSync('first');
72+
const b = zlib.deflateSync('second');
73+
const first = zlib.inflateSync(a);
74+
const second = zlib.inflateSync(b);
75+
assert.strictEqual(first.toString(), 'first');
76+
assert.strictEqual(second.toString(), 'second');
77+
second.fill(0);
78+
assert.strictEqual(first.toString(), 'first');
79+
assert.strictEqual(zlib.inflateSync(a).toString(), 'first');
80+
}
81+
82+
// Errors are the same as through the stream.
83+
for (const [method, input] of [
84+
['inflateSync', Buffer.from('not compressed')],
85+
['gunzipSync', Buffer.from('not compressed')],
86+
['unzipSync', Buffer.from('not compressed')],
87+
['inflateRawSync', Buffer.from('not compressed')],
88+
['brotliDecompressSync', Buffer.from('not compressed')],
89+
['zstdDecompressSync', Buffer.from('not compressed')],
90+
['inflateSync', zlib.deflateSync(text).subarray(0, 100)],
91+
['gunzipSync', zlib.gzipSync(text).subarray(0, 100)],
92+
['brotliDecompressSync', zlib.brotliCompressSync(text).subarray(0, 100)],
93+
['zstdDecompressSync', zlib.zstdCompressSync(text).subarray(0, 100)],
94+
]) {
95+
let expected;
96+
assert.throws(() => zlib[method](input, {}), (err) => {
97+
expected = err;
98+
return true;
99+
});
100+
assert.throws(() => zlib[method](input), (err) => {
101+
assert.strictEqual(err.constructor, expected.constructor);
102+
assert.strictEqual(err.message, expected.message);
103+
assert.strictEqual(err.code, expected.code);
104+
assert.strictEqual(err.errno, expected.errno);
105+
return true;
106+
}, method);
107+
}
108+
109+
for (const [compress] of pairs) {
110+
assert.throws(() => zlib[compress](42), {
111+
code: 'ERR_INVALID_ARG_TYPE',
112+
name: 'TypeError',
113+
});
114+
}
115+
116+
// A synchronous call made from an async hook while another one is running.
117+
{
118+
const compressed = zlib.deflateSync(text);
119+
let nested;
120+
const hook = async_hooks.createHook({
121+
before() {
122+
if (nested === undefined) {
123+
nested = null;
124+
nested = zlib.inflateSync(compressed);
125+
}
126+
},
127+
}).enable();
128+
// The error is reported through a callback into JavaScript, which runs the
129+
// before hook.
130+
assert.throws(() => zlib.inflateSync(Buffer.from('not compressed')), {
131+
code: 'Z_DATA_ERROR',
132+
});
133+
hook.disable();
134+
assert.deepStrictEqual(nested, text);
135+
assert.deepStrictEqual(zlib.inflateSync(compressed), text);
136+
}

0 commit comments

Comments
 (0)