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
@@ -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
  };
@@ -31,10 +31,16 @@ import { NeoCrypto, NEO_CRYPTO_ALGORITHM_AES256_V1 } from './encryption/NeoCrypt
31
31
  export interface CompressOptions {
32
32
  level?: number; // Compression level (1-9, 0=store)
33
33
  password?: string | null; // Password for encryption
34
- encryptionMethod?: 'aes256' | 'zipcrypto' | 'neo-aes256'; // default WinZip AES when password set; 'neo-aes256' = NeoEncrypt extra 0x024E
34
+ encryptionMethod?: 'aes256' | 'zipcrypto' | 'neo-aes256'; // default NeoEncrypt when password set; 'aes256' = WinZip AE (method 99); 'neo-aes256' = Extra Field 0x024E
35
35
  useSHA256?: boolean; // Whether to calculate SHA256 hash default is false
36
- useZstd?: boolean; // Whether to use Zstandard compression default is true
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
  /**
@@ -132,20 +138,19 @@ export class ZipCompress {
132
138
  // Calculate hashes if needed
133
139
  if (hashCalculator) {
134
140
  hashCalculator.update(data);
135
- if (!entry.crc || entry.crc === 0) {
136
- entry.crc = hashCalculator.finalizeCRC32();
137
- }
138
- if (options?.useSHA256 && !entry.sha256) {
139
- entry.sha256 = hashCalculator.finalizeSHA256();
140
- }
141
+ hashCalculator.applyToEntry(entry, {
142
+ setCrc: !entry.crc || entry.crc === 0,
143
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
144
+ });
141
145
  this.log(`Final hashes: CRC32=0x${entry.crc.toString(16).padStart(8, '0')}, SHA256=${entry.sha256 || 'N/A'}`);
142
146
  }
143
147
 
144
148
  // Encrypt if password provided
145
149
  const enc = options?.encryptionMethod;
146
150
  const useZipCrypto = enc === 'zipcrypto';
147
- const useNeo = enc === 'neo-aes256';
148
- const useAes = options?.password && !useZipCrypto && !useNeo;
151
+ // Explicit WinZip AE only when encryptionMethod is 'aes256'. Otherwise NeoEncrypt (default).
152
+ const useAes = enc === 'aes256';
153
+ const useNeo = !!options?.password && !useZipCrypto && !useAes;
149
154
  if (options?.password) {
150
155
  const label = useZipCrypto ? 'ZipCrypto' : useNeo ? 'NeoEncrypt (AES-256)' : 'WinZip AES-256';
151
156
  this.log(`Encrypting compressed data for entry: ${entry.filename} (method: ${label})`);
@@ -425,9 +430,11 @@ export class ZipCompress {
425
430
  hashCalculator.update(fileData);
426
431
 
427
432
  // Set hashes
428
- entry.crc = hashCalculator.finalizeCRC32();
429
- if (cmpOptions?.useSHA256) {
430
- entry.sha256 = hashCalculator.finalizeSHA256();
433
+ hashCalculator.applyToEntry(entry, {
434
+ setCrc: true,
435
+ setSha256: !!cmpOptions?.useSHA256,
436
+ });
437
+ if (entry.sha256) {
431
438
  this.log(`SHA-256 calculated: ${entry.sha256}`);
432
439
  }
433
440
 
@@ -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
  /**
@@ -23,6 +23,7 @@ import Errors from './constants/Errors';
23
23
  import { CMP_METHOD } from './constants/Headers';
24
24
  import { ZipCrypto } from './encryption/ZipCrypto';
25
25
  import { AesCrypto } from './encryption/AesCrypto';
26
+ import { HashCalculator } from './components/HashCalculator';
26
27
 
27
28
  export interface DecompressionResult {
28
29
  success: boolean;
@@ -189,18 +190,23 @@ class ZipDecompress {
189
190
  throw new Error(`Unsupported compression method: ${method}`);
190
191
  }
191
192
 
192
- // Verify hash
193
+ // One pass: CRC + bare SHA-256 (0x014E) + v1 Merkle leaf — no second payload read
194
+ const hashCalc = new HashCalculator({ useSHA256: true });
195
+ hashCalc.update(outBuf);
196
+ const calculatedCRC = hashCalc.finalizeCRC32();
197
+ const calculatedHash = hashCalc.finalizeSHA256();
198
+ const leaf = hashCalc.finalizeMerkleLeafV1();
199
+ if (leaf) {
200
+ entry.merkleLeafV1 = leaf;
201
+ }
202
+
193
203
  if (!skipHashCheck) {
194
204
  if (entry.sha256) {
195
- const isValid = this.zipkit.testSHA256(entry, outBuf);
196
- if (!isValid) {
205
+ if (calculatedHash !== entry.sha256) {
197
206
  throw new Error(Errors.INVALID_SHA256);
198
207
  }
199
- } else {
200
- const isValid = this.zipkit.testCRC32(entry, outBuf);
201
- if (!isValid) {
202
- throw new Error(Errors.INVALID_CRC);
203
- }
208
+ } else if (calculatedCRC !== entry.crc) {
209
+ throw new Error(Errors.INVALID_CRC);
204
210
  }
205
211
  }
206
212