@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/browsers.ts CHANGED
@@ -4,17 +4,10 @@ import type * as fs from "node:fs";
4
4
  import * as fsp from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
- import * as zlib from "node:zlib";
7
+ import { extractArchive } from "./ar";
8
8
 
9
9
  const CHROME_FOR_TESTING_BASE_URL = "https://storage.googleapis.com/chrome-for-testing-public";
10
10
  const CHROME_METADATA_BASE_URL = "https://googlechromelabs.github.io/chrome-for-testing";
11
- const ZIP_LOCAL_FILE_HEADER = 0x04034b50;
12
- const ZIP_CENTRAL_DIRECTORY_HEADER = 0x02014b50;
13
- const ZIP_END_OF_CENTRAL_DIRECTORY = 0x06054b50;
14
- const ZIP_DIRECTORY_MODE = 0o040000;
15
- const ZIP_REGULAR_FILE_MODE = 0o100000;
16
- const ZIP_SYMLINK_MODE = 0o120000;
17
- const ZIP_FILE_TYPE_MASK = 0o170000;
18
11
 
19
12
  /** Supported browser products. */
20
13
  export enum Browser {
@@ -104,16 +97,6 @@ interface PatchVersions {
104
97
  builds: Record<string, { version: string }>;
105
98
  }
106
99
 
107
- interface ZipEntry {
108
- name: string;
109
- method: number;
110
- crc: number;
111
- compressedSize: number;
112
- uncompressedSize: number;
113
- externalAttributes: number;
114
- localHeaderOffset: number;
115
- }
116
-
117
100
  /** Detect the current host's Puppeteer browser platform. */
118
101
  export function detectBrowserPlatform(): BrowserPlatform | undefined {
119
102
  const platform = os.platform();
@@ -259,7 +242,7 @@ export async function install(options: InstallOptions): Promise<InstalledBrowser
259
242
  archivePath,
260
243
  options.downloadProgressCallback,
261
244
  );
262
- await extractZipArchive(archivePath, stagingPath);
245
+ await extractArchive(archivePath, stagingPath);
263
246
  await fsp.mkdir(path.dirname(installPath), { recursive: true });
264
247
  await fsp.rm(installPath, { recursive: true, force: true });
265
248
  await fsp.rename(stagingPath, installPath);
@@ -369,133 +352,3 @@ async function downloadArchive(
369
352
  await file.close();
370
353
  }
371
354
  }
372
-
373
- async function extractZipArchive(archivePath: string, destination: string): Promise<void> {
374
- const archive = await fsp.readFile(archivePath);
375
- const entries = readCentralDirectory(archive);
376
- await fsp.mkdir(destination, { recursive: true });
377
- const root = path.resolve(destination);
378
- for (const entry of entries) {
379
- const outputPath = safeArchivePath(root, entry.name);
380
- const mode = entry.externalAttributes >>> 16;
381
- const type = mode & ZIP_FILE_TYPE_MASK;
382
- if (entry.name.endsWith("/") || type === ZIP_DIRECTORY_MODE) {
383
- await fsp.mkdir(outputPath, { recursive: true });
384
- if (mode & 0o777) await fsp.chmod(outputPath, mode & 0o777);
385
- continue;
386
- }
387
-
388
- const contents = readZipEntry(archive, entry);
389
- await fsp.mkdir(path.dirname(outputPath), { recursive: true });
390
- if (type === ZIP_SYMLINK_MODE) {
391
- const target = contents.toString("utf8");
392
- validateSymlinkTarget(root, outputPath, target);
393
- await fsp.symlink(target, outputPath);
394
- continue;
395
- }
396
- await fsp.writeFile(outputPath, contents, { mode: mode & 0o777 ? mode & 0o777 : 0o644 });
397
- if ((mode & ZIP_FILE_TYPE_MASK) === ZIP_REGULAR_FILE_MODE && mode & 0o777)
398
- await fsp.chmod(outputPath, mode & 0o777);
399
- }
400
- }
401
-
402
- function readCentralDirectory(archive: Buffer): ZipEntry[] {
403
- const minimumOffset = Math.max(0, archive.length - 65_557);
404
- let endOffset = -1;
405
- for (let offset = archive.length - 22; offset >= minimumOffset; offset--) {
406
- if (archive.readUInt32LE(offset) === ZIP_END_OF_CENTRAL_DIRECTORY) {
407
- endOffset = offset;
408
- break;
409
- }
410
- }
411
- if (endOffset < 0) throw new Error("Invalid ZIP archive: central directory was not found");
412
- const disk = archive.readUInt16LE(endOffset + 4);
413
- const centralDisk = archive.readUInt16LE(endOffset + 6);
414
- const entryCount = archive.readUInt16LE(endOffset + 10);
415
- const centralSize = archive.readUInt32LE(endOffset + 12);
416
- const centralOffset = archive.readUInt32LE(endOffset + 16);
417
- if (disk !== 0 || centralDisk !== 0) throw new Error("Multi-disk ZIP archives are not supported");
418
- if (entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff) {
419
- throw new Error("ZIP64 archives are not supported");
420
- }
421
- if (centralOffset + centralSize > endOffset)
422
- throw new Error("Invalid ZIP archive: central directory is out of bounds");
423
-
424
- const entries: ZipEntry[] = [];
425
- let offset = centralOffset;
426
- for (let index = 0; index < entryCount; index++) {
427
- if (offset + 46 > archive.length || archive.readUInt32LE(offset) !== ZIP_CENTRAL_DIRECTORY_HEADER) {
428
- throw new Error("Invalid ZIP archive: malformed central directory entry");
429
- }
430
- const flags = archive.readUInt16LE(offset + 8);
431
- if (flags & 1) throw new Error("Encrypted ZIP entries are not supported");
432
- const nameLength = archive.readUInt16LE(offset + 28);
433
- const extraLength = archive.readUInt16LE(offset + 30);
434
- const commentLength = archive.readUInt16LE(offset + 32);
435
- const end = offset + 46 + nameLength + extraLength + commentLength;
436
- if (end > archive.length) throw new Error("Invalid ZIP archive: truncated central directory entry");
437
- entries.push({
438
- name: archive.subarray(offset + 46, offset + 46 + nameLength).toString("utf8"),
439
- method: archive.readUInt16LE(offset + 10),
440
- crc: archive.readUInt32LE(offset + 16),
441
- compressedSize: archive.readUInt32LE(offset + 20),
442
- uncompressedSize: archive.readUInt32LE(offset + 24),
443
- externalAttributes: archive.readUInt32LE(offset + 38),
444
- localHeaderOffset: archive.readUInt32LE(offset + 42),
445
- });
446
- offset = end;
447
- }
448
- return entries;
449
- }
450
-
451
- function readZipEntry(archive: Buffer, entry: ZipEntry): Buffer {
452
- const offset = entry.localHeaderOffset;
453
- if (offset + 30 > archive.length || archive.readUInt32LE(offset) !== ZIP_LOCAL_FILE_HEADER) {
454
- throw new Error(`Invalid ZIP archive: malformed local header for ${entry.name}`);
455
- }
456
- const nameLength = archive.readUInt16LE(offset + 26);
457
- const extraLength = archive.readUInt16LE(offset + 28);
458
- const start = offset + 30 + nameLength + extraLength;
459
- const end = start + entry.compressedSize;
460
- if (end > archive.length) throw new Error(`Invalid ZIP archive: truncated data for ${entry.name}`);
461
- const compressed = archive.subarray(start, end);
462
- let contents: Buffer;
463
- if (entry.method === 0) contents = Buffer.from(compressed);
464
- else if (entry.method === 8) contents = zlib.inflateRawSync(compressed);
465
- else throw new Error(`Unsupported ZIP compression method ${entry.method} for ${entry.name}`);
466
- if (contents.length !== entry.uncompressedSize)
467
- throw new Error(`Invalid uncompressed size for ZIP entry ${entry.name}`);
468
- if (crc32(contents) !== entry.crc) throw new Error(`CRC mismatch for ZIP entry ${entry.name}`);
469
- return contents;
470
- }
471
-
472
- function safeArchivePath(root: string, name: string): string {
473
- if (!name || name.includes("\0") || name.startsWith("/") || name.startsWith("\\") || /^[A-Za-z]:/.test(name)) {
474
- throw new Error(`Unsafe path in ZIP archive: ${name}`);
475
- }
476
- const segments = name.replaceAll("\\", "/").split("/");
477
- if (segments.some(segment => segment === "..")) throw new Error(`Unsafe path in ZIP archive: ${name}`);
478
- const target = path.resolve(root, ...segments.filter(Boolean));
479
- if (target !== root && !target.startsWith(`${root}${path.sep}`))
480
- throw new Error(`Unsafe path in ZIP archive: ${name}`);
481
- return target;
482
- }
483
-
484
- function validateSymlinkTarget(root: string, linkPath: string, target: string): void {
485
- if (!target || target.includes("\0") || path.isAbsolute(target) || /^[A-Za-z]:/.test(target)) {
486
- throw new Error(`Unsafe symlink target in ZIP archive: ${target}`);
487
- }
488
- const resolved = path.resolve(path.dirname(linkPath), target);
489
- if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
490
- throw new Error(`Unsafe symlink target in ZIP archive: ${target}`);
491
- }
492
- }
493
-
494
- function crc32(contents: Uint8Array): number {
495
- let crc = 0xffffffff;
496
- for (const byte of contents) {
497
- crc ^= byte;
498
- for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
499
- }
500
- return (crc ^ 0xffffffff) >>> 0;
501
- }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { archiveEntryText, readArchiveEntries } from "../ar";
3
4
  import { attribute, childElements, descendants, firstChild, localName, parseXml, type XmlElement } from "./xml";
4
- import { readZip, readZipText, type ZipEntries } from "./zip";
5
5
 
6
6
  /** A mammoth-compatible diagnostic emitted while converting a document. */
7
7
  export interface DocxMessage {
@@ -119,7 +119,7 @@ interface NumberingLevel {
119
119
  }
120
120
 
121
121
  interface ConversionContext {
122
- readonly entries: ZipEntries;
122
+ readonly entries: ReadonlyMap<string, Uint8Array>;
123
123
  readonly relationships: ReadonlyMap<string, Relationship>;
124
124
  readonly contentTypes: ReadonlyMap<string, string>;
125
125
  readonly styles: ReadonlyMap<string, Style>;
@@ -655,21 +655,21 @@ function defaultImageConverter(): ImageConverter {
655
655
  /** Convert a DOCX buffer or path to mammoth-compatible HTML. */
656
656
  export async function convertToHtml(input: DocxInput, options: ConvertToHtmlOptions = {}): Promise<DocxResult> {
657
657
  const bytes = "buffer" in input && input.buffer ? input.buffer : await fs.readFile(input.path);
658
- const entries = readZip(bytes);
659
- const documentXml = readZipText(entries, "word/document.xml");
658
+ const entries = await readArchiveEntries({ bytes, format: "zip" });
659
+ const documentXml = archiveEntryText(entries, "word/document.xml");
660
660
  if (!documentXml) throw new Error("Invalid DOCX: missing word/document.xml");
661
661
  const context: ConversionContext = {
662
662
  entries,
663
- relationships: parseRelationships(readZipText(entries, "word/_rels/document.xml.rels")),
664
- contentTypes: parseContentTypes(readZipText(entries, "[Content_Types].xml")),
665
- styles: parseStyles(readZipText(entries, "word/styles.xml")),
666
- numbering: parseNumbering(readZipText(entries, "word/numbering.xml")),
663
+ relationships: parseRelationships(archiveEntryText(entries, "word/_rels/document.xml.rels")),
664
+ contentTypes: parseContentTypes(archiveEntryText(entries, "[Content_Types].xml")),
665
+ styles: parseStyles(archiveEntryText(entries, "word/styles.xml")),
666
+ numbering: parseNumbering(archiveEntryText(entries, "word/numbering.xml")),
667
667
  messages: [],
668
668
  warnedStyles: new Set(),
669
669
  customStyles: parseCustomStyles(options.styleMap),
670
670
  includeDefaultStyleMap: options.includeDefaultStyleMap !== false,
671
671
  convertImage: options.convertImage ?? defaultImageConverter(),
672
- footnotes: parseFootnotes(readZipText(entries, "word/footnotes.xml")),
672
+ footnotes: parseFootnotes(archiveEntryText(entries, "word/footnotes.xml")),
673
673
  usedFootnotes: [],
674
674
  footnoteOrdinals: new Map(),
675
675
  };
package/src/postmortem.ts CHANGED
@@ -146,6 +146,66 @@ export function isIpcSendEpipe(err: Error): boolean {
146
146
  return classifyBrokenPipe(err) === "ipc-send";
147
147
  }
148
148
 
149
+ /**
150
+ * Detect Bun's advanced-serialization (structured-clone) IPC decode failure.
151
+ *
152
+ * When a worker subprocess spawned with `serialization: "advanced"` sends a
153
+ * malformed or truncated frame, Bun raises the decode failure as a
154
+ * process-level `uncaughtException` in the *parent* rather than routing it to
155
+ * the channel's `ipc()` callback (oven-sh/bun#37287). The error is a bare
156
+ * `TypeError: Unable to deserialize data.` whose only own property is `message`
157
+ * — it carries no `code`, no `syscall`, and no `stack`. Matching all four traits
158
+ * keeps unrelated application `TypeError`s (which always carry a populated
159
+ * multi-frame stack) on the fatal path, so a genuine bug is never silently
160
+ * swallowed.
161
+ *
162
+ * Every advanced-serialization channel in this process is an optional worker
163
+ * subsystem (TTS, STT, tiny-title, mnemopi embeddings, JS eval), so one
164
+ * worker's bad frame must fault only that worker — via its own `onExit`/error
165
+ * path — never tear down the whole session. Callers log-and-continue instead of
166
+ * taking the fatal path. Mirrors {@link classifyBrokenPipe} for the send side
167
+ * (#2997, #9158).
168
+ */
169
+ export function isWorkerIpcDeserializeError(err: unknown): boolean {
170
+ return (
171
+ err instanceof TypeError &&
172
+ err.message === "Unable to deserialize data." &&
173
+ !err.stack &&
174
+ !("code" in err) &&
175
+ !("syscall" in err)
176
+ );
177
+ }
178
+
179
+ /** Recycle callbacks for the active advanced-serialization worker IPC channels. */
180
+ const workerIpcFaultHandlers = new Set<(err: Error) => void>();
181
+
182
+ /**
183
+ * Register a fault/recycle callback for an active advanced-serialization worker
184
+ * IPC channel.
185
+ *
186
+ * Bun surfaces a malformed frame as a process-global `uncaughtException`
187
+ * ({@link isWorkerIpcDeserializeError}) with no way to attribute it to a
188
+ * specific channel, so when one fires every registered handler is invoked to
189
+ * conservatively fault its worker — reject in-flight requests and recycle the
190
+ * subprocess — instead of leaving pending work to await forever. Returns an
191
+ * unregister function; callers MUST unregister when the worker exits.
192
+ */
193
+ export function registerWorkerIpcFaultHandler(handler: (err: Error) => void): () => void {
194
+ workerIpcFaultHandlers.add(handler);
195
+ return () => workerIpcFaultHandlers.delete(handler);
196
+ }
197
+
198
+ /** Invoke every registered worker IPC fault handler, isolating handler throws. */
199
+ function faultWorkerIpcChannels(err: Error): void {
200
+ for (const handler of workerIpcFaultHandlers) {
201
+ try {
202
+ handler(err);
203
+ } catch (handlerErr) {
204
+ logger.warn("Worker IPC fault handler threw", { err: handlerErr });
205
+ }
206
+ }
207
+ }
208
+
149
209
  /**
150
210
  * Treat unhandled stdout EPIPE rejections as a graceful peer disconnect.
151
211
  *
@@ -283,6 +343,20 @@ if (isMainThread) {
283
343
  logger.warn("Ignoring expected cleanup exception", { err });
284
344
  return;
285
345
  }
346
+ // A malformed advanced-serialization frame from a worker subprocess
347
+ // surfaces here as a process-level uncaughtException (oven-sh/bun#37287)
348
+ // rather than in the channel's ipc() callback, and Bun gives no way to
349
+ // tell which channel produced it. Contain it to the worker layer: keep
350
+ // the session alive and conservatively fault every active advanced-IPC
351
+ // worker so its owning client rejects in-flight requests and recycles
352
+ // the subprocess — a worker that sent a bad frame but stays alive would
353
+ // otherwise never fire onExit and leave callers awaiting forever.
354
+ // Mirrors the ipc-send EPIPE containment below (#9158, #2997).
355
+ if (isWorkerIpcDeserializeError(err)) {
356
+ logger.warn("Malformed worker IPC frame; faulting active worker subsystems", { err });
357
+ faultWorkerIpcChannels(err);
358
+ return;
359
+ }
286
360
  await exitAfterFatal("Uncaught Exception", "Uncaught exception", err, Reason.UNCAUGHT_EXCEPTION);
287
361
  })
288
362
  .on("unhandledRejection", async reason => {
@@ -1,6 +0,0 @@
1
- /** The decompressed members of a DOCX ZIP package. */
2
- export type ZipEntries = ReadonlyMap<string, Uint8Array>;
3
- /** Decode a ZIP package by walking its central directory. */
4
- export declare function readZip(bytes: Uint8Array): ZipEntries;
5
- /** Decode a ZIP member as UTF-8 text. */
6
- export declare function readZipText(entries: ZipEntries, name: string): string | undefined;
package/src/docx/zip.ts DELETED
@@ -1,87 +0,0 @@
1
- import * as zlib from "node:zlib";
2
-
3
- const LOCAL_FILE_SIGNATURE = 0x04034b50;
4
- const CENTRAL_FILE_SIGNATURE = 0x02014b50;
5
- const END_SIGNATURE = 0x06054b50;
6
- const MAX_END_SEARCH = 65_557;
7
- const UTF8 = new TextDecoder();
8
-
9
- /** The decompressed members of a DOCX ZIP package. */
10
- export type ZipEntries = ReadonlyMap<string, Uint8Array>;
11
-
12
- function view(bytes: Uint8Array): DataView {
13
- return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
14
- }
15
-
16
- function findEndRecord(bytes: Uint8Array): number {
17
- const data = view(bytes);
18
- for (
19
- let offset = bytes.byteLength - 22, limit = Math.max(0, bytes.byteLength - MAX_END_SEARCH);
20
- offset >= limit;
21
- offset--
22
- ) {
23
- if (data.getUint32(offset, true) === END_SIGNATURE) return offset;
24
- }
25
- throw new Error("Invalid DOCX: missing ZIP end record");
26
- }
27
-
28
- /** Decode a ZIP package by walking its central directory. */
29
- export function readZip(bytes: Uint8Array): ZipEntries {
30
- if (bytes.byteLength < 22) throw new Error("Invalid DOCX: truncated ZIP package");
31
- const data = view(bytes);
32
- const endOffset = findEndRecord(bytes);
33
- const entryCount = data.getUint16(endOffset + 10, true);
34
- const centralSize = data.getUint32(endOffset + 12, true);
35
- const centralOffset = data.getUint32(endOffset + 16, true);
36
- if (entryCount === 0xffff || centralSize === 0xffffffff || centralOffset === 0xffffffff) {
37
- throw new Error("Invalid DOCX: ZIP64 packages are not supported");
38
- }
39
- if (centralOffset + centralSize > bytes.byteLength) throw new Error("Invalid DOCX: truncated ZIP central directory");
40
- const entries = new Map<string, Uint8Array>();
41
- let offset = centralOffset;
42
- for (let index = 0; index < entryCount; index++) {
43
- if (offset + 46 > bytes.byteLength || data.getUint32(offset, true) !== CENTRAL_FILE_SIGNATURE) {
44
- throw new Error("Invalid DOCX: malformed ZIP central directory");
45
- }
46
- const flags = data.getUint16(offset + 8, true);
47
- const method = data.getUint16(offset + 10, true);
48
- const compressedSize = data.getUint32(offset + 20, true);
49
- const uncompressedSize = data.getUint32(offset + 24, true);
50
- const nameLength = data.getUint16(offset + 28, true);
51
- const extraLength = data.getUint16(offset + 30, true);
52
- const commentLength = data.getUint16(offset + 32, true);
53
- const localOffset = data.getUint32(offset + 42, true);
54
- const entryEnd = offset + 46 + nameLength + extraLength + commentLength;
55
- if (entryEnd > bytes.byteLength) throw new Error("Invalid DOCX: truncated ZIP entry");
56
- const name = UTF8.decode(bytes.subarray(offset + 46, offset + 46 + nameLength)).replaceAll("\\", "/");
57
- if ((flags & 1) !== 0) throw new Error(`Invalid DOCX: encrypted ZIP member ${name}`);
58
- if (localOffset + 30 > bytes.byteLength || data.getUint32(localOffset, true) !== LOCAL_FILE_SIGNATURE) {
59
- throw new Error(`Invalid DOCX: missing local header for ${name}`);
60
- }
61
- const localNameLength = data.getUint16(localOffset + 26, true);
62
- const localExtraLength = data.getUint16(localOffset + 28, true);
63
- const payloadOffset = localOffset + 30 + localNameLength + localExtraLength;
64
- const payloadEnd = payloadOffset + compressedSize;
65
- if (payloadEnd > bytes.byteLength) throw new Error(`Invalid DOCX: truncated ZIP member ${name}`);
66
- const compressed = bytes.subarray(payloadOffset, payloadEnd);
67
- let decoded: Uint8Array;
68
- if (method === 0) {
69
- decoded = compressed;
70
- } else if (method === 8) {
71
- decoded = zlib.inflateRawSync(compressed, { maxOutputLength: Math.max(uncompressedSize, 1) });
72
- } else {
73
- throw new Error(`Invalid DOCX: unsupported ZIP compression method ${method}`);
74
- }
75
- if (decoded.byteLength !== uncompressedSize)
76
- throw new Error(`Invalid DOCX: size mismatch for ZIP member ${name}`);
77
- if (name && !name.endsWith("/")) entries.set(name, decoded);
78
- offset = entryEnd;
79
- }
80
- return entries;
81
- }
82
-
83
- /** Decode a ZIP member as UTF-8 text. */
84
- export function readZipText(entries: ZipEntries, name: string): string | undefined {
85
- const bytes = entries.get(name);
86
- return bytes ? UTF8.decode(bytes) : undefined;
87
- }