@zip.js/zip.js 2.8.29 → 2.8.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/BENCHMARKS.md +35 -0
  2. package/deno.json +5 -4
  3. package/dist/zip-core.js +496 -218
  4. package/dist/zip-core.min.js +1 -1
  5. package/dist/zip-fs-core.js +398 -195
  6. package/dist/zip-fs-core.min.js +1 -1
  7. package/dist/zip-fs-native.js +588 -254
  8. package/dist/zip-fs-native.min.js +1 -1
  9. package/dist/zip-fs.js +587 -253
  10. package/dist/zip-fs.min.js +1 -1
  11. package/dist/zip-legacy.js +501 -223
  12. package/dist/zip-legacy.min.js +1 -1
  13. package/dist/zip-module.wasm +0 -0
  14. package/dist/zip-native.js +504 -226
  15. package/dist/zip-native.min.js +1 -1
  16. package/dist/zip-web-worker-native.js +1 -1
  17. package/dist/zip-web-worker.js +1 -1
  18. package/dist/zip.js +503 -225
  19. package/dist/zip.min.js +1 -1
  20. package/index-native.cjs +588 -254
  21. package/index-native.min.js +1 -1
  22. package/index.cjs +587 -253
  23. package/index.d.cts +2814 -0
  24. package/index.d.ts +364 -1
  25. package/index.min.js +1 -1
  26. package/lib/core/codec-worker.js +0 -10
  27. package/lib/core/configuration.js +21 -33
  28. package/lib/core/constants.js +3 -0
  29. package/lib/core/io.js +11 -19
  30. package/lib/core/options.js +11 -1
  31. package/lib/core/streams/aes-crypto-stream.js +16 -20
  32. package/lib/core/streams/codecs/crc32.js +39 -11
  33. package/lib/core/streams/common-crypto.js +0 -2
  34. package/lib/core/streams/zip-crypto-stream.js +12 -16
  35. package/lib/core/streams/zip-entry-stream.js +69 -19
  36. package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
  37. package/lib/core/streams/zlib-wasm/zlib-streams-loader.js +1 -3
  38. package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
  39. package/lib/core/util/base64.js +68 -0
  40. package/lib/core/util/decode-text.js +0 -1
  41. package/lib/core/util/default-mime-type.js +0 -3
  42. package/lib/core/util/inflate.js +194 -0
  43. package/lib/core/util/mime-type.js +26 -22
  44. package/lib/core/util/opfs-temp-stream.js +139 -0
  45. package/lib/core/web-worker-inline-native.js +1 -1
  46. package/lib/core/web-worker-inline-template-native.js +5 -5
  47. package/lib/core/web-worker-inline-wasm.js +1 -1
  48. package/lib/core/zip-fs.js +61 -10
  49. package/lib/core/zip-reader.js +162 -53
  50. package/lib/core/zip-writer.js +34 -41
  51. package/lib/core/zlib-streams-inline-template.js +3 -2
  52. package/lib/core/zlib-streams-inline.js +1 -1
  53. package/lib/zip-core-base.js +7 -2
  54. package/package.json +9 -6
  55. package/reserved-property-names.js +50 -0
  56. package/lib/core/util/mini-lz.js +0 -198
@@ -194,9 +194,6 @@ async function runWorker({ options, readable, writable, onTaskFinished }, config
194
194
  try {
195
195
  await initModule(config);
196
196
  } catch {
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
197
  const ZlibStream = options.codecType.startsWith(CODEC_DEFLATE) ?
201
198
  config.CompressionStreamZlib :
202
199
  config.DecompressionStreamZlib;
@@ -274,7 +271,6 @@ async function runWebWorker(workerData, config) {
274
271
  return resultValue;
275
272
  } catch (error) {
276
273
  await closeWritable();
277
- // unblock the pipe watcher if the worker died before closing its end of the stream
278
274
  abortPipe();
279
275
  try {
280
276
  await closed;
@@ -299,8 +295,6 @@ function watchClosedStream(writableSource) {
299
295
  const abortController = new AbortController();
300
296
  const { writable, readable } = new TransformStream();
301
297
  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
298
  closed.catch(() => { });
305
299
  return { writable, closed, abortPipe: () => abortController.abort() };
306
300
  }
@@ -393,8 +387,6 @@ function onWorkerError(event, workerData) {
393
387
  event.preventDefault();
394
388
  }
395
389
  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
390
  terminateWorker(workerData);
399
391
  if (rejectResult) {
400
392
  rejectResult(event.error || new Error(event.message || ERROR_EVENT_TYPE));
@@ -446,8 +438,6 @@ function sendMessage(message, { worker, writer, transferStreams }) {
446
438
  async function onMessage({ data }, workerData) {
447
439
  const { type, value, messageId, result, error } = data;
448
440
  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
441
  const stale = () => workerData.generation != generation;
452
442
  try {
453
443
  if (error) {
@@ -55,6 +55,22 @@ const DEFAULT_CONFIGURATION = {
55
55
  DecompressionStream: typeof DecompressionStream != UNDEFINED_TYPE && DecompressionStream
56
56
  };
57
57
 
58
+ const CONFIGURABLE_PROPERTY_NAMES = [
59
+ "baseURI",
60
+ "wasmURI",
61
+ "workerURI",
62
+ "chunkSize",
63
+ "maxWorkers",
64
+ "terminateWorkerTimeout",
65
+ "workerStarvationTimeout",
66
+ "useCompressionStream",
67
+ "useWebWorkers",
68
+ "CompressionStream",
69
+ "DecompressionStream",
70
+ "CompressionStreamZlib",
71
+ "DecompressionStreamZlib"
72
+ ];
73
+
58
74
  const config = Object.assign({}, DEFAULT_CONFIGURATION);
59
75
 
60
76
  export {
@@ -72,38 +88,10 @@ function getChunkSize(config) {
72
88
  }
73
89
 
74
90
  function configure(configuration) {
75
- const {
76
- baseURI,
77
- chunkSize,
78
- maxWorkers,
79
- terminateWorkerTimeout,
80
- workerStarvationTimeout,
81
- useCompressionStream,
82
- useWebWorkers,
83
- CompressionStream,
84
- DecompressionStream,
85
- CompressionStreamZlib,
86
- DecompressionStreamZlib,
87
- workerURI,
88
- wasmURI
89
- } = configuration;
90
- setIfDefined("baseURI", baseURI);
91
- setIfDefined("wasmURI", wasmURI);
92
- setIfDefined("workerURI", workerURI);
93
- setIfDefined("chunkSize", chunkSize);
94
- setIfDefined("maxWorkers", maxWorkers);
95
- setIfDefined("terminateWorkerTimeout", terminateWorkerTimeout);
96
- setIfDefined("workerStarvationTimeout", workerStarvationTimeout);
97
- setIfDefined("useCompressionStream", useCompressionStream);
98
- setIfDefined("useWebWorkers", useWebWorkers);
99
- setIfDefined("CompressionStream", CompressionStream);
100
- setIfDefined("DecompressionStream", DecompressionStream);
101
- setIfDefined("CompressionStreamZlib", CompressionStreamZlib);
102
- setIfDefined("DecompressionStreamZlib", DecompressionStreamZlib);
103
- }
104
-
105
- function setIfDefined(propertyName, propertyValue) {
106
- if (propertyValue !== UNDEFINED_VALUE) {
107
- config[propertyName] = propertyValue;
91
+ for (const propertyName of CONFIGURABLE_PROPERTY_NAMES) {
92
+ const propertyValue = configuration[propertyName];
93
+ if (propertyValue !== UNDEFINED_VALUE) {
94
+ config[propertyName] = propertyValue;
95
+ }
108
96
  }
109
97
  }
@@ -104,7 +104,10 @@ const UNDEFINED_TYPE = "undefined";
104
104
  const FUNCTION_TYPE = "function";
105
105
  const OBJECT_TYPE = "object";
106
106
 
107
+ const EMPTY_UINT8_ARRAY = new Uint8Array();
108
+
107
109
  export {
110
+ EMPTY_UINT8_ARRAY,
108
111
  MAX_32_BITS,
109
112
  MAX_16_BITS,
110
113
  MAX_8_BITS,
package/lib/core/io.js CHANGED
@@ -33,7 +33,8 @@ import {
33
33
  UNDEFINED_VALUE,
34
34
  INFINITY_VALUE,
35
35
  FUNCTION_TYPE,
36
- END_OF_CENTRAL_DIR_LENGTH
36
+ END_OF_CENTRAL_DIR_LENGTH,
37
+ EMPTY_UINT8_ARRAY
37
38
  } from "./constants.js";
38
39
  import { getConfiguration } from "./configuration.js";
39
40
 
@@ -71,26 +72,24 @@ class Stream {
71
72
  class Reader extends Stream {
72
73
 
73
74
  get readable() {
75
+ return this.createReadable();
76
+ }
77
+
78
+ createReadable({ offset = 0, size, diskNumberStart, chunkSize = DEFAULT_CHUNK_SIZE } = {}) {
74
79
  const reader = this;
75
- const { chunkSize = DEFAULT_CHUNK_SIZE } = reader;
76
- const readable = new ReadableStream({
77
- start() {
78
- this.chunkOffset = 0;
79
- },
80
+ let chunkOffset = 0;
81
+ return new ReadableStream({
80
82
  async pull(controller) {
81
- const { offset = 0, size, diskNumberStart } = readable;
82
- const { chunkOffset } = this;
83
83
  const dataSize = size === UNDEFINED_VALUE ? chunkSize : Math.min(chunkSize, size - chunkOffset);
84
84
  const data = await readUint8Array(reader, offset + chunkOffset, dataSize, diskNumberStart);
85
85
  controller.enqueue(data);
86
86
  if ((chunkOffset + chunkSize > size) || (size === UNDEFINED_VALUE && !data.length && dataSize)) {
87
87
  controller.close();
88
88
  } else {
89
- this.chunkOffset += chunkSize;
89
+ chunkOffset += chunkSize;
90
90
  }
91
91
  }
92
92
  });
93
- return readable;
94
93
  }
95
94
  }
96
95
 
@@ -229,8 +228,6 @@ class BlobWriter extends Stream {
229
228
  }
230
229
  });
231
230
  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
231
  writer.blob.catch(() => { });
235
232
  }
236
233
 
@@ -392,7 +389,7 @@ async function readUint8ArrayHttpReader(httpReader, index, length, sendRequest,
392
389
  return eocdCache;
393
390
  }
394
391
  if (index >= size || length === 0) {
395
- return new Uint8Array();
392
+ return EMPTY_UINT8_ARRAY;
396
393
  } else {
397
394
  if (index + length > size) {
398
395
  length = size - index;
@@ -449,8 +446,6 @@ async function getXMLHttpRequestData(httpReader) {
449
446
  async function getRequestData(httpReader, sendRequest) {
450
447
  const response = await sendRequest(HTTP_METHOD_GET, httpReader, getHeaders(httpReader));
451
448
  httpReader.data = new Uint8Array(await response.arrayBuffer());
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
449
  httpReader.size = httpReader.data.length;
455
450
  }
456
451
 
@@ -515,7 +510,6 @@ class HttpReader extends Reader {
515
510
  super();
516
511
  Object.assign(this, {
517
512
  url,
518
- // an explicit fetch implementation takes precedence over useXHR
519
513
  reader: options.useXHR && !options.fetch ? new XHRReader(url, options) : new FetchReader(url, options)
520
514
  });
521
515
  }
@@ -654,7 +648,7 @@ class SplitDataReader extends Reader {
654
648
  }
655
649
  }
656
650
  } else {
657
- result = new Uint8Array();
651
+ result = EMPTY_UINT8_ARRAY;
658
652
  }
659
653
  reader.lastDiskNumber = Math.max(currentDiskNumber, reader.lastDiskNumber);
660
654
  return result;
@@ -699,8 +693,6 @@ class SplitDataWriter extends Stream {
699
693
  writer.diskOffset += diskSourceWriter.size;
700
694
  writer.diskNumber++;
701
695
  diskWriter = null;
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
696
  writer.availableSize = writer.maxSize;
705
697
  if (chunk.length > availableSize) {
706
698
  await this.write(chunk.subarray(availableSize));
@@ -58,6 +58,11 @@ const OPTION_ENCODE_TEXT = "encodeText";
58
58
  const OPTION_OFFSET = "offset";
59
59
  const OPTION_USDZ = "usdz";
60
60
  const OPTION_UNIX_EXTRA_FIELD_TYPE = "unixExtraFieldType";
61
+ const OPTION_STRICTNESS = "strictness";
62
+ const OPTION_MAX_APPENDED_DATA_SIZE = "maxAppendedDataSize";
63
+ const STRICTNESS_STRICT = "strict";
64
+ const STRICTNESS_BALANCED = "balanced";
65
+ const STRICTNESS_TOLERANT = "tolerant";
61
66
 
62
67
  export {
63
68
  OPTION_FILENAME_ENCODING,
@@ -91,5 +96,10 @@ export {
91
96
  OPTION_ENCODE_TEXT,
92
97
  OPTION_OFFSET,
93
98
  OPTION_USDZ,
94
- OPTION_UNIX_EXTRA_FIELD_TYPE
99
+ OPTION_UNIX_EXTRA_FIELD_TYPE,
100
+ OPTION_STRICTNESS,
101
+ OPTION_MAX_APPENDED_DATA_SIZE,
102
+ STRICTNESS_STRICT,
103
+ STRICTNESS_BALANCED,
104
+ STRICTNESS_TOLERANT
95
105
  };
@@ -29,7 +29,7 @@
29
29
  /* global crypto, TransformStream */
30
30
  // deno-lint-ignore-file no-this-alias
31
31
 
32
- import { UNDEFINED_VALUE, UNDEFINED_TYPE, FUNCTION_TYPE } from "../constants.js";
32
+ import { UNDEFINED_VALUE, UNDEFINED_TYPE, FUNCTION_TYPE, EMPTY_UINT8_ARRAY } from "../constants.js";
33
33
  import { encodeText } from "./../util/encode-text.js";
34
34
  import {
35
35
  cipher,
@@ -73,12 +73,7 @@ class AESDecryptionStream extends TransformStream {
73
73
  constructor({ password, rawPassword, encryptionStrength, checkPasswordOnly }) {
74
74
  super({
75
75
  start() {
76
- Object.assign(this, {
77
- ready: new Promise(resolve => this.resolveReady = resolve),
78
- password: encodePassword(password, rawPassword),
79
- strength: encryptionStrength - 1,
80
- pending: new Uint8Array()
81
- });
76
+ initAesCrypto(this, password, rawPassword, encryptionStrength);
82
77
  },
83
78
  async transform(chunk, controller) {
84
79
  const aesCrypto = this;
@@ -113,7 +108,7 @@ class AESDecryptionStream extends TransformStream {
113
108
  await ready;
114
109
  const chunkToDecrypt = subarray(pending, 0, pending.length - SIGNATURE_LENGTH);
115
110
  const originalSignature = subarray(pending, pending.length - SIGNATURE_LENGTH);
116
- let decryptedChunkArray = new Uint8Array();
111
+ let decryptedChunkArray = EMPTY_UINT8_ARRAY;
117
112
  if (chunkToDecrypt.length) {
118
113
  const encryptedChunk = toBits(codecBytes, chunkToDecrypt);
119
114
  hmac.update(encryptedChunk);
@@ -121,8 +116,6 @@ class AESDecryptionStream extends TransformStream {
121
116
  decryptedChunkArray = fromBits(codecBytes, decryptedChunk);
122
117
  }
123
118
  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
119
  let invalidSignature = pending.length < SIGNATURE_LENGTH ? 1 : 0;
127
120
  for (let indexSignature = 0; indexSignature < SIGNATURE_LENGTH; indexSignature++) {
128
121
  invalidSignature |= signature[indexSignature] ^ originalSignature[indexSignature];
@@ -144,12 +137,7 @@ class AESEncryptionStream extends TransformStream {
144
137
  let stream;
145
138
  super({
146
139
  start() {
147
- Object.assign(this, {
148
- ready: new Promise(resolve => this.resolveReady = resolve),
149
- password: encodePassword(password, rawPassword),
150
- strength: encryptionStrength - 1,
151
- pending: new Uint8Array()
152
- });
140
+ initAesCrypto(this, password, rawPassword, encryptionStrength);
153
141
  },
154
142
  async transform(chunk, controller) {
155
143
  const aesCrypto = this;
@@ -159,7 +147,7 @@ class AESEncryptionStream extends TransformStream {
159
147
  resolveReady,
160
148
  ready
161
149
  } = aesCrypto;
162
- let preamble = new Uint8Array();
150
+ let preamble = EMPTY_UINT8_ARRAY;
163
151
  if (password) {
164
152
  preamble = await createEncryptionKeys(aesCrypto, strength, password);
165
153
  resolveReady();
@@ -179,7 +167,7 @@ class AESEncryptionStream extends TransformStream {
179
167
  } = this;
180
168
  if (hmac && ctr) {
181
169
  await ready;
182
- let encryptedChunkArray = new Uint8Array();
170
+ let encryptedChunkArray = EMPTY_UINT8_ARRAY;
183
171
  if (pending.length) {
184
172
  const encryptedChunk = ctr.update(toBits(codecBytes, pending));
185
173
  hmac.update(encryptedChunk);
@@ -196,10 +184,18 @@ class AESEncryptionStream extends TransformStream {
196
184
 
197
185
  export {
198
186
  AESDecryptionStream,
199
- AESEncryptionStream,
200
- ERR_INVALID_PASSWORD
187
+ AESEncryptionStream
201
188
  };
202
189
 
190
+ function initAesCrypto(aesCrypto, password, rawPassword, encryptionStrength) {
191
+ Object.assign(aesCrypto, {
192
+ ready: new Promise(resolve => aesCrypto.resolveReady = resolve),
193
+ password: encodePassword(password, rawPassword),
194
+ strength: encryptionStrength - 1,
195
+ pending: EMPTY_UINT8_ARRAY
196
+ });
197
+ }
198
+
203
199
  function append(aesCrypto, input, output, paddingStart, paddingEnd, verifySignature) {
204
200
  const {
205
201
  ctr,
@@ -26,18 +26,30 @@
26
26
  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
 
29
- const table = [];
30
- for (let i = 0; i < 256; i++) {
31
- let t = i;
29
+ // Slicing-by-8 CRC-32 (Intel / zlib). The eight 256-entry tables let the inner loop
30
+ // consume 8 bytes per iteration with a shorter dependency chain, ~4x the byte-at-a-time
31
+ // rate (measured ~320 -> ~1400 MB/s on 64KB chunks).
32
+ //
33
+ // Every table MUST stay a PACKED_SMI array: build with array literals (not `new Array(n)`,
34
+ // which is HOLEY) and store the signed int32 XOR result (no `>>> 0`). An unsigned or holey
35
+ // table becomes a V8 FixedDoubleArray whose every hot-loop lookup unboxes a double (~1.6x
36
+ // slower). Signedness is irrelevant to the result — the reads mask/shift it and the final
37
+ // `~crc` normalizes it. Do NOT reintroduce `>>> 0` here or switch to `new Array(256)`.
38
+ const T = [[], [], [], [], [], [], [], []];
39
+ for (let n = 0; n < 256; n++) {
40
+ let t = n;
32
41
  for (let j = 0; j < 8; j++) {
33
- if (t & 1) {
34
- t = (t >>> 1) ^ 0xEDB88320;
35
- } else {
36
- t = t >>> 1;
37
- }
42
+ t = (t & 1) ? (t >>> 1) ^ 0xEDB88320 : t >>> 1;
43
+ }
44
+ T[0][n] = t;
45
+ }
46
+ for (let n = 0; n < 256; n++) {
47
+ for (let k = 1; k < 8; k++) {
48
+ const previous = T[k - 1][n];
49
+ T[k][n] = (previous >>> 8) ^ T[0][previous & 0xFF];
38
50
  }
39
- table[i] = t;
40
51
  }
52
+ const [T0, T1, T2, T3, T4, T5, T6, T7] = T;
41
53
 
42
54
  class Crc32 {
43
55
 
@@ -47,8 +59,24 @@ class Crc32 {
47
59
 
48
60
  append(data) {
49
61
  let crc = this.crc | 0;
50
- for (let offset = 0, length = data.length | 0; offset < length; offset++) {
51
- crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
62
+ const length = data.length | 0;
63
+ let offset = 0;
64
+ // Process 8 bytes per iteration over the typed-array body. DataView.getInt32(le)
65
+ // reads an unaligned little-endian word as a signed int32 (no double boxing), so no
66
+ // alignment or endianness handling is needed; data.buffer guards non-typed inputs.
67
+ if (length >= 8 && data.buffer) {
68
+ const view = new DataView(data.buffer, data.byteOffset, length);
69
+ const end = length - 8;
70
+ for (; offset <= end; offset += 8) {
71
+ const a = crc ^ view.getInt32(offset, true);
72
+ const b = view.getInt32(offset + 4, true);
73
+ crc = T7[a & 0xFF] ^ T6[(a >>> 8) & 0xFF] ^ T5[(a >>> 16) & 0xFF] ^ T4[(a >>> 24) & 0xFF] ^
74
+ T3[b & 0xFF] ^ T2[(b >>> 8) & 0xFF] ^ T1[(b >>> 16) & 0xFF] ^ T0[(b >>> 24) & 0xFF];
75
+ }
76
+ }
77
+ // Remaining tail (and non-typed inputs) byte-at-a-time with the base table.
78
+ for (; offset < length; offset++) {
79
+ crc = (crc >>> 8) ^ T0[(crc ^ data[offset]) & 0xFF];
52
80
  }
53
81
  this.crc = crc;
54
82
  }
@@ -48,8 +48,6 @@ function getRandomValues(array) {
48
48
  if (GET_RANDOM_VALUES_SUPPORTED) {
49
49
  return crypto.getRandomValues(array);
50
50
  } else {
51
- // all supported runtimes expose crypto.getRandomValues; failing hard beats
52
- // silently generating predictable encryption salts
53
51
  throw new Error(ERR_UNSUPPORTED_CRYPTO_API);
54
52
  }
55
53
  }
@@ -43,20 +43,13 @@ class ZipCryptoDecryptionStream extends TransformStream {
43
43
  constructor({ password, rawPassword, passwordVerification, checkPasswordOnly }) {
44
44
  super({
45
45
  start() {
46
- Object.assign(this, {
47
- password,
48
- rawPassword,
49
- passwordVerification
50
- });
51
- createKeys(this, password, rawPassword);
46
+ initZipCrypto(this, password, rawPassword, passwordVerification);
52
47
  },
53
48
  transform(chunk, controller) {
54
49
  const zipCrypto = this;
55
50
  if (zipCrypto.password || zipCrypto.rawPassword) {
56
51
  const decryptedHeader = decrypt(zipCrypto, chunk.subarray(0, HEADER_LENGTH));
57
52
  zipCrypto.password = zipCrypto.rawPassword = null;
58
- // verify the password with a branch-free comparison of the verification byte, mirroring
59
- // the constant-time AES HMAC signature check
60
53
  if ((decryptedHeader.at(-1) ^ zipCrypto.passwordVerification) != 0) {
61
54
  throw new Error(ERR_INVALID_PASSWORD);
62
55
  }
@@ -77,12 +70,7 @@ class ZipCryptoEncryptionStream extends TransformStream {
77
70
  constructor({ password, rawPassword, passwordVerification }) {
78
71
  super({
79
72
  start() {
80
- Object.assign(this, {
81
- password,
82
- rawPassword,
83
- passwordVerification
84
- });
85
- createKeys(this, password, rawPassword);
73
+ initZipCrypto(this, password, rawPassword, passwordVerification);
86
74
  },
87
75
  transform(chunk, controller) {
88
76
  const zipCrypto = this;
@@ -108,10 +96,18 @@ class ZipCryptoEncryptionStream extends TransformStream {
108
96
 
109
97
  export {
110
98
  ZipCryptoDecryptionStream,
111
- ZipCryptoEncryptionStream,
112
- ERR_INVALID_PASSWORD
99
+ ZipCryptoEncryptionStream
113
100
  };
114
101
 
102
+ function initZipCrypto(zipCrypto, password, rawPassword, passwordVerification) {
103
+ Object.assign(zipCrypto, {
104
+ password,
105
+ rawPassword,
106
+ passwordVerification
107
+ });
108
+ createKeys(zipCrypto, password, rawPassword);
109
+ }
110
+
115
111
  function decrypt(target, input) {
116
112
  const output = new Uint8Array(input.length);
117
113
  for (let index = 0; index < input.length; index++) {
@@ -48,21 +48,32 @@ const ERR_INVALID_UNCOMPRESSED_SIZE = "Invalid uncompressed size";
48
48
  const ERR_INVALID_COMPRESSED_DATA = "Invalid compressed data";
49
49
  const FORMAT_DEFLATE_RAW = "deflate-raw";
50
50
  const FORMAT_DEFLATE64_RAW = "deflate64-raw";
51
+ const FORMAT_GZIP = "gzip";
52
+ const GZIP_HEADER_LENGTH = 10;
53
+ const GZIP_TRAILER_LENGTH = 8;
51
54
 
52
55
  class DeflateStream extends TransformStream {
53
56
 
54
57
  constructor(options, { chunkSize, CompressionStreamZlib, CompressionStream }) {
55
58
  super({});
56
- const { compressed, encrypted, useCompressionStream, zipCrypto, signed, level } = options;
59
+ const { compressed, encrypted, useCompressionStream, zipCrypto, signed, level, deflate64 } = options;
57
60
  const stream = this;
58
- let crc32Stream, encryptionStream;
61
+ let crc32Stream, encryptionStream, gzipCrc32Stream;
59
62
  let readable = super.readable;
60
- if ((!encrypted || zipCrypto) && signed) {
63
+ const useGzipCrc32 = signed && compressed && !deflate64 && (!encrypted || zipCrypto) &&
64
+ Boolean(useCompressionStream && CompressionStream);
65
+ if ((!encrypted || zipCrypto) && signed && !useGzipCrc32) {
61
66
  crc32Stream = new Crc32Stream();
62
67
  readable = pipeThrough(readable, crc32Stream);
63
68
  }
64
69
  if (compressed) {
65
- readable = pipeThroughCommpressionStream(readable, useCompressionStream, { level, chunkSize }, CompressionStream, CompressionStreamZlib, CompressionStream);
70
+ if (useGzipCrc32) {
71
+ gzipCrc32Stream = new GzipToRawDeflateStream();
72
+ readable = pipeThroughBackpressured(readable, new CompressionStream(FORMAT_GZIP));
73
+ readable = pipeThrough(readable, gzipCrc32Stream);
74
+ } else {
75
+ readable = pipeThroughCommpressionStream(readable, useCompressionStream, { level, chunkSize }, CompressionStream, CompressionStreamZlib, CompressionStream);
76
+ }
66
77
  }
67
78
  if (encrypted) {
68
79
  if (zipCrypto) {
@@ -78,13 +89,64 @@ class DeflateStream extends TransformStream {
78
89
  signature = encryptionStream.signature;
79
90
  }
80
91
  if ((!encrypted || zipCrypto) && signed) {
81
- signature = new DataView(crc32Stream.value.buffer).getUint32(0);
92
+ signature = useGzipCrc32 ? gzipCrc32Stream.signature : new DataView(crc32Stream.value.buffer).getUint32(0);
82
93
  }
83
94
  stream.signature = signature;
84
95
  });
85
96
  }
86
97
  }
87
98
 
99
+ class GzipToRawDeflateStream extends TransformStream {
100
+
101
+ constructor() {
102
+ // deno-lint-ignore prefer-const
103
+ let stream;
104
+ let headerLeft = GZIP_HEADER_LENGTH;
105
+ let tail = new Uint8Array(0);
106
+ super({
107
+ transform(chunk, controller) {
108
+ if (headerLeft) {
109
+ const dropped = Math.min(headerLeft, chunk.length);
110
+ headerLeft -= dropped;
111
+ chunk = chunk.subarray(dropped);
112
+ if (!chunk.length) {
113
+ return;
114
+ }
115
+ }
116
+ const available = tail.length + chunk.length;
117
+ if (available <= GZIP_TRAILER_LENGTH) {
118
+ const pending = new Uint8Array(available);
119
+ pending.set(tail);
120
+ pending.set(chunk, tail.length);
121
+ tail = pending;
122
+ return;
123
+ }
124
+ const emitLength = available - GZIP_TRAILER_LENGTH;
125
+ const output = new Uint8Array(emitLength);
126
+ const fromTail = Math.min(emitLength, tail.length);
127
+ output.set(tail.subarray(0, fromTail), 0);
128
+ if (emitLength > fromTail) {
129
+ output.set(chunk.subarray(0, emitLength - fromTail), fromTail);
130
+ }
131
+ controller.enqueue(output);
132
+ const nextTail = new Uint8Array(GZIP_TRAILER_LENGTH);
133
+ const tailRemaining = tail.length - fromTail;
134
+ if (tailRemaining) {
135
+ nextTail.set(tail.subarray(fromTail), 0);
136
+ }
137
+ nextTail.set(chunk.subarray(emitLength - fromTail), tailRemaining);
138
+ tail = nextTail;
139
+ },
140
+ flush() {
141
+ const dataView = new DataView(tail.buffer, tail.byteOffset, tail.byteLength);
142
+ stream.signature = dataView.getUint32(0, true);
143
+ stream.uncompressedSize = dataView.getUint32(4, true);
144
+ }
145
+ });
146
+ stream = this;
147
+ }
148
+ }
149
+
88
150
  class InflateStream extends TransformStream {
89
151
 
90
152
  constructor(options, { chunkSize, DecompressionStreamZlib, DecompressionStream }) {
@@ -157,11 +219,6 @@ function pipeThroughCommpressionStream(readable, useCompressionStream, options,
157
219
  throw error;
158
220
  }
159
221
  }
160
- // The native CompressionStream/DecompressionStream and the pure-JS zlib port do not signal
161
- // backpressure on their writable side (their `ready` never pends), so a plain pipeThrough drains
162
- // the whole source into them and peak memory grows with the entry size. Awaiting each write()
163
- // paces the source to the codec's consumption rate, keeping streaming memory bounded. The WASM
164
- // codec already backpressures, so this is a no-op cost for it.
165
222
  return pipeThroughBackpressured(readable, codecStream);
166
223
  }
167
224
 
@@ -169,10 +226,6 @@ function pipeThrough(readable, transformStream) {
169
226
  return readable.pipeThrough(transformStream);
170
227
  }
171
228
 
172
- // Like readable.pipeThrough(transformStream), but drives the writable side manually and awaits each
173
- // write() so that a codec which under-reports backpressure cannot pull the whole source into memory.
174
- // Errors propagate in both directions: a source error aborts the codec; a downstream cancel/error
175
- // (surfaced through the codec's writable rejecting) cancels the source.
176
229
  function pipeThroughBackpressured(readable, transformStream) {
177
230
  const writer = transformStream.writable.getWriter();
178
231
  const reader = readable.getReader();
@@ -201,7 +254,7 @@ async function abort(writer, error) {
201
254
  try {
202
255
  await writer.abort(error);
203
256
  } catch {
204
- // the writable may already be errored/closed
257
+ // ignored: the writable may already be errored/closed
205
258
  }
206
259
  }
207
260
 
@@ -209,13 +262,10 @@ async function cancel(reader, error) {
209
262
  try {
210
263
  await reader.cancel(error);
211
264
  } catch {
212
- // the readable may already be errored/closed
265
+ // ignored: the readable may already be errored/closed
213
266
  }
214
267
  }
215
268
 
216
- // DecompressionStream implementations can fail with a message-less TypeError on malformed
217
- // input; give these errors an identifiable message (errors raised by the other stages of
218
- // the pipeline, e.g. ERR_INVALID_PASSWORD, always carry a message and pass through as-is)
219
269
  function mapInflateStreamError(readable) {
220
270
  const reader = readable.getReader();
221
271
  return new ReadableStream({