Skip to content

Commit a47d32d

Browse files
committed
zlib: expose rejectGarbageAfterEnd option
Document rejectGarbageAfterEnd as a public decompression option and validate it as a boolean. Add coverage for stream, async convenience, and sync convenience APIs across zlib, gzip, Brotli, and Zstd-backed decompression. Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 237b450 commit a47d32d

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

doc/api/zlib.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,9 @@ These advanced options are available for controlling decompression:
801801
<!-- YAML
802802
added: v0.11.1
803803
changes:
804+
- version: REPLACEME
805+
pr-url: https://github.com/nodejs/node/pull/64023
806+
description: The `rejectGarbageAfterEnd` option was added.
804807
- version:
805808
- v14.5.0
806809
- v12.19.0
@@ -836,6 +839,9 @@ ignored by the decompression classes.
836839
* `info` {boolean} (If `true`, returns an object with `buffer` and `engine`.)
837840
* `maxOutputLength` {integer} Limits output size when using
838841
[convenience methods][]. **Default:** [`buffer.kMaxLength`][]
842+
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
843+
trailing input is detected after the end of the compressed stream.
844+
**Default:** `false`
839845

840846
See the [`deflateInit2` and `inflateInit2`][] documentation for more
841847
information.
@@ -845,6 +851,9 @@ information.
845851
<!-- YAML
846852
added: v11.7.0
847853
changes:
854+
- version: REPLACEME
855+
pr-url: https://github.com/nodejs/node/pull/64023
856+
description: The `rejectGarbageAfterEnd` option was added.
848857
- version:
849858
- v14.5.0
850859
- v12.19.0
@@ -863,6 +872,9 @@ Each Brotli-based class takes an `options` object. All options are optional.
863872
* `maxOutputLength` {integer} Limits output size when using
864873
[convenience methods][]. **Default:** [`buffer.kMaxLength`][]
865874
* `info` {boolean} If `true`, returns an object with `buffer` and `engine`. **Default:** `false`
875+
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
876+
trailing input is detected after the end of the compressed stream.
877+
**Default:** `false`
866878

867879
For example:
868880

@@ -1086,6 +1098,10 @@ the inflate and deflate algorithms.
10861098
added:
10871099
- v23.8.0
10881100
- v22.15.0
1101+
changes:
1102+
- version: REPLACEME
1103+
pr-url: https://github.com/nodejs/node/pull/64023
1104+
description: The `rejectGarbageAfterEnd` option was added.
10891105
-->
10901106

10911107
<!--type=misc-->
@@ -1102,6 +1118,9 @@ Each Zstd-based class takes an `options` object. All options are optional.
11021118
* `dictionary` {Buffer} Optional dictionary used to
11031119
improve compression efficiency when compressing or decompressing data that
11041120
shares common patterns with the dictionary.
1121+
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
1122+
trailing input is detected after the end of the compressed stream.
1123+
**Default:** `false`
11051124

11061125
For example:
11071126

lib/zlib.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const {
6565
const { owner_symbol } = require('internal/async_hooks').symbols;
6666
const {
6767
checkRangesOrGetDefault,
68+
validateBoolean,
6869
validateFunction,
6970
validateUint32,
7071
validateFiniteNumber,
@@ -246,6 +247,13 @@ function ZlibBase(opts, mode, handle, { flush, finishFlush, fullFlush }) {
246247
opts.maxOutputLength, 'options.maxOutputLength',
247248
1, kMaxLength, kMaxLength);
248249

250+
if (opts.rejectGarbageAfterEnd !== undefined) {
251+
validateBoolean(
252+
opts.rejectGarbageAfterEnd,
253+
'options.rejectGarbageAfterEnd',
254+
);
255+
}
256+
249257
if (opts.encoding || opts.objectMode || opts.writableObjectMode) {
250258
opts = { ...opts };
251259
opts.encoding = null;
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('assert');
5+
const test = require('node:test');
6+
const { finished } = require('stream/promises');
7+
const zlib = require('zlib');
8+
9+
const trailingJunkError = {
10+
code: 'ERR_TRAILING_JUNK_AFTER_STREAM_END',
11+
name: 'TypeError',
12+
};
13+
14+
function callAsync(fn, input, options) {
15+
return new Promise((resolve, reject) => {
16+
fn(input, options, (err, result) => {
17+
if (err) {
18+
reject(err);
19+
} else {
20+
resolve(result);
21+
}
22+
});
23+
});
24+
}
25+
26+
async function collect(stream, input) {
27+
const chunks = [];
28+
stream.on('data', (chunk) => chunks.push(chunk));
29+
stream.end(input);
30+
await finished(stream);
31+
return Buffer.concat(chunks);
32+
}
33+
34+
const cases = [
35+
{
36+
label: 'inflate',
37+
compress: zlib.deflateSync,
38+
decompress: zlib.inflate,
39+
decompressSync: zlib.inflateSync,
40+
createDecompress: zlib.createInflate,
41+
defaultOutput: 'a',
42+
},
43+
{
44+
label: 'inflateRaw',
45+
compress: zlib.deflateRawSync,
46+
decompress: zlib.inflateRaw,
47+
decompressSync: zlib.inflateRawSync,
48+
createDecompress: zlib.createInflateRaw,
49+
defaultOutput: 'a',
50+
},
51+
{
52+
label: 'gunzip',
53+
compress: zlib.gzipSync,
54+
decompress: zlib.gunzip,
55+
decompressSync: zlib.gunzipSync,
56+
createDecompress: zlib.createGunzip,
57+
defaultOutput: 'aa',
58+
},
59+
{
60+
label: 'unzip',
61+
compress: zlib.gzipSync,
62+
decompress: zlib.unzip,
63+
decompressSync: zlib.unzipSync,
64+
createDecompress: zlib.createUnzip,
65+
defaultOutput: 'aa',
66+
},
67+
{
68+
label: 'brotli',
69+
compress: zlib.brotliCompressSync,
70+
decompress: zlib.brotliDecompress,
71+
decompressSync: zlib.brotliDecompressSync,
72+
createDecompress: zlib.createBrotliDecompress,
73+
defaultOutput: 'a',
74+
},
75+
{
76+
label: 'zstd',
77+
compress: zlib.zstdCompressSync,
78+
decompress: zlib.zstdDecompress,
79+
decompressSync: zlib.zstdDecompressSync,
80+
createDecompress: zlib.createZstdDecompress,
81+
defaultOutput: 'a',
82+
},
83+
];
84+
85+
for (const {
86+
label,
87+
compress,
88+
decompress,
89+
decompressSync,
90+
createDecompress,
91+
defaultOutput,
92+
} of cases) {
93+
test(`rejectGarbageAfterEnd rejects trailing input for ${label}`, async () => {
94+
const compressed = compress(Buffer.from('a'));
95+
const withTrailingInput = Buffer.concat([compressed, compressed]);
96+
97+
assert.strictEqual(decompressSync(withTrailingInput).toString(), defaultOutput);
98+
assert.strictEqual(
99+
(await callAsync(decompress, withTrailingInput)).toString(),
100+
defaultOutput,
101+
);
102+
assert.strictEqual(
103+
(await collect(createDecompress(), withTrailingInput)).toString(),
104+
defaultOutput,
105+
);
106+
107+
assert.throws(
108+
() => decompressSync(withTrailingInput, { rejectGarbageAfterEnd: true }),
109+
trailingJunkError,
110+
);
111+
await assert.rejects(
112+
callAsync(decompress, withTrailingInput, { rejectGarbageAfterEnd: true }),
113+
trailingJunkError,
114+
);
115+
await assert.rejects(
116+
collect(
117+
createDecompress({ rejectGarbageAfterEnd: true }),
118+
withTrailingInput,
119+
),
120+
trailingJunkError,
121+
);
122+
});
123+
}
124+
125+
test('rejectGarbageAfterEnd must be a boolean', () => {
126+
const compressed = zlib.deflateSync(Buffer.from('a'));
127+
128+
for (const value of [1, 'true', null]) {
129+
assert.throws(
130+
() => zlib.inflateSync(compressed, { rejectGarbageAfterEnd: value }),
131+
{
132+
code: 'ERR_INVALID_ARG_TYPE',
133+
name: 'TypeError',
134+
},
135+
);
136+
assert.throws(
137+
() => zlib.createInflate({ rejectGarbageAfterEnd: value }),
138+
{
139+
code: 'ERR_INVALID_ARG_TYPE',
140+
name: 'TypeError',
141+
},
142+
);
143+
}
144+
});

0 commit comments

Comments
 (0)