@zip.js/zip.js 2.8.26 → 2.8.28

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 (52) hide show
  1. package/README.md +2 -2
  2. package/deno.json +1 -1
  3. package/dist/zip-core.js +1050 -569
  4. package/dist/zip-core.min.js +1 -1
  5. package/dist/zip-fs-core.js +1146 -617
  6. package/dist/zip-fs-core.min.js +1 -1
  7. package/dist/zip-fs-native.js +1207 -627
  8. package/dist/zip-fs-native.min.js +1 -1
  9. package/dist/zip-fs.js +1279 -681
  10. package/dist/zip-fs.min.js +1 -1
  11. package/dist/zip-legacy.js +1051 -570
  12. package/dist/zip-legacy.min.js +1 -1
  13. package/dist/zip-module.wasm +0 -0
  14. package/dist/zip-native.js +1053 -572
  15. package/dist/zip-native.min.js +1 -1
  16. package/dist/zip-web-worker-native.js +1 -1
  17. package/dist/zip-web-worker.js +1 -1
  18. package/dist/zip.js +1125 -626
  19. package/dist/zip.min.js +1 -1
  20. package/eslint.config.mjs +6 -1
  21. package/index-native.cjs +1207 -627
  22. package/index-native.min.js +1 -1
  23. package/index.cjs +1279 -681
  24. package/index.d.ts +2451 -2355
  25. package/index.min.js +1 -1
  26. package/lib/core/codec-pool.js +39 -1
  27. package/lib/core/codec-worker.js +120 -34
  28. package/lib/core/configuration.js +3 -0
  29. package/lib/core/constants.js +6 -0
  30. package/lib/core/io.js +55 -25
  31. package/lib/core/options.js +2 -0
  32. package/lib/core/streams/aes-crypto-stream.js +12 -12
  33. package/lib/core/streams/codec-stream.js +11 -5
  34. package/lib/core/streams/codecs/sjcl.js +1 -33
  35. package/lib/core/streams/common-crypto.js +4 -4
  36. package/lib/core/streams/zip-crypto-stream.js +25 -15
  37. package/lib/core/streams/zip-entry-stream.js +35 -1
  38. package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
  39. package/lib/core/streams/zlib-wasm/zlib-streams.js +73 -55
  40. package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
  41. package/lib/core/util/decode-cp437.js +1 -1
  42. package/lib/core/util/decode-text.js +2 -1
  43. package/lib/core/web-worker-base.js +12 -4
  44. package/lib/core/web-worker-inline-native.js +1 -1
  45. package/lib/core/web-worker-inline-wasm.js +1 -1
  46. package/lib/core/zip-fs.js +154 -55
  47. package/lib/core/zip-reader.js +185 -67
  48. package/lib/core/zip-writer.js +613 -376
  49. package/lib/core/zlib-streams-inline.js +1 -1
  50. package/lib/zip-core-reader.js +3 -1
  51. package/lib/zip-core-writer.js +1 -0
  52. 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,13 +26,14 @@
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 {
37
38
  CodecStream,
38
39
  ChunkStream,
@@ -45,8 +46,10 @@ import {
45
46
  } from "./streams/codec-stream.js";
46
47
 
47
48
  const MODULE_WORKER_OPTIONS = { type: "module" };
49
+ const ERROR_EVENT_TYPE = "error";
50
+ const MESSAGE_ERROR_EVENT_TYPE = "messageerror";
48
51
 
49
- let webWorkerSupported, webWorkerURI, webWorkerOptions;
52
+ let webWorkerSupported, webWorkerSource, webWorkerURI, webWorkerOptions;
50
53
  let transferStreamsSupported = true;
51
54
  try {
52
55
  transferStreamsSupported = typeof structuredClone == FUNCTION_TYPE && structuredClone(new DOMException("", "AbortError")).code !== UNDEFINED_VALUE;
@@ -70,8 +73,9 @@ class CodecWorker {
70
73
  const { signal } = streamOptions;
71
74
  Object.assign(workerData, {
72
75
  busy: true,
76
+ generation: (workerData.generation || 0) + 1,
73
77
  readable: readable
74
- .pipeThrough(new ChunkStream(config.chunkSize))
78
+ .pipeThrough(new ChunkStream(getChunkSize(config)))
75
79
  .pipeThrough(new ProgressWatcherStream(streamOptions), { signal }),
76
80
  writable,
77
81
  options: Object.assign({}, options),
@@ -94,15 +98,17 @@ class CodecWorker {
94
98
  });
95
99
  },
96
100
  onTaskFinished() {
97
- const { resolveTerminated } = workerData;
98
- if (resolveTerminated) {
99
- workerData.resolveTerminated = null;
100
- workerData.terminated = true;
101
- workerData.worker.terminate();
102
- resolveTerminated();
101
+ if (workerData.busy) {
102
+ const { resolveTerminated } = workerData;
103
+ if (resolveTerminated) {
104
+ workerData.resolveTerminated = null;
105
+ workerData.terminated = true;
106
+ workerData.worker.terminate();
107
+ resolveTerminated();
108
+ }
109
+ workerData.busy = false;
110
+ onTaskFinished(workerData);
103
111
  }
104
- workerData.busy = false;
105
- onTaskFinished(workerData);
106
112
  }
107
113
  });
108
114
  if (webWorkerSupported === UNDEFINED_VALUE) {
@@ -171,6 +177,7 @@ function createWebWorkerInterface(workerData, config) {
171
177
  }
172
178
  Object.assign(workerData, {
173
179
  worker,
180
+ terminated: false,
174
181
  interface: {
175
182
  run: () => runWebWorker(workerData, { chunkSize, wasmURI, baseURI })
176
183
  }
@@ -225,37 +232,85 @@ async function runWebWorker(workerData, config) {
225
232
  result
226
233
  });
227
234
  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);
235
+ const { writable, closed, abortPipe } = watchClosedStream(workerData.writable);
236
+ let streamsTransferred;
237
+ try {
238
+ streamsTransferred = sendMessage({
239
+ type: MESSAGE_START,
240
+ options,
241
+ config,
242
+ readable,
243
+ writable
244
+ }, workerData);
245
+ } catch (error) {
246
+ abortPipe();
247
+ try {
248
+ await closed;
249
+ } catch {
250
+ // ignored
251
+ }
252
+ workerData.onTaskFinished();
253
+ throw error;
254
+ }
236
255
  if (!streamsTransferred) {
237
256
  Object.assign(workerData, {
238
257
  reader: readable.getReader(),
239
258
  writer: writable.getWriter()
240
259
  });
241
260
  }
242
- const resultValue = await result;
243
- if (!streamsTransferred) {
244
- await writable.getWriter().close();
261
+ try {
262
+ const resultValue = await result;
263
+ await closeWritable();
264
+ await closed;
265
+ return resultValue;
266
+ } catch (error) {
267
+ await closeWritable();
268
+ // unblock the pipe watcher if the worker died before closing its end of the stream
269
+ abortPipe();
270
+ try {
271
+ await closed;
272
+ } catch {
273
+ // ignored
274
+ }
275
+ throw error;
276
+ }
277
+
278
+ async function closeWritable() {
279
+ if (!streamsTransferred && !writable.locked) {
280
+ try {
281
+ await writable.getWriter().close();
282
+ } catch {
283
+ // ignored
284
+ }
285
+ }
245
286
  }
246
- await closed;
247
- return resultValue;
248
287
  }
249
288
 
250
289
  function watchClosedStream(writableSource) {
290
+ const abortController = new AbortController();
251
291
  const { writable, readable } = new TransformStream();
252
- const closed = readable.pipeTo(writableSource, { preventClose: true });
253
- return { writable, closed };
292
+ const closed = readable.pipeTo(writableSource, { preventClose: true, preventAbort: true, signal: abortController.signal });
293
+ // the pipe can fail before `closed` is awaited (e.g. the destination write fails);
294
+ // mark the rejection as handled so it cannot kill the process in the meantime
295
+ closed.catch(() => { });
296
+ return { writable, closed, abortPipe: () => abortController.abort() };
297
+ }
298
+
299
+ function terminateWorker(workerData) {
300
+ const { worker } = workerData;
301
+ if (worker) {
302
+ try {
303
+ worker.terminate();
304
+ } catch {
305
+ // ignored
306
+ }
307
+ }
308
+ workerData.interface = null;
254
309
  }
255
310
 
256
311
  function getWebWorker(url, baseURI, workerData, isModuleType, useBlobURI = true) {
257
312
  let worker, resolvedURI, resolvedOptions;
258
- if (webWorkerURI === UNDEFINED_VALUE) {
313
+ if (webWorkerURI === UNDEFINED_VALUE || webWorkerSource !== url) {
259
314
  // deno-lint-ignore valid-typeof
260
315
  const isFunctionURI = typeof url == FUNCTION_TYPE;
261
316
  if (isFunctionURI) {
@@ -312,16 +367,36 @@ function getWebWorker(url, baseURI, workerData, isModuleType, useBlobURI = true)
312
367
  }
313
368
  }
314
369
  }
370
+ webWorkerSource = url;
315
371
  webWorkerURI = resolvedURI;
316
372
  webWorkerOptions = resolvedOptions;
317
373
  } else {
318
374
  worker = new Worker(webWorkerURI, webWorkerOptions);
319
375
  }
320
376
  worker.addEventListener(MESSAGE_EVENT_TYPE, event => onMessage(event, workerData));
377
+ worker.addEventListener(ERROR_EVENT_TYPE, event => onWorkerError(event, workerData));
378
+ worker.addEventListener(MESSAGE_ERROR_EVENT_TYPE, event => onWorkerError(event, workerData));
321
379
  return worker;
322
380
  }
323
381
 
324
- function sendMessage(message, { worker, writer, onTaskFinished, transferStreams }) {
382
+ function onWorkerError(event, workerData) {
383
+ if (event.preventDefault) {
384
+ event.preventDefault();
385
+ }
386
+ const { rejectResult, writer, onTaskFinished } = workerData;
387
+ // the worker is unusable (e.g. its script failed to load); terminate it and
388
+ // fail the current task instead of letting it hang forever
389
+ terminateWorker(workerData);
390
+ if (rejectResult) {
391
+ rejectResult(event.error || new Error(event.message || ERROR_EVENT_TYPE));
392
+ if (writer) {
393
+ writer.releaseLock();
394
+ }
395
+ onTaskFinished();
396
+ }
397
+ }
398
+
399
+ function sendMessage(message, { worker, writer, transferStreams }) {
325
400
  try {
326
401
  const { value, readable, writable } = message;
327
402
  const transferables = [];
@@ -355,14 +430,16 @@ function sendMessage(message, { worker, writer, onTaskFinished, transferStreams
355
430
  if (writer) {
356
431
  writer.releaseLock();
357
432
  }
358
- onTaskFinished();
359
433
  throw error;
360
434
  }
361
435
  }
362
436
 
363
437
  async function onMessage({ data }, workerData) {
364
438
  const { type, value, messageId, result, error } = data;
365
- const { reader, writer, resolveResult, rejectResult, onTaskFinished } = workerData;
439
+ const { reader, writer, resolveResult, rejectResult, onTaskFinished, generation } = workerData;
440
+ // continuations below can settle after the worker slot has been reassigned to a new
441
+ // task (e.g. a pending pull read failing late); they must not touch the new task
442
+ const stale = () => workerData.generation != generation;
366
443
  try {
367
444
  if (error) {
368
445
  const { message, stack, code, name, outputSize } = error;
@@ -372,23 +449,32 @@ async function onMessage({ data }, workerData) {
372
449
  } else {
373
450
  if (type == MESSAGE_PULL) {
374
451
  const { value, done } = await reader.read();
375
- sendMessage({ type: MESSAGE_DATA, value, done, messageId }, workerData);
452
+ if (!stale()) {
453
+ sendMessage({ type: MESSAGE_DATA, value, done, messageId }, workerData);
454
+ }
376
455
  }
377
456
  if (type == MESSAGE_DATA) {
378
457
  await writer.ready;
379
458
  await writer.write(new Uint8Array(value));
380
- sendMessage({ type: MESSAGE_ACK_DATA, messageId }, workerData);
459
+ if (!stale()) {
460
+ sendMessage({ type: MESSAGE_ACK_DATA, messageId }, workerData);
461
+ }
381
462
  }
382
463
  if (type == MESSAGE_CLOSE) {
383
464
  close(null, result);
384
465
  }
385
466
  }
386
467
  } catch (error) {
387
- sendMessage({ type: MESSAGE_CLOSE, messageId }, workerData);
388
- close(error);
468
+ if (!stale()) {
469
+ terminateWorker(workerData);
470
+ close(error);
471
+ }
389
472
  }
390
473
 
391
474
  function close(error, result) {
475
+ if (stale()) {
476
+ return;
477
+ }
392
478
  if (error) {
393
479
  rejectResult(error);
394
480
  } 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,
@@ -70,13 +70,12 @@ let DERIVE_BITS_SUPPORTED = CRYPTO_API_SUPPORTED && SUBTLE_API_SUPPORTED && type
70
70
 
71
71
  class AESDecryptionStream extends TransformStream {
72
72
 
73
- constructor({ password, rawPassword, signed, encryptionStrength, checkPasswordOnly }) {
73
+ constructor({ password, rawPassword, encryptionStrength, checkPasswordOnly }) {
74
74
  super({
75
75
  start() {
76
76
  Object.assign(this, {
77
77
  ready: new Promise(resolve => this.resolveReady = resolve),
78
78
  password: encodePassword(password, rawPassword),
79
- signed,
80
79
  strength: encryptionStrength - 1,
81
80
  pending: new Uint8Array()
82
81
  });
@@ -105,7 +104,6 @@ class AESDecryptionStream extends TransformStream {
105
104
  },
106
105
  async flush(controller) {
107
106
  const {
108
- signed,
109
107
  ctr,
110
108
  hmac,
111
109
  pending,
@@ -122,13 +120,15 @@ class AESDecryptionStream extends TransformStream {
122
120
  const decryptedChunk = ctr.update(encryptedChunk);
123
121
  decryptedChunkArray = fromBits(codecBytes, decryptedChunk);
124
122
  }
125
- if (signed) {
126
- const signature = subarray(fromBits(codecBytes, hmac.digest()), 0, SIGNATURE_LENGTH);
127
- for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
128
- if (signature[indexSignature] != originalSignature[indexSignature]) {
129
- throw new Error(ERR_INVALID_SIGNATURE);
130
- }
131
- }
123
+ const signature = subarray(fromBits(codecBytes, hmac.digest()), 0, SIGNATURE_LENGTH);
124
+ // branch-free comparison: accumulate the differences instead of
125
+ // short-circuiting on the first mismatching byte
126
+ let invalidSignature = pending.length < SIGNATURE_LENGTH ? 1 : 0;
127
+ for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
128
+ invalidSignature |= signature[indexSignature] ^ originalSignature[indexSignature];
129
+ }
130
+ if (invalidSignature) {
131
+ throw new Error(ERR_INVALID_SIGNATURE);
132
132
  }
133
133
  controller.enqueue(decryptedChunkArray);
134
134
  }
@@ -206,11 +206,11 @@ function append(aesCrypto, input, output, paddingStart, paddingEnd, verifySignat
206
206
  hmac,
207
207
  pending
208
208
  } = aesCrypto;
209
- const inputLength = input.length - paddingEnd;
210
209
  if (pending.length) {
211
210
  input = concat(pending, input);
212
- output = expand(output, inputLength - (inputLength % BLOCK_LENGTH));
213
211
  }
212
+ const inputLength = input.length - paddingEnd;
213
+ output = expand(output, paddingStart + (inputLength - (inputLength % BLOCK_LENGTH)));
214
214
  let offset;
215
215
  for (offset = 0; offset <= inputLength - BLOCK_LENGTH; offset += BLOCK_LENGTH) {
216
216
  const inputChunk = toBits(codecBytes, subarray(input, offset, offset + BLOCK_LENGTH));