Skip to content

Commit 2d7e316

Browse files
addaleaxbnoordhuis
authored andcommitted
zlib: detect gzip files when using unzip*
Detect whether a gzip file is being passed to `unzip*` by testing the first bytes for the gzip magic bytes, and setting the decompression mode to `GUNZIP` or `INFLATE` according to the result. This enables gzip-only features like multi-member support to be used together with the `unzip*` autodetection support and thereby makes `gunzip*` and `unzip*` return identical results for gzip input again. Add a simple test for checking that features specific to `zlib.gunzip`, notably support for multiple members, also work when using `zlib.unzip`. PR-URL: #5884 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 0d41463 commit 2d7e316

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

src/node_zlib.cc

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ class ZCtx : public AsyncWrap {
6868
windowBits_(0),
6969
write_in_progress_(false),
7070
pending_close_(false),
71-
refs_(0) {
71+
refs_(0),
72+
gzip_id_bytes_read_(0) {
7273
MakeWeak<ZCtx>(this);
7374
}
7475

@@ -225,6 +226,8 @@ class ZCtx : public AsyncWrap {
225226
static void Process(uv_work_t* work_req) {
226227
ZCtx *ctx = ContainerOf(&ZCtx::work_req_, work_req);
227228

229+
const Bytef* next_expected_header_byte = nullptr;
230+
228231
// If the avail_out is left at 0, then it means that it ran out
229232
// of room. If there was avail_out left over, then it means
230233
// that all of the input was consumed.
@@ -235,6 +238,50 @@ class ZCtx : public AsyncWrap {
235238
ctx->err_ = deflate(&ctx->strm_, ctx->flush_);
236239
break;
237240
case UNZIP:
241+
if (ctx->strm_.avail_in > 0) {
242+
next_expected_header_byte = ctx->strm_.next_in;
243+
}
244+
245+
switch (ctx->gzip_id_bytes_read_) {
246+
case 0:
247+
if (next_expected_header_byte == nullptr) {
248+
break;
249+
}
250+
251+
if (*next_expected_header_byte == GZIP_HEADER_ID1) {
252+
ctx->gzip_id_bytes_read_ = 1;
253+
next_expected_header_byte++;
254+
255+
if (ctx->strm_.avail_in == 1) {
256+
// The only available byte was already read.
257+
break;
258+
}
259+
} else {
260+
ctx->mode_ = INFLATE;
261+
break;
262+
}
263+
264+
// fallthrough
265+
case 1:
266+
if (next_expected_header_byte == nullptr) {
267+
break;
268+
}
269+
270+
if (*next_expected_header_byte == GZIP_HEADER_ID2) {
271+
ctx->gzip_id_bytes_read_ = 2;
272+
ctx->mode_ = GUNZIP;
273+
} else {
274+
// There is no actual difference between INFLATE and INFLATERAW
275+
// (after initialization).
276+
ctx->mode_ = INFLATE;
277+
}
278+
279+
break;
280+
default:
281+
CHECK(0 && "invalid number of gzip magic number bytes read");
282+
}
283+
284+
// fallthrough
238285
case INFLATE:
239286
case GUNZIP:
240287
case INFLATERAW:
@@ -591,6 +638,7 @@ class ZCtx : public AsyncWrap {
591638
bool write_in_progress_;
592639
bool pending_close_;
593640
unsigned int refs_;
641+
unsigned int gzip_id_bytes_read_;
594642
};
595643

596644

test/parallel/test-zlib-from-concatenated-gzip.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ zlib.gunzip(data, common.mustCall((err, result) => {
2222
assert.equal(result, 'abcdef', 'result should match original string');
2323
}));
2424

25+
zlib.unzip(data, common.mustCall((err, result) => {
26+
assert.ifError(err);
27+
assert.equal(result, 'abcdef', 'result should match original string');
28+
}));
29+
30+
// Multi-member support does not apply to zlib inflate/deflate.
31+
zlib.unzip(Buffer.concat([
32+
zlib.deflateSync('abc'),
33+
zlib.deflateSync('def')
34+
]), common.mustCall((err, result) => {
35+
assert.ifError(err);
36+
assert.equal(result, 'abc', 'result should match contents of first "member"');
37+
}));
38+
2539
// files that have the "right" magic bytes for starting a new gzip member
2640
// in the middle of themselves, even if they are part of a single
2741
// regularly compressed member
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const zlib = require('zlib');
5+
6+
const data = Buffer.concat([
7+
zlib.gzipSync('abc'),
8+
zlib.gzipSync('def')
9+
]);
10+
11+
const resultBuffers = [];
12+
13+
const unzip = zlib.createUnzip()
14+
.on('error', (err) => {
15+
assert.ifError(err);
16+
})
17+
.on('data', (data) => resultBuffers.push(data))
18+
.on('finish', common.mustCall(() => {
19+
assert.deepStrictEqual(Buffer.concat(resultBuffers).toString(), 'abcdef',
20+
'result should match original string');
21+
}));
22+
23+
for (let i = 0; i < data.length; i++) {
24+
// Write each single byte individually.
25+
unzip.write(Buffer.from([data[i]]));
26+
}
27+
28+
unzip.end();

0 commit comments

Comments
 (0)