@zip.js/zip.js 2.8.28 → 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 +203 -0
- package/deno.json +1 -1
- package/dist/zip-core.js +590 -53
- package/dist/zip-core.min.js +1 -1
- package/dist/zip-fs-core.js +476 -53
- package/dist/zip-fs-core.min.js +1 -1
- package/dist/zip-fs-native.js +652 -57
- package/dist/zip-fs-native.min.js +1 -1
- package/dist/zip-fs.js +655 -55
- package/dist/zip-fs.min.js +1 -1
- package/dist/zip-legacy.js +591 -54
- package/dist/zip-legacy.min.js +1 -1
- package/dist/zip-module.wasm +0 -0
- package/dist/zip-native.js +594 -57
- 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 +597 -55
- package/dist/zip.min.js +1 -1
- package/index-native.cjs +652 -57
- package/index-native.min.js +1 -1
- package/index.cjs +655 -55
- package/index.d.ts +126 -1
- package/index.min.js +1 -1
- package/lib/core/codec-worker.js +10 -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 +139 -9
- package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
- package/lib/core/streams/zlib-wasm/zlib-streams.js +6 -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 }) {
|
|
@@ -2527,14 +2635,15 @@
|
|
|
2527
2635
|
function pipeThroughCommpressionStream(readable, useCompressionStream, options, CompressionStreamNative, CompressionStreamZlib, CompressionStream) {
|
|
2528
2636
|
const Stream = useCompressionStream && CompressionStreamNative ? CompressionStreamNative : CompressionStreamZlib || CompressionStream;
|
|
2529
2637
|
const format = options.deflate64 ? FORMAT_DEFLATE64_RAW : FORMAT_DEFLATE_RAW;
|
|
2638
|
+
let codecStream;
|
|
2530
2639
|
try {
|
|
2531
|
-
|
|
2640
|
+
codecStream = new Stream(format, options);
|
|
2532
2641
|
} catch (error) {
|
|
2533
2642
|
if (useCompressionStream) {
|
|
2534
2643
|
if (CompressionStreamZlib) {
|
|
2535
|
-
|
|
2644
|
+
codecStream = new CompressionStreamZlib(format, options);
|
|
2536
2645
|
} else if (CompressionStream) {
|
|
2537
|
-
|
|
2646
|
+
codecStream = new CompressionStream(format, options);
|
|
2538
2647
|
} else {
|
|
2539
2648
|
throw error;
|
|
2540
2649
|
}
|
|
@@ -2542,13 +2651,62 @@
|
|
|
2542
2651
|
throw error;
|
|
2543
2652
|
}
|
|
2544
2653
|
}
|
|
2545
|
-
|
|
2654
|
+
// The native CompressionStream/DecompressionStream and the pure-JS zlib port do not signal
|
|
2655
|
+
// backpressure on their writable side (their `ready` never pends), so a plain pipeThrough drains
|
|
2656
|
+
// the whole source into them and peak memory grows with the entry size. Awaiting each write()
|
|
2657
|
+
// paces the source to the codec's consumption rate, keeping streaming memory bounded. The WASM
|
|
2658
|
+
// codec already backpressures, so this is a no-op cost for it.
|
|
2659
|
+
return pipeThroughBackpressured(readable, codecStream);
|
|
2546
2660
|
}
|
|
2547
2661
|
|
|
2548
2662
|
function pipeThrough(readable, transformStream) {
|
|
2549
2663
|
return readable.pipeThrough(transformStream);
|
|
2550
2664
|
}
|
|
2551
2665
|
|
|
2666
|
+
// Like readable.pipeThrough(transformStream), but drives the writable side manually and awaits each
|
|
2667
|
+
// write() so that a codec which under-reports backpressure cannot pull the whole source into memory.
|
|
2668
|
+
// Errors propagate in both directions: a source error aborts the codec; a downstream cancel/error
|
|
2669
|
+
// (surfaced through the codec's writable rejecting) cancels the source.
|
|
2670
|
+
function pipeThroughBackpressured(readable, transformStream) {
|
|
2671
|
+
const writer = transformStream.writable.getWriter();
|
|
2672
|
+
const reader = readable.getReader();
|
|
2673
|
+
pump();
|
|
2674
|
+
return transformStream.readable;
|
|
2675
|
+
|
|
2676
|
+
async function pump() {
|
|
2677
|
+
try {
|
|
2678
|
+
for (; ;) {
|
|
2679
|
+
await writer.ready;
|
|
2680
|
+
const result = await reader.read();
|
|
2681
|
+
if (result.done) {
|
|
2682
|
+
await writer.close();
|
|
2683
|
+
break;
|
|
2684
|
+
}
|
|
2685
|
+
await writer.write(result.value);
|
|
2686
|
+
}
|
|
2687
|
+
} catch (error) {
|
|
2688
|
+
await abort(writer, error);
|
|
2689
|
+
await cancel(reader, error);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
async function abort(writer, error) {
|
|
2695
|
+
try {
|
|
2696
|
+
await writer.abort(error);
|
|
2697
|
+
} catch {
|
|
2698
|
+
// the writable may already be errored/closed
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
async function cancel(reader, error) {
|
|
2703
|
+
try {
|
|
2704
|
+
await reader.cancel(error);
|
|
2705
|
+
} catch {
|
|
2706
|
+
// the readable may already be errored/closed
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2552
2710
|
// DecompressionStream implementations can fail with a message-less TypeError on malformed
|
|
2553
2711
|
// input; give these errors an identifiable message (errors raised by the other stages of
|
|
2554
2712
|
// the pipeline, e.g. ERR_INVALID_PASSWORD, always carry a message and pass through as-is)
|
|
@@ -2876,7 +3034,15 @@
|
|
|
2876
3034
|
try {
|
|
2877
3035
|
await initModule(config);
|
|
2878
3036
|
} catch {
|
|
2879
|
-
|
|
3037
|
+
// The WASM module failed to load. Fall back to the native CompressionStream only if the
|
|
3038
|
+
// zlib codec that would otherwise run depends on that module; a self-contained codec
|
|
3039
|
+
// supplied through config (e.g. the pure-JS port) stays usable and is kept as requested.
|
|
3040
|
+
const ZlibStream = options.codecType.startsWith(CODEC_DEFLATE) ?
|
|
3041
|
+
config.CompressionStreamZlib :
|
|
3042
|
+
config.DecompressionStreamZlib;
|
|
3043
|
+
if (!ZlibStream || ZlibStream.requiresModule) {
|
|
3044
|
+
options.useCompressionStream = true;
|
|
3045
|
+
}
|
|
2880
3046
|
}
|
|
2881
3047
|
}
|
|
2882
3048
|
codecStream = new CodecStream(options, config);
|
|
@@ -3583,6 +3749,11 @@
|
|
|
3583
3749
|
const OPTION_OFFSET = "offset";
|
|
3584
3750
|
const OPTION_USDZ = "usdz";
|
|
3585
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";
|
|
3586
3757
|
|
|
3587
3758
|
/*
|
|
3588
3759
|
Copyright (c) 2025 Gildas Lormeau. All rights reserved.
|
|
@@ -3672,7 +3843,11 @@
|
|
|
3672
3843
|
throw new Error(ERR_BAD_FORMAT);
|
|
3673
3844
|
}
|
|
3674
3845
|
reader.chunkSize = getChunkSize(config);
|
|
3675
|
-
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);
|
|
3676
3851
|
if (!endOfDirectoryInfo) {
|
|
3677
3852
|
const signatureArray = await readUint8Array(reader, 0, 4);
|
|
3678
3853
|
const signatureView = getDataView$1(signatureArray);
|
|
@@ -3682,14 +3857,18 @@
|
|
|
3682
3857
|
throw new Error(ERR_EOCDR_NOT_FOUND);
|
|
3683
3858
|
}
|
|
3684
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
|
+
}
|
|
3685
3865
|
const endOfDirectoryView = getDataView$1(endOfDirectoryInfo);
|
|
3686
3866
|
let directoryDataLength = getUint32(endOfDirectoryView, 12);
|
|
3687
3867
|
let directoryDataOffset = getUint32(endOfDirectoryView, 16);
|
|
3688
3868
|
const commentOffset = endOfDirectoryInfo.offset;
|
|
3689
3869
|
const commentLength = getUint16(endOfDirectoryView, 20);
|
|
3690
3870
|
const appendedDataOffset = commentOffset + END_OF_CENTRAL_DIR_LENGTH + commentLength;
|
|
3691
|
-
|
|
3692
|
-
if (checkAmbiguity && appendedDataOffset != reader.size) {
|
|
3871
|
+
if (reader.size - appendedDataOffset > maxAppendedDataSize) {
|
|
3693
3872
|
throwAmbiguousArchive("appended data");
|
|
3694
3873
|
}
|
|
3695
3874
|
let lastDiskNumber = getUint16(endOfDirectoryView, 4);
|
|
@@ -3768,14 +3947,28 @@
|
|
|
3768
3947
|
throw new Error(ERR_BAD_FORMAT);
|
|
3769
3948
|
}
|
|
3770
3949
|
const expectedDirectoryDataOffset = centralDirectoryEndOffset - directoryDataLength - (reader.lastDiskOffset || 0);
|
|
3771
|
-
if (
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
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);
|
|
3776
3971
|
}
|
|
3777
|
-
directoryArray = await readUint8Array(reader, directoryDataOffset, directoryDataLength, diskNumber);
|
|
3778
|
-
directoryView = getDataView$1(directoryArray);
|
|
3779
3972
|
}
|
|
3780
3973
|
}
|
|
3781
3974
|
const expectedDirectoryDataLength = centralDirectoryEndOffset - directoryDataOffset - (reader.lastDiskOffset || 0);
|
|
@@ -3992,7 +4185,7 @@
|
|
|
3992
4185
|
extraFieldLength,
|
|
3993
4186
|
filenameLength
|
|
3994
4187
|
} = localDirectory;
|
|
3995
|
-
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;
|
|
3996
4189
|
let rawLocalFilename = new Uint8Array();
|
|
3997
4190
|
if (checkAmbiguity && (filenameLength || extraFieldLength)) {
|
|
3998
4191
|
const trailingDataArray = await readUint8Array(reader, offset + HEADER_SIZE, filenameLength + extraFieldLength, diskNumberStart);
|
|
@@ -4442,26 +4635,191 @@
|
|
|
4442
4635
|
readRanges.set(index, range);
|
|
4443
4636
|
}
|
|
4444
4637
|
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
return
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
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
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
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;
|
|
4462
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;
|
|
4463
4800
|
}
|
|
4464
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;
|
|
4465
4823
|
}
|
|
4466
4824
|
|
|
4467
4825
|
function checkLocalDirectory(zipEntry, localDirectory, rawLocalFilename) {
|
|
@@ -4526,10 +4884,6 @@
|
|
|
4526
4884
|
return Number(view.getBigUint64(offset, true));
|
|
4527
4885
|
}
|
|
4528
4886
|
|
|
4529
|
-
function setUint32$1(view, offset, value) {
|
|
4530
|
-
view.setUint32(offset, value, true);
|
|
4531
|
-
}
|
|
4532
|
-
|
|
4533
4887
|
function getDataView$1(array) {
|
|
4534
4888
|
return new DataView(array.buffer, array.byteOffset, array.byteLength);
|
|
4535
4889
|
}
|
|
@@ -5257,6 +5611,17 @@
|
|
|
5257
5611
|
if (releaseLockWriter) {
|
|
5258
5612
|
releaseLockWriter();
|
|
5259
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
|
+
}
|
|
5260
5625
|
}
|
|
5261
5626
|
|
|
5262
5627
|
function requestLockCurrentFileEntry() {
|
|
@@ -6621,6 +6986,10 @@
|
|
|
6621
6986
|
return writable;
|
|
6622
6987
|
}
|
|
6623
6988
|
|
|
6989
|
+
exportFileSystemHandle(handle, options = {}) {
|
|
6990
|
+
return exportFileSystemHandle(this, handle, options);
|
|
6991
|
+
}
|
|
6992
|
+
|
|
6624
6993
|
async importZip(reader, options = {}) {
|
|
6625
6994
|
await initStream(reader);
|
|
6626
6995
|
const zipReader = new ZipReader(reader, options);
|
|
@@ -6886,6 +7255,10 @@
|
|
|
6886
7255
|
return this.root.exportWritable(writable, options);
|
|
6887
7256
|
}
|
|
6888
7257
|
|
|
7258
|
+
exportFileSystemHandle(handle, options) {
|
|
7259
|
+
return this.root.exportFileSystemHandle(handle, options);
|
|
7260
|
+
}
|
|
7261
|
+
|
|
6889
7262
|
exportZip(writer, options) {
|
|
6890
7263
|
return this.root.exportZip(writer, options);
|
|
6891
7264
|
}
|
|
@@ -7123,6 +7496,56 @@
|
|
|
7123
7496
|
}
|
|
7124
7497
|
}
|
|
7125
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
|
+
|
|
7126
7549
|
async function transformToFileSystemhandle(entry) {
|
|
7127
7550
|
const handle = {
|
|
7128
7551
|
name: entry.name
|