@zip.js/zip.js 2.8.29 → 2.8.30
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.
- package/BENCHMARKS.md +35 -0
- package/deno.json +1 -1
- package/dist/zip-core.js +527 -48
- package/dist/zip-core.min.js +1 -1
- package/dist/zip-fs-core.js +413 -48
- package/dist/zip-fs-core.min.js +1 -1
- package/dist/zip-fs-native.js +589 -52
- package/dist/zip-fs-native.min.js +1 -1
- package/dist/zip-fs.js +587 -50
- package/dist/zip-fs.min.js +1 -1
- package/dist/zip-legacy.js +528 -49
- package/dist/zip-legacy.min.js +1 -1
- package/dist/zip-module.wasm +0 -0
- package/dist/zip-native.js +531 -52
- package/dist/zip-native.min.js +1 -1
- package/dist/zip-web-worker-native.js +1 -1
- package/dist/zip-web-worker.js +1 -1
- package/dist/zip.js +529 -50
- package/dist/zip.min.js +1 -1
- package/index-native.cjs +589 -52
- package/index-native.min.js +1 -1
- package/index.cjs +587 -50
- package/index.d.ts +126 -1
- package/index.min.js +1 -1
- package/lib/core/options.js +11 -1
- package/lib/core/streams/codecs/crc32.js +39 -11
- package/lib/core/streams/zip-entry-stream.js +85 -5
- package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
- package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
- package/lib/core/util/opfs-temp-stream.js +175 -0
- package/lib/core/web-worker-inline-native.js +1 -1
- package/lib/core/web-worker-inline-wasm.js +1 -1
- package/lib/core/zip-fs.js +58 -0
- package/lib/core/zip-reader.js +219 -31
- package/lib/core/zip-writer.js +11 -0
- package/lib/core/zlib-streams-inline.js +1 -1
- package/lib/zip-core-base.js +4 -1
- package/package.json +1 -1
package/dist/zip-fs-core.js
CHANGED
|
@@ -988,18 +988,30 @@
|
|
|
988
988
|
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
989
989
|
*/
|
|
990
990
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
991
|
+
// Slicing-by-8 CRC-32 (Intel / zlib). The eight 256-entry tables let the inner loop
|
|
992
|
+
// consume 8 bytes per iteration with a shorter dependency chain, ~4x the byte-at-a-time
|
|
993
|
+
// rate (measured ~320 -> ~1400 MB/s on 64KB chunks).
|
|
994
|
+
//
|
|
995
|
+
// Every table MUST stay a PACKED_SMI array: build with array literals (not `new Array(n)`,
|
|
996
|
+
// which is HOLEY) and store the signed int32 XOR result (no `>>> 0`). An unsigned or holey
|
|
997
|
+
// table becomes a V8 FixedDoubleArray whose every hot-loop lookup unboxes a double (~1.6x
|
|
998
|
+
// slower). Signedness is irrelevant to the result — the reads mask/shift it and the final
|
|
999
|
+
// `~crc` normalizes it. Do NOT reintroduce `>>> 0` here or switch to `new Array(256)`.
|
|
1000
|
+
const T = [[], [], [], [], [], [], [], []];
|
|
1001
|
+
for (let n = 0; n < 256; n++) {
|
|
1002
|
+
let t = n;
|
|
994
1003
|
for (let j = 0; j < 8; j++) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1004
|
+
t = (t & 1) ? (t >>> 1) ^ 0xEDB88320 : t >>> 1;
|
|
1005
|
+
}
|
|
1006
|
+
T[0][n] = t;
|
|
1007
|
+
}
|
|
1008
|
+
for (let n = 0; n < 256; n++) {
|
|
1009
|
+
for (let k = 1; k < 8; k++) {
|
|
1010
|
+
const previous = T[k - 1][n];
|
|
1011
|
+
T[k][n] = (previous >>> 8) ^ T[0][previous & 0xFF];
|
|
1000
1012
|
}
|
|
1001
|
-
table[i] = t;
|
|
1002
1013
|
}
|
|
1014
|
+
const [T0, T1, T2, T3, T4, T5, T6, T7] = T;
|
|
1003
1015
|
|
|
1004
1016
|
class Crc32 {
|
|
1005
1017
|
|
|
@@ -1009,8 +1021,24 @@
|
|
|
1009
1021
|
|
|
1010
1022
|
append(data) {
|
|
1011
1023
|
let crc = this.crc | 0;
|
|
1012
|
-
|
|
1013
|
-
|
|
1024
|
+
const length = data.length | 0;
|
|
1025
|
+
let offset = 0;
|
|
1026
|
+
// Process 8 bytes per iteration over the typed-array body. DataView.getInt32(le)
|
|
1027
|
+
// reads an unaligned little-endian word as a signed int32 (no double boxing), so no
|
|
1028
|
+
// alignment or endianness handling is needed; data.buffer guards non-typed inputs.
|
|
1029
|
+
if (length >= 8 && data.buffer) {
|
|
1030
|
+
const view = new DataView(data.buffer, data.byteOffset, length);
|
|
1031
|
+
const end = length - 8;
|
|
1032
|
+
for (; offset <= end; offset += 8) {
|
|
1033
|
+
const a = crc ^ view.getInt32(offset, true);
|
|
1034
|
+
const b = view.getInt32(offset + 4, true);
|
|
1035
|
+
crc = T7[a & 0xFF] ^ T6[(a >>> 8) & 0xFF] ^ T5[(a >>> 16) & 0xFF] ^ T4[(a >>> 24) & 0xFF] ^
|
|
1036
|
+
T3[b & 0xFF] ^ T2[(b >>> 8) & 0xFF] ^ T1[(b >>> 16) & 0xFF] ^ T0[(b >>> 24) & 0xFF];
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
// Remaining tail (and non-typed inputs) byte-at-a-time with the base table.
|
|
1040
|
+
for (; offset < length; offset++) {
|
|
1041
|
+
crc = (crc >>> 8) ^ T0[(crc ^ data[offset]) & 0xFF];
|
|
1014
1042
|
}
|
|
1015
1043
|
this.crc = crc;
|
|
1016
1044
|
}
|
|
@@ -2444,21 +2472,40 @@
|
|
|
2444
2472
|
const ERR_INVALID_COMPRESSED_DATA = "Invalid compressed data";
|
|
2445
2473
|
const FORMAT_DEFLATE_RAW = "deflate-raw";
|
|
2446
2474
|
const FORMAT_DEFLATE64_RAW = "deflate64-raw";
|
|
2475
|
+
const FORMAT_GZIP = "gzip";
|
|
2476
|
+
const GZIP_HEADER_LENGTH = 10;
|
|
2477
|
+
const GZIP_TRAILER_LENGTH = 8;
|
|
2447
2478
|
|
|
2448
2479
|
class DeflateStream extends TransformStream {
|
|
2449
2480
|
|
|
2450
2481
|
constructor(options, { chunkSize, CompressionStreamZlib, CompressionStream }) {
|
|
2451
2482
|
super({});
|
|
2452
|
-
const { compressed, encrypted, useCompressionStream, zipCrypto, signed, level } = options;
|
|
2483
|
+
const { compressed, encrypted, useCompressionStream, zipCrypto, signed, level, deflate64 } = options;
|
|
2453
2484
|
const stream = this;
|
|
2454
|
-
let crc32Stream, encryptionStream;
|
|
2485
|
+
let crc32Stream, encryptionStream, gzipCrc32Stream;
|
|
2455
2486
|
let readable = super.readable;
|
|
2456
|
-
|
|
2487
|
+
// The gzip trailer carries a CRC-32 of the uncompressed data (same polynomial as zip),
|
|
2488
|
+
// computed in native code for free during compression. On the native CompressionStream,
|
|
2489
|
+
// harvest it instead of running a separate CRC pass: compress as gzip, then strip the
|
|
2490
|
+
// fixed 10-byte header and 8-byte trailer to recover the exact raw-deflate payload
|
|
2491
|
+
// (byte-identical to a deflate-raw compression) while capturing the CRC. The CRC becomes
|
|
2492
|
+
// available at end-of-stream, exactly like Crc32Stream, so nothing downstream (data
|
|
2493
|
+
// descriptor, central directory, ZipCrypto) needs it any earlier. Not applied for the
|
|
2494
|
+
// pure-JS/WASM ports (a separate slice-by-8 CRC pass is as fast or faster there).
|
|
2495
|
+
const useGzipCrc32 = signed && compressed && !deflate64 && (!encrypted || zipCrypto) &&
|
|
2496
|
+
Boolean(useCompressionStream && CompressionStream);
|
|
2497
|
+
if ((!encrypted || zipCrypto) && signed && !useGzipCrc32) {
|
|
2457
2498
|
crc32Stream = new Crc32Stream();
|
|
2458
2499
|
readable = pipeThrough(readable, crc32Stream);
|
|
2459
2500
|
}
|
|
2460
2501
|
if (compressed) {
|
|
2461
|
-
|
|
2502
|
+
if (useGzipCrc32) {
|
|
2503
|
+
gzipCrc32Stream = new GzipToRawDeflateStream();
|
|
2504
|
+
readable = pipeThroughBackpressured(readable, new CompressionStream(FORMAT_GZIP));
|
|
2505
|
+
readable = pipeThrough(readable, gzipCrc32Stream);
|
|
2506
|
+
} else {
|
|
2507
|
+
readable = pipeThroughCommpressionStream(readable, useCompressionStream, { level, chunkSize }, CompressionStream, CompressionStreamZlib, CompressionStream);
|
|
2508
|
+
}
|
|
2462
2509
|
}
|
|
2463
2510
|
if (encrypted) {
|
|
2464
2511
|
if (zipCrypto) {
|
|
@@ -2474,13 +2521,74 @@
|
|
|
2474
2521
|
signature = encryptionStream.signature;
|
|
2475
2522
|
}
|
|
2476
2523
|
if ((!encrypted || zipCrypto) && signed) {
|
|
2477
|
-
signature = new DataView(crc32Stream.value.buffer).getUint32(0);
|
|
2524
|
+
signature = useGzipCrc32 ? gzipCrc32Stream.signature : new DataView(crc32Stream.value.buffer).getUint32(0);
|
|
2478
2525
|
}
|
|
2479
2526
|
stream.signature = signature;
|
|
2480
2527
|
});
|
|
2481
2528
|
}
|
|
2482
2529
|
}
|
|
2483
2530
|
|
|
2531
|
+
// Converts a gzip stream into its raw-deflate payload while capturing the CRC-32 from the gzip
|
|
2532
|
+
// trailer. The CompressionStream gzip header is always the fixed 10-byte form (FLG=0, no optional
|
|
2533
|
+
// fields) per the Compression Streams spec, so it is stripped by length; the trailer is the last
|
|
2534
|
+
// 8 bytes (CRC-32 LE, then ISIZE LE). Bounded memory: at most GZIP_TRAILER_LENGTH bytes are held
|
|
2535
|
+
// back across chunks. The signature is read big-endian-agnostically as a number, matching the
|
|
2536
|
+
// value Crc32Stream produces, so the writer path is unchanged.
|
|
2537
|
+
class GzipToRawDeflateStream extends TransformStream {
|
|
2538
|
+
|
|
2539
|
+
constructor() {
|
|
2540
|
+
// deno-lint-ignore prefer-const
|
|
2541
|
+
let stream;
|
|
2542
|
+
let headerLeft = GZIP_HEADER_LENGTH;
|
|
2543
|
+
let tail = new Uint8Array(0);
|
|
2544
|
+
super({
|
|
2545
|
+
transform(chunk, controller) {
|
|
2546
|
+
if (headerLeft) {
|
|
2547
|
+
const dropped = Math.min(headerLeft, chunk.length);
|
|
2548
|
+
headerLeft -= dropped;
|
|
2549
|
+
chunk = chunk.subarray(dropped);
|
|
2550
|
+
if (!chunk.length) {
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const available = tail.length + chunk.length;
|
|
2555
|
+
if (available <= GZIP_TRAILER_LENGTH) {
|
|
2556
|
+
const pending = new Uint8Array(available);
|
|
2557
|
+
pending.set(tail);
|
|
2558
|
+
pending.set(chunk, tail.length);
|
|
2559
|
+
tail = pending;
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
// Emit everything except the trailing GZIP_TRAILER_LENGTH bytes as a standalone,
|
|
2563
|
+
// right-sized Uint8Array. Consumers may read chunk.buffer directly (e.g. custom
|
|
2564
|
+
// writers), so an aliased subarray of a larger buffer would leak the held-back
|
|
2565
|
+
// trailer bytes. Bytes are copied exactly once, into `output` or `tail`.
|
|
2566
|
+
const emitLength = available - GZIP_TRAILER_LENGTH;
|
|
2567
|
+
const output = new Uint8Array(emitLength);
|
|
2568
|
+
const fromTail = Math.min(emitLength, tail.length);
|
|
2569
|
+
output.set(tail.subarray(0, fromTail), 0);
|
|
2570
|
+
if (emitLength > fromTail) {
|
|
2571
|
+
output.set(chunk.subarray(0, emitLength - fromTail), fromTail);
|
|
2572
|
+
}
|
|
2573
|
+
controller.enqueue(output);
|
|
2574
|
+
const nextTail = new Uint8Array(GZIP_TRAILER_LENGTH);
|
|
2575
|
+
const tailRemaining = tail.length - fromTail;
|
|
2576
|
+
if (tailRemaining) {
|
|
2577
|
+
nextTail.set(tail.subarray(fromTail), 0);
|
|
2578
|
+
}
|
|
2579
|
+
nextTail.set(chunk.subarray(emitLength - fromTail), tailRemaining);
|
|
2580
|
+
tail = nextTail;
|
|
2581
|
+
},
|
|
2582
|
+
flush() {
|
|
2583
|
+
const dataView = new DataView(tail.buffer, tail.byteOffset, tail.byteLength);
|
|
2584
|
+
stream.signature = dataView.getUint32(0, true);
|
|
2585
|
+
stream.uncompressedSize = dataView.getUint32(4, true);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
stream = this;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2484
2592
|
class InflateStream extends TransformStream {
|
|
2485
2593
|
|
|
2486
2594
|
constructor(options, { chunkSize, DecompressionStreamZlib, DecompressionStream }) {
|
|
@@ -3641,6 +3749,11 @@
|
|
|
3641
3749
|
const OPTION_OFFSET = "offset";
|
|
3642
3750
|
const OPTION_USDZ = "usdz";
|
|
3643
3751
|
const OPTION_UNIX_EXTRA_FIELD_TYPE = "unixExtraFieldType";
|
|
3752
|
+
const OPTION_STRICTNESS = "strictness";
|
|
3753
|
+
const OPTION_MAX_APPENDED_DATA_SIZE = "maxAppendedDataSize";
|
|
3754
|
+
const STRICTNESS_STRICT = "strict";
|
|
3755
|
+
const STRICTNESS_BALANCED = "balanced";
|
|
3756
|
+
const STRICTNESS_TOLERANT = "tolerant";
|
|
3644
3757
|
|
|
3645
3758
|
/*
|
|
3646
3759
|
Copyright (c) 2025 Gildas Lormeau. All rights reserved.
|
|
@@ -3730,7 +3843,11 @@
|
|
|
3730
3843
|
throw new Error(ERR_BAD_FORMAT);
|
|
3731
3844
|
}
|
|
3732
3845
|
reader.chunkSize = getChunkSize(config);
|
|
3733
|
-
const
|
|
3846
|
+
const strictness = getStrictness(getOptionValue$1(zipReader, options, OPTION_STRICTNESS), getOptionValue$1(zipReader, options, OPTION_CHECK_AMBIGUITY));
|
|
3847
|
+
const checkAmbiguity = strictness == STRICTNESS_STRICT;
|
|
3848
|
+
const rejectAmbiguousEndOfDirectory = strictness != STRICTNESS_TOLERANT;
|
|
3849
|
+
const maxAppendedDataSize = getMaxAppendedDataSize(getOptionValue$1(zipReader, options, OPTION_MAX_APPENDED_DATA_SIZE), strictness);
|
|
3850
|
+
const { endOfDirectoryInfo, endOfDirectoryReachingEndCount } = await findEndOfCentralDirectory(reader, rejectAmbiguousEndOfDirectory, maxAppendedDataSize);
|
|
3734
3851
|
if (!endOfDirectoryInfo) {
|
|
3735
3852
|
const signatureArray = await readUint8Array(reader, 0, 4);
|
|
3736
3853
|
const signatureView = getDataView$1(signatureArray);
|
|
@@ -3740,14 +3857,18 @@
|
|
|
3740
3857
|
throw new Error(ERR_EOCDR_NOT_FOUND);
|
|
3741
3858
|
}
|
|
3742
3859
|
}
|
|
3860
|
+
// two or more end-anchored records that each dereference to a valid central directory cannot be told
|
|
3861
|
+
// apart (see comment_length ⟺ EOF): the archive is genuinely ambiguous, so refuse rather than guess
|
|
3862
|
+
if (rejectAmbiguousEndOfDirectory && endOfDirectoryReachingEndCount > 1) {
|
|
3863
|
+
throwAmbiguousArchive("multiple end of central directory records");
|
|
3864
|
+
}
|
|
3743
3865
|
const endOfDirectoryView = getDataView$1(endOfDirectoryInfo);
|
|
3744
3866
|
let directoryDataLength = getUint32(endOfDirectoryView, 12);
|
|
3745
3867
|
let directoryDataOffset = getUint32(endOfDirectoryView, 16);
|
|
3746
3868
|
const commentOffset = endOfDirectoryInfo.offset;
|
|
3747
3869
|
const commentLength = getUint16(endOfDirectoryView, 20);
|
|
3748
3870
|
const appendedDataOffset = commentOffset + END_OF_CENTRAL_DIR_LENGTH + commentLength;
|
|
3749
|
-
|
|
3750
|
-
if (checkAmbiguity && appendedDataOffset != reader.size) {
|
|
3871
|
+
if (reader.size - appendedDataOffset > maxAppendedDataSize) {
|
|
3751
3872
|
throwAmbiguousArchive("appended data");
|
|
3752
3873
|
}
|
|
3753
3874
|
let lastDiskNumber = getUint16(endOfDirectoryView, 4);
|
|
@@ -3826,14 +3947,28 @@
|
|
|
3826
3947
|
throw new Error(ERR_BAD_FORMAT);
|
|
3827
3948
|
}
|
|
3828
3949
|
const expectedDirectoryDataOffset = centralDirectoryEndOffset - directoryDataLength - (reader.lastDiskOffset || 0);
|
|
3829
|
-
if (
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3950
|
+
if (directoryDataOffset != expectedDirectoryDataOffset && diskNumber == lastDiskNumber) {
|
|
3951
|
+
// the reconciled offset (the directory ends exactly where the canonical record begins) is anchored
|
|
3952
|
+
// to the unforgeable end of the archive; the stored offset is not. Prefer the reconciled offset
|
|
3953
|
+
// unless the stored one points at a directory and the reconciled one does not — that exception
|
|
3954
|
+
// keeps a corrupt declared directory length from sending the read astray, while still moving off a
|
|
3955
|
+
// stored offset that only looks valid because an append remnant left the previous (identically
|
|
3956
|
+
// laid out) directory sitting there.
|
|
3957
|
+
const storedPointsAtDirectory = getUint32(directoryView, offset) == CENTRAL_FILE_HEADER_SIGNATURE;
|
|
3958
|
+
let reconcile = !storedPointsAtDirectory;
|
|
3959
|
+
if (!reconcile && expectedDirectoryDataOffset >= 0 && expectedDirectoryDataOffset + 4 <= reader.size) {
|
|
3960
|
+
const expectedSignatureArray = await readUint8Array(reader, expectedDirectoryDataOffset, 4, diskNumber);
|
|
3961
|
+
reconcile = getUint32(getDataView$1(expectedSignatureArray), 0) == CENTRAL_FILE_HEADER_SIGNATURE;
|
|
3962
|
+
}
|
|
3963
|
+
if (reconcile) {
|
|
3964
|
+
const originalDirectoryDataOffset = directoryDataOffset;
|
|
3965
|
+
directoryDataOffset = expectedDirectoryDataOffset;
|
|
3966
|
+
if (directoryDataOffset > originalDirectoryDataOffset) {
|
|
3967
|
+
prependedDataLength += directoryDataOffset - originalDirectoryDataOffset;
|
|
3968
|
+
}
|
|
3969
|
+
directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
|
|
3970
|
+
directoryView = getDataView$1(directoryArray);
|
|
3834
3971
|
}
|
|
3835
|
-
directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
|
|
3836
|
-
directoryView = getDataView$1(directoryArray);
|
|
3837
3972
|
}
|
|
3838
3973
|
}
|
|
3839
3974
|
const expectedDirectoryDataLength = centralDirectoryEndOffset - directoryDataOffset - (reader.lastDiskOffset || 0);
|
|
@@ -4050,7 +4185,7 @@
|
|
|
4050
4185
|
extraFieldLength,
|
|
4051
4186
|
filenameLength
|
|
4052
4187
|
} = localDirectory;
|
|
4053
|
-
const checkAmbiguity = getOptionValue$1(zipEntry, options, OPTION_CHECK_AMBIGUITY);
|
|
4188
|
+
const checkAmbiguity = getStrictness(getOptionValue$1(zipEntry, options, OPTION_STRICTNESS), getOptionValue$1(zipEntry, options, OPTION_CHECK_AMBIGUITY)) == STRICTNESS_STRICT;
|
|
4054
4189
|
let rawLocalFilename = new Uint8Array();
|
|
4055
4190
|
if (checkAmbiguity && (filenameLength || extraFieldLength)) {
|
|
4056
4191
|
const trailingDataArray = await readUint8Array(reader, offset + HEADER_SIZE, filenameLength + extraFieldLength, diskNumberStart);
|
|
@@ -4500,26 +4635,191 @@
|
|
|
4500
4635
|
readRanges.set(index, range);
|
|
4501
4636
|
}
|
|
4502
4637
|
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
return
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4638
|
+
function getStrictness(strictness, checkAmbiguity) {
|
|
4639
|
+
if (strictness === UNDEFINED_VALUE) {
|
|
4640
|
+
// `checkAmbiguity: true` is kept as a backward-compatible alias for the strictest mode
|
|
4641
|
+
return checkAmbiguity ? STRICTNESS_STRICT : STRICTNESS_BALANCED;
|
|
4642
|
+
}
|
|
4643
|
+
return strictness;
|
|
4644
|
+
}
|
|
4645
|
+
|
|
4646
|
+
function getMaxAppendedDataSize(maxAppendedDataSize, strictness) {
|
|
4647
|
+
if (maxAppendedDataSize !== UNDEFINED_VALUE) {
|
|
4648
|
+
return maxAppendedDataSize;
|
|
4649
|
+
}
|
|
4650
|
+
if (strictness == STRICTNESS_STRICT) {
|
|
4651
|
+
// the comment must reach the end of the file: no trailing data tolerated
|
|
4652
|
+
return 0;
|
|
4653
|
+
}
|
|
4654
|
+
if (strictness == STRICTNESS_TOLERANT) {
|
|
4655
|
+
return Infinity;
|
|
4656
|
+
}
|
|
4657
|
+
// balanced: tolerate up to a legal comment's worth of trailing data (a 16-bit length); beyond that the
|
|
4658
|
+
// trailing bytes cannot hide inside a comment, so they are treated as a second thing bolted onto the file
|
|
4659
|
+
return MAX_16_BITS;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
// cap on how many out-of-window central directory probes the scan below may issue. A legitimate archive needs
|
|
4663
|
+
// at most a couple (the canonical record, plus one more to detect genuine ambiguity); every other reachability
|
|
4664
|
+
// check is served from the tail window already in memory. The cap keeps an archive whose comment is stuffed
|
|
4665
|
+
// with unreachable end-anchored records from forcing an unbounded number of (potentially remote) reads.
|
|
4666
|
+
const MAX_END_OF_CENTRAL_DIR_PROBES = 64;
|
|
4667
|
+
|
|
4668
|
+
// reachability rankings for an end-anchored candidate record (see getCentralDirectoryReachability)
|
|
4669
|
+
const CENTRAL_DIRECTORY_UNREACHABLE = 0;
|
|
4670
|
+
const CENTRAL_DIRECTORY_PLAUSIBLE = 1;
|
|
4671
|
+
const CENTRAL_DIRECTORY_REACHABLE = 2;
|
|
4672
|
+
|
|
4673
|
+
// Locates the authoritative end of central directory record. The canonical record is the last one whose
|
|
4674
|
+
// declared comment reaches exactly the end of the file ("end-anchored") and that points to a central directory
|
|
4675
|
+
// ("reachable"); earlier signatures (stale append remnants, bytes embedded in a comment) are ignored. When two
|
|
4676
|
+
// or more end-anchored records point to a central directory the archive is ambiguous, and the count is returned
|
|
4677
|
+
// so the caller can refuse it. A record that reaches the end of the file but points to no central directory (an
|
|
4678
|
+
// empty archive) is only "plausible": it cannot be corroborated, so it never counts towards ambiguity and is
|
|
4679
|
+
// chosen only when no reachable record exists — otherwise an empty record forged in a comment could outrank the
|
|
4680
|
+
// genuine directory. When no record is end-anchored (appended data or an oversized comment), falls back to the
|
|
4681
|
+
// last record within the tolerated window that points to a directory (see seekEndOfCentralDirectory).
|
|
4682
|
+
async function findEndOfCentralDirectory(reader, rejectAmbiguous, maxAppendedDataSize) {
|
|
4683
|
+
const { size } = reader;
|
|
4684
|
+
// an end-anchored record can only live within the last END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS bytes,
|
|
4685
|
+
// since the comment length that ties it to the end of the file is a 16-bit field
|
|
4686
|
+
const anchoredLength = Math.min(size, END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS);
|
|
4687
|
+
const anchoredOffset = size - anchoredLength;
|
|
4688
|
+
const anchoredArray = await readUint8Array(reader, anchoredOffset, anchoredLength);
|
|
4689
|
+
const anchoredView = getDataView$1(anchoredArray);
|
|
4690
|
+
const remoteProbeBudget = { count: MAX_END_OF_CENTRAL_DIR_PROBES };
|
|
4691
|
+
let endOfDirectoryInfo;
|
|
4692
|
+
let plausibleEndOfDirectoryInfo;
|
|
4693
|
+
let endOfDirectoryReachingEndCount = 0;
|
|
4694
|
+
for (let indexByte = anchoredArray.length - END_OF_CENTRAL_DIR_LENGTH; indexByte >= 0; indexByte--) {
|
|
4695
|
+
if (getUint32(anchoredView, indexByte) == END_OF_CENTRAL_DIR_SIGNATURE) {
|
|
4696
|
+
const offset = anchoredOffset + indexByte;
|
|
4697
|
+
const commentLength = getUint16(anchoredView, indexByte + 20);
|
|
4698
|
+
// end-anchored: the declared comment extends exactly to the end of the file. The comment length is
|
|
4699
|
+
// attacker-controlled, but the reconciliation against the (unforgeable) end of file is not.
|
|
4700
|
+
if (offset + END_OF_CENTRAL_DIR_LENGTH + commentLength == size) {
|
|
4701
|
+
const reachability = await getCentralDirectoryReachability(reader, anchoredView, anchoredOffset, indexByte, offset, size, remoteProbeBudget);
|
|
4702
|
+
if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
|
|
4703
|
+
if (!endOfDirectoryInfo) {
|
|
4704
|
+
endOfDirectoryInfo = {
|
|
4705
|
+
offset,
|
|
4706
|
+
buffer: anchoredArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer
|
|
4707
|
+
};
|
|
4708
|
+
}
|
|
4709
|
+
endOfDirectoryReachingEndCount++;
|
|
4710
|
+
// the canonical record (highest offset) is found first; a second one is enough to flag ambiguity
|
|
4711
|
+
if (!rejectAmbiguous || endOfDirectoryReachingEndCount > 1) {
|
|
4712
|
+
break;
|
|
4713
|
+
}
|
|
4714
|
+
} else if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleEndOfDirectoryInfo) {
|
|
4715
|
+
plausibleEndOfDirectoryInfo = {
|
|
4716
|
+
offset,
|
|
4717
|
+
buffer: anchoredArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer
|
|
4718
|
+
};
|
|
4719
|
+
}
|
|
4520
4720
|
}
|
|
4521
4721
|
}
|
|
4522
4722
|
}
|
|
4723
|
+
if (!endOfDirectoryInfo) {
|
|
4724
|
+
// no record points to a directory: prefer a plausible (empty) end-anchored record before scanning the
|
|
4725
|
+
// tolerated window for a bare signature
|
|
4726
|
+
endOfDirectoryInfo = plausibleEndOfDirectoryInfo;
|
|
4727
|
+
}
|
|
4728
|
+
if (!endOfDirectoryInfo) {
|
|
4729
|
+
endOfDirectoryInfo = await seekEndOfCentralDirectory(reader, maxAppendedDataSize, remoteProbeBudget);
|
|
4730
|
+
}
|
|
4731
|
+
return { endOfDirectoryInfo, endOfDirectoryReachingEndCount };
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
// Fallback for findEndOfCentralDirectory when no record is end-anchored: appended data or an oversized comment
|
|
4735
|
+
// pushed the record's declared comment past the end of the file, so its position can no longer be reconciled
|
|
4736
|
+
// against the end of the file. Scans the tolerated window from the end and returns the first record that points
|
|
4737
|
+
// to a central directory, so a stray signature embedded in appended data cannot hijack discovery (which the
|
|
4738
|
+
// anchored scan already prevents for anchored records). Falls back to the first plausible (empty) record, then
|
|
4739
|
+
// to the last bare signature, so a degenerate archive still opens as before. The window ends at the file, so the
|
|
4740
|
+
// same in-window / metered reads as the anchored scan apply.
|
|
4741
|
+
async function seekEndOfCentralDirectory(reader, maxAppendedDataSize, remoteProbeBudget) {
|
|
4742
|
+
const { size } = reader;
|
|
4743
|
+
const searchLength = Math.min(size, maxAppendedDataSize == Infinity ? size :
|
|
4744
|
+
END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS + maxAppendedDataSize);
|
|
4745
|
+
const searchOffset = size - searchLength;
|
|
4746
|
+
const searchArray = await readUint8Array(reader, searchOffset, searchLength);
|
|
4747
|
+
const searchView = getDataView$1(searchArray);
|
|
4748
|
+
let firstSignatureInfo, plausibleInfo;
|
|
4749
|
+
for (let indexByte = searchArray.length - END_OF_CENTRAL_DIR_LENGTH; indexByte >= 0; indexByte--) {
|
|
4750
|
+
if (getUint32(searchView, indexByte) == END_OF_CENTRAL_DIR_SIGNATURE) {
|
|
4751
|
+
const offset = searchOffset + indexByte;
|
|
4752
|
+
const record = { offset, buffer: searchArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer };
|
|
4753
|
+
if (!firstSignatureInfo) {
|
|
4754
|
+
firstSignatureInfo = record;
|
|
4755
|
+
}
|
|
4756
|
+
const reachability = await getCentralDirectoryReachability(reader, searchView, searchOffset, indexByte, offset, size, remoteProbeBudget);
|
|
4757
|
+
if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
|
|
4758
|
+
return record;
|
|
4759
|
+
}
|
|
4760
|
+
if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleInfo) {
|
|
4761
|
+
plausibleInfo = record;
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
return plausibleInfo || firstSignatureInfo;
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// Ranks how strongly an end-anchored candidate record is backed by an actual central directory, used to decide
|
|
4769
|
+
// which record is canonical and which count towards ambiguity. It is intentionally conservative and does not
|
|
4770
|
+
// fully parse the directory (the caller does that for the canonical record):
|
|
4771
|
+
// - REACHABLE: the record dereferences a central directory (a central file header signature at the
|
|
4772
|
+
// reconciled offset), or, for a zip64 record, the zip64 locator that a genuine zip64 archive places
|
|
4773
|
+
// immediately before it. Only these count towards ambiguity.
|
|
4774
|
+
// - PLAUSIBLE: the record describes an empty archive, which has no central directory to dereference. Its
|
|
4775
|
+
// fields are indistinguishable from an empty end of central directory record forged inside a comment, so it
|
|
4776
|
+
// is accepted only as a last resort and never inflates the ambiguity count.
|
|
4777
|
+
// - UNREACHABLE: neither — stale append remnant or signature bytes embedded in a comment.
|
|
4778
|
+
// Signatures are read from the tail window (`view`, spanning `[anchoredOffset, size)`) when they fall inside it;
|
|
4779
|
+
// only a target below the window costs a (possibly remote) read, metered by `remoteProbeBudget` so a stuffed
|
|
4780
|
+
// comment cannot amplify into an unbounded number of requests.
|
|
4781
|
+
async function getCentralDirectoryReachability(reader, view, anchoredOffset, indexByte, offset, size, remoteProbeBudget) {
|
|
4782
|
+
const filesLength = getUint16(view, indexByte + 10);
|
|
4783
|
+
const directoryDataLength = getUint32(view, indexByte + 12);
|
|
4784
|
+
const directoryDataOffset = getUint32(view, indexByte + 16);
|
|
4785
|
+
if (filesLength == MAX_16_BITS || directoryDataLength == MAX_32_BITS || directoryDataOffset == MAX_32_BITS) {
|
|
4786
|
+
// saturated fields imply a zip64 record; corroborate it with the zip64 end of central directory locator
|
|
4787
|
+
// that sits immediately before it, so saturated bytes inside a comment cannot masquerade as one
|
|
4788
|
+
const locatorSignature = await readSignature(reader, view, anchoredOffset, offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, size, remoteProbeBudget);
|
|
4789
|
+
return locatorSignature == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE ? CENTRAL_DIRECTORY_REACHABLE : CENTRAL_DIRECTORY_UNREACHABLE;
|
|
4790
|
+
}
|
|
4791
|
+
if (!filesLength && !directoryDataLength) {
|
|
4792
|
+
// a valid empty archive has no central directory to dereference
|
|
4793
|
+
return CENTRAL_DIRECTORY_PLAUSIBLE;
|
|
4794
|
+
}
|
|
4795
|
+
// the central directory ends where this record starts; reconcile the stored offset against that actual
|
|
4796
|
+
// position (see stored_cd_offset ⟺ eocd_pos − cd_size) so prepended data does not hide the directory
|
|
4797
|
+
for (const centralDirectoryOffset of [offset - directoryDataLength, directoryDataOffset]) {
|
|
4798
|
+
if (await readSignature(reader, view, anchoredOffset, centralDirectoryOffset, size, remoteProbeBudget) == CENTRAL_FILE_HEADER_SIGNATURE) {
|
|
4799
|
+
return CENTRAL_DIRECTORY_REACHABLE;
|
|
4800
|
+
}
|
|
4801
|
+
}
|
|
4802
|
+
return CENTRAL_DIRECTORY_UNREACHABLE;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
// Reads a 4-byte little-endian signature at `signatureOffset`, served from the tail window (`view`, spanning
|
|
4806
|
+
// `[anchoredOffset, size)`) when it falls inside it and otherwise from a metered (possibly remote) read. Returns
|
|
4807
|
+
// UNDEFINED_VALUE when the position is out of range or the out-of-window read budget is exhausted.
|
|
4808
|
+
async function readSignature(reader, view, anchoredOffset, signatureOffset, size, remoteProbeBudget) {
|
|
4809
|
+
if (signatureOffset < 0 || signatureOffset + 4 > size) {
|
|
4810
|
+
return UNDEFINED_VALUE;
|
|
4811
|
+
}
|
|
4812
|
+
if (signatureOffset >= anchoredOffset) {
|
|
4813
|
+
// the target sits inside the tail window already in memory: no extra read needed
|
|
4814
|
+
return getUint32(view, signatureOffset - anchoredOffset);
|
|
4815
|
+
}
|
|
4816
|
+
if (remoteProbeBudget.count > 0) {
|
|
4817
|
+
remoteProbeBudget.count--;
|
|
4818
|
+
const signatureArray = await readUint8Array(reader, signatureOffset, 4);
|
|
4819
|
+
return getUint32(getDataView$1(signatureArray), 0);
|
|
4820
|
+
}
|
|
4821
|
+
// out of budget: leave this candidate unverified rather than issue another read
|
|
4822
|
+
return UNDEFINED_VALUE;
|
|
4523
4823
|
}
|
|
4524
4824
|
|
|
4525
4825
|
function checkLocalDirectory(zipEntry, localDirectory, rawLocalFilename) {
|
|
@@ -4584,10 +4884,6 @@
|
|
|
4584
4884
|
return Number(view.getBigUint64(offset, true));
|
|
4585
4885
|
}
|
|
4586
4886
|
|
|
4587
|
-
function setUint32$1(view, offset, value) {
|
|
4588
|
-
view.setUint32(offset, value, true);
|
|
4589
|
-
}
|
|
4590
|
-
|
|
4591
4887
|
function getDataView$1(array) {
|
|
4592
4888
|
return new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
4593
4889
|
}
|
|
@@ -5315,6 +5611,17 @@
|
|
|
5315
5611
|
if (releaseLockWriter) {
|
|
5316
5612
|
releaseLockWriter();
|
|
5317
5613
|
}
|
|
5614
|
+
// A buffered entry uses a temporary stream (see `createTempStream`). When that stream is
|
|
5615
|
+
// backed by a resource (a file, an OPFS handle, ...) it must be released on every exit path,
|
|
5616
|
+
// including the error path where the stream is otherwise abandoned without being closed or
|
|
5617
|
+
// cancelled. `dispose()` is optional and best-effort so it never masks the original outcome.
|
|
5618
|
+
if (bufferedWrite && fileWriter && fileWriter.dispose) {
|
|
5619
|
+
try {
|
|
5620
|
+
await fileWriter.dispose();
|
|
5621
|
+
} catch {
|
|
5622
|
+
// ignored
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5318
5625
|
}
|
|
5319
5626
|
|
|
5320
5627
|
function requestLockCurrentFileEntry() {
|
|
@@ -6679,6 +6986,10 @@
|
|
|
6679
6986
|
return writable;
|
|
6680
6987
|
}
|
|
6681
6988
|
|
|
6989
|
+
exportFileSystemHandle(handle, options = {}) {
|
|
6990
|
+
return exportFileSystemHandle(this, handle, options);
|
|
6991
|
+
}
|
|
6992
|
+
|
|
6682
6993
|
async importZip(reader, options = {}) {
|
|
6683
6994
|
await initStream(reader);
|
|
6684
6995
|
const zipReader = new ZipReader(reader, options);
|
|
@@ -6944,6 +7255,10 @@
|
|
|
6944
7255
|
return this.root.exportWritable(writable, options);
|
|
6945
7256
|
}
|
|
6946
7257
|
|
|
7258
|
+
exportFileSystemHandle(handle, options) {
|
|
7259
|
+
return this.root.exportFileSystemHandle(handle, options);
|
|
7260
|
+
}
|
|
7261
|
+
|
|
6947
7262
|
exportZip(writer, options) {
|
|
6948
7263
|
return this.root.exportZip(writer, options);
|
|
6949
7264
|
}
|
|
@@ -7181,6 +7496,56 @@
|
|
|
7181
7496
|
}
|
|
7182
7497
|
}
|
|
7183
7498
|
|
|
7499
|
+
async function exportFileSystemHandle(zipEntry, directoryHandle, options) {
|
|
7500
|
+
const totalSize = getTotalSize([zipEntry], "uncompressedSize");
|
|
7501
|
+
const writtenSizes = new Map();
|
|
7502
|
+
await exportChildren(zipEntry, directoryHandle);
|
|
7503
|
+
return directoryHandle;
|
|
7504
|
+
|
|
7505
|
+
async function exportChildren(entry, parentHandle) {
|
|
7506
|
+
// Separate files have no ordering constraint, so they can be written concurrently on demand.
|
|
7507
|
+
if (options.concurrent) {
|
|
7508
|
+
const results = await Promise.allSettled(entry.children.map(child => exportChild(child, parentHandle)));
|
|
7509
|
+
const failedResult = results.find(result => result.status == "rejected");
|
|
7510
|
+
if (failedResult) {
|
|
7511
|
+
throw failedResult.reason;
|
|
7512
|
+
}
|
|
7513
|
+
} else {
|
|
7514
|
+
for (const child of entry.children) {
|
|
7515
|
+
await exportChild(child, parentHandle);
|
|
7516
|
+
}
|
|
7517
|
+
}
|
|
7518
|
+
}
|
|
7519
|
+
|
|
7520
|
+
async function exportChild(child, parentHandle) {
|
|
7521
|
+
try {
|
|
7522
|
+
if (child.directory) {
|
|
7523
|
+
const childDirectoryHandle = await parentHandle.getDirectoryHandle(child.name, { create: true });
|
|
7524
|
+
await exportChildren(child, childDirectoryHandle);
|
|
7525
|
+
} else {
|
|
7526
|
+
const fileHandle = await parentHandle.getFileHandle(child.name, { create: true });
|
|
7527
|
+
const writable = await fileHandle.createWritable();
|
|
7528
|
+
// getData() streams the entry's data into `writable` and closes it (a FileSystemWritableFileStream
|
|
7529
|
+
// only commits the file on close), applying options such as password, signal and onprogress.
|
|
7530
|
+
await child.getData({ writable }, Object.assign({}, options, {
|
|
7531
|
+
onprogress: async progress => {
|
|
7532
|
+
if (options.onprogress) {
|
|
7533
|
+
writtenSizes.set(child.id, progress);
|
|
7534
|
+
try {
|
|
7535
|
+
await options.onprogress(Array.from(writtenSizes.values()).reduce((previousValue, currentValue) => previousValue + currentValue, 0), totalSize);
|
|
7536
|
+
} catch {
|
|
7537
|
+
// ignored
|
|
7538
|
+
}
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7541
|
+
}));
|
|
7542
|
+
}
|
|
7543
|
+
} catch (error) {
|
|
7544
|
+
throw new Error(error.message + (child ? " (" + child.name + ")" : ""), { cause: error });
|
|
7545
|
+
}
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
|
|
7184
7549
|
async function transformToFileSystemhandle(entry) {
|
|
7185
7550
|
const handle = {
|
|
7186
7551
|
name: entry.name
|