@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
@@ -35,12 +35,15 @@ import {
35
35
  ERR_INVALID_SIGNATURE,
36
36
  ERR_INVALID_UNCOMPRESSED_SIZE,
37
37
  ERR_INVALID_PASSWORD,
38
+ ERR_INVALID_COMPRESSED_DATA,
38
39
  ERR_ABORT_CHECK_PASSWORD
39
40
  } from "./streams/codec-stream.js";
40
41
  import { CodecWorker } from "./codec-worker.js";
41
42
 
42
43
  let pool = [];
43
44
  const pendingRequests = [];
45
+ let starvationTimeout;
46
+ let starvationDelay;
44
47
 
45
48
  export {
46
49
  runWorker,
@@ -50,6 +53,7 @@ export {
50
53
  ERR_INVALID_SIGNATURE,
51
54
  ERR_INVALID_UNCOMPRESSED_SIZE,
52
55
  ERR_INVALID_PASSWORD,
56
+ ERR_INVALID_COMPRESSED_DATA,
53
57
  ERR_ABORT_CHECK_PASSWORD
54
58
  };
55
59
 
@@ -78,14 +82,20 @@ async function runWorker(stream, workerOptions) {
78
82
  pool.push(workerData);
79
83
  return new CodecWorker(workerData, stream, workerOptions, onTaskFinished);
80
84
  } else {
81
- return new Promise(resolve => pendingRequests.push({ resolve, stream, workerOptions }));
85
+ return new Promise(resolve => {
86
+ pendingRequests.push({ resolve, stream, workerOptions });
87
+ starvationDelay = config.workerStarvationTimeout;
88
+ armStarvationTimeout();
89
+ });
82
90
  }
83
91
  }
84
92
 
85
93
  function onTaskFinished(workerData) {
94
+ clearStarvationTimeout();
86
95
  if (pendingRequests.length) {
87
96
  const [{ resolve, stream, workerOptions }] = pendingRequests.splice(0, 1);
88
97
  resolve(new CodecWorker(workerData, stream, workerOptions, onTaskFinished));
98
+ armStarvationTimeout();
89
99
  } else if (workerData.worker) {
90
100
  clearTerminateTimeout(workerData);
91
101
  terminateWorker(workerData, workerOptions);
@@ -95,6 +105,34 @@ async function runWorker(stream, workerOptions) {
95
105
  }
96
106
  }
97
107
 
108
+ function armStarvationTimeout() {
109
+ if (!starvationTimeout && pendingRequests.length && Number.isFinite(starvationDelay) && starvationDelay >= 0) {
110
+ starvationTimeout = setTimeout(onWorkerStarvation, starvationDelay);
111
+ }
112
+ }
113
+
114
+ function clearStarvationTimeout() {
115
+ if (starvationTimeout) {
116
+ clearTimeout(starvationTimeout);
117
+ starvationTimeout = null;
118
+ }
119
+ }
120
+
121
+ function onWorkerStarvation() {
122
+ starvationTimeout = null;
123
+ if (pendingRequests.length) {
124
+ const [{ resolve, stream, workerOptions }] = pendingRequests.splice(0, 1);
125
+ const inlineWorkerOptions = Object.assign({}, workerOptions, { useWebWorkers: false, workerURI: UNDEFINED_VALUE });
126
+ resolve(new CodecWorker({}, stream, inlineWorkerOptions, onInlineTaskFinished));
127
+ armStarvationTimeout();
128
+ }
129
+ }
130
+
131
+ function onInlineTaskFinished() {
132
+ clearStarvationTimeout();
133
+ armStarvationTimeout();
134
+ }
135
+
98
136
  function terminateWorker(workerData, workerOptions) {
99
137
  const { config } = workerOptions;
100
138
  const { terminateWorkerTimeout } = config;
@@ -26,14 +26,16 @@
26
26
  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
 
29
- /* global Worker, URL, TransformStream, WritableStream */
29
+ /* global Worker, URL, TransformStream, AbortController, structuredClone, DOMException */
30
30
 
31
31
  import {
32
32
  UNDEFINED_VALUE,
33
33
  UNDEFINED_TYPE,
34
34
  FUNCTION_TYPE
35
35
  } from "./constants.js";
36
+ import { getChunkSize } from "./configuration.js";
36
37
  import {
38
+ CODEC_DEFLATE,
37
39
  CodecStream,
38
40
  ChunkStream,
39
41
  MESSAGE_EVENT_TYPE,
@@ -45,8 +47,10 @@ import {
45
47
  } from "./streams/codec-stream.js";
46
48
 
47
49
  const MODULE_WORKER_OPTIONS = { type: "module" };
50
+ const ERROR_EVENT_TYPE = "error";
51
+ const MESSAGE_ERROR_EVENT_TYPE = "messageerror";
48
52
 
49
- let webWorkerSupported, webWorkerURI, webWorkerOptions;
53
+ let webWorkerSupported, webWorkerSource, webWorkerURI, webWorkerOptions;
50
54
  let transferStreamsSupported = true;
51
55
  try {
52
56
  transferStreamsSupported = typeof structuredClone == FUNCTION_TYPE && structuredClone(new DOMException("", "AbortError")).code !== UNDEFINED_VALUE;
@@ -70,8 +74,9 @@ class CodecWorker {
70
74
  const { signal } = streamOptions;
71
75
  Object.assign(workerData, {
72
76
  busy: true,
77
+ generation: (workerData.generation || 0) + 1,
73
78
  readable: readable
74
- .pipeThrough(new ChunkStream(config.chunkSize))
79
+ .pipeThrough(new ChunkStream(getChunkSize(config)))
75
80
  .pipeThrough(new ProgressWatcherStream(streamOptions), { signal }),
76
81
  writable,
77
82
  options: Object.assign({}, options),
@@ -94,15 +99,17 @@ class CodecWorker {
94
99
  });
95
100
  },
96
101
  onTaskFinished() {
97
- const { resolveTerminated } = workerData;
98
- if (resolveTerminated) {
99
- workerData.resolveTerminated = null;
100
- workerData.terminated = true;
101
- workerData.worker.terminate();
102
- resolveTerminated();
102
+ if (workerData.busy) {
103
+ const { resolveTerminated } = workerData;
104
+ if (resolveTerminated) {
105
+ workerData.resolveTerminated = null;
106
+ workerData.terminated = true;
107
+ workerData.worker.terminate();
108
+ resolveTerminated();
109
+ }
110
+ workerData.busy = false;
111
+ onTaskFinished(workerData);
103
112
  }
104
- workerData.busy = false;
105
- onTaskFinished(workerData);
106
113
  }
107
114
  });
108
115
  if (webWorkerSupported === UNDEFINED_VALUE) {
@@ -171,6 +178,7 @@ function createWebWorkerInterface(workerData, config) {
171
178
  }
172
179
  Object.assign(workerData, {
173
180
  worker,
181
+ terminated: false,
174
182
  interface: {
175
183
  run: () => runWebWorker(workerData, { chunkSize, wasmURI, baseURI })
176
184
  }
@@ -186,7 +194,15 @@ async function runWorker({ options, readable, writable, onTaskFinished }, config
186
194
  try {
187
195
  await initModule(config);
188
196
  } catch {
189
- options.useCompressionStream = true;
197
+ // The WASM module failed to load. Fall back to the native CompressionStream only if the
198
+ // zlib codec that would otherwise run depends on that module; a self-contained codec
199
+ // supplied through config (e.g. the pure-JS port) stays usable and is kept as requested.
200
+ const ZlibStream = options.codecType.startsWith(CODEC_DEFLATE) ?
201
+ config.CompressionStreamZlib :
202
+ config.DecompressionStreamZlib;
203
+ if (!ZlibStream || ZlibStream.requiresModule) {
204
+ options.useCompressionStream = true;
205
+ }
190
206
  }
191
207
  }
192
208
  codecStream = new CodecStream(options, config);
@@ -225,37 +241,85 @@ async function runWebWorker(workerData, config) {
225
241
  result
226
242
  });
227
243
  const { readable, options } = workerData;
228
- const { writable, closed } = watchClosedStream(workerData.writable);
229
- const streamsTransferred = sendMessage({
230
- type: MESSAGE_START,
231
- options,
232
- config,
233
- readable,
234
- writable
235
- }, workerData);
244
+ const { writable, closed, abortPipe } = watchClosedStream(workerData.writable);
245
+ let streamsTransferred;
246
+ try {
247
+ streamsTransferred = sendMessage({
248
+ type: MESSAGE_START,
249
+ options,
250
+ config,
251
+ readable,
252
+ writable
253
+ }, workerData);
254
+ } catch (error) {
255
+ abortPipe();
256
+ try {
257
+ await closed;
258
+ } catch {
259
+ // ignored
260
+ }
261
+ workerData.onTaskFinished();
262
+ throw error;
263
+ }
236
264
  if (!streamsTransferred) {
237
265
  Object.assign(workerData, {
238
266
  reader: readable.getReader(),
239
267
  writer: writable.getWriter()
240
268
  });
241
269
  }
242
- const resultValue = await result;
243
- if (!streamsTransferred) {
244
- await writable.getWriter().close();
270
+ try {
271
+ const resultValue = await result;
272
+ await closeWritable();
273
+ await closed;
274
+ return resultValue;
275
+ } catch (error) {
276
+ await closeWritable();
277
+ // unblock the pipe watcher if the worker died before closing its end of the stream
278
+ abortPipe();
279
+ try {
280
+ await closed;
281
+ } catch {
282
+ // ignored
283
+ }
284
+ throw error;
285
+ }
286
+
287
+ async function closeWritable() {
288
+ if (!streamsTransferred && !writable.locked) {
289
+ try {
290
+ await writable.getWriter().close();
291
+ } catch {
292
+ // ignored
293
+ }
294
+ }
245
295
  }
246
- await closed;
247
- return resultValue;
248
296
  }
249
297
 
250
298
  function watchClosedStream(writableSource) {
299
+ const abortController = new AbortController();
251
300
  const { writable, readable } = new TransformStream();
252
- const closed = readable.pipeTo(writableSource, { preventClose: true });
253
- return { writable, closed };
301
+ const closed = readable.pipeTo(writableSource, { preventClose: true, preventAbort: true, signal: abortController.signal });
302
+ // the pipe can fail before `closed` is awaited (e.g. the destination write fails);
303
+ // mark the rejection as handled so it cannot kill the process in the meantime
304
+ closed.catch(() => { });
305
+ return { writable, closed, abortPipe: () => abortController.abort() };
306
+ }
307
+
308
+ function terminateWorker(workerData) {
309
+ const { worker } = workerData;
310
+ if (worker) {
311
+ try {
312
+ worker.terminate();
313
+ } catch {
314
+ // ignored
315
+ }
316
+ }
317
+ workerData.interface = null;
254
318
  }
255
319
 
256
320
  function getWebWorker(url, baseURI, workerData, isModuleType, useBlobURI = true) {
257
321
  let worker, resolvedURI, resolvedOptions;
258
- if (webWorkerURI === UNDEFINED_VALUE) {
322
+ if (webWorkerURI === UNDEFINED_VALUE || webWorkerSource !== url) {
259
323
  // deno-lint-ignore valid-typeof
260
324
  const isFunctionURI = typeof url == FUNCTION_TYPE;
261
325
  if (isFunctionURI) {
@@ -312,16 +376,36 @@ function getWebWorker(url, baseURI, workerData, isModuleType, useBlobURI = true)
312
376
  }
313
377
  }
314
378
  }
379
+ webWorkerSource = url;
315
380
  webWorkerURI = resolvedURI;
316
381
  webWorkerOptions = resolvedOptions;
317
382
  } else {
318
383
  worker = new Worker(webWorkerURI, webWorkerOptions);
319
384
  }
320
385
  worker.addEventListener(MESSAGE_EVENT_TYPE, event => onMessage(event, workerData));
386
+ worker.addEventListener(ERROR_EVENT_TYPE, event => onWorkerError(event, workerData));
387
+ worker.addEventListener(MESSAGE_ERROR_EVENT_TYPE, event => onWorkerError(event, workerData));
321
388
  return worker;
322
389
  }
323
390
 
324
- function sendMessage(message, { worker, writer, onTaskFinished, transferStreams }) {
391
+ function onWorkerError(event, workerData) {
392
+ if (event.preventDefault) {
393
+ event.preventDefault();
394
+ }
395
+ const { rejectResult, writer, onTaskFinished } = workerData;
396
+ // the worker is unusable (e.g. its script failed to load); terminate it and
397
+ // fail the current task instead of letting it hang forever
398
+ terminateWorker(workerData);
399
+ if (rejectResult) {
400
+ rejectResult(event.error || new Error(event.message || ERROR_EVENT_TYPE));
401
+ if (writer) {
402
+ writer.releaseLock();
403
+ }
404
+ onTaskFinished();
405
+ }
406
+ }
407
+
408
+ function sendMessage(message, { worker, writer, transferStreams }) {
325
409
  try {
326
410
  const { value, readable, writable } = message;
327
411
  const transferables = [];
@@ -355,14 +439,16 @@ function sendMessage(message, { worker, writer, onTaskFinished, transferStreams
355
439
  if (writer) {
356
440
  writer.releaseLock();
357
441
  }
358
- onTaskFinished();
359
442
  throw error;
360
443
  }
361
444
  }
362
445
 
363
446
  async function onMessage({ data }, workerData) {
364
447
  const { type, value, messageId, result, error } = data;
365
- const { reader, writer, resolveResult, rejectResult, onTaskFinished } = workerData;
448
+ const { reader, writer, resolveResult, rejectResult, onTaskFinished, generation } = workerData;
449
+ // continuations below can settle after the worker slot has been reassigned to a new
450
+ // task (e.g. a pending pull read failing late); they must not touch the new task
451
+ const stale = () => workerData.generation != generation;
366
452
  try {
367
453
  if (error) {
368
454
  const { message, stack, code, name, outputSize } = error;
@@ -372,23 +458,32 @@ async function onMessage({ data }, workerData) {
372
458
  } else {
373
459
  if (type == MESSAGE_PULL) {
374
460
  const { value, done } = await reader.read();
375
- sendMessage({ type: MESSAGE_DATA, value, done, messageId }, workerData);
461
+ if (!stale()) {
462
+ sendMessage({ type: MESSAGE_DATA, value, done, messageId }, workerData);
463
+ }
376
464
  }
377
465
  if (type == MESSAGE_DATA) {
378
466
  await writer.ready;
379
467
  await writer.write(new Uint8Array(value));
380
- sendMessage({ type: MESSAGE_ACK_DATA, messageId }, workerData);
468
+ if (!stale()) {
469
+ sendMessage({ type: MESSAGE_ACK_DATA, messageId }, workerData);
470
+ }
381
471
  }
382
472
  if (type == MESSAGE_CLOSE) {
383
473
  close(null, result);
384
474
  }
385
475
  }
386
476
  } catch (error) {
387
- sendMessage({ type: MESSAGE_CLOSE, messageId }, workerData);
388
- close(error);
477
+ if (!stale()) {
478
+ terminateWorker(workerData);
479
+ close(error);
480
+ }
389
481
  }
390
482
 
391
483
  function close(error, result) {
484
+ if (stale()) {
485
+ return;
486
+ }
392
487
  if (error) {
393
488
  rejectResult(error);
394
489
  } else {
@@ -48,6 +48,7 @@ const DEFAULT_CONFIGURATION = {
48
48
  chunkSize: 64 * 1024,
49
49
  maxWorkers,
50
50
  terminateWorkerTimeout: 5000,
51
+ workerStarvationTimeout: 5000,
51
52
  useWebWorkers: true,
52
53
  useCompressionStream: true,
53
54
  CompressionStream: typeof CompressionStream != UNDEFINED_TYPE && CompressionStream,
@@ -76,6 +77,7 @@ function configure(configuration) {
76
77
  chunkSize,
77
78
  maxWorkers,
78
79
  terminateWorkerTimeout,
80
+ workerStarvationTimeout,
79
81
  useCompressionStream,
80
82
  useWebWorkers,
81
83
  CompressionStream,
@@ -91,6 +93,7 @@ function configure(configuration) {
91
93
  setIfDefined("chunkSize", chunkSize);
92
94
  setIfDefined("maxWorkers", maxWorkers);
93
95
  setIfDefined("terminateWorkerTimeout", terminateWorkerTimeout);
96
+ setIfDefined("workerStarvationTimeout", workerStarvationTimeout);
94
97
  setIfDefined("useCompressionStream", useCompressionStream);
95
98
  setIfDefined("useWebWorkers", useWebWorkers);
96
99
  setIfDefined("CompressionStream", CompressionStream);
@@ -41,6 +41,7 @@ const CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
41
41
  const END_OF_CENTRAL_DIR_SIGNATURE = 0x06054b50;
42
42
  const ZIP64_END_OF_CENTRAL_DIR_SIGNATURE = 0x06064b50;
43
43
  const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE = 0x07064b50;
44
+ const CENTRAL_FILE_HEADER_LENGTH = 46;
44
45
  const END_OF_CENTRAL_DIR_LENGTH = 22;
45
46
  const ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH = 20;
46
47
  const ZIP64_END_OF_CENTRAL_DIR_LENGTH = 56;
@@ -88,9 +89,11 @@ const VERSION_AES = 0x33;
88
89
  const DIRECTORY_SIGNATURE = "/";
89
90
 
90
91
  const HEADER_SIZE = 30;
92
+ const HEADER_OFFSET_VERSION = 0;
91
93
  const HEADER_OFFSET_SIGNATURE = 10;
92
94
  const HEADER_OFFSET_COMPRESSED_SIZE = 14;
93
95
  const HEADER_OFFSET_UNCOMPRESSED_SIZE = 18;
96
+ const LOCAL_HEADER_COMMON_OFFSET = 4;
94
97
 
95
98
  const MAX_DATE = new Date(2107, 11, 31);
96
99
  const MIN_DATE = new Date(1980, 0, 1);
@@ -113,6 +116,7 @@ export {
113
116
  LOCAL_FILE_HEADER_SIGNATURE,
114
117
  DATA_DESCRIPTOR_RECORD_SIGNATURE,
115
118
  CENTRAL_FILE_HEADER_SIGNATURE,
119
+ CENTRAL_FILE_HEADER_LENGTH,
116
120
  END_OF_CENTRAL_DIR_SIGNATURE,
117
121
  ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE,
118
122
  ZIP64_END_OF_CENTRAL_DIR_SIGNATURE,
@@ -156,9 +160,11 @@ export {
156
160
  VERSION_ZIP64,
157
161
  VERSION_AES,
158
162
  HEADER_SIZE,
163
+ HEADER_OFFSET_VERSION,
159
164
  HEADER_OFFSET_SIGNATURE,
160
165
  HEADER_OFFSET_COMPRESSED_SIZE,
161
166
  HEADER_OFFSET_UNCOMPRESSED_SIZE,
167
+ LOCAL_HEADER_COMMON_OFFSET,
162
168
  DIRECTORY_SIGNATURE,
163
169
  MIN_DATE,
164
170
  MAX_DATE,
package/lib/core/io.js CHANGED
@@ -44,6 +44,7 @@ const ERR_WRITER_NOT_INITIALIZED = "Writer not initialized";
44
44
 
45
45
  const CONTENT_TYPE_TEXT_PLAIN = "text/plain";
46
46
  const HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
47
+ const HTTP_HEADER_CONTENT_ENCODING = "Content-Encoding";
47
48
  const HTTP_HEADER_CONTENT_RANGE = "Content-Range";
48
49
  const HTTP_HEADER_ACCEPT_RANGES = "Accept-Ranges";
49
50
  const HTTP_HEADER_RANGE = "Range";
@@ -168,7 +169,7 @@ class Data64URIWriter extends Writer {
168
169
 
169
170
  writeUint8Array(array) {
170
171
  const writer = this;
171
- let indexArray = 0;
172
+ let indexArray;
172
173
  let dataString = writer.pending;
173
174
  const delta = writer.pending.length;
174
175
  writer.pending = "";
@@ -178,12 +179,10 @@ class Data64URIWriter extends Writer {
178
179
  for (; indexArray < array.length; indexArray++) {
179
180
  writer.pending += String.fromCharCode(array[indexArray]);
180
181
  }
181
- if (dataString.length) {
182
- if (dataString.length > 2) {
183
- writer.data += btoa(dataString);
184
- } else {
185
- writer.pending += dataString;
186
- }
182
+ if (dataString.length > 2) {
183
+ writer.data += btoa(dataString);
184
+ } else {
185
+ writer.pending = dataString + writer.pending;
187
186
  }
188
187
  }
189
188
 
@@ -230,6 +229,9 @@ class BlobWriter extends Stream {
230
229
  }
231
230
  });
232
231
  writer.blob = new Response(transformStream.readable, { headers }).blob();
232
+ // mark early rejections (e.g. aborted pipe) as handled; awaiting getData() still
233
+ // surfaces them
234
+ writer.blob.catch(() => { });
233
235
  }
234
236
 
235
237
  getData() {
@@ -314,7 +316,8 @@ function createHttpReader(httpReader, url, options) {
314
316
  preventHeadRequest,
315
317
  useRangeHeader,
316
318
  forceRangeRequests,
317
- combineSizeEocd
319
+ combineSizeEocd,
320
+ fetch
318
321
  } = options;
319
322
  options = Object.assign({}, options);
320
323
  delete options.preventHeadRequest;
@@ -322,13 +325,15 @@ function createHttpReader(httpReader, url, options) {
322
325
  delete options.forceRangeRequests;
323
326
  delete options.combineSizeEocd;
324
327
  delete options.useXHR;
328
+ delete options.fetch;
325
329
  Object.assign(httpReader, {
326
330
  url,
327
331
  options,
328
332
  preventHeadRequest,
329
333
  useRangeHeader,
330
334
  forceRangeRequests,
331
- combineSizeEocd
335
+ combineSizeEocd,
336
+ fetch
332
337
  });
333
338
  }
334
339
 
@@ -347,7 +352,10 @@ async function initHttpReader(httpReader, sendRequest, getRequestData) {
347
352
  throw new Error(ERR_HTTP_RANGE);
348
353
  } else {
349
354
  if (combineSizeEocd) {
350
- httpReader.eocdCache = new Uint8Array(await response.arrayBuffer());
355
+ const eocdCache = new Uint8Array(await response.arrayBuffer());
356
+ if (response.status == 206 && eocdCache.length == END_OF_CENTRAL_DIR_LENGTH) {
357
+ httpReader.eocdCache = eocdCache;
358
+ }
351
359
  }
352
360
  let contentSize;
353
361
  const contentRangeHeader = response.headers.get(HTTP_HEADER_CONTENT_RANGE);
@@ -393,7 +401,18 @@ async function readUint8ArrayHttpReader(httpReader, index, length, sendRequest,
393
401
  if (response.status != 206) {
394
402
  throw new Error(ERR_HTTP_RANGE);
395
403
  }
396
- return new Uint8Array(await response.arrayBuffer());
404
+ const contentRangeHeader = response.headers.get(HTTP_HEADER_CONTENT_RANGE);
405
+ if (contentRangeHeader) {
406
+ const rangeStart = Number(contentRangeHeader.trim().split(/[\s-]+/)[1]);
407
+ if (!Number.isNaN(rangeStart) && rangeStart != index) {
408
+ throw new Error(ERR_HTTP_RANGE);
409
+ }
410
+ }
411
+ const data = new Uint8Array(await response.arrayBuffer());
412
+ if (data.length != length) {
413
+ throw new Error(ERR_HTTP_RANGE);
414
+ }
415
+ return data;
397
416
  }
398
417
  } else {
399
418
  const { data } = httpReader;
@@ -430,9 +449,9 @@ async function getXMLHttpRequestData(httpReader) {
430
449
  async function getRequestData(httpReader, sendRequest) {
431
450
  const response = await sendRequest(HTTP_METHOD_GET, httpReader, getHeaders(httpReader));
432
451
  httpReader.data = new Uint8Array(await response.arrayBuffer());
433
- if (!httpReader.size) {
434
- httpReader.size = httpReader.data.length;
435
- }
452
+ // the downloaded content is authoritative, e.g. the Content-Length response header
453
+ // of the HEAD request can be the encoded size when the content is compressed
454
+ httpReader.size = httpReader.data.length;
436
455
  }
437
456
 
438
457
  async function getContentLength(httpReader, sendRequest, getRequestData) {
@@ -441,7 +460,7 @@ async function getContentLength(httpReader, sendRequest, getRequestData) {
441
460
  } else {
442
461
  const response = await sendRequest(HTTP_METHOD_HEAD, httpReader, getHeaders(httpReader));
443
462
  const contentLength = response.headers.get(HTTP_HEADER_CONTENT_LENGTH);
444
- if (contentLength) {
463
+ if (contentLength && !response.headers.get(HTTP_HEADER_CONTENT_ENCODING)) {
445
464
  httpReader.size = Number(contentLength);
446
465
  } else {
447
466
  await getRequestData(httpReader, httpReader.options);
@@ -449,8 +468,8 @@ async function getContentLength(httpReader, sendRequest, getRequestData) {
449
468
  }
450
469
  }
451
470
 
452
- async function sendFetchRequest(method, { options, url }, headers) {
453
- const response = await fetch(url, Object.assign({}, options, { method, headers }));
471
+ async function sendFetchRequest(method, { fetch: fetchFunction = fetch, options, url }, headers) {
472
+ const response = await fetchFunction(url, Object.assign({}, options, { method, headers }));
454
473
  if (response.status < 400) {
455
474
  return response;
456
475
  } else {
@@ -496,7 +515,8 @@ class HttpReader extends Reader {
496
515
  super();
497
516
  Object.assign(this, {
498
517
  url,
499
- reader: options.useXHR ? new XHRReader(url, options) : new FetchReader(url, options)
518
+ // an explicit fetch implementation takes precedence over useXHR
519
+ reader: options.useXHR && !options.fetch ? new XHRReader(url, options) : new FetchReader(url, options)
500
520
  });
501
521
  }
502
522
 
@@ -521,8 +541,7 @@ class HttpReader extends Reader {
521
541
  class HttpRangeReader extends HttpReader {
522
542
 
523
543
  constructor(url, options = {}) {
524
- options.useRangeHeader = true;
525
- super(url, options);
544
+ super(url, Object.assign({}, options, { useRangeHeader: true }));
526
545
  }
527
546
  }
528
547
 
@@ -596,7 +615,7 @@ class SplitDataReader extends Reader {
596
615
  reader.lastDiskNumber = 0;
597
616
  reader.lastDiskOffset = 0;
598
617
  await Promise.all(readers.map(async (diskReader, indexDiskReader) => {
599
- await diskReader.init();
618
+ await initStream(diskReader);
600
619
  if (indexDiskReader != readers.length - 1) {
601
620
  reader.lastDiskOffset += diskReader.size;
602
621
  }
@@ -680,14 +699,26 @@ class SplitDataWriter extends Stream {
680
699
  writer.diskOffset += diskSourceWriter.size;
681
700
  writer.diskNumber++;
682
701
  diskWriter = null;
683
- await this.write(chunk.subarray(availableSize));
702
+ // the next disk is not opened yet but its space is available; leaving a
703
+ // stale value here would force a new disk for every subsequent write
704
+ writer.availableSize = writer.maxSize;
705
+ if (chunk.length > availableSize) {
706
+ await this.write(chunk.subarray(availableSize));
707
+ }
684
708
  } else {
685
709
  await writeChunk(chunk);
686
710
  }
687
711
  },
688
712
  async close() {
689
- await diskWriter.ready;
690
- await closeDisk();
713
+ if (diskWriter) {
714
+ await diskWriter.ready;
715
+ await closeDisk();
716
+ }
717
+ },
718
+ async abort(reason) {
719
+ if (diskWriter) {
720
+ await diskWriter.abort(reason);
721
+ }
691
722
  }
692
723
  });
693
724
  Object.defineProperty(writer, PROPERTY_NAME_WRITABLE, {
@@ -702,7 +733,6 @@ class SplitDataWriter extends Stream {
702
733
  await diskWriter.ready;
703
734
  await diskWriter.write(chunk);
704
735
  diskSourceWriter.size += chunkLength;
705
- writer.size += chunkLength;
706
736
  writer.availableSize -= chunkLength;
707
737
  }
708
738
  }
@@ -38,6 +38,7 @@ const OPTION_SIGNAL = "signal";
38
38
  const OPTION_CHECK_PASSWORD_ONLY = "checkPasswordOnly";
39
39
  const OPTION_CHECK_OVERLAPPING_ENTRY_ONLY = "checkOverlappingEntryOnly";
40
40
  const OPTION_CHECK_OVERLAPPING_ENTRY = "checkOverlappingEntry";
41
+ const OPTION_CHECK_AMBIGUITY = "checkAmbiguity";
41
42
  const OPTION_CHECK_SIGNATURE = "checkSignature";
42
43
  const OPTION_USE_WEB_WORKERS = "useWebWorkers";
43
44
  const OPTION_USE_COMPRESSION_STREAM = "useCompressionStream";
@@ -71,6 +72,7 @@ export {
71
72
  OPTION_CHECK_PASSWORD_ONLY,
72
73
  OPTION_CHECK_OVERLAPPING_ENTRY_ONLY,
73
74
  OPTION_CHECK_OVERLAPPING_ENTRY,
75
+ OPTION_CHECK_AMBIGUITY,
74
76
  OPTION_CHECK_SIGNATURE,
75
77
  OPTION_USE_WEB_WORKERS,
76
78
  OPTION_USE_COMPRESSION_STREAM,