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.
Files changed (128) hide show
  1. package/NEOZIP_APPNOTE.md +625 -0
  2. package/README.md +44 -11
  3. package/WHATS_NEW.md +153 -10
  4. package/dist/browser/ZipkitBrowser.d.ts.map +1 -1
  5. package/dist/browser/ZipkitBrowser.js +4 -6
  6. package/dist/browser/ZipkitBrowser.js.map +1 -1
  7. package/dist/browser/zstd-browser-shim.d.ts +17 -9
  8. package/dist/browser/zstd-browser-shim.d.ts.map +1 -1
  9. package/dist/browser/zstd-browser-shim.js +99 -21
  10. package/dist/browser/zstd-browser-shim.js.map +1 -1
  11. package/dist/browser-esm/index.js +6534 -1275
  12. package/dist/browser-esm/index.js.map +4 -4
  13. package/dist/browser-umd/index.js +6534 -1275
  14. package/dist/browser-umd/index.js.map +4 -4
  15. package/dist/browser-umd/index.min.js +6 -3
  16. package/dist/core/ZipCompress.d.ts +6 -0
  17. package/dist/core/ZipCompress.d.ts.map +1 -1
  18. package/dist/core/ZipCompress.js +12 -11
  19. package/dist/core/ZipCompress.js.map +1 -1
  20. package/dist/core/ZipCopy.d.ts.map +1 -1
  21. package/dist/core/ZipCopy.js +33 -25
  22. package/dist/core/ZipCopy.js.map +1 -1
  23. package/dist/core/ZipDecompress.d.ts.map +1 -1
  24. package/dist/core/ZipDecompress.js +13 -8
  25. package/dist/core/ZipDecompress.js.map +1 -1
  26. package/dist/core/ZipEntry.d.ts +24 -0
  27. package/dist/core/ZipEntry.d.ts.map +1 -1
  28. package/dist/core/ZipEntry.js +146 -14
  29. package/dist/core/ZipEntry.js.map +1 -1
  30. package/dist/core/Zipkit.d.ts +43 -11
  31. package/dist/core/Zipkit.d.ts.map +1 -1
  32. package/dist/core/Zipkit.js +132 -82
  33. package/dist/core/Zipkit.js.map +1 -1
  34. package/dist/core/components/HashCalculator.d.ts +22 -1
  35. package/dist/core/components/HashCalculator.d.ts.map +1 -1
  36. package/dist/core/components/HashCalculator.js +49 -6
  37. package/dist/core/components/HashCalculator.js.map +1 -1
  38. package/dist/core/components/Util.d.ts +3 -4
  39. package/dist/core/components/Util.d.ts.map +1 -1
  40. package/dist/core/components/Util.js +10 -8
  41. package/dist/core/components/Util.js.map +1 -1
  42. package/dist/core/constants/Headers.d.ts +2 -3
  43. package/dist/core/constants/Headers.d.ts.map +1 -1
  44. package/dist/core/constants/Headers.js +15 -5
  45. package/dist/core/constants/Headers.js.map +1 -1
  46. package/dist/core/constants/MetaPaths.d.ts +38 -0
  47. package/dist/core/constants/MetaPaths.d.ts.map +1 -0
  48. package/dist/core/constants/MetaPaths.js +79 -0
  49. package/dist/core/constants/MetaPaths.js.map +1 -0
  50. package/dist/core/index.d.ts +5 -0
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js +18 -1
  53. package/dist/core/index.js.map +1 -1
  54. package/dist/core/inflateZipPayload.d.ts +6 -0
  55. package/dist/core/inflateZipPayload.d.ts.map +1 -0
  56. package/dist/core/inflateZipPayload.js +28 -0
  57. package/dist/core/inflateZipPayload.js.map +1 -0
  58. package/dist/core/merkle/MerkleRoot.d.ts +109 -0
  59. package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
  60. package/dist/core/merkle/MerkleRoot.js +275 -0
  61. package/dist/core/merkle/MerkleRoot.js.map +1 -0
  62. package/dist/core/origin/OriginExtra.d.ts +43 -0
  63. package/dist/core/origin/OriginExtra.d.ts.map +1 -0
  64. package/dist/core/origin/OriginExtra.js +187 -0
  65. package/dist/core/origin/OriginExtra.js.map +1 -0
  66. package/dist/core/version.js +1 -1
  67. package/dist/core/zip64/Zip64.d.ts +138 -0
  68. package/dist/core/zip64/Zip64.d.ts.map +1 -0
  69. package/dist/core/zip64/Zip64.js +353 -0
  70. package/dist/core/zip64/Zip64.js.map +1 -0
  71. package/dist/node/ZipCompressNode.d.ts.map +1 -1
  72. package/dist/node/ZipCompressNode.js +40 -49
  73. package/dist/node/ZipCompressNode.js.map +1 -1
  74. package/dist/node/ZipCopyNode.d.ts.map +1 -1
  75. package/dist/node/ZipCopyNode.js +26 -26
  76. package/dist/node/ZipCopyNode.js.map +1 -1
  77. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  78. package/dist/node/ZipDecompressNode.js +21 -3
  79. package/dist/node/ZipDecompressNode.js.map +1 -1
  80. package/dist/node/ZipkitNode.d.ts +23 -1
  81. package/dist/node/ZipkitNode.d.ts.map +1 -1
  82. package/dist/node/ZipkitNode.js +158 -57
  83. package/dist/node/ZipkitNode.js.map +1 -1
  84. package/dist/node/ZstdNode.d.ts +14 -4
  85. package/dist/node/ZstdNode.d.ts.map +1 -1
  86. package/dist/node/ZstdNode.js +28 -4
  87. package/dist/node/ZstdNode.js.map +1 -1
  88. package/dist/node/buildZipBuffer.d.ts +49 -0
  89. package/dist/node/buildZipBuffer.d.ts.map +1 -0
  90. package/dist/node/buildZipBuffer.js +146 -0
  91. package/dist/node/buildZipBuffer.js.map +1 -0
  92. package/dist/node/index.d.ts +3 -0
  93. package/dist/node/index.d.ts.map +1 -1
  94. package/dist/node/index.js +6 -1
  95. package/dist/node/index.js.map +1 -1
  96. package/dist/node/inflateZipPayload.d.ts +5 -0
  97. package/dist/node/inflateZipPayload.d.ts.map +1 -0
  98. package/dist/node/inflateZipPayload.js +21 -0
  99. package/dist/node/inflateZipPayload.js.map +1 -0
  100. package/dist/types/index.js +2 -2
  101. package/node-esm.mjs +32 -0
  102. package/package.json +13 -6
  103. package/src/browser/ZipkitBrowser.ts +4 -6
  104. package/src/browser/zstd-browser-shim.ts +110 -28
  105. package/src/core/ZipCompress.ts +20 -13
  106. package/src/core/ZipCopy.ts +56 -27
  107. package/src/core/ZipDecompress.ts +14 -8
  108. package/src/core/ZipEntry.ts +200 -14
  109. package/src/core/Zipkit.ts +189 -98
  110. package/src/core/components/HashCalculator.ts +58 -6
  111. package/src/core/components/Util.ts +16 -7
  112. package/src/core/constants/Headers.ts +15 -4
  113. package/src/core/constants/MetaPaths.ts +86 -0
  114. package/src/core/index.ts +17 -0
  115. package/src/core/inflateZipPayload.ts +27 -0
  116. package/src/core/merkle/MerkleRoot.ts +312 -0
  117. package/src/core/origin/OriginExtra.ts +209 -0
  118. package/src/core/version.ts +1 -1
  119. package/src/core/zip64/Zip64.ts +473 -0
  120. package/src/node/ZipCompressNode.ts +40 -49
  121. package/src/node/ZipCopyNode.ts +31 -29
  122. package/src/node/ZipDecompressNode.ts +16 -3
  123. package/src/node/ZipkitNode.ts +203 -72
  124. package/src/node/ZstdNode.ts +33 -8
  125. package/src/node/buildZipBuffer.ts +221 -0
  126. package/src/node/index.ts +7 -0
  127. package/src/node/inflateZipPayload.ts +19 -0
  128. package/src/types/index.ts +2 -2
@@ -10,7 +10,11 @@
10
10
  import * as fs from 'fs';
11
11
  import ZipEntry from '../core/ZipEntry';
12
12
  import ZipkitNode from './ZipkitNode';
13
- import { LOCAL_HDR, CENTRAL_END, GP_FLAG } from '../core/constants/Headers';
13
+ import { LOCAL_HDR, GP_FLAG } from '../core/constants/Headers';
14
+ import {
15
+ buildEndRecords,
16
+ dataDescriptorByteLength,
17
+ } from '../core/zip64/Zip64';
14
18
 
15
19
  /**
16
20
  * Options for copying ZIP files
@@ -159,9 +163,12 @@ export class ZipCopyNode {
159
163
  const localHeaderSize = this.calculateLocalHeaderSize(sourceFd, entry);
160
164
 
161
165
  // Determine total entry size
162
- // For data descriptor entries, add 16 bytes for the data descriptor
166
+ // For data descriptor entries, add Zip64 (24) or classic (16) descriptor bytes
163
167
  const hasDataDescriptor = (entry.bitFlags & GP_FLAG.DATA_DESC) !== 0;
164
- const totalEntrySize = localHeaderSize + entry.compressedSize + (hasDataDescriptor ? 16 : 0);
168
+ const descLen = hasDataDescriptor
169
+ ? dataDescriptorByteLength(entry.usesZip64Extra)
170
+ : 0;
171
+ const totalEntrySize = localHeaderSize + entry.compressedSize + descLen;
165
172
 
166
173
  // Read the entire entry (local header + compressed data + data descriptor if present) from source
167
174
  const entryBuffer = Buffer.alloc(totalEntrySize);
@@ -248,6 +255,10 @@ export class ZipCopyNode {
248
255
  cloned.uid = entry.uid;
249
256
  cloned.gid = entry.gid;
250
257
  cloned.sha256 = entry.sha256;
258
+ cloned.emitUnicodePath = entry.emitUnicodePath;
259
+ if (entry.additionalExtra) {
260
+ cloned.additionalExtra = Buffer.from(entry.additionalExtra);
261
+ }
251
262
 
252
263
  // Copy symlink data
253
264
  cloned.isSymlink = entry.isSymlink;
@@ -258,6 +269,14 @@ export class ZipCopyNode {
258
269
  cloned.originalEntry = entry.originalEntry;
259
270
  cloned.inode = entry.inode;
260
271
 
272
+ cloned.usesZip64Extra = entry.usesZip64Extra;
273
+ cloned.aesVersion = entry.aesVersion;
274
+ cloned.aesStrength = entry.aesStrength;
275
+ cloned.realCmpMethod = entry.realCmpMethod;
276
+ cloned.neoCryptoPayloadVersion = entry.neoCryptoPayloadVersion;
277
+ cloned.neoCryptoAlgorithm = entry.neoCryptoAlgorithm;
278
+ cloned.neoCryptoFlags = entry.neoCryptoFlags;
279
+
261
280
  return cloned;
262
281
  }
263
282
 
@@ -280,33 +299,16 @@ export class ZipCopyNode {
280
299
  const centralDirEndOffset = fs.fstatSync(destFd).size;
281
300
  const centralDirSize = centralDirEndOffset - centralDirStartOffset;
282
301
 
283
- const commentBytes = Buffer.from(zipComment, 'utf8');
284
- const commentLength = Math.min(commentBytes.length, 0xFFFF);
285
-
286
- const eocdBuffer = Buffer.alloc(22 + commentLength);
287
- let pos = 0;
288
-
289
- eocdBuffer.writeUInt32LE(CENTRAL_END.SIGNATURE, pos);
290
- pos += 4;
291
- eocdBuffer.writeUInt16LE(0, pos);
292
- pos += 2;
293
- eocdBuffer.writeUInt16LE(0, pos);
294
- pos += 2;
295
- eocdBuffer.writeUInt16LE(entries.length, pos);
296
- pos += 2;
297
- eocdBuffer.writeUInt16LE(entries.length, pos);
298
- pos += 2;
299
- eocdBuffer.writeUInt32LE(centralDirSize, pos);
300
- pos += 4;
301
- eocdBuffer.writeUInt32LE(centralDirStartOffset, pos);
302
- pos += 4;
303
- eocdBuffer.writeUInt16LE(commentLength, pos);
304
- pos += 2;
305
- if (commentLength > 0) {
306
- commentBytes.copy(eocdBuffer, pos, 0, commentLength);
307
- }
302
+ const endRecords = buildEndRecords({
303
+ totalEntries: entries.length,
304
+ centralDirSize,
305
+ centralDirOffset: centralDirStartOffset,
306
+ zip64EocdOffset: centralDirStartOffset + centralDirSize,
307
+ archiveComment: zipComment,
308
+ allowSizeOffsetZip64: true,
309
+ });
308
310
 
309
- fs.writeSync(destFd, eocdBuffer, 0, eocdBuffer.length);
311
+ fs.writeSync(destFd, endRecords, 0, endRecords.length);
310
312
 
311
313
  return { centralDirOffset: centralDirStartOffset, centralDirSize };
312
314
  }
@@ -451,6 +451,8 @@ export class ZipDecompressNode {
451
451
  if (!options?.skipHashCheck) {
452
452
  if (entry.sha256) {
453
453
  const calculatedHash = hashCalc.finalizeSHA256();
454
+ const leaf = hashCalc.finalizeMerkleLeafV1();
455
+ if (leaf) entry.merkleLeafV1 = leaf;
454
456
  this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
455
457
  if (calculatedHash !== entry.sha256) {
456
458
  if (options?.outputPath && fs) {
@@ -540,6 +542,8 @@ export class ZipDecompressNode {
540
542
  if (!options?.skipHashCheck) {
541
543
  if (entry.sha256) {
542
544
  const calculatedHash = hashCalc.finalizeSHA256();
545
+ const leaf = hashCalc.finalizeMerkleLeafV1();
546
+ if (leaf) entry.merkleLeafV1 = leaf;
543
547
  this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
544
548
  if (calculatedHash !== entry.sha256) {
545
549
  throw new Error(Errors.INVALID_SHA256);
@@ -588,7 +592,8 @@ export class ZipDecompressNode {
588
592
  const decompressedStream = this.decompressStream(compressedStream, entry.cmpMethod, undefined, entry);
589
593
 
590
594
  // Process and validate chunks - one block at a time
591
- const hashCalc = new HashCalculator({ useSHA256: !!entry.sha256 });
595
+ // Always hash the v1 Merkle leaf so getMerkleRootAsync works for CRC-only archives.
596
+ const hashCalc = new HashCalculator({ useSHA256: true });
592
597
  let totalBytes = 0;
593
598
 
594
599
  try {
@@ -607,6 +612,8 @@ export class ZipDecompressNode {
607
612
  if (!options?.skipHashCheck) {
608
613
  if (entry.sha256) {
609
614
  const calculatedHash = hashCalc.finalizeSHA256();
615
+ const leaf = hashCalc.finalizeMerkleLeafV1();
616
+ if (leaf) entry.merkleLeafV1 = leaf;
610
617
  this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
611
618
  if (calculatedHash !== entry.sha256) {
612
619
  throw new Error(Errors.INVALID_SHA256);
@@ -616,16 +623,22 @@ export class ZipDecompressNode {
616
623
  return { verifiedHash: calculatedHash };
617
624
  } else {
618
625
  const calculatedCRC = hashCalc.finalizeCRC32();
626
+ const leaf = hashCalc.finalizeMerkleLeafV1();
627
+ if (leaf) entry.merkleLeafV1 = leaf;
619
628
  this.log(`CRC-32 comparison: calculated=${calculatedCRC}, stored=${entry.crc}`);
620
629
  if (calculatedCRC !== entry.crc) {
621
630
  throw new Error(Errors.INVALID_CRC);
622
631
  }
623
632
  this.log(`CRC-32 comparison: calculated=${calculatedCRC}, stored=${entry.crc}`);
624
- // No hash to return for CRC-32 only entries
625
633
  return { verifiedHash: undefined };
626
634
  }
627
635
  } else {
628
- // Hash check skipped - return undefined
636
+ const leaf = hashCalc.finalizeMerkleLeafV1();
637
+ if (leaf) entry.merkleLeafV1 = leaf;
638
+ if (entry.sha256 && hashCalc) {
639
+ const calculatedHash = hashCalc.finalizeSHA256();
640
+ return { verifiedHash: calculatedHash || undefined };
641
+ }
629
642
  return { verifiedHash: undefined };
630
643
  }
631
644
  } catch (error) {
@@ -11,22 +11,40 @@ import { ZipDecompressNode } from './ZipDecompressNode';
11
11
  import {
12
12
  CENTRAL_END,
13
13
  CENTRAL_DIR,
14
- ZIP64_CENTRAL_END,
15
- ZIP64_CENTRAL_DIR,
16
14
  LOCAL_HDR,
17
15
  GP_FLAG,
18
16
  CMP_METHOD,
19
- ENCRYPT_HDR_SIZE
17
+ ENCRYPT_HDR_SIZE,
18
+ HDR_ID,
20
19
  } from '../core/constants/Headers';
20
+ import {
21
+ buildEndRecords,
22
+ classicEocdNeedsZip64,
23
+ exceedsU32,
24
+ parseZip64Eocd,
25
+ parseZip64Locator,
26
+ writeU64,
27
+ ZIP64_EOCD_RECORD_SIZE,
28
+ ZIP64_LOCATOR_SIZE,
29
+ ZIP64_U32,
30
+ } from '../core/zip64/Zip64';
21
31
  import { NEO_CRYPTO_ALGORITHM_AES256_V1 } from '../core/encryption/NeoCrypto';
22
32
  import { AesCrypto } from '../core/encryption/AesCrypto';
23
33
  import {
24
34
  detectLegacyWasmZstd,
25
35
  type LegacyZstdDetectResult,
26
36
  } from './LegacyZstd';
37
+ import { writeZipFileSync, type ZipBufferMember, type BuildZipBufferOptions } from './buildZipBuffer';
38
+ export type { ZipBufferMember };
27
39
  import * as fs from 'fs';
28
40
  import * as path from 'path';
29
41
  import { minimatch } from 'minimatch';
42
+ import {
43
+ computeMerkleRootV0FromDigests,
44
+ computeMerkleRootV1FromLeaves,
45
+ type MerkleAlgorithm,
46
+ } from '../core/merkle/MerkleRoot';
47
+ import { isMetaInfPath } from '../core/constants/MetaPaths';
30
48
 
31
49
  // Re-export everything from core Zipkit
32
50
  export * from '../core';
@@ -111,6 +129,79 @@ export default class ZipkitNode extends Zipkit {
111
129
  return this._zipkitDeCmpNode;
112
130
  }
113
131
 
132
+ /**
133
+ * Streaming Merkle root for file-based archives.
134
+ * V1 leaves come from the same decompress→hash pass as integrity (testEntry / extract),
135
+ * chunked — never re-reads uncompressed sources or materializes whole entries for the root alone.
136
+ */
137
+ async getMerkleRootAsync(options?: { algorithm?: MerkleAlgorithm }): Promise<string | null> {
138
+ const algorithm: MerkleAlgorithm = options?.algorithm ?? 'v1';
139
+ let zipEntries: ZipEntry[];
140
+ try {
141
+ zipEntries = this.getDirectory();
142
+ } catch {
143
+ return null;
144
+ }
145
+ if (!zipEntries?.length) {
146
+ return null;
147
+ }
148
+
149
+ const contentEntries = zipEntries.filter((e) => !isMetaInfPath(e.filename || ''));
150
+ if (contentEntries.length === 0) {
151
+ return null;
152
+ }
153
+
154
+ if (algorithm === 'v0') {
155
+ const digests = contentEntries
156
+ .filter((e) => e.sha256)
157
+ .map((e) => ({ path: e.filename || '', sha256: e.sha256 as string }));
158
+ return computeMerkleRootV0FromDigests(digests);
159
+ }
160
+
161
+ if (contentEntries.every((e) => !!e.merkleLeafV1)) {
162
+ return computeMerkleRootV1FromLeaves(
163
+ contentEntries.map((e) => ({
164
+ path: e.filename || '',
165
+ merkleLeafV1: e.merkleLeafV1 as string,
166
+ }))
167
+ );
168
+ }
169
+
170
+ // Buffer-loaded: base class extract path
171
+ if ((this as any).inBuffer) {
172
+ return super.getMerkleRootAsync(options);
173
+ }
174
+
175
+ // File-based: stream hash each content entry once via testEntry (no write, dual hash)
176
+ if (!this.fileHandle && !this.filePath) {
177
+ // Create-session only: may only have live ZipEntry[] with leaves from write
178
+ return super.getMerkleRootAsync(options);
179
+ }
180
+
181
+ try {
182
+ for (const entry of contentEntries) {
183
+ if (entry.merkleLeafV1) {
184
+ continue;
185
+ }
186
+ await this.getZipDecompressNode().testEntry(entry, { skipHashCheck: false });
187
+ }
188
+ } catch (err) {
189
+ console.error('[ZipkitNode.getMerkleRootAsync] stream hash failed:', err);
190
+ return null;
191
+ }
192
+
193
+ if (!contentEntries.every((e) => !!e.merkleLeafV1)) {
194
+ return null;
195
+ }
196
+
197
+ return computeMerkleRootV1FromLeaves(
198
+ contentEntries.map((e) => ({
199
+ path: e.filename || '',
200
+ merkleLeafV1: e.merkleLeafV1 as string,
201
+ }))
202
+ );
203
+ }
204
+
114
205
  // ============================================================================
115
206
  // File Loading Methods
116
207
  // ============================================================================
@@ -638,6 +729,10 @@ export default class ZipkitNode extends Zipkit {
638
729
  onHashCalculated?: (entry: ZipEntry, hash: Buffer) => void;
639
730
  }
640
731
  ): Promise<void> {
732
+ if (options?.forceZip64) {
733
+ entry.forceZip64 = true;
734
+ }
735
+
641
736
  // Set compression method based on options
642
737
  const level = options?.level ?? 6;
643
738
  let realCmpMethod: number;
@@ -650,11 +745,10 @@ export default class ZipkitNode extends Zipkit {
650
745
  }
651
746
 
652
747
  // For AES encryption, pre-configure the entry so the initial local header
653
- // includes the 0x9901 extra field and method 99. The actual compression
654
- // method is preserved in realCmpMethod for the AES extra field.
655
- // Default password encryption is WinZip AES-256 (matches compressData).
748
+ // matches the chosen scheme (WinZip method 99 + 0x9901, or NeoEncrypt + 0x024E).
749
+ // Default password encryption is NeoEncrypt (matches NeoZip product AES-256).
656
750
  const encMethod = options?.password
657
- ? (options.encryptionMethod || 'aes256')
751
+ ? (options.encryptionMethod || 'neo-aes256')
658
752
  : undefined;
659
753
  const isAesRequested = encMethod === 'aes256';
660
754
  const isNeoRequested = encMethod === 'neo-aes256';
@@ -781,9 +875,55 @@ export default class ZipkitNode extends Zipkit {
781
875
  throw new Error(`Compressed size not set for entry: ${entry.filename}`);
782
876
  }
783
877
 
784
- const sizeBuffer = Buffer.alloc(4);
785
- sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
786
- fs.writeSync(writer.outputFd, sizeBuffer, 0, 4, entry.localHdrOffset + 18);
878
+ const classicCmp = entry.localHdrOffset + LOCAL_HDR.CMP_SIZE;
879
+ const classicUncmp = entry.localHdrOffset + LOCAL_HDR.UNCMP_SIZE;
880
+ const useZip64Sizes =
881
+ entry.forceZip64 ||
882
+ exceedsU32(entry.uncompressedSize) ||
883
+ exceedsU32(entry.compressedSize);
884
+
885
+ if (useZip64Sizes) {
886
+ // Classic fields stay at 0xFFFFFFFF; update u64 sizes inside local extra 0x0001
887
+ const sentinel = Buffer.alloc(4);
888
+ sentinel.writeUInt32LE(ZIP64_U32, 0);
889
+ fs.writeSync(writer.outputFd, sentinel, 0, 4, classicCmp);
890
+ fs.writeSync(writer.outputFd, sentinel, 0, 4, classicUncmp);
891
+
892
+ const fnameLenBuf = Buffer.alloc(2);
893
+ fs.readSync(writer.outputFd, fnameLenBuf, 0, 2, entry.localHdrOffset + LOCAL_HDR.FNAME_LEN);
894
+ const fnameLen = fnameLenBuf.readUInt16LE(0);
895
+ const extraLenBuf = Buffer.alloc(2);
896
+ fs.readSync(writer.outputFd, extraLenBuf, 0, 2, entry.localHdrOffset + LOCAL_HDR.EXTRA_LEN);
897
+ const extraLen = extraLenBuf.readUInt16LE(0);
898
+ const extraStart = entry.localHdrOffset + LOCAL_HDR.SIZE + fnameLen;
899
+ const extraBuf = Buffer.alloc(extraLen);
900
+ if (extraLen > 0) {
901
+ fs.readSync(writer.outputFd, extraBuf, 0, extraLen, extraStart);
902
+ }
903
+ let patched = false;
904
+ for (let i = 0; i + 4 <= extraBuf.length; ) {
905
+ const id = extraBuf.readUInt16LE(i);
906
+ const len = extraBuf.readUInt16LE(i + 2);
907
+ if (id === HDR_ID.ZIP64 && len >= 16) {
908
+ // Local Zip64 always has uncompressed (8) then compressed (8)
909
+ writeU64(extraBuf, i + 4, entry.uncompressedSize);
910
+ writeU64(extraBuf, i + 12, entry.compressedSize);
911
+ fs.writeSync(writer.outputFd, extraBuf, i + 4, 16, extraStart + i + 4);
912
+ patched = true;
913
+ break;
914
+ }
915
+ i += 4 + len;
916
+ }
917
+ if (!patched) {
918
+ throw new Error(
919
+ `Zip64 local extra (0x0001) missing for large entry: ${entry.filename}`
920
+ );
921
+ }
922
+ } else {
923
+ const sizeBuffer = Buffer.alloc(4);
924
+ sizeBuffer.writeUInt32LE(entry.compressedSize, 0);
925
+ fs.writeSync(writer.outputFd, sizeBuffer, 0, 4, classicCmp);
926
+ }
787
927
 
788
928
  if (entry.crc !== undefined) {
789
929
  const crcBuffer = Buffer.alloc(4);
@@ -860,6 +1000,7 @@ export default class ZipkitNode extends Zipkit {
860
1000
  * @param centralDirSize - Size of central directory in bytes
861
1001
  * @param centralDirOffset - Offset to start of central directory
862
1002
  * @param archiveComment - Optional archive comment (max 65535 bytes)
1003
+ * @param options - Optional Zip64 force and related EOCD flags
863
1004
  * @returns Promise that resolves when EOCD is written
864
1005
  */
865
1006
  async writeEndOfCentralDirectory(
@@ -867,53 +1008,20 @@ export default class ZipkitNode extends Zipkit {
867
1008
  totalEntries: number,
868
1009
  centralDirSize: number,
869
1010
  centralDirOffset: number,
870
- archiveComment?: string
1011
+ archiveComment?: string,
1012
+ options?: { forceZip64?: boolean }
871
1013
  ): Promise<void> {
872
- const comment = archiveComment || '';
873
- const commentBytes = Buffer.from(comment, 'utf8');
874
- const commentLength = Math.min(commentBytes.length, 0xFFFF); // Max 65535 bytes
875
-
876
- const buffer = Buffer.alloc(22 + commentLength);
877
- let offset = 0;
878
-
879
- // End of central directory signature (4 bytes)
880
- buffer.writeUInt32LE(0x06054b50, offset);
881
- offset += 4;
882
-
883
- // Number of this disk (2 bytes)
884
- buffer.writeUInt16LE(0, offset);
885
- offset += 2;
886
-
887
- // Number of the disk with the start of the central directory (2 bytes)
888
- buffer.writeUInt16LE(0, offset);
889
- offset += 2;
890
-
891
- // Total number of entries in the central directory on this disk (2 bytes)
892
- buffer.writeUInt16LE(totalEntries, offset);
893
- offset += 2;
894
-
895
- // Total number of entries in the central directory (2 bytes)
896
- buffer.writeUInt16LE(totalEntries, offset);
897
- offset += 2;
898
-
899
- // Size of the central directory (4 bytes)
900
- buffer.writeUInt32LE(centralDirSize, offset);
901
- offset += 4;
902
-
903
- // Offset of start of central directory with respect to the starting disk number (4 bytes)
904
- buffer.writeUInt32LE(centralDirOffset, offset);
905
- offset += 4;
906
-
907
- // ZIP file comment length (2 bytes)
908
- buffer.writeUInt16LE(commentLength, offset);
909
- offset += 2;
910
-
911
- // ZIP file comment (variable length)
912
- if (commentLength > 0) {
913
- commentBytes.copy(buffer, offset, 0, commentLength);
914
- }
1014
+ const buffer = buildEndRecords({
1015
+ totalEntries,
1016
+ centralDirSize,
1017
+ centralDirOffset,
1018
+ zip64EocdOffset: centralDirOffset + centralDirSize,
1019
+ archiveComment,
1020
+ allowSizeOffsetZip64: true,
1021
+ forceZip64: options?.forceZip64 === true,
1022
+ });
915
1023
 
916
- // Write EOCD to file
1024
+ // Write EOCD (and Zip64 EOCD + locator when needed) to file
917
1025
  await new Promise<void>((resolve, reject) => {
918
1026
  writer.outputStream.write(buffer, (error) => {
919
1027
  if (error) {
@@ -989,7 +1097,9 @@ export default class ZipkitNode extends Zipkit {
989
1097
  writer,
990
1098
  entries.length,
991
1099
  centralDirSize,
992
- centralDirOffset
1100
+ centralDirOffset,
1101
+ undefined,
1102
+ { forceZip64: options?.forceZip64 === true }
993
1103
  );
994
1104
  } finally {
995
1105
  await this.finalizeZipFile(writer);
@@ -1132,6 +1242,21 @@ export default class ZipkitNode extends Zipkit {
1132
1242
  throw new Error('updateZipFile() - Full implementation pending. Use neozip CLI for now.');
1133
1243
  }
1134
1244
 
1245
+ /**
1246
+ * Stream members to `filePath` (local headers, payloads, central directory, EOCD).
1247
+ * Pass `useSHA256` for Extra Field 0x014E and `additionalExtra` for caller blocks
1248
+ * such as ZipWiki 0x014F. `method` 93 uses zlib zstd at `level` (default 7).
1249
+ * NeoEncrypt is not enabled. Precompressed members are copied without recompression.
1250
+ * Pass `{ forceZip64: true }` to emit Zip64 even for small members (testing).
1251
+ */
1252
+ writeMembersSync(
1253
+ filePath: string,
1254
+ members: ZipBufferMember[],
1255
+ options?: BuildZipBufferOptions
1256
+ ): void {
1257
+ writeZipFileSync(filePath, members, options);
1258
+ }
1259
+
1135
1260
  // ============================================================================
1136
1261
  // File-based ZIP Loading Methods (merged from ZipLoadEntriesServer)
1137
1262
  // ============================================================================
@@ -1197,9 +1322,17 @@ export default class ZipkitNode extends Zipkit {
1197
1322
  const zipkit = this as any;
1198
1323
  zipkit.centralDirSize = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_SIZE);
1199
1324
  zipkit.centralDirOffset = eocdBuffer.readUInt32LE(CENTRAL_END.CENTRAL_DIR_OFFSET);
1200
-
1201
- // Handle ZIP64
1202
- if (zipkit.centralDirOffset === 0xFFFFFFFF) {
1325
+ const volEntries = eocdBuffer.readUInt16LE(CENTRAL_END.VOL_ENTRIES);
1326
+ const totalEntries = eocdBuffer.readUInt16LE(CENTRAL_END.TOTAL_ENTRIES);
1327
+
1328
+ if (
1329
+ classicEocdNeedsZip64({
1330
+ volEntries,
1331
+ totalEntries,
1332
+ centralDirSize: zipkit.centralDirSize,
1333
+ centralDirOffset: zipkit.centralDirOffset,
1334
+ })
1335
+ ) {
1203
1336
  await this.loadZIP64EOCD(eocdOffset);
1204
1337
  }
1205
1338
  } else {
@@ -1229,20 +1362,18 @@ export default class ZipkitNode extends Zipkit {
1229
1362
  }
1230
1363
 
1231
1364
  // Look for ZIP64 locator
1232
- const locatorOffset = eocdOffset - 20;
1233
- const locatorBuffer = Buffer.alloc(20);
1234
- await this.fileHandle.read(locatorBuffer, 0, 20, locatorOffset);
1365
+ const locatorOffset = eocdOffset - ZIP64_LOCATOR_SIZE;
1366
+ const locatorBuffer = Buffer.alloc(ZIP64_LOCATOR_SIZE);
1367
+ await this.fileHandle.read(locatorBuffer, 0, ZIP64_LOCATOR_SIZE, locatorOffset);
1235
1368
 
1236
- if (locatorBuffer.readUInt32LE(0) === ZIP64_CENTRAL_END.SIGNATURE) {
1237
- // Read ZIP64 EOCD
1238
- const zip64Offset = locatorBuffer.readBigUInt64LE(8);
1239
- const zip64Buffer = Buffer.alloc(56);
1240
- await this.fileHandle.read(zip64Buffer, 0, 56, Number(zip64Offset));
1241
-
1242
- const zipkit = this as any;
1243
- zipkit.centralDirSize = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_SIZE));
1244
- zipkit.centralDirOffset = Number(zip64Buffer.readBigUInt64LE(ZIP64_CENTRAL_DIR.CENTRAL_DIR_OFFSET));
1245
- }
1369
+ const zip64Offset = parseZip64Locator(locatorBuffer);
1370
+ const zip64Buffer = Buffer.alloc(ZIP64_EOCD_RECORD_SIZE);
1371
+ await this.fileHandle.read(zip64Buffer, 0, ZIP64_EOCD_RECORD_SIZE, zip64Offset);
1372
+
1373
+ const z64 = parseZip64Eocd(zip64Buffer);
1374
+ const zipkit = this as any;
1375
+ zipkit.centralDirSize = z64.centralDirSize;
1376
+ zipkit.centralDirOffset = z64.centralDirOffset;
1246
1377
  }
1247
1378
 
1248
1379
  /**
@@ -54,26 +54,49 @@ export function createDecompressTransform(): zlib.ZstdDecompress {
54
54
  return zlib.createZstdDecompress();
55
55
  }
56
56
 
57
- export async function compress(data: Buffer, level: number = 6): Promise<Buffer> {
57
+ /** Bytes accepted by the buffer zstd codec (Node Buffer or browser ArrayBuffer). */
58
+ export type ZstdInput = Buffer | Uint8Array | ArrayBuffer;
59
+
60
+ export function asZstdBuffer(data: ZstdInput): Buffer {
61
+ if (Buffer.isBuffer(data)) return data;
62
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
63
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
64
+ }
65
+
66
+ export async function compress(data: ZstdInput, level: number = 6): Promise<Buffer> {
58
67
  assertNativeZstd();
59
- const result = await zstdCompressAsync(data, compressOptions(level));
68
+ const result = await zstdCompressAsync(asZstdBuffer(data), compressOptions(level));
60
69
  return Buffer.from(result);
61
70
  }
62
71
 
63
- export async function decompress(data: Buffer): Promise<Buffer> {
72
+ export async function decompress(data: ZstdInput): Promise<Buffer> {
64
73
  assertNativeZstd();
65
- const result = await zstdDecompressAsync(data);
74
+ const result = await zstdDecompressAsync(asZstdBuffer(data));
66
75
  return Buffer.from(result);
67
76
  }
68
77
 
69
- export function compressSync(data: Buffer, level: number = 6): Buffer {
78
+ export function compressSync(data: ZstdInput, level: number = 6): Buffer {
79
+ assertNativeZstd();
80
+ return zlib.zstdCompressSync(asZstdBuffer(data), compressOptions(level));
81
+ }
82
+
83
+ /**
84
+ * Compress with a raw zlib zstd level (1–19), without the 1–9 kit scale.
85
+ * ZipWiki pack uses level 7 on this scale.
86
+ */
87
+ export function compressSyncAtLevel(data: ZstdInput, zstdLevel: number): Buffer {
70
88
  assertNativeZstd();
71
- return zlib.zstdCompressSync(data, compressOptions(level));
89
+ const level = Math.min(19, Math.max(1, Math.trunc(zstdLevel)));
90
+ return zlib.zstdCompressSync(asZstdBuffer(data), {
91
+ params: {
92
+ [zlib.constants.ZSTD_c_compressionLevel]: level,
93
+ },
94
+ });
72
95
  }
73
96
 
74
- export function decompressSync(data: Buffer): Buffer {
97
+ export function decompressSync(data: ZstdInput): Buffer {
75
98
  assertNativeZstd();
76
- return zlib.zstdDecompressSync(data);
99
+ return zlib.zstdDecompressSync(asZstdBuffer(data));
77
100
  }
78
101
 
79
102
  /**
@@ -147,7 +170,9 @@ export const ZstdNode = {
147
170
  compress,
148
171
  decompress,
149
172
  compressSync,
173
+ compressSyncAtLevel,
150
174
  decompressSync,
175
+ asZstdBuffer,
151
176
  compressChunks,
152
177
  decompressStream,
153
178
  };