Skip to content

Commit ccbe081

Browse files
panvaaduh95
authored andcommitted
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 <[email protected]> PR-URL: #64023 Fixes: #58247 Reviewed-By: Anna Henningsen <[email protected]>
1 parent a58a224 commit ccbe081

3 files changed

Lines changed: 170 additions & 0 deletions

File tree

doc/api/zlib.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,9 @@ These advanced options are available for controlling decompression:
803803
<!-- YAML
804804
added: v0.11.1
805805
changes:
806+
- version: REPLACEME
807+
pr-url: https://github.com/nodejs/node/pull/64023
808+
description: The `rejectGarbageAfterEnd` option was added.
806809
- version:
807810
- v14.5.0
808811
- v12.19.0
@@ -838,6 +841,10 @@ ignored by the decompression classes.
838841
* `info` {boolean} (If `true`, returns an object with `buffer` and `engine`.)
839842
* `maxOutputLength` {integer} Limits output size when using
840843
[convenience methods][]. **Default:** [`buffer.kMaxLength`][]
844+
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
845+
trailing input is detected after the end of the compressed stream. This
846+
includes unreadable bytes and, when decompressing gzip, additional gzip
847+
members following the first member. **Default:** `false`
841848

842849
See the [`deflateInit2` and `inflateInit2`][] documentation for more
843850
information.
@@ -847,6 +854,9 @@ information.
847854
<!-- YAML
848855
added: v11.7.0
849856
changes:
857+
- version: REPLACEME
858+
pr-url: https://github.com/nodejs/node/pull/64023
859+
description: The `rejectGarbageAfterEnd` option was added.
850860
- version:
851861
- v14.5.0
852862
- v12.19.0
@@ -865,6 +875,8 @@ Each Brotli-based class takes an `options` object. All options are optional.
865875
* `maxOutputLength` {integer} Limits output size when using
866876
[convenience methods][]. **Default:** [`buffer.kMaxLength`][]
867877
* `info` {boolean} If `true`, returns an object with `buffer` and `engine`. **Default:** `false`
878+
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
879+
input remains after the first complete compressed stream. **Default:** `false`
868880

869881
For example:
870882

@@ -1088,6 +1100,10 @@ the inflate and deflate algorithms.
10881100
added:
10891101
- v23.8.0
10901102
- v22.15.0
1103+
changes:
1104+
- version: REPLACEME
1105+
pr-url: https://github.com/nodejs/node/pull/64023
1106+
description: The `rejectGarbageAfterEnd` option was added.
10911107
-->
10921108

10931109
<!--type=misc-->
@@ -1104,6 +1120,8 @@ Each Zstd-based class takes an `options` object. All options are optional.
11041120
* `dictionary` {Buffer} Optional dictionary used to
11051121
improve compression efficiency when compressing or decompressing data that
11061122
shares common patterns with the dictionary.
1123+
* `rejectGarbageAfterEnd` {boolean} If `true`, decompression fails when
1124+
input remains after the first complete compressed stream. **Default:** `false`
11071125

11081126
For example:
11091127

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)