@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/seven.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SevenReader = void 0;
|
|
4
|
+
exports.unixMode = unixMode;
|
|
5
|
+
exports.openSevenZip = openSevenZip;
|
|
6
|
+
const aes7z_1 = require("./aes7z");
|
|
7
|
+
const coders_1 = require("./coders");
|
|
8
|
+
const crc32_1 = require("./crc32");
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
const sevenzip_1 = require("./sevenzip");
|
|
11
|
+
const stream_meter_1 = require("./stream-meter");
|
|
12
|
+
const types_1 = require("./types");
|
|
13
|
+
const S_IFMT = 0xf000;
|
|
14
|
+
const UNIX_EXTENSION = 0x8000;
|
|
15
|
+
const FILE_ATTRIBUTE_DIRECTORY = 0x10;
|
|
16
|
+
const FILE_ATTRIBUTE_REPARSE_POINT = 0x400;
|
|
17
|
+
/** The Unix mode stored in the high 16 bits of the attributes, when the low bits say one is there. */
|
|
18
|
+
function unixMode(attrib) {
|
|
19
|
+
return attrib !== undefined && attrib & UNIX_EXTENSION ? attrib >>> 16 : undefined;
|
|
20
|
+
}
|
|
21
|
+
function kindOf(f) {
|
|
22
|
+
const mode = unixMode(f.attrib);
|
|
23
|
+
if (mode !== undefined) {
|
|
24
|
+
switch (mode & S_IFMT) {
|
|
25
|
+
case 0xa000:
|
|
26
|
+
return 'symlink';
|
|
27
|
+
case 0x2000:
|
|
28
|
+
case 0x6000:
|
|
29
|
+
return 'device';
|
|
30
|
+
case 0x1000:
|
|
31
|
+
return 'fifo';
|
|
32
|
+
case 0xc000:
|
|
33
|
+
return 'socket';
|
|
34
|
+
case 0x4000:
|
|
35
|
+
return 'dir';
|
|
36
|
+
default:
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (f.attrib !== undefined && f.attrib & FILE_ATTRIBUTE_REPARSE_POINT)
|
|
41
|
+
return 'symlink';
|
|
42
|
+
if (!f.hasStream && !f.emptyFile && !f.anti)
|
|
43
|
+
return 'dir';
|
|
44
|
+
if (f.attrib !== undefined && f.attrib & FILE_ATTRIBUTE_DIRECTORY && !f.hasStream)
|
|
45
|
+
return 'dir';
|
|
46
|
+
return 'file';
|
|
47
|
+
}
|
|
48
|
+
const keyCache = new WeakMap();
|
|
49
|
+
function folderEnv(env, meter, password) {
|
|
50
|
+
let cache = keyCache.get(env);
|
|
51
|
+
if (!cache) {
|
|
52
|
+
cache = new Map();
|
|
53
|
+
keyCache.set(env, cache);
|
|
54
|
+
}
|
|
55
|
+
const keys = cache;
|
|
56
|
+
return {
|
|
57
|
+
source: env.source,
|
|
58
|
+
deadline: env.deadline,
|
|
59
|
+
onPacked: n => meter.packed(n),
|
|
60
|
+
key(props) {
|
|
61
|
+
if (password === undefined)
|
|
62
|
+
throw new errors_1.DataError('no password to try');
|
|
63
|
+
const id = `${password}\0${Buffer.from(props.salt).toString('hex')}\0${props.numCyclesPower}`;
|
|
64
|
+
let k = keys.get(id);
|
|
65
|
+
if (!k) {
|
|
66
|
+
k = (0, aes7z_1.deriveKey)(password, props, env.deadline);
|
|
67
|
+
keys.set(id, k);
|
|
68
|
+
}
|
|
69
|
+
return k;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Collects a stream into memory, up to `max` bytes. */
|
|
74
|
+
async function collect(chunks, max) {
|
|
75
|
+
const parts = [];
|
|
76
|
+
let n = 0;
|
|
77
|
+
for await (const c of chunks) {
|
|
78
|
+
n += c.length;
|
|
79
|
+
if (n > max)
|
|
80
|
+
throw new errors_1.DataError('the stream decodes to more than its declared size');
|
|
81
|
+
parts.push(c);
|
|
82
|
+
}
|
|
83
|
+
return Buffer.concat(parts);
|
|
84
|
+
}
|
|
85
|
+
const isEncrypted = (f) => f.coders.some(coders_1.isAes);
|
|
86
|
+
/** Turns a folder check's limit into the stop it causes. */
|
|
87
|
+
function folderLimits(env, folder, location) {
|
|
88
|
+
const over = (0, coders_1.checkFolder)(folder, { decompressedBytes: env.limits.totalDecompressedBytes, dictionaryBytes: env.limits.dictionaryBytes });
|
|
89
|
+
if (!over)
|
|
90
|
+
return;
|
|
91
|
+
if (over.limit === 'dictionary')
|
|
92
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DictionarySize, 'Dictionary size limit exceeded', { limit: env.limits.dictionaryBytes, dictionary: over.value }, location);
|
|
93
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.TotalDecompressedSize, 'Total decompressed size limit exceeded', { limit: env.limits.totalDecompressedBytes, declared: over.value }, location);
|
|
94
|
+
}
|
|
95
|
+
/** Decodes an encoded header, trying each password on an encrypted one. */
|
|
96
|
+
async function decodeHeaderStreams(env, info, base) {
|
|
97
|
+
if (info.folders.length !== 1)
|
|
98
|
+
throw new errors_1.ParseError('an encoded header has other than one folder');
|
|
99
|
+
const folder = info.folders[0];
|
|
100
|
+
folderLimits(env, folder, 'header');
|
|
101
|
+
const size = folder.unpackSizes[folder.mainOut];
|
|
102
|
+
if (size > env.limits.decompressedBytes)
|
|
103
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DecompressedSize, 'Decompressed size limit exceeded', { limit: env.limits.decompressedBytes, declared: size }, 'header');
|
|
104
|
+
const offsets = [];
|
|
105
|
+
let at = base + info.packPos;
|
|
106
|
+
for (const s of info.packSizes) {
|
|
107
|
+
offsets.push(at);
|
|
108
|
+
at += s;
|
|
109
|
+
}
|
|
110
|
+
if (at > env.size)
|
|
111
|
+
throw new errors_1.DataError('the encoded header lies past the end of the file', true);
|
|
112
|
+
const spans = offsets.map((o, i) => ({ start: o, end: o + info.packSizes[i], what: 'encoded header' }));
|
|
113
|
+
const encrypted = isEncrypted(folder);
|
|
114
|
+
const tries = encrypted ? env.passwords : [undefined];
|
|
115
|
+
let lastError;
|
|
116
|
+
for (const password of tries) {
|
|
117
|
+
const meter = new stream_meter_1.Meter(env.limits, env.totals, env.deadline, true, 'header');
|
|
118
|
+
try {
|
|
119
|
+
const chunks = (0, coders_1.decodeFolder)(folder, offsets, info.packSizes, folderEnv(env, meter, password));
|
|
120
|
+
const bytes = await collect(meterChunks(chunks, meter), size);
|
|
121
|
+
if (folder.crc !== undefined && (0, crc32_1.crc32)(bytes) !== folder.crc)
|
|
122
|
+
throw new errors_1.CheckError('the encoded header CRC32 does not match', 'encoded header CRC32');
|
|
123
|
+
if (bytes[0] !== sevenzip_1.Nid.Header)
|
|
124
|
+
throw new errors_1.ParseError('the decoded header does not start with kHeader');
|
|
125
|
+
return { bytes, encrypted, spans };
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
if (!encrypted || e instanceof errors_1.StopError || e instanceof errors_1.UnsupportedError || (0, errors_1.stopsTree)(e))
|
|
129
|
+
throw e;
|
|
130
|
+
lastError = e;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (encrypted)
|
|
134
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Encrypted, types_1.ArchiveDetail.PasswordRequired, 'No password opens the header', undefined, 'header');
|
|
135
|
+
throw lastError;
|
|
136
|
+
}
|
|
137
|
+
async function* meterChunks(chunks, meter) {
|
|
138
|
+
for await (const c of chunks) {
|
|
139
|
+
meter.add(c.length);
|
|
140
|
+
yield c;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reads a 7z archive's signature and headers, decoding an encoded header, and checks that its structures fit the file
|
|
145
|
+
* and each other. `offset` is where the signature was found.
|
|
146
|
+
*/
|
|
147
|
+
async function openSevenZip(env, offset) {
|
|
148
|
+
const notes = [];
|
|
149
|
+
const sig = await env.reader.read(offset, 32);
|
|
150
|
+
if (sig.length < 32)
|
|
151
|
+
throw new errors_1.DataError('the file ends in its signature header', true);
|
|
152
|
+
if (sig[6] !== 0)
|
|
153
|
+
throw new errors_1.UnsupportedError(`7z format version ${sig[6]}.${sig[7]}`, false, { reason: `format version ${sig[6]}.${sig[7]}` });
|
|
154
|
+
const storedStart = new DataView(sig.buffer, sig.byteOffset, 32).getUint32(8, true);
|
|
155
|
+
const base = offset + 32;
|
|
156
|
+
if (!sig.subarray(8, 32).every(b => b === 0)) {
|
|
157
|
+
if ((0, crc32_1.crc32)(sig.subarray(12, 32)) !== storedStart)
|
|
158
|
+
throw new errors_1.CheckError('the start header CRC32 does not match', 'start header CRC32');
|
|
159
|
+
return openWith(env, offset, (0, sevenzip_1.parseStartHeader)(sig), notes);
|
|
160
|
+
}
|
|
161
|
+
// An empty start header: each place a header may start near the end is tried, the nearest to the end first.
|
|
162
|
+
// A place that is not the header can claim any count or size, so a limit fails that place, not the file. When no
|
|
163
|
+
// place works, the failure of the one nearest the end is reported.
|
|
164
|
+
let failure;
|
|
165
|
+
for (const start of await headerCandidates(env, base)) {
|
|
166
|
+
try {
|
|
167
|
+
const found = await openWith(env, offset, start, [{ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.EmptyStartHeader }]);
|
|
168
|
+
// An empty header parses anywhere, so a found one must hold data and files, as Commons Compress requires.
|
|
169
|
+
if (found.streams && found.streams.packSizes.length > 0 && found.entries.length > 0)
|
|
170
|
+
return found;
|
|
171
|
+
failure ??= new errors_1.ParseError('the header found at the end of the file holds no data');
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (!(e instanceof errors_1.DataError || e instanceof errors_1.UnsupportedError || e instanceof errors_1.StopError))
|
|
175
|
+
throw e;
|
|
176
|
+
failure ??= e;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw failure ?? new errors_1.CheckError('the start header is empty and no header is found at the end of the file', 'start header CRC32');
|
|
180
|
+
}
|
|
181
|
+
/** Reads the headers an intact or recovered start header points to. */
|
|
182
|
+
async function openWith(env, offset, start, notes) {
|
|
183
|
+
const base = offset + 32;
|
|
184
|
+
const spans = [{ start: offset, end: base, what: 'signature header' }];
|
|
185
|
+
const archive = { offset, entries: [], notes, headerEncrypted: false, packOffsets: [], folderPack: [] };
|
|
186
|
+
if (start.nextHeaderSize === 0) {
|
|
187
|
+
finishSpans(env, archive, spans, base);
|
|
188
|
+
return archive;
|
|
189
|
+
}
|
|
190
|
+
const headerStart = base + start.nextHeaderOffset;
|
|
191
|
+
const headerEnd = headerStart + start.nextHeaderSize;
|
|
192
|
+
if (headerEnd > env.size)
|
|
193
|
+
throw new errors_1.DataError('the header lies past the end of the file', true);
|
|
194
|
+
if (start.nextHeaderSize > env.limits.decompressedBytes) {
|
|
195
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DecompressedSize, 'Decompressed size limit exceeded', { limit: env.limits.decompressedBytes, declared: start.nextHeaderSize }, 'header');
|
|
196
|
+
}
|
|
197
|
+
spans.push({ start: headerStart, end: headerEnd, what: 'header' });
|
|
198
|
+
let headerBytes = await env.reader.read(headerStart, start.nextHeaderSize);
|
|
199
|
+
if ((0, crc32_1.crc32)(headerBytes) !== start.nextHeaderCrc)
|
|
200
|
+
throw new errors_1.CheckError('the next header CRC32 does not match', 'next header CRC32');
|
|
201
|
+
if (headerBytes[0] === sevenzip_1.Nid.EncodedHeader) {
|
|
202
|
+
const decoded = await decodeHeaderStreams(env, (0, sevenzip_1.parseEncodedHeader)(headerBytes), base);
|
|
203
|
+
headerBytes = decoded.bytes;
|
|
204
|
+
archive.headerEncrypted = decoded.encrypted;
|
|
205
|
+
spans.push(...decoded.spans);
|
|
206
|
+
}
|
|
207
|
+
const header = (0, sevenzip_1.parseHeader)(headerBytes, env.limits.entries, n => {
|
|
208
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.EntryCount, 'Entry count limit exceeded', { limit: env.limits.entries, entries: n });
|
|
209
|
+
});
|
|
210
|
+
for (const u of header.unknown)
|
|
211
|
+
notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.UnknownProperty, data: { property: u } });
|
|
212
|
+
const streams = header.streams;
|
|
213
|
+
archive.streams = streams;
|
|
214
|
+
// Packed streams, and which folder starts where.
|
|
215
|
+
if (streams) {
|
|
216
|
+
let at = base + streams.packPos;
|
|
217
|
+
for (const s of streams.packSizes) {
|
|
218
|
+
archive.packOffsets.push(at);
|
|
219
|
+
spans.push({ start: at, end: at + s, what: 'packed stream' });
|
|
220
|
+
at += s;
|
|
221
|
+
}
|
|
222
|
+
if (at > env.size)
|
|
223
|
+
throw new errors_1.DataError('packed streams lie past the end of the file', true);
|
|
224
|
+
let next = 0;
|
|
225
|
+
for (const f of streams.folders) {
|
|
226
|
+
archive.folderPack.push(next);
|
|
227
|
+
next += f.packedStreams.length;
|
|
228
|
+
}
|
|
229
|
+
if (next > streams.packSizes.length)
|
|
230
|
+
throw new errors_1.ParseError('the folders read more packed streams than the archive has');
|
|
231
|
+
}
|
|
232
|
+
// Entries and the substreams that hold their data.
|
|
233
|
+
const subCount = streams ? streams.subSizes.length : 0;
|
|
234
|
+
const withData = header.files.filter(f => f.hasStream).length;
|
|
235
|
+
if (withData !== subCount)
|
|
236
|
+
throw new errors_1.ParseError('the number of files with data does not match the streams that hold it');
|
|
237
|
+
let sub = 0;
|
|
238
|
+
let folder = 0;
|
|
239
|
+
let inFolder = 0;
|
|
240
|
+
let folderAt = 0;
|
|
241
|
+
for (const [index, info] of header.files.entries()) {
|
|
242
|
+
const entry = { index, info, kind: kindOf(info), size: 0, start: 0 };
|
|
243
|
+
if (info.hasStream && streams) {
|
|
244
|
+
while (inFolder >= streams.numUnpackStreams[folder]) {
|
|
245
|
+
folder++;
|
|
246
|
+
inFolder = 0;
|
|
247
|
+
folderAt = 0;
|
|
248
|
+
}
|
|
249
|
+
entry.folder = folder;
|
|
250
|
+
entry.size = streams.subSizes[sub];
|
|
251
|
+
entry.crc = streams.subCrcs[sub];
|
|
252
|
+
entry.start = folderAt;
|
|
253
|
+
folderAt += entry.size;
|
|
254
|
+
sub++;
|
|
255
|
+
inFolder++;
|
|
256
|
+
}
|
|
257
|
+
archive.entries.push(entry);
|
|
258
|
+
}
|
|
259
|
+
if (streams) {
|
|
260
|
+
streams.folders.forEach((f, i) => {
|
|
261
|
+
folderLimits(env, f, `folder ${i + 1}`);
|
|
262
|
+
});
|
|
263
|
+
const total = streams.folders.reduce((sum, f) => sum + f.unpackSizes[f.mainOut], 0);
|
|
264
|
+
if (total > env.limits.totalDecompressedBytes) {
|
|
265
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.TotalDecompressedSize, 'Total decompressed size limit exceeded', { limit: env.limits.totalDecompressedBytes, declared: total });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const e of archive.entries) {
|
|
269
|
+
if (e.size > env.limits.decompressedBytes) {
|
|
270
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DecompressedSize, 'Decompressed size limit exceeded', { limit: env.limits.decompressedBytes, declared: e.size }, e.info.name);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
finishSpans(env, archive, spans, base);
|
|
274
|
+
return archive;
|
|
275
|
+
}
|
|
276
|
+
/** How far from the end a header is looked for when the start header is empty, as Commons Compress looks. */
|
|
277
|
+
const RECOVERY_WINDOW = 1024 * 1024;
|
|
278
|
+
/** How many places in that window are tried at most. */
|
|
279
|
+
const RECOVERY_TRIES = 256;
|
|
280
|
+
/**
|
|
281
|
+
* Where the header of an archive whose start header is all zeros may start, as an interrupted write leaves it:
|
|
282
|
+
* each kEncodedHeader followed by kPackInfo, and each kHeader followed by kMainStreamsInfo, in the last 1 MiB, the
|
|
283
|
+
* nearest to the end first, each taken to run to the end of the file. 7-Zip's FORMAT_7Z_RECOVERY looks for these
|
|
284
|
+
* pairs, and Commons Compress looks as far back.
|
|
285
|
+
*/
|
|
286
|
+
async function headerCandidates(env, base) {
|
|
287
|
+
const n = Math.min(RECOVERY_WINDOW, env.size - base);
|
|
288
|
+
const tail = await env.reader.read(env.size - n, n);
|
|
289
|
+
const out = [];
|
|
290
|
+
for (let i = n - 1; i >= 0 && out.length < RECOVERY_TRIES; i--) {
|
|
291
|
+
if ((tail[i] === sevenzip_1.Nid.EncodedHeader && tail[i + 1] === sevenzip_1.Nid.PackInfo) || (tail[i] === sevenzip_1.Nid.Header && tail[i + 1] === sevenzip_1.Nid.MainStreamsInfo)) {
|
|
292
|
+
out.push({ minorVersion: 0, nextHeaderOffset: env.size - n + i - base, nextHeaderSize: n - i, nextHeaderCrc: (0, crc32_1.crc32)(tail.subarray(i)) });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
/** Notes overlapping structures, bytes between them and bytes after the last one. */
|
|
298
|
+
function finishSpans(env, archive, spans, base) {
|
|
299
|
+
const sorted = spans.filter(s => s.end > s.start).sort((a, b) => a.start - b.start || a.end - b.end);
|
|
300
|
+
let end = archive.offset;
|
|
301
|
+
let hidden = 0;
|
|
302
|
+
let overlap = false;
|
|
303
|
+
for (const s of sorted) {
|
|
304
|
+
if (s.start < end)
|
|
305
|
+
overlap = true;
|
|
306
|
+
else
|
|
307
|
+
hidden += s.start - end;
|
|
308
|
+
end = Math.max(end, s.end);
|
|
309
|
+
}
|
|
310
|
+
if (overlap)
|
|
311
|
+
archive.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.Overlap });
|
|
312
|
+
if (hidden > 0)
|
|
313
|
+
archive.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HiddenData, data: { bytes: hidden } });
|
|
314
|
+
end = Math.max(end, base);
|
|
315
|
+
if (end < env.size)
|
|
316
|
+
archive.trailingAt = end;
|
|
317
|
+
}
|
|
318
|
+
/** Reads a folder's output a stretch at a time, so each entry takes its own bytes. */
|
|
319
|
+
class FolderCursor {
|
|
320
|
+
chunks;
|
|
321
|
+
it;
|
|
322
|
+
buf = new Uint8Array(0);
|
|
323
|
+
pos = 0;
|
|
324
|
+
/** Bytes of the folder's output taken so far. */
|
|
325
|
+
taken = 0;
|
|
326
|
+
/** Why the folder stopped decoding, once it has. */
|
|
327
|
+
failed;
|
|
328
|
+
constructor(chunks) {
|
|
329
|
+
this.chunks = chunks;
|
|
330
|
+
}
|
|
331
|
+
async more() {
|
|
332
|
+
this.it ??= this.chunks[Symbol.asyncIterator]();
|
|
333
|
+
const r = await this.it.next();
|
|
334
|
+
if (r.done)
|
|
335
|
+
return false;
|
|
336
|
+
this.buf = r.value;
|
|
337
|
+
this.pos = 0;
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
/** The next `n` bytes. A failure stays with the cursor, and every later call throws it too. */
|
|
341
|
+
async *take(n) {
|
|
342
|
+
if (this.failed !== undefined)
|
|
343
|
+
throw this.failed;
|
|
344
|
+
let left = n;
|
|
345
|
+
try {
|
|
346
|
+
while (left > 0) {
|
|
347
|
+
if (this.pos >= this.buf.length && !(await this.more()))
|
|
348
|
+
throw new errors_1.DataError('the folder decodes to less than its entries need', true);
|
|
349
|
+
const k = Math.min(left, this.buf.length - this.pos);
|
|
350
|
+
const part = this.buf.subarray(this.pos, this.pos + k);
|
|
351
|
+
this.pos += k;
|
|
352
|
+
this.taken += k;
|
|
353
|
+
left -= k;
|
|
354
|
+
yield part;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
if (!(e instanceof errors_1.StopError) && !(0, errors_1.stopsTree)(e))
|
|
359
|
+
this.failed = e;
|
|
360
|
+
throw e;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async close() {
|
|
364
|
+
await this.it?.return?.();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* True when `password` opens an encrypted folder: the folder decodes under it up to the end of its first entry with a
|
|
369
|
+
* CRC, and that entry matches its CRC. A folder with no CRC to check opens when its first entry, or its first 64 KiB,
|
|
370
|
+
* decodes. The trial counts toward no total.
|
|
371
|
+
*/
|
|
372
|
+
async function opensFolder(env, archive, f, password) {
|
|
373
|
+
const streams = archive.streams;
|
|
374
|
+
if (!streams)
|
|
375
|
+
return false;
|
|
376
|
+
const folder = streams.folders[f];
|
|
377
|
+
const first = archive.entries.find(e => e.folder === f && e.size > 0 && e.crc !== undefined) ?? archive.entries.find(e => e.folder === f && e.size > 0);
|
|
378
|
+
const skip = first ? archive.entries.filter(e => e.folder === f && e.index < first.index).reduce((n, e) => n + e.size, 0) : 0;
|
|
379
|
+
const want = first ? skip + first.size : Math.min(folder.unpackSizes[folder.mainOut], 1 << 16);
|
|
380
|
+
const meter = new stream_meter_1.Meter(env.limits, { decoded: 0, compressed: 0 }, env.deadline, false, `folder ${f + 1}`);
|
|
381
|
+
const cursor = new FolderCursor(meterChunks((0, coders_1.decodeFolder)(folder, packOffsetsOf(archive, f), packSizesOf(archive, f), folderEnv(env, meter, password)), meter));
|
|
382
|
+
try {
|
|
383
|
+
let crc = 0;
|
|
384
|
+
let at = 0;
|
|
385
|
+
for await (const c of cursor.take(want)) {
|
|
386
|
+
if (at + c.length > skip)
|
|
387
|
+
crc = (0, crc32_1.crc32)(c.subarray(Math.max(0, skip - at)), crc);
|
|
388
|
+
at += c.length;
|
|
389
|
+
}
|
|
390
|
+
return !first || first.crc === undefined || crc === first.crc;
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
if (e instanceof errors_1.StopError || e instanceof errors_1.UnsupportedError || (0, errors_1.stopsTree)(e))
|
|
394
|
+
throw e;
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
await cursor.close();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function packOffsetsOf(archive, f) {
|
|
402
|
+
const n = archive.streams?.folders[f].packedStreams.length ?? 0;
|
|
403
|
+
return archive.packOffsets.slice(archive.folderPack[f], archive.folderPack[f] + n);
|
|
404
|
+
}
|
|
405
|
+
function packSizesOf(archive, f) {
|
|
406
|
+
const n = archive.streams?.folders[f].packedStreams.length ?? 0;
|
|
407
|
+
return archive.streams?.packSizes.slice(archive.folderPack[f], archive.folderPack[f] + n) ?? [];
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* The content of each entry, decoded folder by folder. Entries of one folder read in order share one decode; an entry
|
|
411
|
+
* before the last one read starts the folder again.
|
|
412
|
+
*/
|
|
413
|
+
class SevenReader {
|
|
414
|
+
env;
|
|
415
|
+
archive;
|
|
416
|
+
open;
|
|
417
|
+
/**
|
|
418
|
+
* The password that opened each encrypted folder. A folder opens with one password, the first the engine tries that
|
|
419
|
+
* opens it, so an entry that fails its CRC never sends the reader back to the start of the folder with another.
|
|
420
|
+
*/
|
|
421
|
+
chosen = new Map();
|
|
422
|
+
/** Passwords that did not open a folder, by folder and password. */
|
|
423
|
+
refused = new Set();
|
|
424
|
+
constructor(env, archive) {
|
|
425
|
+
this.env = env;
|
|
426
|
+
this.archive = archive;
|
|
427
|
+
}
|
|
428
|
+
/** True when the folder of `entry` is not encrypted, or `password` opens it. */
|
|
429
|
+
async unlock(entry, password) {
|
|
430
|
+
const f = entry.folder;
|
|
431
|
+
const folder = f === undefined ? undefined : this.archive.streams?.folders[f];
|
|
432
|
+
if (f === undefined || !folder || !isEncrypted(folder))
|
|
433
|
+
return true;
|
|
434
|
+
const chosen = this.chosen.get(f);
|
|
435
|
+
if (chosen !== undefined)
|
|
436
|
+
return chosen === password;
|
|
437
|
+
const key = `${f}\0${password}`;
|
|
438
|
+
if (this.refused.has(key))
|
|
439
|
+
return false;
|
|
440
|
+
if (await opensFolder(this.env, this.archive, f, password)) {
|
|
441
|
+
this.chosen.set(f, password);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
this.refused.add(key);
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
async folder(f, password) {
|
|
448
|
+
if (this.open?.index === f && this.open.password === password)
|
|
449
|
+
return this.open;
|
|
450
|
+
await this.open?.cursor.close();
|
|
451
|
+
this.open = undefined;
|
|
452
|
+
const streams = this.archive.streams;
|
|
453
|
+
if (!streams)
|
|
454
|
+
throw new errors_1.ParseError('an entry has data but the archive has no streams');
|
|
455
|
+
const folder = streams.folders[f];
|
|
456
|
+
const encrypted = isEncrypted(folder);
|
|
457
|
+
if (encrypted && password === undefined)
|
|
458
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Encrypted, types_1.ArchiveDetail.PasswordRequired, 'No password opens the folder', undefined, `folder ${f + 1}`);
|
|
459
|
+
// Each decode of a folder counts its own ratio. The folders' declared sizes were checked against the totals when
|
|
460
|
+
// the archive opened, and a decode stops at them.
|
|
461
|
+
const meter = new stream_meter_1.Meter(this.env.limits, { decoded: 0, compressed: 0 }, this.env.deadline, false, `folder ${f + 1}`);
|
|
462
|
+
const chunks = (0, coders_1.decodeFolder)(folder, packOffsetsOf(this.archive, f), packSizesOf(this.archive, f), folderEnv(this.env, meter, encrypted ? password : undefined));
|
|
463
|
+
this.open = { index: f, cursor: new FolderCursor(meterChunks(chunks, meter)), meter, decrypted: encrypted, password };
|
|
464
|
+
return this.open;
|
|
465
|
+
}
|
|
466
|
+
/** The decoded content of an entry with data. It throws when the folder cannot decode it or its CRC does not match. */
|
|
467
|
+
async *content(entry, password) {
|
|
468
|
+
if (entry.folder === undefined)
|
|
469
|
+
return;
|
|
470
|
+
let open = await this.folder(entry.folder, password);
|
|
471
|
+
if (open.cursor.taken > entry.start) {
|
|
472
|
+
await this.close();
|
|
473
|
+
open = await this.folder(entry.folder, password);
|
|
474
|
+
}
|
|
475
|
+
// Bytes of earlier entries nobody read are skipped.
|
|
476
|
+
if (open.cursor.taken < entry.start)
|
|
477
|
+
for await (const _ of open.cursor.take(entry.start - open.cursor.taken))
|
|
478
|
+
;
|
|
479
|
+
let crc = 0;
|
|
480
|
+
for await (const c of open.cursor.take(entry.size)) {
|
|
481
|
+
crc = (0, crc32_1.crc32)(c, crc);
|
|
482
|
+
yield c;
|
|
483
|
+
}
|
|
484
|
+
if (entry.crc !== undefined && crc !== entry.crc)
|
|
485
|
+
throw new errors_1.CheckError('the entry CRC32 does not match', 'CRC32');
|
|
486
|
+
}
|
|
487
|
+
async close() {
|
|
488
|
+
await this.open?.cursor.close();
|
|
489
|
+
this.open = undefined;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
exports.SevenReader = SevenReader;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** Property IDs of the 7z header. */
|
|
2
|
+
export declare enum Nid {
|
|
3
|
+
End = 0,
|
|
4
|
+
Header = 1,
|
|
5
|
+
ArchiveProperties = 2,
|
|
6
|
+
AdditionalStreamsInfo = 3,
|
|
7
|
+
MainStreamsInfo = 4,
|
|
8
|
+
FilesInfo = 5,
|
|
9
|
+
PackInfo = 6,
|
|
10
|
+
UnpackInfo = 7,
|
|
11
|
+
SubStreamsInfo = 8,
|
|
12
|
+
Size = 9,
|
|
13
|
+
Crc = 10,
|
|
14
|
+
Folder = 11,
|
|
15
|
+
CodersUnpackSize = 12,
|
|
16
|
+
NumUnpackStream = 13,
|
|
17
|
+
EmptyStream = 14,
|
|
18
|
+
EmptyFile = 15,
|
|
19
|
+
Anti = 16,
|
|
20
|
+
Name = 17,
|
|
21
|
+
CTime = 18,
|
|
22
|
+
ATime = 19,
|
|
23
|
+
MTime = 20,
|
|
24
|
+
WinAttributes = 21,
|
|
25
|
+
Comment = 22,
|
|
26
|
+
EncodedHeader = 23,
|
|
27
|
+
StartPos = 24,
|
|
28
|
+
Dummy = 25
|
|
29
|
+
}
|
|
30
|
+
/** One coder of a folder. */
|
|
31
|
+
export interface Coder {
|
|
32
|
+
/** The method ID in hex, such as "030101" for LZMA. */
|
|
33
|
+
method: string;
|
|
34
|
+
numIn: number;
|
|
35
|
+
numOut: number;
|
|
36
|
+
props: Uint8Array;
|
|
37
|
+
}
|
|
38
|
+
/** A folder: coders joined by bind pairs, fed by packed streams. */
|
|
39
|
+
export interface Folder {
|
|
40
|
+
coders: Coder[];
|
|
41
|
+
/** Coder in-stream `inIndex` reads coder out-stream `outIndex`. */
|
|
42
|
+
bindPairs: Array<{
|
|
43
|
+
inIndex: number;
|
|
44
|
+
outIndex: number;
|
|
45
|
+
}>;
|
|
46
|
+
/** The in-streams that read packed streams, in the order of the folder's packed streams. */
|
|
47
|
+
packedStreams: number[];
|
|
48
|
+
/** Decoded size of every coder out-stream. */
|
|
49
|
+
unpackSizes: number[];
|
|
50
|
+
/** CRC of the folder's main output, when stored. */
|
|
51
|
+
crc?: number;
|
|
52
|
+
/** The out-stream that is the folder's output: the one no bind pair reads. */
|
|
53
|
+
mainOut: number;
|
|
54
|
+
}
|
|
55
|
+
export interface StreamsInfo {
|
|
56
|
+
/** Offset of the first packed stream, from the end of the signature header. */
|
|
57
|
+
packPos: number;
|
|
58
|
+
packSizes: number[];
|
|
59
|
+
folders: Folder[];
|
|
60
|
+
/** Per folder, how many files its output holds. */
|
|
61
|
+
numUnpackStreams: number[];
|
|
62
|
+
/** Per substream, in folder order: its size and CRC. */
|
|
63
|
+
subSizes: number[];
|
|
64
|
+
subCrcs: Array<number | undefined>;
|
|
65
|
+
}
|
|
66
|
+
/** One file entry as the header describes it. */
|
|
67
|
+
export interface FileInfo {
|
|
68
|
+
name?: string;
|
|
69
|
+
hasStream: boolean;
|
|
70
|
+
/** For an entry without a stream: an empty file rather than a directory. */
|
|
71
|
+
emptyFile: boolean;
|
|
72
|
+
anti: boolean;
|
|
73
|
+
attrib?: number;
|
|
74
|
+
ctime?: Uint8Array;
|
|
75
|
+
atime?: Uint8Array;
|
|
76
|
+
mtime?: Uint8Array;
|
|
77
|
+
}
|
|
78
|
+
export interface Header {
|
|
79
|
+
streams?: StreamsInfo;
|
|
80
|
+
files: FileInfo[];
|
|
81
|
+
/** Header properties this package does not keep, by name. */
|
|
82
|
+
unknown: string[];
|
|
83
|
+
}
|
|
84
|
+
/** What a file declares at its start. */
|
|
85
|
+
export interface StartHeader {
|
|
86
|
+
nextHeaderOffset: number;
|
|
87
|
+
nextHeaderSize: number;
|
|
88
|
+
nextHeaderCrc: number;
|
|
89
|
+
minorVersion: number;
|
|
90
|
+
}
|
|
91
|
+
/** Reads the numbers, bit vectors and digests of a 7z header. */
|
|
92
|
+
export declare class HeaderReader {
|
|
93
|
+
readonly buf: Uint8Array;
|
|
94
|
+
pos: number;
|
|
95
|
+
constructor(buf: Uint8Array);
|
|
96
|
+
get remaining(): number;
|
|
97
|
+
byte(): number;
|
|
98
|
+
/** A property ID, read as a 7z number, so an ID can take more than one byte. Any large one is unknown. */
|
|
99
|
+
id(): number;
|
|
100
|
+
bytes(n: number): Uint8Array;
|
|
101
|
+
/** A 7z number: the first byte's leading one bits count the bytes that follow, which hold the low bits. */
|
|
102
|
+
private raw;
|
|
103
|
+
/** A 7z number used as a size, a count or an index. */
|
|
104
|
+
number(): number;
|
|
105
|
+
/** A number used as a count of things that each take at least `unit` bytes of what is left. */
|
|
106
|
+
count(unit: number, max?: number): number;
|
|
107
|
+
uint32(): number;
|
|
108
|
+
bits(n: number): boolean[];
|
|
109
|
+
/** A byte that says all are defined, or a bit vector that says which. */
|
|
110
|
+
defined(n: number): boolean[];
|
|
111
|
+
digests(n: number): Array<number | undefined>;
|
|
112
|
+
}
|
|
113
|
+
/** Reads the 20 bytes after the signature, version and CRC. */
|
|
114
|
+
export declare function parseStartHeader(b: Uint8Array): StartHeader;
|
|
115
|
+
/** The coder that owns a global in- or out-stream index, and the stream's index within that coder. */
|
|
116
|
+
export declare function coderOf(folder: Folder, index: number, side: 'in' | 'out'): {
|
|
117
|
+
coder: number;
|
|
118
|
+
local: number;
|
|
119
|
+
};
|
|
120
|
+
/** The first global in- or out-stream index of coder `c`. */
|
|
121
|
+
export declare function firstStream(folder: Folder, c: number, side: 'in' | 'out'): number;
|
|
122
|
+
/**
|
|
123
|
+
* Reads a streams info. `maxEntries` caps the substreams, each of which is a file, before anything is allocated for
|
|
124
|
+
* them; `tooMany` reports a count over it.
|
|
125
|
+
*/
|
|
126
|
+
export declare function readStreamsInfo(r: HeaderReader, maxEntries?: number, tooMany?: (n: number) => never): StreamsInfo;
|
|
127
|
+
/**
|
|
128
|
+
* Reads a plain header, starting after its kHeader byte. `maxEntries` stops a file count over the limit before
|
|
129
|
+
* anything is allocated for it.
|
|
130
|
+
*/
|
|
131
|
+
export declare function parseHeader(buf: Uint8Array, maxEntries: number, tooMany: (n: number) => never): Header;
|
|
132
|
+
/** Reads an encoded header's streams info, starting after its kEncodedHeader byte. */
|
|
133
|
+
export declare function parseEncodedHeader(buf: Uint8Array): StreamsInfo;
|