neozipkit 1.0.5 → 1.5.1

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.
Files changed (94) hide show
  1. package/NEOZIP_APPNOTE.md +116 -18
  2. package/README.md +38 -8
  3. package/WHATS_NEW.md +115 -0
  4. package/dist/browser/zstd-browser-shim.d.ts +17 -9
  5. package/dist/browser/zstd-browser-shim.d.ts.map +1 -1
  6. package/dist/browser/zstd-browser-shim.js +99 -21
  7. package/dist/browser/zstd-browser-shim.js.map +1 -1
  8. package/dist/browser-esm/index.js +6178 -1261
  9. package/dist/browser-esm/index.js.map +4 -4
  10. package/dist/browser-umd/index.js +6178 -1261
  11. package/dist/browser-umd/index.js.map +4 -4
  12. package/dist/browser-umd/index.min.js +6 -3
  13. package/dist/core/ZipCompress.d.ts +6 -0
  14. package/dist/core/ZipCompress.d.ts.map +1 -1
  15. package/dist/core/ZipCompress.js.map +1 -1
  16. package/dist/core/ZipCopy.d.ts.map +1 -1
  17. package/dist/core/ZipCopy.js +33 -25
  18. package/dist/core/ZipCopy.js.map +1 -1
  19. package/dist/core/ZipEntry.d.ts +18 -0
  20. package/dist/core/ZipEntry.d.ts.map +1 -1
  21. package/dist/core/ZipEntry.js +139 -13
  22. package/dist/core/ZipEntry.js.map +1 -1
  23. package/dist/core/Zipkit.d.ts.map +1 -1
  24. package/dist/core/Zipkit.js +26 -19
  25. package/dist/core/Zipkit.js.map +1 -1
  26. package/dist/core/constants/Headers.d.ts +1 -0
  27. package/dist/core/constants/Headers.d.ts.map +1 -1
  28. package/dist/core/constants/Headers.js +1 -0
  29. package/dist/core/constants/Headers.js.map +1 -1
  30. package/dist/core/index.d.ts +3 -0
  31. package/dist/core/index.d.ts.map +1 -1
  32. package/dist/core/index.js +5 -1
  33. package/dist/core/index.js.map +1 -1
  34. package/dist/core/inflateZipPayload.d.ts +6 -0
  35. package/dist/core/inflateZipPayload.d.ts.map +1 -0
  36. package/dist/core/inflateZipPayload.js +28 -0
  37. package/dist/core/inflateZipPayload.js.map +1 -0
  38. package/dist/core/origin/OriginExtra.d.ts +43 -0
  39. package/dist/core/origin/OriginExtra.d.ts.map +1 -0
  40. package/dist/core/origin/OriginExtra.js +187 -0
  41. package/dist/core/origin/OriginExtra.js.map +1 -0
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/zip64/Zip64.d.ts +138 -0
  44. package/dist/core/zip64/Zip64.d.ts.map +1 -0
  45. package/dist/core/zip64/Zip64.js +353 -0
  46. package/dist/core/zip64/Zip64.js.map +1 -0
  47. package/dist/node/ZipCopyNode.d.ts.map +1 -1
  48. package/dist/node/ZipCopyNode.js +26 -26
  49. package/dist/node/ZipCopyNode.js.map +1 -1
  50. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  51. package/dist/node/ZipDecompressNode.js +8 -6
  52. package/dist/node/ZipDecompressNode.js.map +1 -1
  53. package/dist/node/ZipkitNode.d.ts +14 -1
  54. package/dist/node/ZipkitNode.d.ts.map +1 -1
  55. package/dist/node/ZipkitNode.js +91 -53
  56. package/dist/node/ZipkitNode.js.map +1 -1
  57. package/dist/node/ZstdNode.d.ts +14 -4
  58. package/dist/node/ZstdNode.d.ts.map +1 -1
  59. package/dist/node/ZstdNode.js +28 -4
  60. package/dist/node/ZstdNode.js.map +1 -1
  61. package/dist/node/buildZipBuffer.d.ts +49 -0
  62. package/dist/node/buildZipBuffer.d.ts.map +1 -0
  63. package/dist/node/buildZipBuffer.js +146 -0
  64. package/dist/node/buildZipBuffer.js.map +1 -0
  65. package/dist/node/index.d.ts +3 -0
  66. package/dist/node/index.d.ts.map +1 -1
  67. package/dist/node/index.js +6 -1
  68. package/dist/node/index.js.map +1 -1
  69. package/dist/node/inflateZipPayload.d.ts +5 -0
  70. package/dist/node/inflateZipPayload.d.ts.map +1 -0
  71. package/dist/node/inflateZipPayload.js +21 -0
  72. package/dist/node/inflateZipPayload.js.map +1 -0
  73. package/dist/types/index.js +2 -2
  74. package/node-esm.mjs +32 -0
  75. package/package.json +11 -5
  76. package/src/browser/zstd-browser-shim.ts +110 -28
  77. package/src/core/ZipCompress.ts +6 -0
  78. package/src/core/ZipCopy.ts +56 -27
  79. package/src/core/ZipEntry.ts +192 -13
  80. package/src/core/Zipkit.ts +40 -22
  81. package/src/core/constants/Headers.ts +1 -0
  82. package/src/core/index.ts +3 -0
  83. package/src/core/inflateZipPayload.ts +27 -0
  84. package/src/core/origin/OriginExtra.ts +209 -0
  85. package/src/core/version.ts +1 -1
  86. package/src/core/zip64/Zip64.ts +473 -0
  87. package/src/node/ZipCopyNode.ts +31 -29
  88. package/src/node/ZipDecompressNode.ts +6 -5
  89. package/src/node/ZipkitNode.ts +121 -68
  90. package/src/node/ZstdNode.ts +33 -8
  91. package/src/node/buildZipBuffer.ts +221 -0
  92. package/src/node/index.ts +7 -0
  93. package/src/node/inflateZipPayload.ts +19 -0
  94. package/src/types/index.ts +2 -2
@@ -1,63 +1,143 @@
1
1
  /**
2
- * Browser stub for ZSTD — native zlib streaming is Node-only.
3
- * Browser builds alias `ZstdNode` to this module so the bad WASM codec is never shipped.
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
- const ZSTD_BROWSER_UNSUPPORTED =
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
- function unsupported(): never {
11
- throw new Error(ZSTD_BROWSER_UNSUPPORTED);
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
- return false;
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
- export function mapCompressionLevel(_level: number): number {
19
- return unsupported();
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
- return unsupported();
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
- return unsupported();
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 async function compress(_data: Buffer, _level?: number): Promise<Buffer> {
31
- return unsupported();
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 async function decompress(_data: Buffer): Promise<Buffer> {
35
- return unsupported();
98
+ export function compressSyncAtLevel(data: ZstdInput, _zstdLevel?: number): Buffer {
99
+ return compressSync(data);
36
100
  }
37
101
 
38
- export function compressSync(_data: Buffer, _level?: number): Buffer {
39
- return unsupported();
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 decompressSync(_data: Buffer): Buffer {
43
- return unsupported();
106
+ export function asZstdBuffer(data: ZstdInput): Buffer {
107
+ return toBuffer(asBytes(data));
44
108
  }
45
109
 
46
110
  export async function compressChunks(
47
- _readChunk: (position: number, size: number) => Buffer,
48
- _totalSize: number,
49
- _chunkSize: number,
50
- _level: number,
111
+ readChunk: (position: number, size: number) => Buffer,
112
+ totalSize: number,
113
+ chunkSize: number,
114
+ level: number,
51
115
  _onPlainChunk?: (chunk: Buffer) => void,
52
- _onCompressedChunk?: (chunk: Buffer) => void | Promise<void>
116
+ onCompressedChunk?: (chunk: Buffer) => void | Promise<void>
53
117
  ): Promise<number> {
54
- return unsupported();
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
- _compressedStream: AsyncIterable<Buffer>
134
+ compressedStream: AsyncIterable<Buffer>
59
135
  ): AsyncGenerator<Buffer> {
60
- unsupported();
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
  };
@@ -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
  /**
@@ -10,7 +10,14 @@
10
10
 
11
11
  import ZipEntry from './ZipEntry';
12
12
  import Zipkit from './Zipkit';
13
- import { LOCAL_HDR, CENTRAL_END, GP_FLAG } from './constants/Headers';
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 16 bytes for the data descriptor
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 totalEntrySize = localHeaderSize + entry.compressedSize + (hasDataDescriptor ? 16 : 0);
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
- const commentBytes = Buffer.from(zipComment, 'utf8');
287
- const commentLength = Math.min(commentBytes.length, 0xffff);
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
- return Buffer.concat([centralDirBuffer, eocdBuffer]);
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
  /**
@@ -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.cmpMethod === CMP_METHOD.AES_ENCRYPT ? VER_AES_EXTRACT : VER_EXTRACT;
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 size
409
- data.writeUInt32LE(this.compressedSize, LOCAL_HDR.CMP_SIZE);
410
- // uncompressed size
411
- data.writeUInt32LE(this.uncompressedSize, LOCAL_HDR.UNCMP_SIZE);
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 extraLen = utfLen + sha256Len + uidgidLen + symlinkLen + hardlinkLen + (needsUnicode ? unicodePathLen : 0) + aesLen + neoLen;
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.cmpMethod === CMP_METHOD.AES_ENCRYPT ? VER_AES_EXTRACT : VER_EXTRACT;
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 Size
520
- data.writeUInt32LE(this.compressedSize, CENTRAL_DIR.CMP_SIZE);
521
- // Uncompressed Size
522
- data.writeUInt32LE(this.uncompressedSize, CENTRAL_DIR.UNCMP_SIZE);
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(0, CENTRAL_DIR.DISK_NUM);
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(this.localHdrOffset, CENTRAL_DIR.LOCAL_HDR_OFFSET);
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