@oh-my-pi/pi-utils 17.4.0 → 17.4.2
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/CHANGELOG.md +12 -0
- package/LICENSE +22 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/ar/arj.d.ts +5 -0
- package/dist/types/ar/asar.d.ts +7 -0
- package/dist/types/ar/bytes.d.ts +20 -0
- package/dist/types/ar/cab.d.ts +5 -0
- package/dist/types/ar/checksums.d.ts +10 -0
- package/dist/types/ar/codecs/bzip2.d.ts +4 -0
- package/dist/types/ar/codecs/gzip.d.ts +6 -0
- package/dist/types/ar/codecs/lzma.d.ts +6 -0
- package/dist/types/ar/codecs/lzw.d.ts +4 -0
- package/dist/types/ar/codecs/lzx.d.ts +7 -0
- package/dist/types/ar/codecs/xz.d.ts +4 -0
- package/dist/types/ar/codecs/zstd.d.ts +6 -0
- package/dist/types/ar/cpio.d.ts +7 -0
- package/dist/types/ar/deb.d.ts +5 -0
- package/dist/types/ar/entries.d.ts +22 -0
- package/dist/types/ar/error.d.ts +8 -0
- package/dist/types/ar/index.d.ts +11 -0
- package/dist/types/ar/iso.d.ts +5 -0
- package/dist/types/ar/limits.d.ts +33 -0
- package/dist/types/ar/lzh.d.ts +7 -0
- package/dist/types/ar/open.d.ts +45 -0
- package/dist/types/ar/paths.d.ts +18 -0
- package/dist/types/ar/rar/rar4-decoder.d.ts +6 -0
- package/dist/types/ar/rar/rar5-decoder.d.ts +6 -0
- package/dist/types/ar/rar.d.ts +5 -0
- package/dist/types/ar/reader.d.ts +28 -0
- package/dist/types/ar/registry.d.ts +17 -0
- package/dist/types/ar/rpm.d.ts +5 -0
- package/dist/types/ar/sevenzip/decode.d.ts +33 -0
- package/dist/types/ar/sevenzip.d.ts +5 -0
- package/dist/types/ar/source.d.ts +54 -0
- package/dist/types/ar/tar.d.ts +9 -0
- package/dist/types/ar/types.d.ts +99 -0
- package/dist/types/ar/unix-ar.d.ts +7 -0
- package/dist/types/ar/write.d.ts +10 -0
- package/dist/types/ar/zip.d.ts +10 -0
- package/dist/types/postmortem.d.ts +33 -0
- package/package.json +9 -3
- package/src/ar/arj.ts +314 -0
- package/src/ar/asar.ts +480 -0
- package/src/ar/bytes.ts +78 -0
- package/src/ar/cab.ts +377 -0
- package/src/ar/checksums.ts +62 -0
- package/src/ar/codecs/bzip2.ts +489 -0
- package/src/ar/codecs/gzip.ts +25 -0
- package/src/ar/codecs/lzma.ts +437 -0
- package/src/ar/codecs/lzw.ts +191 -0
- package/src/ar/codecs/lzx.ts +366 -0
- package/src/ar/codecs/xz.ts +522 -0
- package/src/ar/codecs/zstd.ts +25 -0
- package/src/ar/cpio.ts +389 -0
- package/src/ar/deb.ts +160 -0
- package/src/ar/entries.ts +97 -0
- package/src/ar/error.ts +11 -0
- package/src/ar/index.ts +16 -0
- package/src/ar/iso.ts +712 -0
- package/src/ar/limits.ts +80 -0
- package/src/ar/lzh.ts +659 -0
- package/src/ar/open.ts +225 -0
- package/src/ar/paths.ts +70 -0
- package/src/ar/rar/rar4-decoder.ts +459 -0
- package/src/ar/rar/rar5-decoder.ts +400 -0
- package/src/ar/rar.ts +735 -0
- package/src/ar/reader.ts +162 -0
- package/src/ar/registry.ts +208 -0
- package/src/ar/rpm.ts +320 -0
- package/src/ar/sevenzip/decode.ts +239 -0
- package/src/ar/sevenzip.ts +634 -0
- package/src/ar/source.ts +190 -0
- package/src/ar/tar.ts +771 -0
- package/src/ar/types.ts +131 -0
- package/src/ar/unix-ar.ts +312 -0
- package/src/ar/write.ts +55 -0
- package/src/ar/zip.ts +719 -0
- package/src/browsers.ts +2 -149
- package/src/docx/converter.ts +9 -9
- package/src/postmortem.ts +74 -0
- package/dist/types/docx/zip.d.ts +0 -6
- package/src/docx/zip.ts +0 -87
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
import { crc32 } from "./checksums";
|
|
2
|
+
import { ArchiveError } from "./error";
|
|
3
|
+
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize, assertInMemorySize } from "./limits";
|
|
4
|
+
import { assertArchivePathBytes, assertArchivePathString, normalizeArchiveEntryPath } from "./paths";
|
|
5
|
+
import {
|
|
6
|
+
type SevenZipBindPair,
|
|
7
|
+
type SevenZipCoder,
|
|
8
|
+
type SevenZipFolderRecord,
|
|
9
|
+
SevenZipFolderSource,
|
|
10
|
+
} from "./sevenzip/decode";
|
|
11
|
+
import type { ByteSource } from "./source";
|
|
12
|
+
import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
|
|
13
|
+
|
|
14
|
+
const SIGNATURE = Uint8Array.of(0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c);
|
|
15
|
+
// WHATWG maps the "utf-16" label to the UTF-16LE decoder 7z names use.
|
|
16
|
+
const UTF16_LE = new TextDecoder("utf-16", { fatal: true });
|
|
17
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
18
|
+
const FILETIME_EPOCH_MS = 11_644_473_600_000;
|
|
19
|
+
|
|
20
|
+
class HeaderReader {
|
|
21
|
+
readonly bytes: Uint8Array;
|
|
22
|
+
pos = 0;
|
|
23
|
+
|
|
24
|
+
constructor(bytes: Uint8Array) {
|
|
25
|
+
this.bytes = bytes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get remaining(): number {
|
|
29
|
+
return this.bytes.byteLength - this.pos;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
readByte(): number {
|
|
33
|
+
if (this.pos >= this.bytes.byteLength) throw new ArchiveError("Invalid 7z header: truncated data");
|
|
34
|
+
return this.bytes[this.pos++]!;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
readBytes(size: number): Uint8Array {
|
|
38
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > this.remaining)
|
|
39
|
+
throw new ArchiveError("Invalid 7z header: truncated or invalid-sized data");
|
|
40
|
+
const result = this.bytes.subarray(this.pos, this.pos + size);
|
|
41
|
+
this.pos += size;
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
readUInt32(): number {
|
|
46
|
+
const bytes = this.readBytes(4);
|
|
47
|
+
return (bytes[0]! | (bytes[1]! << 8) | (bytes[2]! << 16) | (bytes[3]! << 24)) >>> 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
readUInt64BigInt(): bigint {
|
|
51
|
+
const bytes = this.readBytes(8);
|
|
52
|
+
let value = 0n;
|
|
53
|
+
for (let index = 0; index < 8; index++) value |= BigInt(bytes[index]!) << BigInt(index * 8);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
readNumberBigInt(): bigint {
|
|
58
|
+
const first = this.readByte();
|
|
59
|
+
let value = 0n;
|
|
60
|
+
let mask = 0x80;
|
|
61
|
+
for (let index = 0; index < 8; index++) {
|
|
62
|
+
if ((first & mask) === 0) return value | (BigInt(first & (mask - 1)) << BigInt(index * 8));
|
|
63
|
+
value |= BigInt(this.readByte()) << BigInt(index * 8);
|
|
64
|
+
mask >>>= 1;
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
readNumber(what = "number"): number {
|
|
70
|
+
const value = this.readNumberBigInt();
|
|
71
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new ArchiveError(`7z ${what} is too large to read safely`);
|
|
72
|
+
return Number(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
skipSizedProperty(): void {
|
|
76
|
+
this.readBytes(this.readNumber("property size"));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface Digests {
|
|
81
|
+
defined: boolean[];
|
|
82
|
+
values: Array<number | undefined>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface SevenZipSubstream {
|
|
86
|
+
folderIndex: number;
|
|
87
|
+
offset: number;
|
|
88
|
+
size: number;
|
|
89
|
+
crc?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface SevenZipStreams {
|
|
93
|
+
packPosition: number;
|
|
94
|
+
packSizes: number[];
|
|
95
|
+
packCrcs: Array<number | undefined>;
|
|
96
|
+
folders: SevenZipFolderRecord[];
|
|
97
|
+
substreams: SevenZipSubstream[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface FileMetadata {
|
|
101
|
+
names: string[];
|
|
102
|
+
emptyStreams: boolean[];
|
|
103
|
+
emptyFiles: boolean[];
|
|
104
|
+
antiFiles: boolean[];
|
|
105
|
+
mtimes: Array<number | undefined>;
|
|
106
|
+
attributes: Array<number | undefined>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readBoolVector(reader: HeaderReader, count: number): boolean[] {
|
|
110
|
+
const result = new Array<boolean>(count);
|
|
111
|
+
let byte = 0;
|
|
112
|
+
let mask = 0;
|
|
113
|
+
for (let index = 0; index < count; index++) {
|
|
114
|
+
if (mask === 0) {
|
|
115
|
+
byte = reader.readByte();
|
|
116
|
+
mask = 0x80;
|
|
117
|
+
}
|
|
118
|
+
result[index] = (byte & mask) !== 0;
|
|
119
|
+
mask >>>= 1;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readDefinedVector(reader: HeaderReader, count: number): boolean[] {
|
|
125
|
+
if (reader.readByte() !== 0) return new Array<boolean>(count).fill(true);
|
|
126
|
+
return readBoolVector(reader, count);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readDigests(reader: HeaderReader, count: number): Digests {
|
|
130
|
+
const defined = readDefinedVector(reader, count);
|
|
131
|
+
const values = new Array<number | undefined>(count);
|
|
132
|
+
for (let index = 0; index < count; index++) if (defined[index]) values[index] = reader.readUInt32();
|
|
133
|
+
return { defined, values };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function expectId(reader: HeaderReader, expected: number, context: string): void {
|
|
137
|
+
const actual = reader.readNumber("property ID");
|
|
138
|
+
if (actual !== expected)
|
|
139
|
+
throw new ArchiveError(
|
|
140
|
+
`Invalid 7z header: expected ${context} ID 0x${expected.toString(16)}, got 0x${actual.toString(16)}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseFolder(reader: HeaderReader, options: FormatReadOptions): SevenZipFolderRecord {
|
|
145
|
+
const coderCount = reader.readNumber("folder coder count");
|
|
146
|
+
if (coderCount === 0 || coderCount > options.limits.maxEntries)
|
|
147
|
+
throw new ArchiveError("Invalid 7z folder coder count");
|
|
148
|
+
const coders: SevenZipCoder[] = [];
|
|
149
|
+
let totalInputs = 0;
|
|
150
|
+
let totalOutputs = 0;
|
|
151
|
+
for (let index = 0; index < coderCount; index++) {
|
|
152
|
+
const flags = reader.readByte();
|
|
153
|
+
if ((flags & 0xc0) !== 0) throw new ArchiveError("Unsupported 7z coder alternatives or reserved flags");
|
|
154
|
+
const idSize = flags & 0x0f;
|
|
155
|
+
if (idSize === 0 || idSize > 8) throw new ArchiveError("Invalid 7z coder method ID size");
|
|
156
|
+
let id = 0n;
|
|
157
|
+
for (const byte of reader.readBytes(idSize)) id = (id << 8n) | BigInt(byte);
|
|
158
|
+
const complex = (flags & 0x10) !== 0;
|
|
159
|
+
const numInputs = complex ? reader.readNumber("coder input count") : 1;
|
|
160
|
+
const numOutputs = complex ? reader.readNumber("coder output count") : 1;
|
|
161
|
+
if (
|
|
162
|
+
numInputs === 0 ||
|
|
163
|
+
numOutputs === 0 ||
|
|
164
|
+
totalInputs + numInputs > options.limits.maxEntries ||
|
|
165
|
+
totalOutputs + numOutputs > options.limits.maxEntries
|
|
166
|
+
) {
|
|
167
|
+
throw new ArchiveError("Invalid or excessive 7z coder stream count");
|
|
168
|
+
}
|
|
169
|
+
const propertySize = (flags & 0x20) !== 0 ? reader.readNumber("coder properties size") : 0;
|
|
170
|
+
assertIndexSize(propertySize, options.limits, "coder properties");
|
|
171
|
+
const properties = reader.readBytes(propertySize).slice();
|
|
172
|
+
coders.push({ id, properties, inputStart: totalInputs, outputStart: totalOutputs, numInputs, numOutputs });
|
|
173
|
+
totalInputs += numInputs;
|
|
174
|
+
totalOutputs += numOutputs;
|
|
175
|
+
}
|
|
176
|
+
const bindCount = totalOutputs - 1;
|
|
177
|
+
if (bindCount < 0 || bindCount > totalInputs) throw new ArchiveError("Invalid 7z folder bind-pair count");
|
|
178
|
+
const bindPairs: SevenZipBindPair[] = [];
|
|
179
|
+
const usedInputs = new Set<number>();
|
|
180
|
+
const usedOutputs = new Set<number>();
|
|
181
|
+
for (let index = 0; index < bindCount; index++) {
|
|
182
|
+
const input = reader.readNumber("bind input index");
|
|
183
|
+
const output = reader.readNumber("bind output index");
|
|
184
|
+
if (input >= totalInputs || output >= totalOutputs || usedInputs.has(input) || usedOutputs.has(output))
|
|
185
|
+
throw new ArchiveError("Invalid 7z folder bind pair");
|
|
186
|
+
usedInputs.add(input);
|
|
187
|
+
usedOutputs.add(output);
|
|
188
|
+
bindPairs.push({ input, output });
|
|
189
|
+
}
|
|
190
|
+
const packedCount = totalInputs - bindCount;
|
|
191
|
+
const packedIndices: number[] = [];
|
|
192
|
+
if (packedCount === 1) {
|
|
193
|
+
for (let index = 0; index < totalInputs; index++) if (!usedInputs.has(index)) packedIndices.push(index);
|
|
194
|
+
} else {
|
|
195
|
+
for (let index = 0; index < packedCount; index++) {
|
|
196
|
+
const packed = reader.readNumber("packed stream index");
|
|
197
|
+
if (packed >= totalInputs || usedInputs.has(packed)) throw new ArchiveError("Invalid 7z packed stream index");
|
|
198
|
+
usedInputs.add(packed);
|
|
199
|
+
packedIndices.push(packed);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (packedIndices.length !== packedCount) throw new ArchiveError("Invalid 7z folder packed-stream map");
|
|
203
|
+
return { coders, bindPairs, packedIndices, unpackSizes: [], packOffsets: [], packSizes: [], packCrcs: [] };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function finalFolderOutput(folder: SevenZipFolderRecord): number {
|
|
207
|
+
const bound = new Set(folder.bindPairs.map(pair => pair.output));
|
|
208
|
+
const outputs = folder.coders.flatMap(coder =>
|
|
209
|
+
Array.from({ length: coder.numOutputs }, (_, index) => coder.outputStart + index),
|
|
210
|
+
);
|
|
211
|
+
const final = outputs.filter(index => !bound.has(index));
|
|
212
|
+
if (final.length !== 1) throw new ArchiveError("Unsupported 7z folder with multiple final output streams");
|
|
213
|
+
return final[0]!;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parsePackInfo(reader: HeaderReader, streams: SevenZipStreams, options: FormatReadOptions): void {
|
|
217
|
+
streams.packPosition = reader.readNumber("pack position");
|
|
218
|
+
const count = reader.readNumber("pack stream count");
|
|
219
|
+
if (count > options.limits.maxEntries) throw new ArchiveError("7z has too many packed streams");
|
|
220
|
+
for (;;) {
|
|
221
|
+
const id = reader.readNumber("PackInfo property ID");
|
|
222
|
+
if (id === 0) break;
|
|
223
|
+
if (id === 9) {
|
|
224
|
+
if (streams.packSizes.length !== 0) throw new ArchiveError("Invalid duplicate 7z pack sizes");
|
|
225
|
+
for (let index = 0; index < count; index++) streams.packSizes.push(reader.readNumber("packed stream size"));
|
|
226
|
+
} else if (id === 10) {
|
|
227
|
+
if (streams.packCrcs.length !== 0) throw new ArchiveError("Invalid duplicate 7z pack CRCs");
|
|
228
|
+
streams.packCrcs = readDigests(reader, count).values;
|
|
229
|
+
} else reader.skipSizedProperty();
|
|
230
|
+
}
|
|
231
|
+
if (streams.packSizes.length !== count) throw new ArchiveError("Invalid 7z PackInfo without complete sizes");
|
|
232
|
+
if (streams.packCrcs.length === 0) streams.packCrcs = new Array(count).fill(undefined);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseUnpackInfo(reader: HeaderReader, streams: SevenZipStreams, options: FormatReadOptions): void {
|
|
236
|
+
expectId(reader, 11, "Folder");
|
|
237
|
+
const count = reader.readNumber("folder count");
|
|
238
|
+
if (count > options.limits.maxEntries) throw new ArchiveError("7z has too many folders");
|
|
239
|
+
if (reader.readByte() !== 0) throw new ArchiveError("Unsupported 7z external folder definitions");
|
|
240
|
+
for (let index = 0; index < count; index++) streams.folders.push(parseFolder(reader, options));
|
|
241
|
+
expectId(reader, 12, "CodersUnpackSize");
|
|
242
|
+
for (const folder of streams.folders) {
|
|
243
|
+
const outputCount = folder.coders.reduce((sum, coder) => sum + coder.numOutputs, 0);
|
|
244
|
+
for (let index = 0; index < outputCount; index++) folder.unpackSizes.push(reader.readNumber("coder unpack size"));
|
|
245
|
+
}
|
|
246
|
+
for (;;) {
|
|
247
|
+
const id = reader.readNumber("UnpackInfo property ID");
|
|
248
|
+
if (id === 0) break;
|
|
249
|
+
if (id === 10) {
|
|
250
|
+
const digests = readDigests(reader, count);
|
|
251
|
+
for (let index = 0; index < count; index++) streams.folders[index]!.crc = digests.values[index];
|
|
252
|
+
} else reader.skipSizedProperty();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function parseSubStreamsInfo(reader: HeaderReader, streams: SevenZipStreams): void {
|
|
257
|
+
const counts = new Array<number>(streams.folders.length).fill(1);
|
|
258
|
+
let sizes: number[] | undefined;
|
|
259
|
+
let rawDigests: Digests | undefined;
|
|
260
|
+
for (;;) {
|
|
261
|
+
const id = reader.readNumber("SubStreamsInfo property ID");
|
|
262
|
+
if (id === 0) break;
|
|
263
|
+
if (id === 13) {
|
|
264
|
+
for (let index = 0; index < counts.length; index++)
|
|
265
|
+
counts[index] = reader.readNumber("folder substream count");
|
|
266
|
+
} else if (id === 9) {
|
|
267
|
+
sizes = [];
|
|
268
|
+
for (let folderIndex = 0; folderIndex < streams.folders.length; folderIndex++) {
|
|
269
|
+
for (let index = 1; index < counts[folderIndex]!; index++) sizes.push(reader.readNumber("substream size"));
|
|
270
|
+
}
|
|
271
|
+
} else if (id === 10) {
|
|
272
|
+
let digestCount = 0;
|
|
273
|
+
for (let index = 0; index < counts.length; index++)
|
|
274
|
+
if (counts[index] !== 1 || streams.folders[index]!.crc === undefined) digestCount += counts[index]!;
|
|
275
|
+
rawDigests = readDigests(reader, digestCount);
|
|
276
|
+
} else reader.skipSizedProperty();
|
|
277
|
+
}
|
|
278
|
+
let sizeIndex = 0;
|
|
279
|
+
let digestIndex = 0;
|
|
280
|
+
for (let folderIndex = 0; folderIndex < streams.folders.length; folderIndex++) {
|
|
281
|
+
const folder = streams.folders[folderIndex]!;
|
|
282
|
+
const count = counts[folderIndex]!;
|
|
283
|
+
let offset = 0;
|
|
284
|
+
const folderSize = folder.unpackSizes[finalFolderOutput(folder)]!;
|
|
285
|
+
for (let index = 0; index < count; index++) {
|
|
286
|
+
const size =
|
|
287
|
+
index + 1 === count
|
|
288
|
+
? folderSize - offset
|
|
289
|
+
: (sizes?.[sizeIndex++] ??
|
|
290
|
+
(() => {
|
|
291
|
+
throw new ArchiveError("Invalid 7z SubStreamsInfo without sizes");
|
|
292
|
+
})());
|
|
293
|
+
if (size < 0 || offset + size > folderSize) throw new ArchiveError("Invalid 7z substream size total");
|
|
294
|
+
let checksum: number | undefined;
|
|
295
|
+
if (count === 1 && folder.crc !== undefined) checksum = folder.crc;
|
|
296
|
+
else checksum = rawDigests?.values[digestIndex++];
|
|
297
|
+
streams.substreams.push({ folderIndex, offset, size, crc: checksum });
|
|
298
|
+
offset += size;
|
|
299
|
+
}
|
|
300
|
+
if (offset !== folderSize) throw new ArchiveError("Invalid 7z substream sizes");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function assignPackStreams(streams: SevenZipStreams, sourceSize: number): void {
|
|
305
|
+
let absolute = 32 + streams.packPosition;
|
|
306
|
+
const offsets: number[] = [];
|
|
307
|
+
for (const size of streams.packSizes) {
|
|
308
|
+
if (!Number.isSafeInteger(absolute) || absolute < 0 || size < 0 || absolute + size > sourceSize)
|
|
309
|
+
throw new ArchiveError("Invalid 7z packed-stream range");
|
|
310
|
+
offsets.push(absolute);
|
|
311
|
+
absolute += size;
|
|
312
|
+
}
|
|
313
|
+
let packIndex = 0;
|
|
314
|
+
for (const folder of streams.folders) {
|
|
315
|
+
for (let index = 0; index < folder.packedIndices.length; index++) {
|
|
316
|
+
if (packIndex >= streams.packSizes.length) throw new ArchiveError("Invalid 7z folder-to-pack-stream mapping");
|
|
317
|
+
folder.packOffsets.push(offsets[packIndex]!);
|
|
318
|
+
folder.packSizes.push(streams.packSizes[packIndex]!);
|
|
319
|
+
folder.packCrcs.push(streams.packCrcs[packIndex]);
|
|
320
|
+
packIndex++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (packIndex !== streams.packSizes.length) throw new ArchiveError("Invalid unused 7z packed streams");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function parseStreamsInfo(reader: HeaderReader, sourceSize: number, options: FormatReadOptions): SevenZipStreams {
|
|
327
|
+
const streams: SevenZipStreams = { packPosition: 0, packSizes: [], packCrcs: [], folders: [], substreams: [] };
|
|
328
|
+
for (;;) {
|
|
329
|
+
const id = reader.readNumber("StreamsInfo property ID");
|
|
330
|
+
if (id === 0) break;
|
|
331
|
+
if (id === 6) parsePackInfo(reader, streams, options);
|
|
332
|
+
else if (id === 7) parseUnpackInfo(reader, streams, options);
|
|
333
|
+
else if (id === 8) parseSubStreamsInfo(reader, streams);
|
|
334
|
+
else throw new ArchiveError(`Unsupported 7z StreamsInfo property ID 0x${id.toString(16)}`);
|
|
335
|
+
}
|
|
336
|
+
if (streams.folders.length > 0 && streams.substreams.length === 0) {
|
|
337
|
+
for (let index = 0; index < streams.folders.length; index++) {
|
|
338
|
+
const folder = streams.folders[index]!;
|
|
339
|
+
streams.substreams.push({
|
|
340
|
+
folderIndex: index,
|
|
341
|
+
offset: 0,
|
|
342
|
+
size: folder.unpackSizes[finalFolderOutput(folder)]!,
|
|
343
|
+
crc: folder.crc,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
assignPackStreams(streams, sourceSize);
|
|
348
|
+
return streams;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function decodeMetadataStreams(
|
|
352
|
+
source: ByteSource,
|
|
353
|
+
streams: SevenZipStreams,
|
|
354
|
+
options: FormatReadOptions,
|
|
355
|
+
): Promise<Uint8Array[]> {
|
|
356
|
+
const buffers: Uint8Array[] = [];
|
|
357
|
+
for (const folder of streams.folders) {
|
|
358
|
+
const size = folder.unpackSizes[finalFolderOutput(folder)]!;
|
|
359
|
+
assertIndexSize(size, options.limits, "decoded header");
|
|
360
|
+
assertInMemorySize(size, options.limits);
|
|
361
|
+
buffers.push(await new SevenZipFolderSource(source, folder, options.limits).read());
|
|
362
|
+
}
|
|
363
|
+
return buffers;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function selectPropertyStream(reader: HeaderReader, external: Uint8Array[]): HeaderReader {
|
|
367
|
+
const isExternal = reader.readByte();
|
|
368
|
+
if (isExternal === 0) return reader;
|
|
369
|
+
if (isExternal !== 1) throw new ArchiveError("Invalid 7z external-property flag");
|
|
370
|
+
const index = reader.readNumber("external property stream index");
|
|
371
|
+
const bytes = external[index];
|
|
372
|
+
if (!bytes) throw new ArchiveError("Invalid 7z external property stream index");
|
|
373
|
+
return new HeaderReader(bytes);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function parseNames(reader: HeaderReader, count: number, external: Uint8Array[], maxPathBytes: number): string[] {
|
|
377
|
+
const values = selectPropertyStream(reader, external);
|
|
378
|
+
const names: string[] = [];
|
|
379
|
+
for (let index = 0; index < count; index++) {
|
|
380
|
+
const start = values.pos;
|
|
381
|
+
while (values.remaining >= 2 && (values.bytes[values.pos] !== 0 || values.bytes[values.pos + 1] !== 0))
|
|
382
|
+
values.pos += 2;
|
|
383
|
+
assertArchivePathBytes(values.pos - start, "encoded member path", maxPathBytes * 2);
|
|
384
|
+
if (values.remaining < 2) throw new ArchiveError("Invalid 7z UTF-16 file-name table");
|
|
385
|
+
let name: string;
|
|
386
|
+
try {
|
|
387
|
+
name = UTF16_LE.decode(values.bytes.subarray(start, values.pos));
|
|
388
|
+
} catch {
|
|
389
|
+
throw new ArchiveError("Invalid 7z UTF-16 file name");
|
|
390
|
+
}
|
|
391
|
+
values.pos += 2;
|
|
392
|
+
assertArchivePathString(name, "member path", maxPathBytes);
|
|
393
|
+
names.push(name);
|
|
394
|
+
}
|
|
395
|
+
return names;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function parseTimes(reader: HeaderReader, count: number, external: Uint8Array[]): Array<number | undefined> {
|
|
399
|
+
const defined = readDefinedVector(reader, count);
|
|
400
|
+
const values = selectPropertyStream(reader, external);
|
|
401
|
+
const result = new Array<number | undefined>(count);
|
|
402
|
+
for (let index = 0; index < count; index++)
|
|
403
|
+
if (defined[index]) {
|
|
404
|
+
const ticks = values.readUInt64BigInt();
|
|
405
|
+
const milliseconds = Number(ticks / 10_000n) - FILETIME_EPOCH_MS;
|
|
406
|
+
if (Number.isFinite(milliseconds)) result[index] = milliseconds;
|
|
407
|
+
}
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function parseAttributes(reader: HeaderReader, count: number, external: Uint8Array[]): Array<number | undefined> {
|
|
412
|
+
const defined = readDefinedVector(reader, count);
|
|
413
|
+
const values = selectPropertyStream(reader, external);
|
|
414
|
+
const result = new Array<number | undefined>(count);
|
|
415
|
+
for (let index = 0; index < count; index++) if (defined[index]) result[index] = values.readUInt32();
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function parseFilesInfo(reader: HeaderReader, external: Uint8Array[], options: FormatReadOptions): FileMetadata {
|
|
420
|
+
const count = reader.readNumber("file count");
|
|
421
|
+
assertEntryCount(count, options.limits);
|
|
422
|
+
const metadata: FileMetadata = {
|
|
423
|
+
names: new Array<string>(count).fill(""),
|
|
424
|
+
emptyStreams: new Array<boolean>(count).fill(false),
|
|
425
|
+
emptyFiles: [],
|
|
426
|
+
antiFiles: [],
|
|
427
|
+
mtimes: new Array<number | undefined>(count),
|
|
428
|
+
attributes: new Array<number | undefined>(count),
|
|
429
|
+
};
|
|
430
|
+
for (;;) {
|
|
431
|
+
const id = reader.readNumber("FilesInfo property ID");
|
|
432
|
+
if (id === 0) break;
|
|
433
|
+
const size = reader.readNumber("FilesInfo property size");
|
|
434
|
+
assertIndexSize(size, options.limits, "file property");
|
|
435
|
+
const property = new HeaderReader(reader.readBytes(size));
|
|
436
|
+
if (id === 14) metadata.emptyStreams = readBoolVector(property, count);
|
|
437
|
+
else if (id === 15) metadata.emptyFiles = readBoolVector(property, metadata.emptyStreams.filter(Boolean).length);
|
|
438
|
+
else if (id === 16) metadata.antiFiles = readBoolVector(property, metadata.emptyStreams.filter(Boolean).length);
|
|
439
|
+
else if (id === 17) metadata.names = parseNames(property, count, external, options.limits.maxPathBytes);
|
|
440
|
+
else if (id === 20) metadata.mtimes = parseTimes(property, count, external);
|
|
441
|
+
else if (id === 21) metadata.attributes = parseAttributes(property, count, external);
|
|
442
|
+
else if (id === 25)
|
|
443
|
+
while (property.remaining > 0)
|
|
444
|
+
if (property.readByte() !== 0) throw new ArchiveError("Invalid non-zero 7z dummy property");
|
|
445
|
+
else property.pos = property.bytes.byteLength;
|
|
446
|
+
if (property.remaining !== 0) throw new ArchiveError(`Invalid 7z file property 0x${id.toString(16)} length`);
|
|
447
|
+
}
|
|
448
|
+
return metadata;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function parseHeader(
|
|
452
|
+
source: ByteSource,
|
|
453
|
+
bytes: Uint8Array,
|
|
454
|
+
options: FormatReadOptions,
|
|
455
|
+
): Promise<{ streams: SevenZipStreams; files: FileMetadata }> {
|
|
456
|
+
const reader = new HeaderReader(bytes);
|
|
457
|
+
expectId(reader, 1, "Header");
|
|
458
|
+
let additional: Uint8Array[] = [];
|
|
459
|
+
let streams: SevenZipStreams | undefined;
|
|
460
|
+
let files: FileMetadata | undefined;
|
|
461
|
+
for (;;) {
|
|
462
|
+
const id = reader.readNumber("Header property ID");
|
|
463
|
+
if (id === 0) break;
|
|
464
|
+
if (id === 2) {
|
|
465
|
+
for (;;) {
|
|
466
|
+
const propertyId = reader.readNumber("archive property ID");
|
|
467
|
+
if (propertyId === 0) break;
|
|
468
|
+
reader.skipSizedProperty();
|
|
469
|
+
}
|
|
470
|
+
} else if (id === 3)
|
|
471
|
+
additional = await decodeMetadataStreams(source, parseStreamsInfo(reader, source.size, options), options);
|
|
472
|
+
else if (id === 4) streams = parseStreamsInfo(reader, source.size, options);
|
|
473
|
+
else if (id === 5) files = parseFilesInfo(reader, additional, options);
|
|
474
|
+
else throw new ArchiveError(`Unsupported 7z header property ID 0x${id.toString(16)}`);
|
|
475
|
+
}
|
|
476
|
+
if (reader.remaining !== 0) throw new ArchiveError("Invalid 7z trailing header data");
|
|
477
|
+
if (!streams) streams = { packPosition: 0, packSizes: [], packCrcs: [], folders: [], substreams: [] };
|
|
478
|
+
if (!files) files = { names: [], emptyStreams: [], emptyFiles: [], antiFiles: [], mtimes: [], attributes: [] };
|
|
479
|
+
return { streams, files };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
class SevenZipMemberSource implements MemberSource {
|
|
483
|
+
readonly #folder: SevenZipFolderSource;
|
|
484
|
+
readonly #offset: number;
|
|
485
|
+
readonly #size: number;
|
|
486
|
+
readonly #crc?: number;
|
|
487
|
+
|
|
488
|
+
constructor(folder: SevenZipFolderSource, stream: SevenZipSubstream) {
|
|
489
|
+
this.#folder = folder;
|
|
490
|
+
this.#offset = stream.offset;
|
|
491
|
+
this.#size = stream.size;
|
|
492
|
+
this.#crc = stream.crc;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async read(size: number, memberPath: string): Promise<Uint8Array> {
|
|
496
|
+
if (size !== this.#size)
|
|
497
|
+
throw new ArchiveError(`7z member '${memberPath}' declared size changed during extraction`);
|
|
498
|
+
const folder = await this.#folder.read();
|
|
499
|
+
if (this.#offset < 0 || this.#offset + size > folder.byteLength)
|
|
500
|
+
throw new ArchiveError(`Invalid 7z member '${memberPath}' folder range`);
|
|
501
|
+
const bytes = folder.subarray(this.#offset, this.#offset + size);
|
|
502
|
+
if (this.#crc !== undefined && crc32(bytes) !== this.#crc)
|
|
503
|
+
throw new ArchiveError(`Invalid 7z member '${memberPath}' CRC32 mismatch`);
|
|
504
|
+
return bytes.slice();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
class EmptyMemberSource implements MemberSource {
|
|
509
|
+
async read(size: number, memberPath: string): Promise<Uint8Array> {
|
|
510
|
+
if (size !== 0) throw new ArchiveError(`Invalid empty 7z member '${memberPath}' size`);
|
|
511
|
+
return new Uint8Array(0);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const EMPTY_MEMBER_SOURCE = new EmptyMemberSource();
|
|
516
|
+
|
|
517
|
+
function canonicalLinkTarget(recordPath: string, rawTarget: string): { targetPath: string; resolveTarget: boolean } {
|
|
518
|
+
const portable = rawTarget.replace(/\\/g, "/");
|
|
519
|
+
if (portable.startsWith("/")) return { targetPath: portable, resolveTarget: false };
|
|
520
|
+
const parts = recordPath.split("/");
|
|
521
|
+
parts.pop();
|
|
522
|
+
for (const part of portable.split("/")) {
|
|
523
|
+
if (!part || part === ".") continue;
|
|
524
|
+
if (part === "..") {
|
|
525
|
+
if (parts.length === 0) return { targetPath: portable, resolveTarget: false };
|
|
526
|
+
parts.pop();
|
|
527
|
+
} else parts.push(part);
|
|
528
|
+
}
|
|
529
|
+
return { targetPath: parts.join("/"), resolveTarget: true };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function buildEntries(
|
|
533
|
+
source: ByteSource,
|
|
534
|
+
streams: SevenZipStreams,
|
|
535
|
+
files: FileMetadata,
|
|
536
|
+
options: FormatReadOptions,
|
|
537
|
+
): Promise<ArchiveIndexEntry[]> {
|
|
538
|
+
const folderSources = streams.folders.map(folder => new SevenZipFolderSource(source, folder, options.limits));
|
|
539
|
+
const entries: ArchiveIndexEntry[] = [];
|
|
540
|
+
let emptyIndex = 0;
|
|
541
|
+
let streamIndex = 0;
|
|
542
|
+
for (let fileIndex = 0; fileIndex < files.names.length; fileIndex++) {
|
|
543
|
+
const rawPath = files.names[fileIndex]!;
|
|
544
|
+
const path = normalizeArchiveEntryPath(rawPath);
|
|
545
|
+
const emptyStream = files.emptyStreams[fileIndex] ?? false;
|
|
546
|
+
const emptyFile = emptyStream ? (files.emptyFiles[emptyIndex] ?? false) : false;
|
|
547
|
+
const anti = emptyStream ? (files.antiFiles[emptyIndex] ?? false) : false;
|
|
548
|
+
if (emptyStream) emptyIndex++;
|
|
549
|
+
const stream = emptyStream ? undefined : streams.substreams[streamIndex++];
|
|
550
|
+
if (!emptyStream && !stream) throw new ArchiveError("Invalid 7z file-to-substream mapping");
|
|
551
|
+
if (!path || anti) continue;
|
|
552
|
+
const attribute = files.attributes[fileIndex];
|
|
553
|
+
const mode = attribute !== undefined && attribute >>> 16 !== 0 ? attribute >>> 16 : undefined;
|
|
554
|
+
const isSymlink = mode !== undefined && (mode & 0o170000) === 0o120000;
|
|
555
|
+
const isDirectory =
|
|
556
|
+
(emptyStream && !emptyFile) ||
|
|
557
|
+
(attribute !== undefined && (attribute & 0x10) !== 0) ||
|
|
558
|
+
(mode !== undefined && (mode & 0o170000) === 0o040000);
|
|
559
|
+
const size = stream?.size ?? 0;
|
|
560
|
+
assertArchiveMemberSize(size, path, options.limits);
|
|
561
|
+
const entry: ArchiveIndexEntry = { path, isDirectory, size, mtimeMs: files.mtimes[fileIndex], mode };
|
|
562
|
+
if (isSymlink && stream) {
|
|
563
|
+
assertArchivePathBytes(size, "link target", options.limits.maxPathBytes);
|
|
564
|
+
const member = new SevenZipMemberSource(folderSources[stream.folderIndex]!, stream);
|
|
565
|
+
let target: string;
|
|
566
|
+
try {
|
|
567
|
+
target = UTF8.decode(await member.read(size, path));
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof ArchiveError) throw error;
|
|
570
|
+
throw new ArchiveError(`Invalid UTF-8 symlink target in 7z member '${path}'`);
|
|
571
|
+
}
|
|
572
|
+
assertArchivePathString(target, "link target", options.limits.maxPathBytes);
|
|
573
|
+
const canonical = canonicalLinkTarget(path, target);
|
|
574
|
+
entry.storage = { type: "link", targetPath: canonical.targetPath, resolveTarget: canonical.resolveTarget };
|
|
575
|
+
} else if (!isDirectory) {
|
|
576
|
+
entry.storage = stream
|
|
577
|
+
? { type: "member", source: new SevenZipMemberSource(folderSources[stream.folderIndex]!, stream) }
|
|
578
|
+
: { type: "member", source: EMPTY_MEMBER_SOURCE };
|
|
579
|
+
}
|
|
580
|
+
entries.push(entry);
|
|
581
|
+
assertEntryCount(entries.length, options.limits);
|
|
582
|
+
}
|
|
583
|
+
if (streamIndex !== streams.substreams.length) throw new ArchiveError("Invalid unused 7z substreams");
|
|
584
|
+
return entries;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Whether bytes begin with the canonical 7z signature. */
|
|
588
|
+
export function sniffSevenZip(bytes: Uint8Array): boolean {
|
|
589
|
+
if (bytes.byteLength < SIGNATURE.byteLength) return false;
|
|
590
|
+
for (let index = 0; index < SIGNATURE.byteLength; index++) if (bytes[index] !== SIGNATURE[index]) return false;
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Index a 7z archive and lazily decode member folders on extraction. */
|
|
595
|
+
export const readSevenZip: FormatReader = async (source, options) => {
|
|
596
|
+
try {
|
|
597
|
+
if (source.size < 32) throw new ArchiveError("Invalid 7z archive: truncated signature header");
|
|
598
|
+
const signatureHeader = await source.read(0, 32);
|
|
599
|
+
if (!sniffSevenZip(signatureHeader)) throw new ArchiveError("Invalid 7z archive signature");
|
|
600
|
+
if (signatureHeader[6] !== 0) throw new ArchiveError(`Unsupported 7z major version ${signatureHeader[6]}`);
|
|
601
|
+
if (crc32(signatureHeader.subarray(12, 32)) !== new HeaderReader(signatureHeader.subarray(8, 12)).readUInt32())
|
|
602
|
+
throw new ArchiveError("Invalid 7z start-header CRC32 mismatch");
|
|
603
|
+
const start = new HeaderReader(signatureHeader.subarray(12));
|
|
604
|
+
const nextOffset = start.readUInt64BigInt();
|
|
605
|
+
const nextSize = start.readUInt64BigInt();
|
|
606
|
+
const nextCrc = start.readUInt32();
|
|
607
|
+
if (nextOffset > BigInt(Number.MAX_SAFE_INTEGER) || nextSize > BigInt(Number.MAX_SAFE_INTEGER))
|
|
608
|
+
throw new ArchiveError("7z header offsets or sizes are too large to read safely");
|
|
609
|
+
const headerStart = 32 + Number(nextOffset);
|
|
610
|
+
const headerSize = Number(nextSize);
|
|
611
|
+
assertIndexSize(headerSize, options.limits, "next header");
|
|
612
|
+
if (!Number.isSafeInteger(headerStart) || headerStart < 32 || headerStart + headerSize > source.size)
|
|
613
|
+
throw new ArchiveError("Invalid 7z next-header range");
|
|
614
|
+
let header = await source.read(headerStart, headerStart + headerSize);
|
|
615
|
+
if (crc32(header) !== nextCrc) throw new ArchiveError("Invalid 7z next-header CRC32 mismatch");
|
|
616
|
+
const first = new HeaderReader(header).readNumber("next-header ID");
|
|
617
|
+
if (first === 23) {
|
|
618
|
+
const encodedReader = new HeaderReader(header);
|
|
619
|
+
encodedReader.readNumber();
|
|
620
|
+
const encodedStreams = parseStreamsInfo(encodedReader, source.size, options);
|
|
621
|
+
if (encodedReader.remaining !== 0) throw new ArchiveError("Invalid 7z encoded-header trailing data");
|
|
622
|
+
const decoded = await decodeMetadataStreams(source, encodedStreams, options);
|
|
623
|
+
if (decoded.length !== 1) throw new ArchiveError("Invalid 7z encoded header folder count");
|
|
624
|
+
header = decoded[0]!;
|
|
625
|
+
if (new HeaderReader(header).readNumber("decoded-header ID") !== 1)
|
|
626
|
+
throw new ArchiveError("Invalid decoded 7z header marker");
|
|
627
|
+
} else if (first !== 1) throw new ArchiveError(`Unsupported 7z next-header ID 0x${first.toString(16)}`);
|
|
628
|
+
const parsed = await parseHeader(source, header, options);
|
|
629
|
+
return await buildEntries(source, parsed.streams, parsed.files, options);
|
|
630
|
+
} catch (error) {
|
|
631
|
+
if (error instanceof ArchiveError) throw error;
|
|
632
|
+
throw new ArchiveError(`Invalid 7z archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
633
|
+
}
|
|
634
|
+
};
|