@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +22 -0
  3. package/THIRD-PARTY-NOTICES.txt +22909 -0
  4. package/dist/types/ar/arj.d.ts +5 -0
  5. package/dist/types/ar/asar.d.ts +7 -0
  6. package/dist/types/ar/bytes.d.ts +20 -0
  7. package/dist/types/ar/cab.d.ts +5 -0
  8. package/dist/types/ar/checksums.d.ts +10 -0
  9. package/dist/types/ar/codecs/bzip2.d.ts +4 -0
  10. package/dist/types/ar/codecs/gzip.d.ts +6 -0
  11. package/dist/types/ar/codecs/lzma.d.ts +6 -0
  12. package/dist/types/ar/codecs/lzw.d.ts +4 -0
  13. package/dist/types/ar/codecs/lzx.d.ts +7 -0
  14. package/dist/types/ar/codecs/xz.d.ts +4 -0
  15. package/dist/types/ar/codecs/zstd.d.ts +6 -0
  16. package/dist/types/ar/cpio.d.ts +7 -0
  17. package/dist/types/ar/deb.d.ts +5 -0
  18. package/dist/types/ar/entries.d.ts +22 -0
  19. package/dist/types/ar/error.d.ts +8 -0
  20. package/dist/types/ar/index.d.ts +11 -0
  21. package/dist/types/ar/iso.d.ts +5 -0
  22. package/dist/types/ar/limits.d.ts +33 -0
  23. package/dist/types/ar/lzh.d.ts +7 -0
  24. package/dist/types/ar/open.d.ts +45 -0
  25. package/dist/types/ar/paths.d.ts +18 -0
  26. package/dist/types/ar/rar/rar4-decoder.d.ts +6 -0
  27. package/dist/types/ar/rar/rar5-decoder.d.ts +6 -0
  28. package/dist/types/ar/rar.d.ts +5 -0
  29. package/dist/types/ar/reader.d.ts +28 -0
  30. package/dist/types/ar/registry.d.ts +17 -0
  31. package/dist/types/ar/rpm.d.ts +5 -0
  32. package/dist/types/ar/sevenzip/decode.d.ts +33 -0
  33. package/dist/types/ar/sevenzip.d.ts +5 -0
  34. package/dist/types/ar/source.d.ts +54 -0
  35. package/dist/types/ar/tar.d.ts +9 -0
  36. package/dist/types/ar/types.d.ts +99 -0
  37. package/dist/types/ar/unix-ar.d.ts +7 -0
  38. package/dist/types/ar/write.d.ts +10 -0
  39. package/dist/types/ar/zip.d.ts +10 -0
  40. package/dist/types/postmortem.d.ts +33 -0
  41. package/package.json +9 -3
  42. package/src/ar/arj.ts +314 -0
  43. package/src/ar/asar.ts +480 -0
  44. package/src/ar/bytes.ts +78 -0
  45. package/src/ar/cab.ts +377 -0
  46. package/src/ar/checksums.ts +62 -0
  47. package/src/ar/codecs/bzip2.ts +489 -0
  48. package/src/ar/codecs/gzip.ts +25 -0
  49. package/src/ar/codecs/lzma.ts +437 -0
  50. package/src/ar/codecs/lzw.ts +191 -0
  51. package/src/ar/codecs/lzx.ts +366 -0
  52. package/src/ar/codecs/xz.ts +522 -0
  53. package/src/ar/codecs/zstd.ts +25 -0
  54. package/src/ar/cpio.ts +389 -0
  55. package/src/ar/deb.ts +160 -0
  56. package/src/ar/entries.ts +97 -0
  57. package/src/ar/error.ts +11 -0
  58. package/src/ar/index.ts +16 -0
  59. package/src/ar/iso.ts +712 -0
  60. package/src/ar/limits.ts +80 -0
  61. package/src/ar/lzh.ts +659 -0
  62. package/src/ar/open.ts +225 -0
  63. package/src/ar/paths.ts +70 -0
  64. package/src/ar/rar/rar4-decoder.ts +459 -0
  65. package/src/ar/rar/rar5-decoder.ts +400 -0
  66. package/src/ar/rar.ts +735 -0
  67. package/src/ar/reader.ts +162 -0
  68. package/src/ar/registry.ts +208 -0
  69. package/src/ar/rpm.ts +320 -0
  70. package/src/ar/sevenzip/decode.ts +239 -0
  71. package/src/ar/sevenzip.ts +634 -0
  72. package/src/ar/source.ts +190 -0
  73. package/src/ar/tar.ts +771 -0
  74. package/src/ar/types.ts +131 -0
  75. package/src/ar/unix-ar.ts +312 -0
  76. package/src/ar/write.ts +55 -0
  77. package/src/ar/zip.ts +719 -0
  78. package/src/browsers.ts +2 -149
  79. package/src/docx/converter.ts +9 -9
  80. package/src/postmortem.ts +74 -0
  81. package/dist/types/docx/zip.d.ts +0 -6
  82. package/src/docx/zip.ts +0 -87
package/src/ar/rar.ts ADDED
@@ -0,0 +1,735 @@
1
+ import { readUInt16LE, readUInt32LE } from "./bytes";
2
+ import { crc32 } from "./checksums";
3
+ import { ArchiveError } from "./error";
4
+ import { assertArchiveMemberSize, assertEntryCount, assertIndexSize, assertInMemorySize } from "./limits";
5
+ import { assertArchivePathBytes, normalizeArchiveEntryPath } from "./paths";
6
+ import { Rar4Decoder } from "./rar/rar4-decoder";
7
+ import { Rar5Decoder } from "./rar/rar5-decoder";
8
+ import type { ByteSource } from "./source";
9
+ import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
10
+
11
+ const RAR4_MARKER = Uint8Array.of(0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00);
12
+ const RAR5_MARKER = Uint8Array.of(0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00);
13
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
14
+ // WHATWG maps the "latin1" label to windows-1252; bun-types only names the latter.
15
+ const LATIN1 = new TextDecoder("windows-1252");
16
+
17
+ interface RarRecord {
18
+ format: 4 | 5;
19
+ path: string;
20
+ dataStart: number;
21
+ packedSize: number;
22
+ unpackedSize: number;
23
+ method: number;
24
+ version: number;
25
+ dictionarySize: number;
26
+ solid: boolean;
27
+ crc?: number;
28
+ isDirectory: boolean;
29
+ mtimeMs?: number;
30
+ mode?: number;
31
+ linkTarget?: string;
32
+ linkResolveTarget?: boolean;
33
+ }
34
+
35
+ class RarMemberSource implements MemberSource {
36
+ readonly #archive: RarArchive;
37
+ readonly #index: number;
38
+
39
+ constructor(archive: RarArchive, index: number) {
40
+ this.#archive = archive;
41
+ this.#index = index;
42
+ }
43
+
44
+ async read(size: number, memberPath: string): Promise<Uint8Array> {
45
+ return this.#archive.read(this.#index, size, memberPath);
46
+ }
47
+ }
48
+
49
+ class RarArchive {
50
+ readonly #source: ByteSource;
51
+ readonly #records: RarRecord[];
52
+ readonly #limits: FormatReadOptions["limits"];
53
+ readonly #cache = new Map<number, Uint8Array>();
54
+ #queue: Promise<void> = Promise.resolve();
55
+
56
+ constructor(source: ByteSource, records: RarRecord[], options: FormatReadOptions) {
57
+ this.#source = source;
58
+ this.#records = records;
59
+ this.#limits = options.limits;
60
+ }
61
+
62
+ member(index: number): MemberSource {
63
+ return new RarMemberSource(this, index);
64
+ }
65
+
66
+ async read(index: number, size: number, memberPath: string): Promise<Uint8Array> {
67
+ const pending = this.#queue.then(async () => {
68
+ if (!this.#cache.has(index)) await this.#decode(index);
69
+ });
70
+ this.#queue = pending.catch(() => undefined);
71
+ await pending;
72
+ const bytes = this.#cache.get(index);
73
+ if (!bytes) throw new ArchiveError(`Failed to extract RAR member '${memberPath}'`);
74
+ if (bytes.byteLength !== size) {
75
+ throw new ArchiveError(`RAR member '${memberPath}' size mismatch (${bytes.byteLength} != ${size})`);
76
+ }
77
+ return bytes.slice();
78
+ }
79
+
80
+ async #decode(index: number): Promise<void> {
81
+ const target = this.#records[index];
82
+ if (!target) throw new ArchiveError("Invalid RAR member index");
83
+ let start = index;
84
+ if (target.solid) {
85
+ while (start > 0 && this.#records[start]!.solid && this.#records[start - 1]!.format === target.format) start--;
86
+ }
87
+ const rar4Decoder = new Rar4Decoder();
88
+ const rar5Decoder = new Rar5Decoder();
89
+ for (let current = start; current <= index; current++) {
90
+ const record = this.#records[current]!;
91
+ if (record.isDirectory) continue;
92
+ assertArchiveMemberSize(record.unpackedSize, record.path, this.#limits);
93
+ if (record.method === 0 && record.packedSize !== record.unpackedSize) {
94
+ corrupt("stored member size mismatch");
95
+ }
96
+ if (record.method > 5) {
97
+ throw new ArchiveError(
98
+ record.format === 4
99
+ ? `Unsupported RAR4 compression method 0x${(record.method + 0x30).toString(16)} for '${record.path}'`
100
+ : `Unsupported RAR5 compression method ${record.method} for '${record.path}'`,
101
+ );
102
+ }
103
+ if (record.method !== 0) {
104
+ assertInMemorySize(
105
+ record.packedSize + 2 * record.unpackedSize + 2 * record.dictionarySize + 8192,
106
+ this.#limits,
107
+ );
108
+ }
109
+ const end = checkedEnd(record.dataStart, record.packedSize, this.#source.size, "member data");
110
+ const packed = await this.#source.read(record.dataStart, end);
111
+ let output: Uint8Array;
112
+ if (record.method === 0) {
113
+ output = packed;
114
+ if (record.solid) {
115
+ if (record.format === 4) rar4Decoder.reset();
116
+ else rar5Decoder.reset();
117
+ }
118
+ } else if (record.format === 5) {
119
+ output = rar5Decoder.decode(
120
+ packed,
121
+ record.unpackedSize,
122
+ record.dictionarySize,
123
+ record.solid,
124
+ record.version,
125
+ );
126
+ } else {
127
+ output = rar4Decoder.decode(
128
+ packed,
129
+ record.unpackedSize,
130
+ record.dictionarySize,
131
+ record.solid,
132
+ record.version,
133
+ );
134
+ }
135
+ if (output.byteLength !== record.unpackedSize) corrupt(`member '${record.path}' size mismatch`);
136
+ if (record.crc !== undefined && crc32(output) !== record.crc) {
137
+ throw new ArchiveError(`RAR member '${record.path}' CRC32 mismatch`);
138
+ }
139
+ this.#cache.set(current, output.slice());
140
+ }
141
+ }
142
+ }
143
+
144
+ /** Probe the RAR 1.5-4.x or RAR5 signature. */
145
+ export function sniffRar(bytes: Uint8Array): boolean {
146
+ return findMarker(bytes) !== undefined;
147
+ }
148
+
149
+ /** Index a RAR4 or RAR5 archive and defer member decompression until extraction. */
150
+ export const readRar: FormatReader = async (source, options) => {
151
+ if (!Number.isSafeInteger(source.size) || source.size < 0) {
152
+ throw new ArchiveError("Archive is too large to read safely");
153
+ }
154
+ const indexed = await indexRarMetadata(source, options);
155
+ const bytes = sparseBytes(source.size, indexed.segments);
156
+ const marker = indexed.marker;
157
+ const records =
158
+ marker.version === 5 ? parseRar5(bytes, marker.offset, options) : parseRar4(bytes, marker.offset, options);
159
+ const archive = new RarArchive(source, records, options);
160
+ const entries: ArchiveIndexEntry[] = [];
161
+ for (let index = 0; index < records.length; index++) {
162
+ const record = records[index]!;
163
+ const entry: ArchiveIndexEntry = {
164
+ path: record.path,
165
+ isDirectory: record.isDirectory,
166
+ size: record.unpackedSize,
167
+ };
168
+ if (record.mtimeMs !== undefined) entry.mtimeMs = record.mtimeMs;
169
+ if (record.mode !== undefined) entry.mode = record.mode;
170
+ if (record.linkTarget !== undefined) {
171
+ entry.storage = {
172
+ type: "link",
173
+ targetPath: record.linkTarget,
174
+ resolveTarget: record.linkResolveTarget ?? false,
175
+ };
176
+ } else if (!record.isDirectory) {
177
+ entry.storage = { type: "member", source: archive.member(index) };
178
+ }
179
+ entries.push(entry);
180
+ }
181
+ return entries;
182
+ };
183
+
184
+ interface SparseSegment {
185
+ start: number;
186
+ bytes: Uint8Array;
187
+ }
188
+
189
+ async function indexRarMetadata(
190
+ source: ByteSource,
191
+ options: FormatReadOptions,
192
+ ): Promise<{ marker: { version: 4 | 5; offset: number }; segments: SparseSegment[] }> {
193
+ let probeEnd = Math.min(source.size, RAR5_MARKER.byteLength);
194
+ let probe = await source.read(0, probeEnd);
195
+ let marker = findMarker(probe);
196
+ while (!marker && probeEnd < Math.min(source.size, 1024 * 1024 + RAR5_MARKER.byteLength)) {
197
+ const nextEnd = Math.min(source.size, 1024 * 1024 + RAR5_MARKER.byteLength, Math.max(64 * 1024, probeEnd * 2));
198
+ assertIndexSize(nextEnd, options.limits, "RAR signature scan");
199
+ const next = await source.read(probeEnd, nextEnd);
200
+ const combined = new Uint8Array(nextEnd);
201
+ combined.set(probe);
202
+ combined.set(next, probeEnd);
203
+ probe = combined;
204
+ probeEnd = nextEnd;
205
+ marker = findMarker(probe);
206
+ }
207
+ if (!marker) throw new ArchiveError("Invalid RAR archive: signature not found");
208
+ const segments: SparseSegment[] = [{ start: 0, bytes: probe }];
209
+ let metadataSize = probe.byteLength;
210
+ assertIndexSize(metadataSize, options.limits, "RAR metadata");
211
+ let offset = marker.offset + (marker.version === 5 ? RAR5_MARKER.byteLength : RAR4_MARKER.byteLength);
212
+ while (offset < source.size) {
213
+ if (marker.version === 5) {
214
+ if (offset + 5 > source.size) corrupt("truncated RAR5 header");
215
+ const prefix = await source.read(offset, Math.min(source.size, offset + 16));
216
+ const sizeCursor = { offset: 4 };
217
+ const headerSize = readVint(prefix, sizeCursor, prefix.byteLength, "header size");
218
+ if (headerSize > 2 * 1024 * 1024) corrupt("RAR5 header exceeds format limit");
219
+ const headerEnd = checkedEnd(offset, sizeCursor.offset + headerSize, source.size, "RAR5 header");
220
+ assertIndexSize(metadataSize + headerEnd - offset, options.limits, "RAR metadata");
221
+ const header = await source.read(offset, headerEnd);
222
+ segments.push({ start: offset, bytes: header });
223
+ metadataSize += header.byteLength;
224
+ assertIndexSize(metadataSize, options.limits, "RAR metadata");
225
+ const cursor = { offset: sizeCursor.offset };
226
+ const type = readVint(header, cursor, header.byteLength, "header type");
227
+ const flags = readVint(header, cursor, header.byteLength, "header flags");
228
+ if ((flags & 1) !== 0) readVint(header, cursor, header.byteLength, "extra area size");
229
+ const dataSize = (flags & 2) !== 0 ? readVint(header, cursor, header.byteLength, "data size") : 0;
230
+ offset = checkedEnd(headerEnd, dataSize, source.size, "RAR5 data area");
231
+ if (type === 4 || type === 5) break;
232
+ } else {
233
+ if (offset + 7 > source.size) corrupt("truncated RAR4 base header");
234
+ const prefix = await source.read(offset, Math.min(source.size, offset + 11));
235
+ const type = prefix[2]!;
236
+ const flags = readUInt16LE(prefix, 3);
237
+ const headerSize = readUInt16LE(prefix, 5);
238
+ if (headerSize < 7) corrupt("invalid RAR4 header size");
239
+ const headerEnd = checkedEnd(offset, headerSize, source.size, "RAR4 header");
240
+ assertIndexSize(metadataSize + headerEnd - offset, options.limits, "RAR metadata");
241
+ const header = await source.read(offset, headerEnd);
242
+ segments.push({ start: offset, bytes: header });
243
+ metadataSize += header.byteLength;
244
+ assertIndexSize(metadataSize, options.limits, "RAR metadata");
245
+ const dataSize = (flags & 0x8000) !== 0 ? readUInt32LE(header, 7) : 0;
246
+ const dataEnd = checkedEnd(headerEnd, dataSize, source.size, "RAR4 data area");
247
+ if (type === 0x74 && dataSize <= options.limits.maxPathBytes) {
248
+ const fileFields = (flags & 0x8000) !== 0 ? 11 : 7;
249
+ if (fileFields + 21 <= header.byteLength) {
250
+ const hostOs = header[fileFields + 4]!;
251
+ const attributes = readUInt32LE(header, fileFields + 17);
252
+ if (hostOs === 3 && (attributes & 0xf000) === 0xa000) {
253
+ assertIndexSize(metadataSize + dataSize, options.limits, "RAR metadata");
254
+ segments.push({ start: headerEnd, bytes: await source.read(headerEnd, dataEnd) });
255
+ metadataSize += dataSize;
256
+ assertIndexSize(metadataSize, options.limits, "RAR metadata");
257
+ }
258
+ }
259
+ }
260
+ offset = dataEnd;
261
+ if (type === 0x7b || (type === 0x73 && (flags & 0x80) !== 0)) break;
262
+ }
263
+ }
264
+ segments.sort((left, right) => left.start - right.start || right.bytes.byteLength - left.bytes.byteLength);
265
+ return { marker, segments };
266
+ }
267
+
268
+ function sparseBytes(size: number, segments: SparseSegment[]): Uint8Array {
269
+ const findSegment = (start: number, end: number): SparseSegment => {
270
+ let low = 0;
271
+ let high = segments.length;
272
+ while (low < high) {
273
+ const middle = (low + high) >>> 1;
274
+ if (segments[middle]!.start <= start) low = middle + 1;
275
+ else high = middle;
276
+ }
277
+ for (let index = low - 1; index >= 0; index--) {
278
+ const segment = segments[index]!;
279
+ if (start >= segment.start && end <= segment.start + segment.bytes.byteLength) return segment;
280
+ }
281
+ corrupt("RAR parser accessed member payload while indexing");
282
+ };
283
+ const target = Object.create(null) as Record<PropertyKey, unknown>;
284
+ const proxy = new Proxy(target, {
285
+ get(_object, property) {
286
+ if (property === "byteLength" || property === "length") return size;
287
+ if (property === "subarray") {
288
+ return (start: number, end = size): Uint8Array => {
289
+ const segment = findSegment(start, end);
290
+ return segment.bytes.subarray(start - segment.start, end - segment.start);
291
+ };
292
+ }
293
+ if (typeof property === "string") {
294
+ const offset = Number(property);
295
+ if (Number.isSafeInteger(offset) && offset >= 0) {
296
+ const segment = findSegment(offset, offset + 1);
297
+ return segment.bytes[offset - segment.start];
298
+ }
299
+ }
300
+ return undefined;
301
+ },
302
+ });
303
+ return proxy as unknown as Uint8Array;
304
+ }
305
+
306
+ function parseRar5(bytes: Uint8Array, marker: number, options: FormatReadOptions): RarRecord[] {
307
+ const records: RarRecord[] = [];
308
+ let offset = marker + RAR5_MARKER.byteLength;
309
+ let sawMain = false;
310
+ while (offset < bytes.byteLength) {
311
+ if (offset + 5 > bytes.byteLength) corrupt("truncated RAR5 header");
312
+ const expectedHeaderCrc = readUInt32LE(bytes, offset);
313
+ const sizeAt = offset + 4;
314
+ const sizeCursor = { offset: sizeAt };
315
+ const headerSize = readVint(bytes, sizeCursor, bytes.byteLength, "header size");
316
+ if (headerSize > 2 * 1024 * 1024) corrupt("RAR5 header exceeds format limit");
317
+ assertIndexSize(headerSize, options.limits, "RAR5 header");
318
+ const headerStart = sizeCursor.offset;
319
+ const headerEnd = checkedEnd(headerStart, headerSize, bytes.byteLength, "RAR5 header");
320
+ if (crc32(bytes.subarray(sizeAt, headerEnd)) !== expectedHeaderCrc) corrupt("RAR5 header CRC32 mismatch");
321
+ const cursor = { offset: headerStart };
322
+ const type = readVint(bytes, cursor, headerEnd, "header type");
323
+ const flags = readVint(bytes, cursor, headerEnd, "header flags");
324
+ const extraSize = (flags & 1) !== 0 ? readVint(bytes, cursor, headerEnd, "extra area size") : 0;
325
+ const dataSize = (flags & 2) !== 0 ? readVint(bytes, cursor, headerEnd, "data size") : 0;
326
+ if ((flags & 0x18) !== 0) throw new ArchiveError("Unsupported multi-volume RAR5 archive");
327
+ const dataStart = headerEnd;
328
+ const dataEnd = checkedEnd(dataStart, dataSize, bytes.byteLength, "RAR5 data area");
329
+ if (extraSize > headerEnd - cursor.offset) corrupt("invalid RAR5 extra area size");
330
+ const extraStart = headerEnd - extraSize;
331
+
332
+ if (type === 4) throw new ArchiveError("Encrypted RAR5 headers are not supported");
333
+ if (type === 1) {
334
+ const archiveFlags = readVint(bytes, cursor, extraStart, "archive flags");
335
+ if ((archiveFlags & 1) !== 0) throw new ArchiveError("Unsupported multi-volume RAR5 archive");
336
+ if ((archiveFlags & 8) !== 0) throw new ArchiveError("Unsupported RAR5 recovery record");
337
+ if ((archiveFlags & 2) !== 0) readVint(bytes, cursor, extraStart, "volume number");
338
+ sawMain = true;
339
+ } else if (type === 2 || type === 3) {
340
+ const fileFlags = readVint(bytes, cursor, extraStart, "file flags");
341
+ const unpackedSize = readVint(bytes, cursor, extraStart, "unpacked size");
342
+ if ((fileFlags & 8) !== 0) throw new ArchiveError("RAR5 member with unknown unpacked size is not supported");
343
+ const attributes = readVint(bytes, cursor, extraStart, "file attributes");
344
+ let mtimeMs: number | undefined;
345
+ if ((fileFlags & 2) !== 0) {
346
+ need(cursor.offset, 4, extraStart, "RAR5 modification time");
347
+ mtimeMs = readUInt32LE(bytes, cursor.offset) * 1000;
348
+ cursor.offset += 4;
349
+ }
350
+ let dataCrc: number | undefined;
351
+ if ((fileFlags & 4) !== 0) {
352
+ need(cursor.offset, 4, extraStart, "RAR5 data CRC32");
353
+ dataCrc = readUInt32LE(bytes, cursor.offset);
354
+ cursor.offset += 4;
355
+ }
356
+ const compression = readVint(bytes, cursor, extraStart, "compression information");
357
+ let version = compression & 0x3f;
358
+ if (version === 1 && (compression & 0x100000) !== 0) version = 0;
359
+ const solid = (compression & 0x40) !== 0;
360
+ const method = (compression >>> 7) & 7;
361
+ const dictionaryPower = (compression >>> 10) & 0x1f;
362
+ let dictionarySize = 128 * 1024 * 2 ** dictionaryPower;
363
+ if ((compression & 0x3f) === 1) dictionarySize += (dictionarySize * ((compression >>> 15) & 0x1f)) / 32;
364
+ if (!Number.isSafeInteger(dictionarySize) || dictionarySize > options.limits.maxInMemorySize) {
365
+ throw new ArchiveError(`RAR5 dictionary is too large (${dictionarySize} bytes)`);
366
+ }
367
+ const hostOs = readVint(bytes, cursor, extraStart, "host OS");
368
+ const nameSize = readVint(bytes, cursor, extraStart, "file name size");
369
+ assertArchivePathBytes(nameSize, "member path", options.limits.maxPathBytes);
370
+ need(cursor.offset, nameSize, extraStart, "RAR5 file name");
371
+ let rawPath: string;
372
+ try {
373
+ rawPath = UTF8.decode(bytes.subarray(cursor.offset, cursor.offset + nameSize));
374
+ } catch {
375
+ throw new ArchiveError("Invalid RAR5 UTF-8 member name");
376
+ }
377
+ cursor.offset += nameSize;
378
+ let linkTarget: string | undefined;
379
+ for (const extra of readExtraRecords(bytes, extraStart, headerEnd)) {
380
+ const extraCursor = { offset: extra.start };
381
+ if (extra.type === 1) throw new ArchiveError(`Encrypted RAR5 member '${rawPath}' is not supported`);
382
+ if (extra.type === 3) {
383
+ const timeFlags = readVint(bytes, extraCursor, extra.end, "time flags");
384
+ if ((timeFlags & 2) !== 0) {
385
+ if ((timeFlags & 1) !== 0) {
386
+ need(extraCursor.offset, 4, extra.end, "Unix modification time");
387
+ mtimeMs = readUInt32LE(bytes, extraCursor.offset) * 1000;
388
+ } else {
389
+ need(extraCursor.offset, 8, extra.end, "Windows modification time");
390
+ mtimeMs = filetimeMs(bytes, extraCursor.offset);
391
+ }
392
+ }
393
+ } else if (extra.type === 5) {
394
+ const redirectionType = readVint(bytes, extraCursor, extra.end, "redirection type");
395
+ readVint(bytes, extraCursor, extra.end, "redirection flags");
396
+ const targetSize = readVint(bytes, extraCursor, extra.end, "link target size");
397
+ assertArchivePathBytes(targetSize, "link target", options.limits.maxPathBytes);
398
+ need(extraCursor.offset, targetSize, extra.end, "link target");
399
+ if (redirectionType < 1 || redirectionType > 5)
400
+ throw new ArchiveError(`Unsupported RAR5 redirection type ${redirectionType}`);
401
+ try {
402
+ linkTarget = UTF8.decode(bytes.subarray(extraCursor.offset, extraCursor.offset + targetSize));
403
+ } catch {
404
+ throw new ArchiveError("Invalid RAR5 UTF-8 link target");
405
+ }
406
+ }
407
+ }
408
+ if (type === 2) {
409
+ assertArchiveMemberSize(unpackedSize, rawPath, options.limits);
410
+ const path = normalizeArchiveEntryPath(rawPath);
411
+ if (path) {
412
+ let linkResolveTarget: boolean | undefined;
413
+ if (linkTarget !== undefined) {
414
+ const canonical = canonicalLinkTarget(path, linkTarget);
415
+ linkTarget = canonical.target;
416
+ linkResolveTarget = canonical.resolveTarget;
417
+ }
418
+ const isDirectory = (fileFlags & 1) !== 0;
419
+ records.push({
420
+ format: 5,
421
+ path,
422
+ dataStart,
423
+ packedSize: dataSize,
424
+ unpackedSize,
425
+ method,
426
+ version,
427
+ dictionarySize,
428
+ solid,
429
+ crc: dataCrc,
430
+ isDirectory,
431
+ mtimeMs,
432
+ mode: hostOs === 1 ? attributes : undefined,
433
+ linkTarget,
434
+ linkResolveTarget,
435
+ });
436
+ assertEntryCount(records.length, options.limits);
437
+ }
438
+ } else if (rawPath === "RR") {
439
+ throw new ArchiveError("Unsupported RAR5 recovery record");
440
+ }
441
+ } else if (type === 5) {
442
+ const endFlags = readVint(bytes, cursor, extraStart, "end flags");
443
+ if ((endFlags & 1) !== 0) throw new ArchiveError("Unsupported multi-volume RAR5 archive");
444
+ break;
445
+ } else if ((flags & 4) === 0) {
446
+ throw new ArchiveError(`Unsupported RAR5 header type ${type}`);
447
+ }
448
+ offset = dataEnd;
449
+ }
450
+ if (!sawMain) corrupt("RAR5 main header is missing");
451
+ return records;
452
+ }
453
+
454
+ function parseRar4(bytes: Uint8Array, marker: number, options: FormatReadOptions): RarRecord[] {
455
+ const records: RarRecord[] = [];
456
+ let offset = marker + RAR4_MARKER.byteLength;
457
+ let sawMain = false;
458
+ while (offset < bytes.byteLength) {
459
+ need(offset, 7, bytes.byteLength, "RAR4 base header");
460
+ const headerCrc = readUInt16LE(bytes, offset);
461
+ const type = bytes[offset + 2]!;
462
+ const flags = readUInt16LE(bytes, offset + 3);
463
+ const headerSize = readUInt16LE(bytes, offset + 5);
464
+ if (headerSize < 7) corrupt("invalid RAR4 header size");
465
+ assertIndexSize(headerSize, options.limits, "RAR4 header");
466
+ const headerEnd = checkedEnd(offset, headerSize, bytes.byteLength, "RAR4 header");
467
+ if ((crc32(bytes.subarray(offset + 2, headerEnd)) & 0xffff) !== headerCrc) corrupt("RAR4 header CRC mismatch");
468
+ let dataSize = 0;
469
+ let cursor = offset + 7;
470
+ if ((flags & 0x8000) !== 0) {
471
+ need(cursor, 4, headerEnd, "RAR4 additional size");
472
+ dataSize = readUInt32LE(bytes, cursor);
473
+ cursor += 4;
474
+ }
475
+ const dataStart = headerEnd;
476
+ let dataEnd = checkedEnd(dataStart, dataSize, bytes.byteLength, "RAR4 data area");
477
+ if (type === 0x73) {
478
+ if ((flags & 1) !== 0) throw new ArchiveError("Unsupported multi-volume RAR4 archive");
479
+ if ((flags & 0x40) !== 0) throw new ArchiveError("Unsupported RAR4 recovery record");
480
+ if ((flags & 0x80) !== 0) throw new ArchiveError("Encrypted RAR4 headers are not supported");
481
+ sawMain = true;
482
+ } else if (type === 0x74) {
483
+ need(cursor, 21, headerEnd, "RAR4 file header");
484
+ const unpackedLow = readUInt32LE(bytes, cursor);
485
+ cursor += 4;
486
+ const hostOs = bytes[cursor++]!;
487
+ const dataCrc = readUInt32LE(bytes, cursor);
488
+ cursor += 4;
489
+ const dosTime = readUInt32LE(bytes, cursor);
490
+ cursor += 4;
491
+ const version = bytes[cursor++]!;
492
+ const methodByte = bytes[cursor++]!;
493
+ const nameSize = readUInt16LE(bytes, cursor);
494
+ cursor += 2;
495
+ const attributes = readUInt32LE(bytes, cursor);
496
+ cursor += 4;
497
+ let packedSize = dataSize;
498
+ let unpackedSize = unpackedLow;
499
+ if ((flags & 0x100) !== 0) {
500
+ need(cursor, 8, headerEnd, "RAR4 high sizes");
501
+ packedSize += readUInt32LE(bytes, cursor) * 0x100000000;
502
+ cursor += 4;
503
+ unpackedSize += readUInt32LE(bytes, cursor) * 0x100000000;
504
+ cursor += 4;
505
+ dataEnd = checkedEnd(dataStart, packedSize, bytes.byteLength, "RAR4 file data");
506
+ }
507
+ if ((flags & 3) !== 0) throw new ArchiveError("Unsupported multi-volume RAR4 member");
508
+ if ((flags & 4) !== 0) throw new ArchiveError("Encrypted RAR4 file data is not supported");
509
+ if (methodByte < 0x30 || methodByte > 0x35)
510
+ throw new ArchiveError(`Unsupported RAR4 compression method 0x${methodByte.toString(16)}`);
511
+ assertArchivePathBytes(nameSize, "member path", options.limits.maxPathBytes);
512
+ need(cursor, nameSize, headerEnd, "RAR4 file name");
513
+ const nameBytes = bytes.subarray(cursor, cursor + nameSize);
514
+ const rawPath = (flags & 0x200) !== 0 ? decodeRar4UnicodeName(nameBytes) : LATIN1.decode(nameBytes);
515
+ cursor += nameSize;
516
+ if ((flags & 0x400) !== 0) {
517
+ need(cursor, 8, headerEnd, "RAR4 salt");
518
+ cursor += 8;
519
+ }
520
+ let mtimeMs = dosTimeMs(dosTime);
521
+ if ((flags & 0x1000) !== 0) {
522
+ need(cursor, 2, headerEnd, "RAR4 extended time flags");
523
+ const timeFlags = readUInt16LE(bytes, cursor);
524
+ cursor += 2;
525
+ for (let timeIndex = 0; timeIndex < 4; timeIndex++) {
526
+ const mode = (timeFlags >>> ((3 - timeIndex) * 4)) & 15;
527
+ if ((mode & 8) === 0) continue;
528
+ let timeValue = dosTime;
529
+ if (timeIndex !== 0) {
530
+ need(cursor, 4, headerEnd, "RAR4 extended time");
531
+ timeValue = readUInt32LE(bytes, cursor);
532
+ cursor += 4;
533
+ }
534
+ let preciseMs = dosTimeMs(timeValue) + ((mode & 4) !== 0 ? 1000 : 0);
535
+ let remainder = 0;
536
+ const count = mode & 3;
537
+ need(cursor, count, headerEnd, "RAR4 extended time precision");
538
+ for (let index = 0; index < count; index++) {
539
+ remainder |= bytes[cursor++]! << ((index + 3 - count) * 8);
540
+ }
541
+ preciseMs += remainder / 10000;
542
+ if (timeIndex === 0) mtimeMs = preciseMs;
543
+ }
544
+ }
545
+ assertArchiveMemberSize(unpackedSize, rawPath, options.limits);
546
+ const path = normalizeArchiveEntryPath(rawPath);
547
+ if (path) {
548
+ const directory = ((flags >>> 5) & 7) === 7;
549
+ const mode = hostOs === 3 ? attributes : undefined;
550
+ let linkTarget: string | undefined;
551
+ let linkResolveTarget: boolean | undefined;
552
+ if (mode !== undefined && (mode & 0xf000) === 0xa000) {
553
+ if (methodByte !== 0x30 || packedSize !== unpackedSize)
554
+ throw new ArchiveError(`Unsupported compressed RAR4 symlink '${path}'`);
555
+ assertArchivePathBytes(packedSize, "link target", options.limits.maxPathBytes);
556
+ const canonical = canonicalLinkTarget(path, LATIN1.decode(bytes.subarray(dataStart, dataEnd)));
557
+ linkTarget = canonical.target;
558
+ linkResolveTarget = canonical.resolveTarget;
559
+ }
560
+ records.push({
561
+ format: 4,
562
+ path,
563
+ dataStart,
564
+ packedSize,
565
+ unpackedSize,
566
+ method: methodByte - 0x30,
567
+ version,
568
+ dictionarySize: rar4Dictionary(flags),
569
+ solid: (flags & 0x10) !== 0,
570
+ crc: dataCrc,
571
+ isDirectory: directory,
572
+ mtimeMs,
573
+ mode,
574
+ linkTarget,
575
+ linkResolveTarget,
576
+ });
577
+ assertEntryCount(records.length, options.limits);
578
+ }
579
+ } else if (type === 0x78) {
580
+ throw new ArchiveError("Unsupported RAR4 recovery record");
581
+ } else if (type === 0x7b) {
582
+ if ((flags & 1) !== 0) throw new ArchiveError("Unsupported multi-volume RAR4 archive");
583
+ break;
584
+ }
585
+ offset = dataEnd;
586
+ }
587
+ if (!sawMain) corrupt("RAR4 main header is missing");
588
+ return records;
589
+ }
590
+
591
+ function readExtraRecords(
592
+ bytes: Uint8Array,
593
+ start: number,
594
+ end: number,
595
+ ): Array<{ type: number; start: number; end: number }> {
596
+ const records: Array<{ type: number; start: number; end: number }> = [];
597
+ const cursor = { offset: start };
598
+ while (cursor.offset < end) {
599
+ const recordSize = readVint(bytes, cursor, end, "extra record size");
600
+ const recordStart = cursor.offset;
601
+ const recordEnd = checkedEnd(recordStart, recordSize, end, "extra record");
602
+ const type = readVint(bytes, cursor, recordEnd, "extra record type");
603
+ records.push({ type, start: cursor.offset, end: recordEnd });
604
+ cursor.offset = recordEnd;
605
+ }
606
+ return records;
607
+ }
608
+
609
+ function readVint(bytes: Uint8Array, cursor: { offset: number }, end: number, what: string): number {
610
+ let value = 0;
611
+ let factor = 1;
612
+ for (let count = 0; count < 10; count++) {
613
+ if (cursor.offset >= end) corrupt(`truncated ${what}`);
614
+ const byte = bytes[cursor.offset++]!;
615
+ value += (byte & 0x7f) * factor;
616
+ if (!Number.isSafeInteger(value)) corrupt(`${what} is too large`);
617
+ if ((byte & 0x80) === 0) return value;
618
+ factor *= 128;
619
+ }
620
+ corrupt(`${what} vint is too long`);
621
+ }
622
+
623
+ function findMarker(bytes: Uint8Array): { version: 4 | 5; offset: number } | undefined {
624
+ const limit = Math.min(bytes.byteLength, 1024 * 1024 + RAR5_MARKER.byteLength);
625
+ for (let offset = 0; offset < limit; offset++) {
626
+ if (matches(bytes, offset, RAR5_MARKER)) return { version: 5, offset };
627
+ if (matches(bytes, offset, RAR4_MARKER)) return { version: 4, offset };
628
+ }
629
+ return undefined;
630
+ }
631
+
632
+ function matches(bytes: Uint8Array, offset: number, marker: Uint8Array): boolean {
633
+ if (offset + marker.byteLength > bytes.byteLength) return false;
634
+ for (let index = 0; index < marker.byteLength; index++) if (bytes[offset + index] !== marker[index]) return false;
635
+ return true;
636
+ }
637
+
638
+ function decodeRar4UnicodeName(bytes: Uint8Array): string {
639
+ let separator = bytes.indexOf(0);
640
+ if (separator < 0) return LATIN1.decode(bytes);
641
+ if (++separator >= bytes.byteLength) return LATIN1.decode(bytes.subarray(0, separator - 1));
642
+ const highByte = bytes[separator++]!;
643
+ let decodedPosition = 0;
644
+ let flags = 0;
645
+ let flagBits = 0;
646
+ let output = "";
647
+ while (separator < bytes.byteLength) {
648
+ if (flagBits === 0) {
649
+ flags = bytes[separator++]!;
650
+ flagBits = 8;
651
+ if (separator >= bytes.byteLength) break;
652
+ }
653
+ const operation = flags >>> 6;
654
+ if (operation === 0) {
655
+ output += String.fromCharCode(bytes[separator++]!);
656
+ decodedPosition++;
657
+ } else if (operation === 1) {
658
+ output += String.fromCharCode(bytes[separator++]! + (highByte << 8));
659
+ decodedPosition++;
660
+ } else if (operation === 2) {
661
+ if (separator + 1 >= bytes.byteLength) corrupt("truncated RAR4 Unicode name");
662
+ output += String.fromCharCode(bytes[separator]! | (bytes[separator + 1]! << 8));
663
+ separator += 2;
664
+ decodedPosition++;
665
+ } else {
666
+ let count = bytes[separator++]!;
667
+ if ((count & 0x80) !== 0) {
668
+ if (separator >= bytes.byteLength) corrupt("truncated RAR4 Unicode name correction");
669
+ const correction = bytes[separator++]!;
670
+ count = (count & 0x7f) + 2;
671
+ for (; count > 0 && decodedPosition < bytes.byteLength; count--, decodedPosition++) {
672
+ output += String.fromCharCode((highByte << 8) | ((bytes[decodedPosition]! + correction) & 0xff));
673
+ }
674
+ } else {
675
+ count += 2;
676
+ for (; count > 0 && decodedPosition < bytes.byteLength; count--, decodedPosition++)
677
+ output += String.fromCharCode(bytes[decodedPosition]!);
678
+ }
679
+ }
680
+ flags = (flags << 2) & 0xff;
681
+ flagBits -= 2;
682
+ }
683
+ return output;
684
+ }
685
+ function canonicalLinkTarget(recordPath: string, rawTarget: string): { target: string; resolveTarget: boolean } {
686
+ const portable = rawTarget.replace(/\\/g, "/");
687
+ if (portable.startsWith("/")) return { target: portable, resolveTarget: false };
688
+ const parts = recordPath.split("/");
689
+ parts.pop();
690
+ for (const part of portable.split("/")) {
691
+ if (!part || part === ".") continue;
692
+ if (part === "..") {
693
+ if (parts.length === 0) return { target: portable, resolveTarget: false };
694
+ parts.pop();
695
+ } else {
696
+ parts.push(part);
697
+ }
698
+ }
699
+ return { target: parts.join("/"), resolveTarget: true };
700
+ }
701
+
702
+ function rar4Dictionary(flags: number): number {
703
+ const index = (flags >>> 5) & 7;
704
+ return index === 7 ? 4 * 1024 * 1024 : 64 * 1024 * 2 ** index;
705
+ }
706
+
707
+ function dosTimeMs(value: number): number {
708
+ const second = (value & 0x1f) * 2;
709
+ const minute = (value >>> 5) & 0x3f;
710
+ const hour = (value >>> 11) & 0x1f;
711
+ const day = (value >>> 16) & 0x1f;
712
+ const month = (value >>> 21) & 0xf;
713
+ const year = ((value >>> 25) & 0x7f) + 1980;
714
+ return new Date(year, month - 1, day, hour, minute, second).getTime();
715
+ }
716
+
717
+ function filetimeMs(bytes: Uint8Array, offset: number): number {
718
+ const ticks = readUInt32LE(bytes, offset) + readUInt32LE(bytes, offset + 4) * 0x100000000;
719
+ return ticks / 10000 - 11644473600000;
720
+ }
721
+
722
+ function checkedEnd(start: number, size: number, limit: number, what: string): number {
723
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || size < 0) corrupt(`invalid ${what} range`);
724
+ const end = start + size;
725
+ if (!Number.isSafeInteger(end) || end < start || end > limit) corrupt(`truncated ${what}`);
726
+ return end;
727
+ }
728
+
729
+ function need(start: number, size: number, end: number, what: string): void {
730
+ checkedEnd(start, size, end, what);
731
+ }
732
+
733
+ function corrupt(reason: string): never {
734
+ throw new ArchiveError(`Invalid RAR archive: ${reason}`);
735
+ }