@zip.js/zip.js 2.14.1 → 2.15.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 (40) hide show
  1. package/BENCHMARKS.md +19 -11
  2. package/deno.json +1 -1
  3. package/dist/zip-core-external.js +82 -41
  4. package/dist/zip-core-external.min.js +1 -1
  5. package/dist/zip-core.js +82 -41
  6. package/dist/zip-core.min.js +1 -1
  7. package/dist/zip-fs-core-external.js +82 -41
  8. package/dist/zip-fs-core-external.min.js +1 -1
  9. package/dist/zip-fs-core.js +82 -41
  10. package/dist/zip-fs-core.min.js +1 -1
  11. package/dist/zip-fs-external.js +82 -41
  12. package/dist/zip-fs-external.min.js +1 -1
  13. package/dist/zip-fs-native.js +84 -43
  14. package/dist/zip-fs-native.min.js +1 -1
  15. package/dist/zip-fs.js +84 -43
  16. package/dist/zip-fs.min.js +1 -1
  17. package/dist/zip-legacy.js +84 -43
  18. package/dist/zip-legacy.min.js +1 -1
  19. package/dist/zip-native.js +84 -43
  20. package/dist/zip-native.min.js +1 -1
  21. package/dist/zip-web-worker-native.js +1 -1
  22. package/dist/zip-web-worker.js +1 -1
  23. package/dist/zip.js +84 -43
  24. package/dist/zip.min.js +1 -1
  25. package/index-native.cjs +84 -43
  26. package/index-native.min.js +1 -1
  27. package/index.cjs +84 -43
  28. package/index.d.cts +20 -13
  29. package/index.d.ts +20 -13
  30. package/index.min.js +1 -1
  31. package/lib/core/codec-pool.js +35 -19
  32. package/lib/core/codec-worker.js +4 -2
  33. package/lib/core/streams/zip-entry-stream.js +18 -10
  34. package/lib/core/util/decode-text.js +15 -1
  35. package/lib/core/version.js +1 -1
  36. package/lib/core/web-worker-inline-native.js +1 -1
  37. package/lib/core/web-worker-inline-wasm.js +1 -1
  38. package/lib/core/zip-reader.js +12 -10
  39. package/lib/core/zlib-streams-inline.js +1 -1
  40. package/package.json +1 -1
package/BENCHMARKS.md CHANGED
@@ -39,8 +39,11 @@ The page answers four questions, each from one or two scripts of the harness:
39
39
  about 2 KB each, and the 8 × 8 MB and 256 MB workloads are text.
40
40
  - **Isolation.** In `bench.js` and `bench-backends.js`, each (library, operation, workload)
41
41
  combination runs in its own freshly spawned Node process under macOS's `/usr/bin/time -l`, so
42
- the libraries never share a heap and peak memory is per library. The codec, encryption and
43
- runtime scripts run in-process, with one warmup run before the timed ones.
42
+ the libraries never share a heap and peak memory is per library. Every row of those two
43
+ scripts includes what a fresh process pays on its first operation: the JIT warm-up of each
44
+ library and, for the zip.js WebAssembly rows, the module instantiation, about 20 ms. The
45
+ codec, encryption and runtime scripts run in-process, with one warmup run before the timed
46
+ ones.
44
47
  - **Timing.** `performance.now()` around the measured operation. Each combination runs 3 times
45
48
  (5 in the encryption script) and the tables report the median. Differences of a few percent
46
49
  are within run-to-run noise. A bold cell is the best number among the alternatives it is
@@ -59,6 +62,9 @@ The page answers four questions, each from one or two scripts of the harness:
59
62
  where they exist; the bundled WebAssembly zlib; and the pure-JavaScript zlib port. The last two
60
63
  ship with the library, like the codecs of jszip and fflate, so those rows compare the
61
64
  libraries without the host's zlib in the picture.
65
+ - **Checks.** No decompression row verifies the CRC-32 of the entries: zip.js and jszip leave
66
+ the check off by default, and fflate has none on read. Turning it on in zip.js adds one pass
67
+ over the output, about 15 ms per 20 MB.
62
68
  - **Units.** MB in the tables is 10^6 bytes. The workload sizes (20 MB, 8 MB, 256 MB) and the
63
69
  MB/s throughputs use 2^20 bytes.
64
70
 
@@ -126,15 +132,17 @@ Peak memory for the same runs (Δ over baseline):
126
132
  | fflate | fflate (JavaScript) | **92 MB** | **120 MB** |
127
133
 
128
134
  On the 20 MB stream `DecompressionStream` takes 66 ms and the WebAssembly inflate 78 ms, then
129
- fflate 116 ms, jszip 137 ms and the pure-JavaScript port 180 ms. On 5,000 small files the order
130
- reverses: fflate takes 102 ms, jszip 444 ms and the three zip.js rows 476 to 628 ms, with
131
- `DecompressionStream` the slowest of them, the same per-entry cost as in compression. fflate
132
- uses the least memory on both workloads, 34 % of the `DecompressionStream` row on the small
133
- files, and the WebAssembly and pure-JavaScript rows peak higher than `DecompressionStream`. The
134
- three zip.js rows allocate the same 800 MB of stream objects over the 5,000 entries, about
135
- 160 KB per entry. The WebAssembly and pure-JavaScript codecs run on the JavaScript thread, so
136
- V8's incremental marking keeps more of that garbage alive between collections, and that is the
137
- difference between the rows.
135
+ fflate 116 ms, jszip 137 ms and the pure-JavaScript port 180 ms. The WebAssembly row includes
136
+ the module instantiation a fresh process pays once, about 20 ms. Warm, the two inflates are
137
+ within 6 % of each other in the [Codecs](#codecs-at-equal-output-size) table. On 5,000 small
138
+ files the order reverses: fflate takes 102 ms, jszip 444 ms and the three zip.js rows 476 to
139
+ 628 ms, with `DecompressionStream` the slowest of them, the same per-entry cost as in
140
+ compression. fflate uses the least memory on both workloads, 34 % of the `DecompressionStream`
141
+ row on the small files, and the WebAssembly and pure-JavaScript rows peak higher than
142
+ `DecompressionStream`. The three zip.js rows allocate the same 800 MB of stream objects over the
143
+ 5,000 entries, about 160 KB per entry. The WebAssembly and pure-JavaScript codecs run on the
144
+ JavaScript thread, so V8's incremental marking keeps more of that garbage alive between
145
+ collections, and that is the difference between the rows.
138
146
 
139
147
  ### Streaming a 256 MB file, disk to disk
140
148
 
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zip-js/zip-js",
3
- "version": "2.14.1",
3
+ "version": "2.15.0",
4
4
  "exports": {
5
5
  ".": "./index.js",
6
6
  "./external": "./lib/zip-fs-external.js",
@@ -1900,8 +1900,8 @@ class DeflateStream extends TransformStream {
1900
1900
  let crc32Stream, encryptionStream, gzipCrc32Stream;
1901
1901
  let readable = super.readable;
1902
1902
  const codecStreams = format && getCodecStreams(format);
1903
- const useGzipCrc32 = computeCrc32 && compressed && !deflate64 && !codecStreams && (!encrypted || zipCrypto) &&
1904
- Boolean(useCompressionStream && CompressionStream);
1903
+ const GzipCompressionStream = getGzipCompressionStream(useCompressionStream, CompressionStream, CompressionStreamFallback);
1904
+ const useGzipCrc32 = computeCrc32 && compressed && !deflate64 && !codecStreams && (!encrypted || zipCrypto) && Boolean(GzipCompressionStream);
1905
1905
  if ((!encrypted || zipCrypto) && computeCrc32 && !useGzipCrc32) {
1906
1906
  crc32Stream = new Crc32Stream();
1907
1907
  readable = pipeThrough(readable, crc32Stream);
@@ -1911,7 +1911,7 @@ class DeflateStream extends TransformStream {
1911
1911
  readable = pipeThroughBackpressured(readable, createCodecStream(codecStreams.CompressionStream, format, { level, chunkSize, compressionMethod, uncompressedSize: inputSize }));
1912
1912
  } else if (useGzipCrc32) {
1913
1913
  gzipCrc32Stream = new GzipToRawDeflateStream();
1914
- readable = pipeThroughBackpressured(readable, new CompressionStream(FORMAT_GZIP$1));
1914
+ readable = pipeThroughBackpressured(readable, new GzipCompressionStream(FORMAT_GZIP$1, { level, chunkSize }));
1915
1915
  readable = pipeThrough(readable, gzipCrc32Stream);
1916
1916
  } else {
1917
1917
  try {
@@ -1985,8 +1985,7 @@ class GzipToRawDeflateStream extends TransformStream {
1985
1985
  }
1986
1986
  }
1987
1987
 
1988
- function pipeThroughGzipDecompressionStream(readable, gzipStream, outputSize) {
1989
- const crc32 = new Crc32();
1988
+ function pipeThroughGzipDecompressionStream(readable, gzipStream, outputSize, crc32) {
1990
1989
  let outputLength = 0;
1991
1990
  let inputDone = false;
1992
1991
  let watchdogTimeout;
@@ -2060,7 +2059,7 @@ class InflateStream extends TransformStream {
2060
2059
  constructor(options, { chunkSize, DecompressionStreamFallback, DecompressionStream }) {
2061
2060
  super({});
2062
2061
  const { zipCrypto, encrypted, checkCrc32, crc32, compressed, useCompressionStream, deflate64, format, compressionMethod, rawBitFlag, outputSize } = options;
2063
- let crc32Stream, decryptionStream;
2062
+ let crc32Stream, decryptionStream, gzipCrc32;
2064
2063
  let readable = super.readable;
2065
2064
  if (encrypted) {
2066
2065
  if (zipCrypto) {
@@ -2087,19 +2086,20 @@ class InflateStream extends TransformStream {
2087
2086
  } catch {
2088
2087
  throw error;
2089
2088
  }
2090
- readable = pipeThroughGzipDecompressionStream(readable, gzipStream, outputSize);
2089
+ gzipCrc32 = new Crc32();
2090
+ readable = pipeThroughGzipDecompressionStream(readable, gzipStream, outputSize, gzipCrc32);
2091
2091
  }
2092
2092
  }
2093
2093
  readable = mapInflateStreamError(readable);
2094
2094
  }
2095
- if (checkCrc32) {
2095
+ if (checkCrc32 && !gzipCrc32) {
2096
2096
  crc32Stream = new Crc32Stream();
2097
2097
  readable = pipeThrough(readable, crc32Stream);
2098
2098
  }
2099
2099
  setReadable(this, readable, () => {
2100
2100
  if (checkCrc32) {
2101
- const computedCrc32View = new DataView(crc32Stream.value.buffer);
2102
- if (crc32 != computedCrc32View.getUint32(0, false)) {
2101
+ const computedCrc32 = gzipCrc32 ? gzipCrc32.get() >>> 0 : new DataView(crc32Stream.value.buffer).getUint32(0, false);
2102
+ if (crc32 != computedCrc32) {
2103
2103
  throw new Error(ERR_INVALID_CRC32);
2104
2104
  }
2105
2105
  }
@@ -2155,6 +2155,14 @@ function createCodecStream(CodecStreamClass, format, options) {
2155
2155
  return new CodecStreamClass(format, options);
2156
2156
  }
2157
2157
 
2158
+ function getGzipCompressionStream(useCompressionStream, CompressionStreamNative, CompressionStreamFallback) {
2159
+ if (useCompressionStream && CompressionStreamNative) {
2160
+ return CompressionStreamNative;
2161
+ } else if (CompressionStreamFallback && CompressionStreamFallback.requiresModule) {
2162
+ return CompressionStreamFallback;
2163
+ }
2164
+ }
2165
+
2158
2166
  function pipeThroughCompressionStream(readable, useCompressionStream, options, CompressionStreamNative, CompressionStreamFallback) {
2159
2167
  const Stream = useCompressionStream && CompressionStreamNative ?
2160
2168
  CompressionStreamNative :
@@ -2527,10 +2535,12 @@ class CodecWorker {
2527
2535
  workerData.terminated = true;
2528
2536
  worker.terminate();
2529
2537
  }
2530
- terminateResolvers.forEach(resolve => resolve());
2531
2538
  }
2532
2539
  workerData.busy = false;
2533
- onTaskFinished(workerData);
2540
+ const pendingTasks = onTaskFinished(workerData);
2541
+ if (terminateResolvers) {
2542
+ terminateResolvers.forEach(resolve => resolve(pendingTasks));
2543
+ }
2534
2544
  }
2535
2545
  }
2536
2546
  });
@@ -3215,7 +3225,10 @@ async function runWorker(stream, workerOptions) {
3215
3225
 
3216
3226
  function onTaskFinished(workerData) {
3217
3227
  clearStarvationTimeout();
3218
- if (pendingRequests.length) {
3228
+ if (workerData.terminated) {
3229
+ workerData.terminated = false;
3230
+ return runPendingRequestsInline();
3231
+ } else if (pendingRequests.length) {
3219
3232
  const [{ resolve, stream, workerOptions }] = pendingRequests.splice(0, 1);
3220
3233
  resolve(new CodecWorker(workerData, stream, workerOptions, onTaskFinished));
3221
3234
  armStarvationTimeout();
@@ -3253,12 +3266,26 @@ function onWorkerStarvation() {
3253
3266
  starvationTimeout = null;
3254
3267
  if (pendingRequests.length) {
3255
3268
  const [{ resolve, stream, workerOptions }] = pendingRequests.splice(0, 1);
3256
- const inlineWorkerOptions = Object.assign({}, workerOptions, { useWebWorkers: false, workerURI: UNDEFINED_VALUE, createWorker: UNDEFINED_VALUE });
3257
- resolve(new CodecWorker({}, stream, inlineWorkerOptions, onInlineTaskFinished));
3269
+ resolve(new CodecWorker({}, stream, getInlineWorkerOptions(workerOptions), onInlineTaskFinished));
3258
3270
  armStarvationTimeout();
3259
3271
  }
3260
3272
  }
3261
3273
 
3274
+ function runPendingRequestsInline() {
3275
+ const tasks = pendingRequests.splice(0).map(({ resolve, stream, workerOptions }) => new Promise(resolveTask => {
3276
+ resolve(new CodecWorker({}, stream, getInlineWorkerOptions(workerOptions), () => {
3277
+ onInlineTaskFinished();
3278
+ resolveTask();
3279
+ }));
3280
+ }));
3281
+ clearStarvationTimeout();
3282
+ return Promise.all(tasks);
3283
+ }
3284
+
3285
+ function getInlineWorkerOptions(workerOptions) {
3286
+ return Object.assign({}, workerOptions, { useWebWorkers: false, workerURI: UNDEFINED_VALUE, createWorker: UNDEFINED_VALUE });
3287
+ }
3288
+
3262
3289
  function onInlineTaskFinished() {
3263
3290
  clearStarvationTimeout();
3264
3291
  armStarvationTimeout();
@@ -3268,18 +3295,14 @@ function terminateWorker(workerData, workerOptions) {
3268
3295
  const { config } = workerOptions;
3269
3296
  const { terminateWorkerTimeout } = config;
3270
3297
  if (Number.isFinite(terminateWorkerTimeout) && terminateWorkerTimeout >= 0) {
3271
- if (workerData.terminated) {
3272
- workerData.terminated = false;
3273
- } else {
3274
- workerData.terminateTimeout = setTimeout(async () => {
3275
- pool = pool.filter(data => data != workerData);
3276
- try {
3277
- await workerData.terminate();
3278
- } catch {
3279
- // ignored
3280
- }
3281
- }, terminateWorkerTimeout);
3282
- }
3298
+ workerData.terminateTimeout = setTimeout(async () => {
3299
+ pool = pool.filter(data => data != workerData);
3300
+ try {
3301
+ await workerData.terminate();
3302
+ } catch {
3303
+ // ignored
3304
+ }
3305
+ }, terminateWorkerTimeout);
3283
3306
  }
3284
3307
  }
3285
3308
 
@@ -3292,10 +3315,13 @@ function clearTerminateTimeout(workerData) {
3292
3315
  }
3293
3316
 
3294
3317
  async function terminateWorkers() {
3295
- await Promise.allSettled(pool.map(workerData => {
3296
- clearTerminateTimeout(workerData);
3297
- return workerData.terminate();
3298
- }));
3318
+ await Promise.allSettled([
3319
+ runPendingRequestsInline(),
3320
+ ...pool.map(workerData => {
3321
+ clearTerminateTimeout(workerData);
3322
+ return workerData.terminate();
3323
+ })
3324
+ ]);
3299
3325
  resetWebWorkerSupport();
3300
3326
  }
3301
3327
 
@@ -3370,6 +3396,19 @@ function decodeText(value, encoding) {
3370
3396
  return decode(value, encoding, true);
3371
3397
  }
3372
3398
 
3399
+ function isUTF8Text(value) {
3400
+ if (value.some(byte => byte > 0x7f)) {
3401
+ try {
3402
+ new TextDecoder("utf-8", { fatal: true }).decode(value);
3403
+ return true;
3404
+ } catch {
3405
+ return false;
3406
+ }
3407
+ } else {
3408
+ return false;
3409
+ }
3410
+ }
3411
+
3373
3412
  function decodeTextRemovingBOM(value, encoding) {
3374
3413
  return decode(value, encoding, false);
3375
3414
  }
@@ -4659,6 +4698,7 @@ const WARNING_MISMATCHED_LOCAL_FILE_HEADER_COMPRESSION_METHOD = "mismatched loca
4659
4698
  const WARNING_MISMATCHED_LOCAL_FILE_HEADER_CRC32_OR_SIZES = "mismatched local file header (crc32 or sizes)";
4660
4699
  const MAX_KNOWN_VERSION = 63;
4661
4700
  const DRIVE_LETTER_REGEXP = /^[a-zA-Z]:/;
4701
+ const PARENT_DIRECTORY_REGEXP = /(^|[\\/])\.\.([\\/]|$)/;
4662
4702
  const CHARSET_UTF8 = "utf-8";
4663
4703
  const PROPERTY_NAME_UTF8_SUFFIX = "UTF8";
4664
4704
  const CHARSET_CP437 = "cp437";
@@ -4911,12 +4951,13 @@ class ZipReader {
4911
4951
  const versionMadeBy = getUint16$1(directoryView, offset + 4);
4912
4952
  const msDosCompatible = versionMadeBy >> 8 == 0;
4913
4953
  const unixCompatible = versionMadeBy >> 8 == 3;
4914
- const rawFilename = directoryArray.subarray(filenameOffset, extraFieldOffset);
4915
4954
  const commentLength = getUint16$1(directoryView, offset + 32);
4916
4955
  const endOffset = commentOffset + commentLength;
4917
- const rawComment = directoryArray.subarray(commentOffset, endOffset);
4918
- const filenameUTF8 = languageEncodingFlag;
4919
- const commentUTF8 = languageEncodingFlag;
4956
+ const rawEntryData = new Uint8Array(directoryArray.subarray(filenameOffset, endOffset));
4957
+ const rawFilename = rawEntryData.subarray(0, fileEntry.filenameLength);
4958
+ const rawComment = rawEntryData.subarray(fileEntry.filenameLength + fileEntry.extraFieldLength);
4959
+ const filenameUTF8 = languageEncodingFlag || (!filenameEncoding && isUTF8Text(rawFilename));
4960
+ const commentUTF8 = languageEncodingFlag || (!commentEncoding && isUTF8Text(rawComment));
4920
4961
  const externalFileAttributes = getUint32$1(directoryView, offset + 38);
4921
4962
  const msdosAttributesRaw = externalFileAttributes & MAX_8_BITS;
4922
4963
  const msdosAttributes = {
@@ -4967,7 +5008,7 @@ class ZipReader {
4967
5008
  rawFilename,
4968
5009
  filenameUTF8,
4969
5010
  commentUTF8,
4970
- rawExtraField: directoryArray.subarray(extraFieldOffset, commentOffset),
5011
+ rawExtraField: rawEntryData.subarray(fileEntry.filenameLength, fileEntry.filenameLength + fileEntry.extraFieldLength),
4971
5012
  rawComment,
4972
5013
  filename,
4973
5014
  comment
@@ -5475,7 +5516,7 @@ function readDigitalSignature(signatureRecordArray) {
5475
5516
  if (getUint32$1(signatureRecordView, 0) == DIGITAL_SIGNATURE_RECORD_SIGNATURE) {
5476
5517
  const signatureDataLength = getUint16$1(signatureRecordView, 4);
5477
5518
  if (6 + signatureDataLength <= signatureRecordArray.length) {
5478
- return signatureRecordArray.subarray(6, 6 + signatureDataLength);
5519
+ return new Uint8Array(signatureRecordArray.subarray(6, 6 + signatureDataLength));
5479
5520
  }
5480
5521
  }
5481
5522
  }
@@ -5998,10 +6039,10 @@ function isUnsafeFilename(filename, filenameValidation) {
5998
6039
  if (pathParts.length > 1 && pathParts[pathParts.length - 1] === "") {
5999
6040
  pathParts.pop();
6000
6041
  }
6001
- if (pathParts.includes("..") || filename.startsWith("/") || filename.startsWith("\\\\") || DRIVE_LETTER_REGEXP.test(filename)) {
6042
+ if (PARENT_DIRECTORY_REGEXP.test(filename) || filename.startsWith("/") || filename.startsWith("\\") || DRIVE_LETTER_REGEXP.test(filename)) {
6002
6043
  return true;
6003
6044
  }
6004
- return filenameValidation == STRICTNESS_STRICT && (pathParts.includes(".") || pathParts.includes(""));
6045
+ return filenameValidation == STRICTNESS_STRICT && (pathParts.includes(".") || pathParts.includes("") || filename.includes("\0"));
6005
6046
  }
6006
6047
 
6007
6048
  function getMaxAppendedDataSize(maxAppendedDataSize, strictness) {
@@ -6087,7 +6128,7 @@ async function* scanEndOfCentralDirectory(reader, scanLength) {
6087
6128
  }
6088
6129
 
6089
6130
  function getEndOfCentralDirectoryInfo(scanArray, indexByte, offset) {
6090
- return { offset, buffer: scanArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer };
6131
+ return { offset, buffer: new Uint8Array(scanArray.subarray(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH)).buffer };
6091
6132
  }
6092
6133
 
6093
6134
  async function getCentralDirectoryReachability(reader, view, anchoredOffset, indexByte, offset, size, remoteProbeBudget) {
@@ -8734,7 +8775,7 @@ function formatSupported(StreamClass, format) {
8734
8775
  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
8735
8776
  */
8736
8777
 
8737
- const VERSION = "2.14.1";
8778
+ const VERSION = "2.15.0";
8738
8779
 
8739
8780
  /*
8740
8781
  Copyright (c) 2025 Gildas Lormeau. All rights reserved.