@patrickdk77/archive-defuse 0.0.0-stage → 0.1.1
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 +378 -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/detect.d.ts +10 -0
- package/dist/detect.js +95 -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 +24 -0
- package/dist/formats.js +106 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +53 -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 +79 -0
- package/dist/pack.js +590 -0
- package/dist/plugins.d.ts +13 -0
- package/dist/plugins.js +56 -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 +311 -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 +51 -0
- package/dist/zip-writer.js +305 -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/unpack.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ArchiveReadError = void 0;
|
|
4
|
+
exports.openArchive = openArchive;
|
|
5
|
+
exports.decompress = decompress;
|
|
6
|
+
// Reading archives and compressed files as a library: list the entries of any format this package reads and read
|
|
7
|
+
// their content, under the same limits the inspection uses. Nothing here applies the rules or writes files; the names
|
|
8
|
+
// are as the archive gives them.
|
|
9
|
+
const engine_1 = require("./engine");
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const formats_1 = require("./formats");
|
|
12
|
+
const io_1 = require("./io");
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when an archive cannot be read: not a format this package reads, a limit, a missing password, or data that
|
|
15
|
+
* does not decode. `category` and `detail` name the finding `inspectArchive` would report.
|
|
16
|
+
*/
|
|
17
|
+
class ArchiveReadError extends Error {
|
|
18
|
+
category;
|
|
19
|
+
detail;
|
|
20
|
+
data;
|
|
21
|
+
constructor(message, category, detail, data) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.category = category;
|
|
24
|
+
this.detail = detail;
|
|
25
|
+
this.data = data;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.ArchiveReadError = ArchiveReadError;
|
|
29
|
+
/** The error a reader throws for what stopped it. Time and nesting limits and I/O errors pass as they are. */
|
|
30
|
+
function readError(e) {
|
|
31
|
+
if ((0, errors_1.stopsTree)(e) || e instanceof ArchiveReadError)
|
|
32
|
+
return e;
|
|
33
|
+
if (e instanceof errors_1.StopError)
|
|
34
|
+
return new ArchiveReadError(e.message, e.category, e.detail, e.data);
|
|
35
|
+
if (e instanceof errors_1.CheckError)
|
|
36
|
+
return new ArchiveReadError(e.message, 'CORRUPTED', 'CHECK_FAILED', { check: e.check });
|
|
37
|
+
if (e instanceof errors_1.DataError)
|
|
38
|
+
return new ArchiveReadError(e.message, 'CORRUPTED', e.truncated ? 'TRUNCATED' : 'DATA_ERROR');
|
|
39
|
+
if (e instanceof errors_1.UnsupportedError)
|
|
40
|
+
return new ArchiveReadError(e.message, 'COMPRESSION', e.method ? 'UNSUPPORTED_METHOD' : 'UNSUPPORTED_FEATURE');
|
|
41
|
+
return e;
|
|
42
|
+
}
|
|
43
|
+
const hasContent = (e) => e.kind === 'file' || e.kind === 'other';
|
|
44
|
+
/**
|
|
45
|
+
* Opens an archive or a compressed file for reading. The format comes from the content, as for `inspectArchive`. The
|
|
46
|
+
* entries are listed at once; content is read only when asked for. The source is closed by `close()`, or at once when
|
|
47
|
+
* opening fails.
|
|
48
|
+
*/
|
|
49
|
+
async function openArchive(input, options = {}) {
|
|
50
|
+
const source = input instanceof Uint8Array ? (0, io_1.bufferSource)(input) : input;
|
|
51
|
+
const limits = (0, engine_1.limitsOf)({ limits: options.limits });
|
|
52
|
+
const deadline = options.limits?.timeMs !== undefined ? Date.now() + options.limits.timeMs : undefined;
|
|
53
|
+
const passwords = (0, engine_1.passwordsOf)({ password: options.password });
|
|
54
|
+
const totals = { decoded: 0 };
|
|
55
|
+
const temp = new io_1.TempDir(options.tempDir);
|
|
56
|
+
let archive;
|
|
57
|
+
try {
|
|
58
|
+
const size = await source.size();
|
|
59
|
+
if (limits.fileSize !== undefined && size > limits.fileSize)
|
|
60
|
+
throw new ArchiveReadError('The file exceeds the file size limit', 'LIMIT', 'FILE_SIZE', { size });
|
|
61
|
+
archive = await (0, formats_1.openArchive)(source, limits, deadline, passwords, { dir: temp, threshold: options.memoryThreshold ?? 8 * 1024 * 1024 });
|
|
62
|
+
// ZIP and 7z keep a symbolic link's target as its content.
|
|
63
|
+
if (archive.linkTarget) {
|
|
64
|
+
for (const e of archive.entries) {
|
|
65
|
+
if (e.kind !== 'symlink' || e.broken || e.linkTarget !== undefined)
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
e.linkTarget = await archive.linkTarget(e, (0, engine_1.makeMeter)(e, limits, deadline, totals));
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (!(err instanceof errors_1.DataError))
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
await source.close?.();
|
|
79
|
+
await temp.cleanup();
|
|
80
|
+
throw readError(e);
|
|
81
|
+
}
|
|
82
|
+
const opened = archive;
|
|
83
|
+
const listed = opened.entries.map(e => ({
|
|
84
|
+
index: e.index,
|
|
85
|
+
name: e.name,
|
|
86
|
+
kind: e.kind,
|
|
87
|
+
size: opened.single ? undefined : e.size,
|
|
88
|
+
mode: e.mode,
|
|
89
|
+
linkTarget: e.linkTarget,
|
|
90
|
+
encrypted: e.encryption !== undefined,
|
|
91
|
+
}));
|
|
92
|
+
const entryOf = (which) => {
|
|
93
|
+
const listedEntry = typeof which === 'string' ? listed.find(l => l.name === which) : which;
|
|
94
|
+
const e = listedEntry === undefined ? undefined : opened.entries[listedEntry.index];
|
|
95
|
+
if (!e)
|
|
96
|
+
throw new ArchiveReadError(`No entry ${JSON.stringify(typeof which === 'string' ? which : which.name)}`, 'ENTRY', 'NOT_FOUND');
|
|
97
|
+
if (!hasContent(e))
|
|
98
|
+
throw new ArchiveReadError(`Entry ${JSON.stringify(e.name)} is a ${e.kind}, which has no content`, 'ENTRY', 'NO_CONTENT');
|
|
99
|
+
return e;
|
|
100
|
+
};
|
|
101
|
+
/** The passwords that open an encrypted entry, in the order to try them; undefined alone for a plain one. */
|
|
102
|
+
async function* keys(e) {
|
|
103
|
+
if (!e.encryption) {
|
|
104
|
+
yield undefined;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (e.encryption.kind === 'unsupported')
|
|
108
|
+
throw new ArchiveReadError(`Entry ${JSON.stringify(e.name)} uses ${e.encryption.reason}`, 'ENCRYPTED', 'UNSUPPORTED');
|
|
109
|
+
let any = false;
|
|
110
|
+
for (const p of passwords) {
|
|
111
|
+
if (await opened.unlock(e, p)) {
|
|
112
|
+
any = true;
|
|
113
|
+
yield p;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!any)
|
|
117
|
+
throw new ArchiveReadError(`No password opens entry ${JSON.stringify(e.name)}`, 'ENCRYPTED', 'PASSWORD_REQUIRED');
|
|
118
|
+
}
|
|
119
|
+
async function* stream(which) {
|
|
120
|
+
const e = entryOf(which);
|
|
121
|
+
if (e.broken)
|
|
122
|
+
throw new ArchiveReadError(`Entry ${JSON.stringify(e.name)} cannot be read: ${e.broken}`, 'CORRUPTED', 'ENTRY_DATA');
|
|
123
|
+
try {
|
|
124
|
+
// A stream cannot take back what it gave, so it reads with the first password that opens the entry.
|
|
125
|
+
for await (const password of keys(e)) {
|
|
126
|
+
yield* opened.read(e, (0, engine_1.makeMeter)(e, limits, deadline, totals), password);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
throw readError(err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function bytes(which) {
|
|
135
|
+
const e = entryOf(which);
|
|
136
|
+
if (e.broken)
|
|
137
|
+
throw new ArchiveReadError(`Entry ${JSON.stringify(e.name)} cannot be read: ${e.broken}`, 'CORRUPTED', 'ENTRY_DATA');
|
|
138
|
+
let failure;
|
|
139
|
+
try {
|
|
140
|
+
// A ZipCrypto header can pass for a wrong password, so a password whose data fails gives way to the next.
|
|
141
|
+
for await (const password of keys(e)) {
|
|
142
|
+
const meter = (0, engine_1.makeMeter)(e, limits, deadline, totals);
|
|
143
|
+
const parts = [];
|
|
144
|
+
try {
|
|
145
|
+
for await (const c of opened.read(e, meter, password))
|
|
146
|
+
parts.push(c);
|
|
147
|
+
return Buffer.concat(parts);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
totals.decoded -= meter.total;
|
|
151
|
+
if (!(err instanceof errors_1.DataError) || e.encryption === undefined)
|
|
152
|
+
throw err;
|
|
153
|
+
failure = err;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
throw readError(err);
|
|
159
|
+
}
|
|
160
|
+
throw readError(failure);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
format: opened.format,
|
|
164
|
+
entries: listed,
|
|
165
|
+
find: name => listed.find(l => l.name === name),
|
|
166
|
+
stream,
|
|
167
|
+
bytes,
|
|
168
|
+
text: async (which, encoding = 'utf8') => (await bytes(which)).toString(encoding),
|
|
169
|
+
close: async () => {
|
|
170
|
+
try {
|
|
171
|
+
await source.close?.();
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await temp.cleanup();
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Decodes a compressed file, such as gzip, bzip2, zstd, Brotli, xz or .lzma, to its content. An archive of several
|
|
181
|
+
* entries throws; `openArchive` reads those.
|
|
182
|
+
*/
|
|
183
|
+
async function decompress(input, options = {}) {
|
|
184
|
+
const reader = await openArchive(input, options);
|
|
185
|
+
try {
|
|
186
|
+
const formats = ['gzip', 'bzip2', 'zstd', 'brotli', 'xz', 'lzma'];
|
|
187
|
+
if (!formats.includes(reader.format))
|
|
188
|
+
throw new ArchiveReadError(`A ${reader.format} archive holds entries; openArchive reads them`, 'FORMAT', 'NOT_SINGLE');
|
|
189
|
+
return await reader.bytes(reader.entries[0]);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
await reader.close();
|
|
193
|
+
}
|
|
194
|
+
}
|
package/dist/xz.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Reader } from './io';
|
|
2
|
+
import type { Note } from './model';
|
|
3
|
+
import type { Limits, StreamCounter as Meter } from './stream-meter';
|
|
4
|
+
/** What reading an xz file found out, beyond its content. */
|
|
5
|
+
export interface XzInfo {
|
|
6
|
+
streams: number;
|
|
7
|
+
/** The check ID of each stream. */
|
|
8
|
+
checks: number[];
|
|
9
|
+
notes: Note[];
|
|
10
|
+
/** Offset of the first byte that is not part of a stream or its padding, when the file goes on past the last stream. */
|
|
11
|
+
trailingAt?: number;
|
|
12
|
+
}
|
|
13
|
+
/** What reading an xz file needs from the run. */
|
|
14
|
+
export interface XzEnv {
|
|
15
|
+
reader: Reader;
|
|
16
|
+
size: number;
|
|
17
|
+
limits: Limits;
|
|
18
|
+
meter: Meter;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decodes an xz file and yields its content: every block of every stream, in order. Stream padding, the index and the
|
|
22
|
+
* footers are checked against what the blocks held. What can be fixed by writing the file again goes to `info.notes`;
|
|
23
|
+
* what cannot throws.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readXz(env: XzEnv, info: XzInfo): AsyncGenerator<Uint8Array>;
|
package/dist/xz.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readXz = readXz;
|
|
4
|
+
const checks_1 = require("./checks");
|
|
5
|
+
const coders_1 = require("./coders");
|
|
6
|
+
const crc32_1 = require("./crc32");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const filters_1 = require("./filters");
|
|
9
|
+
const lzma_sniff_1 = require("./lzma-sniff");
|
|
10
|
+
const lzma1_stream_1 = require("./lzma1-stream");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
const BRANCH_IDS = { 4: 'x86', 5: 'powerpc', 6: 'ia64', 7: 'arm', 8: 'armthumb', 9: 'sparc', 10: 'arm64', 11: 'riscv' };
|
|
13
|
+
const FILTER_LZMA2 = 0x21;
|
|
14
|
+
const FILTER_DELTA = 0x03;
|
|
15
|
+
/** A variable-length integer of xz: 7 bits per byte, low first, at most 9 bytes and no unneeded zero byte at the end. */
|
|
16
|
+
function vli(b, at) {
|
|
17
|
+
let value = 0;
|
|
18
|
+
for (let i = 0; i < 9; i++) {
|
|
19
|
+
if (at + i >= b.length)
|
|
20
|
+
throw new errors_1.ParseError('a number runs past its field');
|
|
21
|
+
const byte = b[at + i];
|
|
22
|
+
value += (byte & 0x7f) * 2 ** (7 * i);
|
|
23
|
+
if ((byte & 0x80) === 0) {
|
|
24
|
+
if (byte === 0 && i > 0)
|
|
25
|
+
throw new errors_1.ParseError('a number is written with an unneeded zero byte');
|
|
26
|
+
return { value, next: at + i + 1 };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
throw new errors_1.ParseError('a number is longer than nine bytes');
|
|
30
|
+
}
|
|
31
|
+
/** Reads bytes one at a time from a reader. */
|
|
32
|
+
class ByteStream {
|
|
33
|
+
reader;
|
|
34
|
+
pos;
|
|
35
|
+
buf = new Uint8Array(0);
|
|
36
|
+
bufAt = 0;
|
|
37
|
+
constructor(reader, pos) {
|
|
38
|
+
this.reader = reader;
|
|
39
|
+
this.pos = pos;
|
|
40
|
+
}
|
|
41
|
+
async byte() {
|
|
42
|
+
if (this.pos >= this.bufAt + this.buf.length) {
|
|
43
|
+
this.buf = await this.reader.read(this.pos, 65536);
|
|
44
|
+
this.bufAt = this.pos;
|
|
45
|
+
if (this.buf.length === 0)
|
|
46
|
+
throw new errors_1.DataError('the index ends past the end of the file', true);
|
|
47
|
+
}
|
|
48
|
+
const b = this.buf[this.pos - this.bufAt];
|
|
49
|
+
this.pos++;
|
|
50
|
+
return b;
|
|
51
|
+
}
|
|
52
|
+
async vli() {
|
|
53
|
+
let value = 0;
|
|
54
|
+
for (let i = 0; i < 9; i++) {
|
|
55
|
+
const byte = await this.byte();
|
|
56
|
+
value += (byte & 0x7f) * 2 ** (7 * i);
|
|
57
|
+
if ((byte & 0x80) === 0) {
|
|
58
|
+
if (byte === 0 && i > 0)
|
|
59
|
+
throw new errors_1.ParseError('a number is written with an unneeded zero byte');
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw new errors_1.ParseError('a number is longer than nine bytes');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function exact(reader, pos, n, what) {
|
|
67
|
+
const b = await reader.read(pos, n);
|
|
68
|
+
if (b.length < n)
|
|
69
|
+
throw new errors_1.DataError(`the ${what} ends past the end of the file`, true);
|
|
70
|
+
return b;
|
|
71
|
+
}
|
|
72
|
+
const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
|
|
73
|
+
const isMagic = (b) => b.length >= 6 && lzma_sniff_1.XZ_MAGIC.every((x, i) => b[i] === x);
|
|
74
|
+
function parseBlockHeader(h) {
|
|
75
|
+
const size = h.length;
|
|
76
|
+
if ((0, crc32_1.crc32)(h.subarray(0, size - 4)) !== u32(h, size - 4))
|
|
77
|
+
throw new errors_1.CheckError('the block header CRC32 does not match', 'block header CRC32');
|
|
78
|
+
const flags = h[1];
|
|
79
|
+
if (flags & 0x3c)
|
|
80
|
+
throw new errors_1.UnsupportedError('reserved block flags', false, { reason: 'reserved block header flags' });
|
|
81
|
+
const end = size - 4;
|
|
82
|
+
let q = 2;
|
|
83
|
+
let compressed;
|
|
84
|
+
let uncompressed;
|
|
85
|
+
const field = (what) => {
|
|
86
|
+
if (q >= end)
|
|
87
|
+
throw new errors_1.ParseError(`the block header ends in its ${what}`);
|
|
88
|
+
const r = vli(h.subarray(0, end), q);
|
|
89
|
+
q = r.next;
|
|
90
|
+
return r.value;
|
|
91
|
+
};
|
|
92
|
+
if (flags & 0x40) {
|
|
93
|
+
compressed = field('compressed size');
|
|
94
|
+
if (compressed === 0)
|
|
95
|
+
throw new errors_1.ParseError('a block declares a compressed size of zero');
|
|
96
|
+
}
|
|
97
|
+
if (flags & 0x80)
|
|
98
|
+
uncompressed = field('uncompressed size');
|
|
99
|
+
const raw = [];
|
|
100
|
+
for (let k = 0; k < (flags & 3) + 1; k++) {
|
|
101
|
+
const id = field('filter flags');
|
|
102
|
+
const propsSize = field('filter flags');
|
|
103
|
+
if (q + propsSize > end)
|
|
104
|
+
throw new errors_1.ParseError('the block header ends in its filter flags');
|
|
105
|
+
raw.push({ id, props: h.slice(q, q + propsSize) });
|
|
106
|
+
q += propsSize;
|
|
107
|
+
}
|
|
108
|
+
for (let i = q; i < end; i++) {
|
|
109
|
+
if (h[i] !== 0)
|
|
110
|
+
throw new errors_1.UnsupportedError('block header padding that is not zero', false, { reason: 'a block header field this package does not know' });
|
|
111
|
+
}
|
|
112
|
+
const last = raw[raw.length - 1];
|
|
113
|
+
const known = (id) => id === FILTER_LZMA2 || id === FILTER_DELTA || BRANCH_IDS[id] !== undefined;
|
|
114
|
+
for (const f of raw) {
|
|
115
|
+
if (!known(f.id))
|
|
116
|
+
throw new errors_1.UnsupportedError(`xz filter 0x${f.id.toString(16)}`, true, { method: `filter 0x${f.id.toString(16)}` });
|
|
117
|
+
}
|
|
118
|
+
if (last.id !== FILTER_LZMA2)
|
|
119
|
+
throw new errors_1.UnsupportedError('a filter chain that does not end in LZMA2', false, { reason: 'a filter chain that does not end in LZMA2' });
|
|
120
|
+
if (raw.slice(0, -1).some(f => f.id === FILTER_LZMA2))
|
|
121
|
+
throw new errors_1.UnsupportedError('LZMA2 before the last filter', false, { reason: 'LZMA2 before the last filter' });
|
|
122
|
+
const dict = last.props.length === 1 ? (0, coders_1.lzma2DictSize)(last.props[0]) : undefined;
|
|
123
|
+
if (dict === undefined)
|
|
124
|
+
throw new errors_1.ParseError('invalid LZMA2 properties');
|
|
125
|
+
const filters = raw.slice(0, -1).map(f => {
|
|
126
|
+
if (f.id === FILTER_DELTA) {
|
|
127
|
+
if (f.props.length !== 1)
|
|
128
|
+
throw new errors_1.ParseError('invalid delta filter properties');
|
|
129
|
+
return { kind: 'delta', distance: f.props[0] + 1 };
|
|
130
|
+
}
|
|
131
|
+
if (f.props.length !== 0 && f.props.length !== 4)
|
|
132
|
+
throw new errors_1.ParseError('invalid branch filter properties');
|
|
133
|
+
return { kind: BRANCH_IDS[f.id], start: f.props.length === 4 ? u32(f.props, 0) : 0 };
|
|
134
|
+
});
|
|
135
|
+
return { size, compressed, uncompressed, dict, filters };
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Decodes an xz file and yields its content: every block of every stream, in order. Stream padding, the index and the
|
|
139
|
+
* footers are checked against what the blocks held. What can be fixed by writing the file again goes to `info.notes`;
|
|
140
|
+
* what cannot throws.
|
|
141
|
+
*/
|
|
142
|
+
async function* readXz(env, info) {
|
|
143
|
+
const { reader, size, meter } = env;
|
|
144
|
+
let pos = 0;
|
|
145
|
+
for (;;) {
|
|
146
|
+
// Stream header. Bytes after a stream that do not start the magic are trailing data, however few; bytes that do
|
|
147
|
+
// start it are a stream cut short.
|
|
148
|
+
const header = await reader.read(pos, 12);
|
|
149
|
+
const magic = header.length >= 6 ? isMagic(header) : header.every((b, i) => b === lzma_sniff_1.XZ_MAGIC[i]);
|
|
150
|
+
if (!magic) {
|
|
151
|
+
if (info.streams === 0)
|
|
152
|
+
throw new errors_1.ParseError('the file does not start with an xz stream');
|
|
153
|
+
info.trailingAt = pos;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (header.length < 12)
|
|
157
|
+
throw new errors_1.DataError('the stream header ends past the end of the file', true);
|
|
158
|
+
const location = info.streams === 0 ? undefined : `stream ${info.streams + 1}`;
|
|
159
|
+
if ((0, crc32_1.crc32)(header.subarray(6, 8)) !== u32(header, 8))
|
|
160
|
+
throw new errors_1.CheckError('the stream header CRC32 does not match', 'stream header CRC32');
|
|
161
|
+
if (header[6] !== 0 || (header[7] & 0xf0) !== 0)
|
|
162
|
+
throw new errors_1.UnsupportedError('reserved stream flags', false, { reason: 'reserved stream flags' });
|
|
163
|
+
const checkId = header[7] & 0x0f;
|
|
164
|
+
const cSize = (0, checks_1.checkSize)(checkId);
|
|
165
|
+
if (checkId === checks_1.CHECK_NONE)
|
|
166
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.NoCheck, location });
|
|
167
|
+
else if (checkId !== checks_1.CHECK_CRC32 && checkId !== checks_1.CHECK_CRC64 && checkId !== checks_1.CHECK_SHA256) {
|
|
168
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.UnknownCheck, location, data: { check: (0, checks_1.checkName)(checkId) } });
|
|
169
|
+
}
|
|
170
|
+
info.checks.push(checkId);
|
|
171
|
+
info.streams++;
|
|
172
|
+
meter.tick();
|
|
173
|
+
pos += 12;
|
|
174
|
+
// Blocks
|
|
175
|
+
const blocks = [];
|
|
176
|
+
for (;;) {
|
|
177
|
+
const first = await exact(reader, pos, 1, 'block header');
|
|
178
|
+
if (first[0] === 0x00)
|
|
179
|
+
break;
|
|
180
|
+
const blockLoc = `${location ? `${location}, ` : ''}block ${blocks.length + 1}`;
|
|
181
|
+
const headerSize = (first[0] + 1) * 4;
|
|
182
|
+
const h = await exact(reader, pos, headerSize, 'block header');
|
|
183
|
+
const bh = parseBlockHeader(h);
|
|
184
|
+
if (bh.uncompressed !== undefined && bh.uncompressed > env.limits.decompressedBytes) {
|
|
185
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DecompressedSize, 'Decompressed size limit exceeded', { limit: env.limits.decompressedBytes, declared: bh.uncompressed }, blockLoc);
|
|
186
|
+
}
|
|
187
|
+
const dictNeed = (0, lzma1_stream_1.effectiveDictSize)(bh.dict, bh.uncompressed);
|
|
188
|
+
if (dictNeed > env.limits.dictionaryBytes) {
|
|
189
|
+
throw new errors_1.StopError(types_1.ArchiveCategory.Limit, types_1.ArchiveDetail.DictionarySize, 'Dictionary size limit exceeded', { limit: env.limits.dictionaryBytes, dictionary: dictNeed }, blockLoc);
|
|
190
|
+
}
|
|
191
|
+
const dataStart = pos + headerSize;
|
|
192
|
+
const dataEnd = bh.compressed !== undefined ? dataStart + bh.compressed : size;
|
|
193
|
+
if (dataEnd > size)
|
|
194
|
+
throw new errors_1.DataError('a block declares more compressed data than the file holds', true);
|
|
195
|
+
// Decoding runs LZMA2 first, then the other filters from the last listed to the first.
|
|
196
|
+
const transforms = bh.filters.map(f => (f.kind === 'delta' ? new filters_1.DeltaStream(f.distance) : new filters_1.SimpleFilterStream(f.kind, f.start))).reverse();
|
|
197
|
+
const check = (0, checks_1.makeCheck)(checkId);
|
|
198
|
+
const feeder = new coders_1.Lzma2Feeder(bh.dict);
|
|
199
|
+
let uncompressed = 0;
|
|
200
|
+
const emit = (chunk, from) => {
|
|
201
|
+
let d = chunk;
|
|
202
|
+
for (let k = from; k < transforms.length && d.length; k++)
|
|
203
|
+
d = transforms[k].push(d);
|
|
204
|
+
return d;
|
|
205
|
+
};
|
|
206
|
+
let p = dataStart;
|
|
207
|
+
while (!feeder.ended) {
|
|
208
|
+
if (p >= dataEnd)
|
|
209
|
+
throw new errors_1.DataError(bh.compressed !== undefined ? 'the LZMA2 data does not end within the compressed size its block declares' : 'the LZMA2 data ends past the end of the file', true);
|
|
210
|
+
const chunk = await reader.read(p, Math.min(65536, dataEnd - p));
|
|
211
|
+
let used = 0;
|
|
212
|
+
while (used < chunk.length && !feeder.ended) {
|
|
213
|
+
const n = feeder.push(chunk.subarray(used, used + 4096));
|
|
214
|
+
used += n;
|
|
215
|
+
meter.packed(n);
|
|
216
|
+
for (const out of feeder.take()) {
|
|
217
|
+
const d = emit(out, 0);
|
|
218
|
+
if (!d.length)
|
|
219
|
+
continue;
|
|
220
|
+
check?.update(d);
|
|
221
|
+
uncompressed += d.length;
|
|
222
|
+
meter.add(d.length);
|
|
223
|
+
yield d;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
p += used;
|
|
227
|
+
}
|
|
228
|
+
for (let k = 0; k < transforms.length; k++) {
|
|
229
|
+
const d = emit(transforms[k].end(), k + 1);
|
|
230
|
+
if (!d.length)
|
|
231
|
+
continue;
|
|
232
|
+
check?.update(d);
|
|
233
|
+
uncompressed += d.length;
|
|
234
|
+
meter.add(d.length);
|
|
235
|
+
yield d;
|
|
236
|
+
}
|
|
237
|
+
const compressed = p - dataStart;
|
|
238
|
+
if (bh.compressed !== undefined && bh.compressed !== compressed) {
|
|
239
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.SizeMismatch, location: blockLoc, data: { reason: 'compressed size of a block', declared: bh.compressed, actual: compressed } });
|
|
240
|
+
}
|
|
241
|
+
if (bh.uncompressed !== undefined && bh.uncompressed !== uncompressed) {
|
|
242
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.SizeMismatch, location: blockLoc, data: { reason: 'uncompressed size of a block', declared: bh.uncompressed, actual: uncompressed } });
|
|
243
|
+
}
|
|
244
|
+
const padding = (4 - ((headerSize + compressed) % 4)) % 4;
|
|
245
|
+
const tail = await exact(reader, p, padding + cSize, 'block');
|
|
246
|
+
if (tail.subarray(0, padding).some(b => b !== 0))
|
|
247
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.BadPadding, location: blockLoc, data: { reason: 'block padding that is not zero' } });
|
|
248
|
+
if (check) {
|
|
249
|
+
const want = check.digest();
|
|
250
|
+
const got = tail.subarray(padding);
|
|
251
|
+
if (want.some((b, i) => b !== got[i]))
|
|
252
|
+
throw new errors_1.CheckError(`the ${(0, checks_1.checkName)(checkId)} of a block does not match`, (0, checks_1.checkName)(checkId));
|
|
253
|
+
}
|
|
254
|
+
blocks.push({ unpadded: headerSize + compressed + cSize, uncompressed });
|
|
255
|
+
pos = p + padding + cSize;
|
|
256
|
+
}
|
|
257
|
+
// Index
|
|
258
|
+
const index = new ByteStream(reader, pos);
|
|
259
|
+
await index.byte();
|
|
260
|
+
const count = await index.vli();
|
|
261
|
+
if (count * 2 > size - index.pos)
|
|
262
|
+
throw new errors_1.DataError('the index lists more blocks than the file can hold', true);
|
|
263
|
+
let mismatch;
|
|
264
|
+
if (count !== blocks.length)
|
|
265
|
+
mismatch = 'the number of blocks';
|
|
266
|
+
for (let i = 0; i < count; i++) {
|
|
267
|
+
const unpadded = await index.vli();
|
|
268
|
+
const uncompressed = await index.vli();
|
|
269
|
+
if (unpadded === 0)
|
|
270
|
+
mismatch ??= 'an unpadded size of zero';
|
|
271
|
+
const b = blocks[i];
|
|
272
|
+
if (b && b.unpadded !== unpadded)
|
|
273
|
+
mismatch ??= 'the unpadded size of a block';
|
|
274
|
+
if (b && b.uncompressed !== uncompressed)
|
|
275
|
+
mismatch ??= 'the uncompressed size of a block';
|
|
276
|
+
}
|
|
277
|
+
let padBad = false;
|
|
278
|
+
while ((index.pos - pos) % 4 !== 0)
|
|
279
|
+
if ((await index.byte()) !== 0)
|
|
280
|
+
padBad = true;
|
|
281
|
+
let indexCrc = 0;
|
|
282
|
+
for (let at = pos; at < index.pos; at += 65536)
|
|
283
|
+
indexCrc = (0, crc32_1.crc32)(await exact(reader, at, Math.min(65536, index.pos - at), 'index'), indexCrc);
|
|
284
|
+
const stored = await exact(reader, index.pos, 4, 'index');
|
|
285
|
+
if (u32(stored, 0) !== indexCrc)
|
|
286
|
+
throw new errors_1.CheckError('the index CRC32 does not match', 'index CRC32');
|
|
287
|
+
if (padBad)
|
|
288
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.BadPadding, location, data: { reason: 'index padding that is not zero' } });
|
|
289
|
+
if (mismatch)
|
|
290
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.IndexMismatch, location, data: { reason: mismatch } });
|
|
291
|
+
const indexSize = index.pos + 4 - pos;
|
|
292
|
+
pos += indexSize;
|
|
293
|
+
// Stream footer
|
|
294
|
+
const footer = await exact(reader, pos, 12, 'stream footer');
|
|
295
|
+
if (footer[10] !== 0x59 || footer[11] !== 0x5a)
|
|
296
|
+
throw new errors_1.ParseError('the stream footer has no magic bytes');
|
|
297
|
+
if ((0, crc32_1.crc32)(footer.subarray(4, 10)) !== u32(footer, 0))
|
|
298
|
+
throw new errors_1.CheckError('the stream footer CRC32 does not match', 'stream footer CRC32');
|
|
299
|
+
if ((u32(footer, 4) + 1) * 4 !== indexSize)
|
|
300
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.IndexMismatch, location, data: { reason: 'the index size in the stream footer' } });
|
|
301
|
+
if (footer[8] !== header[6] || footer[9] !== header[7])
|
|
302
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.HeaderMismatch, location });
|
|
303
|
+
pos += 12;
|
|
304
|
+
// Stream padding, then the next stream or the end.
|
|
305
|
+
let zeros = 0;
|
|
306
|
+
for (;;) {
|
|
307
|
+
const b = await reader.read(pos + zeros, 4096);
|
|
308
|
+
let k = 0;
|
|
309
|
+
while (k < b.length && b[k] === 0)
|
|
310
|
+
k++;
|
|
311
|
+
zeros += k;
|
|
312
|
+
if (k < b.length || b.length === 0)
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (zeros > 0) {
|
|
316
|
+
const next = await reader.read(pos + zeros, 6);
|
|
317
|
+
const atEnd = pos + zeros === size;
|
|
318
|
+
if (atEnd || isMagic(next)) {
|
|
319
|
+
if (zeros % 4 === 0)
|
|
320
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.StreamPadding, data: { bytes: zeros } });
|
|
321
|
+
else
|
|
322
|
+
info.notes.push({ category: types_1.ArchiveCategory.Structure, detail: types_1.ArchiveDetail.BadPadding, data: { reason: 'stream padding that is not a multiple of four bytes', bytes: zeros } });
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
info.trailingAt = pos;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
pos += zeros;
|
|
330
|
+
if (pos >= size)
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Consumed } from './codecs';
|
|
2
|
+
import { type TempDir } from './io';
|
|
3
|
+
import type { Meter } from './model';
|
|
4
|
+
import type { Limits } from './stream-meter';
|
|
5
|
+
/**
|
|
6
|
+
* Decodes a method 14 entry: the LZMA header APPNOTE 5.8.8 gives, then raw LZMA data of the entry's size, which may
|
|
7
|
+
* end with an end marker. `consumed.bytes` follows the input the data took, so the caller can see bytes after it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function zipLzma(chunks: AsyncIterable<Uint8Array>, size: number, consumed: Consumed, dictionaryBytes: number): AsyncGenerator<Uint8Array>;
|
|
10
|
+
/**
|
|
11
|
+
* Decodes a method 95 entry, which holds a whole xz file. The xz reader reads by offset, so the entry's compressed
|
|
12
|
+
* bytes go to memory or a temporary file first. A file that goes on past its streams, or that has notes the xz reader
|
|
13
|
+
* makes, such as bad padding, does not decode: an entry is one xz file and nothing else.
|
|
14
|
+
*/
|
|
15
|
+
export declare function zipXz(chunks: AsyncIterable<Uint8Array>, consumed: Consumed, meter: Meter, temp: TempDir, threshold: number, limits: Limits): AsyncGenerator<Uint8Array>;
|