@zip.js/zip.js 2.8.26 → 2.8.29

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 (53) hide show
  1. package/BENCHMARKS.md +168 -0
  2. package/README.md +2 -2
  3. package/deno.json +1 -1
  4. package/dist/zip-core.js +1113 -574
  5. package/dist/zip-core.min.js +1 -1
  6. package/dist/zip-fs-core.js +1209 -622
  7. package/dist/zip-fs-core.min.js +1 -1
  8. package/dist/zip-fs-native.js +1270 -632
  9. package/dist/zip-fs-native.min.js +1 -1
  10. package/dist/zip-fs.js +1347 -686
  11. package/dist/zip-fs.min.js +1 -1
  12. package/dist/zip-legacy.js +1114 -575
  13. package/dist/zip-legacy.min.js +1 -1
  14. package/dist/zip-module.wasm +0 -0
  15. package/dist/zip-native.js +1116 -577
  16. package/dist/zip-native.min.js +1 -1
  17. package/dist/zip-web-worker-native.js +1 -1
  18. package/dist/zip-web-worker.js +1 -1
  19. package/dist/zip.js +1193 -631
  20. package/dist/zip.min.js +1 -1
  21. package/eslint.config.mjs +6 -1
  22. package/index-native.cjs +1270 -632
  23. package/index-native.min.js +1 -1
  24. package/index.cjs +1347 -686
  25. package/index.d.ts +2451 -2355
  26. package/index.min.js +1 -1
  27. package/lib/core/codec-pool.js +39 -1
  28. package/lib/core/codec-worker.js +130 -35
  29. package/lib/core/configuration.js +3 -0
  30. package/lib/core/constants.js +6 -0
  31. package/lib/core/io.js +55 -25
  32. package/lib/core/options.js +2 -0
  33. package/lib/core/streams/aes-crypto-stream.js +12 -12
  34. package/lib/core/streams/codec-stream.js +11 -5
  35. package/lib/core/streams/codecs/sjcl.js +1 -33
  36. package/lib/core/streams/common-crypto.js +4 -4
  37. package/lib/core/streams/zip-crypto-stream.js +25 -15
  38. package/lib/core/streams/zip-entry-stream.js +89 -5
  39. package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
  40. package/lib/core/streams/zlib-wasm/zlib-streams.js +79 -56
  41. package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
  42. package/lib/core/util/decode-cp437.js +1 -1
  43. package/lib/core/util/decode-text.js +2 -1
  44. package/lib/core/web-worker-base.js +12 -4
  45. package/lib/core/web-worker-inline-native.js +1 -1
  46. package/lib/core/web-worker-inline-wasm.js +1 -1
  47. package/lib/core/zip-fs.js +154 -55
  48. package/lib/core/zip-reader.js +185 -67
  49. package/lib/core/zip-writer.js +613 -376
  50. package/lib/core/zlib-streams-inline.js +1 -1
  51. package/lib/zip-core-reader.js +3 -1
  52. package/lib/zip-core-writer.js +1 -0
  53. package/package.json +20 -6
@@ -49,6 +49,7 @@
49
49
  const END_OF_CENTRAL_DIR_SIGNATURE = 0x06054b50;
50
50
  const ZIP64_END_OF_CENTRAL_DIR_SIGNATURE = 0x06064b50;
51
51
  const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE = 0x07064b50;
52
+ const CENTRAL_FILE_HEADER_LENGTH = 46;
52
53
  const END_OF_CENTRAL_DIR_LENGTH = 22;
53
54
  const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH = 20;
54
55
  const ZIP64_END_OF_CENTRAL_DIR_LENGTH = 56;
@@ -96,9 +97,11 @@
96
97
  const DIRECTORY_SIGNATURE = "/";
97
98
 
98
99
  const HEADER_SIZE = 30;
100
+ const HEADER_OFFSET_VERSION = 0;
99
101
  const HEADER_OFFSET_SIGNATURE = 10;
100
102
  const HEADER_OFFSET_COMPRESSED_SIZE = 14;
101
103
  const HEADER_OFFSET_UNCOMPRESSED_SIZE = 18;
104
+ const LOCAL_HEADER_COMMON_OFFSET = 4;
102
105
 
103
106
  const MAX_DATE = new Date(2107, 11, 31);
104
107
  const MIN_DATE = new Date(1980, 0, 1);
@@ -153,6 +156,7 @@
153
156
  chunkSize: 64 * 1024,
154
157
  maxWorkers,
155
158
  terminateWorkerTimeout: 5000,
159
+ workerStarvationTimeout: 5000,
156
160
  useWebWorkers: true,
157
161
  useCompressionStream: true,
158
162
  CompressionStream: typeof CompressionStream != UNDEFINED_TYPE && CompressionStream,
@@ -205,6 +209,7 @@
205
209
 
206
210
  const CONTENT_TYPE_TEXT_PLAIN = "text/plain";
207
211
  const HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
212
+ const HTTP_HEADER_CONTENT_ENCODING = "Content-Encoding";
208
213
  const HTTP_HEADER_CONTENT_RANGE = "Content-Range";
209
214
  const HTTP_HEADER_ACCEPT_RANGES = "Accept-Ranges";
210
215
  const HTTP_HEADER_RANGE = "Range";
@@ -212,7 +217,7 @@
212
217
  const HTTP_METHOD_HEAD = "HEAD";
213
218
  const HTTP_METHOD_GET = "GET";
214
219
  const HTTP_RANGE_UNIT = "bytes";
215
- const DEFAULT_CHUNK_SIZE = 64 * 1024;
220
+ const DEFAULT_CHUNK_SIZE$1 = 64 * 1024;
216
221
  const DEFAULT_BUFFER_SIZE = 256 * 1024;
217
222
 
218
223
  const PROPERTY_NAME_WRITABLE = "writable";
@@ -232,7 +237,7 @@
232
237
 
233
238
  get readable() {
234
239
  const reader = this;
235
- const { chunkSize = DEFAULT_CHUNK_SIZE } = reader;
240
+ const { chunkSize = DEFAULT_CHUNK_SIZE$1 } = reader;
236
241
  const readable = new ReadableStream({
237
242
  start() {
238
243
  this.chunkOffset = 0;
@@ -329,7 +334,7 @@
329
334
 
330
335
  writeUint8Array(array) {
331
336
  const writer = this;
332
- let indexArray = 0;
337
+ let indexArray;
333
338
  let dataString = writer.pending;
334
339
  const delta = writer.pending.length;
335
340
  writer.pending = "";
@@ -339,12 +344,10 @@
339
344
  for (; indexArray < array.length; indexArray++) {
340
345
  writer.pending += String.fromCharCode(array[indexArray]);
341
346
  }
342
- if (dataString.length) {
343
- if (dataString.length > 2) {
344
- writer.data += btoa(dataString);
345
- } else {
346
- writer.pending += dataString;
347
- }
347
+ if (dataString.length > 2) {
348
+ writer.data += btoa(dataString);
349
+ } else {
350
+ writer.pending = dataString + writer.pending;
348
351
  }
349
352
  }
350
353
 
@@ -391,6 +394,9 @@
391
394
  }
392
395
  });
393
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
+ writer.blob.catch(() => { });
394
400
  }
395
401
 
396
402
  getData() {
@@ -475,7 +481,8 @@
475
481
  preventHeadRequest,
476
482
  useRangeHeader,
477
483
  forceRangeRequests,
478
- combineSizeEocd
484
+ combineSizeEocd,
485
+ fetch
479
486
  } = options;
480
487
  options = Object.assign({}, options);
481
488
  delete options.preventHeadRequest;
@@ -483,13 +490,15 @@
483
490
  delete options.forceRangeRequests;
484
491
  delete options.combineSizeEocd;
485
492
  delete options.useXHR;
493
+ delete options.fetch;
486
494
  Object.assign(httpReader, {
487
495
  url,
488
496
  options,
489
497
  preventHeadRequest,
490
498
  useRangeHeader,
491
499
  forceRangeRequests,
492
- combineSizeEocd
500
+ combineSizeEocd,
501
+ fetch
493
502
  });
494
503
  }
495
504
 
@@ -508,7 +517,10 @@
508
517
  throw new Error(ERR_HTTP_RANGE);
509
518
  } else {
510
519
  if (combineSizeEocd) {
511
- httpReader.eocdCache = new Uint8Array(await response.arrayBuffer());
520
+ const eocdCache = new Uint8Array(await response.arrayBuffer());
521
+ if (response.status == 206 && eocdCache.length == END_OF_CENTRAL_DIR_LENGTH) {
522
+ httpReader.eocdCache = eocdCache;
523
+ }
512
524
  }
513
525
  let contentSize;
514
526
  const contentRangeHeader = response.headers.get(HTTP_HEADER_CONTENT_RANGE);
@@ -554,7 +566,18 @@
554
566
  if (response.status != 206) {
555
567
  throw new Error(ERR_HTTP_RANGE);
556
568
  }
557
- return new Uint8Array(await response.arrayBuffer());
569
+ const contentRangeHeader = response.headers.get(HTTP_HEADER_CONTENT_RANGE);
570
+ if (contentRangeHeader) {
571
+ const rangeStart = Number(contentRangeHeader.trim().split(/[\s-]+/)[1]);
572
+ if (!Number.isNaN(rangeStart) && rangeStart != index) {
573
+ throw new Error(ERR_HTTP_RANGE);
574
+ }
575
+ }
576
+ const data = new Uint8Array(await response.arrayBuffer());
577
+ if (data.length != length) {
578
+ throw new Error(ERR_HTTP_RANGE);
579
+ }
580
+ return data;
558
581
  }
559
582
  } else {
560
583
  const { data } = httpReader;
@@ -591,9 +614,9 @@
591
614
  async function getRequestData(httpReader, sendRequest) {
592
615
  const response = await sendRequest(HTTP_METHOD_GET, httpReader, getHeaders(httpReader));
593
616
  httpReader.data = new Uint8Array(await response.arrayBuffer());
594
- if (!httpReader.size) {
595
- httpReader.size = httpReader.data.length;
596
- }
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
+ httpReader.size = httpReader.data.length;
597
620
  }
598
621
 
599
622
  async function getContentLength(httpReader, sendRequest, getRequestData) {
@@ -602,7 +625,7 @@
602
625
  } else {
603
626
  const response = await sendRequest(HTTP_METHOD_HEAD, httpReader, getHeaders(httpReader));
604
627
  const contentLength = response.headers.get(HTTP_HEADER_CONTENT_LENGTH);
605
- if (contentLength) {
628
+ if (contentLength && !response.headers.get(HTTP_HEADER_CONTENT_ENCODING)) {
606
629
  httpReader.size = Number(contentLength);
607
630
  } else {
608
631
  await getRequestData(httpReader, httpReader.options);
@@ -610,8 +633,8 @@
610
633
  }
611
634
  }
612
635
 
613
- async function sendFetchRequest(method, { options, url }, headers) {
614
- const response = await fetch(url, Object.assign({}, options, { method, headers }));
636
+ async function sendFetchRequest(method, { fetch: fetchFunction = fetch, options, url }, headers) {
637
+ const response = await fetchFunction(url, Object.assign({}, options, { method, headers }));
615
638
  if (response.status < 400) {
616
639
  return response;
617
640
  } else {
@@ -657,7 +680,8 @@
657
680
  super();
658
681
  Object.assign(this, {
659
682
  url,
660
- reader: options.useXHR ? new XHRReader(url, options) : new FetchReader(url, options)
683
+ // an explicit fetch implementation takes precedence over useXHR
684
+ reader: options.useXHR && !options.fetch ? new XHRReader(url, options) : new FetchReader(url, options)
661
685
  });
662
686
  }
663
687
 
@@ -749,7 +773,7 @@
749
773
  reader.lastDiskNumber = 0;
750
774
  reader.lastDiskOffset = 0;
751
775
  await Promise.all(readers.map(async (diskReader, indexDiskReader) => {
752
- await diskReader.init();
776
+ await initStream(diskReader);
753
777
  if (indexDiskReader != readers.length - 1) {
754
778
  reader.lastDiskOffset += diskReader.size;
755
779
  }
@@ -833,14 +857,26 @@
833
857
  writer.diskOffset += diskSourceWriter.size;
834
858
  writer.diskNumber++;
835
859
  diskWriter = null;
836
- await this.write(chunk.subarray(availableSize));
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
+ writer.availableSize = writer.maxSize;
863
+ if (chunk.length > availableSize) {
864
+ await this.write(chunk.subarray(availableSize));
865
+ }
837
866
  } else {
838
867
  await writeChunk(chunk);
839
868
  }
840
869
  },
841
870
  async close() {
842
- await diskWriter.ready;
843
- await closeDisk();
871
+ if (diskWriter) {
872
+ await diskWriter.ready;
873
+ await closeDisk();
874
+ }
875
+ },
876
+ async abort(reason) {
877
+ if (diskWriter) {
878
+ await diskWriter.abort(reason);
879
+ }
844
880
  }
845
881
  });
846
882
  Object.defineProperty(writer, PROPERTY_NAME_WRITABLE, {
@@ -855,7 +891,6 @@
855
891
  await diskWriter.ready;
856
892
  await diskWriter.write(chunk);
857
893
  diskSourceWriter.size += chunkLength;
858
- writer.size += chunkLength;
859
894
  writer.availableSize -= chunkLength;
860
895
  }
861
896
  }
@@ -1672,37 +1707,6 @@
1672
1707
  }
1673
1708
  };
1674
1709
 
1675
- /**
1676
- * Random values
1677
- * @namespace
1678
- */
1679
- const random = {
1680
- /**
1681
- * Generate random words with pure js, cryptographically not as strong & safe as native implementation.
1682
- * @param {TypedArray} typedArray The array to fill.
1683
- * @return {TypedArray} The random values.
1684
- */
1685
- getRandomValues(typedArray) {
1686
- const words = new Uint32Array(typedArray.buffer);
1687
- const r = (m_w) => {
1688
- let m_z = 0x3ade68b1;
1689
- const mask = 0xffffffff;
1690
- return function () {
1691
- m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
1692
- m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
1693
- const result = ((((m_z << 0x10) + m_w) & mask) / 0x100000000) + .5;
1694
- return result * (Math.random() > .5 ? 1 : -1);
1695
- };
1696
- };
1697
- for (let i = 0, rcache; i < typedArray.length; i += 4) {
1698
- const _r = r((rcache || Math.random()) * 0x100000000);
1699
- rcache = _r() * 0x3ade67b7;
1700
- words[i / 4] = (_r() * 0x100000000) | 0;
1701
- }
1702
- return typedArray;
1703
- }
1704
- };
1705
-
1706
1710
  /** @fileOverview CTR mode implementation.
1707
1711
  *
1708
1712
  * Special thanks to Roy Nicholson for pointing out a bug in our
@@ -1933,12 +1937,15 @@
1933
1937
  const ERR_INVALID_PASSWORD = "Invalid password";
1934
1938
  const ERR_INVALID_SIGNATURE = "Invalid signature";
1935
1939
  const ERR_ABORT_CHECK_PASSWORD = "zipjs-abort-check-password";
1940
+ const ERR_UNSUPPORTED_CRYPTO_API = "Crypto API not supported";
1936
1941
 
1937
1942
  function getRandomValues(array) {
1938
1943
  if (GET_RANDOM_VALUES_SUPPORTED) {
1939
1944
  return crypto.getRandomValues(array);
1940
1945
  } else {
1941
- return random.getRandomValues(array);
1946
+ // all supported runtimes expose crypto.getRandomValues; failing hard beats
1947
+ // silently generating predictable encryption salts
1948
+ throw new Error(ERR_UNSUPPORTED_CRYPTO_API);
1942
1949
  }
1943
1950
  }
1944
1951
 
@@ -1997,13 +2004,12 @@
1997
2004
 
1998
2005
  class AESDecryptionStream extends TransformStream {
1999
2006
 
2000
- constructor({ password, rawPassword, signed, encryptionStrength, checkPasswordOnly }) {
2007
+ constructor({ password, rawPassword, encryptionStrength, checkPasswordOnly }) {
2001
2008
  super({
2002
2009
  start() {
2003
2010
  Object.assign(this, {
2004
2011
  ready: new Promise(resolve => this.resolveReady = resolve),
2005
2012
  password: encodePassword(password, rawPassword),
2006
- signed,
2007
2013
  strength: encryptionStrength - 1,
2008
2014
  pending: new Uint8Array()
2009
2015
  });
@@ -2032,7 +2038,6 @@
2032
2038
  },
2033
2039
  async flush(controller) {
2034
2040
  const {
2035
- signed,
2036
2041
  ctr,
2037
2042
  hmac,
2038
2043
  pending,
@@ -2049,13 +2054,15 @@
2049
2054
  const decryptedChunk = ctr.update(encryptedChunk);
2050
2055
  decryptedChunkArray = fromBits(codecBytes, decryptedChunk);
2051
2056
  }
2052
- if (signed) {
2053
- const signature = subarray(fromBits(codecBytes, hmac.digest()), 0, SIGNATURE_LENGTH);
2054
- for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
2055
- if (signature[indexSignature] != originalSignature[indexSignature]) {
2056
- throw new Error(ERR_INVALID_SIGNATURE);
2057
- }
2058
- }
2057
+ 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
+ let invalidSignature = pending.length < SIGNATURE_LENGTH ? 1 : 0;
2061
+ for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
2062
+ invalidSignature |= signature[indexSignature] ^ originalSignature[indexSignature];
2063
+ }
2064
+ if (invalidSignature) {
2065
+ throw new Error(ERR_INVALID_SIGNATURE);
2059
2066
  }
2060
2067
  controller.enqueue(decryptedChunkArray);
2061
2068
  }
@@ -2127,11 +2134,11 @@
2127
2134
  hmac,
2128
2135
  pending
2129
2136
  } = aesCrypto;
2130
- const inputLength = input.length - paddingEnd;
2131
2137
  if (pending.length) {
2132
2138
  input = concat(pending, input);
2133
- output = expand(output, inputLength - (inputLength % BLOCK_LENGTH));
2134
2139
  }
2140
+ const inputLength = input.length - paddingEnd;
2141
+ output = expand(output, paddingStart + (inputLength - (inputLength % BLOCK_LENGTH)));
2135
2142
  let offset;
2136
2143
  for (offset = 0; offset <= inputLength - BLOCK_LENGTH; offset += BLOCK_LENGTH) {
2137
2144
  const inputChunk = toBits(codecBytes, subarray(input, offset, offset + BLOCK_LENGTH));
@@ -2279,21 +2286,24 @@
2279
2286
 
2280
2287
  class ZipCryptoDecryptionStream extends TransformStream {
2281
2288
 
2282
- constructor({ password, passwordVerification, checkPasswordOnly }) {
2289
+ constructor({ password, rawPassword, passwordVerification, checkPasswordOnly }) {
2283
2290
  super({
2284
2291
  start() {
2285
2292
  Object.assign(this, {
2286
2293
  password,
2294
+ rawPassword,
2287
2295
  passwordVerification
2288
2296
  });
2289
- createKeys(this, password);
2297
+ createKeys(this, password, rawPassword);
2290
2298
  },
2291
2299
  transform(chunk, controller) {
2292
2300
  const zipCrypto = this;
2293
- if (zipCrypto.password) {
2301
+ if (zipCrypto.password || zipCrypto.rawPassword) {
2294
2302
  const decryptedHeader = decrypt(zipCrypto, chunk.subarray(0, HEADER_LENGTH));
2295
- zipCrypto.password = null;
2296
- if (decryptedHeader.at(-1) != zipCrypto.passwordVerification) {
2303
+ 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
+ if ((decryptedHeader.at(-1) ^ zipCrypto.passwordVerification) != 0) {
2297
2307
  throw new Error(ERR_INVALID_PASSWORD);
2298
2308
  }
2299
2309
  chunk = chunk.subarray(HEADER_LENGTH);
@@ -2310,21 +2320,22 @@
2310
2320
 
2311
2321
  class ZipCryptoEncryptionStream extends TransformStream {
2312
2322
 
2313
- constructor({ password, passwordVerification }) {
2323
+ constructor({ password, rawPassword, passwordVerification }) {
2314
2324
  super({
2315
2325
  start() {
2316
2326
  Object.assign(this, {
2317
2327
  password,
2328
+ rawPassword,
2318
2329
  passwordVerification
2319
2330
  });
2320
- createKeys(this, password);
2331
+ createKeys(this, password, rawPassword);
2321
2332
  },
2322
2333
  transform(chunk, controller) {
2323
2334
  const zipCrypto = this;
2324
2335
  let output;
2325
2336
  let offset;
2326
- if (zipCrypto.password) {
2327
- zipCrypto.password = null;
2337
+ if (zipCrypto.password || zipCrypto.rawPassword) {
2338
+ zipCrypto.password = zipCrypto.rawPassword = null;
2328
2339
  const header = getRandomValues(new Uint8Array(HEADER_LENGTH));
2329
2340
  header[HEADER_LENGTH - 1] = zipCrypto.passwordVerification;
2330
2341
  output = new Uint8Array(chunk.length + header.length);
@@ -2359,25 +2370,31 @@
2359
2370
  return output;
2360
2371
  }
2361
2372
 
2362
- function createKeys(target, password) {
2373
+ function createKeys(target, password, rawPassword) {
2363
2374
  const keys = [0x12345678, 0x23456789, 0x34567890];
2364
2375
  Object.assign(target, {
2365
2376
  keys,
2366
2377
  crcKey0: new Crc32(keys[0]),
2367
2378
  crcKey2: new Crc32(keys[2])
2368
2379
  });
2369
- for (let index = 0; index < password.length; index++) {
2370
- updateKeys(target, password.charCodeAt(index));
2380
+ if (rawPassword) {
2381
+ for (let index = 0; index < rawPassword.length; index++) {
2382
+ updateKeys(target, rawPassword[index]);
2383
+ }
2384
+ } else {
2385
+ for (let index = 0; index < password.length; index++) {
2386
+ updateKeys(target, password.charCodeAt(index));
2387
+ }
2371
2388
  }
2372
2389
  }
2373
2390
 
2374
2391
  function updateKeys(target, byte) {
2375
- let [key0, key1, key2] = target.keys;
2392
+ let [, key1] = target.keys;
2376
2393
  target.crcKey0.append([byte]);
2377
- key0 = ~target.crcKey0.get();
2394
+ const key0 = ~target.crcKey0.get();
2378
2395
  key1 = getInt32(Math.imul(getInt32(key1 + getInt8(key0)), 134775813) + 1);
2379
2396
  target.crcKey2.append([key1 >>> 24]);
2380
- key2 = ~target.crcKey2.get();
2397
+ const key2 = ~target.crcKey2.get();
2381
2398
  target.keys = [key0, key1, key2];
2382
2399
  }
2383
2400
 
@@ -2424,6 +2441,7 @@
2424
2441
 
2425
2442
 
2426
2443
  const ERR_INVALID_UNCOMPRESSED_SIZE = "Invalid uncompressed size";
2444
+ const ERR_INVALID_COMPRESSED_DATA = "Invalid compressed data";
2427
2445
  const FORMAT_DEFLATE_RAW = "deflate-raw";
2428
2446
  const FORMAT_DEFLATE64_RAW = "deflate64-raw";
2429
2447
 
@@ -2480,6 +2498,7 @@
2480
2498
  }
2481
2499
  if (compressed) {
2482
2500
  readable = pipeThroughCommpressionStream(readable, useCompressionStream, { chunkSize, deflate64 }, DecompressionStream, DecompressionStreamZlib, DecompressionStream);
2501
+ readable = mapInflateStreamError(readable);
2483
2502
  }
2484
2503
  if ((!encrypted || zipCrypto) && signed) {
2485
2504
  crc32Stream = new Crc32Stream();
@@ -2508,14 +2527,15 @@
2508
2527
  function pipeThroughCommpressionStream(readable, useCompressionStream, options, CompressionStreamNative, CompressionStreamZlib, CompressionStream) {
2509
2528
  const Stream = useCompressionStream && CompressionStreamNative ? CompressionStreamNative : CompressionStreamZlib || CompressionStream;
2510
2529
  const format = options.deflate64 ? FORMAT_DEFLATE64_RAW : FORMAT_DEFLATE_RAW;
2530
+ let codecStream;
2511
2531
  try {
2512
- readable = pipeThrough(readable, new Stream(format, options));
2532
+ codecStream = new Stream(format, options);
2513
2533
  } catch (error) {
2514
2534
  if (useCompressionStream) {
2515
2535
  if (CompressionStreamZlib) {
2516
- readable = pipeThrough(readable, new CompressionStreamZlib(format, options));
2536
+ codecStream = new CompressionStreamZlib(format, options);
2517
2537
  } else if (CompressionStream) {
2518
- readable = pipeThrough(readable, new CompressionStream(format, options));
2538
+ codecStream = new CompressionStream(format, options);
2519
2539
  } else {
2520
2540
  throw error;
2521
2541
  }
@@ -2523,13 +2543,93 @@
2523
2543
  throw error;
2524
2544
  }
2525
2545
  }
2526
- return readable;
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
+ return pipeThroughBackpressured(readable, codecStream);
2527
2552
  }
2528
2553
 
2529
2554
  function pipeThrough(readable, transformStream) {
2530
2555
  return readable.pipeThrough(transformStream);
2531
2556
  }
2532
2557
 
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
+ function pipeThroughBackpressured(readable, transformStream) {
2563
+ const writer = transformStream.writable.getWriter();
2564
+ const reader = readable.getReader();
2565
+ pump();
2566
+ return transformStream.readable;
2567
+
2568
+ async function pump() {
2569
+ try {
2570
+ for (; ;) {
2571
+ await writer.ready;
2572
+ const result = await reader.read();
2573
+ if (result.done) {
2574
+ await writer.close();
2575
+ break;
2576
+ }
2577
+ await writer.write(result.value);
2578
+ }
2579
+ } catch (error) {
2580
+ await abort(writer, error);
2581
+ await cancel(reader, error);
2582
+ }
2583
+ }
2584
+ }
2585
+
2586
+ async function abort(writer, error) {
2587
+ try {
2588
+ await writer.abort(error);
2589
+ } catch {
2590
+ // the writable may already be errored/closed
2591
+ }
2592
+ }
2593
+
2594
+ async function cancel(reader, error) {
2595
+ try {
2596
+ await reader.cancel(error);
2597
+ } catch {
2598
+ // the readable may already be errored/closed
2599
+ }
2600
+ }
2601
+
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
+ function mapInflateStreamError(readable) {
2606
+ const reader = readable.getReader();
2607
+ return new ReadableStream({
2608
+ async pull(controller) {
2609
+ let result;
2610
+ try {
2611
+ result = await reader.read();
2612
+ } catch (error) {
2613
+ if (error && error.message) {
2614
+ throw error;
2615
+ }
2616
+ const mappedError = new Error(ERR_INVALID_COMPRESSED_DATA);
2617
+ mappedError.cause = error;
2618
+ throw mappedError;
2619
+ }
2620
+ const { value, done } = result;
2621
+ if (done) {
2622
+ controller.close();
2623
+ } else {
2624
+ controller.enqueue(value);
2625
+ }
2626
+ },
2627
+ cancel(reason) {
2628
+ return reader.cancel(reason);
2629
+ }
2630
+ });
2631
+ }
2632
+
2533
2633
  /*
2534
2634
  Copyright (c) 2022 Gildas Lormeau. All rights reserved.
2535
2635
 
@@ -2559,6 +2659,7 @@
2559
2659
  */
2560
2660
 
2561
2661
 
2662
+ const DEFAULT_CHUNK_SIZE = 64 * 1024;
2562
2663
  const MESSAGE_EVENT_TYPE = "message";
2563
2664
  const MESSAGE_START = "start";
2564
2665
  const MESSAGE_PULL = "pull";
@@ -2627,6 +2728,9 @@
2627
2728
 
2628
2729
  constructor(chunkSize) {
2629
2730
  let pendingChunk;
2731
+ if (!(chunkSize >= 1)) {
2732
+ chunkSize = DEFAULT_CHUNK_SIZE;
2733
+ }
2630
2734
  super({
2631
2735
  transform,
2632
2736
  flush(controller) {
@@ -2644,12 +2748,12 @@
2644
2748
  chunk = newChunk;
2645
2749
  pendingChunk = null;
2646
2750
  }
2647
- if (chunk.length > chunkSize) {
2648
- controller.enqueue(chunk.slice(0, chunkSize));
2649
- transform(chunk.slice(chunkSize), controller);
2650
- } else {
2651
- pendingChunk = chunk;
2751
+ let offset = 0;
2752
+ while (chunk.length - offset > chunkSize) {
2753
+ controller.enqueue(chunk.slice(offset, offset + chunkSize));
2754
+ offset += chunkSize;
2652
2755
  }
2756
+ pendingChunk = offset ? chunk.slice(offset) : chunk;
2653
2757
  }
2654
2758
  }
2655
2759
  }
@@ -2684,8 +2788,10 @@
2684
2788
 
2685
2789
 
2686
2790
  const MODULE_WORKER_OPTIONS = { type: "module" };
2791
+ const ERROR_EVENT_TYPE = "error";
2792
+ const MESSAGE_ERROR_EVENT_TYPE = "messageerror";
2687
2793
 
2688
- let webWorkerSupported, webWorkerURI, webWorkerOptions;
2794
+ let webWorkerSupported, webWorkerSource, webWorkerURI, webWorkerOptions;
2689
2795
  let transferStreamsSupported = true;
2690
2796
  try {
2691
2797
  transferStreamsSupported = typeof structuredClone == FUNCTION_TYPE && structuredClone(new DOMException("", "AbortError")).code !== UNDEFINED_VALUE;
@@ -2700,8 +2806,9 @@
2700
2806
  const { signal } = streamOptions;
2701
2807
  Object.assign(workerData, {
2702
2808
  busy: true,
2809
+ generation: (workerData.generation || 0) + 1,
2703
2810
  readable: readable
2704
- .pipeThrough(new ChunkStream(config.chunkSize))
2811
+ .pipeThrough(new ChunkStream(getChunkSize(config)))
2705
2812
  .pipeThrough(new ProgressWatcherStream(streamOptions), { signal }),
2706
2813
  writable,
2707
2814
  options: Object.assign({}, options),
@@ -2724,15 +2831,17 @@
2724
2831
  });
2725
2832
  },
2726
2833
  onTaskFinished() {
2727
- const { resolveTerminated } = workerData;
2728
- if (resolveTerminated) {
2729
- workerData.resolveTerminated = null;
2730
- workerData.terminated = true;
2731
- workerData.worker.terminate();
2732
- resolveTerminated();
2834
+ if (workerData.busy) {
2835
+ const { resolveTerminated } = workerData;
2836
+ if (resolveTerminated) {
2837
+ workerData.resolveTerminated = null;
2838
+ workerData.terminated = true;
2839
+ workerData.worker.terminate();
2840
+ resolveTerminated();
2841
+ }
2842
+ workerData.busy = false;
2843
+ onTaskFinished(workerData);
2733
2844
  }
2734
- workerData.busy = false;
2735
- onTaskFinished(workerData);
2736
2845
  }
2737
2846
  });
2738
2847
  if (webWorkerSupported === UNDEFINED_VALUE) {
@@ -2801,6 +2910,7 @@
2801
2910
  }
2802
2911
  Object.assign(workerData, {
2803
2912
  worker,
2913
+ terminated: false,
2804
2914
  interface: {
2805
2915
  run: () => runWebWorker(workerData, { chunkSize, wasmURI, baseURI })
2806
2916
  }
@@ -2816,7 +2926,15 @@
2816
2926
  try {
2817
2927
  await initModule(config);
2818
2928
  } catch {
2819
- options.useCompressionStream = true;
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
+ const ZlibStream = options.codecType.startsWith(CODEC_DEFLATE) ?
2933
+ config.CompressionStreamZlib :
2934
+ config.DecompressionStreamZlib;
2935
+ if (!ZlibStream || ZlibStream.requiresModule) {
2936
+ options.useCompressionStream = true;
2937
+ }
2820
2938
  }
2821
2939
  }
2822
2940
  codecStream = new CodecStream(options, config);
@@ -2855,37 +2973,85 @@
2855
2973
  result
2856
2974
  });
2857
2975
  const { readable, options } = workerData;
2858
- const { writable, closed } = watchClosedStream(workerData.writable);
2859
- const streamsTransferred = sendMessage({
2860
- type: MESSAGE_START,
2861
- options,
2862
- config,
2863
- readable,
2864
- writable
2865
- }, workerData);
2976
+ const { writable, closed, abortPipe } = watchClosedStream(workerData.writable);
2977
+ let streamsTransferred;
2978
+ try {
2979
+ streamsTransferred = sendMessage({
2980
+ type: MESSAGE_START,
2981
+ options,
2982
+ config,
2983
+ readable,
2984
+ writable
2985
+ }, workerData);
2986
+ } catch (error) {
2987
+ abortPipe();
2988
+ try {
2989
+ await closed;
2990
+ } catch {
2991
+ // ignored
2992
+ }
2993
+ workerData.onTaskFinished();
2994
+ throw error;
2995
+ }
2866
2996
  if (!streamsTransferred) {
2867
2997
  Object.assign(workerData, {
2868
2998
  reader: readable.getReader(),
2869
2999
  writer: writable.getWriter()
2870
3000
  });
2871
3001
  }
2872
- const resultValue = await result;
2873
- if (!streamsTransferred) {
2874
- await writable.getWriter().close();
3002
+ try {
3003
+ const resultValue = await result;
3004
+ await closeWritable();
3005
+ await closed;
3006
+ return resultValue;
3007
+ } catch (error) {
3008
+ await closeWritable();
3009
+ // unblock the pipe watcher if the worker died before closing its end of the stream
3010
+ abortPipe();
3011
+ try {
3012
+ await closed;
3013
+ } catch {
3014
+ // ignored
3015
+ }
3016
+ throw error;
3017
+ }
3018
+
3019
+ async function closeWritable() {
3020
+ if (!streamsTransferred && !writable.locked) {
3021
+ try {
3022
+ await writable.getWriter().close();
3023
+ } catch {
3024
+ // ignored
3025
+ }
3026
+ }
2875
3027
  }
2876
- await closed;
2877
- return resultValue;
2878
3028
  }
2879
3029
 
2880
3030
  function watchClosedStream(writableSource) {
3031
+ const abortController = new AbortController();
2881
3032
  const { writable, readable } = new TransformStream();
2882
- const closed = readable.pipeTo(writableSource, { preventClose: true });
2883
- return { writable, closed };
3033
+ 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
+ closed.catch(() => { });
3037
+ return { writable, closed, abortPipe: () => abortController.abort() };
3038
+ }
3039
+
3040
+ function terminateWorker$1(workerData) {
3041
+ const { worker } = workerData;
3042
+ if (worker) {
3043
+ try {
3044
+ worker.terminate();
3045
+ } catch {
3046
+ // ignored
3047
+ }
3048
+ }
3049
+ workerData.interface = null;
2884
3050
  }
2885
3051
 
2886
3052
  function getWebWorker(url, baseURI, workerData, isModuleType, useBlobURI = true) {
2887
3053
  let worker, resolvedURI, resolvedOptions;
2888
- if (webWorkerURI === UNDEFINED_VALUE) {
3054
+ if (webWorkerURI === UNDEFINED_VALUE || webWorkerSource !== url) {
2889
3055
  // deno-lint-ignore valid-typeof
2890
3056
  const isFunctionURI = typeof url == FUNCTION_TYPE;
2891
3057
  if (isFunctionURI) {
@@ -2942,16 +3108,36 @@
2942
3108
  }
2943
3109
  }
2944
3110
  }
3111
+ webWorkerSource = url;
2945
3112
  webWorkerURI = resolvedURI;
2946
3113
  webWorkerOptions = resolvedOptions;
2947
3114
  } else {
2948
3115
  worker = new Worker(webWorkerURI, webWorkerOptions);
2949
3116
  }
2950
3117
  worker.addEventListener(MESSAGE_EVENT_TYPE, event => onMessage(event, workerData));
3118
+ worker.addEventListener(ERROR_EVENT_TYPE, event => onWorkerError(event, workerData));
3119
+ worker.addEventListener(MESSAGE_ERROR_EVENT_TYPE, event => onWorkerError(event, workerData));
2951
3120
  return worker;
2952
3121
  }
2953
3122
 
2954
- function sendMessage(message, { worker, writer, onTaskFinished, transferStreams }) {
3123
+ function onWorkerError(event, workerData) {
3124
+ if (event.preventDefault) {
3125
+ event.preventDefault();
3126
+ }
3127
+ 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
+ terminateWorker$1(workerData);
3131
+ if (rejectResult) {
3132
+ rejectResult(event.error || new Error(event.message || ERROR_EVENT_TYPE));
3133
+ if (writer) {
3134
+ writer.releaseLock();
3135
+ }
3136
+ onTaskFinished();
3137
+ }
3138
+ }
3139
+
3140
+ function sendMessage(message, { worker, writer, transferStreams }) {
2955
3141
  try {
2956
3142
  const { value, readable, writable } = message;
2957
3143
  const transferables = [];
@@ -2985,14 +3171,16 @@
2985
3171
  if (writer) {
2986
3172
  writer.releaseLock();
2987
3173
  }
2988
- onTaskFinished();
2989
3174
  throw error;
2990
3175
  }
2991
3176
  }
2992
3177
 
2993
3178
  async function onMessage({ data }, workerData) {
2994
3179
  const { type, value, messageId, result, error } = data;
2995
- const { reader, writer, resolveResult, rejectResult, onTaskFinished } = workerData;
3180
+ 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
+ const stale = () => workerData.generation != generation;
2996
3184
  try {
2997
3185
  if (error) {
2998
3186
  const { message, stack, code, name, outputSize } = error;
@@ -3002,23 +3190,32 @@
3002
3190
  } else {
3003
3191
  if (type == MESSAGE_PULL) {
3004
3192
  const { value, done } = await reader.read();
3005
- sendMessage({ type: MESSAGE_DATA, value, done, messageId }, workerData);
3193
+ if (!stale()) {
3194
+ sendMessage({ type: MESSAGE_DATA, value, done, messageId }, workerData);
3195
+ }
3006
3196
  }
3007
3197
  if (type == MESSAGE_DATA) {
3008
3198
  await writer.ready;
3009
3199
  await writer.write(new Uint8Array(value));
3010
- sendMessage({ type: MESSAGE_ACK_DATA, messageId }, workerData);
3200
+ if (!stale()) {
3201
+ sendMessage({ type: MESSAGE_ACK_DATA, messageId }, workerData);
3202
+ }
3011
3203
  }
3012
3204
  if (type == MESSAGE_CLOSE) {
3013
3205
  close(null, result);
3014
3206
  }
3015
3207
  }
3016
3208
  } catch (error) {
3017
- sendMessage({ type: MESSAGE_CLOSE, messageId }, workerData);
3018
- close(error);
3209
+ if (!stale()) {
3210
+ terminateWorker$1(workerData);
3211
+ close(error);
3212
+ }
3019
3213
  }
3020
3214
 
3021
3215
  function close(error, result) {
3216
+ if (stale()) {
3217
+ return;
3218
+ }
3022
3219
  if (error) {
3023
3220
  rejectResult(error);
3024
3221
  } else {
@@ -3062,6 +3259,8 @@
3062
3259
 
3063
3260
  let pool = [];
3064
3261
  const pendingRequests = [];
3262
+ let starvationTimeout;
3263
+ let starvationDelay;
3065
3264
 
3066
3265
  let indexWorker = 0;
3067
3266
 
@@ -3088,14 +3287,20 @@
3088
3287
  pool.push(workerData);
3089
3288
  return new CodecWorker(workerData, stream, workerOptions, onTaskFinished);
3090
3289
  } else {
3091
- return new Promise(resolve => pendingRequests.push({ resolve, stream, workerOptions }));
3290
+ return new Promise(resolve => {
3291
+ pendingRequests.push({ resolve, stream, workerOptions });
3292
+ starvationDelay = config.workerStarvationTimeout;
3293
+ armStarvationTimeout();
3294
+ });
3092
3295
  }
3093
3296
  }
3094
3297
 
3095
3298
  function onTaskFinished(workerData) {
3299
+ clearStarvationTimeout();
3096
3300
  if (pendingRequests.length) {
3097
3301
  const [{ resolve, stream, workerOptions }] = pendingRequests.splice(0, 1);
3098
3302
  resolve(new CodecWorker(workerData, stream, workerOptions, onTaskFinished));
3303
+ armStarvationTimeout();
3099
3304
  } else if (workerData.worker) {
3100
3305
  clearTerminateTimeout(workerData);
3101
3306
  terminateWorker(workerData, workerOptions);
@@ -3105,6 +3310,34 @@
3105
3310
  }
3106
3311
  }
3107
3312
 
3313
+ function armStarvationTimeout() {
3314
+ if (!starvationTimeout && pendingRequests.length && Number.isFinite(starvationDelay) && starvationDelay >= 0) {
3315
+ starvationTimeout = setTimeout(onWorkerStarvation, starvationDelay);
3316
+ }
3317
+ }
3318
+
3319
+ function clearStarvationTimeout() {
3320
+ if (starvationTimeout) {
3321
+ clearTimeout(starvationTimeout);
3322
+ starvationTimeout = null;
3323
+ }
3324
+ }
3325
+
3326
+ function onWorkerStarvation() {
3327
+ starvationTimeout = null;
3328
+ if (pendingRequests.length) {
3329
+ const [{ resolve, stream, workerOptions }] = pendingRequests.splice(0, 1);
3330
+ const inlineWorkerOptions = Object.assign({}, workerOptions, { useWebWorkers: false, workerURI: UNDEFINED_VALUE });
3331
+ resolve(new CodecWorker({}, stream, inlineWorkerOptions, onInlineTaskFinished));
3332
+ armStarvationTimeout();
3333
+ }
3334
+ }
3335
+
3336
+ function onInlineTaskFinished() {
3337
+ clearStarvationTimeout();
3338
+ armStarvationTimeout();
3339
+ }
3340
+
3108
3341
  function terminateWorker(workerData, workerOptions) {
3109
3342
  const { config } = workerOptions;
3110
3343
  const { terminateWorkerTimeout } = config;
@@ -3162,7 +3395,7 @@
3162
3395
 
3163
3396
  /* global TextDecoder */
3164
3397
 
3165
- const CP437 = "\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ".split("");
3398
+ const CP437 = "\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ".split("");
3166
3399
  const VALID_CP437 = CP437.length == 256;
3167
3400
 
3168
3401
  function decodeCP437(stringValue) {
@@ -3210,7 +3443,8 @@
3210
3443
  if (encoding && encoding.trim().toLowerCase() == "cp437") {
3211
3444
  return decodeCP437(value);
3212
3445
  } else {
3213
- return new TextDecoder(encoding).decode(value);
3446
+ // ignoreBOM keeps a leading U+FEFF in filenames/comments instead of dropping it
3447
+ return new TextDecoder(encoding, { ignoreBOM: true }).decode(value);
3214
3448
  }
3215
3449
  }
3216
3450
 
@@ -3387,6 +3621,7 @@
3387
3621
  const OPTION_CHECK_PASSWORD_ONLY = "checkPasswordOnly";
3388
3622
  const OPTION_CHECK_OVERLAPPING_ENTRY_ONLY = "checkOverlappingEntryOnly";
3389
3623
  const OPTION_CHECK_OVERLAPPING_ENTRY = "checkOverlappingEntry";
3624
+ const OPTION_CHECK_AMBIGUITY = "checkAmbiguity";
3390
3625
  const OPTION_CHECK_SIGNATURE = "checkSignature";
3391
3626
  const OPTION_USE_WEB_WORKERS = "useWebWorkers";
3392
3627
  const OPTION_USE_COMPRESSION_STREAM = "useCompressionStream";
@@ -3444,12 +3679,16 @@
3444
3679
  const ERR_EXTRAFIELD_ZIP64_NOT_FOUND = "Zip64 extra field not found";
3445
3680
  const ERR_ENCRYPTED = "File contains encrypted entry";
3446
3681
  const ERR_UNSUPPORTED_ENCRYPTION = "Encryption method not supported";
3447
- const ERR_UNSUPPORTED_COMPRESSION = "Compression method not supported";
3682
+ const ERR_UNSUPPORTED_COMPRESSION$1 = "Compression method not supported";
3448
3683
  const ERR_SPLIT_ZIP_FILE = "Split zip file";
3449
3684
  const ERR_OVERLAPPING_ENTRY = "Overlapping entry found";
3685
+ const ERR_AMBIGUOUS_ARCHIVE = "Ambiguous archive";
3450
3686
  const CHARSET_UTF8 = "utf-8";
3451
3687
  const PROPERTY_NAME_UTF8_SUFFIX = "UTF8";
3452
3688
  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
+ const BITFLAG_AMBIGUITY_MASK = BITFLAG_ENCRYPTED | BITFLAG_DATA_DESCRIPTOR | BITFLAG_LANG_ENCODING_FLAG;
3453
3692
  const ZIP64_PROPERTIES = [
3454
3693
  [PROPERTY_NAME_UNCOMPRESSED_SIZE, MAX_32_BITS],
3455
3694
  [PROPERTY_NAME_COMPRESSED_SIZE, MAX_32_BITS],
@@ -3474,7 +3713,7 @@
3474
3713
  reader: new GenericReader(reader),
3475
3714
  options,
3476
3715
  config: getConfiguration(),
3477
- readRanges: []
3716
+ readRanges: new Map()
3478
3717
  });
3479
3718
  }
3480
3719
 
@@ -3507,21 +3746,30 @@
3507
3746
  const commentOffset = endOfDirectoryInfo.offset;
3508
3747
  const commentLength = getUint16(endOfDirectoryView, 20);
3509
3748
  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) {
3751
+ throwAmbiguousArchive("appended data");
3752
+ }
3510
3753
  let lastDiskNumber = getUint16(endOfDirectoryView, 4);
3511
3754
  const expectedLastDiskNumber = reader.lastDiskNumber || 0;
3512
3755
  let diskNumber = getUint16(endOfDirectoryView, 6);
3513
- let filesLength = getUint16(endOfDirectoryView, 8);
3756
+ let filesLength = getUint16(endOfDirectoryView, 10);
3514
3757
  let prependedDataLength = 0;
3515
- let startOffset = 0;
3758
+ let startOffset;
3759
+ let zip64EndOfDirectory;
3516
3760
  if (directoryDataOffset == MAX_32_BITS || directoryDataLength == MAX_32_BITS || filesLength == MAX_16_BITS || diskNumber == MAX_16_BITS) {
3517
- const endOfDirectoryLocatorArray = await readUint8Array(reader, endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH);
3761
+ const endOfDirectoryLocatorArray = endOfDirectoryInfo.offset >= ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH ?
3762
+ await readUint8Array(reader, endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH) :
3763
+ new Uint8Array();
3518
3764
  const endOfDirectoryLocatorView = getDataView$1(endOfDirectoryLocatorArray);
3519
- if (getUint32(endOfDirectoryLocatorView, 0) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
3765
+ if (endOfDirectoryLocatorArray.length == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH &&
3766
+ getUint32(endOfDirectoryLocatorView, 0) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
3520
3767
  directoryDataOffset = getBigUint64(endOfDirectoryLocatorView, 8);
3521
3768
  let endOfDirectoryArray = await readUint8Array(reader, directoryDataOffset, ZIP64_END_OF_CENTRAL_DIR_LENGTH, -1);
3522
3769
  let endOfDirectoryView = getDataView$1(endOfDirectoryArray);
3523
- const expectedDirectoryDataOffset = endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH - ZIP64_END_OF_CENTRAL_DIR_LENGTH;
3524
- if (getUint32(endOfDirectoryView, 0) != ZIP64_END_OF_CENTRAL_DIR_SIGNATURE && directoryDataOffset != expectedDirectoryDataOffset) {
3770
+ const expectedDirectoryDataOffset = endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH - ZIP64_END_OF_CENTRAL_DIR_LENGTH - (reader.lastDiskOffset || 0);
3771
+ if ((endOfDirectoryArray.length < ZIP64_END_OF_CENTRAL_DIR_LENGTH || getUint32(endOfDirectoryView, 0) != ZIP64_END_OF_CENTRAL_DIR_SIGNATURE) &&
3772
+ directoryDataOffset != expectedDirectoryDataOffset && expectedDirectoryDataOffset >= 0) {
3525
3773
  const originalDirectoryDataOffset = directoryDataOffset;
3526
3774
  directoryDataOffset = expectedDirectoryDataOffset;
3527
3775
  if (directoryDataOffset > originalDirectoryDataOffset) {
@@ -3530,24 +3778,36 @@
3530
3778
  endOfDirectoryArray = await readUint8Array(reader, directoryDataOffset, ZIP64_END_OF_CENTRAL_DIR_LENGTH, -1);
3531
3779
  endOfDirectoryView = getDataView$1(endOfDirectoryArray);
3532
3780
  }
3533
- if (getUint32(endOfDirectoryView, 0) != ZIP64_END_OF_CENTRAL_DIR_SIGNATURE) {
3781
+ if (endOfDirectoryArray.length < ZIP64_END_OF_CENTRAL_DIR_LENGTH || getUint32(endOfDirectoryView, 0) != ZIP64_END_OF_CENTRAL_DIR_SIGNATURE) {
3534
3782
  throw new Error(ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUND);
3535
3783
  }
3784
+ zip64EndOfDirectory = true;
3536
3785
  if (lastDiskNumber == MAX_16_BITS) {
3537
3786
  lastDiskNumber = getUint32(endOfDirectoryView, 16);
3787
+ } else if (checkAmbiguity && lastDiskNumber != getUint32(endOfDirectoryView, 16)) {
3788
+ throwAmbiguousArchive("mismatched zip64 end of central directory record");
3538
3789
  }
3539
3790
  if (diskNumber == MAX_16_BITS) {
3540
3791
  diskNumber = getUint32(endOfDirectoryView, 20);
3792
+ } else if (checkAmbiguity && diskNumber != getUint32(endOfDirectoryView, 20)) {
3793
+ throwAmbiguousArchive("mismatched zip64 end of central directory record");
3541
3794
  }
3542
3795
  if (filesLength == MAX_16_BITS) {
3543
3796
  filesLength = getBigUint64(endOfDirectoryView, 32);
3797
+ } else if (checkAmbiguity && filesLength != getBigUint64(endOfDirectoryView, 32)) {
3798
+ throwAmbiguousArchive("mismatched zip64 end of central directory record");
3544
3799
  }
3545
3800
  if (directoryDataLength == MAX_32_BITS) {
3546
3801
  directoryDataLength = getBigUint64(endOfDirectoryView, 40);
3802
+ } else if (checkAmbiguity && directoryDataLength != getBigUint64(endOfDirectoryView, 40)) {
3803
+ throwAmbiguousArchive("mismatched zip64 end of central directory record");
3547
3804
  }
3548
- directoryDataOffset -= directoryDataLength;
3805
+ directoryDataOffset = getBigUint64(endOfDirectoryView, 48) + prependedDataLength;
3549
3806
  }
3550
3807
  }
3808
+ const declaredDirectoryDataLength = directoryDataLength;
3809
+ const centralDirectoryEndOffset = endOfDirectoryInfo.offset -
3810
+ (zip64EndOfDirectory ? ZIP64_END_OF_CENTRAL_DIR_LENGTH + ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH : 0);
3551
3811
  if (directoryDataOffset >= reader.size) {
3552
3812
  prependedDataLength = reader.size - directoryDataOffset - directoryDataLength - END_OF_CENTRAL_DIR_LENGTH;
3553
3813
  directoryDataOffset = reader.size - directoryDataLength - END_OF_CENTRAL_DIR_LENGTH;
@@ -3562,8 +3822,11 @@
3562
3822
  let directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
3563
3823
  let directoryView = getDataView$1(directoryArray);
3564
3824
  if (directoryDataLength) {
3565
- const expectedDirectoryDataOffset = endOfDirectoryInfo.offset - directoryDataLength;
3566
- if (getUint32(directoryView, offset) != CENTRAL_FILE_HEADER_SIGNATURE && directoryDataOffset != expectedDirectoryDataOffset) {
3825
+ if (directoryArray.length < 4) {
3826
+ throw new Error(ERR_BAD_FORMAT);
3827
+ }
3828
+ const expectedDirectoryDataOffset = centralDirectoryEndOffset - directoryDataLength - (reader.lastDiskOffset || 0);
3829
+ if (getUint32(directoryView, offset) != CENTRAL_FILE_HEADER_SIGNATURE && directoryDataOffset != expectedDirectoryDataOffset && diskNumber == lastDiskNumber) {
3567
3830
  const originalDirectoryDataOffset = directoryDataOffset;
3568
3831
  directoryDataOffset = expectedDirectoryDataOffset;
3569
3832
  if (directoryDataOffset > originalDirectoryDataOffset) {
@@ -3573,8 +3836,8 @@
3573
3836
  directoryView = getDataView$1(directoryArray);
3574
3837
  }
3575
3838
  }
3576
- const expectedDirectoryDataLength = endOfDirectoryInfo.offset - directoryDataOffset - (reader.lastDiskOffset || 0);
3577
- if (directoryDataLength != expectedDirectoryDataLength && expectedDirectoryDataLength >= 0) {
3839
+ const expectedDirectoryDataLength = centralDirectoryEndOffset - directoryDataOffset - (reader.lastDiskOffset || 0);
3840
+ if (directoryDataLength != expectedDirectoryDataLength && expectedDirectoryDataLength >= 0 && diskNumber == lastDiskNumber) {
3578
3841
  directoryDataLength = expectedDirectoryDataLength;
3579
3842
  directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
3580
3843
  directoryView = getDataView$1(directoryArray);
@@ -3582,16 +3845,19 @@
3582
3845
  if (directoryDataOffset < 0 || directoryDataOffset >= reader.size) {
3583
3846
  throw new Error(ERR_BAD_FORMAT);
3584
3847
  }
3848
+ startOffset = directoryDataOffset;
3585
3849
  const filenameEncoding = getOptionValue$1(zipReader, options, OPTION_FILENAME_ENCODING);
3586
3850
  const commentEncoding = getOptionValue$1(zipReader, options, OPTION_COMMENT_ENCODING);
3851
+ const filenames = checkAmbiguity ? new Set() : UNDEFINED_VALUE;
3852
+ let duplicateFilename;
3587
3853
  for (let indexFile = 0; indexFile < filesLength; indexFile++) {
3588
3854
  const fileEntry = new ZipEntry$1(reader, config, zipReader.options);
3589
- if (getUint32(directoryView, offset) != CENTRAL_FILE_HEADER_SIGNATURE) {
3855
+ if (offset + CENTRAL_FILE_HEADER_LENGTH > directoryArray.length || getUint32(directoryView, offset) != CENTRAL_FILE_HEADER_SIGNATURE) {
3590
3856
  throw new Error(ERR_CENTRAL_DIRECTORY_NOT_FOUND);
3591
3857
  }
3592
3858
  readCommonHeader(fileEntry, directoryView, offset + 6);
3593
3859
  const languageEncodingFlag = Boolean(fileEntry.bitFlag.languageEncodingFlag);
3594
- const filenameOffset = offset + 46;
3860
+ const filenameOffset = offset + CENTRAL_FILE_HEADER_LENGTH;
3595
3861
  const extraFieldOffset = filenameOffset + fileEntry.filenameLength;
3596
3862
  const commentOffset = extraFieldOffset + fileEntry.extraFieldLength;
3597
3863
  const versionMadeBy = getUint16(directoryView, offset + 4);
@@ -3612,7 +3878,7 @@
3612
3878
  directory: Boolean(msdosAttributesRaw & FILE_ATTR_MSDOS_DIR_MASK),
3613
3879
  archive: Boolean(msdosAttributesRaw & FILE_ATTR_MSDOS_ARCHIVE_MASK)
3614
3880
  };
3615
- const offsetFileEntry = getUint32(directoryView, offset + 42) + prependedDataLength;
3881
+ const offsetFileEntry = getUint32(directoryView, offset + 42);
3616
3882
  const decode = getOptionValue$1(zipReader, options, OPTION_DECODE_TEXT) || decodeText;
3617
3883
  const rawFilenameEncoding = filenameUTF8 ? CHARSET_UTF8 : filenameEncoding || CHARSET_CP437;
3618
3884
  const rawCommentEncoding = commentUTF8 ? CHARSET_UTF8 : commentEncoding || CHARSET_CP437;
@@ -3625,6 +3891,7 @@
3625
3891
  comment = decodeText(rawComment, rawCommentEncoding);
3626
3892
  }
3627
3893
  Object.assign(fileEntry, {
3894
+ index: indexFile,
3628
3895
  versionMadeBy,
3629
3896
  msDosCompatible,
3630
3897
  compressedSize: 0,
@@ -3644,8 +3911,15 @@
3644
3911
  filename,
3645
3912
  comment
3646
3913
  });
3647
- startOffset = Math.max(offsetFileEntry, startOffset);
3648
3914
  readCommonFooter(fileEntry, fileEntry, directoryView, offset + 6);
3915
+ fileEntry.offset += prependedDataLength;
3916
+ startOffset = Math.min(fileEntry.offset, startOffset);
3917
+ if (checkAmbiguity) {
3918
+ if (filenames.has(fileEntry.filename)) {
3919
+ duplicateFilename = true;
3920
+ }
3921
+ filenames.add(fileEntry.filename);
3922
+ }
3649
3923
  const unixExternalUpper = (fileEntry.externalFileAttributes >> 16) & MAX_16_BITS;
3650
3924
  if (fileEntry.unixMode === UNDEFINED_VALUE && (unixExternalUpper & (FILE_ATTR_UNIX_DEFAULT_MASK | FILE_ATTR_UNIX_EXECUTABLE_MASK | FILE_ATTR_UNIX_TYPE_DIR)) != 0) {
3651
3925
  fileEntry.unixMode = unixExternalUpper;
@@ -3666,7 +3940,7 @@
3666
3940
  internalFileAttribute: fileEntry.internalFileAttributes,
3667
3941
  externalFileAttribute: fileEntry.externalFileAttributes,
3668
3942
  executable,
3669
- directory: modeIsDir || upperIsDir || (msDosCompatible && msdosAttributes.directory) || (filename.endsWith(DIRECTORY_SIGNATURE) && !fileEntry.uncompressedSize),
3943
+ directory: modeIsDir || upperIsDir || (msDosCompatible && msdosAttributes.directory) || (fileEntry.filename.endsWith(DIRECTORY_SIGNATURE) && !fileEntry.uncompressedSize),
3670
3944
  zipCrypto: fileEntry.encrypted && !fileEntry.extraFieldAES
3671
3945
  });
3672
3946
  const entry = new Entry(fileEntry);
@@ -3689,6 +3963,15 @@
3689
3963
  }
3690
3964
  yield entry;
3691
3965
  }
3966
+ if (checkAmbiguity && offset != declaredDirectoryDataLength) {
3967
+ throwAmbiguousArchive("trailing central directory data");
3968
+ }
3969
+ if (duplicateFilename) {
3970
+ throwAmbiguousArchive("duplicate filename");
3971
+ }
3972
+ if (checkAmbiguity && (prependedDataLength || (filesLength && startOffset > 0))) {
3973
+ throwAmbiguousArchive("prepended data");
3974
+ }
3692
3975
  const extractPrependedData = getOptionValue$1(zipReader, options, OPTION_EXTRACT_PREPENDED_DATA);
3693
3976
  const extractAppendedData = getOptionValue$1(zipReader, options, OPTION_EXTRACT_APPENDED_DATA);
3694
3977
  if (extractPrependedData) {
@@ -3727,6 +4010,7 @@
3727
4010
  const zipEntry = this;
3728
4011
  const {
3729
4012
  reader,
4013
+ index,
3730
4014
  offset,
3731
4015
  diskNumberStart,
3732
4016
  extraFieldAES,
@@ -3752,27 +4036,42 @@
3752
4036
  rawPassword = rawPassword && rawPassword.length && rawPassword;
3753
4037
  if (extraFieldAES) {
3754
4038
  if (extraFieldAES.originalCompressionMethod != COMPRESSION_METHOD_AES) {
3755
- throw new Error(ERR_UNSUPPORTED_COMPRESSION);
4039
+ throw new Error(ERR_UNSUPPORTED_COMPRESSION$1);
3756
4040
  }
3757
4041
  }
3758
4042
  if ((compressionMethod != COMPRESSION_METHOD_STORE && compressionMethod != COMPRESSION_METHOD_DEFLATE && compressionMethod != COMPRESSION_METHOD_DEFLATE_64) && !passThrough) {
3759
- throw new Error(ERR_UNSUPPORTED_COMPRESSION);
4043
+ throw new Error(ERR_UNSUPPORTED_COMPRESSION$1);
3760
4044
  }
3761
- if (getUint32(dataView, 0) != LOCAL_FILE_HEADER_SIGNATURE) {
4045
+ if (dataArray.length < HEADER_SIZE || getUint32(dataView, 0) != LOCAL_FILE_HEADER_SIGNATURE) {
3762
4046
  throw new Error(ERR_LOCAL_FILE_HEADER_NOT_FOUND);
3763
4047
  }
3764
4048
  readCommonHeader(localDirectory, dataView, 4);
3765
4049
  const {
3766
4050
  extraFieldLength,
3767
- filenameLength,
3768
- lastAccessDate,
3769
- creationDate
4051
+ filenameLength
3770
4052
  } = localDirectory;
3771
- localDirectory.rawExtraField = extraFieldLength ?
3772
- await readUint8Array(reader, offset + HEADER_SIZE + filenameLength, extraFieldLength, diskNumberStart) :
3773
- new Uint8Array();
4053
+ const checkAmbiguity = getOptionValue$1(zipEntry, options, OPTION_CHECK_AMBIGUITY);
4054
+ let rawLocalFilename = new Uint8Array();
4055
+ if (checkAmbiguity && (filenameLength || extraFieldLength)) {
4056
+ const trailingDataArray = await readUint8Array(reader, offset + HEADER_SIZE, filenameLength + extraFieldLength, diskNumberStart);
4057
+ rawLocalFilename = trailingDataArray.subarray(0, filenameLength);
4058
+ localDirectory.rawExtraField = trailingDataArray.subarray(filenameLength);
4059
+ } else {
4060
+ localDirectory.rawExtraField = extraFieldLength ?
4061
+ await readUint8Array(reader, offset + HEADER_SIZE + filenameLength, extraFieldLength, diskNumberStart) :
4062
+ new Uint8Array();
4063
+ }
3774
4064
  readCommonFooter(zipEntry, localDirectory, dataView, 4, true);
3775
- Object.assign(fileEntry, { lastAccessDate, creationDate });
4065
+ if (checkAmbiguity) {
4066
+ checkLocalDirectory(zipEntry, localDirectory, rawLocalFilename);
4067
+ }
4068
+ const { lastAccessDate, creationDate } = localDirectory;
4069
+ if (lastAccessDate) {
4070
+ fileEntry.lastAccessDate = lastAccessDate;
4071
+ }
4072
+ if (creationDate) {
4073
+ fileEntry.creationDate = creationDate;
4074
+ }
3776
4075
  const encrypted = zipEntry.encrypted && localDirectory.encrypted && !passThrough;
3777
4076
  const zipCrypto = encrypted && !extraFieldAES;
3778
4077
  if (!passThrough) {
@@ -3818,7 +4117,7 @@
3818
4117
  outputSize: passThrough ? compressedSize : uncompressedSize,
3819
4118
  signature,
3820
4119
  compressed: compressionMethod != 0 && !passThrough,
3821
- encrypted: zipEntry.encrypted && !passThrough,
4120
+ encrypted,
3822
4121
  useWebWorkers: getOptionValue$1(zipEntry, options, OPTION_USE_WEB_WORKERS),
3823
4122
  useCompressionStream,
3824
4123
  transferStreams: getOptionValue$1(zipEntry, options, OPTION_TRANSFER_STREAMS),
@@ -3832,6 +4131,7 @@
3832
4131
  await detectOverlappingEntry({
3833
4132
  reader,
3834
4133
  fileEntry,
4134
+ index,
3835
4135
  offset,
3836
4136
  diskNumberStart,
3837
4137
  signature,
@@ -3934,7 +4234,7 @@
3934
4234
  directory.extraFieldUnicodeComment = extraFieldUnicodeComment;
3935
4235
  }
3936
4236
  const extraFieldAES = extraField.get(EXTRAFIELD_TYPE_AES);
3937
- if (extraFieldAES) {
4237
+ if (extraFieldAES && extraFieldAES.data.length >= 7) {
3938
4238
  readExtraFieldAES(extraFieldAES, directory, compressionMethod);
3939
4239
  directory.extraFieldAES = extraFieldAES;
3940
4240
  } else {
@@ -3971,19 +4271,23 @@
3971
4271
  directory.zip64 = true;
3972
4272
  const extraFieldView = getDataView$1(extraFieldZip64.data);
3973
4273
  const missingProperties = ZIP64_PROPERTIES.filter(([propertyName, max]) => directory[propertyName] == max);
4274
+ const requiredLength = missingProperties.reduce((length, [, max]) => length + ZIP64_EXTRACTION[max].bytes, 0);
4275
+ if (extraFieldZip64.data.length < requiredLength) {
4276
+ throw new Error(ERR_EXTRAFIELD_ZIP64_NOT_FOUND);
4277
+ }
3974
4278
  for (let indexMissingProperty = 0, offset = 0; indexMissingProperty < missingProperties.length; indexMissingProperty++) {
3975
4279
  const [propertyName, max] = missingProperties[indexMissingProperty];
3976
- if (directory[propertyName] == max) {
3977
- const extraction = ZIP64_EXTRACTION[max];
3978
- directory[propertyName] = extraFieldZip64[propertyName] = extraction.getValue(extraFieldView, offset);
3979
- offset += extraction.bytes;
3980
- } else if (extraFieldZip64[propertyName]) {
3981
- throw new Error(ERR_EXTRAFIELD_ZIP64_NOT_FOUND);
3982
- }
4280
+ const extraction = ZIP64_EXTRACTION[max];
4281
+ directory[propertyName] = extraFieldZip64[propertyName] = extraction.getValue(extraFieldView, offset);
4282
+ offset += extraction.bytes;
3983
4283
  }
3984
4284
  }
3985
4285
 
3986
4286
  function readExtraFieldUnicode(extraFieldUnicode, propertyName, rawPropertyName, directory, fileEntry) {
4287
+ if (extraFieldUnicode.data.length < 5) {
4288
+ extraFieldUnicode.valid = false;
4289
+ return;
4290
+ }
3987
4291
  const extraFieldView = getDataView$1(extraFieldUnicode.data);
3988
4292
  const crc32 = new Crc32();
3989
4293
  crc32.append(fileEntry[rawPropertyName]);
@@ -4056,32 +4360,30 @@
4056
4360
  function readExtraFieldUnix(extraField, directory, isInfoZip) {
4057
4361
  try {
4058
4362
  const view = getDataView$1(new Uint8Array(extraField.data));
4059
- let offset = 0;
4060
- const version = getUint8(view, offset++);
4061
- const uidSize = getUint8(view, offset++);
4062
- const uidBytes = extraField.data.subarray(offset, offset + uidSize);
4063
- offset += uidSize;
4064
- const uid = unpackUnixId(uidBytes);
4065
- const gidSize = getUint8(view, offset++);
4066
- const gidBytes = extraField.data.subarray(offset, offset + gidSize);
4067
- offset += gidSize;
4068
- const gid = unpackUnixId(gidBytes);
4069
- let unixMode = UNDEFINED_VALUE;
4070
- if (!isInfoZip && offset + 2 <= extraField.data.length) {
4071
- const base = extraField.data;
4072
- const modeView = new DataView(base.buffer, base.byteOffset + offset, 2);
4073
- unixMode = modeView.getUint16(0, true);
4074
- }
4075
- Object.assign(extraField, { version, uid, gid, unixMode });
4363
+ let uid, gid;
4364
+ if (isInfoZip) {
4365
+ // Info-ZIP New Unix "ux" field (0x7875): version + variable-length uid/gid
4366
+ let offset = 0;
4367
+ const version = getUint8(view, offset++);
4368
+ const uidSize = getUint8(view, offset++);
4369
+ uid = unpackUnixId(extraField.data.subarray(offset, offset + uidSize));
4370
+ offset += uidSize;
4371
+ const gidSize = getUint8(view, offset++);
4372
+ gid = unpackUnixId(extraField.data.subarray(offset, offset + gidSize));
4373
+ Object.assign(extraField, { version, uid, gid });
4374
+ } 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
+ uid = getUint16(view, 0);
4378
+ gid = getUint16(view, 2);
4379
+ Object.assign(extraField, { uid, gid });
4380
+ }
4076
4381
  if (uid !== UNDEFINED_VALUE) {
4077
4382
  directory.uid = uid;
4078
4383
  }
4079
4384
  if (gid !== UNDEFINED_VALUE) {
4080
4385
  directory.gid = gid;
4081
4386
  }
4082
- if (unixMode !== UNDEFINED_VALUE) {
4083
- directory.unixMode = unixMode;
4084
- }
4085
4387
  } catch {
4086
4388
  // ignored
4087
4389
  }
@@ -4095,6 +4397,9 @@
4095
4397
  }
4096
4398
 
4097
4399
  function readExtraFieldExtendedTimestamp(extraFieldExtendedTimestamp, directory, localDirectory) {
4400
+ if (!extraFieldExtendedTimestamp.data.length) {
4401
+ return;
4402
+ }
4098
4403
  const extraFieldView = getDataView$1(extraFieldExtendedTimestamp.data);
4099
4404
  const flags = getUint8(extraFieldView, 0);
4100
4405
  const timeProperties = [];
@@ -4120,7 +4425,8 @@
4120
4425
  timeProperties.forEach((propertyName, indexProperty) => {
4121
4426
  if (extraFieldExtendedTimestamp.data.length >= offset + 4) {
4122
4427
  const time = getUint32(extraFieldView, offset);
4123
- directory[propertyName] = extraFieldExtendedTimestamp[propertyName] = new Date(time * 1000);
4428
+ // the extended timestamp is a signed 32-bit time_t value
4429
+ directory[propertyName] = extraFieldExtendedTimestamp[propertyName] = new Date((time | 0) * 1000);
4124
4430
  const rawPropertyName = timeRawProperties[indexProperty];
4125
4431
  extraFieldExtendedTimestamp[rawPropertyName] = time;
4126
4432
  }
@@ -4131,6 +4437,7 @@
4131
4437
  async function detectOverlappingEntry({
4132
4438
  reader,
4133
4439
  fileEntry,
4440
+ index,
4134
4441
  offset,
4135
4442
  diskNumberStart,
4136
4443
  signature,
@@ -4142,10 +4449,9 @@
4142
4449
  readRanges
4143
4450
  }) {
4144
4451
  let diskOffset = 0;
4145
- if (diskNumberStart) {
4146
- for (let indexReader = 0; indexReader < diskNumberStart; indexReader++) {
4147
- const diskReader = reader.readers[indexReader];
4148
- diskOffset += diskReader.size;
4452
+ if (diskNumberStart && reader.readers) {
4453
+ for (let indexReader = 0; indexReader < Math.min(diskNumberStart, reader.readers.length); indexReader++) {
4454
+ diskOffset += reader.readers[indexReader].size;
4149
4455
  }
4150
4456
  }
4151
4457
  let dataDescriptorLength = 0;
@@ -4158,7 +4464,8 @@
4158
4464
  }
4159
4465
  if (dataDescriptorLength) {
4160
4466
  const dataDescriptorArray = await readUint8Array(reader, dataOffset + compressedSize, dataDescriptorLength + DATA_DESCRIPTOR_RECORD_SIGNATURE_LENGTH, diskNumberStart);
4161
- const dataDescriptorSignature = getUint32(getDataView$1(dataDescriptorArray), 0) == DATA_DESCRIPTOR_RECORD_SIGNATURE;
4467
+ const dataDescriptorSignature = dataDescriptorArray.length == dataDescriptorLength + DATA_DESCRIPTOR_RECORD_SIGNATURE_LENGTH &&
4468
+ getUint32(getDataView$1(dataDescriptorArray), 0) == DATA_DESCRIPTOR_RECORD_SIGNATURE;
4162
4469
  if (dataDescriptorSignature) {
4163
4470
  const readSignature = getUint32(getDataView$1(dataDescriptorArray), 4);
4164
4471
  let readCompressedSize;
@@ -4183,14 +4490,14 @@
4183
4490
  end: diskOffset + dataOffset + compressedSize + dataDescriptorLength,
4184
4491
  fileEntry
4185
4492
  };
4186
- for (const otherRange of readRanges) {
4187
- if (otherRange.fileEntry != fileEntry && range.start >= otherRange.start && range.start < otherRange.end) {
4493
+ for (const [otherIndex, otherRange] of readRanges) {
4494
+ if (otherIndex != index && range.start < otherRange.end && otherRange.start < range.end) {
4188
4495
  const error = new Error(ERR_OVERLAPPING_ENTRY);
4189
4496
  error.overlappingEntry = otherRange.fileEntry;
4190
4497
  throw error;
4191
4498
  }
4192
4499
  }
4193
- readRanges.push(range);
4500
+ readRanges.set(index, range);
4194
4501
  }
4195
4502
 
4196
4503
  async function seekSignature(reader, signature, startOffset, minimumBytes, maximumLength) {
@@ -4215,6 +4522,35 @@
4215
4522
  }
4216
4523
  }
4217
4524
 
4525
+ function checkLocalDirectory(zipEntry, localDirectory, rawLocalFilename) {
4526
+ const { rawFilename } = zipEntry;
4527
+ if (rawLocalFilename.length != rawFilename.length ||
4528
+ rawLocalFilename.some((byteValue, indexByte) => byteValue != rawFilename[indexByte])) {
4529
+ throwAmbiguousArchive("mismatched local file header (filename)");
4530
+ }
4531
+ if ((localDirectory.rawBitFlag & BITFLAG_AMBIGUITY_MASK) != (zipEntry.rawBitFlag & BITFLAG_AMBIGUITY_MASK)) {
4532
+ throwAmbiguousArchive("mismatched local file header (general purpose bit flag)");
4533
+ }
4534
+ if (localDirectory.compressionMethod != zipEntry.compressionMethod) {
4535
+ throwAmbiguousArchive("mismatched local file header (compression method)");
4536
+ }
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
+ if (!localDirectory.bitFlag.dataDescriptor &&
4540
+ (localDirectory.signature || localDirectory.compressedSize || localDirectory.uncompressedSize) &&
4541
+ (localDirectory.signature != zipEntry.signature ||
4542
+ localDirectory.compressedSize != zipEntry.compressedSize ||
4543
+ localDirectory.uncompressedSize != zipEntry.uncompressedSize)) {
4544
+ throwAmbiguousArchive("mismatched local file header (signature or sizes)");
4545
+ }
4546
+ }
4547
+
4548
+ function throwAmbiguousArchive(reason) {
4549
+ const error = new Error(ERR_AMBIGUOUS_ARCHIVE);
4550
+ error.reason = reason;
4551
+ throw error;
4552
+ }
4553
+
4218
4554
  function getOptionValue$1(zipReader, options, name) {
4219
4555
  return options[name] === UNDEFINED_VALUE ? zipReader.options[name] : options[name];
4220
4556
  }
@@ -4253,7 +4589,7 @@
4253
4589
  }
4254
4590
 
4255
4591
  function getDataView$1(array) {
4256
- return new DataView(array.buffer);
4592
+ return new DataView(array.buffer, array.byteOffset, array.byteLength);
4257
4593
  }
4258
4594
 
4259
4595
  /*
@@ -4293,13 +4629,19 @@
4293
4629
  const ERR_INVALID_ENCRYPTION_STRENGTH = "The strength must equal 1, 2, or 3";
4294
4630
  const ERR_INVALID_EXTRAFIELD_TYPE = "Extra field type exceeds 65535";
4295
4631
  const ERR_INVALID_EXTRAFIELD_DATA = "Extra field data exceeds 64KB";
4632
+ // same message as the constant exported by zip-reader.js
4633
+ const ERR_UNSUPPORTED_COMPRESSION = "Compression method not supported";
4634
+ const MIN_UNIX_TIME = -2147483648;
4635
+ const MAX_UNIX_TIME = 2147483647;
4296
4636
  const ERR_UNSUPPORTED_FORMAT = "Zip64 is not supported (set the 'zip64' option to 'true')";
4297
4637
  const ERR_UNDEFINED_UNCOMPRESSED_SIZE = "Undefined uncompressed size";
4638
+ const ERR_UNDEFINED_READER = "Undefined reader";
4298
4639
  const ERR_ZIP_NOT_EMPTY = "Zip file not empty";
4299
4640
  const ERR_INVALID_UID = "Invalid uid (must be integer 0..2^32-1)";
4300
4641
  const ERR_INVALID_GID = "Invalid gid (must be integer 0..2^32-1)";
4301
4642
  const ERR_INVALID_UNIX_MODE = "Invalid UNIX mode (must be integer 0..65535)";
4302
4643
  const ERR_INVALID_UNIX_EXTRA_FIELD_TYPE = "Invalid unixExtraFieldType (must be 'infozip' or 'unix')";
4644
+ const ERR_INVALID_UNIX_ID_SIZE = "uid/gid must be 0..65535 for unixExtraFieldType 'unix' (use 'infozip' for larger ids)";
4303
4645
  const ERR_INVALID_MSDOS_ATTRIBUTES = "Invalid msdosAttributesRaw (must be integer 0..255)";
4304
4646
  const ERR_INVALID_MSDOS_DATA = "Invalid msdosAttributes (must be an object with boolean flags)";
4305
4647
 
@@ -4325,9 +4667,10 @@
4325
4667
  files: new Map(),
4326
4668
  filenames: new Set(),
4327
4669
  offset: options[OPTION_OFFSET] === UNDEFINED_VALUE ? writer.size || writer.writable.size || 0 : options[OPTION_OFFSET],
4328
- pendingEntriesSize: 0,
4670
+ initialOffset: options[OPTION_OFFSET] === UNDEFINED_VALUE ? 0 : options[OPTION_OFFSET] - (writer.size || writer.writable.size || 0),
4329
4671
  pendingAddFileCalls: new Set(),
4330
- bufferedWrites: 0
4672
+ bufferedWrites: 0,
4673
+ lastFileEntry: UNDEFINED_VALUE
4331
4674
  });
4332
4675
  }
4333
4676
 
@@ -4336,17 +4679,18 @@
4336
4679
  throw new Error(ERR_ZIP_NOT_EMPTY);
4337
4680
  }
4338
4681
  reader = new GenericReader(reader);
4682
+ await initStream(reader);
4339
4683
  const zipReader = new ZipReader(reader.readable);
4340
4684
  const entries = await zipReader.getEntries();
4341
4685
  await zipReader.close();
4686
+ await initStream(this.writer);
4342
4687
  await reader.readable.pipeTo(this.writer.writable, { preventClose: true, preventAbort: true });
4343
4688
  this.writer.size = this.offset = reader.size;
4344
4689
  this.filenames = new Set(entries.map(entry => entry.filename));
4345
4690
  this.files = new Map(entries.map(entry => {
4346
4691
  const {
4347
4692
  version,
4348
- compressionMethod,
4349
- lastModDate,
4693
+ rawLastModDate,
4350
4694
  lastAccessDate,
4351
4695
  creationDate,
4352
4696
  rawFilename,
@@ -4359,6 +4703,7 @@
4359
4703
  zip64
4360
4704
  } = entry;
4361
4705
  let {
4706
+ compressionMethod,
4362
4707
  rawExtraFieldZip64,
4363
4708
  rawExtraFieldAES,
4364
4709
  rawExtraFieldExtendedTimestamp,
@@ -4371,31 +4716,39 @@
4371
4716
  rawExtraFieldAES = rawExtraFieldAES || new Uint8Array();
4372
4717
  rawExtraFieldExtendedTimestamp = rawExtraFieldExtendedTimestamp || new Uint8Array();
4373
4718
  rawExtraFieldNTFS = rawExtraFieldNTFS || new Uint8Array();
4374
- rawExtraFieldUnix = entry.rawExtraFieldUnix || new Uint8Array();
4719
+ rawExtraFieldUnix = rawExtraFieldUnix || new Uint8Array();
4375
4720
  rawExtraField = rawExtraField || new Uint8Array();
4721
+ if (entry.extraFieldAES) {
4722
+ compressionMethod = COMPRESSION_METHOD_AES;
4723
+ }
4376
4724
  const extraFieldLength = getLength(rawExtraFieldZip64, rawExtraFieldAES, rawExtraFieldExtendedTimestamp, rawExtraFieldNTFS, rawExtraFieldUnix, rawExtraField);
4377
- const zip64UncompressedSize = zip64 && uncompressedSize > MAX_32_BITS;
4378
- const zip64CompressedSize = zip64 && compressedSize > MAX_32_BITS;
4725
+ const zip64UncompressedSize = zip64 && uncompressedSize >= MAX_32_BITS;
4726
+ const zip64CompressedSize = zip64 && compressedSize >= MAX_32_BITS;
4727
+ const bitFlagValue = (getBitFlag(level, languageEncodingFlag, dataDescriptor, encrypted, compressionMethod) & ~BITFLAG_LEVEL) | (level << 1);
4379
4728
  const {
4380
4729
  headerArray,
4381
4730
  headerView
4382
4731
  } = getHeaderArrayData({
4383
4732
  version,
4384
- bitFlag: getBitFlag(level, languageEncodingFlag, dataDescriptor, encrypted, compressionMethod),
4733
+ bitFlag: bitFlagValue,
4385
4734
  compressionMethod,
4386
4735
  uncompressedSize,
4387
4736
  compressedSize,
4388
- lastModDate,
4737
+ rawLastModDate,
4389
4738
  rawFilename,
4390
4739
  zip64CompressedSize,
4391
4740
  zip64UncompressedSize,
4392
4741
  extraFieldLength
4393
4742
  });
4743
+ const { signature } = entry;
4744
+ if (signature !== UNDEFINED_VALUE) {
4745
+ setUint32(headerView, HEADER_OFFSET_SIGNATURE, signature);
4746
+ }
4394
4747
  Object.assign(entry, {
4395
4748
  zip64UncompressedSize,
4396
4749
  zip64CompressedSize,
4397
- zip64Offset: zip64 && this.offset - diskOffset > MAX_32_BITS,
4398
- zip64DiskNumberStart: zip64 && diskNumber > MAX_16_BITS,
4750
+ zip64Offset: zip64 && this.offset - diskOffset >= MAX_32_BITS,
4751
+ zip64DiskNumberStart: zip64 && diskNumber >= MAX_16_BITS,
4399
4752
  rawExtraFieldZip64,
4400
4753
  rawExtraFieldAES,
4401
4754
  rawExtraFieldExtendedTimestamp,
@@ -4413,6 +4766,7 @@
4413
4766
 
4414
4767
  async add(name = "", reader, options = {}) {
4415
4768
  const zipWriter = this;
4769
+ options = Object.assign({}, options);
4416
4770
  const {
4417
4771
  pendingAddFileCalls,
4418
4772
  config
@@ -4423,17 +4777,24 @@
4423
4777
  await new Promise(resolve => pendingEntries.push(resolve));
4424
4778
  }
4425
4779
  let promiseAddFile;
4780
+ let nameAdded;
4426
4781
  try {
4427
4782
  name = name.trim();
4783
+ if (getOptionValue(zipWriter, options, PROPERTY_NAME_DIRECTORY) && !name.endsWith(DIRECTORY_SIGNATURE)) {
4784
+ name += DIRECTORY_SIGNATURE;
4785
+ }
4428
4786
  if (zipWriter.filenames.has(name)) {
4429
4787
  throw new Error(ERR_DUPLICATED_NAME);
4430
4788
  }
4431
4789
  zipWriter.filenames.add(name);
4790
+ nameAdded = true;
4432
4791
  promiseAddFile = addFile(zipWriter, name, reader, options);
4433
4792
  pendingAddFileCalls.add(promiseAddFile);
4434
4793
  return await promiseAddFile;
4435
4794
  } catch (error) {
4436
- zipWriter.filenames.delete(name);
4795
+ if (nameAdded) {
4796
+ zipWriter.filenames.delete(name);
4797
+ }
4437
4798
  throw error;
4438
4799
  } finally {
4439
4800
  pendingAddFileCalls.delete(promiseAddFile);
@@ -4466,6 +4827,9 @@
4466
4827
  const zipWriter = this;
4467
4828
  const { pendingAddFileCalls, writer } = this;
4468
4829
  const { writable } = writer;
4830
+ if (getLength(comment) > MAX_16_BITS) {
4831
+ throw new Error(ERR_INVALID_COMMENT);
4832
+ }
4469
4833
  while (pendingAddFileCalls.size) {
4470
4834
  await Promise.allSettled(Array.from(pendingAddFileCalls));
4471
4835
  }
@@ -4479,6 +4843,37 @@
4479
4843
  }
4480
4844
 
4481
4845
  async function addFile(zipWriter, name, reader, options) {
4846
+ const attributesInfo = resolveAttributes(zipWriter, name, options);
4847
+ ({ name } = attributesInfo);
4848
+ const metadataInfo = resolveMetadata(zipWriter, name, options);
4849
+ const { comment } = metadataInfo;
4850
+ const extraField = options[PROPERTY_NAME_EXTRA_FIELD];
4851
+ zipWriter.files.set(name, UNDEFINED_VALUE);
4852
+ let fileEntry;
4853
+ try {
4854
+ const sizesInfo = await resolveSizes(zipWriter, reader, metadataInfo, options);
4855
+ ({ reader } = sizesInfo);
4856
+ const { diskOffset, diskNumber } = zipWriter.writer;
4857
+ options = Object.assign({}, options, attributesInfo.resolvedOptions, metadataInfo.resolvedOptions, sizesInfo.resolvedOptions, {
4858
+ internalFileAttribute: metadataInfo.resolvedOptions.internalFileAttributes,
4859
+ externalFileAttribute: attributesInfo.resolvedOptions.externalFileAttributes,
4860
+ signature: options[PROPERTY_NAME_SIGNATURE],
4861
+ offset: zipWriter.offset - diskOffset,
4862
+ diskNumberStart: diskNumber
4863
+ });
4864
+ const headerInfo = getHeaderInfo(options);
4865
+ const dataDescriptorInfo = getDataDescriptorInfo(options);
4866
+ const metadataSize = getLength(headerInfo.localHeaderArray, dataDescriptorInfo.dataDescriptorArray);
4867
+ fileEntry = await getFileEntry(zipWriter, name, reader, { headerInfo, dataDescriptorInfo, metadataSize }, options);
4868
+ } catch (error) {
4869
+ zipWriter.files.delete(name);
4870
+ throw error;
4871
+ }
4872
+ Object.assign(fileEntry, { name, comment, extraField });
4873
+ return new Entry(fileEntry);
4874
+ }
4875
+
4876
+ function resolveAttributes(zipWriter, name, options) {
4482
4877
  name = name.trim();
4483
4878
  let msDosCompatible = getOptionValue(zipWriter, options, PROPERTY_NAME_MS_DOS_COMPATIBLE);
4484
4879
  let versionMadeBy = getOptionValue(zipWriter, options, PROPERTY_NAME_VERSION_MADE_BY, msDosCompatible ? 20 : 768);
@@ -4486,7 +4881,7 @@
4486
4881
  const uid = getOptionValue(zipWriter, options, PROPERTY_NAME_UID);
4487
4882
  const gid = getOptionValue(zipWriter, options, PROPERTY_NAME_GID);
4488
4883
  let unixMode = getOptionValue(zipWriter, options, PROPERTY_NAME_UNIX_MODE);
4489
- const unixExtraFieldType = getOptionValue(zipWriter, options, OPTION_UNIX_EXTRA_FIELD_TYPE);
4884
+ let unixExtraFieldType = getOptionValue(zipWriter, options, OPTION_UNIX_EXTRA_FIELD_TYPE);
4490
4885
  let setuid = getOptionValue(zipWriter, options, PROPERTY_NAME_SETUID);
4491
4886
  let setgid = getOptionValue(zipWriter, options, PROPERTY_NAME_SETGID);
4492
4887
  let sticky = getOptionValue(zipWriter, options, PROPERTY_NAME_STICKY);
@@ -4502,6 +4897,13 @@
4502
4897
  if (unixExtraFieldType !== UNDEFINED_VALUE && unixExtraFieldType !== INFOZIP_EXTRA_FIELD_TYPE && unixExtraFieldType !== UNIX_EXTRA_FIELD_TYPE) {
4503
4898
  throw new Error(ERR_INVALID_UNIX_EXTRA_FIELD_TYPE);
4504
4899
  }
4900
+ if (unixExtraFieldType === UNIX_EXTRA_FIELD_TYPE &&
4901
+ ((uid !== UNDEFINED_VALUE && uid > MAX_16_BITS) || (gid !== UNDEFINED_VALUE && gid > MAX_16_BITS))) {
4902
+ throw new Error(ERR_INVALID_UNIX_ID_SIZE);
4903
+ }
4904
+ if (unixExtraFieldType === UNDEFINED_VALUE && (uid !== UNDEFINED_VALUE || gid !== UNDEFINED_VALUE)) {
4905
+ unixExtraFieldType = INFOZIP_EXTRA_FIELD_TYPE;
4906
+ }
4505
4907
  let msdosAttributesRaw = getOptionValue(zipWriter, options, PROPERTY_NAME_MSDOS_ATTRIBUTES_RAW);
4506
4908
  let msdosAttributes = getOptionValue(zipWriter, options, PROPERTY_NAME_MSDOS_ATTRIBUTES);
4507
4909
  const hasUnixMetadata = uid !== UNDEFINED_VALUE || gid !== UNDEFINED_VALUE || unixMode !== UNDEFINED_VALUE || unixExtraFieldType;
@@ -4572,6 +4974,27 @@
4572
4974
  if (hasMsDosProvided) {
4573
4975
  externalFileAttributes = (externalFileAttributes & MAX_32_BITS) | (msdosAttributesRaw & MAX_8_BITS);
4574
4976
  }
4977
+ return {
4978
+ name,
4979
+ resolvedOptions: {
4980
+ versionMadeBy,
4981
+ msDosCompatible,
4982
+ externalFileAttributes,
4983
+ unixExternalUpper,
4984
+ uid,
4985
+ gid,
4986
+ unixMode,
4987
+ unixExtraFieldType,
4988
+ setuid,
4989
+ setgid,
4990
+ sticky,
4991
+ msdosAttributesRaw,
4992
+ msdosAttributes
4993
+ }
4994
+ };
4995
+ }
4996
+
4997
+ function resolveMetadata(zipWriter, name, options) {
4575
4998
  const encode = getOptionValue(zipWriter, options, OPTION_ENCODE_TEXT, encodeText);
4576
4999
  let rawFilename = encode(name);
4577
5000
  if (rawFilename === UNDEFINED_VALUE) {
@@ -4610,10 +5033,14 @@
4610
5033
  const transferStreams = getOptionValue(zipWriter, options, OPTION_TRANSFER_STREAMS, true);
4611
5034
  const bufferedWrite = getOptionValue(zipWriter, options, OPTION_BUFFERED_WRITE);
4612
5035
  const createTempStream = getOptionValue(zipWriter, options, OPTION_CREATE_TEMP_STREAM);
4613
- const dataDescriptorSignature = getOptionValue(zipWriter, options, OPTION_DATA_DESCRIPTOR_SIGNATURE, false);
5036
+ const dataDescriptorSignature = getOptionValue(zipWriter, options, OPTION_DATA_DESCRIPTOR_SIGNATURE, true);
4614
5037
  const signal = getOptionValue(zipWriter, options, OPTION_SIGNAL);
4615
5038
  const useUnicodeFileNames = getOptionValue(zipWriter, options, OPTION_USE_UNICODE_FILE_NAMES, true);
4616
5039
  const compressionMethod = getOptionValue(zipWriter, options, PROPERTY_NAME_COMPRESSION_METHOD);
5040
+ if (!passThrough && compressionMethod !== UNDEFINED_VALUE &&
5041
+ compressionMethod !== COMPRESSION_METHOD_STORE && compressionMethod !== COMPRESSION_METHOD_DEFLATE) {
5042
+ throw new Error(ERR_UNSUPPORTED_COMPRESSION);
5043
+ }
4617
5044
  let level = getOptionValue(zipWriter, options, OPTION_LEVEL);
4618
5045
  let useCompressionStream = getOptionValue(zipWriter, options, OPTION_USE_COMPRESSION_STREAM);
4619
5046
  let dataDescriptor = getOptionValue(zipWriter, options, OPTION_DATA_DESCRIPTOR);
@@ -4629,7 +5056,7 @@
4629
5056
  if (!useCompressionStream && (zipWriter.config.CompressionStream === UNDEFINED_VALUE && zipWriter.config.CompressionStreamZlib === UNDEFINED_VALUE)) {
4630
5057
  level = 0;
4631
5058
  }
4632
- let zip64 = getOptionValue(zipWriter, options, PROPERTY_NAME_ZIP64);
5059
+ const zip64 = getOptionValue(zipWriter, options, PROPERTY_NAME_ZIP64);
4633
5060
  if (!zipCrypto && (password !== UNDEFINED_VALUE || rawPassword !== UNDEFINED_VALUE) && !(encryptionStrength >= 1 && encryptionStrength <= 3)) {
4634
5061
  throw new Error(ERR_INVALID_ENCRYPTION_STRENGTH);
4635
5062
  }
@@ -4640,6 +5067,7 @@
4640
5067
  let offset = 0;
4641
5068
  extraField.forEach(data => extraFieldSize += 4 + getLength(data));
4642
5069
  rawExtraField = new Uint8Array(extraFieldSize);
5070
+ const rawExtraFieldView = getDataView(rawExtraField);
4643
5071
  extraField.forEach((data, type) => {
4644
5072
  if (type > MAX_16_BITS) {
4645
5073
  throw new Error(ERR_INVALID_EXTRAFIELD_TYPE);
@@ -4647,22 +5075,72 @@
4647
5075
  if (getLength(data) > MAX_16_BITS) {
4648
5076
  throw new Error(ERR_INVALID_EXTRAFIELD_DATA);
4649
5077
  }
4650
- arraySet(rawExtraField, new Uint16Array([type]), offset);
4651
- arraySet(rawExtraField, new Uint16Array([getLength(data)]), offset + 2);
5078
+ setUint16(rawExtraFieldView, offset, type);
5079
+ setUint16(rawExtraFieldView, offset + 2, getLength(data));
4652
5080
  arraySet(rawExtraField, data, offset + 4);
4653
5081
  offset += 4 + getLength(data);
4654
5082
  });
4655
5083
  }
5084
+ return {
5085
+ comment,
5086
+ resolvedOptions: {
5087
+ rawFilename,
5088
+ rawComment,
5089
+ version,
5090
+ lastModDate,
5091
+ lastAccessDate,
5092
+ creationDate,
5093
+ internalFileAttributes,
5094
+ passThrough,
5095
+ password,
5096
+ rawPassword,
5097
+ encryptionStrength,
5098
+ zipCrypto,
5099
+ extendedTimestamp,
5100
+ keepOrder,
5101
+ useWebWorkers,
5102
+ transferStreams,
5103
+ bufferedWrite,
5104
+ createTempStream,
5105
+ dataDescriptorSignature,
5106
+ signal,
5107
+ useUnicodeFileNames,
5108
+ compressionMethod,
5109
+ level,
5110
+ useCompressionStream,
5111
+ dataDescriptor,
5112
+ zip64,
5113
+ rawExtraField
5114
+ }
5115
+ };
5116
+ }
5117
+
5118
+ async function resolveSizes(zipWriter, reader, { resolvedOptions: metadata }, options) {
5119
+ const { passThrough, zipCrypto, password, rawPassword, encryptionStrength } = metadata;
5120
+ let { dataDescriptor, zip64, level, compressionMethod } = metadata;
4656
5121
  let maximumCompressedSize = 0;
4657
- let maximumEntrySize = 0;
4658
5122
  let uncompressedSize = 0;
4659
5123
  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
+ if (!reader) {
5127
+ throw new Error(ERR_UNDEFINED_READER);
5128
+ }
4660
5129
  uncompressedSize = options[PROPERTY_NAME_UNCOMPRESSED_SIZE];
4661
5130
  if (uncompressedSize === UNDEFINED_VALUE) {
4662
5131
  throw new Error(ERR_UNDEFINED_UNCOMPRESSED_SIZE);
4663
5132
  }
4664
5133
  }
4665
5134
  const zip64Enabled = zip64 === true;
5135
+ 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
+ const encryptedEntry = Boolean(reader) && (Boolean((password && getLength(password)) || (rawPassword && getLength(rawPassword))) || (passThrough && encrypted));
5139
+ if (!reader) {
5140
+ level = 0;
5141
+ compressionMethod = COMPRESSION_METHOD_STORE;
5142
+ }
5143
+ const encryptionOverhead = encryptedEntry ? (zipCrypto ? 12 : 16 + encryptionStrength * 4) : 0;
4666
5144
  if (reader) {
4667
5145
  reader = new GenericReader(reader);
4668
5146
  await initStream(reader);
@@ -4675,16 +5153,15 @@
4675
5153
  }
4676
5154
  } else {
4677
5155
  options.uncompressedSize = uncompressedSize = reader.size;
4678
- maximumCompressedSize = getMaximumCompressedSize(uncompressedSize);
5156
+ maximumCompressedSize = getMaximumCompressedSize(uncompressedSize) + encryptionOverhead;
4679
5157
  }
4680
5158
  } else {
4681
5159
  options.uncompressedSize = uncompressedSize;
4682
- maximumCompressedSize = getMaximumCompressedSize(uncompressedSize);
5160
+ maximumCompressedSize = getMaximumCompressedSize(uncompressedSize) + encryptionOverhead;
4683
5161
  }
4684
5162
  }
4685
- const { diskOffset, diskNumber } = zipWriter.writer;
4686
- const zip64UncompressedSize = zip64Enabled || uncompressedSize > MAX_32_BITS;
4687
- const zip64CompressedSize = zip64Enabled || maximumCompressedSize > MAX_32_BITS;
5163
+ const zip64UncompressedSize = zip64Enabled || uncompressedSize >= MAX_32_BITS;
5164
+ const zip64CompressedSize = zip64Enabled || maximumCompressedSize >= MAX_32_BITS;
4688
5165
  if (zip64UncompressedSize || zip64CompressedSize) {
4689
5166
  if (zip64 === false) {
4690
5167
  throw new Error(ERR_UNSUPPORTED_FORMAT);
@@ -4693,73 +5170,19 @@
4693
5170
  }
4694
5171
  }
4695
5172
  zip64 = zip64 || false;
4696
- const encrypted = getOptionValue(zipWriter, options, PROPERTY_NAME_ENCRYPTED);
4697
- options = Object.assign({}, options, {
4698
- rawFilename,
4699
- rawComment,
4700
- version,
4701
- versionMadeBy,
4702
- lastModDate,
4703
- lastAccessDate,
4704
- creationDate,
4705
- rawExtraField,
4706
- zip64,
4707
- zip64UncompressedSize,
4708
- zip64CompressedSize,
4709
- password,
4710
- rawPassword,
4711
- level,
4712
- useWebWorkers,
4713
- transferStreams,
4714
- encryptionStrength,
4715
- extendedTimestamp,
4716
- zipCrypto,
4717
- bufferedWrite,
4718
- createTempStream,
4719
- keepOrder,
4720
- useUnicodeFileNames,
4721
- dataDescriptor,
4722
- dataDescriptorSignature,
4723
- signal,
4724
- msDosCompatible,
4725
- internalFileAttribute: internalFileAttributes,
4726
- internalFileAttributes,
4727
- externalFileAttribute: externalFileAttributes,
4728
- externalFileAttributes,
4729
- useCompressionStream,
4730
- passThrough,
4731
- encrypted: Boolean((password && getLength(password)) || (rawPassword && getLength(rawPassword))) || (passThrough && encrypted),
4732
- signature: options[PROPERTY_NAME_SIGNATURE],
4733
- compressionMethod,
4734
- uncompressedSize,
4735
- offset: zipWriter.offset - diskOffset,
4736
- diskNumberStart: diskNumber,
4737
- uid,
4738
- gid,
4739
- setuid,
4740
- setgid,
4741
- sticky,
4742
- unixMode,
4743
- msdosAttributesRaw,
4744
- msdosAttributes,
4745
- unixExternalUpper
4746
- });
4747
- const headerInfo = getHeaderInfo(options);
4748
- const dataDescriptorInfo = getDataDescriptorInfo(options);
4749
- const metadataSize = getLength(headerInfo.localHeaderArray, dataDescriptorInfo.dataDescriptorArray);
4750
- maximumEntrySize = metadataSize + maximumCompressedSize;
4751
- if (zipWriter.options[OPTION_USDZ]) {
4752
- maximumEntrySize += maximumEntrySize + 64;
4753
- }
4754
- zipWriter.pendingEntriesSize += maximumEntrySize;
4755
- let fileEntry;
4756
- try {
4757
- fileEntry = await getFileEntry(zipWriter, name, reader, { headerInfo, dataDescriptorInfo, metadataSize }, options);
4758
- } finally {
4759
- zipWriter.pendingEntriesSize -= maximumEntrySize;
4760
- }
4761
- Object.assign(fileEntry, { name, comment, extraField });
4762
- return new Entry(fileEntry);
5173
+ return {
5174
+ reader,
5175
+ resolvedOptions: {
5176
+ dataDescriptor,
5177
+ zip64,
5178
+ zip64UncompressedSize,
5179
+ zip64CompressedSize,
5180
+ uncompressedSize,
5181
+ level,
5182
+ compressionMethod,
5183
+ encrypted: encryptedEntry
5184
+ }
5185
+ };
4763
5186
  }
4764
5187
 
4765
5188
  async function getFileEntry(zipWriter, name, reader, entryInfo, options) {
@@ -4776,37 +5199,40 @@
4776
5199
  headerInfo
4777
5200
  } = entryInfo;
4778
5201
  const usdz = zipWriter.options[OPTION_USDZ];
4779
- const previousFileEntry = Array.from(files.values()).pop();
5202
+ const previousFileEntry = zipWriter.lastFileEntry;
4780
5203
  let fileEntry = {};
4781
5204
  let bufferedWrite;
4782
5205
  let releaseLockWriter;
4783
5206
  let releaseLockCurrentFileEntry;
4784
5207
  let writingBufferedEntryData;
4785
5208
  let writingEntryData;
5209
+ let writerSizeBeforeEntry;
5210
+ let flushedBufferedSize = 0;
4786
5211
  let fileWriter;
4787
5212
  files.set(name, fileEntry);
5213
+ zipWriter.lastFileEntry = fileEntry;
4788
5214
  try {
4789
5215
  let lockPreviousFileEntry;
4790
5216
  if (keepOrder) {
4791
5217
  lockPreviousFileEntry = previousFileEntry && previousFileEntry.lock;
4792
5218
  requestLockCurrentFileEntry();
4793
5219
  }
4794
- if ((options.bufferedWrite || !keepOrder || zipWriter.writerLocked || zipWriter.bufferedWrites || !dataDescriptor) && !usdz) {
5220
+ if (options.bufferedWrite || !keepOrder || zipWriter.writerLocked || zipWriter.bufferedWrites || !dataDescriptor) {
5221
+ bufferedWrite = true;
5222
+ zipWriter.bufferedWrites++;
4795
5223
  if (options.createTempStream) {
4796
5224
  fileWriter = await options.createTempStream();
4797
5225
  } else {
4798
5226
  fileWriter = new TransformStream(UNDEFINED_VALUE, UNDEFINED_VALUE, { highWaterMark: INFINITY_VALUE });
4799
5227
  }
4800
5228
  fileWriter.size = 0;
4801
- bufferedWrite = true;
4802
- zipWriter.bufferedWrites++;
4803
5229
  await initStream(writer);
4804
5230
  } else {
4805
5231
  fileWriter = writer;
4806
5232
  await requestLockWriter();
4807
5233
  }
4808
5234
  await initStream(fileWriter);
4809
- const { writable, diskOffset } = writer;
5235
+ const { diskOffset } = writer;
4810
5236
  if (zipWriter.addSplitZipSignature) {
4811
5237
  delete zipWriter.addSplitZipSignature;
4812
5238
  const signatureArray = new Uint8Array(4);
@@ -4815,21 +5241,20 @@
4815
5241
  await writeData(writer, signatureArray);
4816
5242
  zipWriter.offset += 4;
4817
5243
  }
4818
- if (usdz) {
5244
+ if (usdz && !bufferedWrite) {
4819
5245
  appendExtraFieldUSDZ(entryInfo, zipWriter.offset - diskOffset);
4820
5246
  }
4821
- const {
4822
- localHeaderView,
4823
- localHeaderArray
4824
- } = headerInfo;
5247
+ const { localHeaderArray } = headerInfo;
4825
5248
  if (!bufferedWrite) {
4826
5249
  await lockPreviousFileEntry;
4827
- await skipDiskIfNeeded(writable);
5250
+ await skipDiskIfNeeded();
4828
5251
  }
4829
- const { diskNumber } = writer;
4830
- fileEntry.diskNumberStart = diskNumber;
5252
+ const diskNumberStart = writer.diskNumber;
5253
+ const entryOffset = getSegmentOffset(zipWriter, writer);
5254
+ fileEntry.diskNumberStart = diskNumberStart;
4831
5255
  if (!bufferedWrite) {
4832
5256
  writingEntryData = true;
5257
+ writerSizeBeforeEntry = writer.size;
4833
5258
  await writeData(fileWriter, localHeaderArray);
4834
5259
  }
4835
5260
  fileEntry = await createFileEntry(reader, fileWriter, fileEntry, entryInfo, zipWriter.config, options);
@@ -4842,16 +5267,24 @@
4842
5267
  await Promise.all([fileWriter.writable.getWriter().close(), lockPreviousFileEntry]);
4843
5268
  await requestLockWriter();
4844
5269
  writingBufferedEntryData = true;
5270
+ writerSizeBeforeEntry = writer.size;
5271
+ await skipDiskIfNeeded();
4845
5272
  fileEntry.diskNumberStart = writer.diskNumber;
4846
- fileEntry.offset = zipWriter.offset - writer.diskOffset;
4847
- updateLocalHeader(fileEntry, localHeaderView, options);
4848
- await skipDiskIfNeeded(writable);
4849
- await writeData(writer, localHeaderArray);
4850
- await fileWriter.readable.pipeTo(writable, { preventClose: true, preventAbort: true, signal });
5273
+ fileEntry.offset = getSegmentOffset(zipWriter, writer);
5274
+ if (usdz) {
5275
+ // the padding depends on the entry offset, only known now that the writer is locked
5276
+ const previousMetadataSize = entryInfo.metadataSize;
5277
+ appendExtraFieldUSDZ(entryInfo, zipWriter.offset - writer.diskOffset);
5278
+ fileEntry.size += entryInfo.metadataSize - previousMetadataSize;
5279
+ }
5280
+ updateLocalHeader(fileEntry, headerInfo.localHeaderView, options);
5281
+ await writeData(writer, headerInfo.localHeaderArray);
5282
+ await flushBufferedData(fileWriter.readable, writer, signal, chunkLength => flushedBufferedSize += chunkLength);
4851
5283
  writer.size += fileWriter.size;
4852
5284
  writingBufferedEntryData = false;
4853
5285
  } else {
4854
- fileEntry.offset = zipWriter.offset - diskOffset;
5286
+ fileEntry.diskNumberStart = diskNumberStart;
5287
+ fileEntry.offset = entryOffset;
4855
5288
  }
4856
5289
  zipWriter.offset += fileEntry.size;
4857
5290
  return fileEntry;
@@ -4865,10 +5298,9 @@
4865
5298
  // ignored
4866
5299
  }
4867
5300
  }
5301
+ zipWriter.offset += writer.size - writerSizeBeforeEntry;
4868
5302
  if (bufferedWrite) {
4869
- zipWriter.offset += fileWriter.size;
4870
- } else {
4871
- zipWriter.offset = fileWriter.size;
5303
+ zipWriter.offset += flushedBufferedSize;
4872
5304
  }
4873
5305
  }
4874
5306
  files.delete(name);
@@ -4899,10 +5331,10 @@
4899
5331
  await lockWriter;
4900
5332
  }
4901
5333
 
4902
- async function skipDiskIfNeeded(writable) {
5334
+ async function skipDiskIfNeeded() {
4903
5335
  if (getLength(headerInfo.localHeaderArray) > writer.availableSize) {
4904
5336
  writer.availableSize = 0;
4905
- await writeData(writable, new Uint8Array());
5337
+ await writeData(writer, new Uint8Array());
4906
5338
  }
4907
5339
  }
4908
5340
  }
@@ -5043,6 +5475,10 @@
5043
5475
  uncompressedSize = result.inputSize;
5044
5476
  signature = result.signature;
5045
5477
  }
5478
+ if ((!zip64CompressedSize && compressedSize >= MAX_32_BITS) ||
5479
+ (!zip64UncompressedSize && uncompressedSize >= MAX_32_BITS)) {
5480
+ throw new Error(ERR_UNSUPPORTED_FORMAT);
5481
+ }
5046
5482
  } catch (error) {
5047
5483
  if (error.outputSize !== UNDEFINED_VALUE) {
5048
5484
  writer.size += error.outputSize;
@@ -5105,35 +5541,28 @@
5105
5541
  uncompressedSize
5106
5542
  } = options;
5107
5543
  let { version, compressionMethod } = options;
5108
- const compressed = !directory && (level > 0 || (level === UNDEFINED_VALUE && compressionMethod !== 0));
5544
+ const compressed = !directory && (compressionMethod === UNDEFINED_VALUE
5545
+ ? (level === UNDEFINED_VALUE || level > 0)
5546
+ : compressionMethod !== COMPRESSION_METHOD_STORE);
5109
5547
  let rawLocalExtraFieldZip64;
5110
5548
  const uncompressedFile = passThrough || !compressed;
5111
- const zip64ExtraFieldComplete = zip64 && (options.bufferedWrite || ((!zip64UncompressedSize && !zip64CompressedSize) || uncompressedFile));
5112
- if (zip64) {
5113
- let rawLocalExtraFieldZip64Length = 4;
5114
- if (zip64UncompressedSize) {
5115
- rawLocalExtraFieldZip64Length += 8;
5116
- }
5117
- if (zip64CompressedSize) {
5118
- rawLocalExtraFieldZip64Length += 8;
5119
- }
5120
- rawLocalExtraFieldZip64 = new Uint8Array(rawLocalExtraFieldZip64Length);
5121
- const rawLocalExtraFieldZip64View = getDataView(rawLocalExtraFieldZip64);
5122
- setUint16(rawLocalExtraFieldZip64View, 0, EXTRAFIELD_TYPE_ZIP64);
5123
- setUint16(rawLocalExtraFieldZip64View, 2, getLength(rawLocalExtraFieldZip64) - 4);
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
+ const zip64ExtraFieldComplete = zip64 && (options.bufferedWrite || !dataDescriptor || ((!zip64UncompressedSize && !zip64CompressedSize) || uncompressedFile));
5552
+ const writeLocalExtraFieldZip64 = zip64ExtraFieldComplete || (zip64 && dataDescriptor && (zip64UncompressedSize || zip64CompressedSize));
5553
+ 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
+ const length = 4 + 16;
5557
+ const extraFieldZip64 = createRecordWriter(length);
5558
+ extraFieldZip64.uint16(EXTRAFIELD_TYPE_ZIP64);
5559
+ extraFieldZip64.uint16(length - 4);
5560
+ rawLocalExtraFieldZip64 = extraFieldZip64.array;
5124
5561
  if (zip64ExtraFieldComplete) {
5125
- const rawLocalExtraFieldZip64View = getDataView(rawLocalExtraFieldZip64);
5126
- let rawLocalExtraFieldZip64Offset = 4;
5127
- if (zip64UncompressedSize) {
5128
- setBigUint64(rawLocalExtraFieldZip64View, rawLocalExtraFieldZip64Offset, BigInt(uncompressedSize));
5129
- rawLocalExtraFieldZip64Offset += 8;
5130
- }
5131
- if (zip64CompressedSize && uncompressedFile) {
5132
- setBigUint64(rawLocalExtraFieldZip64View, rawLocalExtraFieldZip64Offset, BigInt(uncompressedSize));
5133
- rawLocalExtraFieldZip64Offset += 8;
5134
- }
5135
- if (rawLocalExtraFieldZip64Offset == 4) {
5136
- rawLocalExtraFieldZip64 = new Uint8Array();
5562
+ extraFieldZip64.uint64(uncompressedSize);
5563
+ if (uncompressedFile) {
5564
+ const encryptionOverhead = encrypted ? (zipCrypto ? 12 : 16 + encryptionStrength * 4) : 0;
5565
+ extraFieldZip64.uint64(passThrough ? 0 : uncompressedSize + encryptionOverhead);
5137
5566
  }
5138
5567
  }
5139
5568
  } else {
@@ -5141,11 +5570,11 @@
5141
5570
  }
5142
5571
  let rawExtraFieldAES;
5143
5572
  if (encrypted && !zipCrypto) {
5144
- rawExtraFieldAES = new Uint8Array(getLength(EXTRAFIELD_DATA_AES) + 2);
5145
- const extraFieldAESView = getDataView(rawExtraFieldAES);
5146
- setUint16(extraFieldAESView, 0, EXTRAFIELD_TYPE_AES);
5147
- arraySet(rawExtraFieldAES, EXTRAFIELD_DATA_AES, 2);
5148
- setUint8(extraFieldAESView, 8, encryptionStrength);
5573
+ const extraFieldAES = createRecordWriter(getLength(EXTRAFIELD_DATA_AES) + 2);
5574
+ extraFieldAES.uint16(EXTRAFIELD_TYPE_AES);
5575
+ extraFieldAES.bytes(EXTRAFIELD_DATA_AES);
5576
+ rawExtraFieldAES = extraFieldAES.array;
5577
+ rawExtraFieldAES[8] = encryptionStrength;
5149
5578
  } else {
5150
5579
  rawExtraFieldAES = new Uint8Array();
5151
5580
  }
@@ -5153,33 +5582,38 @@
5153
5582
  let rawExtraFieldExtendedTimestamp;
5154
5583
  let extraFieldExtendedTimestampFlag;
5155
5584
  if (extendedTimestamp) {
5156
- rawExtraFieldExtendedTimestamp = new Uint8Array(9 + (lastAccessDate ? 4 : 0) + (creationDate ? 4 : 0));
5157
- const extraFieldExtendedTimestampView = getDataView(rawExtraFieldExtendedTimestamp);
5158
- setUint16(extraFieldExtendedTimestampView, 0, EXTRAFIELD_TYPE_EXTENDED_TIMESTAMP);
5159
- setUint16(extraFieldExtendedTimestampView, 2, getLength(rawExtraFieldExtendedTimestamp) - 4);
5160
- extraFieldExtendedTimestampFlag = 0x1 + (lastAccessDate ? 0x2 : 0) + (creationDate ? 0x4 : 0);
5161
- setUint8(extraFieldExtendedTimestampView, 4, extraFieldExtendedTimestampFlag);
5162
- let offset = 5;
5163
- setUint32(extraFieldExtendedTimestampView, offset, Math.floor(lastModDate.getTime() / 1000));
5164
- offset += 4;
5165
- if (lastAccessDate) {
5166
- setUint32(extraFieldExtendedTimestampView, offset, Math.floor(lastAccessDate.getTime() / 1000));
5167
- offset += 4;
5168
- }
5169
- if (creationDate) {
5170
- setUint32(extraFieldExtendedTimestampView, offset, Math.floor(creationDate.getTime() / 1000));
5585
+ const lastModTimeUnix = getTimeUnix(lastModDate);
5586
+ if (inUnixTimeRange(lastModTimeUnix)) {
5587
+ const extraFieldTimestampLength = 9 + (lastAccessDate ? 4 : 0) + (creationDate ? 4 : 0);
5588
+ const extraFieldTimestamp = createRecordWriter(extraFieldTimestampLength);
5589
+ extraFieldExtendedTimestampFlag = 0x1 + (lastAccessDate ? 0x2 : 0) + (creationDate ? 0x4 : 0);
5590
+ extraFieldTimestamp.uint16(EXTRAFIELD_TYPE_EXTENDED_TIMESTAMP);
5591
+ extraFieldTimestamp.uint16(extraFieldTimestampLength - 4);
5592
+ extraFieldTimestamp.uint8(extraFieldExtendedTimestampFlag);
5593
+ extraFieldTimestamp.uint32(lastModTimeUnix);
5594
+ if (lastAccessDate) {
5595
+ extraFieldTimestamp.uint32(clampUnixTime(getTimeUnix(lastAccessDate)));
5596
+ }
5597
+ if (creationDate) {
5598
+ extraFieldTimestamp.uint32(clampUnixTime(getTimeUnix(creationDate)));
5599
+ }
5600
+ rawExtraFieldExtendedTimestamp = extraFieldTimestamp.array;
5601
+ } else {
5602
+ // the date cannot be stored as a 32-bit time_t; the NTFS field replaces it
5603
+ rawExtraFieldExtendedTimestamp = new Uint8Array();
5171
5604
  }
5172
5605
  try {
5173
- rawExtraFieldNTFS = new Uint8Array(36);
5174
- const extraFieldNTFSView = getDataView(rawExtraFieldNTFS);
5175
5606
  const lastModTimeNTFS = getTimeNTFS(lastModDate);
5176
- setUint16(extraFieldNTFSView, 0, EXTRAFIELD_TYPE_NTFS);
5177
- setUint16(extraFieldNTFSView, 2, 32);
5178
- setUint16(extraFieldNTFSView, 8, EXTRAFIELD_TYPE_NTFS_TAG1);
5179
- setUint16(extraFieldNTFSView, 10, 24);
5180
- setBigUint64(extraFieldNTFSView, 12, lastModTimeNTFS);
5181
- setBigUint64(extraFieldNTFSView, 20, getTimeNTFS(lastAccessDate) || lastModTimeNTFS);
5182
- setBigUint64(extraFieldNTFSView, 28, getTimeNTFS(creationDate) || lastModTimeNTFS);
5607
+ const extraFieldNTFS = createRecordWriter(36);
5608
+ extraFieldNTFS.uint16(EXTRAFIELD_TYPE_NTFS);
5609
+ extraFieldNTFS.uint16(32);
5610
+ extraFieldNTFS.skip(4);
5611
+ extraFieldNTFS.uint16(EXTRAFIELD_TYPE_NTFS_TAG1);
5612
+ extraFieldNTFS.uint16(24);
5613
+ extraFieldNTFS.uint64(lastModTimeNTFS);
5614
+ extraFieldNTFS.uint64(getTimeNTFS(lastAccessDate) || lastModTimeNTFS);
5615
+ extraFieldNTFS.uint64(getTimeNTFS(creationDate) || lastModTimeNTFS);
5616
+ rawExtraFieldNTFS = extraFieldNTFS.array;
5183
5617
  } catch {
5184
5618
  rawExtraFieldNTFS = new Uint8Array();
5185
5619
  }
@@ -5188,41 +5622,27 @@
5188
5622
  }
5189
5623
  let rawExtraFieldUnix;
5190
5624
  try {
5191
- const { uid, gid, unixMode, setuid, setgid, sticky, unixExtraFieldType } = options;
5192
- if (unixExtraFieldType && (uid !== UNDEFINED_VALUE || gid !== UNDEFINED_VALUE || unixMode !== UNDEFINED_VALUE)) {
5625
+ const { uid, gid, unixExtraFieldType } = options;
5626
+ if (unixExtraFieldType == INFOZIP_EXTRA_FIELD_TYPE && (uid !== UNDEFINED_VALUE || gid !== UNDEFINED_VALUE)) {
5193
5627
  const uidBytes = packUnixId(uid);
5194
5628
  const gidBytes = packUnixId(gid);
5195
- let modeArray = new Uint8Array();
5196
- if (unixExtraFieldType == UNIX_EXTRA_FIELD_TYPE && unixMode !== UNDEFINED_VALUE) {
5197
- let modeToWrite = unixMode & MAX_16_BITS;
5198
- if (setuid) {
5199
- modeToWrite |= FILE_ATTR_UNIX_SETUID_MASK;
5200
- }
5201
- if (setgid) {
5202
- modeToWrite |= FILE_ATTR_UNIX_SETGID_MASK;
5203
- }
5204
- if (sticky) {
5205
- modeToWrite |= FILE_ATTR_UNIX_STICKY_MASK;
5206
- }
5207
- modeArray = new Uint8Array(2);
5208
- const modeDataView = new DataView(modeArray.buffer);
5209
- modeDataView.setUint16(0, modeToWrite, true);
5210
- }
5211
- const payloadLength = 3 + uidBytes.length + gidBytes.length + modeArray.length;
5212
- rawExtraFieldUnix = new Uint8Array(4 + payloadLength);
5213
- const rawExtraFieldUnixView = getDataView(rawExtraFieldUnix);
5214
- setUint16(rawExtraFieldUnixView, 0, unixExtraFieldType == INFOZIP_EXTRA_FIELD_TYPE ? EXTRAFIELD_TYPE_INFOZIP : EXTRAFIELD_TYPE_UNIX);
5215
- setUint16(rawExtraFieldUnixView, 2, payloadLength);
5216
- setUint8(rawExtraFieldUnixView, 4, 1);
5217
- setUint8(rawExtraFieldUnixView, 5, uidBytes.length);
5218
- let offset = 6;
5219
- arraySet(rawExtraFieldUnix, uidBytes, offset);
5220
- offset += uidBytes.length;
5221
- setUint8(rawExtraFieldUnixView, offset, gidBytes.length);
5222
- offset++;
5223
- arraySet(rawExtraFieldUnix, gidBytes, offset);
5224
- offset += gidBytes.length;
5225
- arraySet(rawExtraFieldUnix, modeArray, offset);
5629
+ const payloadLength = 3 + uidBytes.length + gidBytes.length;
5630
+ const extraFieldUnix = createRecordWriter(4 + payloadLength);
5631
+ extraFieldUnix.uint16(EXTRAFIELD_TYPE_INFOZIP);
5632
+ extraFieldUnix.uint16(payloadLength);
5633
+ extraFieldUnix.uint8(1);
5634
+ extraFieldUnix.uint8(uidBytes.length);
5635
+ extraFieldUnix.bytes(uidBytes);
5636
+ extraFieldUnix.uint8(gidBytes.length);
5637
+ extraFieldUnix.bytes(gidBytes);
5638
+ rawExtraFieldUnix = extraFieldUnix.array;
5639
+ } else if (unixExtraFieldType == UNIX_EXTRA_FIELD_TYPE && (uid !== UNDEFINED_VALUE || gid !== UNDEFINED_VALUE)) {
5640
+ const extraFieldUnix = createRecordWriter(8);
5641
+ extraFieldUnix.uint16(EXTRAFIELD_TYPE_UNIX);
5642
+ extraFieldUnix.uint16(4);
5643
+ extraFieldUnix.uint16((uid === UNDEFINED_VALUE ? 0 : uid) & MAX_16_BITS);
5644
+ extraFieldUnix.uint16((gid === UNDEFINED_VALUE ? 0 : gid) & MAX_16_BITS);
5645
+ rawExtraFieldUnix = extraFieldUnix.array;
5226
5646
  } else {
5227
5647
  rawExtraFieldUnix = new Uint8Array();
5228
5648
  }
@@ -5240,8 +5660,11 @@
5240
5660
  rawExtraFieldAES[9] = compressionMethod;
5241
5661
  compressionMethod = COMPRESSION_METHOD_AES;
5242
5662
  }
5243
- const localExtraFieldZip64Length = zip64ExtraFieldComplete ? getLength(rawLocalExtraFieldZip64) : 0;
5663
+ const localExtraFieldZip64Length = writeLocalExtraFieldZip64 ? getLength(rawLocalExtraFieldZip64) : 0;
5244
5664
  const extraFieldLength = localExtraFieldZip64Length + getLength(rawExtraFieldAES, rawExtraFieldExtendedTimestamp, rawExtraFieldNTFS, rawExtraFieldUnix, rawExtraField);
5665
+ if (extraFieldLength > MAX_16_BITS) {
5666
+ throw new Error(ERR_INVALID_EXTRAFIELD_DATA);
5667
+ }
5245
5668
  const {
5246
5669
  headerArray,
5247
5670
  headerView,
@@ -5257,29 +5680,27 @@
5257
5680
  zip64UncompressedSize,
5258
5681
  extraFieldLength
5259
5682
  });
5260
- let localHeaderOffset = HEADER_SIZE;
5261
- const localHeaderArray = new Uint8Array(localHeaderOffset + getLength(rawFilename) + extraFieldLength);
5683
+ const localHeader = createRecordWriter(HEADER_SIZE + getLength(rawFilename) + extraFieldLength);
5684
+ const localHeaderArray = localHeader.array;
5262
5685
  const localHeaderView = getDataView(localHeaderArray);
5263
- setUint32(localHeaderView, 0, LOCAL_FILE_HEADER_SIGNATURE);
5264
- arraySet(localHeaderArray, headerArray, 4);
5265
- arraySet(localHeaderArray, rawFilename, localHeaderOffset);
5266
- localHeaderOffset += getLength(rawFilename);
5267
- if (zip64ExtraFieldComplete) {
5268
- arraySet(localHeaderArray, rawLocalExtraFieldZip64, localHeaderOffset);
5269
- }
5270
- localHeaderOffset += localExtraFieldZip64Length;
5271
- arraySet(localHeaderArray, rawExtraFieldAES, localHeaderOffset);
5272
- localHeaderOffset += getLength(rawExtraFieldAES);
5273
- arraySet(localHeaderArray, rawExtraFieldExtendedTimestamp, localHeaderOffset);
5274
- localHeaderOffset += getLength(rawExtraFieldExtendedTimestamp);
5275
- arraySet(localHeaderArray, rawExtraFieldNTFS, localHeaderOffset);
5276
- localHeaderOffset += getLength(rawExtraFieldNTFS);
5277
- arraySet(localHeaderArray, rawExtraFieldUnix, localHeaderOffset);
5278
- localHeaderOffset += getLength(rawExtraFieldUnix);
5279
- arraySet(localHeaderArray, rawExtraField, localHeaderOffset);
5686
+ localHeader.uint32(LOCAL_FILE_HEADER_SIGNATURE);
5687
+ localHeader.bytes(headerArray);
5688
+ localHeader.bytes(rawFilename);
5689
+ if (writeLocalExtraFieldZip64) {
5690
+ localHeader.bytes(rawLocalExtraFieldZip64);
5691
+ }
5692
+ localHeader.bytes(rawExtraFieldAES);
5693
+ localHeader.bytes(rawExtraFieldExtendedTimestamp);
5694
+ localHeader.bytes(rawExtraFieldNTFS);
5695
+ localHeader.bytes(rawExtraFieldUnix);
5696
+ localHeader.bytes(rawExtraField);
5280
5697
  if (dataDescriptor) {
5281
- setUint32(localHeaderView, HEADER_OFFSET_COMPRESSED_SIZE + 4, 0);
5282
- setUint32(localHeaderView, HEADER_OFFSET_UNCOMPRESSED_SIZE + 4, 0);
5698
+ if (!zip64CompressedSize) {
5699
+ setUint32(localHeaderView, HEADER_OFFSET_COMPRESSED_SIZE + LOCAL_HEADER_COMMON_OFFSET, 0);
5700
+ }
5701
+ if (!zip64UncompressedSize) {
5702
+ setUint32(localHeaderView, HEADER_OFFSET_UNCOMPRESSED_SIZE + LOCAL_HEADER_COMMON_OFFSET, 0);
5703
+ }
5283
5704
  }
5284
5705
  return {
5285
5706
  localHeaderArray,
@@ -5306,7 +5727,7 @@
5306
5727
  function appendExtraFieldUSDZ(entryInfo, zipWriterOffset) {
5307
5728
  const { headerInfo } = entryInfo;
5308
5729
  let { localHeaderArray, extraFieldLength } = headerInfo;
5309
- let localHeaderArrayView = getDataView(localHeaderArray);
5730
+ let localHeaderArrayView;
5310
5731
  let extraBytesLength = 64 - ((zipWriterOffset + getLength(localHeaderArray)) % 64);
5311
5732
  if (extraBytesLength < 4) {
5312
5733
  extraBytesLength += 64;
@@ -5314,13 +5735,14 @@
5314
5735
  const rawExtraFieldUSDZ = new Uint8Array(extraBytesLength);
5315
5736
  const extraFieldUSDZView = getDataView(rawExtraFieldUSDZ);
5316
5737
  setUint16(extraFieldUSDZView, 0, EXTRAFIELD_TYPE_USDZ);
5317
- setUint16(extraFieldUSDZView, 2, extraBytesLength - 2);
5738
+ setUint16(extraFieldUSDZView, 2, extraBytesLength - 4);
5318
5739
  const previousLocalHeaderArray = localHeaderArray;
5319
5740
  headerInfo.localHeaderArray = localHeaderArray = new Uint8Array(getLength(previousLocalHeaderArray) + extraBytesLength);
5320
5741
  arraySet(localHeaderArray, previousLocalHeaderArray);
5321
5742
  arraySet(localHeaderArray, rawExtraFieldUSDZ, getLength(previousLocalHeaderArray));
5322
5743
  localHeaderArrayView = getDataView(localHeaderArray);
5323
5744
  setUint16(localHeaderArrayView, 28, extraFieldLength + extraBytesLength);
5745
+ headerInfo.localHeaderView = localHeaderArrayView;
5324
5746
  entryInfo.metadataSize += extraBytesLength;
5325
5747
  }
5326
5748
 
@@ -5443,37 +5865,32 @@
5443
5865
  }, localHeaderView, { dataDescriptor }) {
5444
5866
  if (!dataDescriptor) {
5445
5867
  if (!encrypted) {
5446
- setUint32(localHeaderView, HEADER_OFFSET_SIGNATURE + 4, signature);
5868
+ setUint32(localHeaderView, HEADER_OFFSET_SIGNATURE + LOCAL_HEADER_COMMON_OFFSET, signature);
5447
5869
  }
5448
5870
  if (!zip64CompressedSize) {
5449
- setUint32(localHeaderView, HEADER_OFFSET_COMPRESSED_SIZE + 4, compressedSize);
5871
+ setUint32(localHeaderView, HEADER_OFFSET_COMPRESSED_SIZE + LOCAL_HEADER_COMMON_OFFSET, compressedSize);
5450
5872
  }
5451
5873
  if (!zip64UncompressedSize) {
5452
- setUint32(localHeaderView, HEADER_OFFSET_UNCOMPRESSED_SIZE + 4, uncompressedSize);
5874
+ setUint32(localHeaderView, HEADER_OFFSET_UNCOMPRESSED_SIZE + LOCAL_HEADER_COMMON_OFFSET, uncompressedSize);
5453
5875
  }
5454
5876
  }
5455
5877
  if (zip64 && localExtraFieldZip64Length) {
5456
- let localHeaderOffset = HEADER_SIZE + getLength(rawFilename) + 4;
5457
- if (zip64UncompressedSize) {
5458
- setBigUint64(localHeaderView, localHeaderOffset, BigInt(uncompressedSize));
5459
- localHeaderOffset += 8;
5460
- }
5461
- if (zip64CompressedSize) {
5462
- setBigUint64(localHeaderView, localHeaderOffset, BigInt(compressedSize));
5463
- localHeaderOffset += 8;
5464
- }
5878
+ // the local zip64 extra field always contains both size slots
5879
+ const localHeaderOffset = HEADER_SIZE + getLength(rawFilename) + 4;
5880
+ setBigUint64(localHeaderView, localHeaderOffset, BigInt(uncompressedSize));
5881
+ setBigUint64(localHeaderView, localHeaderOffset + 8, BigInt(compressedSize));
5465
5882
  }
5466
5883
  }
5467
5884
 
5468
5885
 
5469
5886
  async function closeFile(zipWriter, comment, options) {
5470
- const { files, writer } = zipWriter;
5471
- const { diskOffset } = writer;
5472
- let { diskNumber } = writer;
5473
- let offset = 0;
5887
+ const directoryDataLength = createDirectoryRecords(zipWriter.files);
5888
+ const { cdStartDiskNumber, cdStartDiskOffset } = await writeDirectoryRecords(zipWriter, directoryDataLength, options);
5889
+ await writeEndOfDirectoryRecord(zipWriter, comment, options, { cdStartDiskNumber, cdStartDiskOffset, directoryDataLength });
5890
+ }
5891
+
5892
+ function createDirectoryRecords(files) {
5474
5893
  let directoryDataLength = 0;
5475
- let directoryOffset = zipWriter.offset - diskOffset;
5476
- let filesLength = files.size;
5477
5894
  for (const [, fileEntry] of files) {
5478
5895
  const {
5479
5896
  rawFilename,
@@ -5490,24 +5907,27 @@
5490
5907
  uncompressedSize,
5491
5908
  compressedSize
5492
5909
  } = fileEntry;
5493
- const zip64Offset = fileEntry.offset > MAX_32_BITS;
5494
- const zip64DiskNumberStart = fileEntry.diskNumberStart > MAX_16_BITS;
5910
+ const zip64Offset = fileEntry.offset >= MAX_32_BITS;
5911
+ const zip64DiskNumberStart = fileEntry.diskNumberStart >= MAX_16_BITS;
5495
5912
  let rawExtraFieldZip64;
5496
5913
  if (zip64Offset || zip64DiskNumberStart || zip64UncompressedSize || zip64CompressedSize) {
5497
- let length = 4;
5498
- if (zip64UncompressedSize) length += 8;
5499
- if (zip64CompressedSize) length += 8;
5500
- if (zip64Offset) length += 8;
5501
- if (zip64DiskNumberStart) length += 4;
5502
- rawExtraFieldZip64 = new Uint8Array(length);
5503
- const zip64View = getDataView(rawExtraFieldZip64);
5504
- setUint16(zip64View, 0, EXTRAFIELD_TYPE_ZIP64);
5505
- setUint16(zip64View, 2, length - 4);
5506
- let zip64FieldOffset = 4;
5507
- if (zip64UncompressedSize) { setBigUint64(zip64View, zip64FieldOffset, BigInt(uncompressedSize)); zip64FieldOffset += 8; }
5508
- if (zip64CompressedSize) { setBigUint64(zip64View, zip64FieldOffset, BigInt(compressedSize)); zip64FieldOffset += 8; }
5509
- if (zip64Offset) { setBigUint64(zip64View, zip64FieldOffset, BigInt(fileEntry.offset)); zip64FieldOffset += 8; }
5510
- if (zip64DiskNumberStart) { setUint32(zip64View, zip64FieldOffset, fileEntry.diskNumberStart); }
5914
+ const length = 4 + (zip64UncompressedSize ? 8 : 0) + (zip64CompressedSize ? 8 : 0) + (zip64Offset ? 8 : 0) + (zip64DiskNumberStart ? 4 : 0);
5915
+ const extraFieldZip64 = createRecordWriter(length);
5916
+ extraFieldZip64.uint16(EXTRAFIELD_TYPE_ZIP64);
5917
+ extraFieldZip64.uint16(length - 4);
5918
+ if (zip64UncompressedSize) {
5919
+ extraFieldZip64.uint64(uncompressedSize);
5920
+ }
5921
+ if (zip64CompressedSize) {
5922
+ extraFieldZip64.uint64(compressedSize);
5923
+ }
5924
+ if (zip64Offset) {
5925
+ extraFieldZip64.uint64(fileEntry.offset);
5926
+ }
5927
+ if (zip64DiskNumberStart) {
5928
+ extraFieldZip64.uint32(fileEntry.diskNumberStart);
5929
+ }
5930
+ rawExtraFieldZip64 = extraFieldZip64.array;
5511
5931
  } else {
5512
5932
  rawExtraFieldZip64 = new Uint8Array();
5513
5933
  }
@@ -5515,32 +5935,41 @@
5515
5935
  fileEntry.zip64Offset = zip64Offset;
5516
5936
  fileEntry.zip64DiskNumberStart = zip64DiskNumberStart;
5517
5937
  let rawExtraFieldTimestamp;
5518
- if (extendedTimestamp) {
5519
- rawExtraFieldTimestamp = new Uint8Array(9);
5520
- const extraFieldExtendedTimestampView = getDataView(rawExtraFieldTimestamp);
5521
- setUint16(extraFieldExtendedTimestampView, 0, EXTRAFIELD_TYPE_EXTENDED_TIMESTAMP);
5522
- setUint16(extraFieldExtendedTimestampView, 2, 5);
5523
- setUint8(extraFieldExtendedTimestampView, 4, extraFieldExtendedTimestampFlag);
5524
- setUint32(extraFieldExtendedTimestampView, 5, Math.floor(lastModDate.getTime() / 1000));
5938
+ const lastModTimeUnix = getTimeUnix(lastModDate);
5939
+ if (extendedTimestamp && inUnixTimeRange(lastModTimeUnix)) {
5940
+ const extraFieldTimestamp = createRecordWriter(9);
5941
+ extraFieldTimestamp.uint16(EXTRAFIELD_TYPE_EXTENDED_TIMESTAMP);
5942
+ extraFieldTimestamp.uint16(5);
5943
+ extraFieldTimestamp.uint8(extraFieldExtendedTimestampFlag);
5944
+ extraFieldTimestamp.uint32(lastModTimeUnix);
5945
+ rawExtraFieldTimestamp = extraFieldTimestamp.array;
5525
5946
  } else {
5526
5947
  rawExtraFieldTimestamp = new Uint8Array();
5527
5948
  }
5528
5949
  fileEntry.rawExtraFieldExtendedTimestamp = rawExtraFieldTimestamp;
5529
- directoryDataLength += 46 +
5530
- getLength(
5531
- rawFilename,
5532
- rawComment,
5533
- rawExtraFieldZip64,
5534
- rawExtraFieldAES,
5535
- rawExtraFieldNTFS,
5536
- rawExtraFieldUnix,
5537
- rawExtraFieldTimestamp,
5538
- rawExtraField);
5950
+ const extraFieldLength = getLength(
5951
+ rawExtraFieldZip64,
5952
+ rawExtraFieldAES,
5953
+ rawExtraFieldNTFS,
5954
+ rawExtraFieldUnix,
5955
+ rawExtraFieldTimestamp,
5956
+ rawExtraField);
5957
+ if (extraFieldLength > MAX_16_BITS) {
5958
+ throw new Error(ERR_INVALID_EXTRAFIELD_DATA);
5959
+ }
5960
+ directoryDataLength += CENTRAL_FILE_HEADER_LENGTH + getLength(rawFilename, rawComment) + extraFieldLength;
5539
5961
  }
5962
+ return directoryDataLength;
5963
+ }
5964
+
5965
+ async function writeDirectoryRecords(zipWriter, directoryDataLength, options) {
5966
+ const { files, writer } = zipWriter;
5540
5967
  const directoryArray = new Uint8Array(directoryDataLength);
5541
- const directoryView = getDataView(directoryArray);
5542
5968
  await initStream(writer);
5969
+ let offset = 0;
5543
5970
  let directoryDiskOffset = 0;
5971
+ let cdStartDiskNumber = writer.diskNumber;
5972
+ let cdStartDiskOffset = writer.diskOffset;
5544
5973
  for (const [indexFileEntry, fileEntry] of Array.from(files.values()).entries()) {
5545
5974
  const {
5546
5975
  offset: fileEntryOffset,
@@ -5566,52 +5995,46 @@
5566
5995
  compressedSize
5567
5996
  } = fileEntry;
5568
5997
  const extraFieldLength = getLength(rawExtraFieldZip64, rawExtraFieldAES, rawExtraFieldExtendedTimestamp, rawExtraFieldNTFS, rawExtraFieldUnix, rawExtraField);
5569
- setUint32(directoryView, offset, CENTRAL_FILE_HEADER_SIGNATURE);
5570
- setUint16(directoryView, offset + 4, versionMadeBy);
5998
+ const directoryRecordLength = CENTRAL_FILE_HEADER_LENGTH + getLength(rawFilename, rawComment) + extraFieldLength;
5999
+ if (offset + directoryRecordLength - directoryDiskOffset > writer.availableSize) {
6000
+ await writeData(writer, directoryArray.slice(directoryDiskOffset, offset));
6001
+ directoryDiskOffset = offset;
6002
+ writer.availableSize = 0;
6003
+ await writeData(writer, new Uint8Array());
6004
+ }
6005
+ if (indexFileEntry == 0) {
6006
+ cdStartDiskNumber = writer.diskNumber;
6007
+ cdStartDiskOffset = writer.diskOffset;
6008
+ }
5571
6009
  if (!zip64UncompressedSize) {
5572
6010
  setUint32(headerView, HEADER_OFFSET_UNCOMPRESSED_SIZE, uncompressedSize);
5573
6011
  }
5574
6012
  if (!zip64CompressedSize) {
5575
6013
  setUint32(headerView, HEADER_OFFSET_COMPRESSED_SIZE, compressedSize);
5576
6014
  }
5577
- arraySet(directoryArray, headerArray, offset + 6);
5578
- let directoryOffset = offset + HEADER_SIZE;
5579
- setUint16(directoryView, directoryOffset, extraFieldLength);
5580
- directoryOffset += 2;
5581
- setUint16(directoryView, directoryOffset, getLength(rawComment));
5582
- directoryOffset += 2;
5583
- setUint16(directoryView, directoryOffset, zip64DiskNumberStart ? MAX_16_BITS : diskNumberStart);
5584
- directoryOffset += 2;
5585
- setUint16(directoryView, directoryOffset, internalFileAttributes);
5586
- directoryOffset += 2;
5587
- if (externalFileAttributes) {
5588
- setUint32(directoryView, directoryOffset, externalFileAttributes);
5589
- }
5590
- directoryOffset += 4;
5591
- setUint32(directoryView, directoryOffset, zip64Offset ? MAX_32_BITS : fileEntryOffset);
5592
- directoryOffset += 4;
5593
- arraySet(directoryArray, rawFilename, directoryOffset);
5594
- directoryOffset += getLength(rawFilename);
5595
- arraySet(directoryArray, rawExtraFieldZip64, directoryOffset);
5596
- directoryOffset += getLength(rawExtraFieldZip64);
5597
- arraySet(directoryArray, rawExtraFieldAES, directoryOffset);
5598
- directoryOffset += getLength(rawExtraFieldAES);
5599
- arraySet(directoryArray, rawExtraFieldExtendedTimestamp, directoryOffset);
5600
- directoryOffset += getLength(rawExtraFieldExtendedTimestamp);
5601
- arraySet(directoryArray, rawExtraFieldNTFS, directoryOffset);
5602
- directoryOffset += getLength(rawExtraFieldNTFS);
5603
- arraySet(directoryArray, rawExtraFieldUnix, directoryOffset);
5604
- directoryOffset += getLength(rawExtraFieldUnix);
5605
- arraySet(directoryArray, rawExtraField, directoryOffset);
5606
- directoryOffset += getLength(rawExtraField);
5607
- arraySet(directoryArray, rawComment, directoryOffset);
5608
- directoryOffset += getLength(rawComment);
5609
- if (offset - directoryDiskOffset > writer.availableSize) {
5610
- writer.availableSize = 0;
5611
- await writeData(writer, directoryArray.slice(directoryDiskOffset, offset));
5612
- directoryDiskOffset = offset;
5613
- }
5614
- offset = directoryOffset;
6015
+ if ((zip64Offset || zip64DiskNumberStart) && fileEntry.version < VERSION_ZIP64) {
6016
+ setUint16(headerView, HEADER_OFFSET_VERSION, VERSION_ZIP64);
6017
+ }
6018
+ const directoryRecord = createRecordWriter(directoryRecordLength);
6019
+ directoryRecord.uint32(CENTRAL_FILE_HEADER_SIGNATURE);
6020
+ directoryRecord.uint16(versionMadeBy);
6021
+ directoryRecord.bytes(headerArray.subarray(0, HEADER_SIZE - 4 - 2));
6022
+ directoryRecord.uint16(extraFieldLength);
6023
+ directoryRecord.uint16(getLength(rawComment));
6024
+ directoryRecord.uint16(zip64DiskNumberStart ? MAX_16_BITS : diskNumberStart);
6025
+ directoryRecord.uint16(internalFileAttributes);
6026
+ directoryRecord.uint32(externalFileAttributes);
6027
+ directoryRecord.uint32(zip64Offset ? MAX_32_BITS : fileEntryOffset);
6028
+ directoryRecord.bytes(rawFilename);
6029
+ directoryRecord.bytes(rawExtraFieldZip64);
6030
+ directoryRecord.bytes(rawExtraFieldAES);
6031
+ directoryRecord.bytes(rawExtraFieldExtendedTimestamp);
6032
+ directoryRecord.bytes(rawExtraFieldNTFS);
6033
+ directoryRecord.bytes(rawExtraFieldUnix);
6034
+ directoryRecord.bytes(rawExtraField);
6035
+ directoryRecord.bytes(rawComment);
6036
+ arraySet(directoryArray, directoryRecord.array, offset);
6037
+ offset += directoryRecordLength;
5615
6038
  if (options.onprogress) {
5616
6039
  try {
5617
6040
  await options.onprogress(indexFileEntry + 1, files.size, new Entry(fileEntry));
@@ -5621,36 +6044,53 @@
5621
6044
  }
5622
6045
  }
5623
6046
  await writeData(writer, directoryDiskOffset ? directoryArray.slice(directoryDiskOffset) : directoryArray);
6047
+ return { cdStartDiskNumber, cdStartDiskOffset };
6048
+ }
6049
+
6050
+ async function writeEndOfDirectoryRecord(zipWriter, comment, options, cdInfo) {
6051
+ const { writer } = zipWriter;
6052
+ const { cdStartDiskNumber, cdStartDiskOffset } = cdInfo;
6053
+ let { directoryDataLength } = cdInfo;
6054
+ let filesLength = zipWriter.files.size;
6055
+ let diskNumber = cdStartDiskNumber;
6056
+ let directoryOffset = zipWriter.offset - cdStartDiskOffset - (cdStartDiskNumber ? zipWriter.initialOffset : 0);
5624
6057
  let lastDiskNumber = writer.diskNumber;
5625
- const { availableSize } = writer;
5626
- if (availableSize < END_OF_CENTRAL_DIR_LENGTH) {
6058
+ if (writer.availableSize < END_OF_CENTRAL_DIR_LENGTH) {
5627
6059
  lastDiskNumber++;
5628
6060
  }
5629
6061
  let zip64 = getOptionValue(zipWriter, options, PROPERTY_NAME_ZIP64);
5630
- if (directoryOffset > MAX_32_BITS || directoryDataLength > MAX_32_BITS || filesLength > MAX_16_BITS || lastDiskNumber > MAX_16_BITS) {
6062
+ if (directoryOffset >= MAX_32_BITS || directoryDataLength >= MAX_32_BITS || filesLength >= MAX_16_BITS || lastDiskNumber >= MAX_16_BITS) {
5631
6063
  if (zip64 === false) {
5632
6064
  throw new Error(ERR_UNSUPPORTED_FORMAT);
5633
6065
  } else {
5634
6066
  zip64 = true;
5635
6067
  }
5636
6068
  }
5637
- const endOfdirectoryArray = new Uint8Array(zip64 ? ZIP64_END_OF_CENTRAL_DIR_TOTAL_LENGTH : END_OF_CENTRAL_DIR_LENGTH);
5638
- const endOfdirectoryView = getDataView(endOfdirectoryArray);
5639
- offset = 0;
6069
+ const commentLength = getLength(comment);
6070
+ if (commentLength > MAX_16_BITS) {
6071
+ throw new Error(ERR_INVALID_COMMENT);
6072
+ }
6073
+ const endOfdirectoryRecord = createRecordWriter(zip64 ? ZIP64_END_OF_CENTRAL_DIR_TOTAL_LENGTH : END_OF_CENTRAL_DIR_LENGTH);
6074
+ if (getLength(endOfdirectoryRecord.array) + commentLength > writer.availableSize) {
6075
+ writer.availableSize = 0;
6076
+ await writeData(writer, new Uint8Array());
6077
+ }
6078
+ lastDiskNumber = writer.diskNumber;
5640
6079
  if (zip64) {
5641
- setUint32(endOfdirectoryView, 0, ZIP64_END_OF_CENTRAL_DIR_SIGNATURE);
5642
- setBigUint64(endOfdirectoryView, 4, BigInt(44));
5643
- setUint16(endOfdirectoryView, 12, 45);
5644
- setUint16(endOfdirectoryView, 14, 45);
5645
- setUint32(endOfdirectoryView, 16, lastDiskNumber);
5646
- setUint32(endOfdirectoryView, 20, diskNumber);
5647
- setBigUint64(endOfdirectoryView, 24, BigInt(filesLength));
5648
- setBigUint64(endOfdirectoryView, 32, BigInt(filesLength));
5649
- setBigUint64(endOfdirectoryView, 40, BigInt(directoryDataLength));
5650
- setBigUint64(endOfdirectoryView, 48, BigInt(directoryOffset));
5651
- setUint32(endOfdirectoryView, 56, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE);
5652
- setBigUint64(endOfdirectoryView, 64, BigInt(directoryOffset) + BigInt(directoryDataLength));
5653
- setUint32(endOfdirectoryView, 72, lastDiskNumber + 1);
6080
+ endOfdirectoryRecord.uint32(ZIP64_END_OF_CENTRAL_DIR_SIGNATURE);
6081
+ endOfdirectoryRecord.uint64(44);
6082
+ endOfdirectoryRecord.uint16(45);
6083
+ endOfdirectoryRecord.uint16(45);
6084
+ endOfdirectoryRecord.uint32(lastDiskNumber);
6085
+ endOfdirectoryRecord.uint32(diskNumber);
6086
+ endOfdirectoryRecord.uint64(filesLength);
6087
+ endOfdirectoryRecord.uint64(filesLength);
6088
+ endOfdirectoryRecord.uint64(directoryDataLength);
6089
+ endOfdirectoryRecord.uint64(directoryOffset);
6090
+ endOfdirectoryRecord.uint32(ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE);
6091
+ endOfdirectoryRecord.uint32(lastDiskNumber);
6092
+ endOfdirectoryRecord.uint64(BigInt(zipWriter.offset) + BigInt(directoryDataLength) - BigInt(writer.diskOffset) - BigInt(writer.diskNumber ? zipWriter.initialOffset : 0));
6093
+ endOfdirectoryRecord.uint32(lastDiskNumber + 1);
5654
6094
  const supportZip64SplitFile = getOptionValue(zipWriter, options, OPTION_SUPPORT_ZIP64_SPLIT_FILE, true);
5655
6095
  if (supportZip64SplitFile) {
5656
6096
  lastDiskNumber = MAX_16_BITS;
@@ -5659,29 +6099,40 @@
5659
6099
  filesLength = MAX_16_BITS;
5660
6100
  directoryOffset = MAX_32_BITS;
5661
6101
  directoryDataLength = MAX_32_BITS;
5662
- offset += ZIP64_END_OF_CENTRAL_DIR_LENGTH + ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH;
5663
- }
5664
- setUint32(endOfdirectoryView, offset, END_OF_CENTRAL_DIR_SIGNATURE);
5665
- setUint16(endOfdirectoryView, offset + 4, lastDiskNumber);
5666
- setUint16(endOfdirectoryView, offset + 6, diskNumber);
5667
- setUint16(endOfdirectoryView, offset + 8, filesLength);
5668
- setUint16(endOfdirectoryView, offset + 10, filesLength);
5669
- setUint32(endOfdirectoryView, offset + 12, directoryDataLength);
5670
- setUint32(endOfdirectoryView, offset + 16, directoryOffset);
5671
- const commentLength = getLength(comment);
5672
- if (commentLength) {
5673
- if (commentLength <= MAX_16_BITS) {
5674
- setUint16(endOfdirectoryView, offset + 20, commentLength);
5675
- } else {
5676
- throw new Error(ERR_INVALID_COMMENT);
5677
- }
5678
6102
  }
5679
- await writeData(writer, endOfdirectoryArray);
6103
+ endOfdirectoryRecord.uint32(END_OF_CENTRAL_DIR_SIGNATURE);
6104
+ endOfdirectoryRecord.uint16(lastDiskNumber);
6105
+ endOfdirectoryRecord.uint16(diskNumber);
6106
+ endOfdirectoryRecord.uint16(filesLength);
6107
+ endOfdirectoryRecord.uint16(filesLength);
6108
+ endOfdirectoryRecord.uint32(directoryDataLength);
6109
+ endOfdirectoryRecord.uint32(directoryOffset);
6110
+ endOfdirectoryRecord.uint16(commentLength);
6111
+ await writeData(writer, endOfdirectoryRecord.array);
5680
6112
  if (commentLength) {
5681
6113
  await writeData(writer, comment);
5682
6114
  }
5683
6115
  }
5684
6116
 
6117
+ function createRecordWriter(length) {
6118
+ const array = new Uint8Array(length);
6119
+ const view = getDataView(array);
6120
+ let offset = 0;
6121
+ return {
6122
+ array,
6123
+ uint8: value => { setUint8(view, offset, value); offset += 1; },
6124
+ uint16: value => { setUint16(view, offset, value); offset += 2; },
6125
+ uint32: value => { setUint32(view, offset, value); offset += 4; },
6126
+ uint64: value => { setBigUint64(view, offset, BigInt(value)); offset += 8; },
6127
+ bytes: value => { arraySet(array, value, offset); offset += getLength(value); },
6128
+ skip: count => offset += count
6129
+ };
6130
+ }
6131
+
6132
+ function getSegmentOffset(zipWriter, writer) {
6133
+ return zipWriter.offset - writer.diskOffset - (writer.diskNumber ? zipWriter.initialOffset : 0);
6134
+ }
6135
+
5685
6136
  async function writeData(writer, array) {
5686
6137
  const { writable } = writer;
5687
6138
  const streamWriter = writable.getWriter();
@@ -5694,12 +6145,40 @@
5694
6145
  }
5695
6146
  }
5696
6147
 
6148
+ async function flushBufferedData(readable, writer, signal, onChunkWritten) {
6149
+ const streamWriter = writer.writable.getWriter();
6150
+ try {
6151
+ await readable.pipeTo(new WritableStream({
6152
+ async write(chunk) {
6153
+ await streamWriter.ready;
6154
+ await streamWriter.write(chunk);
6155
+ onChunkWritten(getLength(chunk));
6156
+ }
6157
+ }), { preventClose: true, preventAbort: true, signal });
6158
+ } finally {
6159
+ streamWriter.releaseLock();
6160
+ }
6161
+ }
6162
+
5697
6163
  function getTimeNTFS(date) {
5698
6164
  if (date) {
5699
6165
  return ((BigInt(date.getTime()) + BigInt(11644473600000)) * BigInt(10000));
5700
6166
  }
5701
6167
  }
5702
6168
 
6169
+ function getTimeUnix(date) {
6170
+ return Math.floor(date.getTime() / 1000);
6171
+ }
6172
+
6173
+ // the extended timestamp extra field stores dates as signed 32-bit time_t values
6174
+ function inUnixTimeRange(timeUnix) {
6175
+ return timeUnix >= MIN_UNIX_TIME && timeUnix <= MAX_UNIX_TIME;
6176
+ }
6177
+
6178
+ function clampUnixTime(timeUnix) {
6179
+ return Math.min(MAX_UNIX_TIME, Math.max(MIN_UNIX_TIME, timeUnix));
6180
+ }
6181
+
5703
6182
  function getOptionValue(zipWriter, options, name, defaultValue) {
5704
6183
  const result = options[name] === UNDEFINED_VALUE ? zipWriter.options[name] : options[name];
5705
6184
  return result === UNDEFINED_VALUE ? defaultValue : result;
@@ -5730,7 +6209,7 @@
5730
6209
  }
5731
6210
 
5732
6211
  function getDataView(array) {
5733
- return new DataView(array.buffer);
6212
+ return new DataView(array.buffer, array.byteOffset, array.byteLength);
5734
6213
  }
5735
6214
 
5736
6215
  function getLength(...arrayLikes) {
@@ -5746,30 +6225,39 @@
5746
6225
  uncompressedSize,
5747
6226
  compressedSize,
5748
6227
  lastModDate,
6228
+ rawLastModDate,
5749
6229
  rawFilename,
5750
6230
  zip64CompressedSize,
5751
6231
  zip64UncompressedSize,
5752
6232
  extraFieldLength
5753
6233
  }) {
5754
- const headerArray = new Uint8Array(HEADER_SIZE - 4);
6234
+ const headerRecord = createRecordWriter(HEADER_SIZE - 4);
6235
+ const headerArray = headerRecord.array;
5755
6236
  const headerView = getDataView(headerArray);
5756
- setUint16(headerView, 0, version);
5757
- setUint16(headerView, 2, bitFlag);
5758
- setUint16(headerView, 4, compressionMethod);
5759
- const dateArray = new Uint32Array(1);
5760
- const dateView = getDataView(dateArray);
5761
- setUint16(dateView, 0, (((lastModDate.getHours() << 6) | lastModDate.getMinutes()) << 5) | lastModDate.getSeconds() / 2);
5762
- setUint16(dateView, 2, ((((lastModDate.getFullYear() - 1980) << 4) | (lastModDate.getMonth() + 1)) << 5) | lastModDate.getDate());
5763
- const rawLastModDate = dateArray[0];
5764
- setUint32(headerView, 6, rawLastModDate);
6237
+ headerRecord.uint16(version);
6238
+ headerRecord.uint16(bitFlag);
6239
+ headerRecord.uint16(compressionMethod);
6240
+ if (rawLastModDate === UNDEFINED_VALUE) {
6241
+ const dateArray = new Uint32Array(1);
6242
+ const dateView = getDataView(dateArray);
6243
+ setUint16(dateView, 0, (((lastModDate.getHours() << 6) | lastModDate.getMinutes()) << 5) | lastModDate.getSeconds() / 2);
6244
+ setUint16(dateView, 2, ((((lastModDate.getFullYear() - 1980) << 4) | (lastModDate.getMonth() + 1)) << 5) | lastModDate.getDate());
6245
+ rawLastModDate = dateArray[0];
6246
+ }
6247
+ headerRecord.uint32(rawLastModDate);
6248
+ headerRecord.skip(4);
5765
6249
  if (zip64CompressedSize || compressedSize !== UNDEFINED_VALUE) {
5766
- setUint32(headerView, HEADER_OFFSET_COMPRESSED_SIZE, zip64CompressedSize ? MAX_32_BITS : compressedSize);
6250
+ headerRecord.uint32(zip64CompressedSize ? MAX_32_BITS : compressedSize);
6251
+ } else {
6252
+ headerRecord.skip(4);
5767
6253
  }
5768
6254
  if (zip64UncompressedSize || uncompressedSize !== UNDEFINED_VALUE) {
5769
- setUint32(headerView, HEADER_OFFSET_UNCOMPRESSED_SIZE, zip64UncompressedSize ? MAX_32_BITS : uncompressedSize);
6255
+ headerRecord.uint32(zip64UncompressedSize ? MAX_32_BITS : uncompressedSize);
6256
+ } else {
6257
+ headerRecord.skip(4);
5770
6258
  }
5771
- setUint16(headerView, 22, getLength(rawFilename));
5772
- setUint16(headerView, 24, extraFieldLength);
6259
+ headerRecord.uint16(getLength(rawFilename));
6260
+ headerRecord.uint16(extraFieldLength);
5773
6261
  return {
5774
6262
  headerArray,
5775
6263
  headerView,
@@ -5831,12 +6319,15 @@
5831
6319
  */
5832
6320
 
5833
6321
 
6322
+ const ERR_ENTRY_EXISTS = "Entry filename already exists";
6323
+ const ERR_READABLE_CONSUMED = "Readable stream already consumed";
6324
+
5834
6325
  class ZipEntry {
5835
6326
 
5836
6327
  constructor(fs, name, params, parent) {
5837
6328
  const zipEntry = this;
5838
6329
  if (fs.root && parent && parent.getChildByName(name)) {
5839
- throw new Error("Entry filename already exists");
6330
+ throw new Error(ERR_ENTRY_EXISTS);
5840
6331
  }
5841
6332
  if (!params) {
5842
6333
  params = {};
@@ -5846,13 +6337,17 @@
5846
6337
  name,
5847
6338
  data: params.data,
5848
6339
  options: params.options,
5849
- id: fs.entries.length,
6340
+ id: fs.entryIdCounter++,
5850
6341
  parent,
5851
6342
  children: [],
5852
6343
  uncompressedSize: params.uncompressedSize || 0,
5853
6344
  passThrough: params.passThrough
5854
6345
  });
5855
- fs.entries.push(zipEntry);
6346
+ // detached entries (e.g. clones) are not registered until they are attached,
6347
+ // otherwise they could never be garbage collected
6348
+ if (parent || !fs.root) {
6349
+ fs.entries[zipEntry.id] = zipEntry;
6350
+ }
5856
6351
  if (parent) {
5857
6352
  zipEntry.parent.children.push(zipEntry);
5858
6353
  }
@@ -5890,7 +6385,7 @@
5890
6385
  rename(name) {
5891
6386
  const parent = this.parent;
5892
6387
  if (parent && parent.getChildByName(name)) {
5893
- throw new Error("Entry filename already exists");
6388
+ throw new Error(ERR_ENTRY_EXISTS);
5894
6389
  } else {
5895
6390
  this.name = name;
5896
6391
  }
@@ -5929,16 +6424,17 @@
5929
6424
  }
5930
6425
 
5931
6426
  isPasswordProtected() {
5932
- return this.data.encrypted;
6427
+ return Boolean(this.data && this.data.encrypted);
5933
6428
  }
5934
6429
 
5935
6430
  async checkPassword(password, options = {}) {
5936
6431
  const zipEntry = this;
5937
6432
  if (zipEntry.isPasswordProtected()) {
5938
- options.password = password;
5939
- options.checkPasswordOnly = true;
5940
6433
  try {
5941
- await zipEntry.data.getData(null, options);
6434
+ await zipEntry.data.getData(null, Object.assign({}, options, {
6435
+ password,
6436
+ checkPasswordOnly: true
6437
+ }));
5942
6438
  return true;
5943
6439
  } catch (error) {
5944
6440
  if (error.message == ERR_INVALID_PASSWORD) {
@@ -5972,8 +6468,15 @@
5972
6468
  return this.getData({ writable }, options);
5973
6469
  }
5974
6470
 
5975
- getArrayBuffer(options) {
5976
- return this.data.arrayBuffer(options);
6471
+ 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
+ const array = await this.getUint8Array(options);
6476
+ // return the backing buffer directly when the array spans it, slicing the exact bytes otherwise
6477
+ return array.byteOffset || array.byteLength != array.buffer.byteLength
6478
+ ? array.buffer.slice(array.byteOffset, array.byteOffset + array.byteLength)
6479
+ : array.buffer;
5977
6480
  }
5978
6481
 
5979
6482
  replaceBlob(blob) {
@@ -6015,7 +6518,7 @@
6015
6518
  replaceReadable(readable) {
6016
6519
  Object.assign(this, {
6017
6520
  data: null,
6018
- Reader: function () { return { readable }; },
6521
+ Reader: getReadableReader(readable),
6019
6522
  Writer: null,
6020
6523
  reader: null
6021
6524
  });
@@ -6031,7 +6534,7 @@
6031
6534
 
6032
6535
  clone(deepClone) {
6033
6536
  const zipEntry = this;
6034
- const clonedEntry = new ZipDirectoryEntry(zipEntry.fs, zipEntry.name);
6537
+ const clonedEntry = new ZipDirectoryEntry(zipEntry.fs, zipEntry.name, zipEntry);
6035
6538
  if (deepClone) {
6036
6539
  clonedEntry.children = zipEntry.children.map(child => {
6037
6540
  const childClone = child.clone(deepClone);
@@ -6105,7 +6608,7 @@
6105
6608
 
6106
6609
  addReadable(name, readable, options = {}) {
6107
6610
  return addChild(this, name, {
6108
- Reader: function () { return { readable }; },
6611
+ Reader: getReadableReader(readable),
6109
6612
  options
6110
6613
  });
6111
6614
  }
@@ -6119,6 +6622,7 @@
6119
6622
  }
6120
6623
 
6121
6624
  addFile(file, options = {}) {
6625
+ options = Object.assign({}, options);
6122
6626
  if (!options.lastModDate) {
6123
6627
  options.lastModDate = new Date(file.lastModified);
6124
6628
  }
@@ -6185,11 +6689,15 @@
6185
6689
  try {
6186
6690
  const path = entry.filename.split("/");
6187
6691
  const name = path.pop();
6188
- path.forEach((pathPart, pathIndex) => {
6692
+ path.forEach(pathPart => {
6189
6693
  const previousParent = parent;
6190
6694
  parent = parent.getChildByName(pathPart);
6191
- if (!parent) {
6192
- parent = new ZipDirectoryEntry(this.fs, pathPart, { data: pathIndex == path.length - 1 ? entry : null }, previousParent);
6695
+ if (parent) {
6696
+ if (!parent.directory) {
6697
+ throw new Error(ERR_ENTRY_EXISTS);
6698
+ }
6699
+ } else {
6700
+ parent = new ZipDirectoryEntry(this.fs, pathPart, { data: null }, previousParent);
6193
6701
  importedEntries.push(parent);
6194
6702
  }
6195
6703
  });
@@ -6200,6 +6708,22 @@
6200
6708
  uncompressedSize: entry.uncompressedSize,
6201
6709
  passThrough: options.passThrough
6202
6710
  }));
6711
+ } else {
6712
+ let directoryEntry = parent;
6713
+ if (name) {
6714
+ directoryEntry = parent.getChildByName(name);
6715
+ if (directoryEntry) {
6716
+ if (!directoryEntry.directory) {
6717
+ throw new Error(ERR_ENTRY_EXISTS);
6718
+ }
6719
+ } else {
6720
+ directoryEntry = new ZipDirectoryEntry(this.fs, name, { data: null }, parent);
6721
+ importedEntries.push(directoryEntry);
6722
+ }
6723
+ }
6724
+ if (directoryEntry != this && !directoryEntry.data) {
6725
+ directoryEntry.data = entry;
6726
+ }
6203
6727
  }
6204
6728
  } catch (error) {
6205
6729
  try {
@@ -6215,14 +6739,15 @@
6215
6739
  return importedEntries;
6216
6740
  }
6217
6741
 
6218
- async exportZip(writer, options) {
6742
+ async exportZip(writer, options = {}) {
6219
6743
  const zipEntry = this;
6744
+ options = Object.assign({}, options);
6220
6745
  if (options.bufferedWrite === UNDEFINED_VALUE) {
6221
6746
  options.bufferedWrite = true;
6222
6747
  }
6223
- await Promise.all([initReaders(zipEntry, options.readerOptions), initStream(writer)]);
6748
+ const [readers] = await Promise.all([initReaders(zipEntry, options.readerOptions), initStream(writer)]);
6224
6749
  const zipWriter = new ZipWriter(writer, options);
6225
- await exportZip(zipWriter, zipEntry, getTotalSize([zipEntry], "uncompressedSize"), options);
6750
+ await exportZip(zipWriter, zipEntry, getTotalSize([zipEntry], "uncompressedSize"), options, readers);
6226
6751
  await zipWriter.close();
6227
6752
  return writer.getData ? writer.getData() : writer.writable;
6228
6753
  }
@@ -6268,7 +6793,15 @@
6268
6793
 
6269
6794
  remove(entry) {
6270
6795
  detach(entry);
6271
- this.entries[entry.id] = null;
6796
+ const removedEntries = [entry];
6797
+ while (removedEntries.length) {
6798
+ const removedEntry = removedEntries.pop();
6799
+ this.entries[removedEntry.id] = null;
6800
+ for (const child of removedEntry.children) {
6801
+ removedEntries.push(child);
6802
+ }
6803
+ }
6804
+ entry.parent = UNDEFINED_VALUE;
6272
6805
  }
6273
6806
 
6274
6807
  move(entry, destination) {
@@ -6278,12 +6811,17 @@
6278
6811
  if (destination.directory) {
6279
6812
  if (!destination.isDescendantOf(entry)) {
6280
6813
  if (entry != destination) {
6281
- if (destination.getChildByName(entry.name)) {
6282
- throw new Error("Entry filename already exists");
6814
+ const existingChild = destination.getChildByName(entry.name);
6815
+ if (existingChild) {
6816
+ if (existingChild != entry) {
6817
+ throw new Error(ERR_ENTRY_EXISTS);
6818
+ }
6819
+ } else {
6820
+ detach(entry);
6821
+ entry.parent = destination;
6822
+ destination.children.push(entry);
6823
+ registerEntries(this, entry);
6283
6824
  }
6284
- detach(entry);
6285
- entry.parent = destination;
6286
- destination.children.push(entry);
6287
6825
  }
6288
6826
  } else {
6289
6827
  throw new Error("Entry is a ancestor of target entry");
@@ -6300,6 +6838,11 @@
6300
6838
  for (let index = 0; node && index < path.length; index++) {
6301
6839
  node = node.getChildByName(path[index]);
6302
6840
  }
6841
+ if (!node) {
6842
+ // exclude detached entries, e.g. clones registered in `entries` but not attached
6843
+ node = this.entries.find(entry => entry && (entry == this.root || entry.isDescendantOf(this.root)) &&
6844
+ entry.getRelativeName() == fullname);
6845
+ }
6303
6846
  return node;
6304
6847
  }
6305
6848
 
@@ -6381,6 +6924,7 @@
6381
6924
  }
6382
6925
 
6383
6926
  importZip(reader, options) {
6927
+ resetFS(this);
6384
6928
  return this.root.importZip(reader, options);
6385
6929
  }
6386
6930
 
@@ -6400,6 +6944,10 @@
6400
6944
  return this.root.exportWritable(writable, options);
6401
6945
  }
6402
6946
 
6947
+ exportZip(writer, options) {
6948
+ return this.root.exportZip(writer, options);
6949
+ }
6950
+
6403
6951
  isPasswordProtected() {
6404
6952
  return this.root.isPasswordProtected();
6405
6953
  }
@@ -6413,15 +6961,26 @@
6413
6961
 
6414
6962
  function getTotalSize(entries, propertyName) {
6415
6963
  let size = 0;
6416
- entries.forEach(process);
6964
+ const pendingEntries = Array.from(entries);
6965
+ while (pendingEntries.length) {
6966
+ const entry = pendingEntries.pop();
6967
+ size += entry[propertyName] || 0;
6968
+ for (const child of entry.children) {
6969
+ pendingEntries.push(child);
6970
+ }
6971
+ }
6417
6972
  return size;
6973
+ }
6418
6974
 
6419
- function process(entry) {
6420
- size += entry[propertyName];
6421
- if (entry.children) {
6422
- entry.children.forEach(process);
6975
+ function getReadableReader(readable) {
6976
+ let consumed;
6977
+ return function () {
6978
+ if (consumed) {
6979
+ throw new Error(ERR_READABLE_CONSUMED);
6423
6980
  }
6424
- }
6981
+ consumed = true;
6982
+ return { readable };
6983
+ };
6425
6984
  }
6426
6985
 
6427
6986
  function getZipBlobReader(options) {
@@ -6436,7 +6995,7 @@
6436
6995
  async init() {
6437
6996
  const zipBlobReader = this;
6438
6997
  zipBlobReader.size = zipBlobReader.entry.uncompressedSize;
6439
- const data = await zipBlobReader.entry.getData(new BlobWriter(), Object.assign({}, zipBlobReader.options, options));
6998
+ const data = await zipBlobReader.entry.getData(new BlobWriter(), Object.assign({}, options, zipBlobReader.options));
6440
6999
  zipBlobReader.data = data;
6441
7000
  zipBlobReader.blobReader = new BlobReader(data);
6442
7001
  super.init();
@@ -6449,29 +7008,41 @@
6449
7008
  }
6450
7009
 
6451
7010
  async function initReaders(entry, options) {
6452
- if (entry.children.length) {
6453
- await Promise.all(entry.children.map(async child => {
7011
+ const fileEntries = [];
7012
+ const pendingEntries = [entry];
7013
+ // per-export map so concurrent exports do not share (and lock) the same readers
7014
+ const readers = new Map();
7015
+ while (pendingEntries.length) {
7016
+ const pendingEntry = pendingEntries.pop();
7017
+ for (const child of pendingEntry.children) {
6454
7018
  if (child.directory) {
6455
- await initReaders(child, options);
7019
+ pendingEntries.push(child);
6456
7020
  } else {
6457
- const reader = child.reader = new child.Reader(child.data, options);
6458
- try {
6459
- await initStream(reader);
6460
- } catch (error) {
6461
- try {
6462
- error.entryId = child.id;
6463
- error.cause = {
6464
- entry: child
6465
- };
6466
- } catch {
6467
- // ignored
6468
- }
6469
- throw error;
6470
- }
6471
- child.uncompressedSize = reader.size;
7021
+ fileEntries.push(child);
6472
7022
  }
6473
- }));
7023
+ }
6474
7024
  }
7025
+ await Promise.all(fileEntries.map(async child => {
7026
+ const reader = child.reader = new child.Reader(child.data, options);
7027
+ readers.set(child, reader);
7028
+ try {
7029
+ await initStream(reader);
7030
+ } catch (error) {
7031
+ try {
7032
+ error.entryId = child.id;
7033
+ error.cause = {
7034
+ entry: child
7035
+ };
7036
+ } catch {
7037
+ // ignored
7038
+ }
7039
+ throw error;
7040
+ }
7041
+ if (reader.size !== UNDEFINED_VALUE) {
7042
+ child.uncompressedSize = reader.size;
7043
+ }
7044
+ }));
7045
+ return readers;
6475
7046
  }
6476
7047
 
6477
7048
  function detach(entry) {
@@ -6485,7 +7056,7 @@
6485
7056
  }
6486
7057
  }
6487
7058
 
6488
- async function exportZip(zipWriter, entry, totalSize, options) {
7059
+ async function exportZip(zipWriter, entry, totalSize, options, readers) {
6489
7060
  const selectedEntry = entry;
6490
7061
  const entryOffsets = new Map();
6491
7062
  await process(zipWriter, entry);
@@ -6495,7 +7066,11 @@
6495
7066
 
6496
7067
  async function exportChild() {
6497
7068
  if (options.bufferedWrite) {
6498
- await Promise.allSettled(entry.children.map(processChild));
7069
+ const results = await Promise.allSettled(entry.children.map(processChild));
7070
+ const errorResult = results.find(result => result.status == "rejected");
7071
+ if (errorResult) {
7072
+ throw errorResult.reason;
7073
+ }
6499
7074
  } else {
6500
7075
  for (const child of entry.children) {
6501
7076
  await processChild(child);
@@ -6550,7 +7125,7 @@
6550
7125
  });
6551
7126
  }
6552
7127
  }
6553
- await zipWriter.add(name, child.reader, Object.assign({}, options, zipEntryOptions, childOptions, {
7128
+ await zipWriter.add(name, readers.get(child), Object.assign({}, options, zipEntryOptions, childOptions, {
6554
7129
  directory: child.directory,
6555
7130
  onprogress: async indexProgress => {
6556
7131
  if (options.onprogress) {
@@ -6599,7 +7174,7 @@
6599
7174
  }
6600
7175
  } catch (error) {
6601
7176
  const message = error.message + (handle ? " (" + handle.name + ")" : "");
6602
- throw new Error(message);
7177
+ throw new Error(message, { cause: error });
6603
7178
  }
6604
7179
  }
6605
7180
  return addedEntries;
@@ -6659,9 +7234,21 @@
6659
7234
 
6660
7235
  function resetFS(fs) {
6661
7236
  fs.entries = [];
7237
+ fs.entryIdCounter = 0;
6662
7238
  fs.root = new ZipDirectoryEntry(fs);
6663
7239
  }
6664
7240
 
7241
+ function registerEntries(fs, entry) {
7242
+ const pendingEntries = [entry];
7243
+ while (pendingEntries.length) {
7244
+ const pendingEntry = pendingEntries.pop();
7245
+ fs.entries[pendingEntry.id] = pendingEntry;
7246
+ for (const child of pendingEntry.children) {
7247
+ pendingEntries.push(child);
7248
+ }
7249
+ }
7250
+ }
7251
+
6665
7252
  function addChild(parent, name, params, directory) {
6666
7253
  if (parent.directory) {
6667
7254
  return directory ? new ZipDirectoryEntry(parent.fs, name, params, parent) : new ZipFileEntry(parent.fs, name, params, parent);