neozipkit 1.0.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NEOZIP_APPNOTE.md +625 -0
- package/README.md +44 -11
- package/WHATS_NEW.md +153 -10
- package/dist/browser/ZipkitBrowser.d.ts.map +1 -1
- package/dist/browser/ZipkitBrowser.js +4 -6
- package/dist/browser/ZipkitBrowser.js.map +1 -1
- package/dist/browser/zstd-browser-shim.d.ts +17 -9
- package/dist/browser/zstd-browser-shim.d.ts.map +1 -1
- package/dist/browser/zstd-browser-shim.js +99 -21
- package/dist/browser/zstd-browser-shim.js.map +1 -1
- package/dist/browser-esm/index.js +6534 -1275
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +6534 -1275
- package/dist/browser-umd/index.js.map +4 -4
- package/dist/browser-umd/index.min.js +6 -3
- package/dist/core/ZipCompress.d.ts +6 -0
- package/dist/core/ZipCompress.d.ts.map +1 -1
- package/dist/core/ZipCompress.js +12 -11
- package/dist/core/ZipCompress.js.map +1 -1
- package/dist/core/ZipCopy.d.ts.map +1 -1
- package/dist/core/ZipCopy.js +33 -25
- package/dist/core/ZipCopy.js.map +1 -1
- package/dist/core/ZipDecompress.d.ts.map +1 -1
- package/dist/core/ZipDecompress.js +13 -8
- package/dist/core/ZipDecompress.js.map +1 -1
- package/dist/core/ZipEntry.d.ts +24 -0
- package/dist/core/ZipEntry.d.ts.map +1 -1
- package/dist/core/ZipEntry.js +146 -14
- package/dist/core/ZipEntry.js.map +1 -1
- package/dist/core/Zipkit.d.ts +43 -11
- package/dist/core/Zipkit.d.ts.map +1 -1
- package/dist/core/Zipkit.js +132 -82
- package/dist/core/Zipkit.js.map +1 -1
- package/dist/core/components/HashCalculator.d.ts +22 -1
- package/dist/core/components/HashCalculator.d.ts.map +1 -1
- package/dist/core/components/HashCalculator.js +49 -6
- package/dist/core/components/HashCalculator.js.map +1 -1
- package/dist/core/components/Util.d.ts +3 -4
- package/dist/core/components/Util.d.ts.map +1 -1
- package/dist/core/components/Util.js +10 -8
- package/dist/core/components/Util.js.map +1 -1
- package/dist/core/constants/Headers.d.ts +2 -3
- package/dist/core/constants/Headers.d.ts.map +1 -1
- package/dist/core/constants/Headers.js +15 -5
- package/dist/core/constants/Headers.js.map +1 -1
- package/dist/core/constants/MetaPaths.d.ts +38 -0
- package/dist/core/constants/MetaPaths.d.ts.map +1 -0
- package/dist/core/constants/MetaPaths.js +79 -0
- package/dist/core/constants/MetaPaths.js.map +1 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +18 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/inflateZipPayload.d.ts +6 -0
- package/dist/core/inflateZipPayload.d.ts.map +1 -0
- package/dist/core/inflateZipPayload.js +28 -0
- package/dist/core/inflateZipPayload.js.map +1 -0
- package/dist/core/merkle/MerkleRoot.d.ts +109 -0
- package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
- package/dist/core/merkle/MerkleRoot.js +275 -0
- package/dist/core/merkle/MerkleRoot.js.map +1 -0
- package/dist/core/origin/OriginExtra.d.ts +43 -0
- package/dist/core/origin/OriginExtra.d.ts.map +1 -0
- package/dist/core/origin/OriginExtra.js +187 -0
- package/dist/core/origin/OriginExtra.js.map +1 -0
- package/dist/core/version.js +1 -1
- package/dist/core/zip64/Zip64.d.ts +138 -0
- package/dist/core/zip64/Zip64.d.ts.map +1 -0
- package/dist/core/zip64/Zip64.js +353 -0
- package/dist/core/zip64/Zip64.js.map +1 -0
- package/dist/node/ZipCompressNode.d.ts.map +1 -1
- package/dist/node/ZipCompressNode.js +40 -49
- package/dist/node/ZipCompressNode.js.map +1 -1
- package/dist/node/ZipCopyNode.d.ts.map +1 -1
- package/dist/node/ZipCopyNode.js +26 -26
- package/dist/node/ZipCopyNode.js.map +1 -1
- package/dist/node/ZipDecompressNode.d.ts.map +1 -1
- package/dist/node/ZipDecompressNode.js +21 -3
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +23 -1
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +158 -57
- package/dist/node/ZipkitNode.js.map +1 -1
- package/dist/node/ZstdNode.d.ts +14 -4
- package/dist/node/ZstdNode.d.ts.map +1 -1
- package/dist/node/ZstdNode.js +28 -4
- package/dist/node/ZstdNode.js.map +1 -1
- package/dist/node/buildZipBuffer.d.ts +49 -0
- package/dist/node/buildZipBuffer.d.ts.map +1 -0
- package/dist/node/buildZipBuffer.js +146 -0
- package/dist/node/buildZipBuffer.js.map +1 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +6 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/inflateZipPayload.d.ts +5 -0
- package/dist/node/inflateZipPayload.d.ts.map +1 -0
- package/dist/node/inflateZipPayload.js +21 -0
- package/dist/node/inflateZipPayload.js.map +1 -0
- package/dist/types/index.js +2 -2
- package/node-esm.mjs +32 -0
- package/package.json +13 -6
- package/src/browser/ZipkitBrowser.ts +4 -6
- package/src/browser/zstd-browser-shim.ts +110 -28
- package/src/core/ZipCompress.ts +20 -13
- package/src/core/ZipCopy.ts +56 -27
- package/src/core/ZipDecompress.ts +14 -8
- package/src/core/ZipEntry.ts +200 -14
- package/src/core/Zipkit.ts +189 -98
- package/src/core/components/HashCalculator.ts +58 -6
- package/src/core/components/Util.ts +16 -7
- package/src/core/constants/Headers.ts +15 -4
- package/src/core/constants/MetaPaths.ts +86 -0
- package/src/core/index.ts +17 -0
- package/src/core/inflateZipPayload.ts +27 -0
- package/src/core/merkle/MerkleRoot.ts +312 -0
- package/src/core/origin/OriginExtra.ts +209 -0
- package/src/core/version.ts +1 -1
- package/src/core/zip64/Zip64.ts +473 -0
- package/src/node/ZipCompressNode.ts +40 -49
- package/src/node/ZipCopyNode.ts +31 -29
- package/src/node/ZipDecompressNode.ts +16 -3
- package/src/node/ZipkitNode.ts +203 -72
- package/src/node/ZstdNode.ts +33 -8
- package/src/node/buildZipBuffer.ts +221 -0
- package/src/node/index.ts +7 -0
- package/src/node/inflateZipPayload.ts +19 -0
- package/src/types/index.ts +2 -2
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// ======================================
|
|
2
|
+
// Zip64.ts — APPNOTE Zip64 Version 1 helpers
|
|
3
|
+
// Copyright (c) 2026 NeoWare, Inc. All rights reserved.
|
|
4
|
+
// ======================================
|
|
5
|
+
// Large sizes/offsets/counts (PKWARE APPNOTE §§4.3.14–4.3.16, 4.5.3).
|
|
6
|
+
// Spanned disks and Zip64 EOCD extensible sector are out of scope.
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CENTRAL_END,
|
|
10
|
+
HDR_ID,
|
|
11
|
+
ZIP64_CENTRAL_DIR,
|
|
12
|
+
ZIP64_CENTRAL_END,
|
|
13
|
+
} from '../constants/Headers';
|
|
14
|
+
|
|
15
|
+
/** Classic u32 overflow sentinel */
|
|
16
|
+
export const ZIP64_U32 = 0xffffffff;
|
|
17
|
+
/** Classic u16 overflow sentinel */
|
|
18
|
+
export const ZIP64_U16 = 0xffff;
|
|
19
|
+
/** Version needed to extract when Zip64 extensions are used (4.5) */
|
|
20
|
+
export const ZIP64_VERSION_NEEDED = 45;
|
|
21
|
+
/**
|
|
22
|
+
* Value stored in Zip64 EOCD "size of Zip64 EOCD record" for Version 1 with
|
|
23
|
+
* an empty extensible data sector: fixed body (56) − leading 12 = 44.
|
|
24
|
+
*/
|
|
25
|
+
export const ZIP64_EOCD_SIZE_FIELD = 44;
|
|
26
|
+
/** Fixed Zip64 EOCD record length with empty extensible sector */
|
|
27
|
+
export const ZIP64_EOCD_RECORD_SIZE = 56;
|
|
28
|
+
/** Zip64 EOCD locator length */
|
|
29
|
+
export const ZIP64_LOCATOR_SIZE = 20;
|
|
30
|
+
/** Classic EOCD fixed length (without comment) */
|
|
31
|
+
export const CLASSIC_EOCD_SIZE = 22;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Buffer/browser APIs must not materialize multi-GiB Zip64 archives in memory.
|
|
35
|
+
* Use Node file/streaming APIs for size/offset Zip64.
|
|
36
|
+
*/
|
|
37
|
+
export const ZIP64_BUFFER_SIZE_OFFSET_ERROR =
|
|
38
|
+
'Zip64 for sizes or offsets ≥ 4 GiB requires the Node file/streaming API; ' +
|
|
39
|
+
'in-memory buffer/browser writers only support Zip64 for entry counts > 65535. ' +
|
|
40
|
+
'Use ZipkitNode (createZipFromFiles / writeZipEntry) instead of buildZipBuffer.';
|
|
41
|
+
|
|
42
|
+
export interface Zip64ExtraFields {
|
|
43
|
+
uncompressedSize?: number;
|
|
44
|
+
compressedSize?: number;
|
|
45
|
+
localHdrOffset?: number;
|
|
46
|
+
diskStart?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ClassicSizeFields {
|
|
50
|
+
uncompressedSize: number;
|
|
51
|
+
compressedSize: number;
|
|
52
|
+
localHdrOffset: number;
|
|
53
|
+
diskStart: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Zip64EocdFields {
|
|
57
|
+
totalEntries: number;
|
|
58
|
+
centralDirSize: number;
|
|
59
|
+
centralDirOffset: number;
|
|
60
|
+
versionMadeBy?: number;
|
|
61
|
+
versionNeeded?: number;
|
|
62
|
+
diskNumber?: number;
|
|
63
|
+
diskWithCdStart?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface BuildEndRecordsOptions {
|
|
67
|
+
totalEntries: number;
|
|
68
|
+
centralDirSize: number;
|
|
69
|
+
centralDirOffset: number;
|
|
70
|
+
/** Absolute file offset where the Zip64 EOCD will be written (CD end). */
|
|
71
|
+
zip64EocdOffset: number;
|
|
72
|
+
archiveComment?: string;
|
|
73
|
+
/**
|
|
74
|
+
* When false (buffer/browser), refuse Zip64 triggered by size/offset overflow.
|
|
75
|
+
* Entry-count Zip64 is still allowed. Default true (Node streaming).
|
|
76
|
+
*/
|
|
77
|
+
allowSizeOffsetZip64?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Emit Zip64 EOCD + locator even when classic widths suffice (APPNOTE MAY).
|
|
80
|
+
* Classic EOCD then uses a CD-offset sentinel so readers consult Zip64 EOCD
|
|
81
|
+
* (Info-ZIP `-fz` style).
|
|
82
|
+
*/
|
|
83
|
+
forceZip64?: boolean;
|
|
84
|
+
versionMadeBy?: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function exceedsU32(value: number): boolean {
|
|
88
|
+
return value > ZIP64_U32;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function exceedsU16(value: number): boolean {
|
|
92
|
+
return value > ZIP64_U16;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function assertSafeInteger(value: number | bigint, label: string): number {
|
|
96
|
+
const n = typeof value === 'bigint' ? Number(value) : value;
|
|
97
|
+
if (!Number.isFinite(n) || !Number.isSafeInteger(n) || n < 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Zip64 ${label} is not a safe non-negative integer (got ${String(value)}); ` +
|
|
100
|
+
`values must fit in Number.MAX_SAFE_INTEGER`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return n;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function readU64AsNumber(buf: Buffer, offset: number): number {
|
|
107
|
+
return assertSafeInteger(buf.readBigUInt64LE(offset), `u64@${offset}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function writeU64(buf: Buffer, offset: number, value: number): void {
|
|
111
|
+
assertSafeInteger(value, `u64 write@${offset}`);
|
|
112
|
+
buf.writeBigUInt64LE(BigInt(value), offset);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function needsZip64Entry(
|
|
116
|
+
uncompressedSize: number,
|
|
117
|
+
compressedSize: number,
|
|
118
|
+
localHdrOffset: number,
|
|
119
|
+
diskStart: number = 0
|
|
120
|
+
): boolean {
|
|
121
|
+
return (
|
|
122
|
+
exceedsU32(uncompressedSize) ||
|
|
123
|
+
exceedsU32(compressedSize) ||
|
|
124
|
+
exceedsU32(localHdrOffset) ||
|
|
125
|
+
exceedsU16(diskStart)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** True when Zip64 is required for member size or local/CD offset (not entry count alone). */
|
|
130
|
+
export function needsZip64SizeOrOffset(
|
|
131
|
+
uncompressedSize: number,
|
|
132
|
+
compressedSize: number,
|
|
133
|
+
localHdrOffset: number,
|
|
134
|
+
centralDirSize: number = 0,
|
|
135
|
+
centralDirOffset: number = 0
|
|
136
|
+
): boolean {
|
|
137
|
+
return (
|
|
138
|
+
exceedsU32(uncompressedSize) ||
|
|
139
|
+
exceedsU32(compressedSize) ||
|
|
140
|
+
exceedsU32(localHdrOffset) ||
|
|
141
|
+
exceedsU32(centralDirSize) ||
|
|
142
|
+
exceedsU32(centralDirOffset)
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function needsZip64Archive(
|
|
147
|
+
totalEntries: number,
|
|
148
|
+
centralDirSize: number,
|
|
149
|
+
centralDirOffset: number
|
|
150
|
+
): boolean {
|
|
151
|
+
return (
|
|
152
|
+
exceedsU16(totalEntries) ||
|
|
153
|
+
exceedsU32(centralDirSize) ||
|
|
154
|
+
exceedsU32(centralDirOffset)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Classic EOCD fields that indicate a Zip64 EOCD/locator must be consulted.
|
|
160
|
+
*/
|
|
161
|
+
export function classicEocdNeedsZip64(eocd: {
|
|
162
|
+
volEntries: number;
|
|
163
|
+
totalEntries: number;
|
|
164
|
+
centralDirSize: number;
|
|
165
|
+
centralDirOffset: number;
|
|
166
|
+
}): boolean {
|
|
167
|
+
return (
|
|
168
|
+
eocd.centralDirOffset === ZIP64_U32 ||
|
|
169
|
+
eocd.centralDirSize === ZIP64_U32 ||
|
|
170
|
+
eocd.volEntries === ZIP64_U16 ||
|
|
171
|
+
eocd.totalEntries === ZIP64_U16
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build Zip64 Extended Information Extra Field (0x0001).
|
|
177
|
+
* Field order is fixed; only include fields whose classic header uses a sentinel.
|
|
178
|
+
* For local headers, APPNOTE requires both sizes when the Zip64 block is present —
|
|
179
|
+
* pass `forLocal: true` to always emit both size fields.
|
|
180
|
+
*/
|
|
181
|
+
export function buildZip64Extra(
|
|
182
|
+
fields: Zip64ExtraFields,
|
|
183
|
+
opts?: { forLocal?: boolean }
|
|
184
|
+
): Buffer {
|
|
185
|
+
const forLocal = opts?.forLocal === true;
|
|
186
|
+
const parts: number[] = [];
|
|
187
|
+
|
|
188
|
+
const includeUncompressed =
|
|
189
|
+
forLocal || fields.uncompressedSize !== undefined;
|
|
190
|
+
const includeCompressed = forLocal || fields.compressedSize !== undefined;
|
|
191
|
+
|
|
192
|
+
if (includeUncompressed) {
|
|
193
|
+
if (fields.uncompressedSize === undefined) {
|
|
194
|
+
throw new Error('Zip64 local extra requires uncompressedSize');
|
|
195
|
+
}
|
|
196
|
+
parts.push(8);
|
|
197
|
+
}
|
|
198
|
+
if (includeCompressed) {
|
|
199
|
+
if (fields.compressedSize === undefined) {
|
|
200
|
+
throw new Error('Zip64 local extra requires compressedSize');
|
|
201
|
+
}
|
|
202
|
+
parts.push(8);
|
|
203
|
+
}
|
|
204
|
+
if (fields.localHdrOffset !== undefined) parts.push(8);
|
|
205
|
+
if (fields.diskStart !== undefined) parts.push(4);
|
|
206
|
+
|
|
207
|
+
const dataSize = parts.reduce((a, b) => a + b, 0);
|
|
208
|
+
const buf = Buffer.alloc(4 + dataSize);
|
|
209
|
+
buf.writeUInt16LE(HDR_ID.ZIP64, 0);
|
|
210
|
+
buf.writeUInt16LE(dataSize, 2);
|
|
211
|
+
let o = 4;
|
|
212
|
+
if (includeUncompressed) {
|
|
213
|
+
writeU64(buf, o, fields.uncompressedSize!);
|
|
214
|
+
o += 8;
|
|
215
|
+
}
|
|
216
|
+
if (includeCompressed) {
|
|
217
|
+
writeU64(buf, o, fields.compressedSize!);
|
|
218
|
+
o += 8;
|
|
219
|
+
}
|
|
220
|
+
if (fields.localHdrOffset !== undefined) {
|
|
221
|
+
writeU64(buf, o, fields.localHdrOffset);
|
|
222
|
+
o += 8;
|
|
223
|
+
}
|
|
224
|
+
if (fields.diskStart !== undefined) {
|
|
225
|
+
buf.writeUInt32LE(fields.diskStart >>> 0, o);
|
|
226
|
+
}
|
|
227
|
+
return buf;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse Zip64 extra payload (data after HeaderID + size).
|
|
232
|
+
* Only fields whose classic values are sentinels are consumed, in APPNOTE order.
|
|
233
|
+
*/
|
|
234
|
+
export function parseZip64Extra(
|
|
235
|
+
data: Buffer,
|
|
236
|
+
classic: ClassicSizeFields
|
|
237
|
+
): Zip64ExtraFields {
|
|
238
|
+
const out: Zip64ExtraFields = {};
|
|
239
|
+
let o = 0;
|
|
240
|
+
if (classic.uncompressedSize === ZIP64_U32) {
|
|
241
|
+
if (o + 8 > data.length) {
|
|
242
|
+
throw new Error('Zip64 extra truncated (uncompressed size)');
|
|
243
|
+
}
|
|
244
|
+
out.uncompressedSize = readU64AsNumber(data, o);
|
|
245
|
+
o += 8;
|
|
246
|
+
}
|
|
247
|
+
if (classic.compressedSize === ZIP64_U32) {
|
|
248
|
+
if (o + 8 > data.length) {
|
|
249
|
+
throw new Error('Zip64 extra truncated (compressed size)');
|
|
250
|
+
}
|
|
251
|
+
out.compressedSize = readU64AsNumber(data, o);
|
|
252
|
+
o += 8;
|
|
253
|
+
}
|
|
254
|
+
if (classic.localHdrOffset === ZIP64_U32) {
|
|
255
|
+
if (o + 8 > data.length) {
|
|
256
|
+
throw new Error('Zip64 extra truncated (local header offset)');
|
|
257
|
+
}
|
|
258
|
+
out.localHdrOffset = readU64AsNumber(data, o);
|
|
259
|
+
o += 8;
|
|
260
|
+
}
|
|
261
|
+
if (classic.diskStart === ZIP64_U16) {
|
|
262
|
+
if (o + 4 > data.length) {
|
|
263
|
+
throw new Error('Zip64 extra truncated (disk start)');
|
|
264
|
+
}
|
|
265
|
+
out.diskStart = data.readUInt32LE(o);
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** True if a complete extra-field blob contains a Zip64 (0x0001) record. */
|
|
271
|
+
export function extraFieldHasZip64(extra: Buffer | null | undefined): boolean {
|
|
272
|
+
if (!extra || extra.length < 4) return false;
|
|
273
|
+
for (let i = 0; i + 4 <= extra.length; ) {
|
|
274
|
+
const id = extra.readUInt16LE(i);
|
|
275
|
+
const len = extra.readUInt16LE(i + 2);
|
|
276
|
+
if (id === HDR_ID.ZIP64) return true;
|
|
277
|
+
i += 4 + len;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Strip Zip64 records from a concatenated extra-field blob (avoid double-emit on rewrite). */
|
|
283
|
+
export function stripZip64FromExtra(extra: Buffer | null | undefined): Buffer | null {
|
|
284
|
+
if (!extra || extra.length < 4) return extra ?? null;
|
|
285
|
+
const parts: Buffer[] = [];
|
|
286
|
+
for (let i = 0; i + 4 <= extra.length; ) {
|
|
287
|
+
const id = extra.readUInt16LE(i);
|
|
288
|
+
const len = extra.readUInt16LE(i + 2);
|
|
289
|
+
const recEnd = i + 4 + len;
|
|
290
|
+
if (recEnd > extra.length) break;
|
|
291
|
+
if (id !== HDR_ID.ZIP64) {
|
|
292
|
+
parts.push(Buffer.from(extra.subarray(i, recEnd)));
|
|
293
|
+
}
|
|
294
|
+
i = recEnd;
|
|
295
|
+
}
|
|
296
|
+
if (parts.length === 0) return null;
|
|
297
|
+
return Buffer.concat(parts);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Data descriptor length including optional signature.
|
|
302
|
+
* Zip64: signature(4) + crc(4) + cmp(8) + uncmp(8) = 24
|
|
303
|
+
* Classic: signature(4) + crc(4) + cmp(4) + uncmp(4) = 16
|
|
304
|
+
*/
|
|
305
|
+
export function dataDescriptorByteLength(useZip64Sizes: boolean): number {
|
|
306
|
+
return useZip64Sizes ? 24 : 16;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function buildZip64Eocd(fields: Zip64EocdFields): Buffer {
|
|
310
|
+
const buf = Buffer.alloc(ZIP64_EOCD_RECORD_SIZE);
|
|
311
|
+
buf.writeUInt32LE(ZIP64_CENTRAL_DIR.SIGNATURE, 0);
|
|
312
|
+
writeU64(buf, ZIP64_CENTRAL_DIR.SIZE_FIELD, ZIP64_EOCD_SIZE_FIELD);
|
|
313
|
+
buf.writeUInt16LE(fields.versionMadeBy ?? 45, ZIP64_CENTRAL_DIR.VER_MADEBY);
|
|
314
|
+
buf.writeUInt16LE(
|
|
315
|
+
fields.versionNeeded ?? ZIP64_VERSION_NEEDED,
|
|
316
|
+
ZIP64_CENTRAL_DIR.VER_NEEDED
|
|
317
|
+
);
|
|
318
|
+
buf.writeUInt32LE(fields.diskNumber ?? 0, ZIP64_CENTRAL_DIR.VOL_NUM);
|
|
319
|
+
buf.writeUInt32LE(fields.diskWithCdStart ?? 0, ZIP64_CENTRAL_DIR.VOLDIR_START);
|
|
320
|
+
writeU64(buf, ZIP64_CENTRAL_DIR.VOL_ENTRIES, fields.totalEntries);
|
|
321
|
+
writeU64(buf, ZIP64_CENTRAL_DIR.TOTAL_ENTRIES, fields.totalEntries);
|
|
322
|
+
writeU64(buf, ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE, fields.centralDirSize);
|
|
323
|
+
writeU64(buf, ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET, fields.centralDirOffset);
|
|
324
|
+
return buf;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function buildZip64Locator(zip64EocdOffset: number): Buffer {
|
|
328
|
+
const buf = Buffer.alloc(ZIP64_LOCATOR_SIZE);
|
|
329
|
+
buf.writeUInt32LE(ZIP64_CENTRAL_END.SIGNATURE, 0);
|
|
330
|
+
buf.writeUInt32LE(0, ZIP64_CENTRAL_END.VOL_NUM);
|
|
331
|
+
writeU64(buf, ZIP64_CENTRAL_END.CENTRAL_DIR_OFFSET, zip64EocdOffset);
|
|
332
|
+
buf.writeUInt32LE(1, ZIP64_CENTRAL_END.TOTAL_DISKS);
|
|
333
|
+
return buf;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function buildClassicEocd(opts: {
|
|
337
|
+
totalEntries: number;
|
|
338
|
+
centralDirSize: number;
|
|
339
|
+
centralDirOffset: number;
|
|
340
|
+
archiveComment?: string;
|
|
341
|
+
/** When true, write 0xFFFF / 0xFFFFFFFF for any field that overflows classic width. */
|
|
342
|
+
useZip64Sentinels: boolean;
|
|
343
|
+
/**
|
|
344
|
+
* Force classic CD offset to 0xFFFFFFFF so readers consult Zip64 EOCD
|
|
345
|
+
* even when the real offset fits in u32 (Info-ZIP `-fz`).
|
|
346
|
+
*/
|
|
347
|
+
forceCdOffsetSentinel?: boolean;
|
|
348
|
+
}): Buffer {
|
|
349
|
+
const comment = opts.archiveComment || '';
|
|
350
|
+
const commentBytes = Buffer.from(comment, 'utf8');
|
|
351
|
+
const commentLength = Math.min(commentBytes.length, ZIP64_U16);
|
|
352
|
+
const buf = Buffer.alloc(CLASSIC_EOCD_SIZE + commentLength);
|
|
353
|
+
buf.writeUInt32LE(CENTRAL_END.SIGNATURE, 0);
|
|
354
|
+
buf.writeUInt16LE(0, CENTRAL_END.VOL_NUM);
|
|
355
|
+
buf.writeUInt16LE(0, CENTRAL_END.VOLDIR_START);
|
|
356
|
+
|
|
357
|
+
const entries =
|
|
358
|
+
opts.useZip64Sentinels && exceedsU16(opts.totalEntries)
|
|
359
|
+
? ZIP64_U16
|
|
360
|
+
: opts.totalEntries;
|
|
361
|
+
const cdSize =
|
|
362
|
+
opts.useZip64Sentinels && exceedsU32(opts.centralDirSize)
|
|
363
|
+
? ZIP64_U32
|
|
364
|
+
: opts.centralDirSize;
|
|
365
|
+
const cdOffset =
|
|
366
|
+
opts.forceCdOffsetSentinel ||
|
|
367
|
+
(opts.useZip64Sentinels && exceedsU32(opts.centralDirOffset))
|
|
368
|
+
? ZIP64_U32
|
|
369
|
+
: opts.centralDirOffset;
|
|
370
|
+
|
|
371
|
+
buf.writeUInt16LE(entries, CENTRAL_END.VOL_ENTRIES);
|
|
372
|
+
buf.writeUInt16LE(entries, CENTRAL_END.TOTAL_ENTRIES);
|
|
373
|
+
buf.writeUInt32LE(cdSize, CENTRAL_END.CENTRAL_DIR_SIZE);
|
|
374
|
+
buf.writeUInt32LE(cdOffset, CENTRAL_END.CENTRAL_DIR_OFFSET);
|
|
375
|
+
buf.writeUInt16LE(commentLength, CENTRAL_END.ZIP_COMMENT_LEN);
|
|
376
|
+
if (commentLength > 0) {
|
|
377
|
+
commentBytes.copy(buf, CLASSIC_EOCD_SIZE, 0, commentLength);
|
|
378
|
+
}
|
|
379
|
+
return buf;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Build Zip64 EOCD + Locator + classic EOCD (or classic EOCD alone).
|
|
384
|
+
* Callers write the returned buffer at `zip64EocdOffset` (or CD end when no Zip64).
|
|
385
|
+
*/
|
|
386
|
+
export function buildEndRecords(opts: BuildEndRecordsOptions): Buffer {
|
|
387
|
+
const allowSizeOffset = opts.allowSizeOffsetZip64 !== false;
|
|
388
|
+
const forceZip64 = opts.forceZip64 === true;
|
|
389
|
+
const needArchive =
|
|
390
|
+
forceZip64 ||
|
|
391
|
+
needsZip64Archive(
|
|
392
|
+
opts.totalEntries,
|
|
393
|
+
opts.centralDirSize,
|
|
394
|
+
opts.centralDirOffset
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
if (!needArchive) {
|
|
398
|
+
return buildClassicEocd({
|
|
399
|
+
totalEntries: opts.totalEntries,
|
|
400
|
+
centralDirSize: opts.centralDirSize,
|
|
401
|
+
centralDirOffset: opts.centralDirOffset,
|
|
402
|
+
archiveComment: opts.archiveComment,
|
|
403
|
+
useZip64Sentinels: false,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const sizeOffsetZip64 =
|
|
408
|
+
exceedsU32(opts.centralDirSize) || exceedsU32(opts.centralDirOffset);
|
|
409
|
+
// forceZip64 with fitting size/offset is allowed on buffer path (no multi-GiB materialization).
|
|
410
|
+
if (sizeOffsetZip64 && !allowSizeOffset) {
|
|
411
|
+
throw new Error(ZIP64_BUFFER_SIZE_OFFSET_ERROR);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const zip64Eocd = buildZip64Eocd({
|
|
415
|
+
totalEntries: opts.totalEntries,
|
|
416
|
+
centralDirSize: opts.centralDirSize,
|
|
417
|
+
centralDirOffset: opts.centralDirOffset,
|
|
418
|
+
versionMadeBy: opts.versionMadeBy ?? 45,
|
|
419
|
+
});
|
|
420
|
+
const locator = buildZip64Locator(opts.zip64EocdOffset);
|
|
421
|
+
const classic = buildClassicEocd({
|
|
422
|
+
totalEntries: opts.totalEntries,
|
|
423
|
+
centralDirSize: opts.centralDirSize,
|
|
424
|
+
centralDirOffset: opts.centralDirOffset,
|
|
425
|
+
archiveComment: opts.archiveComment,
|
|
426
|
+
useZip64Sentinels: true,
|
|
427
|
+
forceCdOffsetSentinel: forceZip64,
|
|
428
|
+
});
|
|
429
|
+
return Buffer.concat([zip64Eocd, locator, classic]);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Parse Zip64 EOCD fixed fields into CD size/offset/entry count. */
|
|
433
|
+
export function parseZip64Eocd(buf: Buffer): {
|
|
434
|
+
totalEntries: number;
|
|
435
|
+
centralDirSize: number;
|
|
436
|
+
centralDirOffset: number;
|
|
437
|
+
} {
|
|
438
|
+
if (buf.length < ZIP64_EOCD_RECORD_SIZE) {
|
|
439
|
+
throw new Error('Zip64 EOCD truncated');
|
|
440
|
+
}
|
|
441
|
+
if (buf.readUInt32LE(0) !== ZIP64_CENTRAL_DIR.SIGNATURE) {
|
|
442
|
+
throw new Error('Invalid Zip64 EOCD signature');
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
totalEntries: readU64AsNumber(buf, ZIP64_CENTRAL_DIR.TOTAL_ENTRIES),
|
|
446
|
+
centralDirSize: readU64AsNumber(buf, ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE),
|
|
447
|
+
centralDirOffset: readU64AsNumber(buf, ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function parseZip64Locator(buf: Buffer): number {
|
|
452
|
+
if (buf.length < ZIP64_LOCATOR_SIZE) {
|
|
453
|
+
throw new Error('Zip64 locator truncated');
|
|
454
|
+
}
|
|
455
|
+
if (buf.readUInt32LE(0) !== ZIP64_CENTRAL_END.SIGNATURE) {
|
|
456
|
+
throw new Error('Invalid Zip64 EOCD locator signature');
|
|
457
|
+
}
|
|
458
|
+
return readU64AsNumber(buf, ZIP64_CENTRAL_END.CENTRAL_DIR_OFFSET);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Assert buffer/browser writers may proceed for the given entry sizes/offsets.
|
|
463
|
+
* Throws {@link ZIP64_BUFFER_SIZE_OFFSET_ERROR} when size/offset Zip64 would be required.
|
|
464
|
+
*/
|
|
465
|
+
export function assertBufferAllowsEntryZip64(
|
|
466
|
+
uncompressedSize: number,
|
|
467
|
+
compressedSize: number,
|
|
468
|
+
localHdrOffset: number
|
|
469
|
+
): void {
|
|
470
|
+
if (needsZip64SizeOrOffset(uncompressedSize, compressedSize, localHdrOffset)) {
|
|
471
|
+
throw new Error(ZIP64_BUFFER_SIZE_OFFSET_ERROR);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
@@ -119,12 +119,10 @@ export class ZipCompressNode {
|
|
|
119
119
|
|
|
120
120
|
if (hashCalculator) {
|
|
121
121
|
hashCalculator.update(data);
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
127
|
-
}
|
|
122
|
+
hashCalculator.applyToEntry(entry, {
|
|
123
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
124
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
125
|
+
});
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
// Compress based on method
|
|
@@ -148,10 +146,11 @@ export class ZipCompressNode {
|
|
|
148
146
|
const enc = options.encryptionMethod;
|
|
149
147
|
if (enc === 'zipcrypto') {
|
|
150
148
|
buffer = this.encryptCompressedData(buffer, entry, options.password);
|
|
151
|
-
} else if (enc === '
|
|
152
|
-
buffer = this.encryptCompressedDataNeo(buffer, entry, options.password);
|
|
153
|
-
} else {
|
|
149
|
+
} else if (enc === 'aes256') {
|
|
154
150
|
buffer = this.encryptCompressedDataAes(buffer, entry, options.password);
|
|
151
|
+
} else {
|
|
152
|
+
// Default / 'neo-aes256': NeoEncrypt (0x024E)
|
|
153
|
+
buffer = this.encryptCompressedDataNeo(buffer, entry, options.password);
|
|
155
154
|
}
|
|
156
155
|
}
|
|
157
156
|
|
|
@@ -187,12 +186,10 @@ export class ZipCompressNode {
|
|
|
187
186
|
// For buffer, calculate hashes if needed
|
|
188
187
|
if (hashCalculator && entry) {
|
|
189
188
|
hashCalculator.update(data);
|
|
190
|
-
|
|
191
|
-
entry.crc
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
195
|
-
}
|
|
189
|
+
hashCalculator.applyToEntry(entry, {
|
|
190
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
191
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
192
|
+
});
|
|
196
193
|
}
|
|
197
194
|
return data;
|
|
198
195
|
} else {
|
|
@@ -219,12 +216,10 @@ export class ZipCompressNode {
|
|
|
219
216
|
|
|
220
217
|
// Finalize hashes
|
|
221
218
|
if (hashCalculator && entry) {
|
|
222
|
-
|
|
223
|
-
entry.crc
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
227
|
-
}
|
|
219
|
+
hashCalculator.applyToEntry(entry, {
|
|
220
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
221
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
222
|
+
});
|
|
228
223
|
}
|
|
229
224
|
|
|
230
225
|
return Buffer.alloc(0);
|
|
@@ -245,10 +240,10 @@ export class ZipCompressNode {
|
|
|
245
240
|
if (hashCalculator && entry) {
|
|
246
241
|
hashCalculator.update(data);
|
|
247
242
|
// Always set CRC if hash calculator was used (it calculated the correct CRC)
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
hashCalculator.applyToEntry(entry, {
|
|
244
|
+
setCrc: true,
|
|
245
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
246
|
+
});
|
|
252
247
|
}
|
|
253
248
|
|
|
254
249
|
const result = pako.deflateRaw(data, { level: level ?? 6 });
|
|
@@ -298,10 +293,10 @@ export class ZipCompressNode {
|
|
|
298
293
|
// Finalize hashes after all chunks processed and compressed
|
|
299
294
|
if (hashCalculator && entry) {
|
|
300
295
|
// Always set CRC if hash calculator was used (it calculated the correct CRC)
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
296
|
+
hashCalculator.applyToEntry(entry, {
|
|
297
|
+
setCrc: true,
|
|
298
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
299
|
+
});
|
|
305
300
|
}
|
|
306
301
|
|
|
307
302
|
// For chunked processing, return empty buffer (data already written via onOutputBuffer)
|
|
@@ -375,12 +370,10 @@ export class ZipCompressNode {
|
|
|
375
370
|
if (isBuffer) {
|
|
376
371
|
if (hashCalculator && entry) {
|
|
377
372
|
hashCalculator.update(input);
|
|
378
|
-
|
|
379
|
-
entry.crc
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
383
|
-
}
|
|
373
|
+
hashCalculator.applyToEntry(entry, {
|
|
374
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
375
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
376
|
+
});
|
|
384
377
|
}
|
|
385
378
|
return input;
|
|
386
379
|
}
|
|
@@ -405,12 +398,10 @@ export class ZipCompressNode {
|
|
|
405
398
|
position += chunk.length;
|
|
406
399
|
}
|
|
407
400
|
if (hashCalculator && entry) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
entry.sha256 = hashCalculator.finalizeSHA256();
|
|
413
|
-
}
|
|
401
|
+
hashCalculator.applyToEntry(entry, {
|
|
402
|
+
setCrc: !entry.crc || entry.crc === 0,
|
|
403
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
404
|
+
});
|
|
414
405
|
}
|
|
415
406
|
return onOutputBuffer ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
416
407
|
}
|
|
@@ -430,10 +421,10 @@ export class ZipCompressNode {
|
|
|
430
421
|
|
|
431
422
|
if (isBuffer && hashCalculator && entry) {
|
|
432
423
|
hashCalculator.update(input);
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
entry.sha256
|
|
436
|
-
}
|
|
424
|
+
hashCalculator.applyToEntry(entry, {
|
|
425
|
+
setCrc: true,
|
|
426
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
427
|
+
});
|
|
437
428
|
}
|
|
438
429
|
|
|
439
430
|
const collected: Buffer[] = [];
|
|
@@ -457,10 +448,10 @@ export class ZipCompressNode {
|
|
|
457
448
|
);
|
|
458
449
|
|
|
459
450
|
if (!isBuffer && hashCalculator && entry) {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
entry.sha256
|
|
463
|
-
}
|
|
451
|
+
hashCalculator.applyToEntry(entry, {
|
|
452
|
+
setCrc: true,
|
|
453
|
+
setSha256: !!(options?.useSHA256 && !entry.sha256),
|
|
454
|
+
});
|
|
464
455
|
}
|
|
465
456
|
if (entry) {
|
|
466
457
|
entry.compressedSize = compressedSize;
|