@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/zip.ts ADDED
@@ -0,0 +1,719 @@
1
+ import * as path from "node:path";
2
+ import * as zlib from "node:zlib";
3
+ import { readUInt16LE, readUInt32LE, readUInt64LE, writeUInt16LE, writeUInt32LE, writeUInt64LE } from "./bytes";
4
+ import { crc32 } from "./checksums";
5
+ import { bzip2Decompress } from "./codecs/bzip2";
6
+ import { lzmaDecompress } from "./codecs/lzma";
7
+ import { xzDecompress } from "./codecs/xz";
8
+ import { zstdDecompress } from "./codecs/zstd";
9
+ import { ArchiveError } from "./error";
10
+ import {
11
+ type ArchiveLimits,
12
+ assertArchiveMemberSize,
13
+ assertEntryCount,
14
+ assertIndexSize,
15
+ DEFAULT_ARCHIVE_LIMITS,
16
+ } from "./limits";
17
+ import {
18
+ assertArchivePathBytes,
19
+ assertArchivePathString,
20
+ isArchiveDirectoryName,
21
+ normalizeArchiveEntryPath,
22
+ normalizeArchiveLookupPath,
23
+ } from "./paths";
24
+ import { type ByteSource, memoryByteSource } from "./source";
25
+ import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
26
+
27
+ const LOCAL_HEADER_SIGNATURE = 0x04034b50;
28
+ const CENTRAL_HEADER_SIGNATURE = 0x02014b50;
29
+ const ZIP64_EOCD_SIGNATURE = 0x06064b50;
30
+ const ZIP64_LOCATOR_SIGNATURE = 0x07064b50;
31
+ const EOCD_SIGNATURE = 0x06054b50;
32
+ const DATA_DESCRIPTOR_SIGNATURE = 0x08074b50;
33
+ const EOCD_LENGTH = 22;
34
+ const ZIP64_EOCD_LENGTH = 56;
35
+ const ZIP64_LOCATOR_LENGTH = 20;
36
+ const MAX_COMMENT_LENGTH = 0xffff;
37
+ const U16_MAX = 0xffff;
38
+ const U32_MAX = 0xffffffff;
39
+ const UTF8_FLAG = 0x0800;
40
+ const ENCRYPTED_FLAG = 0x0001;
41
+ const STRONG_ENCRYPTION_FLAG = 0x0040;
42
+ const UNIX_HOST = 3;
43
+ const OSX_HOST = 19;
44
+ const FILE_TYPE_MASK = 0o170000;
45
+ const DIRECTORY_TYPE = 0o040000;
46
+ const SYMLINK_TYPE = 0o120000;
47
+ const WINDOWS_EPOCH_FILETIME_MS = 11_644_473_600_000n;
48
+ const SUPPORTED_METHODS: Readonly<Partial<Record<number, true>>> = {
49
+ 0: true,
50
+ 8: true,
51
+ 12: true,
52
+ 14: true,
53
+ 20: true,
54
+ 93: true,
55
+ 95: true,
56
+ };
57
+
58
+ const UTF8_DECODER = new TextDecoder("utf-8");
59
+ const UTF8_FATAL_DECODER = new TextDecoder("utf-8", { fatal: true });
60
+ const LEGACY_NAME_DECODER = new TextDecoder("windows-1252");
61
+ const TEXT_ENCODER = new TextEncoder();
62
+
63
+ interface CentralDirectoryInfo {
64
+ entries: number;
65
+ offset: number;
66
+ size: number;
67
+ physicalEnd: number;
68
+ archiveOffset: number;
69
+ }
70
+
71
+ interface Zip64Values {
72
+ compressedSize: number;
73
+ uncompressedSize: number;
74
+ localHeaderOffset: number;
75
+ diskStart: number;
76
+ }
77
+
78
+ interface Zip64Placeholders {
79
+ compressedSize: boolean;
80
+ uncompressedSize: boolean;
81
+ localHeaderOffset: boolean;
82
+ diskStart: boolean;
83
+ }
84
+
85
+ interface ParsedExtra {
86
+ zip64?: Uint8Array;
87
+ unicodePath?: string;
88
+ mtimeMs?: number;
89
+ }
90
+
91
+ interface ParsedZipEntry {
92
+ entry: ArchiveIndexEntry;
93
+ isSymlink: boolean;
94
+ }
95
+
96
+ function archiveError(error: unknown, context: string): ArchiveError {
97
+ if (error instanceof ArchiveError) return error;
98
+ return new ArchiveError(`${context}: ${error instanceof Error ? error.message : String(error)}`);
99
+ }
100
+
101
+ function checkedEnd(start: number, size: number, archiveSize: number, what: string): number {
102
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(size) || start < 0 || size < 0) {
103
+ throw new ArchiveError(`Invalid ZIP archive: ${what} has an invalid range`);
104
+ }
105
+ const end = start + size;
106
+ if (!Number.isSafeInteger(end) || end > archiveSize) {
107
+ throw new ArchiveError(`Invalid ZIP archive: ${what} exceeds archive size`);
108
+ }
109
+ return end;
110
+ }
111
+
112
+ function findEocd(tail: Uint8Array): number {
113
+ for (let offset = tail.byteLength - EOCD_LENGTH; offset >= 0; offset--) {
114
+ if (readUInt32LE(tail, offset) !== EOCD_SIGNATURE) continue;
115
+ if (offset + EOCD_LENGTH + readUInt16LE(tail, offset + 20) === tail.byteLength) return offset;
116
+ }
117
+ throw new ArchiveError("Invalid ZIP archive: missing end of central directory");
118
+ }
119
+
120
+ async function readZip64Info(
121
+ source: ByteSource,
122
+ tail: Uint8Array,
123
+ tailStart: number,
124
+ eocdOffset: number,
125
+ ): Promise<CentralDirectoryInfo | undefined> {
126
+ const locatorOffset = eocdOffset - ZIP64_LOCATOR_LENGTH;
127
+ if (locatorOffset < 0) return undefined;
128
+ const locator =
129
+ locatorOffset >= tailStart
130
+ ? tail.subarray(locatorOffset - tailStart, locatorOffset - tailStart + ZIP64_LOCATOR_LENGTH)
131
+ : await source.read(locatorOffset, eocdOffset);
132
+ if (locator.byteLength !== ZIP64_LOCATOR_LENGTH || readUInt32LE(locator, 0) !== ZIP64_LOCATOR_SIGNATURE) {
133
+ return undefined;
134
+ }
135
+ if (readUInt32LE(locator, 4) !== 0 || readUInt32LE(locator, 16) !== 1) {
136
+ throw new ArchiveError("Multi-volume ZIP archives are not supported");
137
+ }
138
+
139
+ const declaredOffset = readUInt64LE(locator, 8);
140
+ const candidates = [declaredOffset];
141
+ const adjacentOffset = locatorOffset - ZIP64_EOCD_LENGTH;
142
+ if (adjacentOffset >= 0 && adjacentOffset !== declaredOffset) candidates.push(adjacentOffset);
143
+ for (const candidate of candidates) {
144
+ if (candidate < 0 || candidate + ZIP64_EOCD_LENGTH > source.size) continue;
145
+ const record = await source.read(candidate, candidate + ZIP64_EOCD_LENGTH);
146
+ if (record.byteLength !== ZIP64_EOCD_LENGTH || readUInt32LE(record, 0) !== ZIP64_EOCD_SIGNATURE) continue;
147
+ const extensibleSize = readUInt64LE(record, 4);
148
+ if (extensibleSize < 44 || candidate + 12 + extensibleSize !== locatorOffset) continue;
149
+ if (readUInt32LE(record, 16) !== 0 || readUInt32LE(record, 20) !== 0) {
150
+ throw new ArchiveError("Multi-volume ZIP archives are not supported");
151
+ }
152
+ const entriesOnDisk = readUInt64LE(record, 24);
153
+ const entries = readUInt64LE(record, 32);
154
+ if (entriesOnDisk !== entries) throw new ArchiveError("Multi-volume ZIP archives are not supported");
155
+ return {
156
+ entries,
157
+ size: readUInt64LE(record, 40),
158
+ offset: readUInt64LE(record, 48),
159
+ physicalEnd: candidate,
160
+ archiveOffset: 0,
161
+ };
162
+ }
163
+ throw new ArchiveError("Invalid ZIP archive: missing ZIP64 end of central directory");
164
+ }
165
+
166
+ async function locateCentralDirectory(source: ByteSource, info: CentralDirectoryInfo): Promise<number> {
167
+ if (info.entries === 0) return info.offset;
168
+ const candidates = [info.offset];
169
+ const adjacent = info.physicalEnd - info.size;
170
+ if (adjacent !== info.offset) candidates.push(adjacent);
171
+ for (const offset of candidates) {
172
+ if (offset < 0 || offset + 4 > source.size || offset + info.size > source.size) continue;
173
+ const signature = await source.read(offset, offset + 4);
174
+ if (signature.byteLength === 4 && readUInt32LE(signature, 0) === CENTRAL_HEADER_SIGNATURE) return offset;
175
+ }
176
+ throw new ArchiveError("Invalid ZIP archive: central directory is out of bounds or malformed");
177
+ }
178
+
179
+ async function readCentralDirectoryInfo(source: ByteSource, limits: ArchiveLimits): Promise<CentralDirectoryInfo> {
180
+ if (source.size < EOCD_LENGTH) throw new ArchiveError("Invalid ZIP archive: missing end of central directory");
181
+ const tailLength = Math.min(source.size, EOCD_LENGTH + MAX_COMMENT_LENGTH);
182
+ const tailStart = source.size - tailLength;
183
+ const tail = await source.read(tailStart, source.size);
184
+ if (tail.byteLength !== tailLength)
185
+ throw new ArchiveError("Invalid ZIP archive: truncated end of central directory");
186
+ const eocdIndex = findEocd(tail);
187
+ const eocdOffset = tailStart + eocdIndex;
188
+ const disk = readUInt16LE(tail, eocdIndex + 4);
189
+ const centralDisk = readUInt16LE(tail, eocdIndex + 6);
190
+ const entriesOnDisk = readUInt16LE(tail, eocdIndex + 8);
191
+ let entries = readUInt16LE(tail, eocdIndex + 10);
192
+ let size = readUInt32LE(tail, eocdIndex + 12);
193
+ let offset = readUInt32LE(tail, eocdIndex + 16);
194
+ if (
195
+ disk !== 0 ||
196
+ centralDisk !== 0 ||
197
+ (entriesOnDisk !== U16_MAX && entries !== U16_MAX && entriesOnDisk !== entries)
198
+ ) {
199
+ throw new ArchiveError("Multi-volume ZIP archives are not supported");
200
+ }
201
+ const needsZip64 = entriesOnDisk === U16_MAX || entries === U16_MAX || size === U32_MAX || offset === U32_MAX;
202
+ const zip64 = await readZip64Info(source, tail, tailStart, eocdOffset);
203
+ let physicalEnd = eocdOffset;
204
+ if (zip64) {
205
+ ({ entries, size, offset, physicalEnd } = zip64);
206
+ } else if (needsZip64) {
207
+ throw new ArchiveError("Invalid ZIP archive: missing ZIP64 central-directory metadata");
208
+ }
209
+ assertEntryCount(entries, limits);
210
+ assertIndexSize(size, limits, "ZIP central directory");
211
+ if (entries > Math.floor(size / 46)) throw new ArchiveError("Invalid ZIP archive: truncated central directory");
212
+ const declaredOffset = offset;
213
+ const info = { entries, size, offset, physicalEnd, archiveOffset: 0 };
214
+ info.offset = await locateCentralDirectory(source, info);
215
+ info.archiveOffset = info.offset - declaredOffset;
216
+ checkedEnd(info.offset, info.size, source.size, "central directory");
217
+ return info;
218
+ }
219
+
220
+ function filetimeToMs(bytes: Uint8Array, offset: number): number | undefined {
221
+ let value = 0n;
222
+ for (let index = 7; index >= 0; index--) value = (value << 8n) | BigInt(bytes[offset + index]!);
223
+ const milliseconds = value / 10_000n - WINDOWS_EPOCH_FILETIME_MS;
224
+ const number = Number(milliseconds);
225
+ return Number.isSafeInteger(number) ? number : undefined;
226
+ }
227
+
228
+ function parseNtfsMtime(data: Uint8Array): number | undefined {
229
+ if (data.byteLength < 4) return undefined;
230
+ let offset = 4;
231
+ while (offset + 4 <= data.byteLength) {
232
+ const tag = readUInt16LE(data, offset);
233
+ const size = readUInt16LE(data, offset + 2);
234
+ const end = offset + 4 + size;
235
+ if (end > data.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed NTFS extra field");
236
+ if (tag === 1 && size >= 8) return filetimeToMs(data, offset + 4);
237
+ offset = end;
238
+ }
239
+ return undefined;
240
+ }
241
+
242
+ function parseDosMtime(time: number, date: number): number | undefined {
243
+ if (date === 0) return undefined;
244
+ const year = 1980 + ((date >>> 9) & 0x7f);
245
+ const month = ((date >>> 5) & 0x0f) - 1;
246
+ const day = date & 0x1f;
247
+ const hour = (time >>> 11) & 0x1f;
248
+ const minute = (time >>> 5) & 0x3f;
249
+ const second = (time & 0x1f) * 2;
250
+ if (month < 0 || month > 11 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return undefined;
251
+ const value = new Date(year, month, day, hour, minute, second).getTime();
252
+ return Number.isFinite(value) ? value : undefined;
253
+ }
254
+
255
+ function parseExtra(extra: Uint8Array, rawName: Uint8Array): ParsedExtra {
256
+ const result: ParsedExtra = {};
257
+ let offset = 0;
258
+ while (offset < extra.byteLength) {
259
+ if (offset + 4 > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: truncated extra-field header");
260
+ const id = readUInt16LE(extra, offset);
261
+ const size = readUInt16LE(extra, offset + 2);
262
+ const dataStart = offset + 4;
263
+ const dataEnd = dataStart + size;
264
+ if (dataEnd > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed extra field");
265
+ const data = extra.subarray(dataStart, dataEnd);
266
+ if (id === 0x0001) {
267
+ result.zip64 = data;
268
+ } else if (id === 0x7075) {
269
+ if (data.byteLength < 5) throw new ArchiveError("Invalid ZIP archive: Unicode path extra field is too small");
270
+ if (data[0] === 1 && readUInt32LE(data, 1) === crc32(rawName)) {
271
+ try {
272
+ result.unicodePath = UTF8_FATAL_DECODER.decode(data.subarray(5));
273
+ } catch {
274
+ // A bad optional Unicode path falls back to the header name.
275
+ }
276
+ }
277
+ } else if (id === 0x5455) {
278
+ if (data.byteLength < 1)
279
+ throw new ArchiveError("Invalid ZIP archive: extended timestamp extra field is too small");
280
+ if ((data[0]! & 1) !== 0) {
281
+ if (data.byteLength < 5)
282
+ throw new ArchiveError("Invalid ZIP archive: extended timestamp extra field is too small");
283
+ result.mtimeMs = (readUInt32LE(data, 1) | 0) * 1000;
284
+ }
285
+ } else if (id === 0x000a && result.mtimeMs === undefined) {
286
+ result.mtimeMs = parseNtfsMtime(data);
287
+ }
288
+ offset = dataEnd;
289
+ }
290
+ return result;
291
+ }
292
+
293
+ function applyZip64Values(
294
+ extra: Uint8Array | undefined,
295
+ current: Zip64Values,
296
+ placeholders: Zip64Placeholders,
297
+ ): Zip64Values {
298
+ const needs =
299
+ placeholders.uncompressedSize ||
300
+ placeholders.compressedSize ||
301
+ placeholders.localHeaderOffset ||
302
+ placeholders.diskStart;
303
+ if (!needs) return current;
304
+ if (!extra) throw new ArchiveError("Invalid ZIP archive: missing ZIP64 extra field");
305
+ let offset = 0;
306
+ const result = { ...current };
307
+ if (placeholders.uncompressedSize) {
308
+ if (offset + 8 > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed ZIP64 extra field");
309
+ result.uncompressedSize = readUInt64LE(extra, offset);
310
+ offset += 8;
311
+ }
312
+ if (placeholders.compressedSize) {
313
+ if (offset + 8 > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed ZIP64 extra field");
314
+ result.compressedSize = readUInt64LE(extra, offset);
315
+ offset += 8;
316
+ }
317
+ if (placeholders.localHeaderOffset) {
318
+ if (offset + 8 > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed ZIP64 extra field");
319
+ result.localHeaderOffset = readUInt64LE(extra, offset);
320
+ offset += 8;
321
+ }
322
+ if (placeholders.diskStart) {
323
+ if (offset + 4 > extra.byteLength) throw new ArchiveError("Invalid ZIP archive: malformed ZIP64 extra field");
324
+ result.diskStart = readUInt32LE(extra, offset);
325
+ }
326
+ return result;
327
+ }
328
+
329
+ async function decodeMember(
330
+ compressed: Uint8Array,
331
+ method: number,
332
+ size: number,
333
+ memberPath: string,
334
+ ): Promise<Uint8Array> {
335
+ try {
336
+ switch (method) {
337
+ case 0:
338
+ return compressed;
339
+ case 8:
340
+ return zlib.inflateRawSync(compressed, { maxOutputLength: Math.max(size, 1) });
341
+ case 12:
342
+ return await bzip2Decompress(compressed, size);
343
+ case 14: {
344
+ if (compressed.byteLength < 9 || compressed[2] !== 5 || compressed[3] !== 0) {
345
+ throw new ArchiveError(`Invalid ZIP archive: malformed LZMA properties for '${memberPath}'`);
346
+ }
347
+ return await lzmaDecompress(compressed.subarray(4, 9), compressed.subarray(9), size);
348
+ }
349
+ case 20:
350
+ case 93:
351
+ return await zstdDecompress(compressed, size);
352
+ case 95:
353
+ return await xzDecompress(compressed, size);
354
+ default:
355
+ throw new ArchiveError(`Unsupported ZIP compression method ${method} for '${memberPath}'`);
356
+ }
357
+ } catch (error) {
358
+ throw archiveError(error, `Failed to decompress ZIP member '${memberPath}'`);
359
+ }
360
+ }
361
+
362
+ class ZipMemberSource implements MemberSource {
363
+ readonly #source: ByteSource;
364
+ readonly #compressedSize: number;
365
+ readonly #method: number;
366
+ readonly #flags: number;
367
+ readonly #crc: number;
368
+ readonly #localHeaderOffset: number;
369
+ readonly #limits: ArchiveLimits;
370
+
371
+ constructor(
372
+ source: ByteSource,
373
+ compressedSize: number,
374
+ method: number,
375
+ flags: number,
376
+ crc: number,
377
+ localHeaderOffset: number,
378
+ limits: ArchiveLimits,
379
+ ) {
380
+ this.#source = source;
381
+ this.#compressedSize = compressedSize;
382
+ this.#method = method;
383
+ this.#flags = flags;
384
+ this.#crc = crc;
385
+ this.#localHeaderOffset = localHeaderOffset;
386
+ this.#limits = limits;
387
+ }
388
+
389
+ async read(size: number, memberPath: string): Promise<Uint8Array> {
390
+ try {
391
+ assertArchiveMemberSize(Math.max(size, this.#compressedSize), memberPath, this.#limits);
392
+ if ((this.#flags & (ENCRYPTED_FLAG | STRONG_ENCRYPTION_FLAG)) !== 0 || this.#method === 99) {
393
+ throw new ArchiveError(`Encrypted ZIP member '${memberPath}' is not supported`);
394
+ }
395
+ if (SUPPORTED_METHODS[this.#method] !== true) {
396
+ throw new ArchiveError(`Unsupported ZIP compression method ${this.#method} for '${memberPath}'`);
397
+ }
398
+ const headerEnd = checkedEnd(
399
+ this.#localHeaderOffset,
400
+ 30,
401
+ this.#source.size,
402
+ `local header for '${memberPath}'`,
403
+ );
404
+ const header = await this.#source.read(this.#localHeaderOffset, headerEnd);
405
+ if (header.byteLength !== 30 || readUInt32LE(header, 0) !== LOCAL_HEADER_SIGNATURE) {
406
+ throw new ArchiveError(`Invalid ZIP archive: malformed local header for '${memberPath}'`);
407
+ }
408
+ const localFlags = readUInt16LE(header, 6);
409
+ if ((localFlags & (ENCRYPTED_FLAG | STRONG_ENCRYPTION_FLAG)) !== 0) {
410
+ throw new ArchiveError(`Encrypted ZIP member '${memberPath}' is not supported`);
411
+ }
412
+ if (readUInt16LE(header, 8) !== this.#method) {
413
+ throw new ArchiveError(
414
+ `Invalid ZIP archive: local and central compression methods disagree for '${memberPath}'`,
415
+ );
416
+ }
417
+ const dataStart = this.#localHeaderOffset + 30 + readUInt16LE(header, 26) + readUInt16LE(header, 28);
418
+ const dataEnd = checkedEnd(dataStart, this.#compressedSize, this.#source.size, `data for '${memberPath}'`);
419
+ if (this.#method === 0 && this.#compressedSize !== size) {
420
+ throw new ArchiveError(
421
+ `Invalid ZIP archive: size mismatch for '${memberPath}' (expected ${size}, got ${this.#compressedSize})`,
422
+ );
423
+ }
424
+ const compressed = await this.#source.read(dataStart, dataEnd);
425
+ if (compressed.byteLength !== this.#compressedSize) {
426
+ throw new ArchiveError(`Invalid ZIP archive: truncated data for '${memberPath}'`);
427
+ }
428
+ const decoded = await decodeMember(compressed, this.#method, size, memberPath);
429
+ if (decoded.byteLength !== size) {
430
+ throw new ArchiveError(
431
+ `Invalid ZIP archive: size mismatch for '${memberPath}' (expected ${size}, got ${decoded.byteLength})`,
432
+ );
433
+ }
434
+ const actualCrc = crc32(decoded);
435
+ if (actualCrc !== this.#crc) {
436
+ throw new ArchiveError(`Invalid ZIP archive: CRC mismatch for '${memberPath}'`);
437
+ }
438
+ return decoded;
439
+ } catch (error) {
440
+ throw archiveError(error, `Failed to read ZIP member '${memberPath}'`);
441
+ }
442
+ }
443
+ }
444
+
445
+ function parseCentralDirectory(
446
+ source: ByteSource,
447
+ directory: Uint8Array,
448
+ info: CentralDirectoryInfo,
449
+ options: FormatReadOptions,
450
+ ): ParsedZipEntry[] {
451
+ const parsed: ParsedZipEntry[] = [];
452
+ let offset = 0;
453
+ for (let index = 0; index < info.entries; index++) {
454
+ if (offset + 46 > directory.byteLength)
455
+ throw new ArchiveError("Invalid ZIP archive: truncated central directory");
456
+ if (readUInt32LE(directory, offset) !== CENTRAL_HEADER_SIGNATURE) {
457
+ throw new ArchiveError("Invalid ZIP archive: malformed central directory");
458
+ }
459
+ const versionMadeBy = readUInt16LE(directory, offset + 4);
460
+ const flags = readUInt16LE(directory, offset + 8);
461
+ const method = readUInt16LE(directory, offset + 10);
462
+ const dosTime = readUInt16LE(directory, offset + 12);
463
+ const dosDate = readUInt16LE(directory, offset + 14);
464
+ const crc = readUInt32LE(directory, offset + 16);
465
+ const compressedRaw = readUInt32LE(directory, offset + 20);
466
+ const uncompressedRaw = readUInt32LE(directory, offset + 24);
467
+ const nameLength = readUInt16LE(directory, offset + 28);
468
+ const extraLength = readUInt16LE(directory, offset + 30);
469
+ const commentLength = readUInt16LE(directory, offset + 32);
470
+ const diskStartRaw = readUInt16LE(directory, offset + 34);
471
+ const externalAttributes = readUInt32LE(directory, offset + 38);
472
+ const localOffsetRaw = readUInt32LE(directory, offset + 42);
473
+ const nameStart = offset + 46;
474
+ const extraStart = nameStart + nameLength;
475
+ const commentStart = extraStart + extraLength;
476
+ const end = commentStart + commentLength;
477
+ if (!Number.isSafeInteger(end) || end > directory.byteLength) {
478
+ throw new ArchiveError("Invalid ZIP archive: truncated central-directory entry");
479
+ }
480
+ assertArchivePathBytes(nameLength, "member path", options.limits.maxPathBytes);
481
+ const rawName = directory.subarray(nameStart, extraStart);
482
+ const extra = parseExtra(directory.subarray(extraStart, commentStart), rawName);
483
+ const values = applyZip64Values(
484
+ extra.zip64,
485
+ {
486
+ compressedSize: compressedRaw,
487
+ uncompressedSize: uncompressedRaw,
488
+ localHeaderOffset: localOffsetRaw,
489
+ diskStart: diskStartRaw,
490
+ },
491
+ {
492
+ compressedSize: compressedRaw === U32_MAX,
493
+ uncompressedSize: uncompressedRaw === U32_MAX,
494
+ localHeaderOffset: localOffsetRaw === U32_MAX,
495
+ diskStart: diskStartRaw === U16_MAX,
496
+ },
497
+ );
498
+ if (values.diskStart !== 0) throw new ArchiveError("Multi-volume ZIP archives are not supported");
499
+ const rawPath =
500
+ extra.unicodePath ?? ((flags & UTF8_FLAG) !== 0 ? UTF8_DECODER : LEGACY_NAME_DECODER).decode(rawName);
501
+ assertArchivePathString(rawPath, "member path", options.limits.maxPathBytes);
502
+ if ((flags & (ENCRYPTED_FLAG | STRONG_ENCRYPTION_FLAG)) !== 0 || method === 99) {
503
+ throw new ArchiveError(`Encrypted ZIP member '${rawPath}' is not supported`);
504
+ }
505
+ const normalizedPath = normalizeArchiveEntryPath(rawPath);
506
+ if (normalizedPath) {
507
+ assertArchiveMemberSize(
508
+ Math.max(values.uncompressedSize, values.compressedSize),
509
+ normalizedPath,
510
+ options.limits,
511
+ );
512
+ const host = versionMadeBy >>> 8;
513
+ const mode = host === UNIX_HOST || host === OSX_HOST ? externalAttributes >>> 16 : undefined;
514
+ const fileType = mode === undefined ? 0 : mode & FILE_TYPE_MASK;
515
+ const isDirectory =
516
+ isArchiveDirectoryName(rawPath) || fileType === DIRECTORY_TYPE || (externalAttributes & 0x10) !== 0;
517
+ const isSymlink = fileType === SYMLINK_TYPE && !isDirectory;
518
+ const localHeaderOffset = values.localHeaderOffset + info.archiveOffset;
519
+ checkedEnd(localHeaderOffset, 30, source.size, `local header for '${normalizedPath}'`);
520
+ const member = new ZipMemberSource(
521
+ source,
522
+ values.compressedSize,
523
+ method,
524
+ flags,
525
+ crc,
526
+ localHeaderOffset,
527
+ options.limits,
528
+ );
529
+ const entry: ArchiveIndexEntry = {
530
+ path: normalizedPath,
531
+ isDirectory,
532
+ size: isDirectory ? 0 : values.uncompressedSize,
533
+ mtimeMs: extra.mtimeMs ?? parseDosMtime(dosTime, dosDate),
534
+ mode: mode || undefined,
535
+ storage: isDirectory ? undefined : { type: "member", source: member },
536
+ };
537
+ parsed.push({ entry, isSymlink });
538
+ assertEntryCount(parsed.length, options.limits);
539
+ }
540
+ offset = end;
541
+ }
542
+ return parsed;
543
+ }
544
+
545
+ async function readZipImpl(source: ByteSource, options: FormatReadOptions): Promise<ArchiveIndexEntry[]> {
546
+ const info = await readCentralDirectoryInfo(source, options.limits);
547
+ if (info.size === 0) return [];
548
+ const directory = await source.read(info.offset, info.offset + info.size);
549
+ if (directory.byteLength !== info.size) throw new ArchiveError("Invalid ZIP archive: truncated central directory");
550
+ const parsed = parseCentralDirectory(source, directory, info, options);
551
+ for (const item of parsed) {
552
+ if (!item.isSymlink) continue;
553
+ assertArchivePathBytes(item.entry.size, "symlink target", options.limits.maxPathBytes);
554
+ if (item.entry.storage?.type !== "member") throw new ArchiveError("Invalid ZIP archive: symlink has no payload");
555
+ const bytes = await item.entry.storage.source.read(item.entry.size, item.entry.path);
556
+ const target = UTF8_DECODER.decode(bytes);
557
+ assertArchivePathString(target, "symlink target", options.limits.maxPathBytes);
558
+ const portable = target.replace(/\\/g, "/");
559
+ const absolute = path.posix.isAbsolute(portable) || /^[A-Za-z]:/.test(portable) || portable.includes("\0");
560
+ const targetPath = absolute
561
+ ? undefined
562
+ : normalizeArchiveLookupPath(path.posix.join(path.posix.dirname(item.entry.path), portable));
563
+ item.entry.size = 0;
564
+ item.entry.storage = {
565
+ type: "link",
566
+ targetPath: targetPath === undefined ? portable : targetPath,
567
+ resolveTarget: targetPath !== undefined,
568
+ };
569
+ }
570
+ return parsed.map(item => item.entry);
571
+ }
572
+
573
+ /** Index a ZIP/ZIP64 archive lazily from its central directory. */
574
+ export const readZip: FormatReader = async (source, options) => {
575
+ try {
576
+ return await readZipImpl(source, options);
577
+ } catch (error) {
578
+ throw archiveError(error, "Failed to read ZIP archive");
579
+ }
580
+ };
581
+
582
+ /** Detect a ZIP local header, empty-archive end record, or leading data descriptor. */
583
+ export function sniffZip(bytes: Uint8Array): boolean {
584
+ if (bytes.byteLength < 4) return false;
585
+ const signature = readUInt32LE(bytes, 0);
586
+ return (
587
+ signature === LOCAL_HEADER_SIGNATURE || signature === EOCD_SIGNATURE || signature === DATA_DESCRIPTOR_SIGNATURE
588
+ );
589
+ }
590
+
591
+ /** Materialize every regular ZIP member into a path-to-bytes map for document converters. */
592
+ export async function readZipEager(
593
+ bytes: Uint8Array,
594
+ limits: ArchiveLimits = DEFAULT_ARCHIVE_LIMITS,
595
+ ): Promise<Map<string, Uint8Array>> {
596
+ try {
597
+ const entries = await readZip(memoryByteSource(bytes), { limits });
598
+ const files = new Map<string, Uint8Array>();
599
+ for (const entry of entries) {
600
+ if (entry.isDirectory || entry.storage?.type !== "member") continue;
601
+ files.set(entry.path, await entry.storage.source.read(entry.size, entry.path));
602
+ }
603
+ return files;
604
+ } catch (error) {
605
+ throw archiveError(error, "Failed to eagerly read ZIP archive");
606
+ }
607
+ }
608
+
609
+ /** Encode deterministic stored/deflated ZIP bytes, emitting ZIP64 end records when the entry count requires them. */
610
+ export async function encodeZip(members: Iterable<readonly [string, Uint8Array]>): Promise<Uint8Array> {
611
+ try {
612
+ const localParts: Uint8Array[] = [];
613
+ const centralParts: Uint8Array[] = [];
614
+ let localSize = 0;
615
+ let centralSize = 0;
616
+ let count = 0;
617
+ for (const [inputName, data] of members) {
618
+ const portableName = inputName.replace(/\\/g, "/");
619
+ const normalizedName = normalizeArchiveEntryPath(portableName);
620
+ if (
621
+ !normalizedName ||
622
+ normalizedName !== portableName.replace(/^\.\//, "") ||
623
+ portableName.startsWith("/") ||
624
+ /^[A-Za-z]:/.test(portableName) ||
625
+ portableName.includes("\0")
626
+ ) {
627
+ throw new ArchiveError(`Cannot write unsafe ZIP member path '${inputName}'`);
628
+ }
629
+ const name = normalizedName;
630
+ const nameBytes = TEXT_ENCODER.encode(name);
631
+ if (nameBytes.byteLength > U16_MAX) throw new ArchiveError(`ZIP member path '${name}' is too long to write`);
632
+ if (data.byteLength >= U32_MAX) throw new ArchiveError(`ZIP member '${name}' is too large to write`);
633
+ const deflated = data.byteLength === 0 ? undefined : zlib.deflateRawSync(data);
634
+ const payload = deflated && deflated.byteLength < data.byteLength ? deflated : data;
635
+ const method = payload === data ? 0 : 8;
636
+ if (payload.byteLength >= U32_MAX || localSize >= U32_MAX) {
637
+ throw new ArchiveError("ZIP archive is too large to write member offsets safely");
638
+ }
639
+ const checksum = crc32(data);
640
+ const local = new Uint8Array(30 + nameBytes.byteLength);
641
+ writeUInt32LE(local, 0, LOCAL_HEADER_SIGNATURE);
642
+ writeUInt16LE(local, 4, 20);
643
+ writeUInt16LE(local, 6, UTF8_FLAG);
644
+ writeUInt16LE(local, 8, method);
645
+ writeUInt16LE(local, 10, 0);
646
+ writeUInt16LE(local, 12, 0x21);
647
+ writeUInt32LE(local, 14, checksum);
648
+ writeUInt32LE(local, 18, payload.byteLength);
649
+ writeUInt32LE(local, 22, data.byteLength);
650
+ writeUInt16LE(local, 26, nameBytes.byteLength);
651
+ local.set(nameBytes, 30);
652
+ localParts.push(local, payload);
653
+
654
+ const central = new Uint8Array(46 + nameBytes.byteLength);
655
+ writeUInt32LE(central, 0, CENTRAL_HEADER_SIGNATURE);
656
+ writeUInt16LE(central, 4, (UNIX_HOST << 8) | 20);
657
+ writeUInt16LE(central, 6, 20);
658
+ writeUInt16LE(central, 8, UTF8_FLAG);
659
+ writeUInt16LE(central, 10, method);
660
+ writeUInt16LE(central, 12, 0);
661
+ writeUInt16LE(central, 14, 0x21);
662
+ writeUInt32LE(central, 16, checksum);
663
+ writeUInt32LE(central, 20, payload.byteLength);
664
+ writeUInt32LE(central, 24, data.byteLength);
665
+ writeUInt16LE(central, 28, nameBytes.byteLength);
666
+ writeUInt32LE(central, 38, (0o100644 << 16) >>> 0);
667
+ writeUInt32LE(central, 42, localSize);
668
+ central.set(nameBytes, 46);
669
+ centralParts.push(central);
670
+ localSize += local.byteLength + payload.byteLength;
671
+ centralSize += central.byteLength;
672
+ count++;
673
+ if (!Number.isSafeInteger(localSize + centralSize) || count > U32_MAX) {
674
+ throw new ArchiveError("ZIP archive is too large to write");
675
+ }
676
+ }
677
+ if (localSize >= U32_MAX || centralSize >= U32_MAX) {
678
+ throw new ArchiveError("ZIP archive is too large to write member offsets safely");
679
+ }
680
+ const zip64 = count >= U16_MAX;
681
+ const trailerLength = EOCD_LENGTH + (zip64 ? ZIP64_EOCD_LENGTH + ZIP64_LOCATOR_LENGTH : 0);
682
+ const totalSize = localSize + centralSize + trailerLength;
683
+ if (!Number.isSafeInteger(totalSize)) throw new ArchiveError("ZIP archive is too large to write");
684
+ const output = new Uint8Array(totalSize);
685
+ let outputOffset = 0;
686
+ for (const part of localParts) {
687
+ output.set(part, outputOffset);
688
+ outputOffset += part.byteLength;
689
+ }
690
+ for (const part of centralParts) {
691
+ output.set(part, outputOffset);
692
+ outputOffset += part.byteLength;
693
+ }
694
+ if (zip64) {
695
+ const zip64EocdOffset = outputOffset;
696
+ writeUInt32LE(output, outputOffset, ZIP64_EOCD_SIGNATURE);
697
+ writeUInt64LE(output, outputOffset + 4, 44);
698
+ writeUInt16LE(output, outputOffset + 12, 45);
699
+ writeUInt16LE(output, outputOffset + 14, 45);
700
+ writeUInt64LE(output, outputOffset + 24, count);
701
+ writeUInt64LE(output, outputOffset + 32, count);
702
+ writeUInt64LE(output, outputOffset + 40, centralSize);
703
+ writeUInt64LE(output, outputOffset + 48, localSize);
704
+ outputOffset += ZIP64_EOCD_LENGTH;
705
+ writeUInt32LE(output, outputOffset, ZIP64_LOCATOR_SIGNATURE);
706
+ writeUInt64LE(output, outputOffset + 8, zip64EocdOffset);
707
+ writeUInt32LE(output, outputOffset + 16, 1);
708
+ outputOffset += ZIP64_LOCATOR_LENGTH;
709
+ }
710
+ writeUInt32LE(output, outputOffset, EOCD_SIGNATURE);
711
+ writeUInt16LE(output, outputOffset + 8, zip64 ? U16_MAX : count);
712
+ writeUInt16LE(output, outputOffset + 10, zip64 ? U16_MAX : count);
713
+ writeUInt32LE(output, outputOffset + 12, centralSize);
714
+ writeUInt32LE(output, outputOffset + 16, localSize);
715
+ return output;
716
+ } catch (error) {
717
+ throw archiveError(error, "Failed to encode ZIP archive");
718
+ }
719
+ }