@zip.js/zip.js 2.8.30 → 2.8.32

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 +82 -1
  2. package/deno.json +5 -4
  3. package/dist/zip-core.js +403 -339
  4. package/dist/zip-core.min.js +1 -1
  5. package/dist/zip-fs-core.js +116 -278
  6. package/dist/zip-fs-core.min.js +1 -1
  7. package/dist/zip-fs-native.js +440 -378
  8. package/dist/zip-fs-native.min.js +1 -1
  9. package/dist/zip-fs.js +439 -377
  10. package/dist/zip-fs.min.js +1 -1
  11. package/dist/zip-legacy.js +408 -344
  12. package/dist/zip-legacy.min.js +1 -1
  13. package/dist/zip-module.wasm +0 -0
  14. package/dist/zip-native.js +411 -347
  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 +410 -346
  19. package/dist/zip.min.js +1 -1
  20. package/index-native.cjs +440 -378
  21. package/index-native.min.js +1 -1
  22. package/index.cjs +439 -377
  23. package/index.d.cts +2853 -0
  24. package/index.d.ts +278 -1
  25. package/index.min.js +1 -1
  26. package/lib/core/codec-worker.js +1 -11
  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/streams/aes-crypto-stream.js +16 -20
  31. package/lib/core/streams/common-crypto.js +0 -2
  32. package/lib/core/streams/zip-crypto-stream.js +12 -16
  33. package/lib/core/streams/zip-entry-stream.js +2 -32
  34. package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
  35. package/lib/core/streams/zlib-wasm/zlib-streams-loader.js +1 -3
  36. package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
  37. package/lib/core/util/base64.js +68 -0
  38. package/lib/core/util/blob-temp-stream.js +124 -0
  39. package/lib/core/util/decode-text.js +0 -1
  40. package/lib/core/util/default-mime-type.js +0 -3
  41. package/lib/core/util/inflate.js +194 -0
  42. package/lib/core/util/mime-type.js +26 -22
  43. package/lib/core/util/opfs-temp-stream.js +1 -37
  44. package/lib/core/util/sync-access-handle-temp-stream.js +147 -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 +6 -13
  49. package/lib/core/zip-reader.js +48 -127
  50. package/lib/core/zip-writer.js +27 -45
  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 +10 -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));
@@ -1971,8 +1964,6 @@
1971
1964
  if (GET_RANDOM_VALUES_SUPPORTED) {
1972
1965
  return crypto.getRandomValues(array);
1973
1966
  } else {
1974
- // all supported runtimes expose crypto.getRandomValues; failing hard beats
1975
- // silently generating predictable encryption salts
1976
1967
  throw new Error(ERR_UNSUPPORTED_CRYPTO_API);
1977
1968
  }
1978
1969
  }
@@ -2035,12 +2026,7 @@
2035
2026
  constructor({ password, rawPassword, encryptionStrength, checkPasswordOnly }) {
2036
2027
  super({
2037
2028
  start() {
2038
- Object.assign(this, {
2039
- ready: new Promise(resolve => this.resolveReady = resolve),
2040
- password: encodePassword(password, rawPassword),
2041
- strength: encryptionStrength - 1,
2042
- pending: new Uint8Array()
2043
- });
2029
+ initAesCrypto(this, password, rawPassword, encryptionStrength);
2044
2030
  },
2045
2031
  async transform(chunk, controller) {
2046
2032
  const aesCrypto = this;
@@ -2075,7 +2061,7 @@
2075
2061
  await ready;
2076
2062
  const chunkToDecrypt = subarray(pending, 0, pending.length - SIGNATURE_LENGTH);
2077
2063
  const originalSignature = subarray(pending, pending.length - SIGNATURE_LENGTH);
2078
- let decryptedChunkArray = new Uint8Array();
2064
+ let decryptedChunkArray = EMPTY_UINT8_ARRAY;
2079
2065
  if (chunkToDecrypt.length) {
2080
2066
  const encryptedChunk = toBits(codecBytes, chunkToDecrypt);
2081
2067
  hmac.update(encryptedChunk);
@@ -2083,8 +2069,6 @@
2083
2069
  decryptedChunkArray = fromBits(codecBytes, decryptedChunk);
2084
2070
  }
2085
2071
  const signature = subarray(fromBits(codecBytes, hmac.digest()), 0, SIGNATURE_LENGTH);
2086
- // branch-free comparison: accumulate the differences instead of
2087
- // short-circuiting on the first mismatching byte
2088
2072
  let invalidSignature = pending.length < SIGNATURE_LENGTH ? 1 : 0;
2089
2073
  for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
2090
2074
  invalidSignature |= signature[indexSignature] ^ originalSignature[indexSignature];
@@ -2106,12 +2090,7 @@
2106
2090
  let stream;
2107
2091
  super({
2108
2092
  start() {
2109
- Object.assign(this, {
2110
- ready: new Promise(resolve => this.resolveReady = resolve),
2111
- password: encodePassword(password, rawPassword),
2112
- strength: encryptionStrength - 1,
2113
- pending: new Uint8Array()
2114
- });
2093
+ initAesCrypto(this, password, rawPassword, encryptionStrength);
2115
2094
  },
2116
2095
  async transform(chunk, controller) {
2117
2096
  const aesCrypto = this;
@@ -2121,7 +2100,7 @@
2121
2100
  resolveReady,
2122
2101
  ready
2123
2102
  } = aesCrypto;
2124
- let preamble = new Uint8Array();
2103
+ let preamble = EMPTY_UINT8_ARRAY;
2125
2104
  if (password) {
2126
2105
  preamble = await createEncryptionKeys(aesCrypto, strength, password);
2127
2106
  resolveReady();
@@ -2141,7 +2120,7 @@
2141
2120
  } = this;
2142
2121
  if (hmac && ctr) {
2143
2122
  await ready;
2144
- let encryptedChunkArray = new Uint8Array();
2123
+ let encryptedChunkArray = EMPTY_UINT8_ARRAY;
2145
2124
  if (pending.length) {
2146
2125
  const encryptedChunk = ctr.update(toBits(codecBytes, pending));
2147
2126
  hmac.update(encryptedChunk);
@@ -2156,6 +2135,15 @@
2156
2135
  }
2157
2136
  }
2158
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
+
2159
2147
  function append(aesCrypto, input, output, paddingStart, paddingEnd, verifySignature) {
2160
2148
  const {
2161
2149
  ctr,
@@ -2317,20 +2305,13 @@
2317
2305
  constructor({ password, rawPassword, passwordVerification, checkPasswordOnly }) {
2318
2306
  super({
2319
2307
  start() {
2320
- Object.assign(this, {
2321
- password,
2322
- rawPassword,
2323
- passwordVerification
2324
- });
2325
- createKeys(this, password, rawPassword);
2308
+ initZipCrypto(this, password, rawPassword, passwordVerification);
2326
2309
  },
2327
2310
  transform(chunk, controller) {
2328
2311
  const zipCrypto = this;
2329
2312
  if (zipCrypto.password || zipCrypto.rawPassword) {
2330
2313
  const decryptedHeader = decrypt(zipCrypto, chunk.subarray(0, HEADER_LENGTH));
2331
2314
  zipCrypto.password = zipCrypto.rawPassword = null;
2332
- // verify the password with a branch-free comparison of the verification byte, mirroring
2333
- // the constant-time AES HMAC signature check
2334
2315
  if ((decryptedHeader.at(-1) ^ zipCrypto.passwordVerification) != 0) {
2335
2316
  throw new Error(ERR_INVALID_PASSWORD);
2336
2317
  }
@@ -2351,12 +2332,7 @@
2351
2332
  constructor({ password, rawPassword, passwordVerification }) {
2352
2333
  super({
2353
2334
  start() {
2354
- Object.assign(this, {
2355
- password,
2356
- rawPassword,
2357
- passwordVerification
2358
- });
2359
- createKeys(this, password, rawPassword);
2335
+ initZipCrypto(this, password, rawPassword, passwordVerification);
2360
2336
  },
2361
2337
  transform(chunk, controller) {
2362
2338
  const zipCrypto = this;
@@ -2380,6 +2356,15 @@
2380
2356
  }
2381
2357
  }
2382
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
+
2383
2368
  function decrypt(target, input) {
2384
2369
  const output = new Uint8Array(input.length);
2385
2370
  for (let index = 0; index < input.length; index++) {
@@ -2484,14 +2469,6 @@
2484
2469
  const stream = this;
2485
2470
  let crc32Stream, encryptionStream, gzipCrc32Stream;
2486
2471
  let readable = super.readable;
2487
- // The gzip trailer carries a CRC-32 of the uncompressed data (same polynomial as zip),
2488
- // computed in native code for free during compression. On the native CompressionStream,
2489
- // harvest it instead of running a separate CRC pass: compress as gzip, then strip the
2490
- // fixed 10-byte header and 8-byte trailer to recover the exact raw-deflate payload
2491
- // (byte-identical to a deflate-raw compression) while capturing the CRC. The CRC becomes
2492
- // available at end-of-stream, exactly like Crc32Stream, so nothing downstream (data
2493
- // descriptor, central directory, ZipCrypto) needs it any earlier. Not applied for the
2494
- // pure-JS/WASM ports (a separate slice-by-8 CRC pass is as fast or faster there).
2495
2472
  const useGzipCrc32 = signed && compressed && !deflate64 && (!encrypted || zipCrypto) &&
2496
2473
  Boolean(useCompressionStream && CompressionStream);
2497
2474
  if ((!encrypted || zipCrypto) && signed && !useGzipCrc32) {
@@ -2528,12 +2505,6 @@
2528
2505
  }
2529
2506
  }
2530
2507
 
2531
- // Converts a gzip stream into its raw-deflate payload while capturing the CRC-32 from the gzip
2532
- // trailer. The CompressionStream gzip header is always the fixed 10-byte form (FLG=0, no optional
2533
- // fields) per the Compression Streams spec, so it is stripped by length; the trailer is the last
2534
- // 8 bytes (CRC-32 LE, then ISIZE LE). Bounded memory: at most GZIP_TRAILER_LENGTH bytes are held
2535
- // back across chunks. The signature is read big-endian-agnostically as a number, matching the
2536
- // value Crc32Stream produces, so the writer path is unchanged.
2537
2508
  class GzipToRawDeflateStream extends TransformStream {
2538
2509
 
2539
2510
  constructor() {
@@ -2559,10 +2530,6 @@
2559
2530
  tail = pending;
2560
2531
  return;
2561
2532
  }
2562
- // Emit everything except the trailing GZIP_TRAILER_LENGTH bytes as a standalone,
2563
- // right-sized Uint8Array. Consumers may read chunk.buffer directly (e.g. custom
2564
- // writers), so an aliased subarray of a larger buffer would leak the held-back
2565
- // trailer bytes. Bytes are copied exactly once, into `output` or `tail`.
2566
2533
  const emitLength = available - GZIP_TRAILER_LENGTH;
2567
2534
  const output = new Uint8Array(emitLength);
2568
2535
  const fromTail = Math.min(emitLength, tail.length);
@@ -2651,11 +2618,6 @@
2651
2618
  throw error;
2652
2619
  }
2653
2620
  }
2654
- // The native CompressionStream/DecompressionStream and the pure-JS zlib port do not signal
2655
- // backpressure on their writable side (their `ready` never pends), so a plain pipeThrough drains
2656
- // the whole source into them and peak memory grows with the entry size. Awaiting each write()
2657
- // paces the source to the codec's consumption rate, keeping streaming memory bounded. The WASM
2658
- // codec already backpressures, so this is a no-op cost for it.
2659
2621
  return pipeThroughBackpressured(readable, codecStream);
2660
2622
  }
2661
2623
 
@@ -2663,10 +2625,6 @@
2663
2625
  return readable.pipeThrough(transformStream);
2664
2626
  }
2665
2627
 
2666
- // Like readable.pipeThrough(transformStream), but drives the writable side manually and awaits each
2667
- // write() so that a codec which under-reports backpressure cannot pull the whole source into memory.
2668
- // Errors propagate in both directions: a source error aborts the codec; a downstream cancel/error
2669
- // (surfaced through the codec's writable rejecting) cancels the source.
2670
2628
  function pipeThroughBackpressured(readable, transformStream) {
2671
2629
  const writer = transformStream.writable.getWriter();
2672
2630
  const reader = readable.getReader();
@@ -2695,7 +2653,7 @@
2695
2653
  try {
2696
2654
  await writer.abort(error);
2697
2655
  } catch {
2698
- // the writable may already be errored/closed
2656
+ // ignored: the writable may already be errored/closed
2699
2657
  }
2700
2658
  }
2701
2659
 
@@ -2703,13 +2661,10 @@
2703
2661
  try {
2704
2662
  await reader.cancel(error);
2705
2663
  } catch {
2706
- // the readable may already be errored/closed
2664
+ // ignored: the readable may already be errored/closed
2707
2665
  }
2708
2666
  }
2709
2667
 
2710
- // DecompressionStream implementations can fail with a message-less TypeError on malformed
2711
- // input; give these errors an identifiable message (errors raised by the other stages of
2712
- // the pipeline, e.g. ERR_INVALID_PASSWORD, always carry a message and pass through as-is)
2713
2668
  function mapInflateStreamError(readable) {
2714
2669
  const reader = readable.getReader();
2715
2670
  return new ReadableStream({
@@ -3034,9 +2989,6 @@
3034
2989
  try {
3035
2990
  await initModule(config);
3036
2991
  } catch {
3037
- // The WASM module failed to load. Fall back to the native CompressionStream only if the
3038
- // zlib codec that would otherwise run depends on that module; a self-contained codec
3039
- // supplied through config (e.g. the pure-JS port) stays usable and is kept as requested.
3040
2992
  const ZlibStream = options.codecType.startsWith(CODEC_DEFLATE) ?
3041
2993
  config.CompressionStreamZlib :
3042
2994
  config.DecompressionStreamZlib;
@@ -3114,7 +3066,6 @@
3114
3066
  return resultValue;
3115
3067
  } catch (error) {
3116
3068
  await closeWritable();
3117
- // unblock the pipe watcher if the worker died before closing its end of the stream
3118
3069
  abortPipe();
3119
3070
  try {
3120
3071
  await closed;
@@ -3139,8 +3090,6 @@
3139
3090
  const abortController = new AbortController();
3140
3091
  const { writable, readable } = new TransformStream();
3141
3092
  const closed = readable.pipeTo(writableSource, { preventClose: true, preventAbort: true, signal: abortController.signal });
3142
- // the pipe can fail before `closed` is awaited (e.g. the destination write fails);
3143
- // mark the rejection as handled so it cannot kill the process in the meantime
3144
3093
  closed.catch(() => { });
3145
3094
  return { writable, closed, abortPipe: () => abortController.abort() };
3146
3095
  }
@@ -3233,8 +3182,6 @@
3233
3182
  event.preventDefault();
3234
3183
  }
3235
3184
  const { rejectResult, writer, onTaskFinished } = workerData;
3236
- // the worker is unusable (e.g. its script failed to load); terminate it and
3237
- // fail the current task instead of letting it hang forever
3238
3185
  terminateWorker$1(workerData);
3239
3186
  if (rejectResult) {
3240
3187
  rejectResult(event.error || new Error(event.message || ERROR_EVENT_TYPE));
@@ -3250,7 +3197,7 @@
3250
3197
  const { value, readable, writable } = message;
3251
3198
  const transferables = [];
3252
3199
  if (value) {
3253
- message.value = value;
3200
+ message.value = value.byteOffset || value.byteLength != value.buffer.byteLength ? new Uint8Array(value) : value;
3254
3201
  transferables.push(message.value.buffer);
3255
3202
  }
3256
3203
  if (transferStreams && transferStreamsSupported) {
@@ -3286,8 +3233,6 @@
3286
3233
  async function onMessage({ data }, workerData) {
3287
3234
  const { type, value, messageId, result, error } = data;
3288
3235
  const { reader, writer, resolveResult, rejectResult, onTaskFinished, generation } = workerData;
3289
- // continuations below can settle after the worker slot has been reassigned to a new
3290
- // task (e.g. a pending pull read failing late); they must not touch the new task
3291
3236
  const stale = () => workerData.generation != generation;
3292
3237
  try {
3293
3238
  if (error) {
@@ -3551,7 +3496,6 @@
3551
3496
  if (encoding && encoding.trim().toLowerCase() == "cp437") {
3552
3497
  return decodeCP437(value);
3553
3498
  } else {
3554
- // ignoreBOM keeps a leading U+FEFF in filenames/comments instead of dropping it
3555
3499
  return new TextDecoder(encoding, { ignoreBOM: true }).decode(value);
3556
3500
  }
3557
3501
  }
@@ -3799,8 +3743,6 @@
3799
3743
  const CHARSET_UTF8 = "utf-8";
3800
3744
  const PROPERTY_NAME_UTF8_SUFFIX = "UTF8";
3801
3745
  const CHARSET_CP437 = "cp437";
3802
- // bits of the general purpose flag that change how an entry is interpreted; the level bits (1-2) are excluded
3803
- // because they are informative only
3804
3746
  const BITFLAG_AMBIGUITY_MASK = BITFLAG_ENCRYPTED | BITFLAG_DATA_DESCRIPTOR | BITFLAG_LANG_ENCODING_FLAG;
3805
3747
  const ZIP64_PROPERTIES = [
3806
3748
  [PROPERTY_NAME_UNCOMPRESSED_SIZE, MAX_32_BITS],
@@ -3842,7 +3784,6 @@
3842
3784
  if (reader.size < END_OF_CENTRAL_DIR_LENGTH) {
3843
3785
  throw new Error(ERR_BAD_FORMAT);
3844
3786
  }
3845
- reader.chunkSize = getChunkSize(config);
3846
3787
  const strictness = getStrictness(getOptionValue$1(zipReader, options, OPTION_STRICTNESS), getOptionValue$1(zipReader, options, OPTION_CHECK_AMBIGUITY));
3847
3788
  const checkAmbiguity = strictness == STRICTNESS_STRICT;
3848
3789
  const rejectAmbiguousEndOfDirectory = strictness != STRICTNESS_TOLERANT;
@@ -3857,8 +3798,6 @@
3857
3798
  throw new Error(ERR_EOCDR_NOT_FOUND);
3858
3799
  }
3859
3800
  }
3860
- // two or more end-anchored records that each dereference to a valid central directory cannot be told
3861
- // apart (see comment_length ⟺ EOF): the archive is genuinely ambiguous, so refuse rather than guess
3862
3801
  if (rejectAmbiguousEndOfDirectory && endOfDirectoryReachingEndCount > 1) {
3863
3802
  throwAmbiguousArchive("multiple end of central directory records");
3864
3803
  }
@@ -3881,7 +3820,7 @@
3881
3820
  if (directoryDataOffset == MAX_32_BITS || directoryDataLength == MAX_32_BITS || filesLength == MAX_16_BITS || diskNumber == MAX_16_BITS) {
3882
3821
  const endOfDirectoryLocatorArray = endOfDirectoryInfo.offset >= ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH ?
3883
3822
  await readUint8Array(reader, endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH) :
3884
- new Uint8Array();
3823
+ EMPTY_UINT8_ARRAY;
3885
3824
  const endOfDirectoryLocatorView = getDataView$1(endOfDirectoryLocatorArray);
3886
3825
  if (endOfDirectoryLocatorArray.length == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH &&
3887
3826
  getUint32(endOfDirectoryLocatorView, 0) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
@@ -3948,12 +3887,6 @@
3948
3887
  }
3949
3888
  const expectedDirectoryDataOffset = centralDirectoryEndOffset - directoryDataLength - (reader.lastDiskOffset || 0);
3950
3889
  if (directoryDataOffset != expectedDirectoryDataOffset && diskNumber == lastDiskNumber) {
3951
- // the reconciled offset (the directory ends exactly where the canonical record begins) is anchored
3952
- // to the unforgeable end of the archive; the stored offset is not. Prefer the reconciled offset
3953
- // unless the stored one points at a directory and the reconciled one does not — that exception
3954
- // keeps a corrupt declared directory length from sending the read astray, while still moving off a
3955
- // stored offset that only looks valid because an append remnant left the previous (identically
3956
- // laid out) directory sitting there.
3957
3890
  const storedPointsAtDirectory = getUint32(directoryView, offset) == CENTRAL_FILE_HEADER_SIGNATURE;
3958
3891
  let reconcile = !storedPointsAtDirectory;
3959
3892
  if (!reconcile && expectedDirectoryDataOffset >= 0 && expectedDirectoryDataOffset + 4 <= reader.size) {
@@ -4110,11 +4043,11 @@
4110
4043
  const extractPrependedData = getOptionValue$1(zipReader, options, OPTION_EXTRACT_PREPENDED_DATA);
4111
4044
  const extractAppendedData = getOptionValue$1(zipReader, options, OPTION_EXTRACT_APPENDED_DATA);
4112
4045
  if (extractPrependedData) {
4113
- zipReader.prependedData = startOffset > 0 ? await readUint8Array(reader, 0, startOffset) : new Uint8Array();
4046
+ zipReader.prependedData = startOffset > 0 ? await readUint8Array(reader, 0, startOffset) : EMPTY_UINT8_ARRAY;
4114
4047
  }
4115
- 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;
4116
4049
  if (extractAppendedData) {
4117
- 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;
4118
4051
  }
4119
4052
  return true;
4120
4053
  }
@@ -4186,7 +4119,7 @@
4186
4119
  filenameLength
4187
4120
  } = localDirectory;
4188
4121
  const checkAmbiguity = getStrictness(getOptionValue$1(zipEntry, options, OPTION_STRICTNESS), getOptionValue$1(zipEntry, options, OPTION_CHECK_AMBIGUITY)) == STRICTNESS_STRICT;
4189
- let rawLocalFilename = new Uint8Array();
4122
+ let rawLocalFilename = EMPTY_UINT8_ARRAY;
4190
4123
  if (checkAmbiguity && (filenameLength || extraFieldLength)) {
4191
4124
  const trailingDataArray = await readUint8Array(reader, offset + HEADER_SIZE, filenameLength + extraFieldLength, diskNumberStart);
4192
4125
  rawLocalFilename = trailingDataArray.subarray(0, filenameLength);
@@ -4194,7 +4127,7 @@
4194
4127
  } else {
4195
4128
  localDirectory.rawExtraField = extraFieldLength ?
4196
4129
  await readUint8Array(reader, offset + HEADER_SIZE + filenameLength, extraFieldLength, diskNumberStart) :
4197
- new Uint8Array();
4130
+ EMPTY_UINT8_ARRAY;
4198
4131
  }
4199
4132
  readCommonFooter(zipEntry, localDirectory, dataView, 4, true);
4200
4133
  if (checkAmbiguity) {
@@ -4221,12 +4154,7 @@
4221
4154
  }
4222
4155
  const dataOffset = offset + HEADER_SIZE + filenameLength + extraFieldLength;
4223
4156
  const size = compressedSize;
4224
- const readable = reader.readable;
4225
- Object.assign(readable, {
4226
- diskNumberStart,
4227
- offset: dataOffset,
4228
- size
4229
- });
4157
+ const readable = reader.createReadable({ offset: dataOffset, size, diskNumberStart, chunkSize: getChunkSize(config) });
4230
4158
  const signal = getOptionValue$1(zipEntry, options, OPTION_SIGNAL);
4231
4159
  const checkPasswordOnly = getOptionValue$1(zipEntry, options, OPTION_CHECK_PASSWORD_ONLY);
4232
4160
  let checkOverlappingEntry = getOptionValue$1(zipEntry, options, OPTION_CHECK_OVERLAPPING_ENTRY);
@@ -4497,7 +4425,6 @@
4497
4425
  const view = getDataView$1(new Uint8Array(extraField.data));
4498
4426
  let uid, gid;
4499
4427
  if (isInfoZip) {
4500
- // Info-ZIP New Unix "ux" field (0x7875): version + variable-length uid/gid
4501
4428
  let offset = 0;
4502
4429
  const version = getUint8(view, offset++);
4503
4430
  const uidSize = getUint8(view, offset++);
@@ -4507,8 +4434,6 @@
4507
4434
  gid = unpackUnixId(extraField.data.subarray(offset, offset + gidSize));
4508
4435
  Object.assign(extraField, { version, uid, gid });
4509
4436
  } else if (extraField.data.length >= 4) {
4510
- // Info-ZIP Unix "Ux" field (0x7855): fixed 2-byte uid + gid (the file mode, if any, is
4511
- // carried in the external file attributes, not here)
4512
4437
  uid = getUint16(view, 0);
4513
4438
  gid = getUint16(view, 2);
4514
4439
  Object.assign(extraField, { uid, gid });
@@ -4560,7 +4485,6 @@
4560
4485
  timeProperties.forEach((propertyName, indexProperty) => {
4561
4486
  if (extraFieldExtendedTimestamp.data.length >= offset + 4) {
4562
4487
  const time = getUint32(extraFieldView, offset);
4563
- // the extended timestamp is a signed 32-bit time_t value
4564
4488
  directory[propertyName] = extraFieldExtendedTimestamp[propertyName] = new Date((time | 0) * 1000);
4565
4489
  const rawPropertyName = timeRawProperties[indexProperty];
4566
4490
  extraFieldExtendedTimestamp[rawPropertyName] = time;
@@ -4637,7 +4561,6 @@
4637
4561
 
4638
4562
  function getStrictness(strictness, checkAmbiguity) {
4639
4563
  if (strictness === UNDEFINED_VALUE) {
4640
- // `checkAmbiguity: true` is kept as a backward-compatible alias for the strictest mode
4641
4564
  return checkAmbiguity ? STRICTNESS_STRICT : STRICTNESS_BALANCED;
4642
4565
  }
4643
4566
  return strictness;
@@ -4648,81 +4571,45 @@
4648
4571
  return maxAppendedDataSize;
4649
4572
  }
4650
4573
  if (strictness == STRICTNESS_STRICT) {
4651
- // the comment must reach the end of the file: no trailing data tolerated
4652
4574
  return 0;
4653
4575
  }
4654
4576
  if (strictness == STRICTNESS_TOLERANT) {
4655
4577
  return Infinity;
4656
4578
  }
4657
- // balanced: tolerate up to a legal comment's worth of trailing data (a 16-bit length); beyond that the
4658
- // trailing bytes cannot hide inside a comment, so they are treated as a second thing bolted onto the file
4659
4579
  return MAX_16_BITS;
4660
4580
  }
4661
4581
 
4662
- // cap on how many out-of-window central directory probes the scan below may issue. A legitimate archive needs
4663
- // at most a couple (the canonical record, plus one more to detect genuine ambiguity); every other reachability
4664
- // check is served from the tail window already in memory. The cap keeps an archive whose comment is stuffed
4665
- // with unreachable end-anchored records from forcing an unbounded number of (potentially remote) reads.
4666
4582
  const MAX_END_OF_CENTRAL_DIR_PROBES = 64;
4667
4583
 
4668
- // reachability rankings for an end-anchored candidate record (see getCentralDirectoryReachability)
4669
4584
  const CENTRAL_DIRECTORY_UNREACHABLE = 0;
4670
4585
  const CENTRAL_DIRECTORY_PLAUSIBLE = 1;
4671
4586
  const CENTRAL_DIRECTORY_REACHABLE = 2;
4672
4587
 
4673
- // Locates the authoritative end of central directory record. The canonical record is the last one whose
4674
- // declared comment reaches exactly the end of the file ("end-anchored") and that points to a central directory
4675
- // ("reachable"); earlier signatures (stale append remnants, bytes embedded in a comment) are ignored. When two
4676
- // or more end-anchored records point to a central directory the archive is ambiguous, and the count is returned
4677
- // so the caller can refuse it. A record that reaches the end of the file but points to no central directory (an
4678
- // empty archive) is only "plausible": it cannot be corroborated, so it never counts towards ambiguity and is
4679
- // chosen only when no reachable record exists — otherwise an empty record forged in a comment could outrank the
4680
- // genuine directory. When no record is end-anchored (appended data or an oversized comment), falls back to the
4681
- // last record within the tolerated window that points to a directory (see seekEndOfCentralDirectory).
4682
4588
  async function findEndOfCentralDirectory(reader, rejectAmbiguous, maxAppendedDataSize) {
4683
4589
  const { size } = reader;
4684
- // an end-anchored record can only live within the last END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS bytes,
4685
- // since the comment length that ties it to the end of the file is a 16-bit field
4686
4590
  const anchoredLength = Math.min(size, END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS);
4687
- const anchoredOffset = size - anchoredLength;
4688
- const anchoredArray = await readUint8Array(reader, anchoredOffset, anchoredLength);
4689
- const anchoredView = getDataView$1(anchoredArray);
4690
4591
  const remoteProbeBudget = { count: MAX_END_OF_CENTRAL_DIR_PROBES };
4691
4592
  let endOfDirectoryInfo;
4692
4593
  let plausibleEndOfDirectoryInfo;
4693
4594
  let endOfDirectoryReachingEndCount = 0;
4694
- for (let indexByte = anchoredArray.length - END_OF_CENTRAL_DIR_LENGTH; indexByte >= 0; indexByte--) {
4695
- if (getUint32(anchoredView, indexByte) == END_OF_CENTRAL_DIR_SIGNATURE) {
4696
- const offset = anchoredOffset + indexByte;
4697
- const commentLength = getUint16(anchoredView, indexByte + 20);
4698
- // end-anchored: the declared comment extends exactly to the end of the file. The comment length is
4699
- // attacker-controlled, but the reconciliation against the (unforgeable) end of file is not.
4700
- if (offset + END_OF_CENTRAL_DIR_LENGTH + commentLength == size) {
4701
- const reachability = await getCentralDirectoryReachability(reader, anchoredView, anchoredOffset, indexByte, offset, size, remoteProbeBudget);
4702
- if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
4703
- if (!endOfDirectoryInfo) {
4704
- endOfDirectoryInfo = {
4705
- offset,
4706
- buffer: anchoredArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer
4707
- };
4708
- }
4709
- endOfDirectoryReachingEndCount++;
4710
- // the canonical record (highest offset) is found first; a second one is enough to flag ambiguity
4711
- if (!rejectAmbiguous || endOfDirectoryReachingEndCount > 1) {
4712
- break;
4713
- }
4714
- } else if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleEndOfDirectoryInfo) {
4715
- plausibleEndOfDirectoryInfo = {
4716
- offset,
4717
- buffer: anchoredArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer
4718
- };
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);
4719
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);
4720
4609
  }
4721
4610
  }
4722
4611
  }
4723
4612
  if (!endOfDirectoryInfo) {
4724
- // no record points to a directory: prefer a plausible (empty) end-anchored record before scanning the
4725
- // tolerated window for a bare signature
4726
4613
  endOfDirectoryInfo = plausibleEndOfDirectoryInfo;
4727
4614
  }
4728
4615
  if (!endOfDirectoryInfo) {
@@ -4731,69 +4618,53 @@
4731
4618
  return { endOfDirectoryInfo, endOfDirectoryReachingEndCount };
4732
4619
  }
4733
4620
 
4734
- // Fallback for findEndOfCentralDirectory when no record is end-anchored: appended data or an oversized comment
4735
- // pushed the record's declared comment past the end of the file, so its position can no longer be reconciled
4736
- // against the end of the file. Scans the tolerated window from the end and returns the first record that points
4737
- // to a central directory, so a stray signature embedded in appended data cannot hijack discovery (which the
4738
- // anchored scan already prevents for anchored records). Falls back to the first plausible (empty) record, then
4739
- // to the last bare signature, so a degenerate archive still opens as before. The window ends at the file, so the
4740
- // same in-window / metered reads as the anchored scan apply.
4741
4621
  async function seekEndOfCentralDirectory(reader, maxAppendedDataSize, remoteProbeBudget) {
4742
4622
  const { size } = reader;
4743
4623
  const searchLength = Math.min(size, maxAppendedDataSize == Infinity ? size :
4744
4624
  END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS + maxAppendedDataSize);
4745
- const searchOffset = size - searchLength;
4746
- const searchArray = await readUint8Array(reader, searchOffset, searchLength);
4747
- const searchView = getDataView$1(searchArray);
4748
4625
  let firstSignatureInfo, plausibleInfo;
4749
- for (let indexByte = searchArray.length - END_OF_CENTRAL_DIR_LENGTH; indexByte >= 0; indexByte--) {
4750
- if (getUint32(searchView, indexByte) == END_OF_CENTRAL_DIR_SIGNATURE) {
4751
- const offset = searchOffset + indexByte;
4752
- const record = { offset, buffer: searchArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer };
4753
- if (!firstSignatureInfo) {
4754
- firstSignatureInfo = record;
4755
- }
4756
- const reachability = await getCentralDirectoryReachability(reader, searchView, searchOffset, indexByte, offset, size, remoteProbeBudget);
4757
- if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
4758
- return record;
4759
- }
4760
- if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleInfo) {
4761
- plausibleInfo = record;
4762
- }
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;
4763
4637
  }
4764
4638
  }
4765
4639
  return plausibleInfo || firstSignatureInfo;
4766
4640
  }
4767
4641
 
4768
- // Ranks how strongly an end-anchored candidate record is backed by an actual central directory, used to decide
4769
- // which record is canonical and which count towards ambiguity. It is intentionally conservative and does not
4770
- // fully parse the directory (the caller does that for the canonical record):
4771
- // - REACHABLE: the record dereferences a central directory (a central file header signature at the
4772
- // reconciled offset), or, for a zip64 record, the zip64 locator that a genuine zip64 archive places
4773
- // immediately before it. Only these count towards ambiguity.
4774
- // - PLAUSIBLE: the record describes an empty archive, which has no central directory to dereference. Its
4775
- // fields are indistinguishable from an empty end of central directory record forged inside a comment, so it
4776
- // is accepted only as a last resort and never inflates the ambiguity count.
4777
- // - UNREACHABLE: neither — stale append remnant or signature bytes embedded in a comment.
4778
- // Signatures are read from the tail window (`view`, spanning `[anchoredOffset, size)`) when they fall inside it;
4779
- // only a target below the window costs a (possibly remote) read, metered by `remoteProbeBudget` so a stuffed
4780
- // comment cannot amplify into an unbounded number of requests.
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
+
4781
4657
  async function getCentralDirectoryReachability(reader, view, anchoredOffset, indexByte, offset, size, remoteProbeBudget) {
4782
4658
  const filesLength = getUint16(view, indexByte + 10);
4783
4659
  const directoryDataLength = getUint32(view, indexByte + 12);
4784
4660
  const directoryDataOffset = getUint32(view, indexByte + 16);
4785
4661
  if (filesLength == MAX_16_BITS || directoryDataLength == MAX_32_BITS || directoryDataOffset == MAX_32_BITS) {
4786
- // saturated fields imply a zip64 record; corroborate it with the zip64 end of central directory locator
4787
- // that sits immediately before it, so saturated bytes inside a comment cannot masquerade as one
4788
4662
  const locatorSignature = await readSignature(reader, view, anchoredOffset, offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, size, remoteProbeBudget);
4789
4663
  return locatorSignature == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE ? CENTRAL_DIRECTORY_REACHABLE : CENTRAL_DIRECTORY_UNREACHABLE;
4790
4664
  }
4791
4665
  if (!filesLength && !directoryDataLength) {
4792
- // a valid empty archive has no central directory to dereference
4793
4666
  return CENTRAL_DIRECTORY_PLAUSIBLE;
4794
4667
  }
4795
- // the central directory ends where this record starts; reconcile the stored offset against that actual
4796
- // position (see stored_cd_offset ⟺ eocd_pos − cd_size) so prepended data does not hide the directory
4797
4668
  for (const centralDirectoryOffset of [offset - directoryDataLength, directoryDataOffset]) {
4798
4669
  if (await readSignature(reader, view, anchoredOffset, centralDirectoryOffset, size, remoteProbeBudget) == CENTRAL_FILE_HEADER_SIGNATURE) {
4799
4670
  return CENTRAL_DIRECTORY_REACHABLE;
@@ -4802,15 +4673,11 @@
4802
4673
  return CENTRAL_DIRECTORY_UNREACHABLE;
4803
4674
  }
4804
4675
 
4805
- // Reads a 4-byte little-endian signature at `signatureOffset`, served from the tail window (`view`, spanning
4806
- // `[anchoredOffset, size)`) when it falls inside it and otherwise from a metered (possibly remote) read. Returns
4807
- // UNDEFINED_VALUE when the position is out of range or the out-of-window read budget is exhausted.
4808
4676
  async function readSignature(reader, view, anchoredOffset, signatureOffset, size, remoteProbeBudget) {
4809
4677
  if (signatureOffset < 0 || signatureOffset + 4 > size) {
4810
4678
  return UNDEFINED_VALUE;
4811
4679
  }
4812
4680
  if (signatureOffset >= anchoredOffset) {
4813
- // the target sits inside the tail window already in memory: no extra read needed
4814
4681
  return getUint32(view, signatureOffset - anchoredOffset);
4815
4682
  }
4816
4683
  if (remoteProbeBudget.count > 0) {
@@ -4818,7 +4685,6 @@
4818
4685
  const signatureArray = await readUint8Array(reader, signatureOffset, 4);
4819
4686
  return getUint32(getDataView$1(signatureArray), 0);
4820
4687
  }
4821
- // out of budget: leave this candidate unverified rather than issue another read
4822
4688
  return UNDEFINED_VALUE;
4823
4689
  }
4824
4690
 
@@ -4834,8 +4700,6 @@
4834
4700
  if (localDirectory.compressionMethod != zipEntry.compressionMethod) {
4835
4701
  throwAmbiguousArchive("mismatched local file header (compression method)");
4836
4702
  }
4837
- // when the data descriptor flag is set, the local signature and sizes are zeroed out; all-zero values are
4838
- // also tolerated when the flag is clear because some tools produce them (streaming writers predating bit 3)
4839
4703
  if (!localDirectory.bitFlag.dataDescriptor &&
4840
4704
  (localDirectory.signature || localDirectory.compressedSize || localDirectory.uncompressedSize) &&
4841
4705
  (localDirectory.signature != zipEntry.signature ||
@@ -4925,7 +4789,6 @@
4925
4789
  const ERR_INVALID_ENCRYPTION_STRENGTH = "The strength must equal 1, 2, or 3";
4926
4790
  const ERR_INVALID_EXTRAFIELD_TYPE = "Extra field type exceeds 65535";
4927
4791
  const ERR_INVALID_EXTRAFIELD_DATA = "Extra field data exceeds 64KB";
4928
- // same message as the constant exported by zip-reader.js
4929
4792
  const ERR_UNSUPPORTED_COMPRESSION = "Compression method not supported";
4930
4793
  const MIN_UNIX_TIME = -2147483648;
4931
4794
  const MAX_UNIX_TIME = 2147483647;
@@ -5008,12 +4871,12 @@
5008
4871
  rawExtraField,
5009
4872
  } = entry;
5010
4873
  const { level, languageEncodingFlag, dataDescriptor } = bitFlag;
5011
- rawExtraFieldZip64 = rawExtraFieldZip64 || new Uint8Array();
5012
- rawExtraFieldAES = rawExtraFieldAES || new Uint8Array();
5013
- rawExtraFieldExtendedTimestamp = rawExtraFieldExtendedTimestamp || new Uint8Array();
5014
- rawExtraFieldNTFS = rawExtraFieldNTFS || new Uint8Array();
5015
- rawExtraFieldUnix = rawExtraFieldUnix || new Uint8Array();
5016
- 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;
5017
4880
  if (entry.extraFieldAES) {
5018
4881
  compressionMethod = COMPRESSION_METHOD_AES;
5019
4882
  }
@@ -5119,7 +4982,7 @@
5119
4982
  return false;
5120
4983
  }
5121
4984
 
5122
- async close(comment = new Uint8Array(), options = {}) {
4985
+ async close(comment = EMPTY_UINT8_ARRAY, options = {}) {
5123
4986
  const zipWriter = this;
5124
4987
  const { pendingAddFileCalls, writer } = this;
5125
4988
  const { writable } = writer;
@@ -5356,7 +5219,7 @@
5356
5219
  if (!zipCrypto && (password !== UNDEFINED_VALUE || rawPassword !== UNDEFINED_VALUE) && !(encryptionStrength >= 1 && encryptionStrength <= 3)) {
5357
5220
  throw new Error(ERR_INVALID_ENCRYPTION_STRENGTH);
5358
5221
  }
5359
- let rawExtraField = new Uint8Array();
5222
+ let rawExtraField = EMPTY_UINT8_ARRAY;
5360
5223
  const extraField = options[PROPERTY_NAME_EXTRA_FIELD];
5361
5224
  if (extraField) {
5362
5225
  let extraFieldSize = 0;
@@ -5417,8 +5280,6 @@
5417
5280
  let maximumCompressedSize = 0;
5418
5281
  let uncompressedSize = 0;
5419
5282
  if (passThrough) {
5420
- // the headers of a passThrough entry describe the payload verbatim: without a
5421
- // reader they would declare content that is not there
5422
5283
  if (!reader) {
5423
5284
  throw new Error(ERR_UNDEFINED_READER);
5424
5285
  }
@@ -5429,8 +5290,6 @@
5429
5290
  }
5430
5291
  const zip64Enabled = zip64 === true;
5431
5292
  const encrypted = getOptionValue(zipWriter, options, PROPERTY_NAME_ENCRYPTED);
5432
- // entries without a reader have no payload: write them as empty stored files instead of
5433
- // declaring a compressed or encrypted content that does not exist
5434
5293
  const encryptedEntry = Boolean(reader) && (Boolean((password && getLength(password)) || (rawPassword && getLength(rawPassword))) || (passThrough && encrypted));
5435
5294
  if (!reader) {
5436
5295
  level = 0;
@@ -5568,7 +5427,6 @@
5568
5427
  fileEntry.diskNumberStart = writer.diskNumber;
5569
5428
  fileEntry.offset = getSegmentOffset(zipWriter, writer);
5570
5429
  if (usdz) {
5571
- // the padding depends on the entry offset, only known now that the writer is locked
5572
5430
  const previousMetadataSize = entryInfo.metadataSize;
5573
5431
  appendExtraFieldUSDZ(entryInfo, zipWriter.offset - writer.diskOffset);
5574
5432
  fileEntry.size += entryInfo.metadataSize - previousMetadataSize;
@@ -5611,10 +5469,6 @@
5611
5469
  if (releaseLockWriter) {
5612
5470
  releaseLockWriter();
5613
5471
  }
5614
- // A buffered entry uses a temporary stream (see `createTempStream`). When that stream is
5615
- // backed by a resource (a file, an OPFS handle, ...) it must be released on every exit path,
5616
- // including the error path where the stream is otherwise abandoned without being closed or
5617
- // cancelled. `dispose()` is optional and best-effort so it never masks the original outcome.
5618
5472
  if (bufferedWrite && fileWriter && fileWriter.dispose) {
5619
5473
  try {
5620
5474
  await fileWriter.dispose();
@@ -5641,7 +5495,7 @@
5641
5495
  async function skipDiskIfNeeded() {
5642
5496
  if (getLength(headerInfo.localHeaderArray) > writer.availableSize) {
5643
5497
  writer.availableSize = 0;
5644
- await writeData(writer, new Uint8Array());
5498
+ await writeData(writer, EMPTY_UINT8_ARRAY);
5645
5499
  }
5646
5500
  }
5647
5501
  }
@@ -5752,8 +5606,7 @@
5752
5606
  }
5753
5607
  const { writable } = writer;
5754
5608
  if (reader) {
5755
- reader.chunkSize = getChunkSize(config);
5756
- const readable = reader.readable;
5609
+ const readable = reader.createReadable ? reader.createReadable({ chunkSize: getChunkSize(config) }) : reader.readable;
5757
5610
  const size = reader.size;
5758
5611
  const workerOptions = {
5759
5612
  options: {
@@ -5853,13 +5706,9 @@
5853
5706
  : compressionMethod !== COMPRESSION_METHOD_STORE);
5854
5707
  let rawLocalExtraFieldZip64;
5855
5708
  const uncompressedFile = passThrough || !compressed;
5856
- // entries without a data descriptor are always buffered (cf. getFileEntry), so the
5857
- // zip64 extra field values can be patched into the local header before flushing it
5858
5709
  const zip64ExtraFieldComplete = zip64 && (options.bufferedWrite || !dataDescriptor || ((!zip64UncompressedSize && !zip64CompressedSize) || uncompressedFile));
5859
5710
  const writeLocalExtraFieldZip64 = zip64ExtraFieldComplete || (zip64 && dataDescriptor && (zip64UncompressedSize || zip64CompressedSize));
5860
5711
  if (zip64 && (zip64UncompressedSize || zip64CompressedSize)) {
5861
- // APPNOTE 4.5.3: the zip64 extra field of a local header must include both the
5862
- // uncompressed and compressed size fields
5863
5712
  const length = 4 + 16;
5864
5713
  const extraFieldZip64 = createRecordWriter(length);
5865
5714
  extraFieldZip64.uint16(EXTRAFIELD_TYPE_ZIP64);
@@ -5873,7 +5722,7 @@
5873
5722
  }
5874
5723
  }
5875
5724
  } else {
5876
- rawLocalExtraFieldZip64 = new Uint8Array();
5725
+ rawLocalExtraFieldZip64 = EMPTY_UINT8_ARRAY;
5877
5726
  }
5878
5727
  let rawExtraFieldAES;
5879
5728
  if (encrypted && !zipCrypto) {
@@ -5883,7 +5732,7 @@
5883
5732
  rawExtraFieldAES = extraFieldAES.array;
5884
5733
  rawExtraFieldAES[8] = encryptionStrength;
5885
5734
  } else {
5886
- rawExtraFieldAES = new Uint8Array();
5735
+ rawExtraFieldAES = EMPTY_UINT8_ARRAY;
5887
5736
  }
5888
5737
  let rawExtraFieldNTFS;
5889
5738
  let rawExtraFieldExtendedTimestamp;
@@ -5906,8 +5755,7 @@
5906
5755
  }
5907
5756
  rawExtraFieldExtendedTimestamp = extraFieldTimestamp.array;
5908
5757
  } else {
5909
- // the date cannot be stored as a 32-bit time_t; the NTFS field replaces it
5910
- rawExtraFieldExtendedTimestamp = new Uint8Array();
5758
+ rawExtraFieldExtendedTimestamp = EMPTY_UINT8_ARRAY;
5911
5759
  }
5912
5760
  try {
5913
5761
  const lastModTimeNTFS = getTimeNTFS(lastModDate);
@@ -5922,10 +5770,10 @@
5922
5770
  extraFieldNTFS.uint64(getTimeNTFS(creationDate) || lastModTimeNTFS);
5923
5771
  rawExtraFieldNTFS = extraFieldNTFS.array;
5924
5772
  } catch {
5925
- rawExtraFieldNTFS = new Uint8Array();
5773
+ rawExtraFieldNTFS = EMPTY_UINT8_ARRAY;
5926
5774
  }
5927
5775
  } else {
5928
- rawExtraFieldNTFS = rawExtraFieldExtendedTimestamp = new Uint8Array();
5776
+ rawExtraFieldNTFS = rawExtraFieldExtendedTimestamp = EMPTY_UINT8_ARRAY;
5929
5777
  }
5930
5778
  let rawExtraFieldUnix;
5931
5779
  try {
@@ -5951,10 +5799,10 @@
5951
5799
  extraFieldUnix.uint16((gid === UNDEFINED_VALUE ? 0 : gid) & MAX_16_BITS);
5952
5800
  rawExtraFieldUnix = extraFieldUnix.array;
5953
5801
  } else {
5954
- rawExtraFieldUnix = new Uint8Array();
5802
+ rawExtraFieldUnix = EMPTY_UINT8_ARRAY;
5955
5803
  }
5956
5804
  } catch {
5957
- rawExtraFieldUnix = new Uint8Array();
5805
+ rawExtraFieldUnix = EMPTY_UINT8_ARRAY;
5958
5806
  }
5959
5807
  if (compressionMethod === UNDEFINED_VALUE) {
5960
5808
  compressionMethod = compressed ? COMPRESSION_METHOD_DEFLATE : COMPRESSION_METHOD_STORE;
@@ -6021,7 +5869,7 @@
6021
5869
  version,
6022
5870
  compressionMethod,
6023
5871
  extraFieldExtendedTimestampFlag,
6024
- rawExtraFieldZip64: new Uint8Array(),
5872
+ rawExtraFieldZip64: EMPTY_UINT8_ARRAY,
6025
5873
  localExtraFieldZip64Length,
6026
5874
  rawExtraFieldExtendedTimestamp,
6027
5875
  rawExtraFieldNTFS,
@@ -6034,7 +5882,6 @@
6034
5882
  function appendExtraFieldUSDZ(entryInfo, zipWriterOffset) {
6035
5883
  const { headerInfo } = entryInfo;
6036
5884
  let { localHeaderArray, extraFieldLength } = headerInfo;
6037
- let localHeaderArrayView;
6038
5885
  let extraBytesLength = 64 - ((zipWriterOffset + getLength(localHeaderArray)) % 64);
6039
5886
  if (extraBytesLength < 4) {
6040
5887
  extraBytesLength += 64;
@@ -6047,7 +5894,7 @@
6047
5894
  headerInfo.localHeaderArray = localHeaderArray = new Uint8Array(getLength(previousLocalHeaderArray) + extraBytesLength);
6048
5895
  arraySet(localHeaderArray, previousLocalHeaderArray);
6049
5896
  arraySet(localHeaderArray, rawExtraFieldUSDZ, getLength(previousLocalHeaderArray));
6050
- localHeaderArrayView = getDataView(localHeaderArray);
5897
+ const localHeaderArrayView = getDataView(localHeaderArray);
6051
5898
  setUint16(localHeaderArrayView, 28, extraFieldLength + extraBytesLength);
6052
5899
  headerInfo.localHeaderView = localHeaderArrayView;
6053
5900
  entryInfo.metadataSize += extraBytesLength;
@@ -6055,7 +5902,7 @@
6055
5902
 
6056
5903
  function packUnixId(id) {
6057
5904
  if (id === UNDEFINED_VALUE) {
6058
- return new Uint8Array();
5905
+ return EMPTY_UINT8_ARRAY;
6059
5906
  } else {
6060
5907
  const dataArray = new Uint8Array(4);
6061
5908
  const dataView = getDataView(dataArray);
@@ -6098,7 +5945,7 @@
6098
5945
  dataDescriptor,
6099
5946
  dataDescriptorSignature
6100
5947
  }) {
6101
- let dataDescriptorArray = new Uint8Array();
5948
+ let dataDescriptorArray = EMPTY_UINT8_ARRAY;
6102
5949
  let dataDescriptorView, dataDescriptorOffset = 0;
6103
5950
  let dataDescriptorLength = zip64 ? DATA_DESCRIPTOR_RECORD_ZIP_64_LENGTH : DATA_DESCRIPTOR_RECORD_LENGTH;
6104
5951
  if (dataDescriptorSignature) {
@@ -6182,7 +6029,6 @@
6182
6029
  }
6183
6030
  }
6184
6031
  if (zip64 && localExtraFieldZip64Length) {
6185
- // the local zip64 extra field always contains both size slots
6186
6032
  const localHeaderOffset = HEADER_SIZE + getLength(rawFilename) + 4;
6187
6033
  setBigUint64(localHeaderView, localHeaderOffset, BigInt(uncompressedSize));
6188
6034
  setBigUint64(localHeaderView, localHeaderOffset + 8, BigInt(compressedSize));
@@ -6236,7 +6082,7 @@
6236
6082
  }
6237
6083
  rawExtraFieldZip64 = extraFieldZip64.array;
6238
6084
  } else {
6239
- rawExtraFieldZip64 = new Uint8Array();
6085
+ rawExtraFieldZip64 = EMPTY_UINT8_ARRAY;
6240
6086
  }
6241
6087
  fileEntry.rawExtraFieldZip64 = rawExtraFieldZip64;
6242
6088
  fileEntry.zip64Offset = zip64Offset;
@@ -6251,7 +6097,7 @@
6251
6097
  extraFieldTimestamp.uint32(lastModTimeUnix);
6252
6098
  rawExtraFieldTimestamp = extraFieldTimestamp.array;
6253
6099
  } else {
6254
- rawExtraFieldTimestamp = new Uint8Array();
6100
+ rawExtraFieldTimestamp = EMPTY_UINT8_ARRAY;
6255
6101
  }
6256
6102
  fileEntry.rawExtraFieldExtendedTimestamp = rawExtraFieldTimestamp;
6257
6103
  const extraFieldLength = getLength(
@@ -6307,7 +6153,7 @@
6307
6153
  await writeData(writer, directoryArray.slice(directoryDiskOffset, offset));
6308
6154
  directoryDiskOffset = offset;
6309
6155
  writer.availableSize = 0;
6310
- await writeData(writer, new Uint8Array());
6156
+ await writeData(writer, EMPTY_UINT8_ARRAY);
6311
6157
  }
6312
6158
  if (indexFileEntry == 0) {
6313
6159
  cdStartDiskNumber = writer.diskNumber;
@@ -6380,7 +6226,7 @@
6380
6226
  const endOfdirectoryRecord = createRecordWriter(zip64 ? ZIP64_END_OF_CENTRAL_DIR_TOTAL_LENGTH : END_OF_CENTRAL_DIR_LENGTH);
6381
6227
  if (getLength(endOfdirectoryRecord.array) + commentLength > writer.availableSize) {
6382
6228
  writer.availableSize = 0;
6383
- await writeData(writer, new Uint8Array());
6229
+ await writeData(writer, EMPTY_UINT8_ARRAY);
6384
6230
  }
6385
6231
  lastDiskNumber = writer.diskNumber;
6386
6232
  if (zip64) {
@@ -6477,7 +6323,6 @@
6477
6323
  return Math.floor(date.getTime() / 1000);
6478
6324
  }
6479
6325
 
6480
- // the extended timestamp extra field stores dates as signed 32-bit time_t values
6481
6326
  function inUnixTimeRange(timeUnix) {
6482
6327
  return timeUnix >= MIN_UNIX_TIME && timeUnix <= MAX_UNIX_TIME;
6483
6328
  }
@@ -6650,8 +6495,6 @@
6650
6495
  uncompressedSize: params.uncompressedSize || 0,
6651
6496
  passThrough: params.passThrough
6652
6497
  });
6653
- // detached entries (e.g. clones) are not registered until they are attached,
6654
- // otherwise they could never be garbage collected
6655
6498
  if (parent || !fs.root) {
6656
6499
  fs.entries[zipEntry.id] = zipEntry;
6657
6500
  }
@@ -6720,7 +6563,7 @@
6720
6563
  if (!writer || (writer.constructor == zipEntry.Writer && zipEntry.data)) {
6721
6564
  return zipEntry.data;
6722
6565
  } else {
6723
- const reader = zipEntry.reader = new zipEntry.Reader(zipEntry.data, options);
6566
+ const reader = zipEntry.reader = createReader(zipEntry.Reader, zipEntry.data, options);
6724
6567
  const uncompressedSize = zipEntry.data ? zipEntry.data.uncompressedSize : reader.size;
6725
6568
  await Promise.all([initStream(reader), initStream(writer, uncompressedSize)]);
6726
6569
  const { readable } = reader;
@@ -6776,11 +6619,7 @@
6776
6619
  }
6777
6620
 
6778
6621
  async getArrayBuffer(options) {
6779
- // this.data may be a string, a Uint8Array, a data URI or a Blob depending on how the entry
6780
- // was created, so retrieve the bytes through getUint8Array rather than assuming a Blob; this
6781
- // also applies the options (password, signal, onprogress...) like the other getData() getters
6782
6622
  const array = await this.getUint8Array(options);
6783
- // return the backing buffer directly when the array spans it, slicing the exact bytes otherwise
6784
6623
  return array.byteOffset || array.byteLength != array.buffer.byteLength
6785
6624
  ? array.buffer.slice(array.byteOffset, array.byteOffset + array.byteLength)
6786
6625
  : array.buffer;
@@ -7150,7 +6989,6 @@
7150
6989
  node = node.getChildByName(path[index]);
7151
6990
  }
7152
6991
  if (!node) {
7153
- // exclude detached entries, e.g. clones registered in `entries` but not attached
7154
6992
  node = this.entries.find(entry => entry && (entry == this.root || entry.isDescendantOf(this.root)) &&
7155
6993
  entry.getRelativeName() == fullname);
7156
6994
  }
@@ -7322,10 +7160,13 @@
7322
7160
  };
7323
7161
  }
7324
7162
 
7163
+ function createReader(Reader, data, options) {
7164
+ return Reader.prototype ? new Reader(data, options) : Reader(data, options);
7165
+ }
7166
+
7325
7167
  async function initReaders(entry, options) {
7326
7168
  const fileEntries = [];
7327
7169
  const pendingEntries = [entry];
7328
- // per-export map so concurrent exports do not share (and lock) the same readers
7329
7170
  const readers = new Map();
7330
7171
  while (pendingEntries.length) {
7331
7172
  const pendingEntry = pendingEntries.pop();
@@ -7338,7 +7179,7 @@
7338
7179
  }
7339
7180
  }
7340
7181
  await Promise.all(fileEntries.map(async child => {
7341
- const reader = child.reader = new child.Reader(child.data, options);
7182
+ const reader = child.reader = createReader(child.Reader, child.data, options);
7342
7183
  readers.set(child, reader);
7343
7184
  try {
7344
7185
  await initStream(reader);
@@ -7503,7 +7344,6 @@
7503
7344
  return directoryHandle;
7504
7345
 
7505
7346
  async function exportChildren(entry, parentHandle) {
7506
- // Separate files have no ordering constraint, so they can be written concurrently on demand.
7507
7347
  if (options.concurrent) {
7508
7348
  const results = await Promise.allSettled(entry.children.map(child => exportChild(child, parentHandle)));
7509
7349
  const failedResult = results.find(result => result.status == "rejected");
@@ -7525,8 +7365,6 @@
7525
7365
  } else {
7526
7366
  const fileHandle = await parentHandle.getFileHandle(child.name, { create: true });
7527
7367
  const writable = await fileHandle.createWritable();
7528
- // getData() streams the entry's data into `writable` and closes it (a FileSystemWritableFileStream
7529
- // only commits the file on close), applying options such as password, signal and onprogress.
7530
7368
  await child.getData({ writable }, Object.assign({}, options, {
7531
7369
  onprogress: async progress => {
7532
7370
  if (options.onprogress) {