styled-map-package 4.0.1 → 4.1.0
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/bin/smp-download.js +1 -1
- package/bin/smp-view.js +32 -23
- package/dist/download.d.cts +1 -1
- package/dist/download.d.ts +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/reader-watch.d.cts +1 -1
- package/dist/reader-watch.d.ts +1 -1
- package/dist/reader.cjs +2 -1
- package/dist/reader.d.cts +1 -1
- package/dist/reader.d.ts +1 -1
- package/dist/reader.js +2 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/style-downloader.cjs +2 -2
- package/dist/style-downloader.d.cts +1 -1
- package/dist/style-downloader.d.ts +1 -1
- package/dist/style-downloader.js +2 -2
- package/dist/tile-downloader.cjs +2 -2
- package/dist/tile-downloader.d.cts +1 -1
- package/dist/tile-downloader.d.ts +1 -1
- package/dist/tile-downloader.js +2 -2
- package/dist/{types-B4Xn1F9K.d.cts → types-yLQy3AKR.d.cts} +6 -3
- package/dist/{types-B4Xn1F9K.d.ts → types-yLQy3AKR.d.ts} +6 -3
- package/dist/utils/file-formats.d.cts +1 -1
- package/dist/utils/file-formats.d.ts +1 -1
- package/dist/utils/style.cjs +2 -2
- package/dist/utils/style.d.cts +1 -1
- package/dist/utils/style.d.ts +1 -1
- package/dist/utils/style.js +2 -2
- package/dist/utils/templates.cjs +4 -0
- package/dist/utils/templates.d.cts +4 -2
- package/dist/utils/templates.d.ts +4 -2
- package/dist/utils/templates.js +2 -0
- package/dist/writer.cjs +51 -3
- package/dist/writer.d.cts +1 -1
- package/dist/writer.d.ts +1 -1
- package/dist/writer.js +53 -3
- package/node_modules/@node-rs/crc32/LICENSE +21 -0
- package/node_modules/@node-rs/crc32/README.md +61 -0
- package/node_modules/@node-rs/crc32/browser.js +1 -0
- package/node_modules/@node-rs/crc32/index.d.ts +5 -0
- package/node_modules/@node-rs/crc32/index.js +368 -0
- package/node_modules/@node-rs/crc32/package.json +97 -0
- package/node_modules/@node-rs/crc32-linux-x64-gnu/README.md +3 -0
- package/node_modules/@node-rs/crc32-linux-x64-gnu/crc32.linux-x64-gnu.node +0 -0
- package/node_modules/@node-rs/crc32-linux-x64-gnu/package.json +43 -0
- package/node_modules/@node-rs/crc32-linux-x64-musl/README.md +3 -0
- package/node_modules/@node-rs/crc32-linux-x64-musl/crc32.linux-x64-musl.node +0 -0
- package/node_modules/@node-rs/crc32-linux-x64-musl/package.json +43 -0
- package/node_modules/define-data-property/.eslintrc +24 -0
- package/node_modules/define-data-property/.github/FUNDING.yml +12 -0
- package/node_modules/define-data-property/.nycrc +13 -0
- package/node_modules/define-data-property/CHANGELOG.md +70 -0
- package/node_modules/define-data-property/LICENSE +21 -0
- package/node_modules/define-data-property/README.md +67 -0
- package/node_modules/define-data-property/index.d.ts +12 -0
- package/node_modules/define-data-property/index.js +56 -0
- package/node_modules/define-data-property/package.json +106 -0
- package/node_modules/define-data-property/test/index.js +392 -0
- package/node_modules/define-data-property/tsconfig.json +59 -0
- package/node_modules/define-properties/.editorconfig +13 -0
- package/node_modules/define-properties/.eslintrc +19 -0
- package/node_modules/define-properties/.github/FUNDING.yml +12 -0
- package/node_modules/define-properties/.nycrc +9 -0
- package/node_modules/define-properties/CHANGELOG.md +91 -0
- package/node_modules/define-properties/LICENSE +21 -0
- package/node_modules/define-properties/README.md +84 -0
- package/node_modules/define-properties/index.js +47 -0
- package/node_modules/define-properties/package.json +88 -0
- package/node_modules/es-define-property/.eslintrc +13 -0
- package/node_modules/es-define-property/.github/FUNDING.yml +12 -0
- package/node_modules/es-define-property/.nycrc +9 -0
- package/node_modules/es-define-property/CHANGELOG.md +29 -0
- package/node_modules/es-define-property/LICENSE +21 -0
- package/node_modules/es-define-property/README.md +49 -0
- package/node_modules/es-define-property/index.d.ts +3 -0
- package/node_modules/es-define-property/index.js +14 -0
- package/node_modules/es-define-property/package.json +81 -0
- package/node_modules/es-define-property/test/index.js +56 -0
- package/node_modules/es-define-property/tsconfig.json +10 -0
- package/node_modules/es-errors/.eslintrc +5 -0
- package/node_modules/es-errors/.github/FUNDING.yml +12 -0
- package/node_modules/es-errors/CHANGELOG.md +40 -0
- package/node_modules/es-errors/LICENSE +21 -0
- package/node_modules/es-errors/README.md +55 -0
- package/node_modules/es-errors/eval.d.ts +3 -0
- package/node_modules/es-errors/eval.js +4 -0
- package/node_modules/es-errors/index.d.ts +3 -0
- package/node_modules/es-errors/index.js +4 -0
- package/node_modules/es-errors/package.json +80 -0
- package/node_modules/es-errors/range.d.ts +3 -0
- package/node_modules/es-errors/range.js +4 -0
- package/node_modules/es-errors/ref.d.ts +3 -0
- package/node_modules/es-errors/ref.js +4 -0
- package/node_modules/es-errors/syntax.d.ts +3 -0
- package/node_modules/es-errors/syntax.js +4 -0
- package/node_modules/es-errors/test/index.js +19 -0
- package/node_modules/es-errors/tsconfig.json +49 -0
- package/node_modules/es-errors/type.d.ts +3 -0
- package/node_modules/es-errors/type.js +4 -0
- package/node_modules/es-errors/uri.d.ts +3 -0
- package/node_modules/es-errors/uri.js +4 -0
- package/node_modules/globalthis/.eslintrc +18 -0
- package/node_modules/globalthis/.nycrc +10 -0
- package/node_modules/globalthis/CHANGELOG.md +109 -0
- package/node_modules/globalthis/LICENSE +21 -0
- package/node_modules/globalthis/README.md +70 -0
- package/node_modules/globalthis/auto.js +3 -0
- package/node_modules/globalthis/implementation.browser.js +11 -0
- package/node_modules/globalthis/implementation.js +3 -0
- package/node_modules/globalthis/index.js +19 -0
- package/node_modules/globalthis/package.json +99 -0
- package/node_modules/globalthis/polyfill.js +10 -0
- package/node_modules/globalthis/shim.js +29 -0
- package/node_modules/globalthis/test/implementation.js +11 -0
- package/node_modules/globalthis/test/index.js +11 -0
- package/node_modules/globalthis/test/native.js +26 -0
- package/node_modules/globalthis/test/shimmed.js +29 -0
- package/node_modules/globalthis/test/tests.js +36 -0
- package/node_modules/gopd/.eslintrc +16 -0
- package/node_modules/gopd/.github/FUNDING.yml +12 -0
- package/node_modules/gopd/CHANGELOG.md +45 -0
- package/node_modules/gopd/LICENSE +21 -0
- package/node_modules/gopd/README.md +40 -0
- package/node_modules/gopd/gOPD.d.ts +1 -0
- package/node_modules/gopd/gOPD.js +4 -0
- package/node_modules/gopd/index.d.ts +5 -0
- package/node_modules/gopd/index.js +15 -0
- package/node_modules/gopd/package.json +77 -0
- package/node_modules/gopd/test/index.js +36 -0
- package/node_modules/gopd/tsconfig.json +9 -0
- package/node_modules/has-property-descriptors/.eslintrc +13 -0
- package/node_modules/has-property-descriptors/.github/FUNDING.yml +12 -0
- package/node_modules/has-property-descriptors/.nycrc +9 -0
- package/node_modules/has-property-descriptors/CHANGELOG.md +35 -0
- package/node_modules/has-property-descriptors/LICENSE +21 -0
- package/node_modules/has-property-descriptors/README.md +43 -0
- package/node_modules/has-property-descriptors/index.js +22 -0
- package/node_modules/has-property-descriptors/package.json +77 -0
- package/node_modules/has-property-descriptors/test/index.js +57 -0
- package/node_modules/is-it-type/License +19 -0
- package/node_modules/is-it-type/README.md +102 -0
- package/node_modules/is-it-type/changelog.md +239 -0
- package/node_modules/is-it-type/dist/cjs/is-it-type.js +173 -0
- package/node_modules/is-it-type/dist/cjs/is-it-type.js.map +1 -0
- package/node_modules/is-it-type/dist/cjs/is-it-type.min.js +2 -0
- package/node_modules/is-it-type/dist/cjs/is-it-type.min.js.map +1 -0
- package/node_modules/is-it-type/dist/esm/is-it-type.js +143 -0
- package/node_modules/is-it-type/dist/esm/is-it-type.js.map +1 -0
- package/node_modules/is-it-type/dist/esm/is-it-type.min.js +2 -0
- package/node_modules/is-it-type/dist/esm/is-it-type.min.js.map +1 -0
- package/node_modules/is-it-type/dist/esm/package.json +3 -0
- package/node_modules/is-it-type/dist/umd/is-it-type.js +450 -0
- package/node_modules/is-it-type/dist/umd/is-it-type.js.map +1 -0
- package/node_modules/is-it-type/dist/umd/is-it-type.min.js +2 -0
- package/node_modules/is-it-type/dist/umd/is-it-type.min.js.map +1 -0
- package/node_modules/is-it-type/es/index.js +8 -0
- package/node_modules/is-it-type/es/package.json +3 -0
- package/node_modules/is-it-type/index.js +10 -0
- package/node_modules/is-it-type/package.json +87 -0
- package/node_modules/is-it-type/src/index.js +169 -0
- package/node_modules/object-keys/.editorconfig +13 -0
- package/node_modules/object-keys/.eslintrc +17 -0
- package/node_modules/object-keys/.travis.yml +277 -0
- package/node_modules/object-keys/CHANGELOG.md +232 -0
- package/node_modules/object-keys/LICENSE +21 -0
- package/node_modules/object-keys/README.md +76 -0
- package/node_modules/object-keys/implementation.js +122 -0
- package/node_modules/object-keys/index.js +32 -0
- package/node_modules/object-keys/isArguments.js +17 -0
- package/node_modules/object-keys/package.json +88 -0
- package/node_modules/object-keys/test/index.js +5 -0
- package/node_modules/simple-invariant/License +19 -0
- package/node_modules/simple-invariant/README.md +64 -0
- package/node_modules/simple-invariant/changelog.md +31 -0
- package/node_modules/simple-invariant/index.js +19 -0
- package/node_modules/simple-invariant/package.json +50 -0
- package/node_modules/yauzl-promise/License +19 -0
- package/node_modules/yauzl-promise/README.md +440 -0
- package/node_modules/yauzl-promise/index.js +10 -0
- package/node_modules/yauzl-promise/lib/entry.js +312 -0
- package/node_modules/yauzl-promise/lib/index.js +160 -0
- package/node_modules/yauzl-promise/lib/reader.js +289 -0
- package/node_modules/yauzl-promise/lib/shared.js +20 -0
- package/node_modules/yauzl-promise/lib/utils.js +105 -0
- package/node_modules/yauzl-promise/lib/zip.js +1224 -0
- package/node_modules/yauzl-promise/package.json +56 -0
- package/package.json +9 -11
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
/* --------------------
|
|
2
|
+
* yauzl-promise module
|
|
3
|
+
* `Zip` class
|
|
4
|
+
* ------------------*/
|
|
5
|
+
|
|
6
|
+
/* global WeakRef */
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Modules
|
|
11
|
+
const calculateCrc32 = require('@node-rs/crc32').crc32,
|
|
12
|
+
assert = require('simple-invariant'),
|
|
13
|
+
{isPositiveIntegerOrZero} = require('is-it-type');
|
|
14
|
+
|
|
15
|
+
// Imports
|
|
16
|
+
const Entry = require('./entry.js'),
|
|
17
|
+
{INTERNAL_SYMBOL, uncertainUncompressedSizeEntriesRegistry} = require('./shared.js'),
|
|
18
|
+
{decodeBuffer, validateFilename, readUInt64LE} = require('./utils.js');
|
|
19
|
+
|
|
20
|
+
// Exports
|
|
21
|
+
|
|
22
|
+
// Spec of ZIP format is here: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
|
|
23
|
+
// Also: https://libzip.org/specifications/appnote_iz.txt
|
|
24
|
+
|
|
25
|
+
const EOCDR_WITHOUT_COMMENT_SIZE = 22,
|
|
26
|
+
MAX_EOCDR_COMMENT_SIZE = 0xFFFF,
|
|
27
|
+
MAC_CDH_EXTRA_FIELD_ID = 22613,
|
|
28
|
+
MAC_CDH_EXTRA_FIELD_LENGTH = 8,
|
|
29
|
+
MAC_CDH_EXTRA_FIELDS_LENGTH = MAC_CDH_EXTRA_FIELD_LENGTH + 4, // Field data + ID + len (2 bytes each)
|
|
30
|
+
MAC_LFH_EXTRA_FIELDS_LENGTH = 16,
|
|
31
|
+
CDH_MIN_LENGTH = 46,
|
|
32
|
+
CDH_MAX_LENGTH = CDH_MIN_LENGTH + 0xFFFF * 3, // 3 = Filename, extra fields, comment
|
|
33
|
+
CDH_MAX_LENGTH_MAC = CDH_MIN_LENGTH + 0xFFFF + MAC_CDH_EXTRA_FIELDS_LENGTH, // No comment
|
|
34
|
+
FOUR_GIB = 0x100000000; // Math.pow(2, 32)
|
|
35
|
+
|
|
36
|
+
class Zip {
|
|
37
|
+
/**
|
|
38
|
+
* Class representing ZIP file.
|
|
39
|
+
* Class is exported in public interface, for purpose of `instanceof` checks, but constructor cannot
|
|
40
|
+
* be called by user. This is enforced by use of private symbol `INTERNAL_SYMBOL`.
|
|
41
|
+
* @class
|
|
42
|
+
* @param {Object} testSymbol - Must be `INTERNAL_SYMBOL`
|
|
43
|
+
* @param {Object} reader - `Reader` to use to access the ZIP
|
|
44
|
+
* @param {number} size - Size of ZIP file in bytes
|
|
45
|
+
* @param {Object} options - Options
|
|
46
|
+
* @param {boolean} [options.decodeStrings=true] - Decode filenames and comments to strings
|
|
47
|
+
* @param {boolean} [options.validateEntrySizes=true] - Validate entry sizes
|
|
48
|
+
* @param {boolean} [options.validateFilenames=true] - Validate filenames
|
|
49
|
+
* @param {boolean} [options.strictFilenames=false] - Don't allow backslashes (`\`) in filenames
|
|
50
|
+
* @param {boolean} [options.supportMacArchive=true] - Support Mac OS Archive Utility faulty ZIP files
|
|
51
|
+
*/
|
|
52
|
+
constructor(testSymbol, reader, size, options) {
|
|
53
|
+
assert(
|
|
54
|
+
testSymbol === INTERNAL_SYMBOL,
|
|
55
|
+
'Zip class cannot be instantiated directly. Use one of the static methods.'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
this.reader = reader;
|
|
59
|
+
this.size = size;
|
|
60
|
+
Object.assign(this, options);
|
|
61
|
+
this.isZip64 = null;
|
|
62
|
+
this.entryCount = null;
|
|
63
|
+
this.entryCountIsCertain = true;
|
|
64
|
+
this.footerOffset = null;
|
|
65
|
+
this.centralDirectoryOffset = null;
|
|
66
|
+
this.centralDirectorySize = null;
|
|
67
|
+
this.centralDirectorySizeIsCertain = true;
|
|
68
|
+
this.comment = null;
|
|
69
|
+
this.numEntriesRead = 0;
|
|
70
|
+
this.isMacArchive = false;
|
|
71
|
+
this.isMaybeMacArchive = false;
|
|
72
|
+
this.compressedSizesAreCertain = true;
|
|
73
|
+
this.uncompressedSizesAreCertain = true;
|
|
74
|
+
this._isReading = false;
|
|
75
|
+
this._entryCursor = null;
|
|
76
|
+
this._fileCursor = null;
|
|
77
|
+
this._uncertainUncompressedSizeEntryRefs = null;
|
|
78
|
+
this._firstEntryProps = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Close ZIP file. Underlying reader will be closed.
|
|
83
|
+
* @async
|
|
84
|
+
* @returns {undefined}
|
|
85
|
+
*/
|
|
86
|
+
close() {
|
|
87
|
+
return this.reader.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Getter for whether `Zip` is open for reading.
|
|
92
|
+
* @returns {boolean} - `true` if open
|
|
93
|
+
*/
|
|
94
|
+
get isOpen() {
|
|
95
|
+
return this.reader.isOpen;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Locate Central Directory.
|
|
100
|
+
* @async
|
|
101
|
+
* @returns {undefined}
|
|
102
|
+
*/
|
|
103
|
+
async _init() {
|
|
104
|
+
// Parse End of Central Directory Record + ZIP64 extension
|
|
105
|
+
// to get location of the Central Directory
|
|
106
|
+
const eocdrBuffer = await this._locateEocdr();
|
|
107
|
+
this._parseEocdr(eocdrBuffer);
|
|
108
|
+
if (this.isZip64) await this._parseZip64Eocdr();
|
|
109
|
+
await this._locateCentralDirectory();
|
|
110
|
+
this._entryCursor = this.centralDirectoryOffset;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Locate End of Central Directory Record.
|
|
115
|
+
* @async
|
|
116
|
+
* @returns {Buffer} - Buffer containing EOCDR
|
|
117
|
+
*/
|
|
118
|
+
async _locateEocdr() {
|
|
119
|
+
// Last field of the End of Central Directory Record is a variable-length comment.
|
|
120
|
+
// The comment size is encoded in a 2-byte field in the EOCDR, which we can't find without trudging
|
|
121
|
+
// backwards through the comment to find it.
|
|
122
|
+
// As a consequence of this design decision, it's possible to have ambiguous ZIP file metadata
|
|
123
|
+
// if a coherent EOCDR was in the comment.
|
|
124
|
+
// Search backwards for a EOCDR signature.
|
|
125
|
+
let bufferSize = EOCDR_WITHOUT_COMMENT_SIZE + MAX_EOCDR_COMMENT_SIZE;
|
|
126
|
+
if (this.size < bufferSize) {
|
|
127
|
+
assert(this.size >= EOCDR_WITHOUT_COMMENT_SIZE, 'End of Central Directory Record not found');
|
|
128
|
+
bufferSize = this.size;
|
|
129
|
+
}
|
|
130
|
+
const bufferOffset = this.size - bufferSize;
|
|
131
|
+
const buffer = await this.reader.read(bufferOffset, bufferSize);
|
|
132
|
+
let pos;
|
|
133
|
+
for (pos = bufferSize - EOCDR_WITHOUT_COMMENT_SIZE; pos >= 0; pos--) {
|
|
134
|
+
if (buffer[pos] !== 0x50) continue;
|
|
135
|
+
if (buffer.readUInt32LE(pos) !== 0x06054b50) continue;
|
|
136
|
+
|
|
137
|
+
const commentLength = buffer.readUInt16LE(pos + 20);
|
|
138
|
+
if (commentLength === bufferSize - pos - EOCDR_WITHOUT_COMMENT_SIZE) {
|
|
139
|
+
this.footerOffset = bufferOffset + pos;
|
|
140
|
+
return buffer.subarray(pos);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
throw new Error('End of Central Directory Record not found');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse End of Central Directory Record.
|
|
148
|
+
* Get Central Directory location, size and entry count.
|
|
149
|
+
* @param {Buffer} eocdrBuffer - Buffer containing EOCDR
|
|
150
|
+
* @returns {undefined}
|
|
151
|
+
*/
|
|
152
|
+
_parseEocdr(eocdrBuffer) {
|
|
153
|
+
// Bytes 0-3: End of Central Directory Record signature = 0x06054b50
|
|
154
|
+
// Bytes 4-5: Number of this disk
|
|
155
|
+
const diskNumber = eocdrBuffer.readUInt16LE(4);
|
|
156
|
+
assert(diskNumber === 0, 'Multi-disk ZIP files are not supported');
|
|
157
|
+
// Bytes 6-7: Disk where Central Directory starts
|
|
158
|
+
// Bytes 8-9: Number of Central Directory records on this disk
|
|
159
|
+
// Bytes 10-11: Total number of Central Directory records
|
|
160
|
+
this.entryCount = eocdrBuffer.readUInt16LE(10);
|
|
161
|
+
// Bytes 12-15: Size of Central Directory (bytes)
|
|
162
|
+
this.centralDirectorySize = eocdrBuffer.readUInt32LE(12);
|
|
163
|
+
// Bytes 16-19: Offset of Central Directory
|
|
164
|
+
this.centralDirectoryOffset = eocdrBuffer.readUInt32LE(16);
|
|
165
|
+
// Bytes 22-...: Comment. Encoding is always CP437.
|
|
166
|
+
// Copy buffer instead of slicing, so rest of buffer can be garbage collected.
|
|
167
|
+
this.comment = this.decodeStrings
|
|
168
|
+
? decodeBuffer(eocdrBuffer, 22, false)
|
|
169
|
+
: Buffer.from(eocdrBuffer.subarray(22));
|
|
170
|
+
|
|
171
|
+
// Original Yauzl does not check `centralDirectorySize` here, only offset, though ZIP spec suggests
|
|
172
|
+
// both should be checked. I suspect this is a bug in Yauzl, and it has remained undiscovered
|
|
173
|
+
// because ZIP files with a Central Directory > 4 GiB are vanishingly rare
|
|
174
|
+
// (would require millions of files, or thousands of files with very long filenames/comments).
|
|
175
|
+
this.isZip64 = this.entryCount === 0xFFFF || this.centralDirectoryOffset === 0xFFFFFFFF
|
|
176
|
+
|| this.centralDirectorySize === 0xFFFFFFFF;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse ZIP64 End of Central Directory Locator + Record.
|
|
181
|
+
* Get Central Directory location, size and entry count, where ZIP64 extension used.
|
|
182
|
+
* @async
|
|
183
|
+
* @returns {undefined}
|
|
184
|
+
*/
|
|
185
|
+
async _parseZip64Eocdr() {
|
|
186
|
+
// Parse ZIP64 End of Central Directory Locator
|
|
187
|
+
const zip64EocdlOffset = this.footerOffset - 20;
|
|
188
|
+
assert(zip64EocdlOffset >= 0, 'Cannot locate ZIP64 End of Central Directory Locator');
|
|
189
|
+
const zip64EocdlBuffer = await this.reader.read(zip64EocdlOffset, 20);
|
|
190
|
+
// Bytes 0-3: ZIP64 End of Central Directory Locator signature = 0x07064b50
|
|
191
|
+
if (zip64EocdlBuffer.readUInt32LE(0) !== 0x07064b50) {
|
|
192
|
+
if (this.supportMacArchive) {
|
|
193
|
+
// Assume this is a faulty Mac OS archive which happens to have entry count of 65535 (possible)
|
|
194
|
+
// or Central Directory size/offset of 4 GiB - 1 (much less likely, but possible).
|
|
195
|
+
// If it's not, we'll get another error when trying to read the Central Directory.
|
|
196
|
+
this.isMacArchive = true;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
throw new Error('Invalid ZIP64 End of Central Directory Locator signature');
|
|
200
|
+
}
|
|
201
|
+
// Bytes 4-7 - Number of the disk with the start of the ZIP64 End of Central Directory Record
|
|
202
|
+
// Bytes 8-15: Position of ZIP64 End of Central Directory Record
|
|
203
|
+
const zip64EocdrOffset = readUInt64LE(zip64EocdlBuffer, 8);
|
|
204
|
+
// Bytes 16-19: Total number of disks
|
|
205
|
+
|
|
206
|
+
// Parse ZIP64 End of Central Directory Record
|
|
207
|
+
assert(
|
|
208
|
+
zip64EocdrOffset + 56 <= zip64EocdlOffset,
|
|
209
|
+
'Cannot locate ZIP64 End of Central Directory Record'
|
|
210
|
+
);
|
|
211
|
+
const zip64EocdrBuffer = await this.reader.read(zip64EocdrOffset, 56);
|
|
212
|
+
// Bytes 0-3: ZIP64 End of Central Directory Record signature = 0x06064b50
|
|
213
|
+
assert(
|
|
214
|
+
zip64EocdrBuffer.readUInt32LE(0) === 0x06064b50,
|
|
215
|
+
'Invalid ZIP64 End of Central Directory Record signature'
|
|
216
|
+
);
|
|
217
|
+
// Bytes 4-11: Size of ZIP64 End of Central Directory Record (not inc first 12 bytes)
|
|
218
|
+
const zip64EocdrSize = readUInt64LE(zip64EocdrBuffer, 4);
|
|
219
|
+
assert(
|
|
220
|
+
zip64EocdrOffset + zip64EocdrSize + 12 <= zip64EocdlOffset,
|
|
221
|
+
'Invalid ZIP64 End of Central Directory Record'
|
|
222
|
+
);
|
|
223
|
+
// Bytes 12-13: Version made by
|
|
224
|
+
// Bytes 14-15: Version needed to extract
|
|
225
|
+
// Bytes 16-19: Number of this disk
|
|
226
|
+
// Bytes 20-23: Number of the disk with the start of the Central Directory
|
|
227
|
+
// Bytes 24-31: Total number of entries in the Central Directory on this disk
|
|
228
|
+
// Bytes 32-39: Total number of entries in the Central Directory
|
|
229
|
+
// Spec: "If an archive is in ZIP64 format and the value in this field is 0xFFFF, the size
|
|
230
|
+
// will be in the corresponding 8 byte zip64 end of central directory field."
|
|
231
|
+
// Original Yauzl expects correct entry count to always be recorded in ZIP64 EOCDR,
|
|
232
|
+
// but have altered that here to be more spec-compliant. Ditto Central Directory size + offset.
|
|
233
|
+
if (this.entryCount === 0xFFFF) this.entryCount = readUInt64LE(zip64EocdrBuffer, 32);
|
|
234
|
+
// Bytes 40-47: Size of the Central Directory
|
|
235
|
+
if (this.centralDirectorySize === 0xFFFFFFFF) {
|
|
236
|
+
this.centralDirectorySize = readUInt64LE(zip64EocdrBuffer, 40);
|
|
237
|
+
}
|
|
238
|
+
// Bytes 48-55: Offset of start of Central Directory with respect to the starting disk number
|
|
239
|
+
if (this.centralDirectoryOffset === 0xFFFFFFFF) {
|
|
240
|
+
this.centralDirectoryOffset = readUInt64LE(zip64EocdrBuffer, 48);
|
|
241
|
+
}
|
|
242
|
+
// Bytes 56-...: ZIP64 extensible data sector
|
|
243
|
+
|
|
244
|
+
// Record offset of start of footers.
|
|
245
|
+
// Either start of ZIP64 EOCDR (if it butts up to ZIP64 EOCDL), or ZIP64 EOCDL.
|
|
246
|
+
this.footerOffset = zip64EocdrOffset + zip64EocdrSize === zip64EocdlOffset
|
|
247
|
+
? zip64EocdrOffset
|
|
248
|
+
: zip64EocdlOffset;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Locate Central Directory.
|
|
253
|
+
*
|
|
254
|
+
* In a well-formed ZIP file, the EOCDR accurately gives us the offset and size of Central
|
|
255
|
+
* Directory, and the entry count.
|
|
256
|
+
*
|
|
257
|
+
* However Mac OS Archive Utility, instead of using ZIP64 extension to record Central Directory
|
|
258
|
+
* offset or size >= 4 GiB, or entry count >= 65536, truncates size and offset to lower 32 bits,
|
|
259
|
+
* and entry count to lower 16 bits.
|
|
260
|
+
* i.e.:
|
|
261
|
+
* Actual offset = reported offset + n * (1 << 32)
|
|
262
|
+
* Actual size = reported size + m * (1 << 32)
|
|
263
|
+
* Actual entry count = reported entry count + o * (1 << 16)
|
|
264
|
+
* (where `n`, `m` and `o` are unknown)
|
|
265
|
+
*
|
|
266
|
+
* Identify if this may be a faulty Mac OS Archive Utility ZIP. If so, find the actual location of
|
|
267
|
+
* the Central Directory. Deduce which of above properties cannot be known with certainty.
|
|
268
|
+
*
|
|
269
|
+
* In some cases, it's not possible to immediately determine if a ZIP is definitely a Mac OS ZIP.
|
|
270
|
+
* If it may be, but not sure yet, record which properties are unknown at present.
|
|
271
|
+
* Later calls to `readEntry()` or `openReadStream()` will reveal more about the ZIP, and the
|
|
272
|
+
* determinaton of whether ZIP is a faulty Mac OS ZIP or not will be made then.
|
|
273
|
+
*
|
|
274
|
+
* Try to do this while ensuring a spec-compliant ZIP will never be misinterpretted.
|
|
275
|
+
*
|
|
276
|
+
* @async
|
|
277
|
+
* @returns {undefined}
|
|
278
|
+
*/
|
|
279
|
+
async _locateCentralDirectory() {
|
|
280
|
+
// Skip this if Mac OS Archive Utility support disabled
|
|
281
|
+
if (!this.supportMacArchive) return;
|
|
282
|
+
|
|
283
|
+
// Mac OS archives don't use ZIP64 extension
|
|
284
|
+
if (this.isZip64) return;
|
|
285
|
+
|
|
286
|
+
// Mac Archives do not contain comment after End of Central Directory Record
|
|
287
|
+
if (this.size - this.footerOffset !== EOCDR_WITHOUT_COMMENT_SIZE) return;
|
|
288
|
+
|
|
289
|
+
// Mac Archives do not have gap between end of last Central Directory Header and start of EOCDR
|
|
290
|
+
let centralDirectoryEnd = this.centralDirectoryOffset + this.centralDirectorySize;
|
|
291
|
+
if (centralDirectoryEnd % FOUR_GIB !== this.footerOffset % FOUR_GIB) return;
|
|
292
|
+
|
|
293
|
+
// If claims to have no entries, and there's no room for any, this must be accurate.
|
|
294
|
+
// Handle this here to avoid trying to read beyond end of file.
|
|
295
|
+
if (this.entryCount === 0 && this.centralDirectoryOffset + CDH_MIN_LENGTH > this.footerOffset) {
|
|
296
|
+
assert(this.centralDirectorySize === 0, 'Inconsistent Central Directory size and entry count');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Ensure size and entry count comply with each other and adjust if they don't
|
|
301
|
+
if (this.centralDirectorySize < this.entryCount * CDH_MIN_LENGTH) {
|
|
302
|
+
// Central Directory size is too small to contain `entryCount` entries. Must be Mac OS ZIP.
|
|
303
|
+
// Check is room to grow Central Directory, and grow it up to EOCDR.
|
|
304
|
+
assert(
|
|
305
|
+
centralDirectoryEnd < this.footerOffset,
|
|
306
|
+
'Inconsistent Central Directory size and entry count'
|
|
307
|
+
);
|
|
308
|
+
this.isMacArchive = true;
|
|
309
|
+
centralDirectoryEnd = this.footerOffset;
|
|
310
|
+
this.centralDirectorySize = centralDirectoryEnd - this.centralDirectoryOffset;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this._recalculateEntryCount(0, this.centralDirectoryOffset)) {
|
|
314
|
+
// Entry count was too small. Must be Mac OS ZIP.
|
|
315
|
+
this.isMacArchive = true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Unless we already know this is a Mac ZIP, check if Central Directory is where EOCDR says it is
|
|
319
|
+
// (if we know it's a Mac ZIP, better to look in last possible position first)
|
|
320
|
+
let entry, alreadyCheckedOffset;
|
|
321
|
+
if (!this.isMacArchive) {
|
|
322
|
+
entry = await this._readEntryAt(this.centralDirectoryOffset);
|
|
323
|
+
|
|
324
|
+
// If found a non-Mac Central Directory Header, exit - it's not a Mac archive
|
|
325
|
+
if (entry && !firstEntryMaybeMac(entry)) {
|
|
326
|
+
assert(this.entryCount > 0, 'Inconsistent Central Directory size and entry count');
|
|
327
|
+
|
|
328
|
+
// Store entry, to be used in first call to `readEntry()`, to avoid reading from file again
|
|
329
|
+
this._firstEntryProps = entry;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
alreadyCheckedOffset = this.centralDirectoryOffset;
|
|
334
|
+
} else {
|
|
335
|
+
alreadyCheckedOffset = -1;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// If no Central Directory found where it should be, this ZIP is either:
|
|
339
|
+
// 1. Valid ZIP with no entries
|
|
340
|
+
// 2. Faulty Mac OS Archive Utility ZIP
|
|
341
|
+
// 3. Invalid ZIP
|
|
342
|
+
// If it's an invalid ZIP, all bets are off, so ignore that possibility.
|
|
343
|
+
// Try to locate Central Directory in possible locations it could be if this is
|
|
344
|
+
// a Mac OS Archive Utility ZIP (`centralDirectoryOffset + n * FOUR_GIB` where `n` is unknown).
|
|
345
|
+
// It's more common to have a ZIP containing large files, than a ZIP with
|
|
346
|
+
// so many files that the Central Directory is 4 GiB+ in size (likely requiring millions of files).
|
|
347
|
+
// So start with last possible position and work backwards towards start of file.
|
|
348
|
+
if (!entry) {
|
|
349
|
+
// Find last possible offset for Central Directory
|
|
350
|
+
let offset = this.footerOffset
|
|
351
|
+
- Math.max(this.centralDirectorySize, this.entryCount * CDH_MIN_LENGTH);
|
|
352
|
+
if (offset % FOUR_GIB < this.centralDirectoryOffset) {
|
|
353
|
+
assert(offset >= FOUR_GIB, 'Inconsistent Central Directory size and entry count');
|
|
354
|
+
offset -= FOUR_GIB;
|
|
355
|
+
}
|
|
356
|
+
offset = Math.floor(offset / FOUR_GIB) * FOUR_GIB + this.centralDirectoryOffset;
|
|
357
|
+
|
|
358
|
+
// Search for Central Directory
|
|
359
|
+
while (offset > alreadyCheckedOffset) {
|
|
360
|
+
entry = await this._readEntryAt(offset);
|
|
361
|
+
if (entry) {
|
|
362
|
+
assert(firstEntryMaybeMac(entry), 'Cannot locate Central Directory');
|
|
363
|
+
this.isMacArchive = true;
|
|
364
|
+
this.centralDirectoryOffset = offset;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
offset -= FOUR_GIB;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// If couldn't find Central Directory, it's a faulty ZIP, unless it has 0 entries
|
|
373
|
+
if (!entry) {
|
|
374
|
+
assert(
|
|
375
|
+
this.entryCount === 0 && this.centralDirectorySize === 0,
|
|
376
|
+
'Cannot locate Central Directory'
|
|
377
|
+
);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// We've found Central Directory, and it is likely to be Mac OS ZIP, but we may not know for sure.
|
|
382
|
+
// If reported entry count was 0, but Central Directory found, must be a Mac OS ZIP.
|
|
383
|
+
if (this.entryCount === 0) this.isMacArchive = true;
|
|
384
|
+
|
|
385
|
+
if (this.isMacArchive) {
|
|
386
|
+
// We know for sure this is a Mac OS Archive Utility ZIP,
|
|
387
|
+
// because some of the size/offset/entry count data has proved faulty.
|
|
388
|
+
// Mac OS ZIPs always have Central Directory going all the way up to the EOCDR.
|
|
389
|
+
centralDirectoryEnd = this.footerOffset;
|
|
390
|
+
this.centralDirectorySize = centralDirectoryEnd - this.centralDirectoryOffset;
|
|
391
|
+
assert(this.centralDirectorySize > 0, 'Inconsistent Central Directory size and entry count');
|
|
392
|
+
|
|
393
|
+
// Recalculate minimum entry count
|
|
394
|
+
this._recalculateEntryCount(1, entry.entryEnd);
|
|
395
|
+
|
|
396
|
+
// Calculate if possible for one or more files to be 4 GiB larger than reported.
|
|
397
|
+
// Each entry takes at minimum 30 bytes for Local File Header + 16 bytes for Data Descriptor.
|
|
398
|
+
// Mac Archives repeat same filename in Local File Header as in Central Directory.
|
|
399
|
+
// Mac Archives contain 16 bytes Extra Fields in Local File Header if CDH contains an Extra Field.
|
|
400
|
+
// So minimum size occupied by first file can be included in this calculation.
|
|
401
|
+
const minTotalDataSize = this.entryCount * 46
|
|
402
|
+
+ entry.compressedSize
|
|
403
|
+
+ entry.filename.length
|
|
404
|
+
+ entry.extraFields.length * MAC_LFH_EXTRA_FIELDS_LENGTH;
|
|
405
|
+
if (minTotalDataSize + FOUR_GIB <= this.centralDirectoryOffset) {
|
|
406
|
+
this.compressedSizesAreCertain = false;
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
// ZIP has Central Directory where it should be, and format of first entry is consistent
|
|
410
|
+
// with this being a Mac OS ZIP, but we don't know for sure that it is
|
|
411
|
+
this.isMaybeMacArchive = true;
|
|
412
|
+
if (centralDirectoryEnd < this.footerOffset) {
|
|
413
|
+
// There's room for Central Directory to be 4 GiB or more bigger than reported.
|
|
414
|
+
// This implies entry count is uncertain too. An extra 4 GiB could fit up to ~9 million entries.
|
|
415
|
+
this.centralDirectorySizeIsCertain = false;
|
|
416
|
+
this.entryCountIsCertain = false;
|
|
417
|
+
} else {
|
|
418
|
+
// Recalculate minimum entry count
|
|
419
|
+
this._recalculateEntryCount(1, entry.entryEnd);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Init set of uncertain uncompressed size entries
|
|
423
|
+
this._uncertainUncompressedSizeEntryRefs = new Set();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check if entry count could be higher than EOCDR says it is
|
|
427
|
+
if (
|
|
428
|
+
this.entryCountIsCertain
|
|
429
|
+
&& !entryCountIsCertain(this.entryCount - 1, centralDirectoryEnd - entry.entryEnd)
|
|
430
|
+
) this.entryCountIsCertain = false;
|
|
431
|
+
|
|
432
|
+
// Even if compressed file sizes are certain, uncompressed file sizes remain uncertain
|
|
433
|
+
// because a file could be < 4 GiB compressed, but >= 4 GiB uncompressed
|
|
434
|
+
this.uncompressedSizesAreCertain = false;
|
|
435
|
+
|
|
436
|
+
// Init local file header cursor
|
|
437
|
+
this._fileCursor = 0;
|
|
438
|
+
|
|
439
|
+
// Store entry, to be used in first call to `readEntry()`, to avoid reading from file again
|
|
440
|
+
this._firstEntryProps = entry;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get next entry.
|
|
445
|
+
* @async
|
|
446
|
+
* @returns {Entry|null} - `Entry` object for next entry, or `null` if none remaining
|
|
447
|
+
*/
|
|
448
|
+
async readEntry() {
|
|
449
|
+
assert(!this._isReading, 'Cannot call `readEntry()` before previous call\'s promise has settled');
|
|
450
|
+
this._isReading = true;
|
|
451
|
+
try {
|
|
452
|
+
return await this._readEntry();
|
|
453
|
+
} finally {
|
|
454
|
+
this._isReading = false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Get next entry.
|
|
460
|
+
* Implementation for `readEntry()`. Should not be called directly.
|
|
461
|
+
* @async
|
|
462
|
+
* @returns {Entry|null} - `Entry` object for next entry, or `null` if none remaining
|
|
463
|
+
*/
|
|
464
|
+
async _readEntry() {
|
|
465
|
+
if (this.numEntriesRead === this.entryCount && this.entryCountIsCertain) return null;
|
|
466
|
+
|
|
467
|
+
// Read Central Directory entry properties (or use the one already read)
|
|
468
|
+
let entryProps = this._firstEntryProps,
|
|
469
|
+
entryEnd;
|
|
470
|
+
if (entryProps) {
|
|
471
|
+
this._firstEntryProps = null;
|
|
472
|
+
entryEnd = entryProps.entryEnd;
|
|
473
|
+
} else {
|
|
474
|
+
entryProps = await this._readEntryAt(this._entryCursor);
|
|
475
|
+
|
|
476
|
+
const centralDirectoryEnd = this.centralDirectoryOffset + this.centralDirectorySize;
|
|
477
|
+
if (!entryProps) {
|
|
478
|
+
// Only way to get here if the ZIP file isn't corrupt is if Central Directory size wasn't
|
|
479
|
+
// certain, and therefore entry count wasn't certain either, so we weren't sure if this was
|
|
480
|
+
// the end or not. If we've reached end of reported entries, and are at end of reported
|
|
481
|
+
// Central Directory, then there being no entry means the Central Directory entry size
|
|
482
|
+
// and entry count are accurate, and this is indeed the end.
|
|
483
|
+
// That implies this can't be a Mac ZIP, because Central Directory doesn't go up to EOCDR.
|
|
484
|
+
// NB: No need to check for `this.centralDirectorySizeIsCertain === false` because if that
|
|
485
|
+
// was the case, `this.entryCountIsCertain` would be `false` too, and we wouldn't be here.
|
|
486
|
+
assert(
|
|
487
|
+
!this.isMacArchive && this.numEntriesRead === this.entryCount
|
|
488
|
+
&& this._entryCursor === centralDirectoryEnd,
|
|
489
|
+
'Invalid Central Directory File Header signature'
|
|
490
|
+
);
|
|
491
|
+
// `isMaybeMacArchive` must have been `true` at start of this function, but check it here
|
|
492
|
+
// just in case it was already changed in a call to `openReadStream()` made by user while async
|
|
493
|
+
// `_readEntryAt()` call above was executing.
|
|
494
|
+
if (this.isMaybeMacArchive) this._setAsNotMacArchive();
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
entryEnd = entryProps.entryEnd;
|
|
499
|
+
if (this.isMacArchive) {
|
|
500
|
+
// Properties have been found to be inconsistent already, signalling a Mac OS ZIP.
|
|
501
|
+
// So all entries must be Mac-type, or it isn't a Mac ZIP after all, and is corrupt.
|
|
502
|
+
// File data is tightly packed in Mac OS ZIPs with no gaps in between.
|
|
503
|
+
assert(
|
|
504
|
+
entryMaybeMac(entryProps) && entryProps.fileHeaderOffset === this._fileCursor % FOUR_GIB,
|
|
505
|
+
'Inconsistent Central Directory structure'
|
|
506
|
+
);
|
|
507
|
+
entryProps.fileHeaderOffset = this._fileCursor;
|
|
508
|
+
|
|
509
|
+
if (!this.entryCountIsCertain) {
|
|
510
|
+
this._recalculateEntryCount(this.numEntriesRead + 1, entryEnd);
|
|
511
|
+
this._recalculateEntryCountIsCertain(this.numEntriesRead + 1, entryEnd);
|
|
512
|
+
}
|
|
513
|
+
} else if (this.isMaybeMacArchive) {
|
|
514
|
+
if (this._fileCursor >= FOUR_GIB) {
|
|
515
|
+
// This ZIP is flagged as maybe Mac which means all data up to `_fileCursor`
|
|
516
|
+
// has been consumed by previous files.
|
|
517
|
+
// `fileHeaderOffset` is 32 bit (so < 4 GiB), and `_fileCursor` > 4 GiB, so either
|
|
518
|
+
// 1. file data for this entry covers data already consumed (invalid, possible ZIP bomb)
|
|
519
|
+
// or 2. this must be a Mac ZIP and `fileHeaderOffset` is more than stated.
|
|
520
|
+
assert(
|
|
521
|
+
entryMaybeMac(entryProps) && entryProps.fileHeaderOffset === this._fileCursor % FOUR_GIB,
|
|
522
|
+
'Inconsistent Central Directory structure'
|
|
523
|
+
);
|
|
524
|
+
this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
|
|
525
|
+
} else if (!entryMaybeMac(entryProps) || entryProps.fileHeaderOffset !== this._fileCursor) {
|
|
526
|
+
// Entry doesn't match signature of Mac entries, or file header is not where it would be
|
|
527
|
+
// in a Mac ZIP, so it can't be one
|
|
528
|
+
this._setAsNotMacArchive();
|
|
529
|
+
|
|
530
|
+
// If entries were meant to be exhausted, there's an error somewhere
|
|
531
|
+
assert(this.numEntriesRead !== this.entryCount, 'Central Directory contains too many entries');
|
|
532
|
+
} else if (!this.centralDirectorySizeIsCertain && (
|
|
533
|
+
entryEnd + (this.entryCount - this.numEntriesRead - 1) * CDH_MIN_LENGTH > centralDirectoryEnd
|
|
534
|
+
)) {
|
|
535
|
+
// Not enough space in Central Directory for number of entries remaining,
|
|
536
|
+
// so this must be a Mac ZIP. Grow Central Directory.
|
|
537
|
+
this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
|
|
538
|
+
} else if (!this.entryCountIsCertain) {
|
|
539
|
+
// Recalculate if entry count is now impossibly low
|
|
540
|
+
if (this._recalculateEntryCount(this.numEntriesRead + 1, entryEnd)) {
|
|
541
|
+
// Entry count was impossibly low for size of Central Directory so this must be Mac ZIP
|
|
542
|
+
this._setAsMacArchive(this.numEntriesRead + 1, entryEnd);
|
|
543
|
+
} else if (this.centralDirectorySizeIsCertain) {
|
|
544
|
+
// Check if entry count is now high enough vs remaining Central Directory space
|
|
545
|
+
// that it can't be any larger
|
|
546
|
+
this._recalculateEntryCountIsCertain(this.numEntriesRead + 1, entryEnd);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Calculate what location of file data will be if this is a Mac OS ZIP.
|
|
553
|
+
// Mac OS ZIPs always contain Local File Header of 30 bytes
|
|
554
|
+
// + same filename as in Central Directory entry
|
|
555
|
+
// + 16 bytes Extra Fields if Central Directory entry has extra fields.
|
|
556
|
+
const fileDataOffsetIfMac = entryProps.fileHeaderOffset + 30 + entryProps.filename.length
|
|
557
|
+
+ entryProps.extraFields.length * MAC_LFH_EXTRA_FIELDS_LENGTH;
|
|
558
|
+
|
|
559
|
+
// Determine if possible for compressed data to be larger than reported,
|
|
560
|
+
// and, if so, the actual compressed size
|
|
561
|
+
if (!this.compressedSizesAreCertain) {
|
|
562
|
+
const isNowCertain = await this._determineCompressedSize(entryProps, fileDataOffsetIfMac);
|
|
563
|
+
if (isNowCertain) this.compressedSizesAreCertain = true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Determine if possible for this entry's uncompressed size to be larger than reported
|
|
567
|
+
if (!this.uncompressedSizesAreCertain) {
|
|
568
|
+
if (entryProps.compressionMethod === 0) {
|
|
569
|
+
// No compression - uncompressed size always equal to compressed.
|
|
570
|
+
// NB: We know encryption is not enabled as entry would have been flagged as non-Mac if it was.
|
|
571
|
+
entryProps.uncompressedSize = entryProps.compressedSize;
|
|
572
|
+
} else if (entryProps.compressionMethod !== 8) {
|
|
573
|
+
// Not Deflate compression - no idea what uncompressed size could be
|
|
574
|
+
entryProps.uncompressedSizeIsCertain = false;
|
|
575
|
+
} else {
|
|
576
|
+
// Deflate compression. Maximum compression ratio is 1032.
|
|
577
|
+
// https://stackoverflow.com/questions/16792189/gzip-compression-ratio-for-zeros/16794960#16794960
|
|
578
|
+
const maxUncompressedSize = entryProps.compressedSize * 1032;
|
|
579
|
+
if (
|
|
580
|
+
maxUncompressedSize > FOUR_GIB * 2
|
|
581
|
+
|| (
|
|
582
|
+
maxUncompressedSize > FOUR_GIB
|
|
583
|
+
&& maxUncompressedSize % FOUR_GIB > entryProps.uncompressedSize
|
|
584
|
+
)
|
|
585
|
+
) entryProps.uncompressedSizeIsCertain = false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Create entry object + advance cursor to next entry
|
|
590
|
+
const entry = this._validateAndDecodeEntry(entryProps);
|
|
591
|
+
this._entryCursor = entryEnd;
|
|
592
|
+
this.numEntriesRead++;
|
|
593
|
+
|
|
594
|
+
if (this.isMacArchive || this.isMaybeMacArchive) {
|
|
595
|
+
// Record offset of where next Local File Header will be if this is a Mac OS ZIP.
|
|
596
|
+
// 16 bytes for Data Descriptor after file data, unless it's a folder, empty file, or symlink.
|
|
597
|
+
this._fileCursor = fileDataOffsetIfMac + entry.compressedSize
|
|
598
|
+
+ (entryProps.compressionMethod === 8) * 16;
|
|
599
|
+
|
|
600
|
+
if (this.isMacArchive) {
|
|
601
|
+
// We know offset of file data for sure, so record it
|
|
602
|
+
entry.fileDataOffset = fileDataOffsetIfMac;
|
|
603
|
+
} else if (!entry.uncompressedSizeIsCertain) {
|
|
604
|
+
// This is a suspected Mac OS ZIP (but not for sure), and uncompressed size is uncertain.
|
|
605
|
+
// Record entry, so that if ZIP turns out not to be a Mac OS ZIP later,
|
|
606
|
+
// `uncompressedSizeIsCertain` can be changed to `true`.
|
|
607
|
+
// Entries are recorded as `WeakRef`s, to allow them to be garbage collected.
|
|
608
|
+
// The entry is also added to a `FinalizationRegistry`, which removes the ref from the set
|
|
609
|
+
// when entry object is garbage collected. This should prevent escalating memory usage
|
|
610
|
+
// if lots of entries.
|
|
611
|
+
const ref = new WeakRef(entry);
|
|
612
|
+
entry._ref = ref;
|
|
613
|
+
this._uncertainUncompressedSizeEntryRefs.add(ref);
|
|
614
|
+
uncertainUncompressedSizeEntriesRegistry.register(entry, {zip: this, ref}, ref);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Return `Entry` object
|
|
619
|
+
return entry;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Determine actual compressed size of entry.
|
|
624
|
+
* Update `compressedSize` if it's not what was reported.
|
|
625
|
+
* Return whether *all future* entries have certain compressed size.
|
|
626
|
+
*
|
|
627
|
+
* This method should only be called if this is a Mac ZIP, or possibly a Mac ZIP.
|
|
628
|
+
* i.e. Compressed sizes are not certain to be as reported in the ZIP.
|
|
629
|
+
*
|
|
630
|
+
* First attempt to prove that size can be known with certainty without reading from ZIP file.
|
|
631
|
+
* If that's not possible, search ZIP file for the Data Descriptor which follows file data.
|
|
632
|
+
*
|
|
633
|
+
* Care has to be taken to avoid data races, because this function contains async IO calls,
|
|
634
|
+
* and possible for user to call `openReadStream()` on another Entry, or an event on a stream
|
|
635
|
+
* already in process to cause the ZIP to be identified as definitely Mac or definitely not Mac
|
|
636
|
+
* during this function's async calls.
|
|
637
|
+
*
|
|
638
|
+
* @param {Object} entryProps - Entry properties
|
|
639
|
+
* @param {number} fileDataOffsetIfMac - If ZIP is a Mac OS ZIP, offset file data will start at
|
|
640
|
+
* @returns {boolean} - `true` if all later entry compressed sizes must be certain
|
|
641
|
+
*/
|
|
642
|
+
async _determineCompressedSize(entryProps, fileDataOffsetIfMac) {
|
|
643
|
+
// ZIP may only be a suspected Mac OS ZIP, rather than definitely one.
|
|
644
|
+
// However, we can assume it is a Mac ZIP for purposes of calculations here,
|
|
645
|
+
// as if actually it's not, compressed size of all entries is certain anyway.
|
|
646
|
+
//
|
|
647
|
+
// In a Mac ZIP:
|
|
648
|
+
// - Files (unless empty) are compressed and have Data Descriptor and Extra Fields.
|
|
649
|
+
// Size may be incorrect - truncated to lower 32 bits.
|
|
650
|
+
// - Folders and empty files are not compressed and have no Data Descriptor,
|
|
651
|
+
// but do have Extra Fields.
|
|
652
|
+
// Size = 0.
|
|
653
|
+
// - Symlinks are not compressed and have no Data Descriptor or Extra Fields.
|
|
654
|
+
// Size assumed under 4GiB as file content is just path to linked file.
|
|
655
|
+
//
|
|
656
|
+
// So we can know exact end point of this entry's data section (unless it's 4 GiB larger),
|
|
657
|
+
// and all other entries yet to come must use 30 bytes each at minimum.
|
|
658
|
+
let numEntriesRemaining = this.entryCount - this.numEntriesRead - 1;
|
|
659
|
+
let dataSpaceRemaining = this.centralDirectoryOffset - fileDataOffsetIfMac
|
|
660
|
+
- entryProps.compressedSize - (entryProps.compressionMethod === 8) * 16;
|
|
661
|
+
|
|
662
|
+
// Check if not enough data space left for this entry or any later entry
|
|
663
|
+
// to be 4 GiB larger than reported
|
|
664
|
+
if (dataSpaceRemaining - numEntriesRemaining * 30 < FOUR_GIB) return true;
|
|
665
|
+
|
|
666
|
+
if (this.isMacArchive && numEntriesRemaining === 0) {
|
|
667
|
+
// Last entry in Mac ZIP - must use all remaining space.
|
|
668
|
+
// We can trust `entryCount` at this point, as it would have been increased
|
|
669
|
+
// if there was excess space in the Central Directory.
|
|
670
|
+
// We cannot assume file takes up all remaining space if we don't know for sure that
|
|
671
|
+
// this is a Mac ZIP, because if it's not, it would be legitimate as per the ZIP spec
|
|
672
|
+
// to have unused space between end of file data and the Central Directory.
|
|
673
|
+
assert(
|
|
674
|
+
dataSpaceRemaining % FOUR_GIB === 0,
|
|
675
|
+
'Invalid ZIP structure for Mac OS Archive Utility ZIP'
|
|
676
|
+
);
|
|
677
|
+
entryProps.compressedSize += dataSpaceRemaining;
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (entryProps.compressionMethod === 0) {
|
|
682
|
+
// If this is a Mac ZIP, entry is a folder, empty file, or symlink (see `entryMaybeMac()` below).
|
|
683
|
+
// Folders and empty files definitely have 0 size.
|
|
684
|
+
// We have to assume symlinks are under 4 GiB because they have no data descriptor after to
|
|
685
|
+
// search for (and what kind of maniac uses a symlink bigger than 4 GiB anyway?).
|
|
686
|
+
// If it's not a Mac ZIP, reported compressed size will be accurate.
|
|
687
|
+
// So either way, we know size is correct.
|
|
688
|
+
// Return `false`, because compressed size of later files may still be larger than reported.
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Compressed size is not certain.
|
|
693
|
+
// Search for Data Descriptor after file data.
|
|
694
|
+
// It could be where it's reported to be, or anywhere after that in 4 GiB jumps.
|
|
695
|
+
let fileDataEnd = fileDataOffsetIfMac + entryProps.compressedSize;
|
|
696
|
+
while (true) { // eslint-disable-line no-constant-condition
|
|
697
|
+
const buffer = await this.reader.read(fileDataEnd, 20);
|
|
698
|
+
if (
|
|
699
|
+
buffer.readUInt32LE(0) === 0x08074b50 // Data Descriptor signature
|
|
700
|
+
&& buffer.readUInt32LE(4) === entryProps.crc32
|
|
701
|
+
&& buffer.readUInt32LE(8) === entryProps.compressedSize
|
|
702
|
+
&& buffer.readUInt32LE(12) === entryProps.uncompressedSize
|
|
703
|
+
&& (
|
|
704
|
+
buffer.readUInt32LE(16) === 0x04034b50 // Local File Header signature
|
|
705
|
+
|| fileDataEnd + 16 === this.centralDirectoryOffset // Last entry
|
|
706
|
+
)
|
|
707
|
+
) break;
|
|
708
|
+
|
|
709
|
+
// During async `read()` call above, if user called `openReadStream()` on another entry,
|
|
710
|
+
// it could have discovered this isn't a Mac ZIP after all.
|
|
711
|
+
// If so, stop searching for data descriptor.
|
|
712
|
+
if (this.compressedSizesAreCertain) {
|
|
713
|
+
fileDataEnd = null;
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
fileDataEnd += FOUR_GIB;
|
|
718
|
+
if (fileDataEnd + 16 > this.centralDirectoryOffset) {
|
|
719
|
+
// Data Descriptor not found
|
|
720
|
+
fileDataEnd = null;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (fileDataEnd === null) {
|
|
726
|
+
// Could not find Data Descriptor, so this can't be a Mac ZIP
|
|
727
|
+
assert(!this.isMacArchive, 'Cannot locate file Data Descriptor');
|
|
728
|
+
// Have to check `isMaybeMacArchive` again, as could have changed during async calls
|
|
729
|
+
// to `read()` above, if `openReadStream()` was called and found this isn't a Mac ZIP after all
|
|
730
|
+
if (this.isMaybeMacArchive) this._setAsNotMacArchive();
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (fileDataEnd === fileDataOffsetIfMac + entryProps.compressedSize) {
|
|
735
|
+
// Compressed size is what was stated. So size of later entries is still uncertain.
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Size is larger than stated, so this must be Mac ZIP
|
|
740
|
+
if (!this.isMacArchive) {
|
|
741
|
+
// Have to check `isMaybeMacArchive` again, as could have changed during async calls
|
|
742
|
+
// to `read()` above, if `openReadStream()` was called and found this isn't a Mac ZIP after all
|
|
743
|
+
assert(this.isMaybeMacArchive, 'Cannot locate file Data Descriptor');
|
|
744
|
+
this._setAsMacArchive(this.numEntriesRead + 1, entryProps.entryEnd);
|
|
745
|
+
}
|
|
746
|
+
entryProps.compressedSize = fileDataEnd - fileDataOffsetIfMac;
|
|
747
|
+
|
|
748
|
+
// Check if there's now not enough data space left after this entry for any later entry
|
|
749
|
+
// to be 4 GiB larger than reported.
|
|
750
|
+
// Need to recalculate `numEntriesRemaining` as `entryCount` could have changed.
|
|
751
|
+
// That could happen in `_setAsMacArchive()` call above. Or there's also a possible race
|
|
752
|
+
// if another entry is being streamed at the moment, and that stream happened to exceed
|
|
753
|
+
// its reported uncompressed size. That could happen during async `read()` calls above,
|
|
754
|
+
// and would also cause a call to `_setAsMacArchive()`.
|
|
755
|
+
// More obviously, `dataSpaceRemaining` has to be recalculated too,
|
|
756
|
+
// as initial `fileDataEnd` may have been found to be inaccurate.
|
|
757
|
+
numEntriesRemaining = this.entryCount - this.numEntriesRead - 1;
|
|
758
|
+
dataSpaceRemaining = this.centralDirectoryOffset - fileDataEnd - 16;
|
|
759
|
+
return dataSpaceRemaining - numEntriesRemaining * 30 < FOUR_GIB;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Attempt to read Central Directory Header at offset.
|
|
764
|
+
* Returns properties of entry. Does not decode strings or validate file sizes.
|
|
765
|
+
* @async
|
|
766
|
+
* @param {number} offset - Offset to parse CDH at
|
|
767
|
+
* @returns {Object|null} - Entry properties or `null` if no Central Directory File Header found
|
|
768
|
+
*/
|
|
769
|
+
async _readEntryAt(offset) {
|
|
770
|
+
// Bytes 0-3: Central Directory File Header signature
|
|
771
|
+
assert(offset + CDH_MIN_LENGTH <= this.footerOffset, 'Invalid Central Directory File Header');
|
|
772
|
+
const entryBuffer = await this.reader.read(offset, CDH_MIN_LENGTH);
|
|
773
|
+
if (entryBuffer.readUInt32LE(0) !== 0x02014b50) return null;
|
|
774
|
+
|
|
775
|
+
// Bytes 4-5: Version made by
|
|
776
|
+
const versionMadeBy = entryBuffer.readUInt16LE(4);
|
|
777
|
+
// Bytes 6-7: Version needed to extract (minimum)
|
|
778
|
+
const versionNeededToExtract = entryBuffer.readUInt16LE(6);
|
|
779
|
+
// Bytes 8-9: General Purpose Bit Flag
|
|
780
|
+
const generalPurposeBitFlag = entryBuffer.readUInt16LE(8);
|
|
781
|
+
// Bytes 10-11: Compression method
|
|
782
|
+
const compressionMethod = entryBuffer.readUInt16LE(10);
|
|
783
|
+
// Bytes 12-13: File last modification time
|
|
784
|
+
const lastModTime = entryBuffer.readUInt16LE(12);
|
|
785
|
+
// Bytes 14-15: File last modification date
|
|
786
|
+
const lastModDate = entryBuffer.readUInt16LE(14);
|
|
787
|
+
// Bytes 16-17: CRC32
|
|
788
|
+
const crc32 = entryBuffer.readUInt32LE(16);
|
|
789
|
+
// Bytes 20-23: Compressed size
|
|
790
|
+
let compressedSize = entryBuffer.readUInt32LE(20);
|
|
791
|
+
// Bytes 24-27: Uncompressed size
|
|
792
|
+
let uncompressedSize = entryBuffer.readUInt32LE(24);
|
|
793
|
+
// Bytes 28-29: Filename length
|
|
794
|
+
const filenameLength = entryBuffer.readUInt16LE(28);
|
|
795
|
+
// Bytes 30-31: Extra field length
|
|
796
|
+
const extraFieldLength = entryBuffer.readUInt16LE(30);
|
|
797
|
+
// Bytes 32-33: File comment length
|
|
798
|
+
const commentLength = entryBuffer.readUInt16LE(32);
|
|
799
|
+
// Bytes 34-35: Disk number where file starts
|
|
800
|
+
// Bytes 36-37: Internal file attributes
|
|
801
|
+
const internalFileAttributes = entryBuffer.readUInt16LE(36);
|
|
802
|
+
// Bytes 38-41: External file attributes
|
|
803
|
+
const externalFileAttributes = entryBuffer.readUInt32LE(38);
|
|
804
|
+
// Bytes 42-45: Relative offset of Local File Header
|
|
805
|
+
let fileHeaderOffset = entryBuffer.readUInt32LE(42);
|
|
806
|
+
|
|
807
|
+
// eslint-disable-next-line no-bitwise
|
|
808
|
+
assert((generalPurposeBitFlag & 0x40) === 0, 'Strong encryption is not supported');
|
|
809
|
+
|
|
810
|
+
// Get filename
|
|
811
|
+
const extraDataOffset = offset + CDH_MIN_LENGTH,
|
|
812
|
+
extraDataSize = filenameLength + extraFieldLength + commentLength,
|
|
813
|
+
entryEnd = extraDataOffset + extraDataSize;
|
|
814
|
+
assert(entryEnd <= this.footerOffset, 'Invalid Central Directory File Header');
|
|
815
|
+
const extraBuffer = await this.reader.read(extraDataOffset, extraDataSize);
|
|
816
|
+
|
|
817
|
+
const filename = extraBuffer.subarray(0, filenameLength);
|
|
818
|
+
|
|
819
|
+
// Get extra fields
|
|
820
|
+
const commentStart = filenameLength + extraFieldLength;
|
|
821
|
+
const extraFieldBuffer = extraBuffer.subarray(filenameLength, commentStart);
|
|
822
|
+
let i = 0;
|
|
823
|
+
const extraFields = [];
|
|
824
|
+
let zip64EiefBuffer;
|
|
825
|
+
while (i < extraFieldBuffer.length - 3) {
|
|
826
|
+
const headerId = extraFieldBuffer.readUInt16LE(i + 0),
|
|
827
|
+
dataSize = extraFieldBuffer.readUInt16LE(i + 2),
|
|
828
|
+
dataStart = i + 4,
|
|
829
|
+
dataEnd = dataStart + dataSize;
|
|
830
|
+
assert(dataEnd <= extraFieldBuffer.length, 'Extra field length exceeds extra field buffer size');
|
|
831
|
+
const dataBuffer = extraFieldBuffer.subarray(dataStart, dataEnd);
|
|
832
|
+
extraFields.push({id: headerId, data: dataBuffer});
|
|
833
|
+
i = dataEnd;
|
|
834
|
+
|
|
835
|
+
if (headerId === 1) zip64EiefBuffer = dataBuffer;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Get file comment
|
|
839
|
+
const comment = extraBuffer.subarray(commentStart, extraDataSize);
|
|
840
|
+
|
|
841
|
+
// Handle ZIP64
|
|
842
|
+
const isZip64 = uncompressedSize === 0xFFFFFFFF || compressedSize === 0xFFFFFFFF
|
|
843
|
+
|| fileHeaderOffset === 0xFFFFFFFF;
|
|
844
|
+
if (isZip64) {
|
|
845
|
+
assert(zip64EiefBuffer, 'Expected ZIP64 Extended Information Extra Field');
|
|
846
|
+
|
|
847
|
+
// @overlookmotel: According to the spec, I'd expect all 3 of these fields to be present,
|
|
848
|
+
// but Yauzl's implementation makes them optional.
|
|
849
|
+
// There may be a good reason for this, so leaving it as in Yauzl's implementation.
|
|
850
|
+
let index = 0;
|
|
851
|
+
|
|
852
|
+
// 8 bytes: Uncompressed size
|
|
853
|
+
if (uncompressedSize === 0xFFFFFFFF) {
|
|
854
|
+
assert(
|
|
855
|
+
index + 8 <= zip64EiefBuffer.length,
|
|
856
|
+
'ZIP64 Extended Information Extra Field does not include uncompressed size'
|
|
857
|
+
);
|
|
858
|
+
uncompressedSize = readUInt64LE(zip64EiefBuffer, index);
|
|
859
|
+
index += 8;
|
|
860
|
+
}
|
|
861
|
+
// 8 bytes: Compressed size
|
|
862
|
+
if (compressedSize === 0xFFFFFFFF) {
|
|
863
|
+
assert(
|
|
864
|
+
index + 8 <= zip64EiefBuffer.length,
|
|
865
|
+
'ZIP64 Extended Information Extra Field does not include compressed size'
|
|
866
|
+
);
|
|
867
|
+
compressedSize = readUInt64LE(zip64EiefBuffer, index);
|
|
868
|
+
index += 8;
|
|
869
|
+
}
|
|
870
|
+
// 8 bytes: Local File Header offset
|
|
871
|
+
if (fileHeaderOffset === 0xFFFFFFFF) {
|
|
872
|
+
assert(
|
|
873
|
+
index + 8 <= zip64EiefBuffer.length,
|
|
874
|
+
'ZIP64 Extended Information Extra Field does not include relative header offset'
|
|
875
|
+
);
|
|
876
|
+
fileHeaderOffset = readUInt64LE(zip64EiefBuffer, index);
|
|
877
|
+
index += 8;
|
|
878
|
+
}
|
|
879
|
+
// 4 bytes: Disk Start Number
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Minimum length of Local File Header = 30
|
|
883
|
+
assert(fileHeaderOffset + 30 <= this.footerOffset, 'Invalid location for file data');
|
|
884
|
+
|
|
885
|
+
// Return entry properties
|
|
886
|
+
return {
|
|
887
|
+
filename,
|
|
888
|
+
compressedSize,
|
|
889
|
+
uncompressedSize,
|
|
890
|
+
uncompressedSizeIsCertain: true, // May not be correct - may be set to `false` in `readEntry()`
|
|
891
|
+
compressionMethod,
|
|
892
|
+
fileHeaderOffset,
|
|
893
|
+
fileDataOffset: null,
|
|
894
|
+
isZip64,
|
|
895
|
+
crc32,
|
|
896
|
+
lastModTime,
|
|
897
|
+
lastModDate,
|
|
898
|
+
comment,
|
|
899
|
+
extraFields,
|
|
900
|
+
versionMadeBy,
|
|
901
|
+
versionNeededToExtract,
|
|
902
|
+
generalPurposeBitFlag,
|
|
903
|
+
internalFileAttributes,
|
|
904
|
+
externalFileAttributes,
|
|
905
|
+
filenameLength,
|
|
906
|
+
entryEnd
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Update `entryCount` if it's lower than is possible for it to be.
|
|
912
|
+
* @param {number} numEntriesRead - Number of entries read so far
|
|
913
|
+
* @param {number} entryCursor - Current position in Central Directory
|
|
914
|
+
* @returns {boolean} - `true` if entry count was increased
|
|
915
|
+
*/
|
|
916
|
+
_recalculateEntryCount(numEntriesRead, entryCursor) {
|
|
917
|
+
const numEntriesRemaining = this.entryCount - numEntriesRead,
|
|
918
|
+
centralDirectoryRemaining = this.centralDirectoryOffset + this.centralDirectorySize - entryCursor,
|
|
919
|
+
entryMaxLen = this.isMacArchive ? CDH_MAX_LENGTH_MAC : CDH_MAX_LENGTH;
|
|
920
|
+
if (numEntriesRemaining * entryMaxLen >= centralDirectoryRemaining) return false;
|
|
921
|
+
|
|
922
|
+
// Entry count can't be right.
|
|
923
|
+
// This must be a Mac Archive, so we calculate minimum entry count based on
|
|
924
|
+
// max length of entries in Mac OS ZIPs (which is less than for non-Mac entries).
|
|
925
|
+
const minEntriesRemaining = Math.ceil(centralDirectoryRemaining / CDH_MAX_LENGTH_MAC);
|
|
926
|
+
// eslint-disable-next-line no-bitwise
|
|
927
|
+
this.entryCount += (minEntriesRemaining - numEntriesRemaining + 0xFFFF) & 0x10000;
|
|
928
|
+
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Update `entryCountIsCertain` if it's impossible for entry count to be 65536 larger than
|
|
934
|
+
* current `entryCount` without exceeding bounds of Central Directory.
|
|
935
|
+
* This calculation is only valid if size of Central Directory is certain,
|
|
936
|
+
* so must only be called if `centralDirectorySizeIsCertain` is `true`.
|
|
937
|
+
* @param {number} numEntriesRead - Number of entries read so far
|
|
938
|
+
* @param {number} entryCursor - Current position in Central Directory
|
|
939
|
+
* @returns {undefined}
|
|
940
|
+
*/
|
|
941
|
+
_recalculateEntryCountIsCertain(numEntriesRead, entryCursor) {
|
|
942
|
+
const numEntriesRemaining = this.entryCount - numEntriesRead,
|
|
943
|
+
centralDirectoryRemaining = this.centralDirectoryOffset + this.centralDirectorySize - entryCursor;
|
|
944
|
+
if (entryCountIsCertain(numEntriesRemaining, centralDirectoryRemaining)) {
|
|
945
|
+
this.entryCountIsCertain = true;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Suspected Mac OS Archive Utility ZIP has turned out to definitely be one.
|
|
951
|
+
* Flag as Mac ZIP and calculate Central Directory size if it was ambiguous previously.
|
|
952
|
+
* Recalculate minimum entry count and whether it's now certain.
|
|
953
|
+
* @param {number} numEntriesRead - Number of entries read so far
|
|
954
|
+
* @param {number} entryCursor - Current position in Central Directory
|
|
955
|
+
* @returns {undefined}
|
|
956
|
+
*/
|
|
957
|
+
_setAsMacArchive(numEntriesRead, entryCursor) {
|
|
958
|
+
this.isMacArchive = true;
|
|
959
|
+
this.isMaybeMacArchive = false;
|
|
960
|
+
if (!this.centralDirectorySizeIsCertain) {
|
|
961
|
+
this.centralDirectorySize = this.footerOffset - this.centralDirectoryOffset;
|
|
962
|
+
this.centralDirectorySizeIsCertain = true;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Recalculate minimum entry count + whether entry count is certain
|
|
966
|
+
if (!this.entryCountIsCertain) {
|
|
967
|
+
this._recalculateEntryCount(numEntriesRead, entryCursor);
|
|
968
|
+
this._recalculateEntryCountIsCertain(numEntriesRead, entryCursor);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Clear set of uncertain uncompressed size entries
|
|
972
|
+
for (const ref of this._uncertainUncompressedSizeEntryRefs) {
|
|
973
|
+
uncertainUncompressedSizeEntriesRegistry.unregister(ref);
|
|
974
|
+
const entry = ref.deref();
|
|
975
|
+
if (entry) entry._ref = null;
|
|
976
|
+
}
|
|
977
|
+
this._uncertainUncompressedSizeEntryRefs = null;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Suspected Mac OS Archive Utility ZIP has turned out not to be one.
|
|
982
|
+
* Reset flags.
|
|
983
|
+
* @returns {undefined}
|
|
984
|
+
*/
|
|
985
|
+
_setAsNotMacArchive() {
|
|
986
|
+
this.isMaybeMacArchive = false;
|
|
987
|
+
this.entryCountIsCertain = true;
|
|
988
|
+
this.centralDirectorySizeIsCertain = true;
|
|
989
|
+
this.compressedSizesAreCertain = true;
|
|
990
|
+
this.uncompressedSizesAreCertain = true;
|
|
991
|
+
this._fileCursor = null;
|
|
992
|
+
|
|
993
|
+
// Flag all entries flagged as having uncertain uncompressed size as now having certain size
|
|
994
|
+
for (const ref of this._uncertainUncompressedSizeEntryRefs) {
|
|
995
|
+
uncertainUncompressedSizeEntriesRegistry.unregister(ref);
|
|
996
|
+
const entry = ref.deref();
|
|
997
|
+
if (entry) {
|
|
998
|
+
entry._ref = null;
|
|
999
|
+
entry.uncompressedSizeIsCertain = true;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
this._uncertainUncompressedSizeEntryRefs = null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Convert entry properties returned from `_readEntryAt()` to a full `Entry` object.
|
|
1007
|
+
* Decode strings and validate entry size according to options.
|
|
1008
|
+
* @param {Object} entry - Entry properties returned by `_readEntryAt()`
|
|
1009
|
+
* @returns {Entry} - `Entry` object
|
|
1010
|
+
*/
|
|
1011
|
+
_validateAndDecodeEntry(entry) {
|
|
1012
|
+
if (this.decodeStrings) {
|
|
1013
|
+
// Check for Info-ZIP Unicode Path Extra Field (0x7075).
|
|
1014
|
+
// See: https://github.com/thejoshwolfe/yauzl/issues/33
|
|
1015
|
+
let filename;
|
|
1016
|
+
for (const extraField of entry.extraFields) {
|
|
1017
|
+
if (extraField.id !== 0x7075) continue;
|
|
1018
|
+
if (extraField.data.length < 6) continue; // Too short to be meaningful
|
|
1019
|
+
// Check version is 1. "Changes may not be backward compatible so this extra
|
|
1020
|
+
// field should not be used if the version is not recognized."
|
|
1021
|
+
if (extraField.data[0] !== 1) continue;
|
|
1022
|
+
// Check CRC32 matches original filename.
|
|
1023
|
+
// "The NameCRC32 is the standard zip CRC32 checksum of the File Name
|
|
1024
|
+
// field in the header. This is used to verify that the header
|
|
1025
|
+
// File Name field has not changed since the Unicode Path extra field
|
|
1026
|
+
// was created. This can happen if a utility renames the File Name but
|
|
1027
|
+
// does not update the UTF-8 path extra field. If the CRC check fails,
|
|
1028
|
+
// this UTF-8 Path Extra Field SHOULD be ignored and the File Name field
|
|
1029
|
+
// in the header SHOULD be used instead."
|
|
1030
|
+
const oldNameCrc32 = extraField.data.readUInt32LE(1);
|
|
1031
|
+
if (calculateCrc32(entry.filename) !== oldNameCrc32) continue;
|
|
1032
|
+
filename = decodeBuffer(extraField.data, 5, true);
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Decode filename
|
|
1037
|
+
const isUtf8 = (entry.generalPurposeBitFlag & 0x800) !== 0; // eslint-disable-line no-bitwise
|
|
1038
|
+
if (filename === undefined) filename = decodeBuffer(entry.filename, 0, isUtf8);
|
|
1039
|
+
|
|
1040
|
+
// Validate filename
|
|
1041
|
+
if (this.validateFilenames) {
|
|
1042
|
+
// Allow backslash if `strictFilenames` option disabled
|
|
1043
|
+
if (!this.strictFilenames) filename = filename.replace(/\\/g, '/');
|
|
1044
|
+
validateFilename(filename);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
entry.filename = filename;
|
|
1048
|
+
|
|
1049
|
+
// Clone Extra Fields buffers, so rest of buffer that they're sliced from
|
|
1050
|
+
// (which also contains strings which are now decoded) can be garbage collected
|
|
1051
|
+
for (const extraField of entry.extraFields) {
|
|
1052
|
+
extraField.data = Buffer.from(extraField.data);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Decode comment
|
|
1056
|
+
entry.comment = decodeBuffer(entry.comment, 0, isUtf8);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Validate file size
|
|
1060
|
+
if (this.validateEntrySizes && entry.compressionMethod === 0) {
|
|
1061
|
+
// Lowest bit of General Purpose Bit Flag is for traditional encryption.
|
|
1062
|
+
// Traditional encryption prefixes the file data with a header.
|
|
1063
|
+
// eslint-disable-next-line no-bitwise
|
|
1064
|
+
const expectedCompressedSize = (entry.generalPurposeBitFlag & 0x1)
|
|
1065
|
+
? entry.uncompressedSize + 12
|
|
1066
|
+
: entry.uncompressedSize;
|
|
1067
|
+
assert(
|
|
1068
|
+
entry.compressedSize === expectedCompressedSize,
|
|
1069
|
+
'Compressed/uncompressed size mismatch for stored file: '
|
|
1070
|
+
+ `${entry.compressedSize} !== ${expectedCompressedSize}`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Create `Entry` object
|
|
1075
|
+
let entryEnd;
|
|
1076
|
+
({entryEnd, ...entry} = entry); // eslint-disable-line prefer-const
|
|
1077
|
+
return new Entry(INTERNAL_SYMBOL, {...entry, zip: this, _ref: null});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Read multiple entries.
|
|
1082
|
+
* If `numEntries` is provided, will read at maximum that number of entries.
|
|
1083
|
+
* Otherwise, reads all entries.
|
|
1084
|
+
* @async
|
|
1085
|
+
* @param {number} [numEntries] - Number of entries to read
|
|
1086
|
+
* @returns {Array<Entry>} - Array of entries
|
|
1087
|
+
*/
|
|
1088
|
+
async readEntries(numEntries) {
|
|
1089
|
+
if (numEntries != null) {
|
|
1090
|
+
assert(isPositiveIntegerOrZero(numEntries), '`numEntries` must be a positive integer if provided');
|
|
1091
|
+
} else {
|
|
1092
|
+
numEntries = Infinity;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const entries = [];
|
|
1096
|
+
for (let i = 0; i < numEntries; i++) {
|
|
1097
|
+
const entry = await this.readEntry();
|
|
1098
|
+
if (!entry) break;
|
|
1099
|
+
entries.push(entry);
|
|
1100
|
+
}
|
|
1101
|
+
return entries;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Get async iterator for entries.
|
|
1106
|
+
* Usage: `for await (const entry of zip) { ... }`
|
|
1107
|
+
* @returns {Object} - Async iterator
|
|
1108
|
+
*/
|
|
1109
|
+
[Symbol.asyncIterator]() {
|
|
1110
|
+
return {
|
|
1111
|
+
next: async () => {
|
|
1112
|
+
const entry = await this.readEntry();
|
|
1113
|
+
return {value: entry, done: entry === null};
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Get readable stream for file data.
|
|
1120
|
+
* @async
|
|
1121
|
+
* @param {Entry} entry - `Entry` object
|
|
1122
|
+
* @param {Object} [options] - Options
|
|
1123
|
+
* @param {boolean} [options.decompress] - `false` to output raw data without decompression
|
|
1124
|
+
* @param {boolean} [options.decrypt] - `true` to decrypt if is encrypted
|
|
1125
|
+
* @param {number} [options.start] - Start offset (only valid if not decompressing)
|
|
1126
|
+
* @param {number} [options.end] - End offset (only valid if not decompressing)
|
|
1127
|
+
* @returns {Object} - Readable stream
|
|
1128
|
+
*/
|
|
1129
|
+
async openReadStream(entry, options) {
|
|
1130
|
+
assert(entry instanceof Entry, '`entry` must be an instance of `Entry`');
|
|
1131
|
+
assert(entry.zip === this, '`entry` must be an `Entry` from this ZIP file');
|
|
1132
|
+
return await entry.openReadStream(options);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
module.exports = Zip;
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Determine if entry count is certain.
|
|
1140
|
+
* i.e. `centralDirectorySize` bytes could not fit 65536 more entries than stated.
|
|
1141
|
+
* @param {number} entryCount - Number of entries expected (may be under-estimate)
|
|
1142
|
+
* @param {number} centralDirectorySize - Size of Central Directory space to store entries
|
|
1143
|
+
* @returns {boolean} - `true` if entry count is certain
|
|
1144
|
+
*/
|
|
1145
|
+
function entryCountIsCertain(entryCount, centralDirectorySize) {
|
|
1146
|
+
return (entryCount + 0x10000) * CDH_MIN_LENGTH > centralDirectorySize;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Check if first entry may be a Mac OS Archive Utility entry,
|
|
1151
|
+
* according to various distinguishing characteristics.
|
|
1152
|
+
* @param {Object} entry - Entry props from `_readEntryAt()`
|
|
1153
|
+
* @returns {boolean} - `true` if matches signature of a Mac OS ZIP first entry
|
|
1154
|
+
*/
|
|
1155
|
+
function firstEntryMaybeMac(entry) {
|
|
1156
|
+
// First file always starts at byte 0
|
|
1157
|
+
if (entry.fileHeaderOffset !== 0) return false;
|
|
1158
|
+
return entryMaybeMac(entry);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Check if entry may be a Mac OS Archive Utility entry,
|
|
1163
|
+
* according to various distinguishing characteristics.
|
|
1164
|
+
* @param {Object} entry - Entry props from `_readEntryAt()`
|
|
1165
|
+
* @returns {boolean} - `true` if matches signature of a Mac OS ZIP entry
|
|
1166
|
+
*/
|
|
1167
|
+
function entryMaybeMac(entry) {
|
|
1168
|
+
// Entries always have this `versionMadeBy` value
|
|
1169
|
+
if (entry.versionMadeBy !== 789) return false;
|
|
1170
|
+
|
|
1171
|
+
// Entries never have comments
|
|
1172
|
+
if (entry.comment.length !== 0) return false;
|
|
1173
|
+
|
|
1174
|
+
// Entries never have ZIP64 headers
|
|
1175
|
+
if (entry.isZip64) return false;
|
|
1176
|
+
|
|
1177
|
+
// Check various attributes for files, folders and symlinks
|
|
1178
|
+
if (entry.versionNeededToExtract === 20) {
|
|
1179
|
+
// File
|
|
1180
|
+
if (
|
|
1181
|
+
entry.generalPurposeBitFlag !== 8 || entry.compressionMethod !== 8 || endsWithSlash(entry.filename)
|
|
1182
|
+
) return false;
|
|
1183
|
+
} else if (entry.versionNeededToExtract === 10) {
|
|
1184
|
+
// Folder, empty file, or symlink
|
|
1185
|
+
if (
|
|
1186
|
+
entry.generalPurposeBitFlag !== 0 || entry.compressionMethod !== 0
|
|
1187
|
+
|| entry.uncompressedSize !== entry.compressedSize
|
|
1188
|
+
) return false;
|
|
1189
|
+
|
|
1190
|
+
if (entry.extraFields.length === 0) {
|
|
1191
|
+
// Symlink
|
|
1192
|
+
if (entry.compressedSize === 0 || endsWithSlash(entry.filename)) return false;
|
|
1193
|
+
// Symlinks have no Extra Fields, so skip the check below.
|
|
1194
|
+
// It is probably a Mac Archive Utility ZIP file.
|
|
1195
|
+
return true;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Folder or empty file
|
|
1199
|
+
if (entry.compressedSize !== 0 || entry.crc32 !== 0) return false;
|
|
1200
|
+
} else {
|
|
1201
|
+
// Unrecognised
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Files + folders always have 1 Extra Field with certain id and length
|
|
1206
|
+
if (
|
|
1207
|
+
entry.extraFields.length !== 1
|
|
1208
|
+
|| entry.extraFields[0].id !== MAC_CDH_EXTRA_FIELD_ID
|
|
1209
|
+
|| entry.extraFields[0].data.length !== MAC_CDH_EXTRA_FIELD_LENGTH
|
|
1210
|
+
) return false;
|
|
1211
|
+
|
|
1212
|
+
// It is probably a Mac Archive Utility ZIP file
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* Determine if filename (as undecoded buffer) ends with a slash.
|
|
1218
|
+
* @param {Buffer} filename - Filename as buffer
|
|
1219
|
+
* @returns {boolean} - `true` if filename ends with slash
|
|
1220
|
+
*/
|
|
1221
|
+
function endsWithSlash(filename) {
|
|
1222
|
+
// Code for '/' is 47 in both CP437 and UTF8
|
|
1223
|
+
return filename[filename.length - 1] === 47;
|
|
1224
|
+
}
|