@patrickdk77/archive-defuse 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +345 -2
- package/dist/aes7z.d.ts +16 -0
- package/dist/aes7z.js +131 -0
- package/dist/alone.d.ts +21 -0
- package/dist/alone.js +65 -0
- package/dist/bcj2.d.ts +21 -0
- package/dist/bcj2.js +140 -0
- package/dist/bzip2.d.ts +21 -0
- package/dist/bzip2.js +688 -0
- package/dist/cab.d.ts +63 -0
- package/dist/cab.js +493 -0
- package/dist/checks.d.ts +23 -0
- package/dist/checks.js +123 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +364 -0
- package/dist/codecs.d.ts +39 -0
- package/dist/codecs.js +124 -0
- package/dist/coders.d.ts +58 -0
- package/dist/coders.js +392 -0
- package/dist/crc32.d.ts +8 -0
- package/dist/crc32.js +59 -0
- package/dist/encoder.d.ts +44 -0
- package/dist/encoder.js +114 -0
- package/dist/engine.d.ts +44 -0
- package/dist/engine.js +753 -0
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +98 -0
- package/dist/filters.d.ts +34 -0
- package/dist/filters.js +385 -0
- package/dist/findings.d.ts +26 -0
- package/dist/findings.js +176 -0
- package/dist/formats.d.ts +14 -0
- package/dist/formats.js +97 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +50 -0
- package/dist/index.mjs +3 -0
- package/dist/inflate64.d.ts +6 -0
- package/dist/inflate64.js +388 -0
- package/dist/io.d.ts +78 -0
- package/dist/io.js +379 -0
- package/dist/lzma-formats.d.ts +75 -0
- package/dist/lzma-formats.js +373 -0
- package/dist/lzma-sniff.d.ts +20 -0
- package/dist/lzma-sniff.js +73 -0
- package/dist/lzma-writers.d.ts +35 -0
- package/dist/lzma-writers.js +276 -0
- package/dist/lzma1/LICENSE +201 -0
- package/dist/lzma1/NOTICE +36 -0
- package/dist/lzma1/crc32.d.ts +4 -0
- package/dist/lzma1/crc32.js +19 -0
- package/dist/lzma1/hash234.d.ts +22 -0
- package/dist/lzma1/hash234.js +75 -0
- package/dist/lzma1/hc4.d.ts +30 -0
- package/dist/lzma1/hc4.js +142 -0
- package/dist/lzma1/header.d.ts +32 -0
- package/dist/lzma1/header.js +73 -0
- package/dist/lzma1/lz-decoder.d.ts +41 -0
- package/dist/lzma1/lz-decoder.js +128 -0
- package/dist/lzma1/lz-encoder.d.ts +87 -0
- package/dist/lzma1/lz-encoder.js +166 -0
- package/dist/lzma1/lzma-coder.d.ts +107 -0
- package/dist/lzma1/lzma-coder.js +162 -0
- package/dist/lzma1/lzma-decoder.d.ts +36 -0
- package/dist/lzma1/lzma-decoder.js +484 -0
- package/dist/lzma1/lzma-encoder-fast.d.ts +13 -0
- package/dist/lzma1/lzma-encoder-fast.js +114 -0
- package/dist/lzma1/lzma-encoder.d.ts +105 -0
- package/dist/lzma1/lzma-encoder.js +497 -0
- package/dist/lzma1/lzma2-decoder.d.ts +85 -0
- package/dist/lzma1/lzma2-decoder.js +249 -0
- package/dist/lzma1/range-coder.d.ts +18 -0
- package/dist/lzma1/range-coder.js +23 -0
- package/dist/lzma1/range-decoder.d.ts +41 -0
- package/dist/lzma1/range-decoder.js +78 -0
- package/dist/lzma1/range-encoder.d.ts +53 -0
- package/dist/lzma1/range-encoder.js +269 -0
- package/dist/lzma1-stream.d.ts +60 -0
- package/dist/lzma1-stream.js +202 -0
- package/dist/model.d.ts +141 -0
- package/dist/model.js +16 -0
- package/dist/names.d.ts +13 -0
- package/dist/names.js +66 -0
- package/dist/pack.d.ts +72 -0
- package/dist/pack.js +514 -0
- package/dist/plugins.d.ts +13 -0
- package/dist/plugins.js +55 -0
- package/dist/rules.d.ts +25 -0
- package/dist/rules.js +403 -0
- package/dist/score.d.ts +17 -0
- package/dist/score.js +58 -0
- package/dist/seven.d.ts +75 -0
- package/dist/seven.js +492 -0
- package/dist/sevenzip.d.ts +133 -0
- package/dist/sevenzip.js +529 -0
- package/dist/single.d.ts +103 -0
- package/dist/single.js +453 -0
- package/dist/sniff.d.ts +20 -0
- package/dist/sniff.js +224 -0
- package/dist/stream-meter.d.ts +39 -0
- package/dist/stream-meter.js +54 -0
- package/dist/tar.d.ts +102 -0
- package/dist/tar.js +765 -0
- package/dist/types.d.ts +284 -0
- package/dist/types.js +133 -0
- package/dist/unpack.d.ts +67 -0
- package/dist/unpack.js +194 -0
- package/dist/xz.d.ts +25 -0
- package/dist/xz.js +333 -0
- package/dist/zip-lzma.d.ts +15 -0
- package/dist/zip-lzma.js +137 -0
- package/dist/zip-writer.d.ts +41 -0
- package/dist/zip-writer.js +263 -0
- package/dist/zip.d.ts +99 -0
- package/dist/zip.js +901 -0
- package/dist/zipcrypt.d.ts +47 -0
- package/dist/zipcrypt.js +233 -0
- package/dist/zstd.d.ts +25 -0
- package/dist/zstd.js +160 -0
- package/package.json +94 -4
package/dist/zip.js
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZipArchive = void 0;
|
|
4
|
+
exports.hasZipEnd = hasZipEnd;
|
|
5
|
+
// Copied from office-defuse src/zip.ts. Changed: a header that only one reader would take differently becomes a note
|
|
6
|
+
// for the engine instead of a refusal, so the rewrite can settle it. The reader picks among several end records,
|
|
7
|
+
// follows a central directory whose offsets are shifted, decodes names in CP437 and from the Unicode path field, reads
|
|
8
|
+
// symbolic links, devices and permissions from Unix attributes, opens ZipCrypto and WinZip AES entries, and decodes
|
|
9
|
+
// bzip2 and zstd entries besides stored and deflated ones. Name rules and duplicate names moved to the engine.
|
|
10
|
+
const bzip2_1 = require("./bzip2");
|
|
11
|
+
const codecs_1 = require("./codecs");
|
|
12
|
+
const crc32_1 = require("./crc32");
|
|
13
|
+
const errors_1 = require("./errors");
|
|
14
|
+
const inflate64_1 = require("./inflate64");
|
|
15
|
+
const zip_lzma_1 = require("./zip-lzma");
|
|
16
|
+
/** The LZMA dictionary limit's default, for a reader opened without the run's limits. */
|
|
17
|
+
const DEFAULT_DICTIONARY = 256 * 1024 * 1024;
|
|
18
|
+
const io_1 = require("./io");
|
|
19
|
+
const names_1 = require("./names");
|
|
20
|
+
const sniff_1 = require("./sniff");
|
|
21
|
+
const types_1 = require("./types");
|
|
22
|
+
const zip_writer_1 = require("./zip-writer");
|
|
23
|
+
const zipcrypt_1 = require("./zipcrypt");
|
|
24
|
+
const SIG_LOCAL = 0x04034b50;
|
|
25
|
+
const SIG_CENTRAL = 0x02014b50;
|
|
26
|
+
const SIG_END = 0x06054b50;
|
|
27
|
+
const SIG_END64 = 0x06064b50;
|
|
28
|
+
const SIG_LOCATOR64 = 0x07064b50;
|
|
29
|
+
const SIG_DESCRIPTOR = 0x08074b50;
|
|
30
|
+
const MAX16 = 0xffff;
|
|
31
|
+
const MAX32 = 0xffffffff;
|
|
32
|
+
const UNKNOWN64 = 0xffffffffffffffffn;
|
|
33
|
+
/** The end record is 22 bytes and its comment at most 65,535. */
|
|
34
|
+
const END_SEARCH = 22 + MAX16;
|
|
35
|
+
/** Flags whose meaning a local header and the central directory must agree on: encryption, data descriptor, strong encryption, UTF-8 names and an encrypted directory. */
|
|
36
|
+
const FLAG_MASK = 0x0001 | 0x0008 | 0x0040 | 0x0800 | 0x2000;
|
|
37
|
+
/** A symbolic link's target is read whole before the rules run, so it has to be short. */
|
|
38
|
+
const MAX_LINK = 4096;
|
|
39
|
+
/** Hidden data is searched for local headers up to this many bytes. */
|
|
40
|
+
const HIDDEN_SCAN = 64 * 1024 * 1024;
|
|
41
|
+
/** Extra field records the rewrite replaces with its own: ZIP64 sizes, Unicode names and comments, and AES. */
|
|
42
|
+
const STRUCTURAL_EXTRAS = new Set([0x0001, 0x7075, 0x6375, 0x9901]);
|
|
43
|
+
class Bytes {
|
|
44
|
+
b;
|
|
45
|
+
view;
|
|
46
|
+
constructor(b) {
|
|
47
|
+
this.b = b;
|
|
48
|
+
this.view = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
49
|
+
}
|
|
50
|
+
u16(at) {
|
|
51
|
+
return this.view.getUint16(at, true);
|
|
52
|
+
}
|
|
53
|
+
u32(at) {
|
|
54
|
+
return this.view.getUint32(at, true);
|
|
55
|
+
}
|
|
56
|
+
/** An 8-byte value as it is, for comparing bytes that may not be a 64-bit field at all. */
|
|
57
|
+
big(at) {
|
|
58
|
+
return this.view.getBigUint64(at, true);
|
|
59
|
+
}
|
|
60
|
+
/** An 8-byte value, or undefined when it does not fit a JavaScript number exactly. */
|
|
61
|
+
u64(at) {
|
|
62
|
+
const v = this.big(at);
|
|
63
|
+
return v > BigInt(Number.MAX_SAFE_INTEGER) ? undefined : Number(v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const unparseable = (reason) => {
|
|
67
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Corrupted, types_1.ArchiveDetail.Unparseable, reason, { reason });
|
|
68
|
+
};
|
|
69
|
+
/** Splits an extra field block into records. A block that does not split exactly keeps the records before the fault. */
|
|
70
|
+
function parseExtra(extra) {
|
|
71
|
+
const records = [];
|
|
72
|
+
const b = new Bytes(extra);
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
let p = 0;
|
|
75
|
+
while (p < extra.length) {
|
|
76
|
+
if (p + 4 > extra.length)
|
|
77
|
+
return { records, malformed: 'an extra field block ends inside a record header' };
|
|
78
|
+
const id = b.u16(p);
|
|
79
|
+
const len = b.u16(p + 2);
|
|
80
|
+
if (p + 4 + len > extra.length)
|
|
81
|
+
return { records, malformed: 'an extra field record runs past its block' };
|
|
82
|
+
if (seen.has(id))
|
|
83
|
+
return { records, malformed: `extra field 0x${id.toString(16).padStart(4, '0')} appears twice` };
|
|
84
|
+
seen.add(id);
|
|
85
|
+
records.push({ id, data: extra.subarray(p + 4, p + 4 + len) });
|
|
86
|
+
p += 4 + len;
|
|
87
|
+
}
|
|
88
|
+
return { records };
|
|
89
|
+
}
|
|
90
|
+
const hex = (id) => `0x${id.toString(16).padStart(4, '0')}`;
|
|
91
|
+
/** The names of the times and owners records, for findings. */
|
|
92
|
+
const METADATA_NAMES = {
|
|
93
|
+
21589: 'extended timestamp',
|
|
94
|
+
10: 'NTFS times',
|
|
95
|
+
22613: 'old Info-ZIP Unix',
|
|
96
|
+
30805: 'Info-ZIP Unix owners',
|
|
97
|
+
30837: 'Info-ZIP new Unix owners',
|
|
98
|
+
};
|
|
99
|
+
const setBits = (v) => (v & 1) + ((v >> 1) & 1) + ((v >> 2) & 1);
|
|
100
|
+
/**
|
|
101
|
+
* Why a times or owners record from the central directory does not have the layout Info-ZIP's extrafld.txt gives
|
|
102
|
+
* it, or undefined when it does. The rewrite copies these records into its own headers, so one that a reader could
|
|
103
|
+
* read past its end is not kept.
|
|
104
|
+
*/
|
|
105
|
+
function metadataFault(r) {
|
|
106
|
+
const d = r.data;
|
|
107
|
+
const n = d.length;
|
|
108
|
+
switch (r.id) {
|
|
109
|
+
case 0x5455: {
|
|
110
|
+
// A flags byte, then the times its low three bits name. The central version may hold the modification time alone.
|
|
111
|
+
if (n === 0)
|
|
112
|
+
return 'it has no flags byte';
|
|
113
|
+
if ((n - 1) % 4 !== 0 || (n - 1) / 4 > setBits(d[0] & 7))
|
|
114
|
+
return `its ${n} bytes do not fit the times its flags name`;
|
|
115
|
+
if (d[0] & 1 && n < 5)
|
|
116
|
+
return 'its flags name a modification time it does not hold';
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
case 0x000a: {
|
|
120
|
+
// Four reserved bytes, then tagged attributes. Tag 1 holds three 8-byte times.
|
|
121
|
+
if (n < 4)
|
|
122
|
+
return 'it ends inside its reserved bytes';
|
|
123
|
+
const b = new Bytes(d);
|
|
124
|
+
for (let p = 4; p < n;) {
|
|
125
|
+
if (p + 4 > n)
|
|
126
|
+
return 'it ends inside an attribute header';
|
|
127
|
+
const tag = b.u16(p);
|
|
128
|
+
const size = b.u16(p + 2);
|
|
129
|
+
if (p + 4 + size > n)
|
|
130
|
+
return `attribute ${tag} runs past the record`;
|
|
131
|
+
if (tag === 1 && size !== 24)
|
|
132
|
+
return `its times attribute has ${size} bytes, not 24`;
|
|
133
|
+
p += 4 + size;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
case 0x5855:
|
|
138
|
+
// Access and modification times, then a user and a group ID only in a local header.
|
|
139
|
+
return n === 8 || n === 12 ? undefined : `it has ${n} bytes, not 8 or 12`;
|
|
140
|
+
case 0x7855:
|
|
141
|
+
// Empty in the central directory, a 2-byte user and group ID in a local header.
|
|
142
|
+
return n === 0 || n === 4 ? undefined : `it has ${n} bytes, not 0 or 4`;
|
|
143
|
+
case 0x7875: {
|
|
144
|
+
// A version, then a user ID and a group ID, each after its size. An empty record only flags that they exist.
|
|
145
|
+
if (n === 0)
|
|
146
|
+
return undefined;
|
|
147
|
+
if (d[0] !== 1)
|
|
148
|
+
return `it is version ${d[0]}, and only version 1 is defined`;
|
|
149
|
+
if (n < 2 || n < 3 + d[1])
|
|
150
|
+
return 'it ends inside its user ID';
|
|
151
|
+
return 3 + d[1] + d[2 + d[1]] === n ? undefined : 'its ID sizes do not add up to its length';
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** The method a ZIP entry's data uses, by its number. */
|
|
158
|
+
function methodOf(code) {
|
|
159
|
+
switch (code) {
|
|
160
|
+
case 0:
|
|
161
|
+
return { name: 'stored', readable: true, unusual: false };
|
|
162
|
+
case 8:
|
|
163
|
+
return { name: 'deflate', readable: true, unusual: false };
|
|
164
|
+
case 9:
|
|
165
|
+
return { name: 'deflate64', readable: true, unusual: true };
|
|
166
|
+
case 12:
|
|
167
|
+
return { name: 'bzip2', readable: true, unusual: true };
|
|
168
|
+
case 14:
|
|
169
|
+
return { name: 'lzma', readable: true, unusual: true };
|
|
170
|
+
case 95:
|
|
171
|
+
return { name: 'xz', readable: true, unusual: true };
|
|
172
|
+
case 93:
|
|
173
|
+
return { name: 'zstd', readable: true, unusual: true, unavailable: codecs_1.zstdDecompressor === undefined };
|
|
174
|
+
default: {
|
|
175
|
+
const names = {
|
|
176
|
+
1: 'shrink',
|
|
177
|
+
2: 'reduce',
|
|
178
|
+
3: 'reduce',
|
|
179
|
+
4: 'reduce',
|
|
180
|
+
5: 'reduce',
|
|
181
|
+
6: 'implode',
|
|
182
|
+
10: 'PKWARE implode',
|
|
183
|
+
14: 'LZMA',
|
|
184
|
+
18: 'IBM TERSE',
|
|
185
|
+
19: 'IBM LZ77',
|
|
186
|
+
20: 'zstd (old number)',
|
|
187
|
+
94: 'MP3',
|
|
188
|
+
95: 'XZ',
|
|
189
|
+
96: 'JPEG',
|
|
190
|
+
97: 'WavPack',
|
|
191
|
+
98: 'PPMd',
|
|
192
|
+
};
|
|
193
|
+
return { name: names[code] ?? `method ${code}`, readable: false, unusual: true };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const S_IFMT = 0o170000;
|
|
198
|
+
/** What an entry is, from its name and, for entries made on Unix, the file type in its attributes. */
|
|
199
|
+
function kindOf(name, versionMadeBy, externalAttr) {
|
|
200
|
+
const host = versionMadeBy >>> 8;
|
|
201
|
+
const unixMode = externalAttr >>> 16;
|
|
202
|
+
if (host === 3 && unixMode !== 0) {
|
|
203
|
+
const type = unixMode & S_IFMT;
|
|
204
|
+
const mode = unixMode & 0o7777;
|
|
205
|
+
if (name.endsWith('/') || type === 0o040000)
|
|
206
|
+
return { kind: 'directory', mode };
|
|
207
|
+
if (type === 0o120000)
|
|
208
|
+
return { kind: 'symlink', mode };
|
|
209
|
+
if (type === 0o020000 || type === 0o060000)
|
|
210
|
+
return { kind: 'device', mode, typeCode: type === 0o020000 ? 'character device' : 'block device' };
|
|
211
|
+
if (type === 0o010000)
|
|
212
|
+
return { kind: 'fifo', mode };
|
|
213
|
+
if (type === 0o140000)
|
|
214
|
+
return { kind: 'socket', mode };
|
|
215
|
+
if (type === 0o100000 || type === 0)
|
|
216
|
+
return { kind: 'file', mode };
|
|
217
|
+
return { kind: 'other', mode, typeCode: `Unix file type 0o${type.toString(8)}` };
|
|
218
|
+
}
|
|
219
|
+
if (name.endsWith('/') || (externalAttr & 0x10) !== 0)
|
|
220
|
+
return { kind: 'directory' };
|
|
221
|
+
return { kind: 'file' };
|
|
222
|
+
}
|
|
223
|
+
/** Decodes a name: UTF-8 under the flag, ASCII as it is, and anything else as CP437 or UTF-8, which readers pick differently. */
|
|
224
|
+
function decodeName(raw, flags) {
|
|
225
|
+
if (flags & 0x0800) {
|
|
226
|
+
const u = (0, names_1.utf8OrUndefined)(raw);
|
|
227
|
+
return u === undefined ? { name: Buffer.from(raw).toString('utf8'), ambiguous: true } : { name: u, ambiguous: false };
|
|
228
|
+
}
|
|
229
|
+
if ((0, names_1.isAscii)(raw))
|
|
230
|
+
return { name: Buffer.from(raw).toString('latin1'), ambiguous: false };
|
|
231
|
+
// Without the flag the format says CP437, but many writers put UTF-8 there and many readers take it.
|
|
232
|
+
return { name: (0, names_1.utf8OrUndefined)(raw) ?? (0, names_1.cp437)(raw), ambiguous: true };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The Info-ZIP Unicode path or comment field: version 1, the CRC-32 of the header's own bytes, then UTF-8. Returns
|
|
236
|
+
* the text when the CRC matches, `stale` when the field belongs to other bytes, and nothing for a field it cannot read.
|
|
237
|
+
*/
|
|
238
|
+
function unicodeField(field, headerBytes) {
|
|
239
|
+
if (!field || field.length < 5 || field[0] !== 1)
|
|
240
|
+
return {};
|
|
241
|
+
const text = (0, names_1.utf8OrUndefined)(field.subarray(5));
|
|
242
|
+
if (text === undefined)
|
|
243
|
+
return { stale: true };
|
|
244
|
+
if (new Bytes(field).u32(1) !== (0, crc32_1.crc32)(headerBytes))
|
|
245
|
+
return { stale: true };
|
|
246
|
+
return { text };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Reads the end record at `at`, with its ZIP64 records. Returns undefined when the bytes there cannot be an end record
|
|
250
|
+
* whose directory exists. A ZIP64 record that contradicts the end record throws, since no reading of it is safe.
|
|
251
|
+
*/
|
|
252
|
+
async function endRecordAt(reader, size, at) {
|
|
253
|
+
const r = new Bytes(await reader.read(at, 22));
|
|
254
|
+
if (r.b.length < 22 || r.u32(0) !== SIG_END)
|
|
255
|
+
return undefined;
|
|
256
|
+
const short = {
|
|
257
|
+
at,
|
|
258
|
+
commentLength: r.u16(20),
|
|
259
|
+
disk: r.u16(4),
|
|
260
|
+
dirDisk: r.u16(6),
|
|
261
|
+
countOnDisk: r.u16(8),
|
|
262
|
+
count: r.u16(10),
|
|
263
|
+
dirSize: r.u32(12),
|
|
264
|
+
dirOffset: r.u32(16),
|
|
265
|
+
dirEnd: at,
|
|
266
|
+
};
|
|
267
|
+
// The directory sits where the end record says, or, in an archive that had data put in front of it, right before
|
|
268
|
+
// the end record, where readers that correct the offsets look.
|
|
269
|
+
const sigAt = async (p) => p >= 0 && p + 4 <= size && new Bytes(await reader.read(p, 4)).u32(0) === SIG_CENTRAL;
|
|
270
|
+
const locate = async (e) => {
|
|
271
|
+
if (e.count === 0 && e.dirSize === 0)
|
|
272
|
+
e.dirStart = e.dirEnd;
|
|
273
|
+
else if (e.dirOffset + e.dirSize <= e.dirEnd && (await sigAt(e.dirOffset)))
|
|
274
|
+
e.dirStart = e.dirOffset;
|
|
275
|
+
else if (e.dirEnd - e.dirSize >= 0 && (await sigAt(e.dirEnd - e.dirSize)))
|
|
276
|
+
e.dirStart = e.dirEnd - e.dirSize;
|
|
277
|
+
else
|
|
278
|
+
return undefined;
|
|
279
|
+
return e;
|
|
280
|
+
};
|
|
281
|
+
const locatorAt = at - 20;
|
|
282
|
+
const locator = new Bytes(locatorAt >= 0 ? await reader.read(locatorAt, 20) : new Uint8Array(0));
|
|
283
|
+
if (locator.b.length !== 20 || locator.u32(0) !== SIG_LOCATOR64) {
|
|
284
|
+
if (short.dirSize === MAX32 || short.dirOffset === MAX32)
|
|
285
|
+
return undefined;
|
|
286
|
+
return locate(short);
|
|
287
|
+
}
|
|
288
|
+
if (locator.u32(4) !== 0 || locator.u32(16) > 1)
|
|
289
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.MultiVolume, 'The ZIP64 locator names another disk');
|
|
290
|
+
// A locator that leads to no ZIP64 end record leaves readers that ignore it with the end record's own values.
|
|
291
|
+
const fallback = async () => {
|
|
292
|
+
if (short.dirSize === MAX32 || short.dirOffset === MAX32)
|
|
293
|
+
return undefined;
|
|
294
|
+
const found = await locate(short);
|
|
295
|
+
if (found)
|
|
296
|
+
found.disagree = 'a ZIP64 locator leads to no ZIP64 end record';
|
|
297
|
+
return found;
|
|
298
|
+
};
|
|
299
|
+
const z = locator.u64(8);
|
|
300
|
+
if (z === undefined || z + 56 > locatorAt)
|
|
301
|
+
return fallback();
|
|
302
|
+
const rec = new Bytes(await reader.read(z, 56));
|
|
303
|
+
if (rec.u32(0) !== SIG_END64)
|
|
304
|
+
return fallback();
|
|
305
|
+
const len = rec.u64(4);
|
|
306
|
+
if (len === undefined || z + 12 + len !== locatorAt)
|
|
307
|
+
return fallback();
|
|
308
|
+
const values = [rec.u32(16), rec.u32(20), rec.u64(24), rec.u64(32), rec.u64(40), rec.u64(48)];
|
|
309
|
+
if (values.some(v => v === undefined))
|
|
310
|
+
return unparseable('a ZIP64 end record holds a value beyond any file this reader can hold');
|
|
311
|
+
const [disk, dirDisk, countOnDisk, count, dirSize, dirOffset] = values;
|
|
312
|
+
const long = { at, commentLength: short.commentLength, disk, dirDisk, countOnDisk, count, dirSize, dirOffset, dirEnd: z };
|
|
313
|
+
// A field the end record does not leave to the ZIP64 record and gives otherwise splits readers: some take the ZIP64
|
|
314
|
+
// record whenever it is there, and others only for the fields at their maximum.
|
|
315
|
+
const split = (a, max, b) => a !== max && a !== b;
|
|
316
|
+
const disagree = split(short.disk, MAX16, disk) ||
|
|
317
|
+
split(short.dirDisk, MAX16, dirDisk) ||
|
|
318
|
+
split(short.countOnDisk, MAX16, countOnDisk) ||
|
|
319
|
+
split(short.count, MAX16, count) ||
|
|
320
|
+
split(short.dirSize, MAX32, dirSize) ||
|
|
321
|
+
split(short.dirOffset, MAX32, dirOffset);
|
|
322
|
+
const found = (await locate(long)) ?? (disagree ? await locate({ ...short, dirEnd: z }) : undefined);
|
|
323
|
+
if (found && disagree)
|
|
324
|
+
found.disagree = 'the end record and the ZIP64 end record give different values';
|
|
325
|
+
return found;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* A ZIP archive read from its central directory, with every local header checked against it. Reading an entry
|
|
329
|
+
* decrypts and decodes it in chunks and checks its size and CRC.
|
|
330
|
+
*/
|
|
331
|
+
class ZipArchive {
|
|
332
|
+
reader;
|
|
333
|
+
entries;
|
|
334
|
+
notes;
|
|
335
|
+
start;
|
|
336
|
+
end;
|
|
337
|
+
commentBytes;
|
|
338
|
+
rivals;
|
|
339
|
+
format = 'zip';
|
|
340
|
+
single = false;
|
|
341
|
+
lateNotes = [];
|
|
342
|
+
comment;
|
|
343
|
+
constructor(reader, entries, notes,
|
|
344
|
+
/** Where the archive starts in the file: the first local header, or the directory when there is none. */
|
|
345
|
+
start,
|
|
346
|
+
/** Where the archive ends: after the end record's comment. */
|
|
347
|
+
end, commentBytes,
|
|
348
|
+
/** Where the other end records that lead to a directory sit, which an entry's data can hold. */
|
|
349
|
+
rivals = []) {
|
|
350
|
+
this.reader = reader;
|
|
351
|
+
this.entries = entries;
|
|
352
|
+
this.notes = notes;
|
|
353
|
+
this.start = start;
|
|
354
|
+
this.end = end;
|
|
355
|
+
this.commentBytes = commentBytes;
|
|
356
|
+
this.rivals = rivals;
|
|
357
|
+
if (commentBytes.length)
|
|
358
|
+
this.comment = Buffer.from(commentBytes).toString('latin1');
|
|
359
|
+
}
|
|
360
|
+
/** What LZMA and XZ entries need from the run: its limits, and a temporary folder for an XZ entry's bytes. */
|
|
361
|
+
decoders;
|
|
362
|
+
static async open(source, limits, deadline) {
|
|
363
|
+
const size = await source.size();
|
|
364
|
+
const reader = new io_1.Reader(source, size);
|
|
365
|
+
const notes = [];
|
|
366
|
+
if (size < 22)
|
|
367
|
+
unparseable('the file is too short to be a ZIP archive');
|
|
368
|
+
const tailStart = Math.max(0, size - END_SEARCH);
|
|
369
|
+
const tail = new Bytes(await reader.read(tailStart, size - tailStart));
|
|
370
|
+
// Every end signature in the last 64 KiB, last first, since that is the one a reader scanning backwards takes.
|
|
371
|
+
const found = [];
|
|
372
|
+
for (let p = tail.b.length - 22; p >= 0; p--) {
|
|
373
|
+
if (tail.b[p] !== 0x50 || tail.u32(p) !== SIG_END)
|
|
374
|
+
continue;
|
|
375
|
+
(0, errors_1.checkTime)(deadline);
|
|
376
|
+
const e = await endRecordAt(reader, size, tailStart + p);
|
|
377
|
+
if (e)
|
|
378
|
+
found.push(e);
|
|
379
|
+
}
|
|
380
|
+
// The last end record whose comment fits in the file is the archive. Another one whose comment ends exactly at the
|
|
381
|
+
// end of the file, and that leads to a directory, is one that readers checking the comment length can take instead.
|
|
382
|
+
const end = found.find(e => e.at + 22 + e.commentLength <= size) ?? found[0];
|
|
383
|
+
if (!end) {
|
|
384
|
+
// A file that starts with the spanning marker and has no end record is the first part of a split archive.
|
|
385
|
+
const head = await reader.read(0, 4);
|
|
386
|
+
if (new Bytes(head).b.length === 4 && new Bytes(head).u32(0) === SIG_DESCRIPTOR)
|
|
387
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.MultiVolume, 'The first part of a split archive');
|
|
388
|
+
return unparseable('no end of central directory record that leads to a central directory');
|
|
389
|
+
}
|
|
390
|
+
const rivals = found.filter(e => e !== end && e.at + 22 + e.commentLength === size);
|
|
391
|
+
if (rivals.length)
|
|
392
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.SeveralEndRecords, data: { records: rivals.length + 1 } });
|
|
393
|
+
if (end.disagree)
|
|
394
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HeaderMismatch, data: { what: end.disagree } });
|
|
395
|
+
let archiveEnd = end.at + 22 + end.commentLength;
|
|
396
|
+
if (archiveEnd > size) {
|
|
397
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: "the end record's comment runs past the end of the file" } });
|
|
398
|
+
archiveEnd = size;
|
|
399
|
+
}
|
|
400
|
+
const commentBytes = (await reader.read(end.at + 22, archiveEnd - end.at - 22)).slice();
|
|
401
|
+
if (end.disk !== 0 || end.dirDisk !== 0)
|
|
402
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.MultiVolume, 'The archive spans several disks');
|
|
403
|
+
const dirStart = end.dirStart ?? end.dirOffset;
|
|
404
|
+
const shift = dirStart - end.dirOffset;
|
|
405
|
+
if (shift !== 0)
|
|
406
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.OffsetShift, data: { bytes: shift } });
|
|
407
|
+
if (end.count > limits.entries)
|
|
408
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.EntryCount, `${end.count} entries`, { entries: end.count, limit: limits.entries });
|
|
409
|
+
const entries = [];
|
|
410
|
+
let total = 0;
|
|
411
|
+
let pos = dirStart;
|
|
412
|
+
while (pos + 46 <= end.dirEnd) {
|
|
413
|
+
if (entries.length % 256 === 0)
|
|
414
|
+
(0, errors_1.checkTime)(deadline);
|
|
415
|
+
const h = new Bytes(await reader.read(pos, 46));
|
|
416
|
+
if (h.u32(0) !== SIG_CENTRAL)
|
|
417
|
+
break;
|
|
418
|
+
const nameLength = h.u16(28);
|
|
419
|
+
const extraLength = h.u16(30);
|
|
420
|
+
const commentLength = h.u16(32);
|
|
421
|
+
const recordEnd = pos + 46 + nameLength + extraLength + commentLength;
|
|
422
|
+
if (recordEnd > end.dirEnd)
|
|
423
|
+
unparseable('a central directory header runs past the directory');
|
|
424
|
+
if (entries.length >= limits.entries)
|
|
425
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.EntryCount, 'Too many entries', { limit: limits.entries });
|
|
426
|
+
const body = await reader.read(pos + 46, nameLength + extraLength + commentLength);
|
|
427
|
+
const entry = ZipArchive.central(entries.length, h, body.subarray(0, nameLength).slice(), body.subarray(nameLength, nameLength + extraLength), body.subarray(nameLength + extraLength).slice(), shift);
|
|
428
|
+
// A broken entry's sizes mean nothing, and the entry is removed unread.
|
|
429
|
+
if (entry.broken) {
|
|
430
|
+
entries.push(entry);
|
|
431
|
+
pos = recordEnd;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (entry.size > limits.decompressedBytes)
|
|
435
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DecompressedSize, `${entry.name} declares ${entry.size} bytes`, { size: entry.size, limit: limits.decompressedBytes }, entry.name);
|
|
436
|
+
total += entry.size;
|
|
437
|
+
if (total > limits.totalDecompressedBytes)
|
|
438
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.TotalDecompressedSize, 'The entries declare too many bytes', { limit: limits.totalDecompressedBytes });
|
|
439
|
+
if (entry.size > 1024 * 1024 && entry.size > entry.compressedSize * limits.compressionRatio) {
|
|
440
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.Ratio, `${entry.name} declares a ratio past the limit`, { size: entry.size, compressedSize: entry.compressedSize, limit: limits.compressionRatio }, entry.name);
|
|
441
|
+
}
|
|
442
|
+
entries.push(entry);
|
|
443
|
+
pos = recordEnd;
|
|
444
|
+
}
|
|
445
|
+
// Readers that go by the count, by the directory size or by the entries they find can each see other entries.
|
|
446
|
+
if (entries.length !== end.count || pos !== end.dirEnd || end.countOnDisk !== end.count || dirStart + end.dirSize !== end.dirEnd) {
|
|
447
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HeaderMismatch, data: { what: 'the end record and the central directory disagree on the entries' } });
|
|
448
|
+
}
|
|
449
|
+
for (const [i, e] of entries.entries()) {
|
|
450
|
+
if (i % 256 === 0)
|
|
451
|
+
(0, errors_1.checkTime)(deadline);
|
|
452
|
+
if (!e.broken)
|
|
453
|
+
await ZipArchive.checkLocal(reader, e, dirStart);
|
|
454
|
+
}
|
|
455
|
+
const cover = await ZipArchive.coverage(reader, entries, dirStart, deadline);
|
|
456
|
+
const start = cover.leading;
|
|
457
|
+
if (cover.leading > 0) {
|
|
458
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.LeadingBytes, data: { bytes: cover.leading } });
|
|
459
|
+
const type = (0, sniff_1.sniffType)(await reader.read(0, Math.min(1024, cover.leading)));
|
|
460
|
+
if (type !== undefined)
|
|
461
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.Polyglot, data: { type, where: 'before the archive' } });
|
|
462
|
+
}
|
|
463
|
+
if (cover.hidden > 0)
|
|
464
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HiddenData, data: { bytes: cover.hidden, localHeaders: cover.localHeaders } });
|
|
465
|
+
if (cover.overlap > 0)
|
|
466
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.Overlap, data: { entries: cover.overlap } });
|
|
467
|
+
if (archiveEnd < size) {
|
|
468
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.TrailingBytes, data: { bytes: size - archiveEnd } });
|
|
469
|
+
const type = (0, sniff_1.sniffType)(await reader.read(archiveEnd, Math.min(1024, size - archiveEnd)));
|
|
470
|
+
if (type !== undefined)
|
|
471
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.Polyglot, data: { type, where: 'after the archive' } });
|
|
472
|
+
}
|
|
473
|
+
return new ZipArchive(reader, entries, notes, start, archiveEnd, commentBytes, rivals.map(r => r.at));
|
|
474
|
+
}
|
|
475
|
+
/** One entry from its central directory header. */
|
|
476
|
+
static central(index, h, rawName, extra, commentBytes, shift) {
|
|
477
|
+
const flags = h.u16(8);
|
|
478
|
+
const storedMethod = h.u16(10);
|
|
479
|
+
const versionMadeBy = h.u16(4);
|
|
480
|
+
const externalAttr = h.u32(38);
|
|
481
|
+
const notes = [];
|
|
482
|
+
const decoded = decodeName(rawName, flags);
|
|
483
|
+
let name = decoded.name;
|
|
484
|
+
const parsed = parseExtra(extra);
|
|
485
|
+
const byId = new Map(parsed.records.map(r => [r.id, r.data]));
|
|
486
|
+
const up = unicodeField(byId.get(0x7075), rawName);
|
|
487
|
+
if (up.text !== undefined && up.text !== name) {
|
|
488
|
+
// The field exists to give a name the header could not, so it differs from a non-ASCII header by design.
|
|
489
|
+
if (!decoded.ambiguous)
|
|
490
|
+
notes.push({ category: types_1.ArchiveCategory.Name, detail: types_1.ArchiveDetail.UnicodePath, data: { name, unicodeName: up.text } });
|
|
491
|
+
name = up.text;
|
|
492
|
+
}
|
|
493
|
+
else if (up.stale)
|
|
494
|
+
notes.push({ category: types_1.ArchiveCategory.Name, detail: types_1.ArchiveDetail.UnicodePath, data: { name, reason: 'the field belongs to another name' } });
|
|
495
|
+
if (decoded.ambiguous)
|
|
496
|
+
notes.push({ category: types_1.ArchiveCategory.Name, detail: types_1.ArchiveDetail.Encoding, data: { name } });
|
|
497
|
+
const comment = unicodeField(byId.get(0x6375), commentBytes).text ?? (commentBytes.length ? decodeName(commentBytes, flags).name : undefined);
|
|
498
|
+
if (parsed.malformed)
|
|
499
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.ExtraField, data: { field: 'a malformed extra field', reason: parsed.malformed } });
|
|
500
|
+
const extras = parsed.records.filter(r => !STRUCTURAL_EXTRAS.has(r.id));
|
|
501
|
+
for (const r of extras) {
|
|
502
|
+
if (!zip_writer_1.METADATA_EXTRAS.has(r.id)) {
|
|
503
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.ExtraField, data: { field: `extra field ${hex(r.id)}` }, keptOnInfo: true });
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const fault = metadataFault(r);
|
|
507
|
+
if (fault === undefined)
|
|
508
|
+
continue;
|
|
509
|
+
r.malformed = true;
|
|
510
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.ExtraField, data: { field: `a malformed ${METADATA_NAMES[r.id]} field`, reason: fault }, keptOnInfo: true });
|
|
511
|
+
}
|
|
512
|
+
const e = {
|
|
513
|
+
index,
|
|
514
|
+
name,
|
|
515
|
+
rawName,
|
|
516
|
+
kind: 'file',
|
|
517
|
+
size: h.u32(24),
|
|
518
|
+
compressedSize: h.u32(20),
|
|
519
|
+
notes,
|
|
520
|
+
flags,
|
|
521
|
+
storedMethod,
|
|
522
|
+
methodCode: storedMethod,
|
|
523
|
+
versionMadeBy,
|
|
524
|
+
externalAttr,
|
|
525
|
+
internalAttr: h.u16(36),
|
|
526
|
+
dosTime: h.u16(12),
|
|
527
|
+
dosDate: h.u16(14),
|
|
528
|
+
crc: h.u32(16),
|
|
529
|
+
localOffset: h.u32(42),
|
|
530
|
+
dataOffset: 0,
|
|
531
|
+
end: 0,
|
|
532
|
+
extras,
|
|
533
|
+
commentBytes,
|
|
534
|
+
comment,
|
|
535
|
+
};
|
|
536
|
+
const k = kindOf(name, versionMadeBy, externalAttr);
|
|
537
|
+
e.kind = k.kind;
|
|
538
|
+
if (k.mode !== undefined)
|
|
539
|
+
e.mode = k.mode;
|
|
540
|
+
if (k.typeCode !== undefined)
|
|
541
|
+
e.typeCode = k.typeCode;
|
|
542
|
+
// ZIP64 values the header defers to its extra field, in the order the format fixes.
|
|
543
|
+
const disk = h.u16(34);
|
|
544
|
+
const want = [e.size === MAX32, e.compressedSize === MAX32, e.localOffset === MAX32, disk === MAX16];
|
|
545
|
+
const length = (want[0] ? 8 : 0) + (want[1] ? 8 : 0) + (want[2] ? 8 : 0) + (want[3] ? 4 : 0);
|
|
546
|
+
const z = byId.get(0x0001);
|
|
547
|
+
if (length > 0) {
|
|
548
|
+
if (!z || z.length < length)
|
|
549
|
+
e.broken = 'a field at its maximum has no ZIP64 value behind it';
|
|
550
|
+
else {
|
|
551
|
+
const b = new Bytes(z);
|
|
552
|
+
let p = 0;
|
|
553
|
+
const next = () => {
|
|
554
|
+
const v = b.u64(p);
|
|
555
|
+
p += 8;
|
|
556
|
+
if (v === undefined)
|
|
557
|
+
e.broken = 'a ZIP64 value beyond any file this reader can hold';
|
|
558
|
+
return v ?? 0;
|
|
559
|
+
};
|
|
560
|
+
if (want[0])
|
|
561
|
+
e.size = next();
|
|
562
|
+
if (want[1])
|
|
563
|
+
e.compressedSize = next();
|
|
564
|
+
if (want[2])
|
|
565
|
+
e.localOffset = next();
|
|
566
|
+
if (want[3] && b.u32(p) !== 0)
|
|
567
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.MultiVolume, 'An entry starts on another disk');
|
|
568
|
+
if (z.length > length)
|
|
569
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.MalformedHeader, data: { reason: 'a ZIP64 extra field holds more values than its header defers to it' } });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
else if (disk !== 0)
|
|
573
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Structure, types_1.ArchiveDetail.MultiVolume, 'An entry starts on another disk');
|
|
574
|
+
e.localOffset += shift;
|
|
575
|
+
e.encryption = ZipArchive.encryption(e, byId.get(0x9901));
|
|
576
|
+
e.method = methodOf(e.methodCode);
|
|
577
|
+
if (e.kind === 'directory' && e.size > 0)
|
|
578
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.NonFileData, data: { name, bytes: e.size } });
|
|
579
|
+
if (e.methodCode === 0 && !e.encryption && e.size !== e.compressedSize)
|
|
580
|
+
e.broken ??= 'a stored entry has two different sizes';
|
|
581
|
+
return e;
|
|
582
|
+
}
|
|
583
|
+
/** The encryption an entry uses, from its flags and its AES field, which also gives the method inside. */
|
|
584
|
+
static encryption(e, aes) {
|
|
585
|
+
if (e.flags & 0x2000)
|
|
586
|
+
return { kind: 'unsupported', reason: 'an encrypted central directory' };
|
|
587
|
+
if (!(e.flags & 0x0001))
|
|
588
|
+
return undefined;
|
|
589
|
+
if (e.flags & 0x0040)
|
|
590
|
+
return { kind: 'unsupported', reason: 'PKWARE strong encryption' };
|
|
591
|
+
if (e.storedMethod !== 99)
|
|
592
|
+
return { kind: 'zipcrypto' };
|
|
593
|
+
if (!aes || aes.length < 7)
|
|
594
|
+
return { kind: 'unsupported', reason: 'an AES entry without its AES field' };
|
|
595
|
+
const b = new Bytes(aes);
|
|
596
|
+
const version = b.u16(0);
|
|
597
|
+
const bits = (0, zipcrypt_1.aesBits)(aes[4]);
|
|
598
|
+
if ((version !== 1 && version !== 2) || aes[2] !== 0x41 || aes[3] !== 0x45 || bits === undefined)
|
|
599
|
+
return { kind: 'unsupported', reason: 'an AES field of an unknown version' };
|
|
600
|
+
e.aesVersion = version;
|
|
601
|
+
e.aesStrength = aes[4];
|
|
602
|
+
e.methodCode = b.u16(5);
|
|
603
|
+
return { kind: 'aes', bits };
|
|
604
|
+
}
|
|
605
|
+
/** Checks a local header and any data descriptor against the central directory, and fills in where the entry's data and the entry end. */
|
|
606
|
+
static async checkLocal(reader, e, dirStart) {
|
|
607
|
+
const mismatch = (what) => e.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HeaderMismatch, data: { name: e.name, what } });
|
|
608
|
+
if (e.localOffset < 0 || e.localOffset + 30 > reader.size) {
|
|
609
|
+
e.broken = 'its local header lies outside the file';
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const h = new Bytes(await reader.read(e.localOffset, 30));
|
|
613
|
+
if (h.u32(0) !== SIG_LOCAL) {
|
|
614
|
+
e.broken = 'no local header where the central directory points';
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const flags = h.u16(6);
|
|
618
|
+
const nameLength = h.u16(26);
|
|
619
|
+
const extraLength = h.u16(28);
|
|
620
|
+
e.dataOffset = e.localOffset + 30 + nameLength + extraLength;
|
|
621
|
+
if (e.dataOffset + e.compressedSize > reader.size) {
|
|
622
|
+
e.broken = 'its data runs past the end of the file';
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const rest = await reader.read(e.localOffset + 30, nameLength + extraLength);
|
|
626
|
+
if (Buffer.compare(rest.subarray(0, nameLength), e.rawName) !== 0)
|
|
627
|
+
mismatch('the local header names another file');
|
|
628
|
+
if ((flags & FLAG_MASK) !== (e.flags & FLAG_MASK))
|
|
629
|
+
mismatch('the local header has other flags');
|
|
630
|
+
if (h.u16(8) !== e.storedMethod)
|
|
631
|
+
mismatch('the local header gives another compression method');
|
|
632
|
+
const parsed = parseExtra(rest.subarray(nameLength));
|
|
633
|
+
if (parsed.malformed)
|
|
634
|
+
e.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.ExtraField, data: { field: 'a malformed local extra field', reason: parsed.malformed } });
|
|
635
|
+
const local = new Map(parsed.records.map(r => [r.id, r.data]));
|
|
636
|
+
for (const r of parsed.records) {
|
|
637
|
+
if (!STRUCTURAL_EXTRAS.has(r.id) && !zip_writer_1.METADATA_EXTRAS.has(r.id) && !e.extras.some(x => x.id === r.id)) {
|
|
638
|
+
e.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.ExtraField, data: { field: `local extra field ${hex(r.id)}` } });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const lp = unicodeField(local.get(0x7075), e.rawName);
|
|
642
|
+
if (lp.text !== undefined && lp.text !== e.name)
|
|
643
|
+
mismatch("the local header's Unicode path names another file");
|
|
644
|
+
let size = h.u32(22);
|
|
645
|
+
let compressedSize = h.u32(18);
|
|
646
|
+
const z = local.get(0x0001);
|
|
647
|
+
if (z && z.length >= 16) {
|
|
648
|
+
const b = new Bytes(z);
|
|
649
|
+
// A writer that streams the entry does not know its sizes yet. It sets the data descriptor flag and may fill
|
|
650
|
+
// the ZIP64 sizes with ones, which then mean what zero means: the data descriptor has them.
|
|
651
|
+
const wide = (at) => (e.flags & 0x0008 && b.big(at) === UNKNOWN64 ? 0 : b.u64(at));
|
|
652
|
+
const ws = wide(0);
|
|
653
|
+
const wc = wide(8);
|
|
654
|
+
if ((size !== MAX32 && size !== 0 && size !== ws) || (compressedSize !== MAX32 && compressedSize !== 0 && compressedSize !== wc))
|
|
655
|
+
mismatch('the local header and its ZIP64 field give other sizes');
|
|
656
|
+
size = ws;
|
|
657
|
+
compressedSize = wc;
|
|
658
|
+
}
|
|
659
|
+
else if (z)
|
|
660
|
+
mismatch('the local ZIP64 field is too short');
|
|
661
|
+
const crc = h.u32(14);
|
|
662
|
+
if (e.flags & 0x0008) {
|
|
663
|
+
// With a data descriptor the local fields may be zero. Any that are set must agree.
|
|
664
|
+
if (crc !== 0 && crc !== e.crc)
|
|
665
|
+
mismatch('the local header gives another CRC');
|
|
666
|
+
if (size !== 0 && size !== e.size)
|
|
667
|
+
mismatch('the local header gives another size');
|
|
668
|
+
if (compressedSize !== 0 && compressedSize !== e.compressedSize)
|
|
669
|
+
mismatch('the local header gives another compressed size');
|
|
670
|
+
}
|
|
671
|
+
else if (crc !== e.crc || size !== e.size || compressedSize !== e.compressedSize) {
|
|
672
|
+
mismatch('the local header gives another CRC or size');
|
|
673
|
+
}
|
|
674
|
+
const dataEnd = e.dataOffset + e.compressedSize;
|
|
675
|
+
e.end = dataEnd;
|
|
676
|
+
if (!(e.flags & 0x0008))
|
|
677
|
+
return;
|
|
678
|
+
const d = new Bytes(await reader.read(dataEnd, Math.max(0, Math.min(24, reader.size - dataEnd))));
|
|
679
|
+
let p = 0;
|
|
680
|
+
if (d.b.length >= 8 && d.u32(0) === SIG_DESCRIPTOR && d.u32(4) === e.crc)
|
|
681
|
+
p = 4;
|
|
682
|
+
if (d.b.length < p + 12 || d.u32(p) !== e.crc) {
|
|
683
|
+
mismatch('the data descriptor gives another CRC');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// The format says eight-byte sizes go with a ZIP64 field. Either width is accepted when its values agree; the
|
|
687
|
+
// coverage check then sees any bytes the other reading would leave over. Past a narrow descriptor the wide reading
|
|
688
|
+
// takes in the next header's signature, so it compares the bytes without demanding that they fit a number.
|
|
689
|
+
const wide = d.b.length >= p + 20 && d.big(p + 4) === BigInt(e.compressedSize) && d.big(p + 12) === BigInt(e.size);
|
|
690
|
+
const narrow = d.u32(p + 4) === e.compressedSize && d.u32(p + 8) === e.size;
|
|
691
|
+
if (z ? wide : narrow)
|
|
692
|
+
e.end = dataEnd + p + (z ? 20 : 12);
|
|
693
|
+
else if (z ? narrow : wide)
|
|
694
|
+
e.end = dataEnd + p + (z ? 12 : 20);
|
|
695
|
+
else
|
|
696
|
+
mismatch('the data descriptor gives other sizes');
|
|
697
|
+
if (e.end > dirStart && dataEnd <= dirStart)
|
|
698
|
+
e.end = dirStart;
|
|
699
|
+
}
|
|
700
|
+
/** Bytes before the first entry, bytes between entries and the directory that belong to none, and entries that share bytes. */
|
|
701
|
+
static async coverage(reader, entries, dirStart, deadline) {
|
|
702
|
+
const placed = entries.filter(e => !e.broken).sort((a, b) => a.localOffset - b.localOffset || a.index - b.index);
|
|
703
|
+
const gaps = [];
|
|
704
|
+
let covered = -1;
|
|
705
|
+
let leading = 0;
|
|
706
|
+
let overlap = 0;
|
|
707
|
+
for (const e of placed) {
|
|
708
|
+
if (covered < 0) {
|
|
709
|
+
leading = e.localOffset;
|
|
710
|
+
covered = e.localOffset;
|
|
711
|
+
}
|
|
712
|
+
if (e.localOffset < covered)
|
|
713
|
+
overlap++;
|
|
714
|
+
else if (e.localOffset > covered)
|
|
715
|
+
gaps.push([covered, e.localOffset]);
|
|
716
|
+
covered = Math.max(covered, e.end);
|
|
717
|
+
}
|
|
718
|
+
if (covered < 0) {
|
|
719
|
+
leading = dirStart;
|
|
720
|
+
covered = dirStart;
|
|
721
|
+
}
|
|
722
|
+
if (covered > dirStart)
|
|
723
|
+
overlap++;
|
|
724
|
+
else if (covered < dirStart)
|
|
725
|
+
gaps.push([covered, dirStart]);
|
|
726
|
+
let hidden = 0;
|
|
727
|
+
let localHeaders = 0;
|
|
728
|
+
let scanned = 0;
|
|
729
|
+
for (const [from, to] of gaps) {
|
|
730
|
+
hidden += to - from;
|
|
731
|
+
// A local header in hidden data is an entry a streaming reader extracts and a directory reader never sees.
|
|
732
|
+
for (let at = from; at < to && scanned < HIDDEN_SCAN; at += 65536) {
|
|
733
|
+
(0, errors_1.checkTime)(deadline);
|
|
734
|
+
const b = await reader.read(at, Math.min(65536 + 3, to - at));
|
|
735
|
+
scanned += b.length;
|
|
736
|
+
for (let i = 0; i + 4 <= b.length && i < 65536; i++)
|
|
737
|
+
if (b[i] === 0x50 && b[i + 1] === 0x4b && b[i + 2] === 3 && b[i + 3] === 4)
|
|
738
|
+
localHeaders++;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return { leading, hidden, localHeaders, overlap };
|
|
742
|
+
}
|
|
743
|
+
hasMetadata() {
|
|
744
|
+
return this.commentBytes.length > 0 || this.entries.some(e => e.commentBytes.length > 0 || e.dosTime !== 0 || e.dosDate !== 0x21 || e.extras.some(r => zip_writer_1.METADATA_EXTRAS.has(r.id)));
|
|
745
|
+
}
|
|
746
|
+
/** Reads a symbolic link's target, which a ZIP stores as the entry's content. */
|
|
747
|
+
async linkTarget(e, meter) {
|
|
748
|
+
if (e.size > MAX_LINK || e.encryption || !e.method?.readable || e.method.unavailable)
|
|
749
|
+
return undefined;
|
|
750
|
+
const parts = [];
|
|
751
|
+
for await (const c of this.read(e, meter))
|
|
752
|
+
parts.push(c);
|
|
753
|
+
return Buffer.concat(parts).toString('utf8');
|
|
754
|
+
}
|
|
755
|
+
/** The stored bytes of an entry. */
|
|
756
|
+
raw(e) {
|
|
757
|
+
return this.reader.chunks(e.dataOffset, e.compressedSize);
|
|
758
|
+
}
|
|
759
|
+
/** The bytes before the archive and after it. */
|
|
760
|
+
leading() {
|
|
761
|
+
return this.reader.chunks(0, this.start);
|
|
762
|
+
}
|
|
763
|
+
trailing() {
|
|
764
|
+
return this.reader.chunks(this.end, this.reader.size - this.end);
|
|
765
|
+
}
|
|
766
|
+
/** The 12-byte ZipCrypto header's check byte: the CRC's high byte, or the time's with a data descriptor. */
|
|
767
|
+
checkByte(e) {
|
|
768
|
+
return e.flags & 0x0008 ? (e.dosTime >>> 8) & 0xff : e.crc >>> 24;
|
|
769
|
+
}
|
|
770
|
+
async unlock(e, password) {
|
|
771
|
+
const enc = e.encryption;
|
|
772
|
+
if (!enc || enc.kind === 'unsupported')
|
|
773
|
+
return false;
|
|
774
|
+
if (enc.kind === 'zipcrypto') {
|
|
775
|
+
if (e.compressedSize < 12)
|
|
776
|
+
return false;
|
|
777
|
+
const header = await this.reader.read(e.dataOffset, 12);
|
|
778
|
+
return (0, zipcrypt_1.passwordBytes)(password).some(p => (0, zipcrypt_1.zipCryptoOpens)(header, p, this.checkByte(e)));
|
|
779
|
+
}
|
|
780
|
+
const salt = zipcrypt_1.AES_SALT[e.aesStrength ?? 0];
|
|
781
|
+
if (e.compressedSize < salt + 12)
|
|
782
|
+
return false;
|
|
783
|
+
const head = await this.reader.read(e.dataOffset, salt + 2);
|
|
784
|
+
for (const p of (0, zipcrypt_1.passwordBytes)(password)) {
|
|
785
|
+
const keys = (0, zipcrypt_1.aesKeys)(p, head.subarray(0, salt), enc.bits);
|
|
786
|
+
if (Buffer.compare(keys.verifier, head.subarray(salt, salt + 2)) !== 0)
|
|
787
|
+
continue;
|
|
788
|
+
const mac = await this.reader.read(e.dataOffset + e.compressedSize - 10, 10);
|
|
789
|
+
if (await (0, zipcrypt_1.aesAuthentic)(this.reader.chunks(e.dataOffset + salt + 2, e.compressedSize - salt - 12), keys.hmacKey, mac))
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
/** The entry's compressed bytes without their encryption, for a rewrite that copies them. Their length comes from `plainLength`. */
|
|
795
|
+
async *plain(e, password) {
|
|
796
|
+
const enc = e.encryption;
|
|
797
|
+
if (!enc) {
|
|
798
|
+
yield* this.raw(e);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (enc.kind === 'unsupported' || password === undefined)
|
|
802
|
+
throw new errors_1.DataError('The entry is encrypted');
|
|
803
|
+
if (enc.kind === 'zipcrypto') {
|
|
804
|
+
const header = await this.reader.read(e.dataOffset, 12);
|
|
805
|
+
const p = (0, zipcrypt_1.passwordBytes)(password).find(b => (0, zipcrypt_1.zipCryptoOpens)(header, b, this.checkByte(e)));
|
|
806
|
+
if (!p)
|
|
807
|
+
throw new errors_1.DataError('The password does not open the entry');
|
|
808
|
+
yield* (0, zipcrypt_1.zipCryptoDecrypt)(this.raw(e), p);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const salt = zipcrypt_1.AES_SALT[e.aesStrength ?? 0];
|
|
812
|
+
const head = await this.reader.read(e.dataOffset, salt + 2);
|
|
813
|
+
const p = (0, zipcrypt_1.passwordBytes)(password).find(b => Buffer.compare((0, zipcrypt_1.aesKeys)(b, head.subarray(0, salt), enc.bits).verifier, head.subarray(salt)) === 0);
|
|
814
|
+
if (!p)
|
|
815
|
+
throw new errors_1.DataError('The password does not open the entry');
|
|
816
|
+
const mac = await this.reader.read(e.dataOffset + e.compressedSize - 10, 10);
|
|
817
|
+
yield* (0, zipcrypt_1.aesDecrypt)(this.reader.chunks(e.dataOffset + salt + 2, e.compressedSize - salt - 12), (0, zipcrypt_1.aesKeys)(p, head.subarray(0, salt), enc.bits), mac);
|
|
818
|
+
}
|
|
819
|
+
/** The length of what plain() yields. */
|
|
820
|
+
plainLength(e) {
|
|
821
|
+
if (!e.encryption)
|
|
822
|
+
return e.compressedSize;
|
|
823
|
+
if (e.encryption.kind === 'zipcrypto')
|
|
824
|
+
return e.compressedSize - 12;
|
|
825
|
+
return e.compressedSize - zipcrypt_1.AES_SALT[e.aesStrength ?? 0] - 12;
|
|
826
|
+
}
|
|
827
|
+
/** The entry's content in chunks. Its size, its CRC unless AE-2 leaves it out, and the compressed bytes used must all agree with the central directory. */
|
|
828
|
+
async *read(e, meter, password) {
|
|
829
|
+
if (e.broken)
|
|
830
|
+
throw new errors_1.DataError(e.broken);
|
|
831
|
+
const compressed = this.plainLength(e);
|
|
832
|
+
const consumed = { bytes: 0 };
|
|
833
|
+
async function* timed(chunks) {
|
|
834
|
+
for await (const c of chunks) {
|
|
835
|
+
meter.time();
|
|
836
|
+
yield c;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
const input = timed(this.plain(e, password));
|
|
840
|
+
let out;
|
|
841
|
+
if (e.methodCode === 0) {
|
|
842
|
+
out = input;
|
|
843
|
+
}
|
|
844
|
+
else if (e.methodCode === 8)
|
|
845
|
+
out = (0, codecs_1.inflateRaw)(input, consumed);
|
|
846
|
+
else if (e.methodCode === 9)
|
|
847
|
+
out = (0, inflate64_1.inflate64)(input, consumed);
|
|
848
|
+
else if (e.methodCode === 14)
|
|
849
|
+
out = (0, zip_lzma_1.zipLzma)(input, e.size, consumed, this.decoders?.limits.dictionaryBytes ?? DEFAULT_DICTIONARY);
|
|
850
|
+
else if (e.methodCode === 95) {
|
|
851
|
+
if (!this.decoders)
|
|
852
|
+
throw new errors_1.DataError('an XZ entry needs a temporary folder, which this run has none of');
|
|
853
|
+
out = (0, zip_lzma_1.zipXz)(input, consumed, meter, this.decoders.temp, this.decoders.threshold, this.decoders.limits);
|
|
854
|
+
}
|
|
855
|
+
else if (e.methodCode === 12) {
|
|
856
|
+
const info = { streams: [], trailing: 0, trailingZeros: true };
|
|
857
|
+
out = (0, bzip2_1.bunzip2)(input, info, false, consumed);
|
|
858
|
+
}
|
|
859
|
+
else if (e.methodCode === 93) {
|
|
860
|
+
const make = codecs_1.zstdDecompressor;
|
|
861
|
+
if (!make)
|
|
862
|
+
throw new errors_1.DataError('This Node has no zstd');
|
|
863
|
+
out = (0, codecs_1.through)(make(), input, consumed);
|
|
864
|
+
}
|
|
865
|
+
else
|
|
866
|
+
throw new errors_1.DataError(`compression method ${e.methodCode}`);
|
|
867
|
+
let crc = 0;
|
|
868
|
+
let total = 0;
|
|
869
|
+
for await (const c of out) {
|
|
870
|
+
total += c.length;
|
|
871
|
+
if (total > e.size)
|
|
872
|
+
throw new errors_1.DataError('it decodes past its declared size');
|
|
873
|
+
meter.add(total, e.methodCode === 0 ? total : consumed.bytes);
|
|
874
|
+
crc = (0, crc32_1.crc32)(c, crc);
|
|
875
|
+
yield c;
|
|
876
|
+
}
|
|
877
|
+
if (e.methodCode !== 0 && consumed.bytes !== compressed)
|
|
878
|
+
throw new errors_1.DataError('its compressed data ends before its compressed size', consumed.bytes < compressed);
|
|
879
|
+
if (total !== e.size)
|
|
880
|
+
throw new errors_1.DataError(`it decodes to ${total} bytes, not ${e.size}`, true);
|
|
881
|
+
// AE-2 leaves the CRC out, since the authentication code covers the data.
|
|
882
|
+
if (crc !== e.crc && e.aesVersion !== 2)
|
|
883
|
+
throw new errors_1.DataError('it does not match its CRC');
|
|
884
|
+
}
|
|
885
|
+
write(plan, sink, context) {
|
|
886
|
+
return (0, zip_writer_1.writeZip)(this, plan, sink, context);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
exports.ZipArchive = ZipArchive;
|
|
890
|
+
/** True when the file ends in what may be a ZIP archive: an end record signature in the last 64 KiB. */
|
|
891
|
+
async function hasZipEnd(source) {
|
|
892
|
+
const size = await source.size();
|
|
893
|
+
if (size < 22)
|
|
894
|
+
return false;
|
|
895
|
+
const start = Math.max(0, size - END_SEARCH);
|
|
896
|
+
const tail = await source.read(start, size - start);
|
|
897
|
+
for (let p = tail.length - 22; p >= 0; p--)
|
|
898
|
+
if (tail[p] === 0x50 && tail[p + 1] === 0x4b && tail[p + 2] === 5 && tail[p + 3] === 6)
|
|
899
|
+
return true;
|
|
900
|
+
return false;
|
|
901
|
+
}
|