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.
- package/NEOZIP_APPNOTE.md +527 -0
- package/README.md +57 -21
- package/WHATS_NEW.md +146 -0
- package/dist/browser/ZipkitBrowser.d.ts.map +1 -1
- package/dist/browser/ZipkitBrowser.js +4 -6
- package/dist/browser/ZipkitBrowser.js.map +1 -1
- package/dist/browser/zstd-browser-shim.d.ts +28 -0
- package/dist/browser/zstd-browser-shim.d.ts.map +1 -0
- package/dist/browser/zstd-browser-shim.js +66 -0
- package/dist/browser/zstd-browser-shim.js.map +1 -0
- package/dist/browser-esm/index.js +2110 -41755
- package/dist/browser-esm/index.js.map +4 -4
- package/dist/browser-umd/index.js +2110 -41755
- package/dist/browser-umd/index.js.map +4 -4
- package/dist/browser-umd/index.min.js +3 -21
- package/dist/core/ZipCompress.d.ts +2 -7
- package/dist/core/ZipCompress.d.ts.map +1 -1
- package/dist/core/ZipCompress.js +57 -33
- package/dist/core/ZipCompress.js.map +1 -1
- package/dist/core/ZipDecompress.d.ts +1 -3
- package/dist/core/ZipDecompress.d.ts.map +1 -1
- package/dist/core/ZipDecompress.js +27 -17
- package/dist/core/ZipDecompress.js.map +1 -1
- package/dist/core/ZipEntry.d.ts +6 -0
- package/dist/core/ZipEntry.d.ts.map +1 -1
- package/dist/core/ZipEntry.js +7 -1
- package/dist/core/ZipEntry.js.map +1 -1
- package/dist/core/Zipkit.d.ts +43 -11
- package/dist/core/Zipkit.d.ts.map +1 -1
- package/dist/core/Zipkit.js +106 -63
- package/dist/core/Zipkit.js.map +1 -1
- package/dist/core/components/HashCalculator.d.ts +22 -1
- package/dist/core/components/HashCalculator.d.ts.map +1 -1
- package/dist/core/components/HashCalculator.js +49 -6
- package/dist/core/components/HashCalculator.js.map +1 -1
- package/dist/core/components/Util.d.ts +3 -4
- package/dist/core/components/Util.d.ts.map +1 -1
- package/dist/core/components/Util.js +10 -8
- package/dist/core/components/Util.js.map +1 -1
- package/dist/core/constants/Headers.d.ts +1 -3
- package/dist/core/constants/Headers.d.ts.map +1 -1
- package/dist/core/constants/Headers.js +14 -5
- package/dist/core/constants/Headers.js.map +1 -1
- package/dist/core/constants/MetaPaths.d.ts +38 -0
- package/dist/core/constants/MetaPaths.d.ts.map +1 -0
- package/dist/core/constants/MetaPaths.js +79 -0
- package/dist/core/constants/MetaPaths.js.map +1 -0
- package/dist/core/encryption/AesCrypto.d.ts +55 -11
- package/dist/core/encryption/AesCrypto.d.ts.map +1 -1
- package/dist/core/encryption/AesCrypto.js +223 -68
- package/dist/core/encryption/AesCrypto.js.map +1 -1
- package/dist/core/encryption/index.d.ts +1 -1
- package/dist/core/encryption/index.d.ts.map +1 -1
- package/dist/core/encryption/index.js +3 -1
- package/dist/core/encryption/index.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +14 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/merkle/MerkleRoot.d.ts +109 -0
- package/dist/core/merkle/MerkleRoot.d.ts.map +1 -0
- package/dist/core/merkle/MerkleRoot.js +275 -0
- package/dist/core/merkle/MerkleRoot.js.map +1 -0
- package/dist/core/version.js +1 -1
- package/dist/node/LegacyZstd.d.ts +67 -0
- package/dist/node/LegacyZstd.d.ts.map +1 -0
- package/dist/node/LegacyZstd.js +209 -0
- package/dist/node/LegacyZstd.js.map +1 -0
- package/dist/node/ZipCompressNode.d.ts +9 -7
- package/dist/node/ZipCompressNode.d.ts.map +1 -1
- package/dist/node/ZipCompressNode.js +105 -75
- package/dist/node/ZipCompressNode.js.map +1 -1
- package/dist/node/ZipDecompressNode.d.ts +7 -3
- package/dist/node/ZipDecompressNode.d.ts.map +1 -1
- package/dist/node/ZipDecompressNode.js +68 -56
- package/dist/node/ZipDecompressNode.js.map +1 -1
- package/dist/node/ZipkitNode.d.ts +23 -0
- package/dist/node/ZipkitNode.d.ts.map +1 -1
- package/dist/node/ZipkitNode.js +195 -41
- package/dist/node/ZipkitNode.js.map +1 -1
- package/dist/node/ZstdNode.d.ts +39 -0
- package/dist/node/ZstdNode.d.ts.map +1 -0
- package/dist/node/ZstdNode.js +171 -0
- package/dist/node/ZstdNode.js.map +1 -0
- package/dist/node/index.d.ts +3 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +11 -1
- package/dist/node/index.js.map +1 -1
- package/package.json +4 -3
- package/src/browser/ZipkitBrowser.ts +4 -6
- package/src/browser/zstd-browser-shim.ts +76 -0
- package/src/core/ZipCompress.ts +71 -42
- package/src/core/ZipDecompress.ts +35 -18
- package/src/core/ZipEntry.ts +8 -1
- package/src/core/Zipkit.ts +149 -76
- package/src/core/components/HashCalculator.ts +58 -6
- package/src/core/components/Util.ts +16 -7
- package/src/core/constants/Headers.ts +14 -4
- package/src/core/constants/MetaPaths.ts +86 -0
- package/src/core/encryption/AesCrypto.ts +265 -86
- package/src/core/encryption/index.ts +1 -1
- package/src/core/index.ts +14 -0
- package/src/core/merkle/MerkleRoot.ts +312 -0
- package/src/core/version.ts +1 -1
- package/src/node/LegacyZstd.ts +270 -0
- package/src/node/ZipCompressNode.ts +127 -84
- package/src/node/ZipDecompressNode.ts +93 -67
- package/src/node/ZipkitNode.ts +230 -42
- package/src/node/ZstdNode.ts +155 -0
- package/src/node/index.ts +10 -0
- package/dist/core/ZstdManager.d.ts +0 -56
- package/dist/core/ZstdManager.d.ts.map +0 -1
- package/dist/core/ZstdManager.js +0 -144
- package/dist/core/ZstdManager.js.map +0 -1
- 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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 === '
|
|
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
|
-
|
|
191
|
-
entry.crc
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
223
|
-
entry.crc
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
|
343
|
-
*
|
|
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 -
|
|
346
|
-
* @param entry - Optional ZIP entry for hash
|
|
347
|
-
* @param onOutputBuffer - Optional callback for streaming output
|
|
348
|
-
* @returns Compressed
|
|
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:
|
|
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
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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 =
|
|
457
|
+
entry.compressedSize = compressedSize;
|
|
413
458
|
}
|
|
414
|
-
|
|
459
|
+
|
|
415
460
|
if (onOutputBuffer) {
|
|
416
|
-
|
|
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
|
-
//
|
|
595
|
-
|
|
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
|
|
244
|
+
// WinZip AES / NeoEncrypt: stream decrypt (HMAC verified at end of ciphertext)
|
|
232
245
|
const dataStream = this.readCompressedDataStream(fileHandle, entry);
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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(
|
|
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
|
}
|