@zip.js/zip.js 2.8.29 → 2.8.31

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 (56) hide show
  1. package/BENCHMARKS.md +35 -0
  2. package/deno.json +5 -4
  3. package/dist/zip-core.js +496 -218
  4. package/dist/zip-core.min.js +1 -1
  5. package/dist/zip-fs-core.js +398 -195
  6. package/dist/zip-fs-core.min.js +1 -1
  7. package/dist/zip-fs-native.js +588 -254
  8. package/dist/zip-fs-native.min.js +1 -1
  9. package/dist/zip-fs.js +587 -253
  10. package/dist/zip-fs.min.js +1 -1
  11. package/dist/zip-legacy.js +501 -223
  12. package/dist/zip-legacy.min.js +1 -1
  13. package/dist/zip-module.wasm +0 -0
  14. package/dist/zip-native.js +504 -226
  15. package/dist/zip-native.min.js +1 -1
  16. package/dist/zip-web-worker-native.js +1 -1
  17. package/dist/zip-web-worker.js +1 -1
  18. package/dist/zip.js +503 -225
  19. package/dist/zip.min.js +1 -1
  20. package/index-native.cjs +588 -254
  21. package/index-native.min.js +1 -1
  22. package/index.cjs +587 -253
  23. package/index.d.cts +2814 -0
  24. package/index.d.ts +364 -1
  25. package/index.min.js +1 -1
  26. package/lib/core/codec-worker.js +0 -10
  27. package/lib/core/configuration.js +21 -33
  28. package/lib/core/constants.js +3 -0
  29. package/lib/core/io.js +11 -19
  30. package/lib/core/options.js +11 -1
  31. package/lib/core/streams/aes-crypto-stream.js +16 -20
  32. package/lib/core/streams/codecs/crc32.js +39 -11
  33. package/lib/core/streams/common-crypto.js +0 -2
  34. package/lib/core/streams/zip-crypto-stream.js +12 -16
  35. package/lib/core/streams/zip-entry-stream.js +69 -19
  36. package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
  37. package/lib/core/streams/zlib-wasm/zlib-streams-loader.js +1 -3
  38. package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
  39. package/lib/core/util/base64.js +68 -0
  40. package/lib/core/util/decode-text.js +0 -1
  41. package/lib/core/util/default-mime-type.js +0 -3
  42. package/lib/core/util/inflate.js +194 -0
  43. package/lib/core/util/mime-type.js +26 -22
  44. package/lib/core/util/opfs-temp-stream.js +139 -0
  45. package/lib/core/web-worker-inline-native.js +1 -1
  46. package/lib/core/web-worker-inline-template-native.js +5 -5
  47. package/lib/core/web-worker-inline-wasm.js +1 -1
  48. package/lib/core/zip-fs.js +61 -10
  49. package/lib/core/zip-reader.js +162 -53
  50. package/lib/core/zip-writer.js +34 -41
  51. package/lib/core/zlib-streams-inline-template.js +3 -2
  52. package/lib/core/zlib-streams-inline.js +1 -1
  53. package/lib/zip-core-base.js +7 -2
  54. package/package.json +9 -6
  55. package/reserved-property-names.js +50 -0
  56. package/lib/core/util/mini-lz.js +0 -198
@@ -112,6 +112,8 @@
112
112
  const FUNCTION_TYPE = "function";
113
113
  const OBJECT_TYPE = "object";
114
114
 
115
+ const EMPTY_UINT8_ARRAY = new Uint8Array();
116
+
115
117
  /*
116
118
  Copyright (c) 2025 Gildas Lormeau. All rights reserved.
117
119
 
@@ -236,26 +238,24 @@
236
238
  class Reader extends Stream {
237
239
 
238
240
  get readable() {
241
+ return this.createReadable();
242
+ }
243
+
244
+ createReadable({ offset = 0, size, diskNumberStart, chunkSize = DEFAULT_CHUNK_SIZE$1 } = {}) {
239
245
  const reader = this;
240
- const { chunkSize = DEFAULT_CHUNK_SIZE$1 } = reader;
241
- const readable = new ReadableStream({
242
- start() {
243
- this.chunkOffset = 0;
244
- },
246
+ let chunkOffset = 0;
247
+ return new ReadableStream({
245
248
  async pull(controller) {
246
- const { offset = 0, size, diskNumberStart } = readable;
247
- const { chunkOffset } = this;
248
249
  const dataSize = size === UNDEFINED_VALUE ? chunkSize : Math.min(chunkSize, size - chunkOffset);
249
250
  const data = await readUint8Array(reader, offset + chunkOffset, dataSize, diskNumberStart);
250
251
  controller.enqueue(data);
251
252
  if ((chunkOffset + chunkSize > size) || (size === UNDEFINED_VALUE && !data.length && dataSize)) {
252
253
  controller.close();
253
254
  } else {
254
- this.chunkOffset += chunkSize;
255
+ chunkOffset += chunkSize;
255
256
  }
256
257
  }
257
258
  });
258
- return readable;
259
259
  }
260
260
  }
261
261
 
@@ -394,8 +394,6 @@
394
394
  }
395
395
  });
396
396
  writer.blob = new Response(transformStream.readable, { headers }).blob();
397
- // mark early rejections (e.g. aborted pipe) as handled; awaiting getData() still
398
- // surfaces them
399
397
  writer.blob.catch(() => { });
400
398
  }
401
399
 
@@ -557,7 +555,7 @@
557
555
  return eocdCache;
558
556
  }
559
557
  if (index >= size || length === 0) {
560
- return new Uint8Array();
558
+ return EMPTY_UINT8_ARRAY;
561
559
  } else {
562
560
  if (index + length > size) {
563
561
  length = size - index;
@@ -614,8 +612,6 @@
614
612
  async function getRequestData(httpReader, sendRequest) {
615
613
  const response = await sendRequest(HTTP_METHOD_GET, httpReader, getHeaders(httpReader));
616
614
  httpReader.data = new Uint8Array(await response.arrayBuffer());
617
- // the downloaded content is authoritative, e.g. the Content-Length response header
618
- // of the HEAD request can be the encoded size when the content is compressed
619
615
  httpReader.size = httpReader.data.length;
620
616
  }
621
617
 
@@ -680,7 +676,6 @@
680
676
  super();
681
677
  Object.assign(this, {
682
678
  url,
683
- // an explicit fetch implementation takes precedence over useXHR
684
679
  reader: options.useXHR && !options.fetch ? new XHRReader(url, options) : new FetchReader(url, options)
685
680
  });
686
681
  }
@@ -812,7 +807,7 @@
812
807
  }
813
808
  }
814
809
  } else {
815
- result = new Uint8Array();
810
+ result = EMPTY_UINT8_ARRAY;
816
811
  }
817
812
  reader.lastDiskNumber = Math.max(currentDiskNumber, reader.lastDiskNumber);
818
813
  return result;
@@ -857,8 +852,6 @@
857
852
  writer.diskOffset += diskSourceWriter.size;
858
853
  writer.diskNumber++;
859
854
  diskWriter = null;
860
- // the next disk is not opened yet but its space is available; leaving a
861
- // stale value here would force a new disk for every subsequent write
862
855
  writer.availableSize = writer.maxSize;
863
856
  if (chunk.length > availableSize) {
864
857
  await this.write(chunk.subarray(availableSize));
@@ -988,18 +981,30 @@
988
981
  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
989
982
  */
990
983
 
991
- const table = [];
992
- for (let i = 0; i < 256; i++) {
993
- let t = i;
984
+ // Slicing-by-8 CRC-32 (Intel / zlib). The eight 256-entry tables let the inner loop
985
+ // consume 8 bytes per iteration with a shorter dependency chain, ~4x the byte-at-a-time
986
+ // rate (measured ~320 -> ~1400 MB/s on 64KB chunks).
987
+ //
988
+ // Every table MUST stay a PACKED_SMI array: build with array literals (not `new Array(n)`,
989
+ // which is HOLEY) and store the signed int32 XOR result (no `>>> 0`). An unsigned or holey
990
+ // table becomes a V8 FixedDoubleArray whose every hot-loop lookup unboxes a double (~1.6x
991
+ // slower). Signedness is irrelevant to the result — the reads mask/shift it and the final
992
+ // `~crc` normalizes it. Do NOT reintroduce `>>> 0` here or switch to `new Array(256)`.
993
+ const T = [[], [], [], [], [], [], [], []];
994
+ for (let n = 0; n < 256; n++) {
995
+ let t = n;
994
996
  for (let j = 0; j < 8; j++) {
995
- if (t & 1) {
996
- t = (t >>> 1) ^ 0xEDB88320;
997
- } else {
998
- t = t >>> 1;
999
- }
997
+ t = (t & 1) ? (t >>> 1) ^ 0xEDB88320 : t >>> 1;
1000
998
  }
1001
- table[i] = t;
999
+ T[0][n] = t;
1002
1000
  }
1001
+ for (let n = 0; n < 256; n++) {
1002
+ for (let k = 1; k < 8; k++) {
1003
+ const previous = T[k - 1][n];
1004
+ T[k][n] = (previous >>> 8) ^ T[0][previous & 0xFF];
1005
+ }
1006
+ }
1007
+ const [T0, T1, T2, T3, T4, T5, T6, T7] = T;
1003
1008
 
1004
1009
  class Crc32 {
1005
1010
 
@@ -1009,8 +1014,24 @@
1009
1014
 
1010
1015
  append(data) {
1011
1016
  let crc = this.crc | 0;
1012
- for (let offset = 0, length = data.length | 0; offset < length; offset++) {
1013
- crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
1017
+ const length = data.length | 0;
1018
+ let offset = 0;
1019
+ // Process 8 bytes per iteration over the typed-array body. DataView.getInt32(le)
1020
+ // reads an unaligned little-endian word as a signed int32 (no double boxing), so no
1021
+ // alignment or endianness handling is needed; data.buffer guards non-typed inputs.
1022
+ if (length >= 8 && data.buffer) {
1023
+ const view = new DataView(data.buffer, data.byteOffset, length);
1024
+ const end = length - 8;
1025
+ for (; offset <= end; offset += 8) {
1026
+ const a = crc ^ view.getInt32(offset, true);
1027
+ const b = view.getInt32(offset + 4, true);
1028
+ crc = T7[a & 0xFF] ^ T6[(a >>> 8) & 0xFF] ^ T5[(a >>> 16) & 0xFF] ^ T4[(a >>> 24) & 0xFF] ^
1029
+ T3[b & 0xFF] ^ T2[(b >>> 8) & 0xFF] ^ T1[(b >>> 16) & 0xFF] ^ T0[(b >>> 24) & 0xFF];
1030
+ }
1031
+ }
1032
+ // Remaining tail (and non-typed inputs) byte-at-a-time with the base table.
1033
+ for (; offset < length; offset++) {
1034
+ crc = (crc >>> 8) ^ T0[(crc ^ data[offset]) & 0xFF];
1014
1035
  }
1015
1036
  this.crc = crc;
1016
1037
  }
@@ -1943,8 +1964,6 @@
1943
1964
  if (GET_RANDOM_VALUES_SUPPORTED) {
1944
1965
  return crypto.getRandomValues(array);
1945
1966
  } else {
1946
- // all supported runtimes expose crypto.getRandomValues; failing hard beats
1947
- // silently generating predictable encryption salts
1948
1967
  throw new Error(ERR_UNSUPPORTED_CRYPTO_API);
1949
1968
  }
1950
1969
  }
@@ -2007,12 +2026,7 @@
2007
2026
  constructor({ password, rawPassword, encryptionStrength, checkPasswordOnly }) {
2008
2027
  super({
2009
2028
  start() {
2010
- Object.assign(this, {
2011
- ready: new Promise(resolve => this.resolveReady = resolve),
2012
- password: encodePassword(password, rawPassword),
2013
- strength: encryptionStrength - 1,
2014
- pending: new Uint8Array()
2015
- });
2029
+ initAesCrypto(this, password, rawPassword, encryptionStrength);
2016
2030
  },
2017
2031
  async transform(chunk, controller) {
2018
2032
  const aesCrypto = this;
@@ -2047,7 +2061,7 @@
2047
2061
  await ready;
2048
2062
  const chunkToDecrypt = subarray(pending, 0, pending.length - SIGNATURE_LENGTH);
2049
2063
  const originalSignature = subarray(pending, pending.length - SIGNATURE_LENGTH);
2050
- let decryptedChunkArray = new Uint8Array();
2064
+ let decryptedChunkArray = EMPTY_UINT8_ARRAY;
2051
2065
  if (chunkToDecrypt.length) {
2052
2066
  const encryptedChunk = toBits(codecBytes, chunkToDecrypt);
2053
2067
  hmac.update(encryptedChunk);
@@ -2055,8 +2069,6 @@
2055
2069
  decryptedChunkArray = fromBits(codecBytes, decryptedChunk);
2056
2070
  }
2057
2071
  const signature = subarray(fromBits(codecBytes, hmac.digest()), 0, SIGNATURE_LENGTH);
2058
- // branch-free comparison: accumulate the differences instead of
2059
- // short-circuiting on the first mismatching byte
2060
2072
  let invalidSignature = pending.length < SIGNATURE_LENGTH ? 1 : 0;
2061
2073
  for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
2062
2074
  invalidSignature |= signature[indexSignature] ^ originalSignature[indexSignature];
@@ -2078,12 +2090,7 @@
2078
2090
  let stream;
2079
2091
  super({
2080
2092
  start() {
2081
- Object.assign(this, {
2082
- ready: new Promise(resolve => this.resolveReady = resolve),
2083
- password: encodePassword(password, rawPassword),
2084
- strength: encryptionStrength - 1,
2085
- pending: new Uint8Array()
2086
- });
2093
+ initAesCrypto(this, password, rawPassword, encryptionStrength);
2087
2094
  },
2088
2095
  async transform(chunk, controller) {
2089
2096
  const aesCrypto = this;
@@ -2093,7 +2100,7 @@
2093
2100
  resolveReady,
2094
2101
  ready
2095
2102
  } = aesCrypto;
2096
- let preamble = new Uint8Array();
2103
+ let preamble = EMPTY_UINT8_ARRAY;
2097
2104
  if (password) {
2098
2105
  preamble = await createEncryptionKeys(aesCrypto, strength, password);
2099
2106
  resolveReady();
@@ -2113,7 +2120,7 @@
2113
2120
  } = this;
2114
2121
  if (hmac && ctr) {
2115
2122
  await ready;
2116
- let encryptedChunkArray = new Uint8Array();
2123
+ let encryptedChunkArray = EMPTY_UINT8_ARRAY;
2117
2124
  if (pending.length) {
2118
2125
  const encryptedChunk = ctr.update(toBits(codecBytes, pending));
2119
2126
  hmac.update(encryptedChunk);
@@ -2128,6 +2135,15 @@
2128
2135
  }
2129
2136
  }
2130
2137
 
2138
+ function initAesCrypto(aesCrypto, password, rawPassword, encryptionStrength) {
2139
+ Object.assign(aesCrypto, {
2140
+ ready: new Promise(resolve => aesCrypto.resolveReady = resolve),
2141
+ password: encodePassword(password, rawPassword),
2142
+ strength: encryptionStrength - 1,
2143
+ pending: EMPTY_UINT8_ARRAY
2144
+ });
2145
+ }
2146
+
2131
2147
  function append(aesCrypto, input, output, paddingStart, paddingEnd, verifySignature) {
2132
2148
  const {
2133
2149
  ctr,
@@ -2289,20 +2305,13 @@
2289
2305
  constructor({ password, rawPassword, passwordVerification, checkPasswordOnly }) {
2290
2306
  super({
2291
2307
  start() {
2292
- Object.assign(this, {
2293
- password,
2294
- rawPassword,
2295
- passwordVerification
2296
- });
2297
- createKeys(this, password, rawPassword);
2308
+ initZipCrypto(this, password, rawPassword, passwordVerification);
2298
2309
  },
2299
2310
  transform(chunk, controller) {
2300
2311
  const zipCrypto = this;
2301
2312
  if (zipCrypto.password || zipCrypto.rawPassword) {
2302
2313
  const decryptedHeader = decrypt(zipCrypto, chunk.subarray(0, HEADER_LENGTH));
2303
2314
  zipCrypto.password = zipCrypto.rawPassword = null;
2304
- // verify the password with a branch-free comparison of the verification byte, mirroring
2305
- // the constant-time AES HMAC signature check
2306
2315
  if ((decryptedHeader.at(-1) ^ zipCrypto.passwordVerification) != 0) {
2307
2316
  throw new Error(ERR_INVALID_PASSWORD);
2308
2317
  }
@@ -2323,12 +2332,7 @@
2323
2332
  constructor({ password, rawPassword, passwordVerification }) {
2324
2333
  super({
2325
2334
  start() {
2326
- Object.assign(this, {
2327
- password,
2328
- rawPassword,
2329
- passwordVerification
2330
- });
2331
- createKeys(this, password, rawPassword);
2335
+ initZipCrypto(this, password, rawPassword, passwordVerification);
2332
2336
  },
2333
2337
  transform(chunk, controller) {
2334
2338
  const zipCrypto = this;
@@ -2352,6 +2356,15 @@
2352
2356
  }
2353
2357
  }
2354
2358
 
2359
+ function initZipCrypto(zipCrypto, password, rawPassword, passwordVerification) {
2360
+ Object.assign(zipCrypto, {
2361
+ password,
2362
+ rawPassword,
2363
+ passwordVerification
2364
+ });
2365
+ createKeys(zipCrypto, password, rawPassword);
2366
+ }
2367
+
2355
2368
  function decrypt(target, input) {
2356
2369
  const output = new Uint8Array(input.length);
2357
2370
  for (let index = 0; index < input.length; index++) {
@@ -2444,21 +2457,32 @@
2444
2457
  const ERR_INVALID_COMPRESSED_DATA = "Invalid compressed data";
2445
2458
  const FORMAT_DEFLATE_RAW = "deflate-raw";
2446
2459
  const FORMAT_DEFLATE64_RAW = "deflate64-raw";
2460
+ const FORMAT_GZIP = "gzip";
2461
+ const GZIP_HEADER_LENGTH = 10;
2462
+ const GZIP_TRAILER_LENGTH = 8;
2447
2463
 
2448
2464
  class DeflateStream extends TransformStream {
2449
2465
 
2450
2466
  constructor(options, { chunkSize, CompressionStreamZlib, CompressionStream }) {
2451
2467
  super({});
2452
- const { compressed, encrypted, useCompressionStream, zipCrypto, signed, level } = options;
2468
+ const { compressed, encrypted, useCompressionStream, zipCrypto, signed, level, deflate64 } = options;
2453
2469
  const stream = this;
2454
- let crc32Stream, encryptionStream;
2470
+ let crc32Stream, encryptionStream, gzipCrc32Stream;
2455
2471
  let readable = super.readable;
2456
- if ((!encrypted || zipCrypto) && signed) {
2472
+ const useGzipCrc32 = signed && compressed && !deflate64 && (!encrypted || zipCrypto) &&
2473
+ Boolean(useCompressionStream && CompressionStream);
2474
+ if ((!encrypted || zipCrypto) && signed && !useGzipCrc32) {
2457
2475
  crc32Stream = new Crc32Stream();
2458
2476
  readable = pipeThrough(readable, crc32Stream);
2459
2477
  }
2460
2478
  if (compressed) {
2461
- readable = pipeThroughCommpressionStream(readable, useCompressionStream, { level, chunkSize }, CompressionStream, CompressionStreamZlib, CompressionStream);
2479
+ if (useGzipCrc32) {
2480
+ gzipCrc32Stream = new GzipToRawDeflateStream();
2481
+ readable = pipeThroughBackpressured(readable, new CompressionStream(FORMAT_GZIP));
2482
+ readable = pipeThrough(readable, gzipCrc32Stream);
2483
+ } else {
2484
+ readable = pipeThroughCommpressionStream(readable, useCompressionStream, { level, chunkSize }, CompressionStream, CompressionStreamZlib, CompressionStream);
2485
+ }
2462
2486
  }
2463
2487
  if (encrypted) {
2464
2488
  if (zipCrypto) {
@@ -2474,13 +2498,64 @@
2474
2498
  signature = encryptionStream.signature;
2475
2499
  }
2476
2500
  if ((!encrypted || zipCrypto) && signed) {
2477
- signature = new DataView(crc32Stream.value.buffer).getUint32(0);
2501
+ signature = useGzipCrc32 ? gzipCrc32Stream.signature : new DataView(crc32Stream.value.buffer).getUint32(0);
2478
2502
  }
2479
2503
  stream.signature = signature;
2480
2504
  });
2481
2505
  }
2482
2506
  }
2483
2507
 
2508
+ class GzipToRawDeflateStream extends TransformStream {
2509
+
2510
+ constructor() {
2511
+ // deno-lint-ignore prefer-const
2512
+ let stream;
2513
+ let headerLeft = GZIP_HEADER_LENGTH;
2514
+ let tail = new Uint8Array(0);
2515
+ super({
2516
+ transform(chunk, controller) {
2517
+ if (headerLeft) {
2518
+ const dropped = Math.min(headerLeft, chunk.length);
2519
+ headerLeft -= dropped;
2520
+ chunk = chunk.subarray(dropped);
2521
+ if (!chunk.length) {
2522
+ return;
2523
+ }
2524
+ }
2525
+ const available = tail.length + chunk.length;
2526
+ if (available <= GZIP_TRAILER_LENGTH) {
2527
+ const pending = new Uint8Array(available);
2528
+ pending.set(tail);
2529
+ pending.set(chunk, tail.length);
2530
+ tail = pending;
2531
+ return;
2532
+ }
2533
+ const emitLength = available - GZIP_TRAILER_LENGTH;
2534
+ const output = new Uint8Array(emitLength);
2535
+ const fromTail = Math.min(emitLength, tail.length);
2536
+ output.set(tail.subarray(0, fromTail), 0);
2537
+ if (emitLength > fromTail) {
2538
+ output.set(chunk.subarray(0, emitLength - fromTail), fromTail);
2539
+ }
2540
+ controller.enqueue(output);
2541
+ const nextTail = new Uint8Array(GZIP_TRAILER_LENGTH);
2542
+ const tailRemaining = tail.length - fromTail;
2543
+ if (tailRemaining) {
2544
+ nextTail.set(tail.subarray(fromTail), 0);
2545
+ }
2546
+ nextTail.set(chunk.subarray(emitLength - fromTail), tailRemaining);
2547
+ tail = nextTail;
2548
+ },
2549
+ flush() {
2550
+ const dataView = new DataView(tail.buffer, tail.byteOffset, tail.byteLength);
2551
+ stream.signature = dataView.getUint32(0, true);
2552
+ stream.uncompressedSize = dataView.getUint32(4, true);
2553
+ }
2554
+ });
2555
+ stream = this;
2556
+ }
2557
+ }
2558
+
2484
2559
  class InflateStream extends TransformStream {
2485
2560
 
2486
2561
  constructor(options, { chunkSize, DecompressionStreamZlib, DecompressionStream }) {
@@ -2543,11 +2618,6 @@
2543
2618
  throw error;
2544
2619
  }
2545
2620
  }
2546
- // The native CompressionStream/DecompressionStream and the pure-JS zlib port do not signal
2547
- // backpressure on their writable side (their `ready` never pends), so a plain pipeThrough drains
2548
- // the whole source into them and peak memory grows with the entry size. Awaiting each write()
2549
- // paces the source to the codec's consumption rate, keeping streaming memory bounded. The WASM
2550
- // codec already backpressures, so this is a no-op cost for it.
2551
2621
  return pipeThroughBackpressured(readable, codecStream);
2552
2622
  }
2553
2623
 
@@ -2555,10 +2625,6 @@
2555
2625
  return readable.pipeThrough(transformStream);
2556
2626
  }
2557
2627
 
2558
- // Like readable.pipeThrough(transformStream), but drives the writable side manually and awaits each
2559
- // write() so that a codec which under-reports backpressure cannot pull the whole source into memory.
2560
- // Errors propagate in both directions: a source error aborts the codec; a downstream cancel/error
2561
- // (surfaced through the codec's writable rejecting) cancels the source.
2562
2628
  function pipeThroughBackpressured(readable, transformStream) {
2563
2629
  const writer = transformStream.writable.getWriter();
2564
2630
  const reader = readable.getReader();
@@ -2587,7 +2653,7 @@
2587
2653
  try {
2588
2654
  await writer.abort(error);
2589
2655
  } catch {
2590
- // the writable may already be errored/closed
2656
+ // ignored: the writable may already be errored/closed
2591
2657
  }
2592
2658
  }
2593
2659
 
@@ -2595,13 +2661,10 @@
2595
2661
  try {
2596
2662
  await reader.cancel(error);
2597
2663
  } catch {
2598
- // the readable may already be errored/closed
2664
+ // ignored: the readable may already be errored/closed
2599
2665
  }
2600
2666
  }
2601
2667
 
2602
- // DecompressionStream implementations can fail with a message-less TypeError on malformed
2603
- // input; give these errors an identifiable message (errors raised by the other stages of
2604
- // the pipeline, e.g. ERR_INVALID_PASSWORD, always carry a message and pass through as-is)
2605
2668
  function mapInflateStreamError(readable) {
2606
2669
  const reader = readable.getReader();
2607
2670
  return new ReadableStream({
@@ -2926,9 +2989,6 @@
2926
2989
  try {
2927
2990
  await initModule(config);
2928
2991
  } catch {
2929
- // The WASM module failed to load. Fall back to the native CompressionStream only if the
2930
- // zlib codec that would otherwise run depends on that module; a self-contained codec
2931
- // supplied through config (e.g. the pure-JS port) stays usable and is kept as requested.
2932
2992
  const ZlibStream = options.codecType.startsWith(CODEC_DEFLATE) ?
2933
2993
  config.CompressionStreamZlib :
2934
2994
  config.DecompressionStreamZlib;
@@ -3006,7 +3066,6 @@
3006
3066
  return resultValue;
3007
3067
  } catch (error) {
3008
3068
  await closeWritable();
3009
- // unblock the pipe watcher if the worker died before closing its end of the stream
3010
3069
  abortPipe();
3011
3070
  try {
3012
3071
  await closed;
@@ -3031,8 +3090,6 @@
3031
3090
  const abortController = new AbortController();
3032
3091
  const { writable, readable } = new TransformStream();
3033
3092
  const closed = readable.pipeTo(writableSource, { preventClose: true, preventAbort: true, signal: abortController.signal });
3034
- // the pipe can fail before `closed` is awaited (e.g. the destination write fails);
3035
- // mark the rejection as handled so it cannot kill the process in the meantime
3036
3093
  closed.catch(() => { });
3037
3094
  return { writable, closed, abortPipe: () => abortController.abort() };
3038
3095
  }
@@ -3125,8 +3182,6 @@
3125
3182
  event.preventDefault();
3126
3183
  }
3127
3184
  const { rejectResult, writer, onTaskFinished } = workerData;
3128
- // the worker is unusable (e.g. its script failed to load); terminate it and
3129
- // fail the current task instead of letting it hang forever
3130
3185
  terminateWorker$1(workerData);
3131
3186
  if (rejectResult) {
3132
3187
  rejectResult(event.error || new Error(event.message || ERROR_EVENT_TYPE));
@@ -3178,8 +3233,6 @@
3178
3233
  async function onMessage({ data }, workerData) {
3179
3234
  const { type, value, messageId, result, error } = data;
3180
3235
  const { reader, writer, resolveResult, rejectResult, onTaskFinished, generation } = workerData;
3181
- // continuations below can settle after the worker slot has been reassigned to a new
3182
- // task (e.g. a pending pull read failing late); they must not touch the new task
3183
3236
  const stale = () => workerData.generation != generation;
3184
3237
  try {
3185
3238
  if (error) {
@@ -3443,7 +3496,6 @@
3443
3496
  if (encoding && encoding.trim().toLowerCase() == "cp437") {
3444
3497
  return decodeCP437(value);
3445
3498
  } else {
3446
- // ignoreBOM keeps a leading U+FEFF in filenames/comments instead of dropping it
3447
3499
  return new TextDecoder(encoding, { ignoreBOM: true }).decode(value);
3448
3500
  }
3449
3501
  }
@@ -3641,6 +3693,11 @@
3641
3693
  const OPTION_OFFSET = "offset";
3642
3694
  const OPTION_USDZ = "usdz";
3643
3695
  const OPTION_UNIX_EXTRA_FIELD_TYPE = "unixExtraFieldType";
3696
+ const OPTION_STRICTNESS = "strictness";
3697
+ const OPTION_MAX_APPENDED_DATA_SIZE = "maxAppendedDataSize";
3698
+ const STRICTNESS_STRICT = "strict";
3699
+ const STRICTNESS_BALANCED = "balanced";
3700
+ const STRICTNESS_TOLERANT = "tolerant";
3644
3701
 
3645
3702
  /*
3646
3703
  Copyright (c) 2025 Gildas Lormeau. All rights reserved.
@@ -3686,8 +3743,6 @@
3686
3743
  const CHARSET_UTF8 = "utf-8";
3687
3744
  const PROPERTY_NAME_UTF8_SUFFIX = "UTF8";
3688
3745
  const CHARSET_CP437 = "cp437";
3689
- // bits of the general purpose flag that change how an entry is interpreted; the level bits (1-2) are excluded
3690
- // because they are informative only
3691
3746
  const BITFLAG_AMBIGUITY_MASK = BITFLAG_ENCRYPTED | BITFLAG_DATA_DESCRIPTOR | BITFLAG_LANG_ENCODING_FLAG;
3692
3747
  const ZIP64_PROPERTIES = [
3693
3748
  [PROPERTY_NAME_UNCOMPRESSED_SIZE, MAX_32_BITS],
@@ -3729,8 +3784,11 @@
3729
3784
  if (reader.size < END_OF_CENTRAL_DIR_LENGTH) {
3730
3785
  throw new Error(ERR_BAD_FORMAT);
3731
3786
  }
3732
- reader.chunkSize = getChunkSize(config);
3733
- const endOfDirectoryInfo = await seekSignature(reader, END_OF_CENTRAL_DIR_SIGNATURE, reader.size, END_OF_CENTRAL_DIR_LENGTH, MAX_16_BITS * 16);
3787
+ const strictness = getStrictness(getOptionValue$1(zipReader, options, OPTION_STRICTNESS), getOptionValue$1(zipReader, options, OPTION_CHECK_AMBIGUITY));
3788
+ const checkAmbiguity = strictness == STRICTNESS_STRICT;
3789
+ const rejectAmbiguousEndOfDirectory = strictness != STRICTNESS_TOLERANT;
3790
+ const maxAppendedDataSize = getMaxAppendedDataSize(getOptionValue$1(zipReader, options, OPTION_MAX_APPENDED_DATA_SIZE), strictness);
3791
+ const { endOfDirectoryInfo, endOfDirectoryReachingEndCount } = await findEndOfCentralDirectory(reader, rejectAmbiguousEndOfDirectory, maxAppendedDataSize);
3734
3792
  if (!endOfDirectoryInfo) {
3735
3793
  const signatureArray = await readUint8Array(reader, 0, 4);
3736
3794
  const signatureView = getDataView$1(signatureArray);
@@ -3740,14 +3798,16 @@
3740
3798
  throw new Error(ERR_EOCDR_NOT_FOUND);
3741
3799
  }
3742
3800
  }
3801
+ if (rejectAmbiguousEndOfDirectory && endOfDirectoryReachingEndCount > 1) {
3802
+ throwAmbiguousArchive("multiple end of central directory records");
3803
+ }
3743
3804
  const endOfDirectoryView = getDataView$1(endOfDirectoryInfo);
3744
3805
  let directoryDataLength = getUint32(endOfDirectoryView, 12);
3745
3806
  let directoryDataOffset = getUint32(endOfDirectoryView, 16);
3746
3807
  const commentOffset = endOfDirectoryInfo.offset;
3747
3808
  const commentLength = getUint16(endOfDirectoryView, 20);
3748
3809
  const appendedDataOffset = commentOffset + END_OF_CENTRAL_DIR_LENGTH + commentLength;
3749
- const checkAmbiguity = getOptionValue$1(zipReader, options, OPTION_CHECK_AMBIGUITY);
3750
- if (checkAmbiguity && appendedDataOffset != reader.size) {
3810
+ if (reader.size - appendedDataOffset > maxAppendedDataSize) {
3751
3811
  throwAmbiguousArchive("appended data");
3752
3812
  }
3753
3813
  let lastDiskNumber = getUint16(endOfDirectoryView, 4);
@@ -3760,7 +3820,7 @@
3760
3820
  if (directoryDataOffset == MAX_32_BITS || directoryDataLength == MAX_32_BITS || filesLength == MAX_16_BITS || diskNumber == MAX_16_BITS) {
3761
3821
  const endOfDirectoryLocatorArray = endOfDirectoryInfo.offset >= ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH ?
3762
3822
  await readUint8Array(reader, endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH) :
3763
- new Uint8Array();
3823
+ EMPTY_UINT8_ARRAY;
3764
3824
  const endOfDirectoryLocatorView = getDataView$1(endOfDirectoryLocatorArray);
3765
3825
  if (endOfDirectoryLocatorArray.length == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH &&
3766
3826
  getUint32(endOfDirectoryLocatorView, 0) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
@@ -3826,14 +3886,22 @@
3826
3886
  throw new Error(ERR_BAD_FORMAT);
3827
3887
  }
3828
3888
  const expectedDirectoryDataOffset = centralDirectoryEndOffset - directoryDataLength - (reader.lastDiskOffset || 0);
3829
- if (getUint32(directoryView, offset) != CENTRAL_FILE_HEADER_SIGNATURE && directoryDataOffset != expectedDirectoryDataOffset && diskNumber == lastDiskNumber) {
3830
- const originalDirectoryDataOffset = directoryDataOffset;
3831
- directoryDataOffset = expectedDirectoryDataOffset;
3832
- if (directoryDataOffset > originalDirectoryDataOffset) {
3833
- prependedDataLength += directoryDataOffset - originalDirectoryDataOffset;
3889
+ if (directoryDataOffset != expectedDirectoryDataOffset && diskNumber == lastDiskNumber) {
3890
+ const storedPointsAtDirectory = getUint32(directoryView, offset) == CENTRAL_FILE_HEADER_SIGNATURE;
3891
+ let reconcile = !storedPointsAtDirectory;
3892
+ if (!reconcile && expectedDirectoryDataOffset >= 0 && expectedDirectoryDataOffset + 4 <= reader.size) {
3893
+ const expectedSignatureArray = await readUint8Array(reader, expectedDirectoryDataOffset, 4, diskNumber);
3894
+ reconcile = getUint32(getDataView$1(expectedSignatureArray), 0) == CENTRAL_FILE_HEADER_SIGNATURE;
3895
+ }
3896
+ if (reconcile) {
3897
+ const originalDirectoryDataOffset = directoryDataOffset;
3898
+ directoryDataOffset = expectedDirectoryDataOffset;
3899
+ if (directoryDataOffset > originalDirectoryDataOffset) {
3900
+ prependedDataLength += directoryDataOffset - originalDirectoryDataOffset;
3901
+ }
3902
+ directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
3903
+ directoryView = getDataView$1(directoryArray);
3834
3904
  }
3835
- directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
3836
- directoryView = getDataView$1(directoryArray);
3837
3905
  }
3838
3906
  }
3839
3907
  const expectedDirectoryDataLength = centralDirectoryEndOffset - directoryDataOffset - (reader.lastDiskOffset || 0);
@@ -3975,11 +4043,11 @@
3975
4043
  const extractPrependedData = getOptionValue$1(zipReader, options, OPTION_EXTRACT_PREPENDED_DATA);
3976
4044
  const extractAppendedData = getOptionValue$1(zipReader, options, OPTION_EXTRACT_APPENDED_DATA);
3977
4045
  if (extractPrependedData) {
3978
- zipReader.prependedData = startOffset > 0 ? await readUint8Array(reader, 0, startOffset) : new Uint8Array();
4046
+ zipReader.prependedData = startOffset > 0 ? await readUint8Array(reader, 0, startOffset) : EMPTY_UINT8_ARRAY;
3979
4047
  }
3980
- zipReader.comment = commentLength ? await readUint8Array(reader, commentOffset + END_OF_CENTRAL_DIR_LENGTH, commentLength) : new Uint8Array();
4048
+ zipReader.comment = commentLength ? await readUint8Array(reader, commentOffset + END_OF_CENTRAL_DIR_LENGTH, commentLength) : EMPTY_UINT8_ARRAY;
3981
4049
  if (extractAppendedData) {
3982
- zipReader.appendedData = appendedDataOffset < reader.size ? await readUint8Array(reader, appendedDataOffset, reader.size - appendedDataOffset) : new Uint8Array();
4050
+ zipReader.appendedData = appendedDataOffset < reader.size ? await readUint8Array(reader, appendedDataOffset, reader.size - appendedDataOffset) : EMPTY_UINT8_ARRAY;
3983
4051
  }
3984
4052
  return true;
3985
4053
  }
@@ -4050,8 +4118,8 @@
4050
4118
  extraFieldLength,
4051
4119
  filenameLength
4052
4120
  } = localDirectory;
4053
- const checkAmbiguity = getOptionValue$1(zipEntry, options, OPTION_CHECK_AMBIGUITY);
4054
- let rawLocalFilename = new Uint8Array();
4121
+ const checkAmbiguity = getStrictness(getOptionValue$1(zipEntry, options, OPTION_STRICTNESS), getOptionValue$1(zipEntry, options, OPTION_CHECK_AMBIGUITY)) == STRICTNESS_STRICT;
4122
+ let rawLocalFilename = EMPTY_UINT8_ARRAY;
4055
4123
  if (checkAmbiguity && (filenameLength || extraFieldLength)) {
4056
4124
  const trailingDataArray = await readUint8Array(reader, offset + HEADER_SIZE, filenameLength + extraFieldLength, diskNumberStart);
4057
4125
  rawLocalFilename = trailingDataArray.subarray(0, filenameLength);
@@ -4059,7 +4127,7 @@
4059
4127
  } else {
4060
4128
  localDirectory.rawExtraField = extraFieldLength ?
4061
4129
  await readUint8Array(reader, offset + HEADER_SIZE + filenameLength, extraFieldLength, diskNumberStart) :
4062
- new Uint8Array();
4130
+ EMPTY_UINT8_ARRAY;
4063
4131
  }
4064
4132
  readCommonFooter(zipEntry, localDirectory, dataView, 4, true);
4065
4133
  if (checkAmbiguity) {
@@ -4086,12 +4154,7 @@
4086
4154
  }
4087
4155
  const dataOffset = offset + HEADER_SIZE + filenameLength + extraFieldLength;
4088
4156
  const size = compressedSize;
4089
- const readable = reader.readable;
4090
- Object.assign(readable, {
4091
- diskNumberStart,
4092
- offset: dataOffset,
4093
- size
4094
- });
4157
+ const readable = reader.createReadable({ offset: dataOffset, size, diskNumberStart, chunkSize: getChunkSize(config) });
4095
4158
  const signal = getOptionValue$1(zipEntry, options, OPTION_SIGNAL);
4096
4159
  const checkPasswordOnly = getOptionValue$1(zipEntry, options, OPTION_CHECK_PASSWORD_ONLY);
4097
4160
  let checkOverlappingEntry = getOptionValue$1(zipEntry, options, OPTION_CHECK_OVERLAPPING_ENTRY);
@@ -4362,7 +4425,6 @@
4362
4425
  const view = getDataView$1(new Uint8Array(extraField.data));
4363
4426
  let uid, gid;
4364
4427
  if (isInfoZip) {
4365
- // Info-ZIP New Unix "ux" field (0x7875): version + variable-length uid/gid
4366
4428
  let offset = 0;
4367
4429
  const version = getUint8(view, offset++);
4368
4430
  const uidSize = getUint8(view, offset++);
@@ -4372,8 +4434,6 @@
4372
4434
  gid = unpackUnixId(extraField.data.subarray(offset, offset + gidSize));
4373
4435
  Object.assign(extraField, { version, uid, gid });
4374
4436
  } else if (extraField.data.length >= 4) {
4375
- // Info-ZIP Unix "Ux" field (0x7855): fixed 2-byte uid + gid (the file mode, if any, is
4376
- // carried in the external file attributes, not here)
4377
4437
  uid = getUint16(view, 0);
4378
4438
  gid = getUint16(view, 2);
4379
4439
  Object.assign(extraField, { uid, gid });
@@ -4425,7 +4485,6 @@
4425
4485
  timeProperties.forEach((propertyName, indexProperty) => {
4426
4486
  if (extraFieldExtendedTimestamp.data.length >= offset + 4) {
4427
4487
  const time = getUint32(extraFieldView, offset);
4428
- // the extended timestamp is a signed 32-bit time_t value
4429
4488
  directory[propertyName] = extraFieldExtendedTimestamp[propertyName] = new Date((time | 0) * 1000);
4430
4489
  const rawPropertyName = timeRawProperties[indexProperty];
4431
4490
  extraFieldExtendedTimestamp[rawPropertyName] = time;
@@ -4500,26 +4559,133 @@
4500
4559
  readRanges.set(index, range);
4501
4560
  }
4502
4561
 
4503
- async function seekSignature(reader, signature, startOffset, minimumBytes, maximumLength) {
4504
- const signatureArray = new Uint8Array(4);
4505
- const signatureView = getDataView$1(signatureArray);
4506
- setUint32$1(signatureView, 0, signature);
4507
- const maximumBytes = minimumBytes + maximumLength;
4508
- return (await seek(minimumBytes)) || await seek(Math.min(maximumBytes, startOffset));
4509
-
4510
- async function seek(length) {
4511
- const offset = startOffset - length;
4512
- const bytes = await readUint8Array(reader, offset, length);
4513
- for (let indexByte = bytes.length - minimumBytes; indexByte >= 0; indexByte--) {
4514
- if (bytes[indexByte] == signatureArray[0] && bytes[indexByte + 1] == signatureArray[1] &&
4515
- bytes[indexByte + 2] == signatureArray[2] && bytes[indexByte + 3] == signatureArray[3]) {
4516
- return {
4517
- offset: offset + indexByte,
4518
- buffer: bytes.slice(indexByte, indexByte + minimumBytes).buffer
4519
- };
4562
+ function getStrictness(strictness, checkAmbiguity) {
4563
+ if (strictness === UNDEFINED_VALUE) {
4564
+ return checkAmbiguity ? STRICTNESS_STRICT : STRICTNESS_BALANCED;
4565
+ }
4566
+ return strictness;
4567
+ }
4568
+
4569
+ function getMaxAppendedDataSize(maxAppendedDataSize, strictness) {
4570
+ if (maxAppendedDataSize !== UNDEFINED_VALUE) {
4571
+ return maxAppendedDataSize;
4572
+ }
4573
+ if (strictness == STRICTNESS_STRICT) {
4574
+ return 0;
4575
+ }
4576
+ if (strictness == STRICTNESS_TOLERANT) {
4577
+ return Infinity;
4578
+ }
4579
+ return MAX_16_BITS;
4580
+ }
4581
+
4582
+ const MAX_END_OF_CENTRAL_DIR_PROBES = 64;
4583
+
4584
+ const CENTRAL_DIRECTORY_UNREACHABLE = 0;
4585
+ const CENTRAL_DIRECTORY_PLAUSIBLE = 1;
4586
+ const CENTRAL_DIRECTORY_REACHABLE = 2;
4587
+
4588
+ async function findEndOfCentralDirectory(reader, rejectAmbiguous, maxAppendedDataSize) {
4589
+ const { size } = reader;
4590
+ const anchoredLength = Math.min(size, END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS);
4591
+ const remoteProbeBudget = { count: MAX_END_OF_CENTRAL_DIR_PROBES };
4592
+ let endOfDirectoryInfo;
4593
+ let plausibleEndOfDirectoryInfo;
4594
+ let endOfDirectoryReachingEndCount = 0;
4595
+ for await (const [anchoredView, anchoredOffset, anchoredArray, indexByte, offset] of scanEndOfCentralDirectory(reader, anchoredLength)) {
4596
+ const commentLength = getUint16(anchoredView, indexByte + 20);
4597
+ if (offset + END_OF_CENTRAL_DIR_LENGTH + commentLength == size) {
4598
+ const reachability = await getCentralDirectoryReachability(reader, anchoredView, anchoredOffset, indexByte, offset, size, remoteProbeBudget);
4599
+ if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
4600
+ if (!endOfDirectoryInfo) {
4601
+ endOfDirectoryInfo = getEndOfCentralDirectoryInfo(anchoredArray, indexByte, offset);
4602
+ }
4603
+ endOfDirectoryReachingEndCount++;
4604
+ if (!rejectAmbiguous || endOfDirectoryReachingEndCount > 1) {
4605
+ break;
4606
+ }
4607
+ } else if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleEndOfDirectoryInfo) {
4608
+ plausibleEndOfDirectoryInfo = getEndOfCentralDirectoryInfo(anchoredArray, indexByte, offset);
4520
4609
  }
4521
4610
  }
4522
4611
  }
4612
+ if (!endOfDirectoryInfo) {
4613
+ endOfDirectoryInfo = plausibleEndOfDirectoryInfo;
4614
+ }
4615
+ if (!endOfDirectoryInfo) {
4616
+ endOfDirectoryInfo = await seekEndOfCentralDirectory(reader, maxAppendedDataSize, remoteProbeBudget);
4617
+ }
4618
+ return { endOfDirectoryInfo, endOfDirectoryReachingEndCount };
4619
+ }
4620
+
4621
+ async function seekEndOfCentralDirectory(reader, maxAppendedDataSize, remoteProbeBudget) {
4622
+ const { size } = reader;
4623
+ const searchLength = Math.min(size, maxAppendedDataSize == Infinity ? size :
4624
+ END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS + maxAppendedDataSize);
4625
+ let firstSignatureInfo, plausibleInfo;
4626
+ for await (const [searchView, searchOffset, searchArray, indexByte, offset] of scanEndOfCentralDirectory(reader, searchLength)) {
4627
+ const record = getEndOfCentralDirectoryInfo(searchArray, indexByte, offset);
4628
+ if (!firstSignatureInfo) {
4629
+ firstSignatureInfo = record;
4630
+ }
4631
+ const reachability = await getCentralDirectoryReachability(reader, searchView, searchOffset, indexByte, offset, size, remoteProbeBudget);
4632
+ if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
4633
+ return record;
4634
+ }
4635
+ if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleInfo) {
4636
+ plausibleInfo = record;
4637
+ }
4638
+ }
4639
+ return plausibleInfo || firstSignatureInfo;
4640
+ }
4641
+
4642
+ async function* scanEndOfCentralDirectory(reader, scanLength) {
4643
+ const scanOffset = reader.size - scanLength;
4644
+ const scanArray = await readUint8Array(reader, scanOffset, scanLength);
4645
+ const scanView = getDataView$1(scanArray);
4646
+ for (let indexByte = scanArray.length - END_OF_CENTRAL_DIR_LENGTH; indexByte >= 0; indexByte--) {
4647
+ if (getUint32(scanView, indexByte) == END_OF_CENTRAL_DIR_SIGNATURE) {
4648
+ yield [scanView, scanOffset, scanArray, indexByte, scanOffset + indexByte];
4649
+ }
4650
+ }
4651
+ }
4652
+
4653
+ function getEndOfCentralDirectoryInfo(scanArray, indexByte, offset) {
4654
+ return { offset, buffer: scanArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer };
4655
+ }
4656
+
4657
+ async function getCentralDirectoryReachability(reader, view, anchoredOffset, indexByte, offset, size, remoteProbeBudget) {
4658
+ const filesLength = getUint16(view, indexByte + 10);
4659
+ const directoryDataLength = getUint32(view, indexByte + 12);
4660
+ const directoryDataOffset = getUint32(view, indexByte + 16);
4661
+ if (filesLength == MAX_16_BITS || directoryDataLength == MAX_32_BITS || directoryDataOffset == MAX_32_BITS) {
4662
+ const locatorSignature = await readSignature(reader, view, anchoredOffset, offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, size, remoteProbeBudget);
4663
+ return locatorSignature == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE ? CENTRAL_DIRECTORY_REACHABLE : CENTRAL_DIRECTORY_UNREACHABLE;
4664
+ }
4665
+ if (!filesLength && !directoryDataLength) {
4666
+ return CENTRAL_DIRECTORY_PLAUSIBLE;
4667
+ }
4668
+ for (const centralDirectoryOffset of [offset - directoryDataLength, directoryDataOffset]) {
4669
+ if (await readSignature(reader, view, anchoredOffset, centralDirectoryOffset, size, remoteProbeBudget) == CENTRAL_FILE_HEADER_SIGNATURE) {
4670
+ return CENTRAL_DIRECTORY_REACHABLE;
4671
+ }
4672
+ }
4673
+ return CENTRAL_DIRECTORY_UNREACHABLE;
4674
+ }
4675
+
4676
+ async function readSignature(reader, view, anchoredOffset, signatureOffset, size, remoteProbeBudget) {
4677
+ if (signatureOffset < 0 || signatureOffset + 4 > size) {
4678
+ return UNDEFINED_VALUE;
4679
+ }
4680
+ if (signatureOffset >= anchoredOffset) {
4681
+ return getUint32(view, signatureOffset - anchoredOffset);
4682
+ }
4683
+ if (remoteProbeBudget.count > 0) {
4684
+ remoteProbeBudget.count--;
4685
+ const signatureArray = await readUint8Array(reader, signatureOffset, 4);
4686
+ return getUint32(getDataView$1(signatureArray), 0);
4687
+ }
4688
+ return UNDEFINED_VALUE;
4523
4689
  }
4524
4690
 
4525
4691
  function checkLocalDirectory(zipEntry, localDirectory, rawLocalFilename) {
@@ -4534,8 +4700,6 @@
4534
4700
  if (localDirectory.compressionMethod != zipEntry.compressionMethod) {
4535
4701
  throwAmbiguousArchive("mismatched local file header (compression method)");
4536
4702
  }
4537
- // when the data descriptor flag is set, the local signature and sizes are zeroed out; all-zero values are
4538
- // also tolerated when the flag is clear because some tools produce them (streaming writers predating bit 3)
4539
4703
  if (!localDirectory.bitFlag.dataDescriptor &&
4540
4704
  (localDirectory.signature || localDirectory.compressedSize || localDirectory.uncompressedSize) &&
4541
4705
  (localDirectory.signature != zipEntry.signature ||
@@ -4584,10 +4748,6 @@
4584
4748
  return Number(view.getBigUint64(offset, true));
4585
4749
  }
4586
4750
 
4587
- function setUint32$1(view, offset, value) {
4588
- view.setUint32(offset, value, true);
4589
- }
4590
-
4591
4751
  function getDataView$1(array) {
4592
4752
  return new DataView(array.buffer, array.byteOffset, array.byteLength);
4593
4753
  }
@@ -4629,7 +4789,6 @@
4629
4789
  const ERR_INVALID_ENCRYPTION_STRENGTH = "The strength must equal 1, 2, or 3";
4630
4790
  const ERR_INVALID_EXTRAFIELD_TYPE = "Extra field type exceeds 65535";
4631
4791
  const ERR_INVALID_EXTRAFIELD_DATA = "Extra field data exceeds 64KB";
4632
- // same message as the constant exported by zip-reader.js
4633
4792
  const ERR_UNSUPPORTED_COMPRESSION = "Compression method not supported";
4634
4793
  const MIN_UNIX_TIME = -2147483648;
4635
4794
  const MAX_UNIX_TIME = 2147483647;
@@ -4712,12 +4871,12 @@
4712
4871
  rawExtraField,
4713
4872
  } = entry;
4714
4873
  const { level, languageEncodingFlag, dataDescriptor } = bitFlag;
4715
- rawExtraFieldZip64 = rawExtraFieldZip64 || new Uint8Array();
4716
- rawExtraFieldAES = rawExtraFieldAES || new Uint8Array();
4717
- rawExtraFieldExtendedTimestamp = rawExtraFieldExtendedTimestamp || new Uint8Array();
4718
- rawExtraFieldNTFS = rawExtraFieldNTFS || new Uint8Array();
4719
- rawExtraFieldUnix = rawExtraFieldUnix || new Uint8Array();
4720
- rawExtraField = rawExtraField || new Uint8Array();
4874
+ rawExtraFieldZip64 = rawExtraFieldZip64 || EMPTY_UINT8_ARRAY;
4875
+ rawExtraFieldAES = rawExtraFieldAES || EMPTY_UINT8_ARRAY;
4876
+ rawExtraFieldExtendedTimestamp = rawExtraFieldExtendedTimestamp || EMPTY_UINT8_ARRAY;
4877
+ rawExtraFieldNTFS = rawExtraFieldNTFS || EMPTY_UINT8_ARRAY;
4878
+ rawExtraFieldUnix = rawExtraFieldUnix || EMPTY_UINT8_ARRAY;
4879
+ rawExtraField = rawExtraField || EMPTY_UINT8_ARRAY;
4721
4880
  if (entry.extraFieldAES) {
4722
4881
  compressionMethod = COMPRESSION_METHOD_AES;
4723
4882
  }
@@ -4823,7 +4982,7 @@
4823
4982
  return false;
4824
4983
  }
4825
4984
 
4826
- async close(comment = new Uint8Array(), options = {}) {
4985
+ async close(comment = EMPTY_UINT8_ARRAY, options = {}) {
4827
4986
  const zipWriter = this;
4828
4987
  const { pendingAddFileCalls, writer } = this;
4829
4988
  const { writable } = writer;
@@ -5060,7 +5219,7 @@
5060
5219
  if (!zipCrypto && (password !== UNDEFINED_VALUE || rawPassword !== UNDEFINED_VALUE) && !(encryptionStrength >= 1 && encryptionStrength <= 3)) {
5061
5220
  throw new Error(ERR_INVALID_ENCRYPTION_STRENGTH);
5062
5221
  }
5063
- let rawExtraField = new Uint8Array();
5222
+ let rawExtraField = EMPTY_UINT8_ARRAY;
5064
5223
  const extraField = options[PROPERTY_NAME_EXTRA_FIELD];
5065
5224
  if (extraField) {
5066
5225
  let extraFieldSize = 0;
@@ -5121,8 +5280,6 @@
5121
5280
  let maximumCompressedSize = 0;
5122
5281
  let uncompressedSize = 0;
5123
5282
  if (passThrough) {
5124
- // the headers of a passThrough entry describe the payload verbatim: without a
5125
- // reader they would declare content that is not there
5126
5283
  if (!reader) {
5127
5284
  throw new Error(ERR_UNDEFINED_READER);
5128
5285
  }
@@ -5133,8 +5290,6 @@
5133
5290
  }
5134
5291
  const zip64Enabled = zip64 === true;
5135
5292
  const encrypted = getOptionValue(zipWriter, options, PROPERTY_NAME_ENCRYPTED);
5136
- // entries without a reader have no payload: write them as empty stored files instead of
5137
- // declaring a compressed or encrypted content that does not exist
5138
5293
  const encryptedEntry = Boolean(reader) && (Boolean((password && getLength(password)) || (rawPassword && getLength(rawPassword))) || (passThrough && encrypted));
5139
5294
  if (!reader) {
5140
5295
  level = 0;
@@ -5272,7 +5427,6 @@
5272
5427
  fileEntry.diskNumberStart = writer.diskNumber;
5273
5428
  fileEntry.offset = getSegmentOffset(zipWriter, writer);
5274
5429
  if (usdz) {
5275
- // the padding depends on the entry offset, only known now that the writer is locked
5276
5430
  const previousMetadataSize = entryInfo.metadataSize;
5277
5431
  appendExtraFieldUSDZ(entryInfo, zipWriter.offset - writer.diskOffset);
5278
5432
  fileEntry.size += entryInfo.metadataSize - previousMetadataSize;
@@ -5315,6 +5469,13 @@
5315
5469
  if (releaseLockWriter) {
5316
5470
  releaseLockWriter();
5317
5471
  }
5472
+ if (bufferedWrite && fileWriter && fileWriter.dispose) {
5473
+ try {
5474
+ await fileWriter.dispose();
5475
+ } catch {
5476
+ // ignored
5477
+ }
5478
+ }
5318
5479
  }
5319
5480
 
5320
5481
  function requestLockCurrentFileEntry() {
@@ -5334,7 +5495,7 @@
5334
5495
  async function skipDiskIfNeeded() {
5335
5496
  if (getLength(headerInfo.localHeaderArray) > writer.availableSize) {
5336
5497
  writer.availableSize = 0;
5337
- await writeData(writer, new Uint8Array());
5498
+ await writeData(writer, EMPTY_UINT8_ARRAY);
5338
5499
  }
5339
5500
  }
5340
5501
  }
@@ -5445,8 +5606,7 @@
5445
5606
  }
5446
5607
  const { writable } = writer;
5447
5608
  if (reader) {
5448
- reader.chunkSize = getChunkSize(config);
5449
- const readable = reader.readable;
5609
+ const readable = reader.createReadable ? reader.createReadable({ chunkSize: getChunkSize(config) }) : reader.readable;
5450
5610
  const size = reader.size;
5451
5611
  const workerOptions = {
5452
5612
  options: {
@@ -5546,13 +5706,9 @@
5546
5706
  : compressionMethod !== COMPRESSION_METHOD_STORE);
5547
5707
  let rawLocalExtraFieldZip64;
5548
5708
  const uncompressedFile = passThrough || !compressed;
5549
- // entries without a data descriptor are always buffered (cf. getFileEntry), so the
5550
- // zip64 extra field values can be patched into the local header before flushing it
5551
5709
  const zip64ExtraFieldComplete = zip64 && (options.bufferedWrite || !dataDescriptor || ((!zip64UncompressedSize && !zip64CompressedSize) || uncompressedFile));
5552
5710
  const writeLocalExtraFieldZip64 = zip64ExtraFieldComplete || (zip64 && dataDescriptor && (zip64UncompressedSize || zip64CompressedSize));
5553
5711
  if (zip64 && (zip64UncompressedSize || zip64CompressedSize)) {
5554
- // APPNOTE 4.5.3: the zip64 extra field of a local header must include both the
5555
- // uncompressed and compressed size fields
5556
5712
  const length = 4 + 16;
5557
5713
  const extraFieldZip64 = createRecordWriter(length);
5558
5714
  extraFieldZip64.uint16(EXTRAFIELD_TYPE_ZIP64);
@@ -5566,7 +5722,7 @@
5566
5722
  }
5567
5723
  }
5568
5724
  } else {
5569
- rawLocalExtraFieldZip64 = new Uint8Array();
5725
+ rawLocalExtraFieldZip64 = EMPTY_UINT8_ARRAY;
5570
5726
  }
5571
5727
  let rawExtraFieldAES;
5572
5728
  if (encrypted && !zipCrypto) {
@@ -5576,7 +5732,7 @@
5576
5732
  rawExtraFieldAES = extraFieldAES.array;
5577
5733
  rawExtraFieldAES[8] = encryptionStrength;
5578
5734
  } else {
5579
- rawExtraFieldAES = new Uint8Array();
5735
+ rawExtraFieldAES = EMPTY_UINT8_ARRAY;
5580
5736
  }
5581
5737
  let rawExtraFieldNTFS;
5582
5738
  let rawExtraFieldExtendedTimestamp;
@@ -5599,8 +5755,7 @@
5599
5755
  }
5600
5756
  rawExtraFieldExtendedTimestamp = extraFieldTimestamp.array;
5601
5757
  } else {
5602
- // the date cannot be stored as a 32-bit time_t; the NTFS field replaces it
5603
- rawExtraFieldExtendedTimestamp = new Uint8Array();
5758
+ rawExtraFieldExtendedTimestamp = EMPTY_UINT8_ARRAY;
5604
5759
  }
5605
5760
  try {
5606
5761
  const lastModTimeNTFS = getTimeNTFS(lastModDate);
@@ -5615,10 +5770,10 @@
5615
5770
  extraFieldNTFS.uint64(getTimeNTFS(creationDate) || lastModTimeNTFS);
5616
5771
  rawExtraFieldNTFS = extraFieldNTFS.array;
5617
5772
  } catch {
5618
- rawExtraFieldNTFS = new Uint8Array();
5773
+ rawExtraFieldNTFS = EMPTY_UINT8_ARRAY;
5619
5774
  }
5620
5775
  } else {
5621
- rawExtraFieldNTFS = rawExtraFieldExtendedTimestamp = new Uint8Array();
5776
+ rawExtraFieldNTFS = rawExtraFieldExtendedTimestamp = EMPTY_UINT8_ARRAY;
5622
5777
  }
5623
5778
  let rawExtraFieldUnix;
5624
5779
  try {
@@ -5644,10 +5799,10 @@
5644
5799
  extraFieldUnix.uint16((gid === UNDEFINED_VALUE ? 0 : gid) & MAX_16_BITS);
5645
5800
  rawExtraFieldUnix = extraFieldUnix.array;
5646
5801
  } else {
5647
- rawExtraFieldUnix = new Uint8Array();
5802
+ rawExtraFieldUnix = EMPTY_UINT8_ARRAY;
5648
5803
  }
5649
5804
  } catch {
5650
- rawExtraFieldUnix = new Uint8Array();
5805
+ rawExtraFieldUnix = EMPTY_UINT8_ARRAY;
5651
5806
  }
5652
5807
  if (compressionMethod === UNDEFINED_VALUE) {
5653
5808
  compressionMethod = compressed ? COMPRESSION_METHOD_DEFLATE : COMPRESSION_METHOD_STORE;
@@ -5714,7 +5869,7 @@
5714
5869
  version,
5715
5870
  compressionMethod,
5716
5871
  extraFieldExtendedTimestampFlag,
5717
- rawExtraFieldZip64: new Uint8Array(),
5872
+ rawExtraFieldZip64: EMPTY_UINT8_ARRAY,
5718
5873
  localExtraFieldZip64Length,
5719
5874
  rawExtraFieldExtendedTimestamp,
5720
5875
  rawExtraFieldNTFS,
@@ -5727,7 +5882,6 @@
5727
5882
  function appendExtraFieldUSDZ(entryInfo, zipWriterOffset) {
5728
5883
  const { headerInfo } = entryInfo;
5729
5884
  let { localHeaderArray, extraFieldLength } = headerInfo;
5730
- let localHeaderArrayView;
5731
5885
  let extraBytesLength = 64 - ((zipWriterOffset + getLength(localHeaderArray)) % 64);
5732
5886
  if (extraBytesLength < 4) {
5733
5887
  extraBytesLength += 64;
@@ -5740,7 +5894,7 @@
5740
5894
  headerInfo.localHeaderArray = localHeaderArray = new Uint8Array(getLength(previousLocalHeaderArray) + extraBytesLength);
5741
5895
  arraySet(localHeaderArray, previousLocalHeaderArray);
5742
5896
  arraySet(localHeaderArray, rawExtraFieldUSDZ, getLength(previousLocalHeaderArray));
5743
- localHeaderArrayView = getDataView(localHeaderArray);
5897
+ const localHeaderArrayView = getDataView(localHeaderArray);
5744
5898
  setUint16(localHeaderArrayView, 28, extraFieldLength + extraBytesLength);
5745
5899
  headerInfo.localHeaderView = localHeaderArrayView;
5746
5900
  entryInfo.metadataSize += extraBytesLength;
@@ -5748,7 +5902,7 @@
5748
5902
 
5749
5903
  function packUnixId(id) {
5750
5904
  if (id === UNDEFINED_VALUE) {
5751
- return new Uint8Array();
5905
+ return EMPTY_UINT8_ARRAY;
5752
5906
  } else {
5753
5907
  const dataArray = new Uint8Array(4);
5754
5908
  const dataView = getDataView(dataArray);
@@ -5791,7 +5945,7 @@
5791
5945
  dataDescriptor,
5792
5946
  dataDescriptorSignature
5793
5947
  }) {
5794
- let dataDescriptorArray = new Uint8Array();
5948
+ let dataDescriptorArray = EMPTY_UINT8_ARRAY;
5795
5949
  let dataDescriptorView, dataDescriptorOffset = 0;
5796
5950
  let dataDescriptorLength = zip64 ? DATA_DESCRIPTOR_RECORD_ZIP_64_LENGTH : DATA_DESCRIPTOR_RECORD_LENGTH;
5797
5951
  if (dataDescriptorSignature) {
@@ -5875,7 +6029,6 @@
5875
6029
  }
5876
6030
  }
5877
6031
  if (zip64 && localExtraFieldZip64Length) {
5878
- // the local zip64 extra field always contains both size slots
5879
6032
  const localHeaderOffset = HEADER_SIZE + getLength(rawFilename) + 4;
5880
6033
  setBigUint64(localHeaderView, localHeaderOffset, BigInt(uncompressedSize));
5881
6034
  setBigUint64(localHeaderView, localHeaderOffset + 8, BigInt(compressedSize));
@@ -5929,7 +6082,7 @@
5929
6082
  }
5930
6083
  rawExtraFieldZip64 = extraFieldZip64.array;
5931
6084
  } else {
5932
- rawExtraFieldZip64 = new Uint8Array();
6085
+ rawExtraFieldZip64 = EMPTY_UINT8_ARRAY;
5933
6086
  }
5934
6087
  fileEntry.rawExtraFieldZip64 = rawExtraFieldZip64;
5935
6088
  fileEntry.zip64Offset = zip64Offset;
@@ -5944,7 +6097,7 @@
5944
6097
  extraFieldTimestamp.uint32(lastModTimeUnix);
5945
6098
  rawExtraFieldTimestamp = extraFieldTimestamp.array;
5946
6099
  } else {
5947
- rawExtraFieldTimestamp = new Uint8Array();
6100
+ rawExtraFieldTimestamp = EMPTY_UINT8_ARRAY;
5948
6101
  }
5949
6102
  fileEntry.rawExtraFieldExtendedTimestamp = rawExtraFieldTimestamp;
5950
6103
  const extraFieldLength = getLength(
@@ -6000,7 +6153,7 @@
6000
6153
  await writeData(writer, directoryArray.slice(directoryDiskOffset, offset));
6001
6154
  directoryDiskOffset = offset;
6002
6155
  writer.availableSize = 0;
6003
- await writeData(writer, new Uint8Array());
6156
+ await writeData(writer, EMPTY_UINT8_ARRAY);
6004
6157
  }
6005
6158
  if (indexFileEntry == 0) {
6006
6159
  cdStartDiskNumber = writer.diskNumber;
@@ -6073,7 +6226,7 @@
6073
6226
  const endOfdirectoryRecord = createRecordWriter(zip64 ? ZIP64_END_OF_CENTRAL_DIR_TOTAL_LENGTH : END_OF_CENTRAL_DIR_LENGTH);
6074
6227
  if (getLength(endOfdirectoryRecord.array) + commentLength > writer.availableSize) {
6075
6228
  writer.availableSize = 0;
6076
- await writeData(writer, new Uint8Array());
6229
+ await writeData(writer, EMPTY_UINT8_ARRAY);
6077
6230
  }
6078
6231
  lastDiskNumber = writer.diskNumber;
6079
6232
  if (zip64) {
@@ -6170,7 +6323,6 @@
6170
6323
  return Math.floor(date.getTime() / 1000);
6171
6324
  }
6172
6325
 
6173
- // the extended timestamp extra field stores dates as signed 32-bit time_t values
6174
6326
  function inUnixTimeRange(timeUnix) {
6175
6327
  return timeUnix >= MIN_UNIX_TIME && timeUnix <= MAX_UNIX_TIME;
6176
6328
  }
@@ -6343,8 +6495,6 @@
6343
6495
  uncompressedSize: params.uncompressedSize || 0,
6344
6496
  passThrough: params.passThrough
6345
6497
  });
6346
- // detached entries (e.g. clones) are not registered until they are attached,
6347
- // otherwise they could never be garbage collected
6348
6498
  if (parent || !fs.root) {
6349
6499
  fs.entries[zipEntry.id] = zipEntry;
6350
6500
  }
@@ -6413,7 +6563,7 @@
6413
6563
  if (!writer || (writer.constructor == zipEntry.Writer && zipEntry.data)) {
6414
6564
  return zipEntry.data;
6415
6565
  } else {
6416
- const reader = zipEntry.reader = new zipEntry.Reader(zipEntry.data, options);
6566
+ const reader = zipEntry.reader = createReader(zipEntry.Reader, zipEntry.data, options);
6417
6567
  const uncompressedSize = zipEntry.data ? zipEntry.data.uncompressedSize : reader.size;
6418
6568
  await Promise.all([initStream(reader), initStream(writer, uncompressedSize)]);
6419
6569
  const { readable } = reader;
@@ -6469,11 +6619,7 @@
6469
6619
  }
6470
6620
 
6471
6621
  async getArrayBuffer(options) {
6472
- // this.data may be a string, a Uint8Array, a data URI or a Blob depending on how the entry
6473
- // was created, so retrieve the bytes through getUint8Array rather than assuming a Blob; this
6474
- // also applies the options (password, signal, onprogress...) like the other getData() getters
6475
6622
  const array = await this.getUint8Array(options);
6476
- // return the backing buffer directly when the array spans it, slicing the exact bytes otherwise
6477
6623
  return array.byteOffset || array.byteLength != array.buffer.byteLength
6478
6624
  ? array.buffer.slice(array.byteOffset, array.byteOffset + array.byteLength)
6479
6625
  : array.buffer;
@@ -6679,6 +6825,10 @@
6679
6825
  return writable;
6680
6826
  }
6681
6827
 
6828
+ exportFileSystemHandle(handle, options = {}) {
6829
+ return exportFileSystemHandle(this, handle, options);
6830
+ }
6831
+
6682
6832
  async importZip(reader, options = {}) {
6683
6833
  await initStream(reader);
6684
6834
  const zipReader = new ZipReader(reader, options);
@@ -6839,7 +6989,6 @@
6839
6989
  node = node.getChildByName(path[index]);
6840
6990
  }
6841
6991
  if (!node) {
6842
- // exclude detached entries, e.g. clones registered in `entries` but not attached
6843
6992
  node = this.entries.find(entry => entry && (entry == this.root || entry.isDescendantOf(this.root)) &&
6844
6993
  entry.getRelativeName() == fullname);
6845
6994
  }
@@ -6944,6 +7093,10 @@
6944
7093
  return this.root.exportWritable(writable, options);
6945
7094
  }
6946
7095
 
7096
+ exportFileSystemHandle(handle, options) {
7097
+ return this.root.exportFileSystemHandle(handle, options);
7098
+ }
7099
+
6947
7100
  exportZip(writer, options) {
6948
7101
  return this.root.exportZip(writer, options);
6949
7102
  }
@@ -7007,10 +7160,13 @@
7007
7160
  };
7008
7161
  }
7009
7162
 
7163
+ function createReader(Reader, data, options) {
7164
+ return Reader.prototype ? new Reader(data, options) : Reader(data, options);
7165
+ }
7166
+
7010
7167
  async function initReaders(entry, options) {
7011
7168
  const fileEntries = [];
7012
7169
  const pendingEntries = [entry];
7013
- // per-export map so concurrent exports do not share (and lock) the same readers
7014
7170
  const readers = new Map();
7015
7171
  while (pendingEntries.length) {
7016
7172
  const pendingEntry = pendingEntries.pop();
@@ -7023,7 +7179,7 @@
7023
7179
  }
7024
7180
  }
7025
7181
  await Promise.all(fileEntries.map(async child => {
7026
- const reader = child.reader = new child.Reader(child.data, options);
7182
+ const reader = child.reader = createReader(child.Reader, child.data, options);
7027
7183
  readers.set(child, reader);
7028
7184
  try {
7029
7185
  await initStream(reader);
@@ -7181,6 +7337,53 @@
7181
7337
  }
7182
7338
  }
7183
7339
 
7340
+ async function exportFileSystemHandle(zipEntry, directoryHandle, options) {
7341
+ const totalSize = getTotalSize([zipEntry], "uncompressedSize");
7342
+ const writtenSizes = new Map();
7343
+ await exportChildren(zipEntry, directoryHandle);
7344
+ return directoryHandle;
7345
+
7346
+ async function exportChildren(entry, parentHandle) {
7347
+ if (options.concurrent) {
7348
+ const results = await Promise.allSettled(entry.children.map(child => exportChild(child, parentHandle)));
7349
+ const failedResult = results.find(result => result.status == "rejected");
7350
+ if (failedResult) {
7351
+ throw failedResult.reason;
7352
+ }
7353
+ } else {
7354
+ for (const child of entry.children) {
7355
+ await exportChild(child, parentHandle);
7356
+ }
7357
+ }
7358
+ }
7359
+
7360
+ async function exportChild(child, parentHandle) {
7361
+ try {
7362
+ if (child.directory) {
7363
+ const childDirectoryHandle = await parentHandle.getDirectoryHandle(child.name, { create: true });
7364
+ await exportChildren(child, childDirectoryHandle);
7365
+ } else {
7366
+ const fileHandle = await parentHandle.getFileHandle(child.name, { create: true });
7367
+ const writable = await fileHandle.createWritable();
7368
+ await child.getData({ writable }, Object.assign({}, options, {
7369
+ onprogress: async progress => {
7370
+ if (options.onprogress) {
7371
+ writtenSizes.set(child.id, progress);
7372
+ try {
7373
+ await options.onprogress(Array.from(writtenSizes.values()).reduce((previousValue, currentValue) => previousValue + currentValue, 0), totalSize);
7374
+ } catch {
7375
+ // ignored
7376
+ }
7377
+ }
7378
+ }
7379
+ }));
7380
+ }
7381
+ } catch (error) {
7382
+ throw new Error(error.message + (child ? " (" + child.name + ")" : ""), { cause: error });
7383
+ }
7384
+ }
7385
+ }
7386
+
7184
7387
  async function transformToFileSystemhandle(entry) {
7185
7388
  const handle = {
7186
7389
  name: entry.name