Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1954,9 +1954,34 @@ export function gunzipSync(data: Uint8Array): Uint8Array<ArrayBuffer>;
*/
export function gunzipSync<TArrayBuffer extends ArrayBufferLike = ArrayBuffer>(data: Uint8Array, opts?: GunzipOptions<TArrayBuffer>): Uint8Array<TArrayBuffer>;
export function gunzipSync(data: Uint8Array, opts?: GunzipOptions) {
const st = gzs(data);
let st = gzs(data);
if (st + 8 > data.length) err(6, 'invalid gzip data');
return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary);
const dict = opts && opts.dictionary;
// RFC 1952 2.2: a gzip stream is a series of members, all of which must be
// decoded and concatenated. Decode each member in turn, advancing past its
// header, DEFLATE data, and 8-byte trailer. inflt stops at the member's
// BFINAL, leaving ist.p at the end of its DEFLATE data, which locates the
// member boundary. Per-member output sizes are not known ahead of time (the
// trailing ISIZE only describes the last member), so let inflt size each
// member itself, then concatenate.
const members: Uint8Array[] = [];
let total = 0, end: number;
for (;;) {
const ist: InflateState = { i: 2 };
const mem = inflt(data.subarray(st, -8), ist, undefined, dict) as Uint8Array;
end = st + shft(ist.p as number) + 8;
members.push(mem), total += mem.length;
if (end + 8 > data.length) break;
st = gzs(data.subarray(end)) + end;
if (st + 8 > data.length) err(6, 'invalid gzip data');
}
// Single member: hand back its buffer directly (unless a target was given).
if (members.length == 1 && !(opts && opts.out)) return members[0] as Uint8Array<ArrayBuffer>;
const out = opts && opts.out || new u8(total);
for (let i = 0, off = 0; i < members.length; off += members[i++].length) {
out.set(members[i], off);
}
return out as Uint8Array<ArrayBuffer>;
}

/**
Expand Down
51 changes: 51 additions & 0 deletions test/6-multimember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { gzipSync as nodeGzip, gunzipSync as nodeGunzip } from 'zlib';
import { gunzipSync, decompressSync, Gunzip } from '..';

// RFC 1952 section 2.2: a gzip file consists of a series of members; all of
// them must be decoded and concatenated. fflate's streaming Gunzip already
// does this; gunzipSync (and decompressSync, which routes to it) must too.
const multimember = suite('multi-member gzip (sync)');

const concat = (...parts: Buffer[]) => Buffer.concat(parts);

multimember('gunzipSync decodes a two-member stream', () => {
const input = concat(nodeGzip(Buffer.from('AAAAAAAA')), nodeGzip(Buffer.from('BB')));
// Oracle: node zlib decodes the full concatenation.
assert.equal(Buffer.from(nodeGunzip(input)).toString(), 'AAAAAAAABB');
assert.equal(Buffer.from(gunzipSync(input)).toString(), 'AAAAAAAABB');
});

multimember('gunzipSync matches the streaming Gunzip on many members', () => {
const parts: Buffer[] = [];
const chunks: string[] = [];
for (let i = 0; i < 5; ++i) {
const s = 'member-' + i + '-payload';
chunks.push(s);
parts.push(nodeGzip(Buffer.from(s)));
}
const input = concat(...parts);
const expected = chunks.join('');

assert.equal(Buffer.from(nodeGunzip(input)).toString(), expected);
assert.equal(Buffer.from(gunzipSync(input)).toString(), expected);

// fflate's own streaming path is already correct; sync must agree with it.
const acc: Buffer[] = [];
const g = new Gunzip(chunk => acc.push(Buffer.from(chunk)));
g.push(input, true);
assert.equal(Buffer.concat(acc).toString(), expected);
});

multimember('decompressSync handles multi-member gzip', () => {
const input = concat(nodeGzip(Buffer.from('hello ')), nodeGzip(Buffer.from('world')));
assert.equal(Buffer.from(decompressSync(input)).toString(), 'hello world');
});

multimember('single-member gunzipSync still works', () => {
const input = nodeGzip(Buffer.from('just one member'));
assert.equal(Buffer.from(gunzipSync(input)).toString(), 'just one member');
});

multimember.run();