neozipkit 0.8.0 → 1.0.5

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 (115) hide show
  1. package/NEOZIP_APPNOTE.md +527 -0
  2. package/README.md +57 -21
  3. package/WHATS_NEW.md +146 -0
  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 +28 -0
  8. package/dist/browser/zstd-browser-shim.d.ts.map +1 -0
  9. package/dist/browser/zstd-browser-shim.js +66 -0
  10. package/dist/browser/zstd-browser-shim.js.map +1 -0
  11. package/dist/browser-esm/index.js +2110 -41755
  12. package/dist/browser-esm/index.js.map +4 -4
  13. package/dist/browser-umd/index.js +2110 -41755
  14. package/dist/browser-umd/index.js.map +4 -4
  15. package/dist/browser-umd/index.min.js +3 -21
  16. package/dist/core/ZipCompress.d.ts +2 -7
  17. package/dist/core/ZipCompress.d.ts.map +1 -1
  18. package/dist/core/ZipCompress.js +57 -33
  19. package/dist/core/ZipCompress.js.map +1 -1
  20. package/dist/core/ZipDecompress.d.ts +1 -3
  21. package/dist/core/ZipDecompress.d.ts.map +1 -1
  22. package/dist/core/ZipDecompress.js +27 -17
  23. package/dist/core/ZipDecompress.js.map +1 -1
  24. package/dist/core/ZipEntry.d.ts +6 -0
  25. package/dist/core/ZipEntry.d.ts.map +1 -1
  26. package/dist/core/ZipEntry.js +7 -1
  27. package/dist/core/ZipEntry.js.map +1 -1
  28. package/dist/core/Zipkit.d.ts +43 -11
  29. package/dist/core/Zipkit.d.ts.map +1 -1
  30. package/dist/core/Zipkit.js +106 -63
  31. package/dist/core/Zipkit.js.map +1 -1
  32. package/dist/core/components/HashCalculator.d.ts +22 -1
  33. package/dist/core/components/HashCalculator.d.ts.map +1 -1
  34. package/dist/core/components/HashCalculator.js +49 -6
  35. package/dist/core/components/HashCalculator.js.map +1 -1
  36. package/dist/core/components/Util.d.ts +3 -4
  37. package/dist/core/components/Util.d.ts.map +1 -1
  38. package/dist/core/components/Util.js +10 -8
  39. package/dist/core/components/Util.js.map +1 -1
  40. package/dist/core/constants/Headers.d.ts +1 -3
  41. package/dist/core/constants/Headers.d.ts.map +1 -1
  42. package/dist/core/constants/Headers.js +14 -5
  43. package/dist/core/constants/Headers.js.map +1 -1
  44. package/dist/core/constants/MetaPaths.d.ts +38 -0
  45. package/dist/core/constants/MetaPaths.d.ts.map +1 -0
  46. package/dist/core/constants/MetaPaths.js +79 -0
  47. package/dist/core/constants/MetaPaths.js.map +1 -0
  48. package/dist/core/encryption/AesCrypto.d.ts +55 -11
  49. package/dist/core/encryption/AesCrypto.d.ts.map +1 -1
  50. package/dist/core/encryption/AesCrypto.js +223 -68
  51. package/dist/core/encryption/AesCrypto.js.map +1 -1
  52. package/dist/core/encryption/index.d.ts +1 -1
  53. package/dist/core/encryption/index.d.ts.map +1 -1
  54. package/dist/core/encryption/index.js +3 -1
  55. package/dist/core/encryption/index.js.map +1 -1
  56. package/dist/core/index.d.ts +2 -0
  57. package/dist/core/index.d.ts.map +1 -1
  58. package/dist/core/index.js +14 -1
  59. package/dist/core/index.js.map +1 -1
  60. package/dist/core/merkle/MerkleRoot.d.ts +109 -0
  61. package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
  62. package/dist/core/merkle/MerkleRoot.js +275 -0
  63. package/dist/core/merkle/MerkleRoot.js.map +1 -0
  64. package/dist/core/version.js +1 -1
  65. package/dist/node/LegacyZstd.d.ts +67 -0
  66. package/dist/node/LegacyZstd.d.ts.map +1 -0
  67. package/dist/node/LegacyZstd.js +209 -0
  68. package/dist/node/LegacyZstd.js.map +1 -0
  69. package/dist/node/ZipCompressNode.d.ts +9 -7
  70. package/dist/node/ZipCompressNode.d.ts.map +1 -1
  71. package/dist/node/ZipCompressNode.js +105 -75
  72. package/dist/node/ZipCompressNode.js.map +1 -1
  73. package/dist/node/ZipDecompressNode.d.ts +7 -3
  74. package/dist/node/ZipDecompressNode.d.ts.map +1 -1
  75. package/dist/node/ZipDecompressNode.js +68 -56
  76. package/dist/node/ZipDecompressNode.js.map +1 -1
  77. package/dist/node/ZipkitNode.d.ts +23 -0
  78. package/dist/node/ZipkitNode.d.ts.map +1 -1
  79. package/dist/node/ZipkitNode.js +195 -41
  80. package/dist/node/ZipkitNode.js.map +1 -1
  81. package/dist/node/ZstdNode.d.ts +39 -0
  82. package/dist/node/ZstdNode.d.ts.map +1 -0
  83. package/dist/node/ZstdNode.js +171 -0
  84. package/dist/node/ZstdNode.js.map +1 -0
  85. package/dist/node/index.d.ts +3 -0
  86. package/dist/node/index.d.ts.map +1 -1
  87. package/dist/node/index.js +11 -1
  88. package/dist/node/index.js.map +1 -1
  89. package/package.json +4 -3
  90. package/src/browser/ZipkitBrowser.ts +4 -6
  91. package/src/browser/zstd-browser-shim.ts +76 -0
  92. package/src/core/ZipCompress.ts +71 -42
  93. package/src/core/ZipDecompress.ts +35 -18
  94. package/src/core/ZipEntry.ts +8 -1
  95. package/src/core/Zipkit.ts +149 -76
  96. package/src/core/components/HashCalculator.ts +58 -6
  97. package/src/core/components/Util.ts +16 -7
  98. package/src/core/constants/Headers.ts +14 -4
  99. package/src/core/constants/MetaPaths.ts +86 -0
  100. package/src/core/encryption/AesCrypto.ts +265 -86
  101. package/src/core/encryption/index.ts +1 -1
  102. package/src/core/index.ts +14 -0
  103. package/src/core/merkle/MerkleRoot.ts +312 -0
  104. package/src/core/version.ts +1 -1
  105. package/src/node/LegacyZstd.ts +270 -0
  106. package/src/node/ZipCompressNode.ts +127 -84
  107. package/src/node/ZipDecompressNode.ts +93 -67
  108. package/src/node/ZipkitNode.ts +230 -42
  109. package/src/node/ZstdNode.ts +155 -0
  110. package/src/node/index.ts +10 -0
  111. package/dist/core/ZstdManager.d.ts +0 -56
  112. package/dist/core/ZstdManager.d.ts.map +0 -1
  113. package/dist/core/ZstdManager.js +0 -144
  114. package/dist/core/ZstdManager.js.map +0 -1
  115. package/src/core/ZstdManager.ts +0 -165
@@ -22,10 +22,10 @@ import { HashCalculator } from '../core/components/HashCalculator';
22
22
  import { ZipCrypto } from '../core/encryption/ZipCrypto';
23
23
  import { AesCrypto } from '../core/encryption/AesCrypto';
24
24
  import { NeoCrypto, NEO_CRYPTO_ALGORITHM_AES256_V1 } from '../core/encryption/NeoCrypto';
25
- import { ZstdManager } from '../core/ZstdManager';
26
25
  import Errors from '../core/constants/Errors';
27
26
  import * as fs from 'fs';
28
27
  import * as path from 'path';
28
+ import { ZstdNode } from './ZstdNode';
29
29
 
30
30
  const pako = require('pako');
31
31
 
@@ -119,12 +119,10 @@ export class ZipCompressNode {
119
119
 
120
120
  if (hashCalculator) {
121
121
  hashCalculator.update(data);
122
- if (!entry.crc || entry.crc === 0) {
123
- entry.crc = hashCalculator.finalizeCRC32();
124
- }
125
- if (options?.useSHA256 && !entry.sha256) {
126
- entry.sha256 = hashCalculator.finalizeSHA256();
127
- }
122
+ hashCalculator.applyToEntry(entry, {
123
+ setCrc: !entry.crc || entry.crc === 0,
124
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
125
+ });
128
126
  }
129
127
 
130
128
  // Compress based on method
@@ -148,10 +146,11 @@ export class ZipCompressNode {
148
146
  const enc = options.encryptionMethod;
149
147
  if (enc === 'zipcrypto') {
150
148
  buffer = this.encryptCompressedData(buffer, entry, options.password);
151
- } else if (enc === 'neo-aes256') {
152
- buffer = this.encryptCompressedDataNeo(buffer, entry, options.password);
153
- } else {
149
+ } else if (enc === 'aes256') {
154
150
  buffer = this.encryptCompressedDataAes(buffer, entry, options.password);
151
+ } else {
152
+ // Default / 'neo-aes256': NeoEncrypt (0x024E)
153
+ buffer = this.encryptCompressedDataNeo(buffer, entry, options.password);
155
154
  }
156
155
  }
157
156
 
@@ -187,12 +186,10 @@ export class ZipCompressNode {
187
186
  // For buffer, calculate hashes if needed
188
187
  if (hashCalculator && entry) {
189
188
  hashCalculator.update(data);
190
- if (!entry.crc || entry.crc === 0) {
191
- entry.crc = hashCalculator.finalizeCRC32();
192
- }
193
- if (options?.useSHA256 && !entry.sha256) {
194
- entry.sha256 = hashCalculator.finalizeSHA256();
195
- }
189
+ hashCalculator.applyToEntry(entry, {
190
+ setCrc: !entry.crc || entry.crc === 0,
191
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
192
+ });
196
193
  }
197
194
  return data;
198
195
  } else {
@@ -219,12 +216,10 @@ export class ZipCompressNode {
219
216
 
220
217
  // Finalize hashes
221
218
  if (hashCalculator && entry) {
222
- if (!entry.crc || entry.crc === 0) {
223
- entry.crc = hashCalculator.finalizeCRC32();
224
- }
225
- if (options?.useSHA256 && !entry.sha256) {
226
- entry.sha256 = hashCalculator.finalizeSHA256();
227
- }
219
+ hashCalculator.applyToEntry(entry, {
220
+ setCrc: !entry.crc || entry.crc === 0,
221
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
222
+ });
228
223
  }
229
224
 
230
225
  return Buffer.alloc(0);
@@ -245,10 +240,10 @@ export class ZipCompressNode {
245
240
  if (hashCalculator && entry) {
246
241
  hashCalculator.update(data);
247
242
  // Always set CRC if hash calculator was used (it calculated the correct CRC)
248
- entry.crc = hashCalculator.finalizeCRC32();
249
- if (options?.useSHA256 && !entry.sha256) {
250
- entry.sha256 = hashCalculator.finalizeSHA256();
251
- }
243
+ hashCalculator.applyToEntry(entry, {
244
+ setCrc: true,
245
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
246
+ });
252
247
  }
253
248
 
254
249
  const result = pako.deflateRaw(data, { level: level ?? 6 });
@@ -298,10 +293,10 @@ export class ZipCompressNode {
298
293
  // Finalize hashes after all chunks processed and compressed
299
294
  if (hashCalculator && entry) {
300
295
  // Always set CRC if hash calculator was used (it calculated the correct CRC)
301
- entry.crc = hashCalculator.finalizeCRC32();
302
- if (options?.useSHA256 && !entry.sha256) {
303
- entry.sha256 = hashCalculator.finalizeSHA256();
304
- }
296
+ hashCalculator.applyToEntry(entry, {
297
+ setCrc: true,
298
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
299
+ });
305
300
  }
306
301
 
307
302
  // For chunked processing, return empty buffer (data already written via onOutputBuffer)
@@ -339,16 +334,23 @@ export class ZipCompressNode {
339
334
  }
340
335
 
341
336
  /**
342
- * Compresses data using Zstandard (zstd) algorithm
343
- * @param input - Buffer to compress OR chunked reader object with totalSize and readChunk callback
337
+ * Compresses data using Zstandard via Node native zlib chunked streaming.
338
+ *
339
+ * @param input - Buffer OR chunked reader (`onReadChunk` preferred; `readChunk` accepted)
344
340
  * @param options - Compression options
345
- * @param bufferSize - Size of buffer to read if using chunked reader (default: 512KB)
346
- * @param entry - Optional ZIP entry for hash calculation
347
- * @param onOutputBuffer - Optional callback for streaming output
348
- * @returns Compressed data buffer
341
+ * @param bufferSize - Chunk size for streaming reads (default: 512KB)
342
+ * @param entry - Optional ZIP entry for hash / compressedSize
343
+ * @param onOutputBuffer - Optional callback for streaming compressed output
344
+ * @returns Compressed buffer, or empty buffer when streamed via onOutputBuffer
349
345
  */
350
346
  async zstdCompress(
351
- input: Buffer | { totalSize: number, readChunk: (position: number, size: number) => Buffer },
347
+ input:
348
+ | Buffer
349
+ | {
350
+ totalSize: number;
351
+ onReadChunk?: (position: number, size: number) => Buffer;
352
+ readChunk?: (position: number, size: number) => Buffer;
353
+ },
352
354
  options?: CompressOptions,
353
355
  bufferSize?: number,
354
356
  entry?: ZipEntry,
@@ -357,66 +359,108 @@ export class ZipCompressNode {
357
359
  const effectiveBufferSize = bufferSize || options?.bufferSize || this.zipkitNode.getBufferSize();
358
360
  const isBuffer = Buffer.isBuffer(input);
359
361
  const totalSize = isBuffer ? input.length : input.totalSize;
360
-
362
+
363
+ const needsHashCalculation =
364
+ entry && ((!entry.crc || entry.crc === 0) || (options?.useSHA256 && !entry.sha256));
365
+ const hashCalculator = needsHashCalculation
366
+ ? new HashCalculator({ useSHA256: !!(options?.useSHA256 && entry && !entry.sha256) })
367
+ : null;
368
+
361
369
  if (options?.level == 0) {
362
- // For store mode, return as-is
363
370
  if (isBuffer) {
371
+ if (hashCalculator && entry) {
372
+ hashCalculator.update(input);
373
+ hashCalculator.applyToEntry(entry, {
374
+ setCrc: !entry.crc || entry.crc === 0,
375
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
376
+ });
377
+ }
364
378
  return input;
365
- } else {
366
- // Read all chunks
367
- const chunks: Buffer[] = [];
368
- let position = 0;
369
- while (position < totalSize) {
370
- const size = Math.min(effectiveBufferSize, totalSize - position);
371
- const chunk = input.readChunk(position, size);
379
+ }
380
+
381
+ const chunks: Buffer[] = [];
382
+ const read = input.onReadChunk ?? input.readChunk;
383
+ if (!read) {
384
+ throw new Error('ZSTD store mode: chunked reader missing read callback');
385
+ }
386
+ let position = 0;
387
+ while (position < totalSize) {
388
+ const size = Math.min(effectiveBufferSize, totalSize - position);
389
+ const chunk = read(position, size);
390
+ if (hashCalculator) {
391
+ hashCalculator.update(chunk);
392
+ }
393
+ if (onOutputBuffer) {
394
+ await onOutputBuffer(chunk);
395
+ } else {
372
396
  chunks.push(chunk);
373
- position += size;
374
397
  }
375
- return Buffer.concat(chunks);
398
+ position += chunk.length;
399
+ }
400
+ if (hashCalculator && entry) {
401
+ hashCalculator.applyToEntry(entry, {
402
+ setCrc: !entry.crc || entry.crc === 0,
403
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
404
+ });
376
405
  }
406
+ return onOutputBuffer ? Buffer.alloc(0) : Buffer.concat(chunks);
377
407
  }
378
-
408
+
379
409
  try {
380
- // Zstd compression levels range from 1 (fastest) to 22 (highest compression)
381
- // Map our 1-9 level to a reasonable zstd range (1-19)
382
410
  const level = options?.level ?? 6;
383
- const zstdLevel = Math.min(Math.max(1, Math.floor(level * 2.1)), 19);
384
411
 
385
- // Get the full buffer (zstd doesn't support true streaming compression)
386
- const inbuf = isBuffer ? input : (() => {
387
- const chunks: Buffer[] = [];
388
- let position = 0;
389
- while (position < totalSize) {
390
- const size = Math.min(effectiveBufferSize, totalSize - position);
391
- const chunk = input.readChunk(position, size);
392
- chunks.push(chunk);
393
- position += size;
394
- }
395
- return Buffer.concat(chunks);
396
- })();
397
-
398
- // Validate input
399
- if (!inbuf || inbuf.length === 0) {
412
+ const read = isBuffer
413
+ ? (position: number, size: number) => input.subarray(position, position + size)
414
+ : (input.onReadChunk ?? input.readChunk);
415
+ if (!read) {
416
+ throw new Error('ZSTD compression: chunked reader missing read callback');
417
+ }
418
+ if (!totalSize) {
400
419
  throw new Error('ZSTD compression: empty input buffer');
401
420
  }
402
-
403
- // Convert Buffer to Uint8Array for WASM module
404
- const inputArray = new Uint8Array(inbuf.buffer, inbuf.byteOffset, inbuf.byteLength);
405
-
406
- // Compress the data with zstd using global ZstdManager
407
- const compressedData = await ZstdManager.compress(inputArray, zstdLevel);
408
- const compressedBuffer = Buffer.from(compressedData);
409
-
410
- // Set the compressed size in the entry for ZIP file structure
421
+
422
+ if (isBuffer && hashCalculator && entry) {
423
+ hashCalculator.update(input);
424
+ hashCalculator.applyToEntry(entry, {
425
+ setCrc: true,
426
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
427
+ });
428
+ }
429
+
430
+ const collected: Buffer[] = [];
431
+ const compressedSize = await ZstdNode.compressChunks(
432
+ read,
433
+ totalSize,
434
+ effectiveBufferSize,
435
+ level,
436
+ isBuffer
437
+ ? undefined
438
+ : (plain) => {
439
+ hashCalculator?.update(plain);
440
+ },
441
+ async (compressed) => {
442
+ if (onOutputBuffer) {
443
+ await onOutputBuffer(compressed);
444
+ } else {
445
+ collected.push(compressed);
446
+ }
447
+ }
448
+ );
449
+
450
+ if (!isBuffer && hashCalculator && entry) {
451
+ hashCalculator.applyToEntry(entry, {
452
+ setCrc: true,
453
+ setSha256: !!(options?.useSHA256 && !entry.sha256),
454
+ });
455
+ }
411
456
  if (entry) {
412
- entry.compressedSize = compressedBuffer.length;
457
+ entry.compressedSize = compressedSize;
413
458
  }
414
-
459
+
415
460
  if (onOutputBuffer) {
416
- await onOutputBuffer(compressedBuffer);
461
+ return Buffer.alloc(0);
417
462
  }
418
-
419
- return compressedBuffer;
463
+ return Buffer.concat(collected);
420
464
  } catch (e) {
421
465
  Logger.error('Error during zstd compression:', e);
422
466
  throw new Error(Errors.COMPRESSION_ERROR);
@@ -591,9 +635,8 @@ export class ZipCompressNode {
591
635
  const fileData = fs.readFileSync(filePath);
592
636
  return await this.compressData(entry, fileData, options, onOutputBuffer);
593
637
  } else if (compressionMethod === 'ZSTD') {
594
- // ZSTD requires full buffer, so read file first
595
- const fileData = fs.readFileSync(filePath);
596
- return await this.compressData(entry, fileData, options, onOutputBuffer);
638
+ // Native Node zstd chunked streaming
639
+ return await this.zstdCompress(chunkedReader, options, bufferSize, entry, onOutputBuffer);
597
640
  } else {
598
641
  // DEFLATED: Use deflateCompress with chunked reader
599
642
  return await this.deflateCompress(chunkedReader, options, bufferSize, entry, onOutputBuffer);
@@ -14,8 +14,9 @@
14
14
  //
15
15
 
16
16
  const pako = require('pako');
17
- import { ZstdManager } from '../core/ZstdManager';
18
17
  import ZipkitNode from './ZipkitNode';
18
+ import { ZstdNode } from './ZstdNode';
19
+ import { repairLegacyWasmZstdPlaintext } from './LegacyZstd';
19
20
  import { Logger } from '../core/components/Logger';
20
21
  import ZipEntry from '../core/ZipEntry';
21
22
  import Errors from '../core/constants/Errors';
@@ -125,6 +126,18 @@ export class ZipDecompressNode {
125
126
  return await this.extractToBufferInternal(fileHandle, entry, options);
126
127
  }
127
128
 
129
+ /**
130
+ * Read the raw compressed payload for an entry (no decrypt / decompress).
131
+ */
132
+ async readCompressedPayload(entry: ZipEntry): Promise<Buffer> {
133
+ const fileHandle = (this.zipkitNode as any).getFileHandle();
134
+ const chunks: Buffer[] = [];
135
+ for await (const chunk of this.readCompressedDataStream(fileHandle, entry)) {
136
+ chunks.push(chunk);
137
+ }
138
+ return Buffer.concat(chunks);
139
+ }
140
+
128
141
  /**
129
142
  * Test entry integrity without extracting to disk
130
143
  * Validates CRC-32 or SHA-256 hash without writing decompressed data
@@ -228,19 +241,15 @@ export class ZipDecompressNode {
228
241
  entry.aesVersion > 0 || entry.cmpMethod === CMP_METHOD.AES_ENCRYPT || entry.neoCryptoAlgorithm > 0;
229
242
 
230
243
  if (isEncrypted && isFullPayloadCrypto) {
231
- // WinZip AES or NeoEncrypt: read full payload, decrypt in one shot (HMAC must be verified first)
244
+ // WinZip AES / NeoEncrypt: stream decrypt (HMAC verified at end of ciphertext)
232
245
  const dataStream = this.readCompressedDataStream(fileHandle, entry);
233
- const chunks: Buffer[] = [];
234
- for await (const chunk of dataStream) {
235
- chunks.push(chunk);
236
- }
237
- const fullPayload = Buffer.concat(chunks);
238
- const decrypted = AesCrypto.decryptBuffer(entry, fullPayload, (this.zipkitNode as any).password);
239
-
240
- // Wrap decrypted data as a single-item generator for the decompression pipeline
246
+ const decrypted = AesCrypto.decryptStream(
247
+ (this.zipkitNode as any).password,
248
+ entry.compressedSize,
249
+ dataStream
250
+ );
241
251
  const skipHash = entry.aesVersion === 2 ? true : options?.skipHashCheck;
242
- async function* singleChunk() { yield decrypted; }
243
- await this.unCompressToFile(singleChunk(), entry, writeStream, {
252
+ await this.unCompressToFile(decrypted, entry, writeStream, {
244
253
  skipHashCheck: skipHash,
245
254
  onProgress: options?.onProgress,
246
255
  outputPath
@@ -297,16 +306,13 @@ export class ZipDecompressNode {
297
306
 
298
307
  if (isEncrypted && isFullPayloadCrypto) {
299
308
  const dataStream = this.readCompressedDataStream(fileHandle, entry);
300
- const chunks: Buffer[] = [];
301
- for await (const chunk of dataStream) {
302
- chunks.push(chunk);
303
- }
304
- const fullPayload = Buffer.concat(chunks);
305
- const decrypted = AesCrypto.decryptBuffer(entry, fullPayload, (this.zipkitNode as any).password);
306
-
309
+ const decrypted = AesCrypto.decryptStream(
310
+ (this.zipkitNode as any).password,
311
+ entry.compressedSize,
312
+ dataStream
313
+ );
307
314
  const skipHash = entry.aesVersion === 2 ? true : options?.skipHashCheck;
308
- async function* singleChunk() { yield decrypted; }
309
- return await this.unCompressToBuffer(singleChunk(), entry, {
315
+ return await this.unCompressToBuffer(decrypted, entry, {
310
316
  skipHashCheck: skipHash,
311
317
  onProgress: options?.onProgress
312
318
  });
@@ -357,16 +363,13 @@ export class ZipDecompressNode {
357
363
 
358
364
  if (isEncrypted && isFullPayloadCrypto) {
359
365
  const dataStream = this.readCompressedDataStream(fileHandle, entry);
360
- const chunks: Buffer[] = [];
361
- for await (const chunk of dataStream) {
362
- chunks.push(chunk);
363
- }
364
- const fullPayload = Buffer.concat(chunks);
365
- const decrypted = AesCrypto.decryptBuffer(entry, fullPayload, (this.zipkitNode as any).password);
366
-
366
+ const decrypted = AesCrypto.decryptStream(
367
+ (this.zipkitNode as any).password,
368
+ entry.compressedSize,
369
+ dataStream
370
+ );
367
371
  const skipHash = entry.aesVersion === 2 ? true : options?.skipHashCheck;
368
- async function* singleChunk() { yield decrypted; }
369
- return await this.unCompressToTest(singleChunk(), entry, {
372
+ return await this.unCompressToTest(decrypted, entry, {
370
373
  skipHashCheck: skipHash,
371
374
  onProgress: options?.onProgress
372
375
  });
@@ -448,6 +451,8 @@ export class ZipDecompressNode {
448
451
  if (!options?.skipHashCheck) {
449
452
  if (entry.sha256) {
450
453
  const calculatedHash = hashCalc.finalizeSHA256();
454
+ const leaf = hashCalc.finalizeMerkleLeafV1();
455
+ if (leaf) entry.merkleLeafV1 = leaf;
451
456
  this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
452
457
  if (calculatedHash !== entry.sha256) {
453
458
  if (options?.outputPath && fs) {
@@ -537,6 +542,8 @@ export class ZipDecompressNode {
537
542
  if (!options?.skipHashCheck) {
538
543
  if (entry.sha256) {
539
544
  const calculatedHash = hashCalc.finalizeSHA256();
545
+ const leaf = hashCalc.finalizeMerkleLeafV1();
546
+ if (leaf) entry.merkleLeafV1 = leaf;
540
547
  this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
541
548
  if (calculatedHash !== entry.sha256) {
542
549
  throw new Error(Errors.INVALID_SHA256);
@@ -604,6 +611,8 @@ export class ZipDecompressNode {
604
611
  if (!options?.skipHashCheck) {
605
612
  if (entry.sha256) {
606
613
  const calculatedHash = hashCalc.finalizeSHA256();
614
+ const leaf = hashCalc.finalizeMerkleLeafV1();
615
+ if (leaf) entry.merkleLeafV1 = leaf;
607
616
  this.log(`SHA-256 comparison: calculated=${calculatedHash}, stored=${entry.sha256}`);
608
617
  if (calculatedHash !== entry.sha256) {
609
618
  throw new Error(Errors.INVALID_SHA256);
@@ -622,7 +631,13 @@ export class ZipDecompressNode {
622
631
  return { verifiedHash: undefined };
623
632
  }
624
633
  } else {
625
- // Hash check skipped - return undefined
634
+ // Hash check skipped — still finalize leaf/digest once from this stream when SHA was enabled
635
+ if (entry.sha256 && hashCalc) {
636
+ const calculatedHash = hashCalc.finalizeSHA256();
637
+ const leaf = hashCalc.finalizeMerkleLeafV1();
638
+ if (leaf) entry.merkleLeafV1 = leaf;
639
+ return { verifiedHash: calculatedHash || undefined };
640
+ }
626
641
  return { verifiedHash: undefined };
627
642
  }
628
643
  } catch (error) {
@@ -662,8 +677,8 @@ export class ZipDecompressNode {
662
677
  // Use pako streaming inflate - maintains state across chunks
663
678
  yield* this.inflateStream(compressedStream);
664
679
  } else if (method === CMP_METHOD.ZSTD) {
665
- // Use ZSTD streaming decompression - note: ZSTD requires full buffer
666
- yield* this.zstdDecompressStream(compressedStream);
680
+ // Native Node zstd; bound output to entry size (repairs legacy WASM +18 padding)
681
+ yield* this.zstdDecompressStream(compressedStream, entry?.uncompressedSize ?? 0);
667
682
  } else {
668
683
  throw new Error(`Unsupported compression method: ${method}`);
669
684
  }
@@ -707,36 +722,32 @@ export class ZipDecompressNode {
707
722
  }
708
723
 
709
724
  /**
710
- * Streaming ZSTD decompression
725
+ * Streaming ZSTD decompression via Node zlib (native).
726
+ * When maxOut > 0, truncates output (legacy WASM emitted +18 zero bytes).
711
727
  */
712
728
  private async *zstdDecompressStream(
713
- compressedStream: AsyncGenerator<Buffer>
729
+ compressedStream: AsyncGenerator<Buffer>,
730
+ maxOut: number = 0
714
731
  ): AsyncGenerator<Buffer> {
715
- // ZSTD is guaranteed to be initialized via factory method
716
-
717
- // Collect all compressed chunks first (ZSTD needs complete data)
718
- const compressedChunks: Buffer[] = [];
719
- for await (const chunk of compressedStream) {
720
- compressedChunks.push(chunk);
721
- }
722
-
723
- const compressedData = Buffer.concat(compressedChunks);
724
-
725
732
  try {
726
- // Use global ZstdManager for decompression
727
- const decompressed = await ZstdManager.decompress(compressedData);
728
- const decompressedBuffer = Buffer.from(decompressed);
729
-
730
- // Yield decompressed data in chunks using ZipkitServer's bufferSize
731
- const chunkSize = this.zipkitNode.getBufferSize();
732
- let offset = 0;
733
- while (offset < decompressedBuffer.length) {
734
- const end = Math.min(offset + chunkSize, decompressedBuffer.length);
735
- yield decompressedBuffer.slice(offset, end);
736
- offset = end;
733
+ let remaining = maxOut > 0 ? maxOut : Number.POSITIVE_INFINITY;
734
+ for await (const chunk of ZstdNode.decompressStream(compressedStream)) {
735
+ if (remaining <= 0) {
736
+ break;
737
+ }
738
+ if (chunk.length <= remaining) {
739
+ remaining -= chunk.length;
740
+ yield chunk;
741
+ } else {
742
+ yield chunk.subarray(0, remaining);
743
+ remaining = 0;
744
+ break;
745
+ }
737
746
  }
738
747
  } catch (error) {
739
- throw new Error(`ZSTD streaming decompression failed: ${error instanceof Error ? error.message : String(error)}`);
748
+ throw new Error(
749
+ `ZSTD streaming decompression failed: ${error instanceof Error ? error.message : String(error)}`
750
+ );
740
751
  }
741
752
  }
742
753
 
@@ -750,20 +761,35 @@ export class ZipDecompressNode {
750
761
  }
751
762
 
752
763
  /**
753
- * Zstd decompress method (now async with ZstdManager)
754
- * Internal method only
764
+ * Zstd decompress via Node zlib (native), repairing legacy WASM +18 padding when present.
755
765
  */
756
- private async zstdDecompressSync(data: Buffer): Promise<Buffer> {
766
+ private async zstdDecompressSync(data: Buffer, entry?: ZipEntry): Promise<Buffer> {
757
767
  this.log(`zstdDecompressSync() called with ${data.length} bytes`);
758
-
768
+
759
769
  try {
760
- // Use global ZstdManager for decompression
761
- const decompressed = await ZstdManager.decompress(data);
762
- this.log(`ZSTD decompression successful: ${data.length} bytes -> ${decompressed.length} bytes`);
763
- return Buffer.from(decompressed);
770
+ let decompressed = await ZstdNode.decompress(data);
771
+ if (entry && entry.uncompressedSize > 0) {
772
+ const { repaired, wasLegacy } = repairLegacyWasmZstdPlaintext(
773
+ decompressed,
774
+ entry.uncompressedSize,
775
+ entry.crc
776
+ );
777
+ if (wasLegacy) {
778
+ this.log(
779
+ `Legacy WASM zstd padding repaired: ${decompressed.length} -> ${repaired.length} bytes`
780
+ );
781
+ }
782
+ decompressed = repaired;
783
+ }
784
+ this.log(
785
+ `ZSTD decompression successful: ${data.length} bytes -> ${decompressed.length} bytes`
786
+ );
787
+ return decompressed;
764
788
  } catch (error) {
765
789
  this.log(`ZSTD decompression failed: ${error}`);
766
- throw new Error(`ZSTD decompression failed: ${error instanceof Error ? error.message : String(error)}`);
790
+ throw new Error(
791
+ `ZSTD decompression failed: ${error instanceof Error ? error.message : String(error)}`
792
+ );
767
793
  }
768
794
  }
769
795
 
@@ -794,7 +820,7 @@ export class ZipDecompressNode {
794
820
  } else if (method === CMP_METHOD.DEFLATED) {
795
821
  outBuf = this.inflate(compressedData);
796
822
  } else if (method === CMP_METHOD.ZSTD) {
797
- outBuf = await this.zstdDecompressSync(compressedData);
823
+ outBuf = await this.zstdDecompressSync(compressedData, entry);
798
824
  } else {
799
825
  throw new Error(`Unsupported compression method: ${method}`);
800
826
  }