@zip.js/zip.js 2.8.30 → 2.8.32
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 +82 -1
- package/deno.json +5 -4
- package/dist/zip-core.js +403 -339
- package/dist/zip-core.min.js +1 -1
- package/dist/zip-fs-core.js +116 -278
- package/dist/zip-fs-core.min.js +1 -1
- package/dist/zip-fs-native.js +440 -378
- package/dist/zip-fs-native.min.js +1 -1
- package/dist/zip-fs.js +439 -377
- package/dist/zip-fs.min.js +1 -1
- package/dist/zip-legacy.js +408 -344
- package/dist/zip-legacy.min.js +1 -1
- package/dist/zip-module.wasm +0 -0
- package/dist/zip-native.js +411 -347
- 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 +410 -346
- package/dist/zip.min.js +1 -1
- package/index-native.cjs +440 -378
- package/index-native.min.js +1 -1
- package/index.cjs +439 -377
- package/index.d.cts +2853 -0
- package/index.d.ts +278 -1
- package/index.min.js +1 -1
- package/lib/core/codec-worker.js +1 -11
- package/lib/core/configuration.js +21 -33
- package/lib/core/constants.js +3 -0
- package/lib/core/io.js +11 -19
- package/lib/core/streams/aes-crypto-stream.js +16 -20
- package/lib/core/streams/common-crypto.js +0 -2
- package/lib/core/streams/zip-crypto-stream.js +12 -16
- package/lib/core/streams/zip-entry-stream.js +2 -32
- package/lib/core/streams/zlib-js/zlib-streams.min.js +1 -1
- package/lib/core/streams/zlib-wasm/zlib-streams-loader.js +1 -3
- package/lib/core/streams/zlib-wasm/zlib-streams.wasm +0 -0
- package/lib/core/util/base64.js +68 -0
- package/lib/core/util/blob-temp-stream.js +124 -0
- package/lib/core/util/decode-text.js +0 -1
- package/lib/core/util/default-mime-type.js +0 -3
- package/lib/core/util/inflate.js +194 -0
- package/lib/core/util/mime-type.js +26 -22
- package/lib/core/util/opfs-temp-stream.js +1 -37
- package/lib/core/util/sync-access-handle-temp-stream.js +147 -0
- package/lib/core/web-worker-inline-native.js +1 -1
- package/lib/core/web-worker-inline-template-native.js +5 -5
- package/lib/core/web-worker-inline-wasm.js +1 -1
- package/lib/core/zip-fs.js +6 -13
- package/lib/core/zip-reader.js +48 -127
- package/lib/core/zip-writer.js +27 -45
- package/lib/core/zlib-streams-inline-template.js +3 -2
- package/lib/core/zlib-streams-inline.js +1 -1
- package/lib/zip-core-base.js +10 -2
- package/package.json +9 -6
- package/reserved-property-names.js +50 -0
- package/lib/core/util/mini-lz.js +0 -198
package/lib/core/zip-fs.js
CHANGED
|
@@ -78,8 +78,6 @@ class ZipEntry {
|
|
|
78
78
|
uncompressedSize: params.uncompressedSize || 0,
|
|
79
79
|
passThrough: params.passThrough
|
|
80
80
|
});
|
|
81
|
-
// detached entries (e.g. clones) are not registered until they are attached,
|
|
82
|
-
// otherwise they could never be garbage collected
|
|
83
81
|
if (parent || !fs.root) {
|
|
84
82
|
fs.entries[zipEntry.id] = zipEntry;
|
|
85
83
|
}
|
|
@@ -148,7 +146,7 @@ class ZipFileEntry extends ZipEntry {
|
|
|
148
146
|
if (!writer || (writer.constructor == zipEntry.Writer && zipEntry.data)) {
|
|
149
147
|
return zipEntry.data;
|
|
150
148
|
} else {
|
|
151
|
-
const reader = zipEntry.reader =
|
|
149
|
+
const reader = zipEntry.reader = createReader(zipEntry.Reader, zipEntry.data, options);
|
|
152
150
|
const uncompressedSize = zipEntry.data ? zipEntry.data.uncompressedSize : reader.size;
|
|
153
151
|
await Promise.all([initStream(reader), initStream(writer, uncompressedSize)]);
|
|
154
152
|
const { readable } = reader;
|
|
@@ -204,11 +202,7 @@ class ZipFileEntry extends ZipEntry {
|
|
|
204
202
|
}
|
|
205
203
|
|
|
206
204
|
async getArrayBuffer(options) {
|
|
207
|
-
// this.data may be a string, a Uint8Array, a data URI or a Blob depending on how the entry
|
|
208
|
-
// was created, so retrieve the bytes through getUint8Array rather than assuming a Blob; this
|
|
209
|
-
// also applies the options (password, signal, onprogress...) like the other getData() getters
|
|
210
205
|
const array = await this.getUint8Array(options);
|
|
211
|
-
// return the backing buffer directly when the array spans it, slicing the exact bytes otherwise
|
|
212
206
|
return array.byteOffset || array.byteLength != array.buffer.byteLength
|
|
213
207
|
? array.buffer.slice(array.byteOffset, array.byteOffset + array.byteLength)
|
|
214
208
|
: array.buffer;
|
|
@@ -578,7 +572,6 @@ class FS {
|
|
|
578
572
|
node = node.getChildByName(path[index]);
|
|
579
573
|
}
|
|
580
574
|
if (!node) {
|
|
581
|
-
// exclude detached entries, e.g. clones registered in `entries` but not attached
|
|
582
575
|
node = this.entries.find(entry => entry && (entry == this.root || entry.isDescendantOf(this.root)) &&
|
|
583
576
|
entry.getRelativeName() == fullname);
|
|
584
577
|
}
|
|
@@ -751,10 +744,13 @@ function getZipBlobReader(options) {
|
|
|
751
744
|
};
|
|
752
745
|
}
|
|
753
746
|
|
|
747
|
+
function createReader(Reader, data, options) {
|
|
748
|
+
return Reader.prototype ? new Reader(data, options) : Reader(data, options);
|
|
749
|
+
}
|
|
750
|
+
|
|
754
751
|
async function initReaders(entry, options) {
|
|
755
752
|
const fileEntries = [];
|
|
756
753
|
const pendingEntries = [entry];
|
|
757
|
-
// per-export map so concurrent exports do not share (and lock) the same readers
|
|
758
754
|
const readers = new Map();
|
|
759
755
|
while (pendingEntries.length) {
|
|
760
756
|
const pendingEntry = pendingEntries.pop();
|
|
@@ -767,7 +763,7 @@ async function initReaders(entry, options) {
|
|
|
767
763
|
}
|
|
768
764
|
}
|
|
769
765
|
await Promise.all(fileEntries.map(async child => {
|
|
770
|
-
const reader = child.reader =
|
|
766
|
+
const reader = child.reader = createReader(child.Reader, child.data, options);
|
|
771
767
|
readers.set(child, reader);
|
|
772
768
|
try {
|
|
773
769
|
await initStream(reader);
|
|
@@ -932,7 +928,6 @@ async function exportFileSystemHandle(zipEntry, directoryHandle, options) {
|
|
|
932
928
|
return directoryHandle;
|
|
933
929
|
|
|
934
930
|
async function exportChildren(entry, parentHandle) {
|
|
935
|
-
// Separate files have no ordering constraint, so they can be written concurrently on demand.
|
|
936
931
|
if (options.concurrent) {
|
|
937
932
|
const results = await Promise.allSettled(entry.children.map(child => exportChild(child, parentHandle)));
|
|
938
933
|
const failedResult = results.find(result => result.status == "rejected");
|
|
@@ -954,8 +949,6 @@ async function exportFileSystemHandle(zipEntry, directoryHandle, options) {
|
|
|
954
949
|
} else {
|
|
955
950
|
const fileHandle = await parentHandle.getFileHandle(child.name, { create: true });
|
|
956
951
|
const writable = await fileHandle.createWritable();
|
|
957
|
-
// getData() streams the entry's data into `writable` and closes it (a FileSystemWritableFileStream
|
|
958
|
-
// only commits the file on close), applying options such as password, signal and onprogress.
|
|
959
952
|
await child.getData({ writable }, Object.assign({}, options, {
|
|
960
953
|
onprogress: async progress => {
|
|
961
954
|
if (options.onprogress) {
|
package/lib/core/zip-reader.js
CHANGED
|
@@ -82,7 +82,8 @@ import {
|
|
|
82
82
|
COMPRESSION_METHOD_DEFLATE_64,
|
|
83
83
|
FILE_ATTR_UNIX_SETUID_MASK,
|
|
84
84
|
FILE_ATTR_UNIX_SETGID_MASK,
|
|
85
|
-
FILE_ATTR_UNIX_STICKY_MASK
|
|
85
|
+
FILE_ATTR_UNIX_STICKY_MASK,
|
|
86
|
+
EMPTY_UINT8_ARRAY
|
|
86
87
|
} from "./constants.js";
|
|
87
88
|
import {
|
|
88
89
|
getConfiguration,
|
|
@@ -164,8 +165,6 @@ const ERR_AMBIGUOUS_ARCHIVE = "Ambiguous archive";
|
|
|
164
165
|
const CHARSET_UTF8 = "utf-8";
|
|
165
166
|
const PROPERTY_NAME_UTF8_SUFFIX = "UTF8";
|
|
166
167
|
const CHARSET_CP437 = "cp437";
|
|
167
|
-
// bits of the general purpose flag that change how an entry is interpreted; the level bits (1-2) are excluded
|
|
168
|
-
// because they are informative only
|
|
169
168
|
const BITFLAG_AMBIGUITY_MASK = BITFLAG_ENCRYPTED | BITFLAG_DATA_DESCRIPTOR | BITFLAG_LANG_ENCODING_FLAG;
|
|
170
169
|
const ZIP64_PROPERTIES = [
|
|
171
170
|
[PROPERTY_NAME_UNCOMPRESSED_SIZE, MAX_32_BITS],
|
|
@@ -207,7 +206,6 @@ class ZipReader {
|
|
|
207
206
|
if (reader.size < END_OF_CENTRAL_DIR_LENGTH) {
|
|
208
207
|
throw new Error(ERR_BAD_FORMAT);
|
|
209
208
|
}
|
|
210
|
-
reader.chunkSize = getChunkSize(config);
|
|
211
209
|
const strictness = getStrictness(getOptionValue(zipReader, options, OPTION_STRICTNESS), getOptionValue(zipReader, options, OPTION_CHECK_AMBIGUITY));
|
|
212
210
|
const checkAmbiguity = strictness == STRICTNESS_STRICT;
|
|
213
211
|
const rejectAmbiguousEndOfDirectory = strictness != STRICTNESS_TOLERANT;
|
|
@@ -222,8 +220,6 @@ class ZipReader {
|
|
|
222
220
|
throw new Error(ERR_EOCDR_NOT_FOUND);
|
|
223
221
|
}
|
|
224
222
|
}
|
|
225
|
-
// two or more end-anchored records that each dereference to a valid central directory cannot be told
|
|
226
|
-
// apart (see comment_length ⟺ EOF): the archive is genuinely ambiguous, so refuse rather than guess
|
|
227
223
|
if (rejectAmbiguousEndOfDirectory && endOfDirectoryReachingEndCount > 1) {
|
|
228
224
|
throwAmbiguousArchive("multiple end of central directory records");
|
|
229
225
|
}
|
|
@@ -246,7 +242,7 @@ class ZipReader {
|
|
|
246
242
|
if (directoryDataOffset == MAX_32_BITS || directoryDataLength == MAX_32_BITS || filesLength == MAX_16_BITS || diskNumber == MAX_16_BITS) {
|
|
247
243
|
const endOfDirectoryLocatorArray = endOfDirectoryInfo.offset >= ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH ?
|
|
248
244
|
await readUint8Array(reader, endOfDirectoryInfo.offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH) :
|
|
249
|
-
|
|
245
|
+
EMPTY_UINT8_ARRAY;
|
|
250
246
|
const endOfDirectoryLocatorView = getDataView(endOfDirectoryLocatorArray);
|
|
251
247
|
if (endOfDirectoryLocatorArray.length == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH &&
|
|
252
248
|
getUint32(endOfDirectoryLocatorView, 0) == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE) {
|
|
@@ -313,12 +309,6 @@ class ZipReader {
|
|
|
313
309
|
}
|
|
314
310
|
const expectedDirectoryDataOffset = centralDirectoryEndOffset - directoryDataLength - (reader.lastDiskOffset || 0);
|
|
315
311
|
if (directoryDataOffset != expectedDirectoryDataOffset && diskNumber == lastDiskNumber) {
|
|
316
|
-
// the reconciled offset (the directory ends exactly where the canonical record begins) is anchored
|
|
317
|
-
// to the unforgeable end of the archive; the stored offset is not. Prefer the reconciled offset
|
|
318
|
-
// unless the stored one points at a directory and the reconciled one does not — that exception
|
|
319
|
-
// keeps a corrupt declared directory length from sending the read astray, while still moving off a
|
|
320
|
-
// stored offset that only looks valid because an append remnant left the previous (identically
|
|
321
|
-
// laid out) directory sitting there.
|
|
322
312
|
const storedPointsAtDirectory = getUint32(directoryView, offset) == CENTRAL_FILE_HEADER_SIGNATURE;
|
|
323
313
|
let reconcile = !storedPointsAtDirectory;
|
|
324
314
|
if (!reconcile && expectedDirectoryDataOffset >= 0 && expectedDirectoryDataOffset + 4 <= reader.size) {
|
|
@@ -475,11 +465,11 @@ class ZipReader {
|
|
|
475
465
|
const extractPrependedData = getOptionValue(zipReader, options, OPTION_EXTRACT_PREPENDED_DATA);
|
|
476
466
|
const extractAppendedData = getOptionValue(zipReader, options, OPTION_EXTRACT_APPENDED_DATA);
|
|
477
467
|
if (extractPrependedData) {
|
|
478
|
-
zipReader.prependedData = startOffset > 0 ? await readUint8Array(reader, 0, startOffset) :
|
|
468
|
+
zipReader.prependedData = startOffset > 0 ? await readUint8Array(reader, 0, startOffset) : EMPTY_UINT8_ARRAY;
|
|
479
469
|
}
|
|
480
|
-
zipReader.comment = commentLength ? await readUint8Array(reader, commentOffset + END_OF_CENTRAL_DIR_LENGTH, commentLength) :
|
|
470
|
+
zipReader.comment = commentLength ? await readUint8Array(reader, commentOffset + END_OF_CENTRAL_DIR_LENGTH, commentLength) : EMPTY_UINT8_ARRAY;
|
|
481
471
|
if (extractAppendedData) {
|
|
482
|
-
zipReader.appendedData = appendedDataOffset < reader.size ? await readUint8Array(reader, appendedDataOffset, reader.size - appendedDataOffset) :
|
|
472
|
+
zipReader.appendedData = appendedDataOffset < reader.size ? await readUint8Array(reader, appendedDataOffset, reader.size - appendedDataOffset) : EMPTY_UINT8_ARRAY;
|
|
483
473
|
}
|
|
484
474
|
return true;
|
|
485
475
|
}
|
|
@@ -612,7 +602,7 @@ class ZipEntry {
|
|
|
612
602
|
filenameLength
|
|
613
603
|
} = localDirectory;
|
|
614
604
|
const checkAmbiguity = getStrictness(getOptionValue(zipEntry, options, OPTION_STRICTNESS), getOptionValue(zipEntry, options, OPTION_CHECK_AMBIGUITY)) == STRICTNESS_STRICT;
|
|
615
|
-
let rawLocalFilename =
|
|
605
|
+
let rawLocalFilename = EMPTY_UINT8_ARRAY;
|
|
616
606
|
if (checkAmbiguity && (filenameLength || extraFieldLength)) {
|
|
617
607
|
const trailingDataArray = await readUint8Array(reader, offset + HEADER_SIZE, filenameLength + extraFieldLength, diskNumberStart);
|
|
618
608
|
rawLocalFilename = trailingDataArray.subarray(0, filenameLength);
|
|
@@ -620,7 +610,7 @@ class ZipEntry {
|
|
|
620
610
|
} else {
|
|
621
611
|
localDirectory.rawExtraField = extraFieldLength ?
|
|
622
612
|
await readUint8Array(reader, offset + HEADER_SIZE + filenameLength, extraFieldLength, diskNumberStart) :
|
|
623
|
-
|
|
613
|
+
EMPTY_UINT8_ARRAY;
|
|
624
614
|
}
|
|
625
615
|
readCommonFooter(zipEntry, localDirectory, dataView, 4, true);
|
|
626
616
|
if (checkAmbiguity) {
|
|
@@ -647,12 +637,7 @@ class ZipEntry {
|
|
|
647
637
|
}
|
|
648
638
|
const dataOffset = offset + HEADER_SIZE + filenameLength + extraFieldLength;
|
|
649
639
|
const size = compressedSize;
|
|
650
|
-
const readable = reader.
|
|
651
|
-
Object.assign(readable, {
|
|
652
|
-
diskNumberStart,
|
|
653
|
-
offset: dataOffset,
|
|
654
|
-
size
|
|
655
|
-
});
|
|
640
|
+
const readable = reader.createReadable({ offset: dataOffset, size, diskNumberStart, chunkSize: getChunkSize(config) });
|
|
656
641
|
const signal = getOptionValue(zipEntry, options, OPTION_SIGNAL);
|
|
657
642
|
const checkPasswordOnly = getOptionValue(zipEntry, options, OPTION_CHECK_PASSWORD_ONLY);
|
|
658
643
|
let checkOverlappingEntry = getOptionValue(zipEntry, options, OPTION_CHECK_OVERLAPPING_ENTRY);
|
|
@@ -923,7 +908,6 @@ function readExtraFieldUnix(extraField, directory, isInfoZip) {
|
|
|
923
908
|
const view = getDataView(new Uint8Array(extraField.data));
|
|
924
909
|
let uid, gid;
|
|
925
910
|
if (isInfoZip) {
|
|
926
|
-
// Info-ZIP New Unix "ux" field (0x7875): version + variable-length uid/gid
|
|
927
911
|
let offset = 0;
|
|
928
912
|
const version = getUint8(view, offset++);
|
|
929
913
|
const uidSize = getUint8(view, offset++);
|
|
@@ -933,8 +917,6 @@ function readExtraFieldUnix(extraField, directory, isInfoZip) {
|
|
|
933
917
|
gid = unpackUnixId(extraField.data.subarray(offset, offset + gidSize));
|
|
934
918
|
Object.assign(extraField, { version, uid, gid });
|
|
935
919
|
} else if (extraField.data.length >= 4) {
|
|
936
|
-
// Info-ZIP Unix "Ux" field (0x7855): fixed 2-byte uid + gid (the file mode, if any, is
|
|
937
|
-
// carried in the external file attributes, not here)
|
|
938
920
|
uid = getUint16(view, 0);
|
|
939
921
|
gid = getUint16(view, 2);
|
|
940
922
|
Object.assign(extraField, { uid, gid });
|
|
@@ -986,7 +968,6 @@ function readExtraFieldExtendedTimestamp(extraFieldExtendedTimestamp, directory,
|
|
|
986
968
|
timeProperties.forEach((propertyName, indexProperty) => {
|
|
987
969
|
if (extraFieldExtendedTimestamp.data.length >= offset + 4) {
|
|
988
970
|
const time = getUint32(extraFieldView, offset);
|
|
989
|
-
// the extended timestamp is a signed 32-bit time_t value
|
|
990
971
|
directory[propertyName] = extraFieldExtendedTimestamp[propertyName] = new Date((time | 0) * 1000);
|
|
991
972
|
const rawPropertyName = timeRawProperties[indexProperty];
|
|
992
973
|
extraFieldExtendedTimestamp[rawPropertyName] = time;
|
|
@@ -1063,7 +1044,6 @@ async function detectOverlappingEntry({
|
|
|
1063
1044
|
|
|
1064
1045
|
function getStrictness(strictness, checkAmbiguity) {
|
|
1065
1046
|
if (strictness === UNDEFINED_VALUE) {
|
|
1066
|
-
// `checkAmbiguity: true` is kept as a backward-compatible alias for the strictest mode
|
|
1067
1047
|
return checkAmbiguity ? STRICTNESS_STRICT : STRICTNESS_BALANCED;
|
|
1068
1048
|
}
|
|
1069
1049
|
return strictness;
|
|
@@ -1074,81 +1054,45 @@ function getMaxAppendedDataSize(maxAppendedDataSize, strictness) {
|
|
|
1074
1054
|
return maxAppendedDataSize;
|
|
1075
1055
|
}
|
|
1076
1056
|
if (strictness == STRICTNESS_STRICT) {
|
|
1077
|
-
// the comment must reach the end of the file: no trailing data tolerated
|
|
1078
1057
|
return 0;
|
|
1079
1058
|
}
|
|
1080
1059
|
if (strictness == STRICTNESS_TOLERANT) {
|
|
1081
1060
|
return Infinity;
|
|
1082
1061
|
}
|
|
1083
|
-
// balanced: tolerate up to a legal comment's worth of trailing data (a 16-bit length); beyond that the
|
|
1084
|
-
// trailing bytes cannot hide inside a comment, so they are treated as a second thing bolted onto the file
|
|
1085
1062
|
return MAX_16_BITS;
|
|
1086
1063
|
}
|
|
1087
1064
|
|
|
1088
|
-
// cap on how many out-of-window central directory probes the scan below may issue. A legitimate archive needs
|
|
1089
|
-
// at most a couple (the canonical record, plus one more to detect genuine ambiguity); every other reachability
|
|
1090
|
-
// check is served from the tail window already in memory. The cap keeps an archive whose comment is stuffed
|
|
1091
|
-
// with unreachable end-anchored records from forcing an unbounded number of (potentially remote) reads.
|
|
1092
1065
|
const MAX_END_OF_CENTRAL_DIR_PROBES = 64;
|
|
1093
1066
|
|
|
1094
|
-
// reachability rankings for an end-anchored candidate record (see getCentralDirectoryReachability)
|
|
1095
1067
|
const CENTRAL_DIRECTORY_UNREACHABLE = 0;
|
|
1096
1068
|
const CENTRAL_DIRECTORY_PLAUSIBLE = 1;
|
|
1097
1069
|
const CENTRAL_DIRECTORY_REACHABLE = 2;
|
|
1098
1070
|
|
|
1099
|
-
// Locates the authoritative end of central directory record. The canonical record is the last one whose
|
|
1100
|
-
// declared comment reaches exactly the end of the file ("end-anchored") and that points to a central directory
|
|
1101
|
-
// ("reachable"); earlier signatures (stale append remnants, bytes embedded in a comment) are ignored. When two
|
|
1102
|
-
// or more end-anchored records point to a central directory the archive is ambiguous, and the count is returned
|
|
1103
|
-
// so the caller can refuse it. A record that reaches the end of the file but points to no central directory (an
|
|
1104
|
-
// empty archive) is only "plausible": it cannot be corroborated, so it never counts towards ambiguity and is
|
|
1105
|
-
// chosen only when no reachable record exists — otherwise an empty record forged in a comment could outrank the
|
|
1106
|
-
// genuine directory. When no record is end-anchored (appended data or an oversized comment), falls back to the
|
|
1107
|
-
// last record within the tolerated window that points to a directory (see seekEndOfCentralDirectory).
|
|
1108
1071
|
async function findEndOfCentralDirectory(reader, rejectAmbiguous, maxAppendedDataSize) {
|
|
1109
1072
|
const { size } = reader;
|
|
1110
|
-
// an end-anchored record can only live within the last END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS bytes,
|
|
1111
|
-
// since the comment length that ties it to the end of the file is a 16-bit field
|
|
1112
1073
|
const anchoredLength = Math.min(size, END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS);
|
|
1113
|
-
const anchoredOffset = size - anchoredLength;
|
|
1114
|
-
const anchoredArray = await readUint8Array(reader, anchoredOffset, anchoredLength);
|
|
1115
|
-
const anchoredView = getDataView(anchoredArray);
|
|
1116
1074
|
const remoteProbeBudget = { count: MAX_END_OF_CENTRAL_DIR_PROBES };
|
|
1117
1075
|
let endOfDirectoryInfo;
|
|
1118
1076
|
let plausibleEndOfDirectoryInfo;
|
|
1119
1077
|
let endOfDirectoryReachingEndCount = 0;
|
|
1120
|
-
for (
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const reachability = await getCentralDirectoryReachability(reader, anchoredView, anchoredOffset, indexByte, offset, size, remoteProbeBudget);
|
|
1128
|
-
if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
|
|
1129
|
-
if (!endOfDirectoryInfo) {
|
|
1130
|
-
endOfDirectoryInfo = {
|
|
1131
|
-
offset,
|
|
1132
|
-
buffer: anchoredArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
endOfDirectoryReachingEndCount++;
|
|
1136
|
-
// the canonical record (highest offset) is found first; a second one is enough to flag ambiguity
|
|
1137
|
-
if (!rejectAmbiguous || endOfDirectoryReachingEndCount > 1) {
|
|
1138
|
-
break;
|
|
1139
|
-
}
|
|
1140
|
-
} else if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleEndOfDirectoryInfo) {
|
|
1141
|
-
plausibleEndOfDirectoryInfo = {
|
|
1142
|
-
offset,
|
|
1143
|
-
buffer: anchoredArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer
|
|
1144
|
-
};
|
|
1078
|
+
for await (const [anchoredView, anchoredOffset, anchoredArray, indexByte, offset] of scanEndOfCentralDirectory(reader, anchoredLength)) {
|
|
1079
|
+
const commentLength = getUint16(anchoredView, indexByte + 20);
|
|
1080
|
+
if (offset + END_OF_CENTRAL_DIR_LENGTH + commentLength == size) {
|
|
1081
|
+
const reachability = await getCentralDirectoryReachability(reader, anchoredView, anchoredOffset, indexByte, offset, size, remoteProbeBudget);
|
|
1082
|
+
if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
|
|
1083
|
+
if (!endOfDirectoryInfo) {
|
|
1084
|
+
endOfDirectoryInfo = getEndOfCentralDirectoryInfo(anchoredArray, indexByte, offset);
|
|
1145
1085
|
}
|
|
1086
|
+
endOfDirectoryReachingEndCount++;
|
|
1087
|
+
if (!rejectAmbiguous || endOfDirectoryReachingEndCount > 1) {
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
} else if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleEndOfDirectoryInfo) {
|
|
1091
|
+
plausibleEndOfDirectoryInfo = getEndOfCentralDirectoryInfo(anchoredArray, indexByte, offset);
|
|
1146
1092
|
}
|
|
1147
1093
|
}
|
|
1148
1094
|
}
|
|
1149
1095
|
if (!endOfDirectoryInfo) {
|
|
1150
|
-
// no record points to a directory: prefer a plausible (empty) end-anchored record before scanning the
|
|
1151
|
-
// tolerated window for a bare signature
|
|
1152
1096
|
endOfDirectoryInfo = plausibleEndOfDirectoryInfo;
|
|
1153
1097
|
}
|
|
1154
1098
|
if (!endOfDirectoryInfo) {
|
|
@@ -1157,69 +1101,53 @@ async function findEndOfCentralDirectory(reader, rejectAmbiguous, maxAppendedDat
|
|
|
1157
1101
|
return { endOfDirectoryInfo, endOfDirectoryReachingEndCount };
|
|
1158
1102
|
}
|
|
1159
1103
|
|
|
1160
|
-
// Fallback for findEndOfCentralDirectory when no record is end-anchored: appended data or an oversized comment
|
|
1161
|
-
// pushed the record's declared comment past the end of the file, so its position can no longer be reconciled
|
|
1162
|
-
// against the end of the file. Scans the tolerated window from the end and returns the first record that points
|
|
1163
|
-
// to a central directory, so a stray signature embedded in appended data cannot hijack discovery (which the
|
|
1164
|
-
// anchored scan already prevents for anchored records). Falls back to the first plausible (empty) record, then
|
|
1165
|
-
// to the last bare signature, so a degenerate archive still opens as before. The window ends at the file, so the
|
|
1166
|
-
// same in-window / metered reads as the anchored scan apply.
|
|
1167
1104
|
async function seekEndOfCentralDirectory(reader, maxAppendedDataSize, remoteProbeBudget) {
|
|
1168
1105
|
const { size } = reader;
|
|
1169
1106
|
const searchLength = Math.min(size, maxAppendedDataSize == Infinity ? size :
|
|
1170
1107
|
END_OF_CENTRAL_DIR_LENGTH + MAX_16_BITS + maxAppendedDataSize);
|
|
1171
|
-
const searchOffset = size - searchLength;
|
|
1172
|
-
const searchArray = await readUint8Array(reader, searchOffset, searchLength);
|
|
1173
|
-
const searchView = getDataView(searchArray);
|
|
1174
1108
|
let firstSignatureInfo, plausibleInfo;
|
|
1175
|
-
for (
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleInfo) {
|
|
1187
|
-
plausibleInfo = record;
|
|
1188
|
-
}
|
|
1109
|
+
for await (const [searchView, searchOffset, searchArray, indexByte, offset] of scanEndOfCentralDirectory(reader, searchLength)) {
|
|
1110
|
+
const record = getEndOfCentralDirectoryInfo(searchArray, indexByte, offset);
|
|
1111
|
+
if (!firstSignatureInfo) {
|
|
1112
|
+
firstSignatureInfo = record;
|
|
1113
|
+
}
|
|
1114
|
+
const reachability = await getCentralDirectoryReachability(reader, searchView, searchOffset, indexByte, offset, size, remoteProbeBudget);
|
|
1115
|
+
if (reachability == CENTRAL_DIRECTORY_REACHABLE) {
|
|
1116
|
+
return record;
|
|
1117
|
+
}
|
|
1118
|
+
if (reachability == CENTRAL_DIRECTORY_PLAUSIBLE && !plausibleInfo) {
|
|
1119
|
+
plausibleInfo = record;
|
|
1189
1120
|
}
|
|
1190
1121
|
}
|
|
1191
1122
|
return plausibleInfo || firstSignatureInfo;
|
|
1192
1123
|
}
|
|
1193
1124
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1125
|
+
async function* scanEndOfCentralDirectory(reader, scanLength) {
|
|
1126
|
+
const scanOffset = reader.size - scanLength;
|
|
1127
|
+
const scanArray = await readUint8Array(reader, scanOffset, scanLength);
|
|
1128
|
+
const scanView = getDataView(scanArray);
|
|
1129
|
+
for (let indexByte = scanArray.length - END_OF_CENTRAL_DIR_LENGTH; indexByte >= 0; indexByte--) {
|
|
1130
|
+
if (getUint32(scanView, indexByte) == END_OF_CENTRAL_DIR_SIGNATURE) {
|
|
1131
|
+
yield [scanView, scanOffset, scanArray, indexByte, scanOffset + indexByte];
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function getEndOfCentralDirectoryInfo(scanArray, indexByte, offset) {
|
|
1137
|
+
return { offset, buffer: scanArray.slice(indexByte, indexByte + END_OF_CENTRAL_DIR_LENGTH).buffer };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1207
1140
|
async function getCentralDirectoryReachability(reader, view, anchoredOffset, indexByte, offset, size, remoteProbeBudget) {
|
|
1208
1141
|
const filesLength = getUint16(view, indexByte + 10);
|
|
1209
1142
|
const directoryDataLength = getUint32(view, indexByte + 12);
|
|
1210
1143
|
const directoryDataOffset = getUint32(view, indexByte + 16);
|
|
1211
1144
|
if (filesLength == MAX_16_BITS || directoryDataLength == MAX_32_BITS || directoryDataOffset == MAX_32_BITS) {
|
|
1212
|
-
// saturated fields imply a zip64 record; corroborate it with the zip64 end of central directory locator
|
|
1213
|
-
// that sits immediately before it, so saturated bytes inside a comment cannot masquerade as one
|
|
1214
1145
|
const locatorSignature = await readSignature(reader, view, anchoredOffset, offset - ZIP64_END_OF_CENTRAL_DIR_LOCATOR_LENGTH, size, remoteProbeBudget);
|
|
1215
1146
|
return locatorSignature == ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIGNATURE ? CENTRAL_DIRECTORY_REACHABLE : CENTRAL_DIRECTORY_UNREACHABLE;
|
|
1216
1147
|
}
|
|
1217
1148
|
if (!filesLength && !directoryDataLength) {
|
|
1218
|
-
// a valid empty archive has no central directory to dereference
|
|
1219
1149
|
return CENTRAL_DIRECTORY_PLAUSIBLE;
|
|
1220
1150
|
}
|
|
1221
|
-
// the central directory ends where this record starts; reconcile the stored offset against that actual
|
|
1222
|
-
// position (see stored_cd_offset ⟺ eocd_pos − cd_size) so prepended data does not hide the directory
|
|
1223
1151
|
for (const centralDirectoryOffset of [offset - directoryDataLength, directoryDataOffset]) {
|
|
1224
1152
|
if (await readSignature(reader, view, anchoredOffset, centralDirectoryOffset, size, remoteProbeBudget) == CENTRAL_FILE_HEADER_SIGNATURE) {
|
|
1225
1153
|
return CENTRAL_DIRECTORY_REACHABLE;
|
|
@@ -1228,15 +1156,11 @@ async function getCentralDirectoryReachability(reader, view, anchoredOffset, ind
|
|
|
1228
1156
|
return CENTRAL_DIRECTORY_UNREACHABLE;
|
|
1229
1157
|
}
|
|
1230
1158
|
|
|
1231
|
-
// Reads a 4-byte little-endian signature at `signatureOffset`, served from the tail window (`view`, spanning
|
|
1232
|
-
// `[anchoredOffset, size)`) when it falls inside it and otherwise from a metered (possibly remote) read. Returns
|
|
1233
|
-
// UNDEFINED_VALUE when the position is out of range or the out-of-window read budget is exhausted.
|
|
1234
1159
|
async function readSignature(reader, view, anchoredOffset, signatureOffset, size, remoteProbeBudget) {
|
|
1235
1160
|
if (signatureOffset < 0 || signatureOffset + 4 > size) {
|
|
1236
1161
|
return UNDEFINED_VALUE;
|
|
1237
1162
|
}
|
|
1238
1163
|
if (signatureOffset >= anchoredOffset) {
|
|
1239
|
-
// the target sits inside the tail window already in memory: no extra read needed
|
|
1240
1164
|
return getUint32(view, signatureOffset - anchoredOffset);
|
|
1241
1165
|
}
|
|
1242
1166
|
if (remoteProbeBudget.count > 0) {
|
|
@@ -1244,7 +1168,6 @@ async function readSignature(reader, view, anchoredOffset, signatureOffset, size
|
|
|
1244
1168
|
const signatureArray = await readUint8Array(reader, signatureOffset, 4);
|
|
1245
1169
|
return getUint32(getDataView(signatureArray), 0);
|
|
1246
1170
|
}
|
|
1247
|
-
// out of budget: leave this candidate unverified rather than issue another read
|
|
1248
1171
|
return UNDEFINED_VALUE;
|
|
1249
1172
|
}
|
|
1250
1173
|
|
|
@@ -1260,8 +1183,6 @@ function checkLocalDirectory(zipEntry, localDirectory, rawLocalFilename) {
|
|
|
1260
1183
|
if (localDirectory.compressionMethod != zipEntry.compressionMethod) {
|
|
1261
1184
|
throwAmbiguousArchive("mismatched local file header (compression method)");
|
|
1262
1185
|
}
|
|
1263
|
-
// when the data descriptor flag is set, the local signature and sizes are zeroed out; all-zero values are
|
|
1264
|
-
// also tolerated when the flag is clear because some tools produce them (streaming writers predating bit 3)
|
|
1265
1186
|
if (!localDirectory.bitFlag.dataDescriptor &&
|
|
1266
1187
|
(localDirectory.signature || localDirectory.compressedSize || localDirectory.uncompressedSize) &&
|
|
1267
1188
|
(localDirectory.signature != zipEntry.signature ||
|