Skip to content

Commit fd0fb00

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 <panva.ip@gmail.com> PR-URL: #64023Fixes: #58247 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent e334d30 commit fd0fb00

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+
constassert=require('assert');
5+
consttest=require('node:test');
6+
const{ finished }=require('stream/promises');
7+
constzlib=require('zlib');
8+
9+
consttrailingJunkError={
10+
code: 'ERR_TRAILING_JUNK_AFTER_STREAM_END',
11+
name: 'TypeError',
12+
};
13+
14+
functioncallAsync(fn,input,options){
15+
returnnewPromise((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+
asyncfunctioncollect(stream,input){
27+
constchunks=[];
28+
stream.on('data',(chunk)=>chunks.push(chunk));
29+
stream.end(input);
30+
awaitfinished(stream);
31+
returnBuffer.concat(chunks);
32+
}
33+
34+
constcases=[
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+
}ofcases){
93+
test(`rejectGarbageAfterEnd rejects trailing input for ${label}`,async()=>{
94+
constcompressed=compress(Buffer.from('a'));
95+
constwithTrailingInput=Buffer.concat([compressed,compressed]);
96+
97+
assert.strictEqual(decompressSync(withTrailingInput).toString(),defaultOutput);
98+
assert.strictEqual(
99+
(awaitcallAsync(decompress,withTrailingInput)).toString(),
100+
defaultOutput,
101+
);
102+
assert.strictEqual(
103+
(awaitcollect(createDecompress(),withTrailingInput)).toString(),
104+
defaultOutput,
105+
);
106+
107+
assert.throws(
108+
()=>decompressSync(withTrailingInput,{rejectGarbageAfterEnd: true}),
109+
trailingJunkError,
110+
);
111+
awaitassert.rejects(
112+
callAsync(decompress,withTrailingInput,{rejectGarbageAfterEnd: true}),
113+
trailingJunkError,
114+
);
115+
awaitassert.rejects(
116+
collect(
117+
createDecompress({rejectGarbageAfterEnd: true}),
118+
withTrailingInput,
119+
),
120+
trailingJunkError,
121+
);
122+
});
123+
}
124+
125+
test('rejectGarbageAfterEnd must be a boolean',()=>{
126+
constcompressed=zlib.deflateSync(Buffer.from('a'));
127+
128+
for(constvalueof[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)