@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
package/src/ar/cab.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import * as zlib from "node:zlib";
|
|
2
|
+
import { readUInt16LE, readUInt32LE } from "./bytes";
|
|
3
|
+
import { LzxDecoder } from "./codecs/lzx";
|
|
4
|
+
import { ArchiveError } from "./error";
|
|
5
|
+
import {
|
|
6
|
+
type ArchiveLimits,
|
|
7
|
+
assertArchiveMemberSize,
|
|
8
|
+
assertEntryCount,
|
|
9
|
+
assertIndexSize,
|
|
10
|
+
assertInMemorySize,
|
|
11
|
+
} from "./limits";
|
|
12
|
+
import { assertArchivePathBytes, normalizeArchiveEntryPath } from "./paths";
|
|
13
|
+
import type { ByteSource } from "./source";
|
|
14
|
+
import type { ArchiveIndexEntry, FormatReader, MemberSource } from "./types";
|
|
15
|
+
|
|
16
|
+
const CAB_SIGNATURE = "MSCF";
|
|
17
|
+
const FIXED_HEADER_SIZE = 36;
|
|
18
|
+
const DATA_BLOCK_SIZE = 8;
|
|
19
|
+
const MAX_DATA_OUTPUT = 32 * 1024;
|
|
20
|
+
const ATTRIBUTE_READ_ONLY = 0x01;
|
|
21
|
+
const ATTRIBUTE_DIRECTORY = 0x10;
|
|
22
|
+
const ATTRIBUTE_EXECUTE = 0x40;
|
|
23
|
+
const ATTRIBUTE_UTF8_NAME = 0x80;
|
|
24
|
+
const UTF8_FATAL_DECODER = new TextDecoder("utf-8", { fatal: true });
|
|
25
|
+
const LEGACY_NAME_DECODER = new TextDecoder("windows-1252");
|
|
26
|
+
|
|
27
|
+
interface CabFolderDescription {
|
|
28
|
+
dataStart: number;
|
|
29
|
+
dataEnd: number;
|
|
30
|
+
blockCount: number;
|
|
31
|
+
method: number;
|
|
32
|
+
parameter: number;
|
|
33
|
+
requiredSize: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readExact(
|
|
37
|
+
source: ByteSource,
|
|
38
|
+
start: number,
|
|
39
|
+
end: number,
|
|
40
|
+
cabinetSize = source.size,
|
|
41
|
+
): Promise<Uint8Array> {
|
|
42
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > cabinetSize) {
|
|
43
|
+
throw new ArchiveError("Invalid CAB archive: metadata range is out of bounds");
|
|
44
|
+
}
|
|
45
|
+
let bytes: Uint8Array;
|
|
46
|
+
try {
|
|
47
|
+
bytes = await source.read(start, end);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof ArchiveError) throw error;
|
|
50
|
+
throw new ArchiveError(`Unable to read CAB archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
if (bytes.byteLength !== end - start) throw new ArchiveError("Invalid CAB archive: truncated data");
|
|
53
|
+
return bytes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasSignature(bytes: Uint8Array): boolean {
|
|
57
|
+
return bytes.byteLength >= 4 && bytes[0] === 0x4d && bytes[1] === 0x53 && bytes[2] === 0x43 && bytes[3] === 0x46;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cabChecksum(bytes: Uint8Array, initial = 0): number {
|
|
61
|
+
let checksum = initial >>> 0;
|
|
62
|
+
let offset = 0;
|
|
63
|
+
while (offset + 4 <= bytes.byteLength) {
|
|
64
|
+
checksum ^= readUInt32LE(bytes, offset);
|
|
65
|
+
offset += 4;
|
|
66
|
+
}
|
|
67
|
+
const remaining = bytes.byteLength - offset;
|
|
68
|
+
let remainder = 0;
|
|
69
|
+
if (remaining === 3) {
|
|
70
|
+
remainder = (bytes[offset]! << 16) | (bytes[offset + 1]! << 8) | bytes[offset + 2]!;
|
|
71
|
+
} else if (remaining === 2) {
|
|
72
|
+
remainder = (bytes[offset]! << 8) | bytes[offset + 1]!;
|
|
73
|
+
} else if (remaining === 1) {
|
|
74
|
+
remainder = bytes[offset]!;
|
|
75
|
+
}
|
|
76
|
+
return (checksum ^ remainder) >>> 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function decodeName(bytes: Uint8Array, utf8: boolean): string {
|
|
80
|
+
try {
|
|
81
|
+
return utf8 ? UTF8_FATAL_DECODER.decode(bytes) : LEGACY_NAME_DECODER.decode(bytes);
|
|
82
|
+
} catch {
|
|
83
|
+
throw new ArchiveError("Invalid CAB archive: file name is not valid UTF-8");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function dosTimestamp(date: number, time: number): number | undefined {
|
|
88
|
+
if (date === 0 && time === 0) return undefined;
|
|
89
|
+
const year = 1980 + (date >>> 9);
|
|
90
|
+
const month = (date >>> 5) & 0x0f;
|
|
91
|
+
const day = date & 0x1f;
|
|
92
|
+
const hour = time >>> 11;
|
|
93
|
+
const minute = (time >>> 5) & 0x3f;
|
|
94
|
+
const second = (time & 0x1f) * 2;
|
|
95
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) {
|
|
96
|
+
throw new ArchiveError("Invalid CAB archive: file has an invalid DOS timestamp");
|
|
97
|
+
}
|
|
98
|
+
return new Date(year, month - 1, day, hour, minute, second).getTime();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function modeFromAttributes(attributes: number, directory: boolean): number {
|
|
102
|
+
if (directory) return 0o040755;
|
|
103
|
+
let permissions = attributes & ATTRIBUTE_READ_ONLY ? 0o444 : 0o644;
|
|
104
|
+
if (attributes & ATTRIBUTE_EXECUTE) permissions |= 0o111;
|
|
105
|
+
return 0o100000 | permissions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class CabFolder {
|
|
109
|
+
readonly #source: ByteSource;
|
|
110
|
+
readonly #description: CabFolderDescription;
|
|
111
|
+
readonly #dataReserveSize: number;
|
|
112
|
+
readonly #limits: ArchiveLimits;
|
|
113
|
+
#decoded?: Promise<Uint8Array>;
|
|
114
|
+
|
|
115
|
+
constructor(source: ByteSource, description: CabFolderDescription, dataReserveSize: number, limits: ArchiveLimits) {
|
|
116
|
+
this.#source = source;
|
|
117
|
+
this.#description = description;
|
|
118
|
+
this.#dataReserveSize = dataReserveSize;
|
|
119
|
+
this.#limits = limits;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
readAll(): Promise<Uint8Array> {
|
|
123
|
+
this.#decoded ??= this.#decode();
|
|
124
|
+
return this.#decoded;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async #decode(): Promise<Uint8Array> {
|
|
128
|
+
const description = this.#description;
|
|
129
|
+
if (description.method === 2) {
|
|
130
|
+
throw new ArchiveError(`Unsupported CAB compression method: Quantum (level ${description.parameter})`);
|
|
131
|
+
}
|
|
132
|
+
if (description.method > 3) {
|
|
133
|
+
throw new ArchiveError(`Unsupported CAB compression method: ${description.method}`);
|
|
134
|
+
}
|
|
135
|
+
if (description.method === 3 && (description.parameter < 15 || description.parameter > 21)) {
|
|
136
|
+
throw new ArchiveError(`Unsupported CAB LZX window size: ${description.parameter} bits (expected 15-21)`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const compressedSize = description.dataEnd - description.dataStart;
|
|
140
|
+
assertInMemorySize(compressedSize, this.#limits);
|
|
141
|
+
const bytes = await readExact(this.#source, description.dataStart, description.dataEnd);
|
|
142
|
+
let position = 0;
|
|
143
|
+
let outputSize = 0;
|
|
144
|
+
for (let block = 0; block < description.blockCount; block++) {
|
|
145
|
+
if (position + DATA_BLOCK_SIZE + this.#dataReserveSize > bytes.byteLength) {
|
|
146
|
+
throw new ArchiveError("Invalid CAB archive: truncated CFDATA header");
|
|
147
|
+
}
|
|
148
|
+
const compressed = readUInt16LE(bytes, position + 4);
|
|
149
|
+
const uncompressed = readUInt16LE(bytes, position + 6);
|
|
150
|
+
if (uncompressed === 0) throw new ArchiveError("Unsupported multi-volume CAB archive: split CFDATA block");
|
|
151
|
+
if (uncompressed > MAX_DATA_OUTPUT) {
|
|
152
|
+
throw new ArchiveError(`Invalid CAB archive: CFDATA expands to ${uncompressed} bytes (maximum 32768)`);
|
|
153
|
+
}
|
|
154
|
+
const payloadStart = position + DATA_BLOCK_SIZE + this.#dataReserveSize;
|
|
155
|
+
const payloadEnd = payloadStart + compressed;
|
|
156
|
+
if (payloadEnd > bytes.byteLength) throw new ArchiveError("Invalid CAB archive: truncated CFDATA payload");
|
|
157
|
+
const expectedChecksum = readUInt32LE(bytes, position);
|
|
158
|
+
if (expectedChecksum !== 0) {
|
|
159
|
+
const payloadChecksum = cabChecksum(bytes.subarray(payloadStart, payloadEnd));
|
|
160
|
+
const actualChecksum = cabChecksum(bytes.subarray(position + 4, payloadStart), payloadChecksum);
|
|
161
|
+
if (actualChecksum !== expectedChecksum) {
|
|
162
|
+
throw new ArchiveError(`Invalid CAB archive: CFDATA block ${block} checksum mismatch`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
outputSize += uncompressed;
|
|
166
|
+
assertInMemorySize(outputSize, this.#limits);
|
|
167
|
+
position = payloadEnd;
|
|
168
|
+
}
|
|
169
|
+
if (outputSize < description.requiredSize) {
|
|
170
|
+
throw new ArchiveError("Invalid CAB archive: folder data is shorter than its file table declares");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const output = new Uint8Array(outputSize);
|
|
174
|
+
const lzx = description.method === 3 ? new LzxDecoder(description.parameter) : undefined;
|
|
175
|
+
position = 0;
|
|
176
|
+
let outputPosition = 0;
|
|
177
|
+
for (let block = 0; block < description.blockCount; block++) {
|
|
178
|
+
const compressed = readUInt16LE(bytes, position + 4);
|
|
179
|
+
const uncompressed = readUInt16LE(bytes, position + 6);
|
|
180
|
+
const payloadStart = position + DATA_BLOCK_SIZE + this.#dataReserveSize;
|
|
181
|
+
const payloadEnd = payloadStart + compressed;
|
|
182
|
+
const payload = bytes.subarray(payloadStart, payloadEnd);
|
|
183
|
+
let decoded: Uint8Array;
|
|
184
|
+
if (description.method === 0) {
|
|
185
|
+
if (compressed !== uncompressed) {
|
|
186
|
+
throw new ArchiveError("Invalid CAB archive: uncompressed CFDATA sizes do not match");
|
|
187
|
+
}
|
|
188
|
+
decoded = payload;
|
|
189
|
+
} else if (description.method === 1) {
|
|
190
|
+
if (payload.byteLength < 2 || payload[0] !== 0x43 || payload[1] !== 0x4b) {
|
|
191
|
+
throw new ArchiveError("Invalid CAB archive: MSZIP block is missing its CK signature");
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const dictionary = output.subarray(Math.max(0, outputPosition - MAX_DATA_OUTPUT), outputPosition);
|
|
195
|
+
decoded = new Uint8Array(
|
|
196
|
+
zlib.inflateRawSync(payload.subarray(2), { dictionary, maxOutputLength: uncompressed }),
|
|
197
|
+
);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
throw new ArchiveError(
|
|
200
|
+
`Invalid CAB archive: MSZIP decompression failed${error instanceof Error ? `: ${error.message}` : ""}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
decoded = lzx!.decompressFrame(payload, uncompressed);
|
|
205
|
+
}
|
|
206
|
+
if (decoded.byteLength !== uncompressed) {
|
|
207
|
+
throw new ArchiveError(
|
|
208
|
+
`Invalid CAB archive: CFDATA block ${block} produced ${decoded.byteLength} bytes, expected ${uncompressed}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
output.set(decoded, outputPosition);
|
|
212
|
+
outputPosition += decoded.byteLength;
|
|
213
|
+
position = payloadEnd;
|
|
214
|
+
}
|
|
215
|
+
return output;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class CabMemberSource implements MemberSource {
|
|
220
|
+
readonly #folder: CabFolder;
|
|
221
|
+
readonly #offset: number;
|
|
222
|
+
readonly #declaredSize: number;
|
|
223
|
+
|
|
224
|
+
constructor(folder: CabFolder, offset: number, size: number) {
|
|
225
|
+
this.#folder = folder;
|
|
226
|
+
this.#offset = offset;
|
|
227
|
+
this.#declaredSize = size;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async read(size: number, memberPath: string): Promise<Uint8Array> {
|
|
231
|
+
if (size !== this.#declaredSize) {
|
|
232
|
+
throw new ArchiveError(`Invalid CAB archive: size changed while extracting '${memberPath}'`);
|
|
233
|
+
}
|
|
234
|
+
const folder = await this.#folder.readAll();
|
|
235
|
+
const end = this.#offset + size;
|
|
236
|
+
if (!Number.isSafeInteger(end) || this.#offset < 0 || end > folder.byteLength) {
|
|
237
|
+
throw new ArchiveError(`Invalid CAB archive: member '${memberPath}' is outside its folder data`);
|
|
238
|
+
}
|
|
239
|
+
return folder.slice(this.#offset, end);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readCabArchive(source: ByteSource, options: Parameters<FormatReader>[1]): Promise<ArchiveIndexEntry[]> {
|
|
244
|
+
if (source.size < FIXED_HEADER_SIZE) throw new ArchiveError("Invalid CAB archive: truncated CFHEADER");
|
|
245
|
+
const fixed = await readExact(source, 0, FIXED_HEADER_SIZE);
|
|
246
|
+
if (!hasSignature(fixed)) throw new ArchiveError(`Invalid CAB archive: expected ${CAB_SIGNATURE} signature`);
|
|
247
|
+
if (readUInt32LE(fixed, 4) !== 0 || readUInt32LE(fixed, 12) !== 0 || readUInt32LE(fixed, 20) !== 0) {
|
|
248
|
+
throw new ArchiveError("Invalid CAB archive: reserved CFHEADER fields must be zero");
|
|
249
|
+
}
|
|
250
|
+
const cabinetSize = readUInt32LE(fixed, 8);
|
|
251
|
+
if (cabinetSize < FIXED_HEADER_SIZE || cabinetSize > source.size) {
|
|
252
|
+
throw new ArchiveError("Invalid CAB archive: declared cabinet size is out of bounds");
|
|
253
|
+
}
|
|
254
|
+
const fileTableOffset = readUInt32LE(fixed, 16);
|
|
255
|
+
if (fileTableOffset < FIXED_HEADER_SIZE || fileTableOffset > cabinetSize) {
|
|
256
|
+
throw new ArchiveError("Invalid CAB archive: CFFILE table offset is out of bounds");
|
|
257
|
+
}
|
|
258
|
+
if (fixed[24] !== 3 || fixed[25] !== 1) {
|
|
259
|
+
throw new ArchiveError(`Unsupported CAB format version ${fixed[25]}.${fixed[24]} (expected 1.3)`);
|
|
260
|
+
}
|
|
261
|
+
const folderCount = readUInt16LE(fixed, 26);
|
|
262
|
+
const fileCount = readUInt16LE(fixed, 28);
|
|
263
|
+
const flags = readUInt16LE(fixed, 30);
|
|
264
|
+
if (flags & 0x0003) throw new ArchiveError("Unsupported multi-volume CAB archive (previous/next cabinet link)");
|
|
265
|
+
assertEntryCount(folderCount + fileCount, options.limits);
|
|
266
|
+
if (folderCount === 0 && fileCount !== 0)
|
|
267
|
+
throw new ArchiveError("Invalid CAB archive: files exist without a folder");
|
|
268
|
+
|
|
269
|
+
let headerReserveSize = 0;
|
|
270
|
+
let folderReserveSize = 0;
|
|
271
|
+
let dataReserveSize = 0;
|
|
272
|
+
let folderTableOffset = FIXED_HEADER_SIZE;
|
|
273
|
+
if (flags & 0x0004) {
|
|
274
|
+
const reserveHeader = await readExact(source, FIXED_HEADER_SIZE, FIXED_HEADER_SIZE + 4, cabinetSize);
|
|
275
|
+
headerReserveSize = readUInt16LE(reserveHeader, 0);
|
|
276
|
+
folderReserveSize = reserveHeader[2]!;
|
|
277
|
+
dataReserveSize = reserveHeader[3]!;
|
|
278
|
+
if (headerReserveSize > 60_000)
|
|
279
|
+
throw new ArchiveError("Invalid CAB archive: CFHEADER reserve area exceeds 60000 bytes");
|
|
280
|
+
folderTableOffset += 4 + headerReserveSize;
|
|
281
|
+
}
|
|
282
|
+
const folderRecordSize = 8 + folderReserveSize;
|
|
283
|
+
const folderTableEnd = folderTableOffset + folderCount * folderRecordSize;
|
|
284
|
+
if (!Number.isSafeInteger(folderTableEnd) || folderTableEnd > cabinetSize || folderTableEnd > fileTableOffset) {
|
|
285
|
+
throw new ArchiveError("Invalid CAB archive: CFFOLDER table is out of bounds");
|
|
286
|
+
}
|
|
287
|
+
assertIndexSize(folderTableEnd, options.limits, "CAB header");
|
|
288
|
+
const header = await readExact(source, 0, folderTableEnd, cabinetSize);
|
|
289
|
+
const descriptions: CabFolderDescription[] = [];
|
|
290
|
+
for (let index = 0; index < folderCount; index++) {
|
|
291
|
+
const offset = folderTableOffset + index * folderRecordSize;
|
|
292
|
+
const type = readUInt16LE(header, offset + 6);
|
|
293
|
+
descriptions.push({
|
|
294
|
+
dataStart: readUInt32LE(header, offset),
|
|
295
|
+
dataEnd: cabinetSize,
|
|
296
|
+
blockCount: readUInt16LE(header, offset + 4),
|
|
297
|
+
method: type & 0x000f,
|
|
298
|
+
parameter: type >>> 8,
|
|
299
|
+
requiredSize: 0,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
for (const description of descriptions) {
|
|
303
|
+
if (description.dataStart < folderTableEnd || description.dataStart > cabinetSize) {
|
|
304
|
+
throw new ArchiveError("Invalid CAB archive: CFFOLDER data offset is out of bounds");
|
|
305
|
+
}
|
|
306
|
+
for (const candidate of descriptions) {
|
|
307
|
+
if (candidate.dataStart > description.dataStart && candidate.dataStart < description.dataEnd) {
|
|
308
|
+
description.dataEnd = candidate.dataStart;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const firstDataOffset = descriptions.reduce(
|
|
313
|
+
(minimum, description) => Math.min(minimum, description.dataStart),
|
|
314
|
+
cabinetSize,
|
|
315
|
+
);
|
|
316
|
+
if (fileTableOffset > firstDataOffset)
|
|
317
|
+
throw new ArchiveError("Invalid CAB archive: CFFILE table overlaps folder data");
|
|
318
|
+
assertIndexSize(folderTableEnd + (firstDataOffset - fileTableOffset), options.limits, "CAB index");
|
|
319
|
+
const fileTable = await readExact(source, fileTableOffset, firstDataOffset, cabinetSize);
|
|
320
|
+
const folders = descriptions.map(description => new CabFolder(source, description, dataReserveSize, options.limits));
|
|
321
|
+
const entries: ArchiveIndexEntry[] = [];
|
|
322
|
+
let position = 0;
|
|
323
|
+
for (let index = 0; index < fileCount; index++) {
|
|
324
|
+
if (position + 16 > fileTable.byteLength) throw new ArchiveError("Invalid CAB archive: truncated CFFILE entry");
|
|
325
|
+
const size = readUInt32LE(fileTable, position);
|
|
326
|
+
const folderOffset = readUInt32LE(fileTable, position + 4);
|
|
327
|
+
const folderIndex = readUInt16LE(fileTable, position + 8);
|
|
328
|
+
const date = readUInt16LE(fileTable, position + 10);
|
|
329
|
+
const time = readUInt16LE(fileTable, position + 12);
|
|
330
|
+
const attributes = readUInt16LE(fileTable, position + 14);
|
|
331
|
+
position += 16;
|
|
332
|
+
const nameEnd = fileTable.indexOf(0, position);
|
|
333
|
+
if (nameEnd < 0) throw new ArchiveError("Invalid CAB archive: unterminated CFFILE name");
|
|
334
|
+
const nameBytes = fileTable.subarray(position, nameEnd);
|
|
335
|
+
if (nameBytes.byteLength > 256) throw new ArchiveError("Invalid CAB archive: CFFILE name exceeds 256 bytes");
|
|
336
|
+
assertArchivePathBytes(nameBytes.byteLength, "member path", options.limits.maxPathBytes);
|
|
337
|
+
position = nameEnd + 1;
|
|
338
|
+
if (folderIndex >= 0xfffd) throw new ArchiveError("Unsupported multi-volume CAB archive: continued file");
|
|
339
|
+
if (folderIndex >= folders.length)
|
|
340
|
+
throw new ArchiveError("Invalid CAB archive: CFFILE references a missing folder");
|
|
341
|
+
const rawName = decodeName(nameBytes, (attributes & ATTRIBUTE_UTF8_NAME) !== 0);
|
|
342
|
+
assertArchiveMemberSize(size, rawName, options.limits);
|
|
343
|
+
const end = folderOffset + size;
|
|
344
|
+
if (!Number.isSafeInteger(end)) throw new ArchiveError("Invalid CAB archive: CFFILE range is too large");
|
|
345
|
+
if (end > descriptions[folderIndex]!.requiredSize) descriptions[folderIndex]!.requiredSize = end;
|
|
346
|
+
const path = normalizeArchiveEntryPath(rawName);
|
|
347
|
+
if (!path) continue;
|
|
348
|
+
const isDirectory = (attributes & ATTRIBUTE_DIRECTORY) !== 0;
|
|
349
|
+
entries.push({
|
|
350
|
+
path,
|
|
351
|
+
isDirectory,
|
|
352
|
+
size,
|
|
353
|
+
mtimeMs: dosTimestamp(date, time),
|
|
354
|
+
mode: modeFromAttributes(attributes, isDirectory),
|
|
355
|
+
storage: isDirectory
|
|
356
|
+
? undefined
|
|
357
|
+
: { type: "member", source: new CabMemberSource(folders[folderIndex]!, folderOffset, size) },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
for (const description of descriptions) assertInMemorySize(description.requiredSize, options.limits);
|
|
361
|
+
return entries;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Probe a byte prefix for the Microsoft Cabinet `MSCF` signature. */
|
|
365
|
+
export function sniffCab(bytes: Uint8Array): boolean {
|
|
366
|
+
return hasSignature(bytes);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** Index a Microsoft Cabinet archive and defer folder decompression until a member is read. */
|
|
370
|
+
export const readCab: FormatReader = async (source, options) => {
|
|
371
|
+
try {
|
|
372
|
+
return await readCabArchive(source, options);
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof ArchiveError) throw error;
|
|
375
|
+
throw new ArchiveError(`Invalid CAB archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Shared checksum primitives for archive containers. CRC-32 (IEEE, reflected)
|
|
2
|
+
// delegates to Bun's native implementation; CRC-64/XZ and CRC-16/ARC are
|
|
3
|
+
// table-driven so format modules never hand-roll per-bit loops. Format-owned
|
|
4
|
+
// oddballs (bzip2's MSB-first CRC-32, CAB's block checksum, cpio/tar sums)
|
|
5
|
+
// stay in their modules.
|
|
6
|
+
|
|
7
|
+
/** CRC-32 (IEEE 802.3, reflected). Chainable: pass the previous value as `seed`. */
|
|
8
|
+
export function crc32(bytes: Uint8Array, seed = 0): number {
|
|
9
|
+
return Bun.hash.crc32(bytes, seed) >>> 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CRC64_LO = new Uint32Array(256);
|
|
13
|
+
const CRC64_HI = new Uint32Array(256);
|
|
14
|
+
for (let index = 0; index < 256; index++) {
|
|
15
|
+
let lo = index;
|
|
16
|
+
let hi = 0;
|
|
17
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
18
|
+
const carry = (lo & 1) !== 0;
|
|
19
|
+
lo = ((lo >>> 1) | ((hi & 1) << 31)) >>> 0;
|
|
20
|
+
hi >>>= 1;
|
|
21
|
+
if (carry) {
|
|
22
|
+
lo = (lo ^ 0xd7870f42) >>> 0;
|
|
23
|
+
hi = (hi ^ 0xc96c5795) >>> 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
CRC64_LO[index] = lo;
|
|
27
|
+
CRC64_HI[index] = hi;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* CRC-64/XZ (ECMA-182, reflected) as used by `.xz` block checks. Chainable
|
|
32
|
+
* via `seed`. State is split into 32-bit halves so the hot loop stays on
|
|
33
|
+
* fast integer paths instead of per-byte BigInt arithmetic.
|
|
34
|
+
*/
|
|
35
|
+
export function crc64(bytes: Uint8Array, seed = 0n): bigint {
|
|
36
|
+
const initial = seed ^ 0xffffffffffffffffn;
|
|
37
|
+
let lo = Number(initial & 0xffffffffn) >>> 0;
|
|
38
|
+
let hi = Number((initial >> 32n) & 0xffffffffn) >>> 0;
|
|
39
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
40
|
+
const slot = (lo ^ bytes[index]!) & 0xff;
|
|
41
|
+
const nextLo = ((lo >>> 8) | ((hi & 0xff) << 24)) >>> 0;
|
|
42
|
+
lo = (nextLo ^ CRC64_LO[slot]!) >>> 0;
|
|
43
|
+
hi = ((hi >>> 8) ^ CRC64_HI[slot]!) >>> 0;
|
|
44
|
+
}
|
|
45
|
+
return ((BigInt(hi) << 32n) | BigInt(lo)) ^ 0xffffffffffffffffn;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const CRC16_TABLE = new Uint16Array(256);
|
|
49
|
+
for (let index = 0; index < 256; index++) {
|
|
50
|
+
let value = index;
|
|
51
|
+
for (let bit = 0; bit < 8; bit++) value = (value & 1) !== 0 ? (value >>> 1) ^ 0xa001 : value >>> 1;
|
|
52
|
+
CRC16_TABLE[index] = value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** CRC-16/ARC (reflected, poly 0xA001, init 0) as used by LZH member data and ARJ. */
|
|
56
|
+
export function crc16Arc(bytes: Uint8Array, seed = 0): number {
|
|
57
|
+
let value = seed;
|
|
58
|
+
for (let index = 0; index < bytes.length; index++) {
|
|
59
|
+
value = ((value >>> 8) ^ CRC16_TABLE[(value ^ bytes[index]!) & 0xff]!) & 0xffff;
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|