neozipkit 1.0.5 → 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 +116 -18
- package/README.md +38 -8
- package/WHATS_NEW.md +111 -0
- 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 +6178 -1261
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +6178 -1261
- 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.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/ZipEntry.d.ts +18 -0
- package/dist/core/ZipEntry.d.ts.map +1 -1
- package/dist/core/ZipEntry.js +139 -13
- package/dist/core/ZipEntry.js.map +1 -1
- package/dist/core/Zipkit.d.ts.map +1 -1
- package/dist/core/Zipkit.js +26 -19
- package/dist/core/Zipkit.js.map +1 -1
- package/dist/core/constants/Headers.d.ts +1 -0
- package/dist/core/constants/Headers.d.ts.map +1 -1
- package/dist/core/constants/Headers.js +1 -0
- package/dist/core/constants/Headers.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -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/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/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 +8 -6
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +14 -1
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +91 -53
- 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 +11 -5
- package/src/browser/zstd-browser-shim.ts +110 -28
- package/src/core/ZipCompress.ts +6 -0
- package/src/core/ZipCopy.ts +56 -27
- package/src/core/ZipEntry.ts +192 -13
- package/src/core/Zipkit.ts +40 -22
- package/src/core/constants/Headers.ts +1 -0
- package/src/core/index.ts +3 -0
- package/src/core/inflateZipPayload.ts +27 -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/ZipCopyNode.ts +31 -29
- package/src/node/ZipDecompressNode.ts +6 -5
- package/src/node/ZipkitNode.ts +121 -68
- 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
|
@@ -1,63 +1,143 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser
|
|
3
|
-
*
|
|
2
|
+
* Browser zstd codec for in-memory buffers.
|
|
3
|
+
* Uses CompressionStream / DecompressionStream ('zstd') so the old WASM codec stays out.
|
|
4
|
+
* Node builds use src/node/ZstdNode.ts (native zlib) instead of this file.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
'ZSTD requires Node.js with native zlib support (Node.js >= 22.15 or >= 23.8). ' +
|
|
8
|
-
'Browser builds do not include a zstd codec; use Deflate or process zstd archives in Node.';
|
|
7
|
+
import { Buffer } from 'buffer';
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
/** Bytes accepted by the buffer zstd codec. */
|
|
10
|
+
export type ZstdInput = Buffer | Uint8Array | ArrayBuffer;
|
|
11
|
+
|
|
12
|
+
const ZSTD_UNAVAILABLE =
|
|
13
|
+
'This browser does not expose CompressionStream zstd. ' +
|
|
14
|
+
'Use a current Chromium or Safari, or inflate the archive in Node.';
|
|
15
|
+
|
|
16
|
+
function asBytes(data: ZstdInput): Uint8Array {
|
|
17
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
18
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toBuffer(bytes: Uint8Array): Buffer {
|
|
22
|
+
return Buffer.from(bytes);
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
export function isNativeZstdAvailable(): boolean {
|
|
15
|
-
|
|
26
|
+
try {
|
|
27
|
+
if (typeof CompressionStream === 'undefined' || typeof DecompressionStream === 'undefined') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
// 'zstd' is not in the older CompressionFormat union.
|
|
31
|
+
new CompressionStream('zstd' as CompressionFormat);
|
|
32
|
+
new DecompressionStream('zstd' as CompressionFormat);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function mapCompressionLevel(level: number): number {
|
|
40
|
+
return Math.min(Math.max(1, Math.floor(level * 2.1)), 19);
|
|
16
41
|
}
|
|
17
42
|
|
|
18
|
-
|
|
19
|
-
|
|
43
|
+
function assertZstd(): void {
|
|
44
|
+
if (!isNativeZstdAvailable()) {
|
|
45
|
+
throw new Error(ZSTD_UNAVAILABLE);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function transformZstd(data: Uint8Array, mode: 'compress' | 'decompress'): Promise<Uint8Array> {
|
|
50
|
+
assertZstd();
|
|
51
|
+
const Stream = mode === 'compress' ? CompressionStream : DecompressionStream;
|
|
52
|
+
const stream = new Stream('zstd' as CompressionFormat);
|
|
53
|
+
const copy = new Uint8Array(data.byteLength);
|
|
54
|
+
copy.set(data);
|
|
55
|
+
const writer = stream.writable.getWriter();
|
|
56
|
+
await writer.write(copy);
|
|
57
|
+
await writer.close();
|
|
58
|
+
const reader = stream.readable.getReader();
|
|
59
|
+
const chunks: Uint8Array[] = [];
|
|
60
|
+
let total = 0;
|
|
61
|
+
for (;;) {
|
|
62
|
+
const { done, value } = await reader.read();
|
|
63
|
+
if (done) break;
|
|
64
|
+
if (value) {
|
|
65
|
+
chunks.push(value);
|
|
66
|
+
total += value.byteLength;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const out = new Uint8Array(total);
|
|
70
|
+
let offset = 0;
|
|
71
|
+
for (const chunk of chunks) {
|
|
72
|
+
out.set(chunk, offset);
|
|
73
|
+
offset += chunk.byteLength;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
20
76
|
}
|
|
21
77
|
|
|
22
78
|
export function createCompressTransform(_level?: number): never {
|
|
23
|
-
|
|
79
|
+
throw new Error('Browser zstd uses buffer compress(); Node zlib transforms are not available.');
|
|
24
80
|
}
|
|
25
81
|
|
|
26
82
|
export function createDecompressTransform(): never {
|
|
27
|
-
|
|
83
|
+
throw new Error('Browser zstd uses buffer decompress(); Node zlib transforms are not available.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function compress(data: ZstdInput, _level?: number): Promise<Buffer> {
|
|
87
|
+
return toBuffer(await transformZstd(asBytes(data), 'compress'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function decompress(data: ZstdInput): Promise<Buffer> {
|
|
91
|
+
return toBuffer(await transformZstd(asBytes(data), 'decompress'));
|
|
28
92
|
}
|
|
29
93
|
|
|
30
|
-
export
|
|
31
|
-
|
|
94
|
+
export function compressSync(_data: ZstdInput, _level?: number): Buffer {
|
|
95
|
+
throw new Error('Browser zstd compress is async. Use compress().');
|
|
32
96
|
}
|
|
33
97
|
|
|
34
|
-
export
|
|
35
|
-
return
|
|
98
|
+
export function compressSyncAtLevel(data: ZstdInput, _zstdLevel?: number): Buffer {
|
|
99
|
+
return compressSync(data);
|
|
36
100
|
}
|
|
37
101
|
|
|
38
|
-
export function
|
|
39
|
-
|
|
102
|
+
export function decompressSync(_data: ZstdInput): Buffer {
|
|
103
|
+
throw new Error('Browser zstd decompress is async. Use decompress().');
|
|
40
104
|
}
|
|
41
105
|
|
|
42
|
-
export function
|
|
43
|
-
return
|
|
106
|
+
export function asZstdBuffer(data: ZstdInput): Buffer {
|
|
107
|
+
return toBuffer(asBytes(data));
|
|
44
108
|
}
|
|
45
109
|
|
|
46
110
|
export async function compressChunks(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
111
|
+
readChunk: (position: number, size: number) => Buffer,
|
|
112
|
+
totalSize: number,
|
|
113
|
+
chunkSize: number,
|
|
114
|
+
level: number,
|
|
51
115
|
_onPlainChunk?: (chunk: Buffer) => void,
|
|
52
|
-
|
|
116
|
+
onCompressedChunk?: (chunk: Buffer) => void | Promise<void>
|
|
53
117
|
): Promise<number> {
|
|
54
|
-
|
|
118
|
+
const parts: Buffer[] = [];
|
|
119
|
+
let position = 0;
|
|
120
|
+
while (position < totalSize) {
|
|
121
|
+
const size = Math.min(chunkSize, totalSize - position);
|
|
122
|
+
const chunk = readChunk(position, size);
|
|
123
|
+
parts.push(Buffer.from(chunk));
|
|
124
|
+
position += chunk.length;
|
|
125
|
+
}
|
|
126
|
+
const compressed = await compress(Buffer.concat(parts), level);
|
|
127
|
+
if (onCompressedChunk) {
|
|
128
|
+
await onCompressedChunk(compressed);
|
|
129
|
+
}
|
|
130
|
+
return compressed.length;
|
|
55
131
|
}
|
|
56
132
|
|
|
57
133
|
export async function* decompressStream(
|
|
58
|
-
|
|
134
|
+
compressedStream: AsyncIterable<Buffer>
|
|
59
135
|
): AsyncGenerator<Buffer> {
|
|
60
|
-
|
|
136
|
+
const parts: Buffer[] = [];
|
|
137
|
+
for await (const chunk of compressedStream) {
|
|
138
|
+
parts.push(Buffer.from(chunk));
|
|
139
|
+
}
|
|
140
|
+
yield await decompress(Buffer.concat(parts));
|
|
61
141
|
}
|
|
62
142
|
|
|
63
143
|
export const ZstdNode = {
|
|
@@ -68,7 +148,9 @@ export const ZstdNode = {
|
|
|
68
148
|
compress,
|
|
69
149
|
decompress,
|
|
70
150
|
compressSync,
|
|
151
|
+
compressSyncAtLevel,
|
|
71
152
|
decompressSync,
|
|
153
|
+
asZstdBuffer,
|
|
72
154
|
compressChunks,
|
|
73
155
|
decompressStream,
|
|
74
156
|
};
|
package/src/core/ZipCompress.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface CompressOptions {
|
|
|
35
35
|
useSHA256?: boolean; // Whether to calculate SHA256 hash default is false
|
|
36
36
|
useZstd?: boolean; // Use Zstandard (method 93); default false (Deflate). Node ≥ 22.15 required when true.
|
|
37
37
|
bufferSize?: number; // Override default buffer size
|
|
38
|
+
/**
|
|
39
|
+
* Force APPNOTE Zip64 Version 1 even when sizes/offsets/counts fit classic fields.
|
|
40
|
+
* Useful for testing Zip64 read/list/integrity without multi-GiB payloads.
|
|
41
|
+
* Node streaming writers emit Zip64 EOCD + locator and per-member Extra `0x0001`.
|
|
42
|
+
*/
|
|
43
|
+
forceZip64?: boolean;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
package/src/core/ZipCopy.ts
CHANGED
|
@@ -10,7 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import ZipEntry from './ZipEntry';
|
|
12
12
|
import Zipkit from './Zipkit';
|
|
13
|
-
import { LOCAL_HDR,
|
|
13
|
+
import { LOCAL_HDR, GP_FLAG } from './constants/Headers';
|
|
14
|
+
import {
|
|
15
|
+
assertBufferAllowsEntryZip64,
|
|
16
|
+
buildEndRecords,
|
|
17
|
+
dataDescriptorByteLength,
|
|
18
|
+
needsZip64SizeOrOffset,
|
|
19
|
+
ZIP64_BUFFER_SIZE_OFFSET_ERROR,
|
|
20
|
+
} from './zip64/Zip64';
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* Options for copying ZIP files
|
|
@@ -174,9 +181,19 @@ export class ZipCopy {
|
|
|
174
181
|
const localHeaderSize = this.calculateLocalHeaderSize(sourceBuffer, entry);
|
|
175
182
|
|
|
176
183
|
// Determine total entry size
|
|
177
|
-
// For data descriptor entries, add
|
|
184
|
+
// For data descriptor entries, add Zip64 (24) or classic (16) descriptor bytes
|
|
178
185
|
const hasDataDescriptor = (entry.bitFlags & GP_FLAG.DATA_DESC) !== 0;
|
|
179
|
-
const
|
|
186
|
+
const descLen = hasDataDescriptor
|
|
187
|
+
? dataDescriptorByteLength(entry.usesZip64Extra)
|
|
188
|
+
: 0;
|
|
189
|
+
const totalEntrySize = localHeaderSize + entry.compressedSize + descLen;
|
|
190
|
+
|
|
191
|
+
// Buffer copy must remain memory-feasible
|
|
192
|
+
assertBufferAllowsEntryZip64(
|
|
193
|
+
entry.uncompressedSize,
|
|
194
|
+
entry.compressedSize,
|
|
195
|
+
entry.localHdrOffset
|
|
196
|
+
);
|
|
180
197
|
|
|
181
198
|
// Verify we have enough data in the buffer
|
|
182
199
|
if (entry.localHdrOffset + totalEntrySize > sourceBuffer.length) {
|
|
@@ -264,6 +281,17 @@ export class ZipCopy {
|
|
|
264
281
|
cloned.originalEntry = entry.originalEntry;
|
|
265
282
|
cloned.inode = entry.inode;
|
|
266
283
|
|
|
284
|
+
cloned.usesZip64Extra = entry.usesZip64Extra;
|
|
285
|
+
if (entry.additionalExtra) {
|
|
286
|
+
cloned.additionalExtra = Buffer.from(entry.additionalExtra);
|
|
287
|
+
}
|
|
288
|
+
cloned.aesVersion = entry.aesVersion;
|
|
289
|
+
cloned.aesStrength = entry.aesStrength;
|
|
290
|
+
cloned.realCmpMethod = entry.realCmpMethod;
|
|
291
|
+
cloned.neoCryptoPayloadVersion = entry.neoCryptoPayloadVersion;
|
|
292
|
+
cloned.neoCryptoAlgorithm = entry.neoCryptoAlgorithm;
|
|
293
|
+
cloned.neoCryptoFlags = entry.neoCryptoFlags;
|
|
294
|
+
|
|
267
295
|
return cloned;
|
|
268
296
|
}
|
|
269
297
|
|
|
@@ -276,6 +304,19 @@ export class ZipCopy {
|
|
|
276
304
|
zipComment: string,
|
|
277
305
|
centralDirOffset: number
|
|
278
306
|
): Buffer {
|
|
307
|
+
if (
|
|
308
|
+
needsZip64SizeOrOffset(0, 0, 0, 0, centralDirOffset) ||
|
|
309
|
+
entries.some((e) =>
|
|
310
|
+
needsZip64SizeOrOffset(
|
|
311
|
+
e.uncompressedSize,
|
|
312
|
+
e.compressedSize,
|
|
313
|
+
e.localHdrOffset
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
) {
|
|
317
|
+
throw new Error(ZIP64_BUFFER_SIZE_OFFSET_ERROR);
|
|
318
|
+
}
|
|
319
|
+
|
|
279
320
|
const centralDirChunks: Buffer[] = [];
|
|
280
321
|
for (const entry of entries) {
|
|
281
322
|
centralDirChunks.push(entry.centralDirEntry());
|
|
@@ -283,32 +324,20 @@ export class ZipCopy {
|
|
|
283
324
|
const centralDirBuffer = Buffer.concat(centralDirChunks);
|
|
284
325
|
const centralDirSize = centralDirBuffer.length;
|
|
285
326
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const eocdBuffer = Buffer.alloc(22 + commentLength);
|
|
290
|
-
let pos = 0;
|
|
291
|
-
eocdBuffer.writeUInt32LE(CENTRAL_END.SIGNATURE, pos);
|
|
292
|
-
pos += 4;
|
|
293
|
-
eocdBuffer.writeUInt16LE(0, pos);
|
|
294
|
-
pos += 2;
|
|
295
|
-
eocdBuffer.writeUInt16LE(0, pos);
|
|
296
|
-
pos += 2;
|
|
297
|
-
eocdBuffer.writeUInt16LE(entries.length, pos);
|
|
298
|
-
pos += 2;
|
|
299
|
-
eocdBuffer.writeUInt16LE(entries.length, pos);
|
|
300
|
-
pos += 2;
|
|
301
|
-
eocdBuffer.writeUInt32LE(centralDirSize, pos);
|
|
302
|
-
pos += 4;
|
|
303
|
-
eocdBuffer.writeUInt32LE(centralDirOffset, pos);
|
|
304
|
-
pos += 4;
|
|
305
|
-
eocdBuffer.writeUInt16LE(commentLength, pos);
|
|
306
|
-
pos += 2;
|
|
307
|
-
if (commentLength > 0) {
|
|
308
|
-
commentBytes.copy(eocdBuffer, pos, 0, commentLength);
|
|
327
|
+
if (needsZip64SizeOrOffset(0, 0, 0, centralDirSize, centralDirOffset)) {
|
|
328
|
+
throw new Error(ZIP64_BUFFER_SIZE_OFFSET_ERROR);
|
|
309
329
|
}
|
|
310
330
|
|
|
311
|
-
|
|
331
|
+
const endRecords = buildEndRecords({
|
|
332
|
+
totalEntries: entries.length,
|
|
333
|
+
centralDirSize,
|
|
334
|
+
centralDirOffset,
|
|
335
|
+
zip64EocdOffset: centralDirOffset + centralDirSize,
|
|
336
|
+
archiveComment: zipComment,
|
|
337
|
+
allowSizeOffsetZip64: false,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return Buffer.concat([centralDirBuffer, endRecords]);
|
|
312
341
|
}
|
|
313
342
|
|
|
314
343
|
/**
|
package/src/core/ZipEntry.ts
CHANGED
|
@@ -24,6 +24,17 @@ import {
|
|
|
24
24
|
import { ZipFileEntry, FileData } from '../types';
|
|
25
25
|
import { Logger } from './components/Logger';
|
|
26
26
|
import { crc32 } from './encryption/ZipCrypto';
|
|
27
|
+
import {
|
|
28
|
+
ZIP64_U16,
|
|
29
|
+
ZIP64_U32,
|
|
30
|
+
ZIP64_VERSION_NEEDED,
|
|
31
|
+
buildZip64Extra,
|
|
32
|
+
exceedsU16,
|
|
33
|
+
exceedsU32,
|
|
34
|
+
needsZip64Entry,
|
|
35
|
+
parseZip64Extra,
|
|
36
|
+
stripZip64FromExtra,
|
|
37
|
+
} from './zip64/Zip64';
|
|
27
38
|
|
|
28
39
|
const VER_ENCODING = 30;
|
|
29
40
|
const VER_EXTRACT = 10; // Version needed to extract (1.0)
|
|
@@ -49,6 +60,15 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
49
60
|
localHdrOffset: number = 0; // Relative offset to local header from File/Disk start
|
|
50
61
|
filename: string = ''; // File name
|
|
51
62
|
extraField: Buffer | null = null; // Extra field
|
|
63
|
+
/**
|
|
64
|
+
* Caller-supplied extra-field records (complete header id + size + payload).
|
|
65
|
+
* Written on the local header and the central-directory header beside 0x014E.
|
|
66
|
+
* Unknown ids parsed from an existing header (including ZipWiki 0x014F) are kept here
|
|
67
|
+
* so a later header rebuild does not drop them.
|
|
68
|
+
*/
|
|
69
|
+
additionalExtra: Buffer | null = null;
|
|
70
|
+
/** When false, local/central headers omit the Info-ZIP Unicode Path extra. */
|
|
71
|
+
emitUnicodePath = true;
|
|
52
72
|
comment: string | null = null; // Entry comment
|
|
53
73
|
|
|
54
74
|
// File Data
|
|
@@ -100,6 +120,14 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
100
120
|
originalEntry: string | null = null; // Original entry name for hard links
|
|
101
121
|
inode: number | null = null; // Inode number for hard links
|
|
102
122
|
|
|
123
|
+
/** True when Zip64 extra (0x0001) was applied or will be emitted (8-byte data descriptors). */
|
|
124
|
+
usesZip64Extra: boolean = false;
|
|
125
|
+
/**
|
|
126
|
+
* When true, emit Zip64 Extra Field `0x0001` and version-needed 45 even if
|
|
127
|
+
* sizes/offsets fit classic u16/u32 fields (APPNOTE MAY; for testing).
|
|
128
|
+
*/
|
|
129
|
+
forceZip64: boolean = false;
|
|
130
|
+
|
|
103
131
|
fileData?: FileData;
|
|
104
132
|
|
|
105
133
|
/**
|
|
@@ -306,6 +334,31 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
306
334
|
this.neoCryptoFlags = _data.readUInt16LE(7);
|
|
307
335
|
}
|
|
308
336
|
}
|
|
337
|
+
} else if (_id === HDR_ID.ZIP64) {
|
|
338
|
+
const z64 = parseZip64Extra(_data, {
|
|
339
|
+
uncompressedSize: this.uncompressedSize,
|
|
340
|
+
compressedSize: this.compressedSize,
|
|
341
|
+
localHdrOffset: this.localHdrOffset,
|
|
342
|
+
diskStart: this.volNumber,
|
|
343
|
+
});
|
|
344
|
+
this.usesZip64Extra = true;
|
|
345
|
+
if (z64.uncompressedSize !== undefined) {
|
|
346
|
+
this.uncompressedSize = z64.uncompressedSize;
|
|
347
|
+
}
|
|
348
|
+
if (z64.compressedSize !== undefined) {
|
|
349
|
+
this.compressedSize = z64.compressedSize;
|
|
350
|
+
}
|
|
351
|
+
if (z64.localHdrOffset !== undefined) {
|
|
352
|
+
this.localHdrOffset = z64.localHdrOffset;
|
|
353
|
+
}
|
|
354
|
+
if (z64.diskStart !== undefined) {
|
|
355
|
+
this.volNumber = z64.diskStart;
|
|
356
|
+
}
|
|
357
|
+
} else if (_id !== HDR_ID.UNICODE_PATH) {
|
|
358
|
+
const rec = Buffer.from(this.extraField.subarray(i, i + 4 + _len));
|
|
359
|
+
this.additionalExtra = this.additionalExtra
|
|
360
|
+
? Buffer.concat([this.additionalExtra, rec])
|
|
361
|
+
: rec;
|
|
309
362
|
}
|
|
310
363
|
// Skip Unicode Path here as we already processed it
|
|
311
364
|
i += 4 + _len;
|
|
@@ -330,10 +383,30 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
330
383
|
* @returns true if the filename contains non-ASCII characters or special characters
|
|
331
384
|
*/
|
|
332
385
|
needsUnicodeHandling(): boolean {
|
|
386
|
+
if (!this.emitUnicodePath) return false;
|
|
333
387
|
// Check if filename contains non-ASCII characters or special characters like apostrophes
|
|
334
388
|
return /[^\x00-\x7E]|['"]/.test(this.filename);
|
|
335
389
|
}
|
|
336
390
|
|
|
391
|
+
/** Version needed to extract. Method 93 (zstd) is 6.3; Zip64 is 4.5. */
|
|
392
|
+
private versionNeededToExtract(): number {
|
|
393
|
+
let ver = VER_EXTRACT;
|
|
394
|
+
if (this.cmpMethod === CMP_METHOD.AES_ENCRYPT) ver = VER_AES_EXTRACT;
|
|
395
|
+
else if (this.cmpMethod === CMP_METHOD.ZSTD) ver = 63;
|
|
396
|
+
if (
|
|
397
|
+
this.forceZip64 ||
|
|
398
|
+
needsZip64Entry(
|
|
399
|
+
this.uncompressedSize,
|
|
400
|
+
this.compressedSize,
|
|
401
|
+
this.localHdrOffset,
|
|
402
|
+
this.volNumber
|
|
403
|
+
)
|
|
404
|
+
) {
|
|
405
|
+
ver = Math.max(ver, ZIP64_VERSION_NEEDED);
|
|
406
|
+
}
|
|
407
|
+
return ver;
|
|
408
|
+
}
|
|
409
|
+
|
|
337
410
|
/**
|
|
338
411
|
* Adds UTF-8 Unicode Path field to a buffer
|
|
339
412
|
* @param buffer - The buffer to write to
|
|
@@ -389,13 +462,38 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
389
462
|
if (isNeoEncrypt) {
|
|
390
463
|
extraFieldLen += NEO_CRYPTO_EXTRA_FIELD_SIZE;
|
|
391
464
|
}
|
|
465
|
+
|
|
466
|
+
const sha256Local = this.sha256 ? Buffer.from(this.sha256, 'hex') : null;
|
|
467
|
+
const sha256LocalLen = sha256Local && sha256Local.length > 0 ? 4 + sha256Local.length : 0;
|
|
468
|
+
const callerExtra = stripZip64FromExtra(this.additionalExtra);
|
|
469
|
+
const callerExtraLen = callerExtra?.length ?? 0;
|
|
470
|
+
|
|
471
|
+
const useZip64Sizes =
|
|
472
|
+
this.forceZip64 ||
|
|
473
|
+
exceedsU32(this.uncompressedSize) ||
|
|
474
|
+
exceedsU32(this.compressedSize);
|
|
475
|
+
if (useZip64Sizes) {
|
|
476
|
+
this.usesZip64Extra = true;
|
|
477
|
+
}
|
|
478
|
+
const zip64LocalExtra = useZip64Sizes
|
|
479
|
+
? buildZip64Extra(
|
|
480
|
+
{
|
|
481
|
+
uncompressedSize: this.uncompressedSize,
|
|
482
|
+
compressedSize: this.compressedSize,
|
|
483
|
+
},
|
|
484
|
+
{ forLocal: true }
|
|
485
|
+
)
|
|
486
|
+
: null;
|
|
487
|
+
const zip64LocalLen = zip64LocalExtra?.length ?? 0;
|
|
488
|
+
|
|
489
|
+
extraFieldLen += sha256LocalLen + callerExtraLen + zip64LocalLen;
|
|
392
490
|
|
|
393
491
|
const data = Buffer.alloc(LOCAL_HDR.SIZE + this.filename.length + extraFieldLen);
|
|
394
492
|
|
|
395
493
|
// "PK\003\004"
|
|
396
494
|
data.writeUInt32LE(LOCAL_HDR.SIGNATURE, 0);
|
|
397
495
|
// version needed to extract (NeoEncrypt keeps standard ZIP method — use normal extract version)
|
|
398
|
-
const verExtract = this.
|
|
496
|
+
const verExtract = this.versionNeededToExtract();
|
|
399
497
|
data.writeUInt16LE(verExtract, LOCAL_HDR.VER_EXTRACT);
|
|
400
498
|
// general purpose bit flag
|
|
401
499
|
data.writeUInt16LE(this.bitFlags >>> 0, LOCAL_HDR.FLAGS);
|
|
@@ -405,10 +503,15 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
405
503
|
data.writeUInt32LE(this.timeDateDOS >>> 0, LOCAL_HDR.TIMEDATE_DOS);
|
|
406
504
|
// uncompressed file crc-32 value (AE-2 stores 0)
|
|
407
505
|
data.writeUInt32LE(this.aesVersion === 2 ? 0 : this.crc, LOCAL_HDR.CRC);
|
|
408
|
-
// compressed
|
|
409
|
-
data.writeUInt32LE(
|
|
410
|
-
|
|
411
|
-
|
|
506
|
+
// compressed / uncompressed sizes (Zip64 sentinels when needed)
|
|
507
|
+
data.writeUInt32LE(
|
|
508
|
+
useZip64Sizes ? ZIP64_U32 : this.compressedSize,
|
|
509
|
+
LOCAL_HDR.CMP_SIZE
|
|
510
|
+
);
|
|
511
|
+
data.writeUInt32LE(
|
|
512
|
+
useZip64Sizes ? ZIP64_U32 : this.uncompressedSize,
|
|
513
|
+
LOCAL_HDR.UNCMP_SIZE
|
|
514
|
+
);
|
|
412
515
|
// filename length
|
|
413
516
|
data.writeUInt16LE(this.filename.length, LOCAL_HDR.FNAME_LEN);
|
|
414
517
|
// extra field length
|
|
@@ -427,6 +530,12 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
427
530
|
extraOffset = this.addUnicodePathField(data, extraOffset);
|
|
428
531
|
}
|
|
429
532
|
|
|
533
|
+
// Zip64 sizes first among optional extras so Node can patch them by scanning
|
|
534
|
+
if (zip64LocalExtra) {
|
|
535
|
+
zip64LocalExtra.copy(data, extraOffset);
|
|
536
|
+
extraOffset += zip64LocalLen;
|
|
537
|
+
}
|
|
538
|
+
|
|
430
539
|
// Add AES extra field
|
|
431
540
|
if (isAes) {
|
|
432
541
|
data.writeUInt16LE(HDR_ID.AES, extraOffset);
|
|
@@ -452,6 +561,17 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
452
561
|
extraOffset += NEO_CRYPTO_EXTRA_FIELD_SIZE;
|
|
453
562
|
}
|
|
454
563
|
|
|
564
|
+
if (sha256Local && sha256LocalLen > 0) {
|
|
565
|
+
data.writeUInt16LE(HDR_ID.SHA256, extraOffset);
|
|
566
|
+
data.writeUInt16LE(sha256Local.length, extraOffset + 2);
|
|
567
|
+
sha256Local.copy(data, extraOffset + 4);
|
|
568
|
+
extraOffset += sha256LocalLen;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (callerExtra && callerExtraLen > 0) {
|
|
572
|
+
callerExtra.copy(data, extraOffset);
|
|
573
|
+
}
|
|
574
|
+
|
|
455
575
|
return data;
|
|
456
576
|
}
|
|
457
577
|
|
|
@@ -492,7 +612,45 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
492
612
|
const isNeoEncrypt = this.neoCryptoAlgorithm > 0;
|
|
493
613
|
const neoLen = isNeoEncrypt ? NEO_CRYPTO_EXTRA_FIELD_SIZE : 0;
|
|
494
614
|
|
|
495
|
-
const
|
|
615
|
+
const callerExtra = stripZip64FromExtra(this.additionalExtra);
|
|
616
|
+
const callerExtraLen = callerExtra?.length ?? 0;
|
|
617
|
+
|
|
618
|
+
const zip64Fields: {
|
|
619
|
+
uncompressedSize?: number;
|
|
620
|
+
compressedSize?: number;
|
|
621
|
+
localHdrOffset?: number;
|
|
622
|
+
diskStart?: number;
|
|
623
|
+
} = {};
|
|
624
|
+
if (this.forceZip64 || exceedsU32(this.uncompressedSize)) {
|
|
625
|
+
zip64Fields.uncompressedSize = this.uncompressedSize;
|
|
626
|
+
}
|
|
627
|
+
if (this.forceZip64 || exceedsU32(this.compressedSize)) {
|
|
628
|
+
zip64Fields.compressedSize = this.compressedSize;
|
|
629
|
+
}
|
|
630
|
+
if (this.forceZip64 || exceedsU32(this.localHdrOffset)) {
|
|
631
|
+
zip64Fields.localHdrOffset = this.localHdrOffset;
|
|
632
|
+
}
|
|
633
|
+
if (exceedsU16(this.volNumber)) {
|
|
634
|
+
zip64Fields.diskStart = this.volNumber;
|
|
635
|
+
}
|
|
636
|
+
const zip64CentralExtra =
|
|
637
|
+
Object.keys(zip64Fields).length > 0 ? buildZip64Extra(zip64Fields) : null;
|
|
638
|
+
if (zip64CentralExtra) {
|
|
639
|
+
this.usesZip64Extra = true;
|
|
640
|
+
}
|
|
641
|
+
const zip64CentralLen = zip64CentralExtra?.length ?? 0;
|
|
642
|
+
|
|
643
|
+
const extraLen =
|
|
644
|
+
utfLen +
|
|
645
|
+
sha256Len +
|
|
646
|
+
uidgidLen +
|
|
647
|
+
symlinkLen +
|
|
648
|
+
hardlinkLen +
|
|
649
|
+
(needsUnicode ? unicodePathLen : 0) +
|
|
650
|
+
aesLen +
|
|
651
|
+
neoLen +
|
|
652
|
+
callerExtraLen +
|
|
653
|
+
zip64CentralLen;
|
|
496
654
|
|
|
497
655
|
// Calculate actual filename length (ASCII conversion may change length)
|
|
498
656
|
const asciiName = this.filename.replace(/[^\x00-\x7E]/g, '?');
|
|
@@ -506,7 +664,7 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
506
664
|
// Version made by - Needs to be set for NeoZip
|
|
507
665
|
data.writeUInt16LE(this.isUpdated ? this.VER_MADE_BY : this.verMadeBy, CENTRAL_DIR.VER_MADE);
|
|
508
666
|
// Version needed to extract
|
|
509
|
-
const centralVerExtract = this.
|
|
667
|
+
const centralVerExtract = this.versionNeededToExtract();
|
|
510
668
|
data.writeInt16LE(this.isUpdated ? centralVerExtract : this.verMadeBy, CENTRAL_DIR.VER_EXT);
|
|
511
669
|
// Encrypt, Decrypt Flags
|
|
512
670
|
data.writeInt16LE(this.bitFlags >>> 0, CENTRAL_DIR.FLAGS);
|
|
@@ -516,10 +674,15 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
516
674
|
data.writeUInt32LE(this.timeDateDOS >>> 0, CENTRAL_DIR.TIMEDATE_DOS);
|
|
517
675
|
// Uncompressed file CRC-32 value (AE-2 stores 0)
|
|
518
676
|
data.writeUInt32LE(this.aesVersion === 2 ? 0 : this.crc, CENTRAL_DIR.CRC);
|
|
519
|
-
// Compressed
|
|
520
|
-
data.writeUInt32LE(
|
|
521
|
-
|
|
522
|
-
|
|
677
|
+
// Compressed / uncompressed sizes and local offset (Zip64 sentinels when needed)
|
|
678
|
+
data.writeUInt32LE(
|
|
679
|
+
zip64Fields.compressedSize !== undefined ? ZIP64_U32 : this.compressedSize,
|
|
680
|
+
CENTRAL_DIR.CMP_SIZE
|
|
681
|
+
);
|
|
682
|
+
data.writeUInt32LE(
|
|
683
|
+
zip64Fields.uncompressedSize !== undefined ? ZIP64_U32 : this.uncompressedSize,
|
|
684
|
+
CENTRAL_DIR.UNCMP_SIZE
|
|
685
|
+
);
|
|
523
686
|
// Filename Length
|
|
524
687
|
data.writeUInt16LE(this.filename.length, CENTRAL_DIR.FNAME_LEN);
|
|
525
688
|
// Extra Field Length
|
|
@@ -527,13 +690,19 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
527
690
|
// File Comment Length
|
|
528
691
|
data.writeUInt16LE(commentLen, CENTRAL_DIR.COMMENT_LEN);
|
|
529
692
|
// Volume Number Start
|
|
530
|
-
data.writeUInt16LE(
|
|
693
|
+
data.writeUInt16LE(
|
|
694
|
+
zip64Fields.diskStart !== undefined ? ZIP64_U16 : this.volNumber,
|
|
695
|
+
CENTRAL_DIR.DISK_NUM
|
|
696
|
+
);
|
|
531
697
|
// Internal File Attributes
|
|
532
698
|
data.writeUInt16LE(this.intFileAttr >>> 0, CENTRAL_DIR.INT_FILE_ATTR);
|
|
533
699
|
// External File Attributes
|
|
534
700
|
data.writeUInt32LE(this.extFileAttr >>> 0, CENTRAL_DIR.EXT_FILE_ATTR);
|
|
535
701
|
// Local Header Offset
|
|
536
|
-
data.writeUInt32LE(
|
|
702
|
+
data.writeUInt32LE(
|
|
703
|
+
zip64Fields.localHdrOffset !== undefined ? ZIP64_U32 : this.localHdrOffset,
|
|
704
|
+
CENTRAL_DIR.LOCAL_HDR_OFFSET
|
|
705
|
+
);
|
|
537
706
|
|
|
538
707
|
// Write filename - use ASCII filename (replacing non-ASCII with ?)
|
|
539
708
|
// This ensures compatibility with older ZIP readers
|
|
@@ -551,6 +720,12 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
551
720
|
// Add Extra Field data after file comment
|
|
552
721
|
let extraOffset = currentOffset;
|
|
553
722
|
|
|
723
|
+
// Zip64 extended information (sizes / offset / disk)
|
|
724
|
+
if (zip64CentralExtra) {
|
|
725
|
+
zip64CentralExtra.copy(data, extraOffset);
|
|
726
|
+
extraOffset += zip64CentralLen;
|
|
727
|
+
}
|
|
728
|
+
|
|
554
729
|
// Add Universal Time field
|
|
555
730
|
if (this.universalTime) {
|
|
556
731
|
data.writeUInt16LE(HDR_ID.UNV_TIME, extraOffset); // 0x5455
|
|
@@ -637,6 +812,10 @@ export default class ZipEntry implements ZipFileEntry {
|
|
|
637
812
|
extraOffset += NEO_CRYPTO_EXTRA_FIELD_SIZE;
|
|
638
813
|
}
|
|
639
814
|
|
|
815
|
+
if (callerExtra && callerExtraLen > 0) {
|
|
816
|
+
callerExtra.copy(data, extraOffset);
|
|
817
|
+
}
|
|
818
|
+
|
|
640
819
|
return data;
|
|
641
820
|
}
|
|
642
821
|
|