@oh-my-pi/pi-utils 17.3.8 → 17.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/LICENSE +22 -0
- package/THIRD-PARTY-NOTICES.txt +22909 -0
- package/dist/types/ar/arj.d.ts +5 -0
- package/dist/types/ar/asar.d.ts +7 -0
- package/dist/types/ar/bytes.d.ts +20 -0
- package/dist/types/ar/cab.d.ts +5 -0
- package/dist/types/ar/checksums.d.ts +10 -0
- package/dist/types/ar/codecs/bzip2.d.ts +4 -0
- package/dist/types/ar/codecs/gzip.d.ts +6 -0
- package/dist/types/ar/codecs/lzma.d.ts +6 -0
- package/dist/types/ar/codecs/lzw.d.ts +4 -0
- package/dist/types/ar/codecs/lzx.d.ts +7 -0
- package/dist/types/ar/codecs/xz.d.ts +4 -0
- package/dist/types/ar/codecs/zstd.d.ts +6 -0
- package/dist/types/ar/cpio.d.ts +7 -0
- package/dist/types/ar/deb.d.ts +5 -0
- package/dist/types/ar/entries.d.ts +22 -0
- package/dist/types/ar/error.d.ts +8 -0
- package/dist/types/ar/index.d.ts +11 -0
- package/dist/types/ar/iso.d.ts +5 -0
- package/dist/types/ar/limits.d.ts +33 -0
- package/dist/types/ar/lzh.d.ts +7 -0
- package/dist/types/ar/open.d.ts +45 -0
- package/dist/types/ar/paths.d.ts +18 -0
- package/dist/types/ar/rar/rar4-decoder.d.ts +6 -0
- package/dist/types/ar/rar/rar5-decoder.d.ts +6 -0
- package/dist/types/ar/rar.d.ts +5 -0
- package/dist/types/ar/reader.d.ts +28 -0
- package/dist/types/ar/registry.d.ts +17 -0
- package/dist/types/ar/rpm.d.ts +5 -0
- package/dist/types/ar/sevenzip/decode.d.ts +33 -0
- package/dist/types/ar/sevenzip.d.ts +5 -0
- package/dist/types/ar/source.d.ts +54 -0
- package/dist/types/ar/tar.d.ts +9 -0
- package/dist/types/ar/types.d.ts +99 -0
- package/dist/types/ar/unix-ar.d.ts +7 -0
- package/dist/types/ar/write.d.ts +10 -0
- package/dist/types/ar/zip.d.ts +10 -0
- package/dist/types/dirs.d.ts +6 -0
- package/package.json +9 -3
- package/src/ar/arj.ts +314 -0
- package/src/ar/asar.ts +480 -0
- package/src/ar/bytes.ts +78 -0
- package/src/ar/cab.ts +377 -0
- package/src/ar/checksums.ts +62 -0
- package/src/ar/codecs/bzip2.ts +489 -0
- package/src/ar/codecs/gzip.ts +25 -0
- package/src/ar/codecs/lzma.ts +437 -0
- package/src/ar/codecs/lzw.ts +191 -0
- package/src/ar/codecs/lzx.ts +366 -0
- package/src/ar/codecs/xz.ts +522 -0
- package/src/ar/codecs/zstd.ts +25 -0
- package/src/ar/cpio.ts +389 -0
- package/src/ar/deb.ts +160 -0
- package/src/ar/entries.ts +97 -0
- package/src/ar/error.ts +11 -0
- package/src/ar/index.ts +16 -0
- package/src/ar/iso.ts +712 -0
- package/src/ar/limits.ts +80 -0
- package/src/ar/lzh.ts +659 -0
- package/src/ar/open.ts +225 -0
- package/src/ar/paths.ts +70 -0
- package/src/ar/rar/rar4-decoder.ts +459 -0
- package/src/ar/rar/rar5-decoder.ts +400 -0
- package/src/ar/rar.ts +735 -0
- package/src/ar/reader.ts +162 -0
- package/src/ar/registry.ts +208 -0
- package/src/ar/rpm.ts +320 -0
- package/src/ar/sevenzip/decode.ts +239 -0
- package/src/ar/sevenzip.ts +634 -0
- package/src/ar/source.ts +190 -0
- package/src/ar/tar.ts +771 -0
- package/src/ar/types.ts +131 -0
- package/src/ar/unix-ar.ts +312 -0
- package/src/ar/write.ts +55 -0
- package/src/ar/zip.ts +719 -0
- package/src/browsers.ts +2 -149
- package/src/dirs.ts +17 -2
- package/src/docx/converter.ts +9 -9
- package/dist/types/docx/zip.d.ts +0 -6
- 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
|
|
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
|
|
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
|
-
}
|
package/src/dirs.ts
CHANGED
|
@@ -678,6 +678,11 @@ export function getGithubCacheDbPath(): string {
|
|
|
678
678
|
return dirs.rootSubdir(path.join("cache", "github-cache.db"), "cache");
|
|
679
679
|
}
|
|
680
680
|
|
|
681
|
+
/** Get the legacy Pi extension parse cache database path. */
|
|
682
|
+
export function getLegacyPiExtensionCacheDbPath(): string {
|
|
683
|
+
return dirs.rootSubdir(path.join("cache", "legacy-pi-extension-cache.db"), "cache");
|
|
684
|
+
}
|
|
685
|
+
|
|
681
686
|
/**
|
|
682
687
|
* Get the encrypted auth-broker snapshot cache path (~/.omp/cache/auth-broker-snapshot.enc).
|
|
683
688
|
* Honors the `OMP_AUTH_BROKER_SNAPSHOT_CACHE` env var when set so tests and
|
|
@@ -854,10 +859,20 @@ export function getSecretPlaceholderKeyPath(): string {
|
|
|
854
859
|
return keyPath;
|
|
855
860
|
}
|
|
856
861
|
|
|
862
|
+
/** Root directory containing every per-project daemon runtime scope (~/.omp/run/daemons; XDG default: $XDG_STATE_HOME/omp/run/daemons). */
|
|
863
|
+
export function getDaemonRuntimeRoot(): string {
|
|
864
|
+
return dirs.rootSubdir(path.join("run", "daemons"), "state");
|
|
865
|
+
}
|
|
866
|
+
|
|
857
867
|
/** Get the daemon runtime directory for a project (~/.omp/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/omp/run/daemons/<hash>). */
|
|
858
868
|
export function getDaemonRuntimeDir(projectDir: string): string {
|
|
859
869
|
const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0");
|
|
860
|
-
return
|
|
870
|
+
return path.join(getDaemonRuntimeRoot(), key);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Root directory containing every machine-global daemon service scope. */
|
|
874
|
+
export function getGlobalDaemonRuntimeRoot(): string {
|
|
875
|
+
return path.join(getBaseConfigRoot(), "run", "daemons", "global");
|
|
861
876
|
}
|
|
862
877
|
|
|
863
878
|
/** Get a profile-independent runtime directory for a machine-global daemon service. */
|
|
@@ -865,7 +880,7 @@ export function getGlobalDaemonRuntimeDir(service: string): string {
|
|
|
865
880
|
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(service)) {
|
|
866
881
|
throw new Error(`Invalid global daemon service name: ${JSON.stringify(service)}`);
|
|
867
882
|
}
|
|
868
|
-
return path.join(
|
|
883
|
+
return path.join(getGlobalDaemonRuntimeRoot(), service);
|
|
869
884
|
}
|
|
870
885
|
|
|
871
886
|
/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */
|
package/src/docx/converter.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
659
|
-
const documentXml =
|
|
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(
|
|
664
|
-
contentTypes: parseContentTypes(
|
|
665
|
-
styles: parseStyles(
|
|
666
|
-
numbering: parseNumbering(
|
|
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(
|
|
672
|
+
footnotes: parseFootnotes(archiveEntryText(entries, "word/footnotes.xml")),
|
|
673
673
|
usedFootnotes: [],
|
|
674
674
|
footnoteOrdinals: new Map(),
|
|
675
675
|
};
|
package/dist/types/docx/zip.d.ts
DELETED
|
@@ -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
|
-
}
|