From 10c66206e86dc606d3a0eea0f73618375ce36d99 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 13:16:26 +0900 Subject: [PATCH 01/11] Add ZIP64 footer support for archives with more than 65,535 files --- src/index.ts | 61 +++++++++++++++++++++++++++++++++++++++++++-------- test/3-zip.ts | 40 ++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8460f8d..104b6fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2843,13 +2843,53 @@ const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c: nu return b; } -// write zip footer (end of central directory) +// write 64-bit little-endian integer, safe up to Number.MAX_SAFE_INTEGER +const wbytes8 = (d: Uint8Array, b: number, v: number) => { + wbytes(d, b, v); + wbytes(d, b + 4, Math.floor(v / 0x100000000)); +} + +// ZIP footer length needed for EOCD or ZIP64 EOCD + locator + EOCD +const wzfs = (c: number, d: number, e: number) => + c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; + +// write zip footer (end of central directory); returns bytes written const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => { - wbytes(o, b, 0x6054B50); // skip disk - wbytes(o, b + 8, c); - wbytes(o, b + 10, c); - wbytes(o, b + 12, d); - wbytes(o, b + 16, e); + const z = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF; + + if (z) { + const zb = b; + + // ZIP64 end of central directory record + wbytes(o, b, 0x6064B50); // signature + wbytes8(o, b + 4, 44); // size of remaining record + wbytes(o, b + 12, 45); // version made by / needed + wbytes(o, b + 14, 45); + wbytes(o, b + 16, 0); // disk number + wbytes(o, b + 20, 0); // disk with central directory + wbytes8(o, b + 24, c); // entries on this disk + wbytes8(o, b + 32, c); // total entries + wbytes8(o, b + 40, d); // central directory size + wbytes8(o, b + 48, e); // central directory offset + b += 56; + + // ZIP64 end of central directory locator + wbytes(o, b, 0x7064B50); // signature + wbytes(o, b + 4, 0); // disk with ZIP64 EOCD + wbytes8(o, b + 8, zb); // offset of ZIP64 EOCD + wbytes(o, b + 16, 1); // total disks + b += 20; + } + + // classic EOCD, with sentinels when ZIP64 is used + wbytes(o, b, 0x6054B50); // skip disk + wbytes(o, b + 4, 0); // disk fields + wbytes(o, b + 8, z ? 0xFFFF : c); + wbytes(o, b + 10, z ? 0xFFFF : c); + wbytes(o, b + 12, z ? 0xFFFFFFFF : d); + wbytes(o, b + 16, z ? 0xFFFFFFFF : e); + wbytes(o, b + 20, 0); // comment length + return z ? 98 : 22; } /** @@ -3228,7 +3268,8 @@ export class Zip { private e() { let bt = 0, l = 0, tl = 0; for (const f of this.u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0); - const out = new u8(tl + 22); + const fl = wzfs(this.u.length, tl, l); + const out = new u8(tl + fl); for (const f of this.u) { wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o); bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; @@ -3285,7 +3326,8 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback, } mt(() => { cbd = cb; }); const cbf = () => { - const out = new u8(tot + 22), oe = o, cdl = tot - o; + const oe = o, cdl = tot - o; + const out = new u8(tot + wzfs(files.length, cdl, oe)); tot = 0; for (let i = 0; i < slft; ++i) { const f = files[i]; @@ -3384,7 +3426,8 @@ export function zipSync(data: Zippable, opts?: ZipOptions) { o += 30 + s + exl + l; tot += 76 + 2 * (s + exl) + (ms || 0) + l; } - const out = new u8(tot + 22), oe = o, cdl = tot - o; + const oe = o, cdl = tot - o; + const out = new u8(tot + wzfs(files.length, cdl, oe)); for (let i = 0; i < files.length; ++i) { const f = files[i]; wzh(out, f.o, f, f.f, f.u, f.c.length); diff --git a/test/3-zip.ts b/test/3-zip.ts index 840a999..c26c2b5 100644 --- a/test/3-zip.ts +++ b/test/3-zip.ts @@ -1 +1,39 @@ -// TODO: test ZIP \ No newline at end of file +// TODO: test ZIP + +import { testSuites } from './util'; +import * as assert from 'uvu/assert'; +import { zipSync, unzipSync, zip, unzip } from '../src'; +import type { Zippable, Unzipped } from '../src'; + +const b4 = (d: Uint8Array, b: number) => + d[b] | (d[b + 1] << 8) | (d[b + 2] << 16) | (d[b + 3] << 24); + +testSuites({ + zip64_supports_more_than_65535_files_zipSync() { + const data: Zippable = {}; + for (let i = 0; i < 65536; ++i) { + data[i + '.txt'] = new Uint8Array(0); + } + const zipped = zipSync(data, { level: 0 }); + assert.ok(b4(zipped, zipped.length - 98) == 0x06064b50); + assert.ok(b4(zipped, zipped.length - 42) == 0x07064b50); + assert.ok(b4(zipped, zipped.length - 22) == 0x06054b50); + assert.ok(Object.keys(unzipSync(zipped)).length == 65536); + }, + async zip64_supports_more_than_65535_files_zip() { + const data: Zippable = {}; + for (let i = 0; i < 65536; ++i) { + data[i + '.txt'] = new Uint8Array(0); + } + const zipped = await new Promise>((resolve, reject) => { + zip(data, { level: 0 }, (err, result) => err ? reject(err) : resolve(result)); + }); + assert.ok(b4(zipped, zipped.length - 98) == 0x06064b50); + assert.ok(b4(zipped, zipped.length - 42) == 0x07064b50); + assert.ok(b4(zipped, zipped.length - 22) == 0x06054b50); + const unzipped = await new Promise((resolve, reject) => { + unzip(zipped, (err, result) => err ? reject(err) : resolve(result)); + }); + assert.ok(Object.keys(unzipped).length == 65536); + } +}); From af3a01f63797c4a74d43fe9a4cbd8e0c6322ee90 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 14:11:59 +0900 Subject: [PATCH 02/11] reduce bundle size --- src/index.ts | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index 104b6fc..ec150d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2853,43 +2853,41 @@ const wbytes8 = (d: Uint8Array, b: number, v: number) => { const wzfs = (c: number, d: number, e: number) => c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; -// write zip footer (end of central directory); returns bytes written +// write zip footer (end of central directory) const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => { const z = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF; if (z) { - const zb = b; - // ZIP64 end of central directory record wbytes(o, b, 0x6064B50); // signature wbytes8(o, b + 4, 44); // size of remaining record wbytes(o, b + 12, 45); // version made by / needed wbytes(o, b + 14, 45); - wbytes(o, b + 16, 0); // disk number - wbytes(o, b + 20, 0); // disk with central directory + // wbytes(o, b + 16, 0); // disk number + // wbytes(o, b + 20, 0); // disk with central directory wbytes8(o, b + 24, c); // entries on this disk wbytes8(o, b + 32, c); // total entries wbytes8(o, b + 40, d); // central directory size wbytes8(o, b + 48, e); // central directory offset - b += 56; // ZIP64 end of central directory locator - wbytes(o, b, 0x7064B50); // signature - wbytes(o, b + 4, 0); // disk with ZIP64 EOCD - wbytes8(o, b + 8, zb); // offset of ZIP64 EOCD - wbytes(o, b + 16, 1); // total disks - b += 20; + wbytes(o, b + 56, 0x7064B50); // signature + // wbytes(o, b + 60, 0); // disk with ZIP64 EOCD + wbytes8(o, b + 64, b); // offset of ZIP64 EOCD + wbytes(o, b + 72, 1); // total disks + b += 76; + c = 0xFFFF; + d = e = 0xFFFFFFFF; } // classic EOCD, with sentinels when ZIP64 is used wbytes(o, b, 0x6054B50); // skip disk - wbytes(o, b + 4, 0); // disk fields - wbytes(o, b + 8, z ? 0xFFFF : c); - wbytes(o, b + 10, z ? 0xFFFF : c); - wbytes(o, b + 12, z ? 0xFFFFFFFF : d); - wbytes(o, b + 16, z ? 0xFFFFFFFF : e); - wbytes(o, b + 20, 0); // comment length - return z ? 98 : 22; + // wbytes(o, b + 4, 0); // disk fields + wbytes(o, b + 8, c); + wbytes(o, b + 10, c); + wbytes(o, b + 12, d); + wbytes(o, b + 16, e); + // wbytes(o, b + 20, 0); // comment length } /** From 89fc5453a9d694501013fbba8117420ca3690482 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 14:17:22 +0900 Subject: [PATCH 03/11] reduce bundle size --- src/index.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index ec150d8..2cb04b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2850,8 +2850,11 @@ const wbytes8 = (d: Uint8Array, b: number, v: number) => { } // ZIP footer length needed for EOCD or ZIP64 EOCD + locator + EOCD -const wzfs = (c: number, d: number, e: number) => - c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; +const wzfo = (c: number, d: number, e: number) => { + const fl = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; + const out = new u8(d + e + fl); + return out; +} // write zip footer (end of central directory) const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => { @@ -3266,8 +3269,7 @@ export class Zip { private e() { let bt = 0, l = 0, tl = 0; for (const f of this.u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0); - const fl = wzfs(this.u.length, tl, l); - const out = new u8(tl + fl); + const out = wzfo(this.u.length, tl, l); for (const f of this.u) { wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o); bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; @@ -3325,7 +3327,7 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback, mt(() => { cbd = cb; }); const cbf = () => { const oe = o, cdl = tot - o; - const out = new u8(tot + wzfs(files.length, cdl, oe)); + const out = wzfo(files.length, cdl, oe); tot = 0; for (let i = 0; i < slft; ++i) { const f = files[i]; @@ -3425,7 +3427,7 @@ export function zipSync(data: Zippable, opts?: ZipOptions) { tot += 76 + 2 * (s + exl) + (ms || 0) + l; } const oe = o, cdl = tot - o; - const out = new u8(tot + wzfs(files.length, cdl, oe)); + const out = wzfo(files.length, cdl, oe); for (let i = 0; i < files.length; ++i) { const f = files[i]; wzh(out, f.o, f, f.f, f.u, f.c.length); From 9c786609b42c8935745e7da4b8672b8d95a876ca Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 14:18:54 +0900 Subject: [PATCH 04/11] comment fix --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 2cb04b1..2a63758 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2849,7 +2849,7 @@ const wbytes8 = (d: Uint8Array, b: number, v: number) => { wbytes(d, b + 4, Math.floor(v / 0x100000000)); } -// ZIP footer length needed for EOCD or ZIP64 EOCD + locator + EOCD +// allocate ZIP output with enough space for EOCD or ZIP64 EOCD + locator + EOCD const wzfo = (c: number, d: number, e: number) => { const fl = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; const out = new u8(d + e + fl); From 43ea7c4df668d78e9e2139d6f1aa37d96fa11c49 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 14:27:25 +0900 Subject: [PATCH 05/11] reduce bundle size --- src/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a63758..2d0688e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1185,11 +1185,16 @@ const b4 = (d: Uint8Array, b: number) => (d[b] | (d[b + 1] << 8) | (d[b + 2] << // read 8 bytes const b8 = (d: Uint8Array, b: number) => b4(d, b) + (b4(d, b + 4) * 4294967296); -// write bytes +// write bytes, assumes zero-filled output const wbytes = (d: Uint8Array, b: number, v: number) => { for (; v; ++b) d[b] = v, v >>>= 8; } +// write 64-bit little-endian integer, assumes zero-filled output +const wbytes8 = (d: Uint8Array, b: number, v: number) => { + for (; v; v /= 256) v -= d[b++] = v % 256; +} + // gzip header const gzh = (c: Uint8Array, o: GzipOptions) => { const fn = o.filename; @@ -2843,12 +2848,6 @@ const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c: nu return b; } -// write 64-bit little-endian integer, safe up to Number.MAX_SAFE_INTEGER -const wbytes8 = (d: Uint8Array, b: number, v: number) => { - wbytes(d, b, v); - wbytes(d, b + 4, Math.floor(v / 0x100000000)); -} - // allocate ZIP output with enough space for EOCD or ZIP64 EOCD + locator + EOCD const wzfo = (c: number, d: number, e: number) => { const fl = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; From f10b6a546f2ef0c610e1962eebbb3cd7e701b065 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 14:31:36 +0900 Subject: [PATCH 06/11] fix comments --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 2d0688e..59bd2f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1185,7 +1185,7 @@ const b4 = (d: Uint8Array, b: number) => (d[b] | (d[b + 1] << 8) | (d[b + 2] << // read 8 bytes const b8 = (d: Uint8Array, b: number) => b4(d, b) + (b4(d, b + 4) * 4294967296); -// write bytes, assumes zero-filled output +// write 32-bit little-endian integer, assumes zero-filled output const wbytes = (d: Uint8Array, b: number, v: number) => { for (; v; ++b) d[b] = v, v >>>= 8; } From 7290ac3bfb15bea28f0498f6354087fd62227626 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 17:41:05 +0900 Subject: [PATCH 07/11] fix for streaming Zip --- src/index.ts | 60 +++++++++++++++++++++++++++++++++++++-------------- test/3-zip.ts | 32 ++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/index.ts b/src/index.ts index 59bd2f4..bf57de1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2848,15 +2848,36 @@ const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c: nu return b; } -// allocate ZIP output with enough space for EOCD or ZIP64 EOCD + locator + EOCD -const wzfo = (c: number, d: number, e: number) => { +/** + * Allocate ZIP output with enough space for EOCD, or ZIP64 EOCD + locator + EOCD. + * + * @param c Number of files / central directory entries. + * @param d Central directory size. + * @param e Central directory absolute offset in the complete ZIP archive. + * @param k Number of bytes to allocate before the footer in this output buffer. + * For zip()/zipSync(), this is local data + central directory size. + * For streaming Zip.e(), this is only central directory size because + * local data has already been emitted. + */ +const wzfo = (c: number, d: number, e: number, k: number) => { const fl = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; - const out = new u8(d + e + fl); + const out = new u8(k + fl); return out; } -// write zip footer (end of central directory) -const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => { +/** + * Write ZIP footer: EOCD, or ZIP64 EOCD + ZIP64 locator + EOCD. + * + * @param o Output buffer to write into. + * @param b Offset inside `o` where the footer starts. + * @param c Number of files / central directory entries. + * @param d Central directory size. + * @param e Central directory absolute offset in the complete ZIP archive. + * @param bo ZIP64 EOCD absolute offset in the complete ZIP archive. + * Defaults to `b`, which is correct when `o` starts at archive offset 0 + * as in zip()/zipSync(). Streaming Zip.e() must pass `e + d`. + */ +const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number, bo: number = b) => { const z = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF; if (z) { @@ -2875,7 +2896,7 @@ const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number) => { // ZIP64 end of central directory locator wbytes(o, b + 56, 0x7064B50); // signature // wbytes(o, b + 60, 0); // disk with ZIP64 EOCD - wbytes8(o, b + 64, b); // offset of ZIP64 EOCD + wbytes8(o, b + 64, bo); // offset of ZIP64 EOCD (absolute archive offset) wbytes(o, b + 72, 1); // total disks b += 76; c = 0xFFFF; @@ -3266,14 +3287,21 @@ export class Zip { } private e() { - let bt = 0, l = 0, tl = 0; - for (const f of this.u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0); - const out = wzfo(this.u.length, tl, l); - for (const f of this.u) { - wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o); - bt += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; - } - wzf(out, bt, this.u.length, tl, l) + const u = this.u; + let o2 = 0, s2 = 0, o1 = 0, s1 = 0; + for (const f of u) s1 += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), o1 += f.b; + // Streaming Zip has already emitted local headers + file data, so allocate + // only central directory + footer. o1 is still passed so wzfo can decide + // whether ZIP64 is needed due to centralDirectoryOffset overflow. + const out = wzfo(u.length, s1, o1, s1); // k = s1 for streaming + for (const f of u) s2 = wzh(out, s2, f, f.f, f.u, -f.c - 2, o2, f.o), o2 += f.b; + // o1/o2 = centralDirectoryOffset: absolute archive offset where this final + // chunk's central directory starts. + // s1 = predicted centralDirectorySize used for allocation. + // s2 = actual centralDirectorySize, i.e. write cursor after writing all + // central directory entries into this final chunk. + // o2+s2 = absolute archive offset of ZIP64 EOCD, used by the ZIP64 locator. + wzf(out, s2, u.length, s2, o2, o2 + s2); this.ondata(null, out, true); this.d = 2; } @@ -3326,7 +3354,7 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback, mt(() => { cbd = cb; }); const cbf = () => { const oe = o, cdl = tot - o; - const out = wzfo(files.length, cdl, oe); + const out = wzfo(files.length, cdl, oe, tot); tot = 0; for (let i = 0; i < slft; ++i) { const f = files[i]; @@ -3426,7 +3454,7 @@ export function zipSync(data: Zippable, opts?: ZipOptions) { tot += 76 + 2 * (s + exl) + (ms || 0) + l; } const oe = o, cdl = tot - o; - const out = wzfo(files.length, cdl, oe); + const out = wzfo(files.length, cdl, oe, tot); for (let i = 0; i < files.length; ++i) { const f = files[i]; wzh(out, f.o, f, f.f, f.u, f.c.length); diff --git a/test/3-zip.ts b/test/3-zip.ts index c26c2b5..d4018fc 100644 --- a/test/3-zip.ts +++ b/test/3-zip.ts @@ -2,7 +2,7 @@ import { testSuites } from './util'; import * as assert from 'uvu/assert'; -import { zipSync, unzipSync, zip, unzip } from '../src'; +import { zipSync, unzipSync, zip, unzip, Zip, ZipPassThrough } from '../src'; import type { Zippable, Unzipped } from '../src'; const b4 = (d: Uint8Array, b: number) => @@ -35,5 +35,35 @@ testSuites({ unzip(zipped, (err, result) => err ? reject(err) : resolve(result)); }); assert.ok(Object.keys(unzipped).length == 65536); + }, + zip64_supports_more_than_65535_files_streaming_Zip() { + const chunks: Uint8Array[] = []; + const zip = new Zip((err, dat, final) => { + if (err) throw err; + if (dat) chunks.push(dat); + }); + + for (let i = 0; i < 65536; ++i) { + const f = new ZipPassThrough(i + '.txt'); + zip.add(f); + f.push(new Uint8Array(0), true); + } + + zip.end(); + + let len = 0; + for (const c of chunks) len += c.length; + + const zipped = new Uint8Array(len); + let off = 0; + for (const c of chunks) { + zipped.set(c, off); + off += c.length; + } + + assert.ok(b4(zipped, zipped.length - 98) == 0x06064b50); + assert.ok(b4(zipped, zipped.length - 42) == 0x07064b50); + assert.ok(b4(zipped, zipped.length - 22) == 0x06054b50); + assert.ok(Object.keys(unzipSync(zipped)).length == 65536); } }); From cb18cca118bb71041c377415389d464006fcd5c7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 17:45:39 +0900 Subject: [PATCH 08/11] reduce bundle size --- src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index bf57de1..5af74c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2877,7 +2877,7 @@ const wzfo = (c: number, d: number, e: number, k: number) => { * Defaults to `b`, which is correct when `o` starts at archive offset 0 * as in zip()/zipSync(). Streaming Zip.e() must pass `e + d`. */ -const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number, bo: number = b) => { +const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number, bo: number) => { const z = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF; if (z) { @@ -3369,7 +3369,7 @@ export function zip(data: AsyncZippable, opts: AsyncZipOptions | FlateCallback, return cbd(e, null); } } - wzf(out, o, files.length, cdl, oe); + wzf(out, o, files.length, cdl, oe, o); cbd(null, out); } if (!lft) cbf(); @@ -3462,7 +3462,7 @@ export function zipSync(data: Zippable, opts?: ZipOptions) { out.set(f.c, f.o + badd); wzh(out, o, f, f.f, f.u, f.c.length, f.o, f.m), o += 16 + badd + (f.m ? f.m.length : 0); } - wzf(out, o, files.length, cdl, oe); + wzf(out, o, files.length, cdl, oe, o); return out; } From 777a3eedde03b65707efef46955ed19d66f10f25 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 18:08:39 +0900 Subject: [PATCH 09/11] reduce bundle size --- src/index.ts | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/index.ts b/src/index.ts index 5af74c6..39bb5d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2882,22 +2882,22 @@ const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number, bo: numb if (z) { // ZIP64 end of central directory record - wbytes(o, b, 0x6064B50); // signature - wbytes8(o, b + 4, 44); // size of remaining record - wbytes(o, b + 12, 45); // version made by / needed - wbytes(o, b + 14, 45); - // wbytes(o, b + 16, 0); // disk number - // wbytes(o, b + 20, 0); // disk with central directory - wbytes8(o, b + 24, c); // entries on this disk - wbytes8(o, b + 32, c); // total entries - wbytes8(o, b + 40, d); // central directory size - wbytes8(o, b + 48, e); // central directory offset + wbytes(o, b, 0x6064B50); // signature <32-bit> + wbytes(o, b + 4, 44); // size of remaining record <64-bit> + // wbytes(o, b + 8, 0); + wbytes(o, b + 12, 0x2D002D); // version made by / needed <32-bit> = 45 | (45 << 16) + // wbytes(o, b + 16, 0); // disk number <32-bit> + // wbytes(o, b + 20, 0); // disk with central directory <32-bit> + wbytes8(o, b + 24, c); // entries on this disk <64-bit> + wbytes8(o, b + 32, c); // total entries <64-bit> + wbytes8(o, b + 40, d); // central directory size <64-bit> + wbytes8(o, b + 48, e); // central directory offset <64-bit> // ZIP64 end of central directory locator - wbytes(o, b + 56, 0x7064B50); // signature - // wbytes(o, b + 60, 0); // disk with ZIP64 EOCD - wbytes8(o, b + 64, bo); // offset of ZIP64 EOCD (absolute archive offset) - wbytes(o, b + 72, 1); // total disks + wbytes(o, b + 56, 0x7064B50); // signature <32-bit> + // wbytes(o, b + 60, 0); // disk with ZIP64 EOCD <32-bit> + wbytes8(o, b + 64, bo); // offset of ZIP64 EOCD (absolute archive offset) <64-bit> + wbytes(o, b + 72, 1); // total disks <32-bit> b += 76; c = 0xFFFF; d = e = 0xFFFFFFFF; @@ -3288,20 +3288,20 @@ export class Zip { private e() { const u = this.u; - let o2 = 0, s2 = 0, o1 = 0, s1 = 0; - for (const f of u) s1 += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), o1 += f.b; + let p = 0, bt = 0, l = 0, tl = 0; + for (const f of u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; // Streaming Zip has already emitted local headers + file data, so allocate - // only central directory + footer. o1 is still passed so wzfo can decide + // only central directory + footer. `l` is still passed so wzfo can decide // whether ZIP64 is needed due to centralDirectoryOffset overflow. - const out = wzfo(u.length, s1, o1, s1); // k = s1 for streaming - for (const f of u) s2 = wzh(out, s2, f, f.f, f.u, -f.c - 2, o2, f.o), o2 += f.b; - // o1/o2 = centralDirectoryOffset: absolute archive offset where this final + const out = wzfo(u.length, tl, l, tl); // k = `tl` for streaming + for (const f of u) bt = wzh(out, bt, f, f.f, f.u, -f.c - 2, p, f.o), p += f.b; + // l/p = centralDirectoryOffset: absolute archive offset where this final // chunk's central directory starts. - // s1 = predicted centralDirectorySize used for allocation. - // s2 = actual centralDirectorySize, i.e. write cursor after writing all + // tl = predicted centralDirectorySize used for allocation. + // bt = actual centralDirectorySize, i.e. write cursor after writing all // central directory entries into this final chunk. - // o2+s2 = absolute archive offset of ZIP64 EOCD, used by the ZIP64 locator. - wzf(out, s2, u.length, s2, o2, o2 + s2); + // p+bt = absolute archive offset of ZIP64 EOCD, used by the ZIP64 locator. + wzf(out, bt, u.length, bt, p, p + bt); this.ondata(null, out, true); this.d = 2; } From 47d54a9ab4d1af01c6049d606b133aa5acf3523f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 18:12:30 +0900 Subject: [PATCH 10/11] swap p and l --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 39bb5d5..9270fb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3288,20 +3288,20 @@ export class Zip { private e() { const u = this.u; - let p = 0, bt = 0, l = 0, tl = 0; - for (const f of u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), l += f.b; + let bt = 0, l = 0, tl = 0, p = 0; + for (const f of u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), p += f.b; // Streaming Zip has already emitted local headers + file data, so allocate - // only central directory + footer. `l` is still passed so wzfo can decide + // only central directory + footer. `p` is still passed so wzfo can decide // whether ZIP64 is needed due to centralDirectoryOffset overflow. - const out = wzfo(u.length, tl, l, tl); // k = `tl` for streaming - for (const f of u) bt = wzh(out, bt, f, f.f, f.u, -f.c - 2, p, f.o), p += f.b; + const out = wzfo(u.length, tl, p, tl); // k = `tl` for streaming + for (const f of u) bt = wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o), l += f.b; // l/p = centralDirectoryOffset: absolute archive offset where this final // chunk's central directory starts. // tl = predicted centralDirectorySize used for allocation. // bt = actual centralDirectorySize, i.e. write cursor after writing all // central directory entries into this final chunk. - // p+bt = absolute archive offset of ZIP64 EOCD, used by the ZIP64 locator. - wzf(out, bt, u.length, bt, p, p + bt); + // l+bt = absolute archive offset of ZIP64 EOCD, used by the ZIP64 locator. + wzf(out, bt, u.length, bt, l, l + bt); this.ondata(null, out, true); this.d = 2; } From 24b93e80ce6544534e66a5e88b8360e7826c18d0 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sat, 30 May 2026 18:32:06 +0900 Subject: [PATCH 11/11] rewrite the comments to match coding style --- src/index.ts | 64 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9270fb9..857caed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2849,15 +2849,18 @@ const wzh = (d: Uint8Array, b: number, f: ZHF, fn: Uint8Array, u: boolean, c: nu } /** - * Allocate ZIP output with enough space for EOCD, or ZIP64 EOCD + locator + EOCD. + * Allocates the ZIP output buffer with enough space for the trailing footer. + * Dynamically handles classic EOCD (22 bytes) or ZIP64 EOCD + locator (98 bytes). * - * @param c Number of files / central directory entries. - * @param d Central directory size. - * @param e Central directory absolute offset in the complete ZIP archive. - * @param k Number of bytes to allocate before the footer in this output buffer. - * For zip()/zipSync(), this is local data + central directory size. - * For streaming Zip.e(), this is only central directory size because - * local data has already been emitted. + * Parameters: + * @param c - Number of files / Central Directory entries. + * @param d - Total Central Directory size. + * @param e - Absolute Central Directory offset in the complete ZIP archive. + * @param k - Number of bytes to allocate before the footer in this specific buffer. + * + * Note on `k`: + * - For full-buffer generation, `k` includes local headers + file data + Central Directory. + * - For streaming, `k` is just the Central Directory size (`d`). */ const wzfo = (c: number, d: number, e: number, k: number) => { const fl = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF ? 98 : 22; @@ -2866,16 +2869,20 @@ const wzfo = (c: number, d: number, e: number, k: number) => { } /** - * Write ZIP footer: EOCD, or ZIP64 EOCD + ZIP64 locator + EOCD. + * Writes the ZIP trailing footers: classic EOCD, or ZIP64 EOCD + locator + classic EOCD. + * + * Parameters: + * @param o - Output buffer to write into. + * @param b - Offset inside `o` where the footer data starts. + * @param c - Number of files / Central Directory entries. + * @param d - Total Central Directory size. + * @param e - Absolute Central Directory offset in the complete ZIP archive. + * @param bo - Absolute archive offset where the ZIP64 EOCD record begins. * - * @param o Output buffer to write into. - * @param b Offset inside `o` where the footer starts. - * @param c Number of files / central directory entries. - * @param d Central directory size. - * @param e Central directory absolute offset in the complete ZIP archive. - * @param bo ZIP64 EOCD absolute offset in the complete ZIP archive. - * Defaults to `b`, which is correct when `o` starts at archive offset 0 - * as in zip()/zipSync(). Streaming Zip.e() must pass `e + d`. + * Note on `bo`: + * In full-buffer generation, `b` and `bo` are identical. In streaming generation, + * `o` only contains the trailing chunk, meaning the ZIP64 locator needs the + * absolute archive offset (`bo`) rather than the local buffer offset (`b`). */ const wzf = (o: Uint8Array, b: number, c: number, d: number, e: number, bo: number) => { const z = c > 0xFFFF || d > 0xFFFFFFFF || e > 0xFFFFFFFF; @@ -3286,21 +3293,24 @@ export class Zip { this.d = 3; } + /** + * Emits the final streaming ZIP chunk (Central Directory + EOCD footers). + * Local headers and file data are already emitted; this writes the trailer. + * + * Two-pass execution: + * - Pass 1: Predicts Central Directory size (`tl`) and absolute offset (`p`) + * for allocation and ZIP64 decisions. + * - Pass 2: Writes the Central Directory, tracking actual size (`bt`) and + * running absolute offset (`l`). + * + * For ZIP64, the locator uses `l + bt` as the absolute offset of the ZIP64 EOCD. + */ private e() { const u = this.u; let bt = 0, l = 0, tl = 0, p = 0; for (const f of u) tl += 46 + f.f.length + exfl(f.extra) + (f.o ? f.o.length : 0), p += f.b; - // Streaming Zip has already emitted local headers + file data, so allocate - // only central directory + footer. `p` is still passed so wzfo can decide - // whether ZIP64 is needed due to centralDirectoryOffset overflow. - const out = wzfo(u.length, tl, p, tl); // k = `tl` for streaming + const out = wzfo(u.length, tl, p, tl); for (const f of u) bt = wzh(out, bt, f, f.f, f.u, -f.c - 2, l, f.o), l += f.b; - // l/p = centralDirectoryOffset: absolute archive offset where this final - // chunk's central directory starts. - // tl = predicted centralDirectorySize used for allocation. - // bt = actual centralDirectorySize, i.e. write cursor after writing all - // central directory entries into this final chunk. - // l+bt = absolute archive offset of ZIP64 EOCD, used by the ZIP64 locator. wzf(out, bt, u.length, bt, l, l + bt); this.ondata(null, out, true); this.d = 2;