@linxiraos/pi-utils 1.0.9 → 1.1.0
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 +2 -477
- 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/binary.d.ts +6 -0
- package/dist/types/dirs.d.ts +10 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/postmortem.d.ts +33 -0
- package/dist/types/sqlite.d.ts +23 -0
- package/package.json +10 -4
- 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/binary.ts +6 -2
- package/src/browsers.ts +2 -149
- package/src/dirs.ts +24 -4
- package/src/docx/converter.ts +9 -9
- package/src/env.ts +36 -5
- package/src/index.ts +1 -0
- package/src/logger.ts +19 -15
- package/src/postmortem.ts +74 -0
- package/src/sqlite.ts +33 -0
- package/src/stream.ts +11 -23
- package/src/vendor/mermaid-ascii/ascii/grid.ts +16 -1
- package/dist/types/docx/zip.d.ts +0 -6
- package/src/docx/zip.ts +0 -87
package/dist/types/dirs.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare const CONFIG_DIR_NAME: string;
|
|
|
18
18
|
export declare const MAIN_CONFIG_FILENAMES: readonly ["config.yml", "config.yaml"];
|
|
19
19
|
/** Version (e.g. "1.0.0") */
|
|
20
20
|
export declare const VERSION: string;
|
|
21
|
-
/** Default User-Agent header string (e.g. "
|
|
21
|
+
/** Default User-Agent header string (e.g. "zeta/1.0.10") */
|
|
22
22
|
export declare const USER_AGENT: string;
|
|
23
23
|
/** Minimum Bun version */
|
|
24
24
|
export declare const MIN_BUN_VERSION: string;
|
|
@@ -189,6 +189,8 @@ export declare function getGpuCachePath(): string;
|
|
|
189
189
|
* cache file without touching the rest of the config root.
|
|
190
190
|
*/
|
|
191
191
|
export declare function getGithubCacheDbPath(): string;
|
|
192
|
+
/** Get the legacy Pi extension parse cache database path. */
|
|
193
|
+
export declare function getLegacyPiExtensionCacheDbPath(): string;
|
|
192
194
|
/**
|
|
193
195
|
* Get the encrypted auth-broker snapshot cache path (~/.zeta/cache/auth-broker-snapshot.enc).
|
|
194
196
|
* Honors the `OMP_AUTH_BROKER_SNAPSHOT_CACHE` env var when set so tests and
|
|
@@ -196,6 +198,9 @@ export declare function getGithubCacheDbPath(): string;
|
|
|
196
198
|
*/
|
|
197
199
|
export declare function getAuthBrokerSnapshotCachePath(): string;
|
|
198
200
|
/** Get the local FastEmbed model cache directory (~/.zeta/cache/fastembed). */
|
|
201
|
+
/** Get the commit-author avatar cache directory (~/.omp/cache/avatars). */
|
|
202
|
+
export declare function getAvatarCacheDir(): string;
|
|
203
|
+
/** Get the local FastEmbed model cache directory (~/.omp/cache/fastembed). */
|
|
199
204
|
export declare function getFastembedCacheDir(): string;
|
|
200
205
|
/** Get the on-demand fastembed runtime install root (~/.zeta/cache/fastembed-runtime). */
|
|
201
206
|
export declare function getFastembedRuntimeDir(): string;
|
|
@@ -251,8 +256,12 @@ export declare function getCrashLogPath(agentDir?: string): string;
|
|
|
251
256
|
export declare function getDebugLogPath(agentDir?: string): string;
|
|
252
257
|
/** Get the secret placeholder key path (~/.zeta/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/zeta/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
|
|
253
258
|
export declare function getSecretPlaceholderKeyPath(): string;
|
|
259
|
+
/** Root directory containing every per-project daemon runtime scope (~/.zeta/run/daemons; XDG default: $XDG_STATE_HOME/zeta/run/daemons). */
|
|
260
|
+
export declare function getDaemonRuntimeRoot(): string;
|
|
254
261
|
/** Get the daemon runtime directory for a project (~/.zeta/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/zeta/run/daemons/<hash>). */
|
|
255
262
|
export declare function getDaemonRuntimeDir(projectDir: string): string;
|
|
263
|
+
/** Root directory containing every machine-global daemon service scope. */
|
|
264
|
+
export declare function getGlobalDaemonRuntimeRoot(): string;
|
|
256
265
|
/** Get a profile-independent runtime directory for a machine-global daemon service. */
|
|
257
266
|
export declare function getGlobalDaemonRuntimeDir(service: string): string;
|
|
258
267
|
/** Get the provider in-flight root directory (~/.zeta/run/provider-inflight; XDG default: $XDG_STATE_HOME/zeta/run/provider-inflight). */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { AbortError, ChildProcess, Exception, NonZeroExitError } from "./ptree.j
|
|
|
28
28
|
export * from "./runtime-install.js";
|
|
29
29
|
export * from "./sanitize-text.js";
|
|
30
30
|
export * from "./snowflake.js";
|
|
31
|
+
export * from "./sqlite.js";
|
|
31
32
|
export * from "./stderr-guard.js";
|
|
32
33
|
export * from "./stream.js";
|
|
33
34
|
export * from "./tab-spacing.js";
|
|
@@ -44,6 +44,39 @@ export type BrokenPipeSource = "ipc-send" | "stdio-write";
|
|
|
44
44
|
export declare function classifyBrokenPipe(err: Error): BrokenPipeSource | undefined;
|
|
45
45
|
/** Whether an EPIPE came from an IPC `send()` to an optional worker. */
|
|
46
46
|
export declare function isIpcSendEpipe(err: Error): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Detect Bun's advanced-serialization (structured-clone) IPC decode failure.
|
|
49
|
+
*
|
|
50
|
+
* When a worker subprocess spawned with `serialization: "advanced"` sends a
|
|
51
|
+
* malformed or truncated frame, Bun raises the decode failure as a
|
|
52
|
+
* process-level `uncaughtException` in the *parent* rather than routing it to
|
|
53
|
+
* the channel's `ipc()` callback (oven-sh/bun#37287). The error is a bare
|
|
54
|
+
* `TypeError: Unable to deserialize data.` whose only own property is `message`
|
|
55
|
+
* — it carries no `code`, no `syscall`, and no `stack`. Matching all four traits
|
|
56
|
+
* keeps unrelated application `TypeError`s (which always carry a populated
|
|
57
|
+
* multi-frame stack) on the fatal path, so a genuine bug is never silently
|
|
58
|
+
* swallowed.
|
|
59
|
+
*
|
|
60
|
+
* Every advanced-serialization channel in this process is an optional worker
|
|
61
|
+
* subsystem (TTS, STT, tiny-title, mnemopi embeddings, JS eval), so one
|
|
62
|
+
* worker's bad frame must fault only that worker — via its own `onExit`/error
|
|
63
|
+
* path — never tear down the whole session. Callers log-and-continue instead of
|
|
64
|
+
* taking the fatal path. Mirrors {@link classifyBrokenPipe} for the send side
|
|
65
|
+
* (#2997, #9158).
|
|
66
|
+
*/
|
|
67
|
+
export declare function isWorkerIpcDeserializeError(err: unknown): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Register a fault/recycle callback for an active advanced-serialization worker
|
|
70
|
+
* IPC channel.
|
|
71
|
+
*
|
|
72
|
+
* Bun surfaces a malformed frame as a process-global `uncaughtException`
|
|
73
|
+
* ({@link isWorkerIpcDeserializeError}) with no way to attribute it to a
|
|
74
|
+
* specific channel, so when one fires every registered handler is invoked to
|
|
75
|
+
* conservatively fault its worker — reject in-flight requests and recycle the
|
|
76
|
+
* subprocess — instead of leaving pending work to await forever. Returns an
|
|
77
|
+
* unregister function; callers MUST unregister when the worker exits.
|
|
78
|
+
*/
|
|
79
|
+
export declare function registerWorkerIpcFaultHandler(handler: (err: Error) => void): () => void;
|
|
47
80
|
/**
|
|
48
81
|
* Treat unhandled stdout EPIPE rejections as a graceful peer disconnect.
|
|
49
82
|
*
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared classifiers for `bun:sqlite` error result codes.
|
|
3
|
+
*
|
|
4
|
+
* Every omp SQLite store (`agent.db` credential/usage store, `models.db` model
|
|
5
|
+
* cache, `history.db`) needs the same two distinctions: a transient BUSY that
|
|
6
|
+
* clears by retrying, and an unrecoverable corruption that never does. Keeping
|
|
7
|
+
* one implementation here prevents the classifiers from drifting between the
|
|
8
|
+
* credential store and the model cache.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* SQLite's busy result-code family — base `SQLITE_BUSY` plus the extended
|
|
12
|
+
* variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
|
|
13
|
+
* and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isSqliteBusyError(err: unknown): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
|
|
18
|
+
* (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
|
|
19
|
+
* and `SQLITE_NOTADB` (the file header is not a database). Unlike
|
|
20
|
+
* {@link isSqliteBusyError}, these never clear by retrying: the store must be
|
|
21
|
+
* repaired or replaced, so callers latch, quarantine, or recreate the file.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isSqliteCorruptionError(err: unknown): boolean;
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@linxiraos/pi-utils",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"description": "Shared utilities for pi packages",
|
|
6
|
-
"homepage": "https://linxira-os.github.io",
|
|
6
|
+
"homepage": "https://linxira-os.github.io/zeta/",
|
|
7
7
|
"author": "Can Boluk",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"directory": "packages/utils"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/
|
|
15
|
+
"url": "https://github.com/Linxira-OS/linxira-zeta/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"utilities",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@linxiraos/pi-natives": "1.0
|
|
34
|
+
"@linxiraos/pi-natives": "1.1.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/bun": "^1.3.14"
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"src",
|
|
44
44
|
"README.md",
|
|
45
45
|
"CHANGELOG.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"THIRD-PARTY-NOTICES.txt",
|
|
46
48
|
"dist/types"
|
|
47
49
|
],
|
|
48
50
|
"exports": {
|
|
@@ -50,6 +52,10 @@
|
|
|
50
52
|
"types": "./dist/types/index.d.ts",
|
|
51
53
|
"import": "./src/index.ts"
|
|
52
54
|
},
|
|
55
|
+
"./ar": {
|
|
56
|
+
"types": "./dist/types/ar/index.d.ts",
|
|
57
|
+
"import": "./src/ar/index.ts"
|
|
58
|
+
},
|
|
53
59
|
"./*": {
|
|
54
60
|
"types": "./dist/types/*.d.ts",
|
|
55
61
|
"import": "./src/*.ts"
|
package/src/ar/arj.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { crc32 } from "./checksums";
|
|
2
|
+
import { ArchiveError } from "./error";
|
|
3
|
+
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize, assertInMemorySize } from "./limits";
|
|
4
|
+
import { decompressLhStatic } from "./lzh";
|
|
5
|
+
import { assertArchivePathBytes, assertArchivePathString, normalizeArchiveEntryPath } from "./paths";
|
|
6
|
+
import { type ByteSource, readAllBytes } from "./source";
|
|
7
|
+
import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
|
|
8
|
+
|
|
9
|
+
const ARJ_SIGNATURE_0 = 0x60;
|
|
10
|
+
const ARJ_SIGNATURE_1 = 0xea;
|
|
11
|
+
const ARJ_MAX_BASIC_HEADER = 2600;
|
|
12
|
+
const LEGACY_DECODER = new TextDecoder("windows-1252");
|
|
13
|
+
|
|
14
|
+
function u16(bytes: Uint8Array, offset: number): number {
|
|
15
|
+
return bytes[offset]! | (bytes[offset + 1]! << 8);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function u32(bytes: Uint8Array, offset: number): number {
|
|
19
|
+
return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function assertRange(bytes: Uint8Array, start: number, end: number, what: string): void {
|
|
23
|
+
if (
|
|
24
|
+
!Number.isSafeInteger(start) ||
|
|
25
|
+
!Number.isSafeInteger(end) ||
|
|
26
|
+
start < 0 ||
|
|
27
|
+
end < start ||
|
|
28
|
+
end > bytes.byteLength
|
|
29
|
+
) {
|
|
30
|
+
throw new ArchiveError(`Invalid ARJ archive: truncated ${what}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dosTimeToMs(value: number): number | undefined {
|
|
35
|
+
if (value === 0) return undefined;
|
|
36
|
+
const year = 1980 + ((value >>> 25) & 0x7f);
|
|
37
|
+
const month = (value >>> 21) & 0x0f;
|
|
38
|
+
const day = (value >>> 16) & 0x1f;
|
|
39
|
+
const hour = (value >>> 11) & 0x1f;
|
|
40
|
+
const minute = (value >>> 5) & 0x3f;
|
|
41
|
+
const second = (value & 0x1f) * 2;
|
|
42
|
+
if (month < 1 || month > 12 || day < 1 || day > 31 || hour > 23 || minute > 59 || second > 59) return undefined;
|
|
43
|
+
return Date.UTC(year, month - 1, day, hour, minute, second);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readCString(bytes: Uint8Array, start: number, end: number, field: string): { value: string; next: number } {
|
|
47
|
+
let terminator = start;
|
|
48
|
+
while (terminator < end && bytes[terminator] !== 0) terminator++;
|
|
49
|
+
if (terminator === end) throw new ArchiveError(`Invalid ARJ ${field}: missing terminator`);
|
|
50
|
+
return { value: LEGACY_DECODER.decode(bytes.subarray(start, terminator)), next: terminator + 1 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ArjBlock {
|
|
54
|
+
bodyStart: number;
|
|
55
|
+
bodySize: number;
|
|
56
|
+
nextOffset: number;
|
|
57
|
+
metadataSize: number;
|
|
58
|
+
isEnd: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseArjBlock(bytes: Uint8Array, offset: number, options: FormatReadOptions): ArjBlock {
|
|
62
|
+
assertRange(bytes, offset, offset + 4, "header signature");
|
|
63
|
+
if (bytes[offset] !== ARJ_SIGNATURE_0 || bytes[offset + 1] !== ARJ_SIGNATURE_1) {
|
|
64
|
+
throw new ArchiveError("Invalid ARJ header signature");
|
|
65
|
+
}
|
|
66
|
+
const bodySize = u16(bytes, offset + 2);
|
|
67
|
+
if (bodySize === 0)
|
|
68
|
+
return { bodyStart: offset + 4, bodySize: 0, nextOffset: offset + 4, metadataSize: 4, isEnd: true };
|
|
69
|
+
if (bodySize < 30 || bodySize > ARJ_MAX_BASIC_HEADER) throw new ArchiveError("Invalid ARJ basic header size");
|
|
70
|
+
const bodyStart = offset + 4;
|
|
71
|
+
const bodyEnd = bodyStart + bodySize;
|
|
72
|
+
assertRange(bytes, bodyStart, bodyEnd + 4, "basic header");
|
|
73
|
+
if (crc32(bytes.subarray(bodyStart, bodyEnd)) !== u32(bytes, bodyEnd)) {
|
|
74
|
+
throw new ArchiveError("Invalid ARJ basic header CRC32");
|
|
75
|
+
}
|
|
76
|
+
let cursor = bodyEnd + 4;
|
|
77
|
+
let extensionCount = 0;
|
|
78
|
+
for (;;) {
|
|
79
|
+
assertRange(bytes, cursor, cursor + 2, "extended header size");
|
|
80
|
+
const extensionSize = u16(bytes, cursor);
|
|
81
|
+
cursor += 2;
|
|
82
|
+
if (extensionSize === 0) break;
|
|
83
|
+
if (++extensionCount > 65_535) throw new ArchiveError("Invalid ARJ archive: too many extended headers");
|
|
84
|
+
assertRange(bytes, cursor, cursor + extensionSize + 4, "extended header");
|
|
85
|
+
if (crc32(bytes.subarray(cursor, cursor + extensionSize)) !== u32(bytes, cursor + extensionSize)) {
|
|
86
|
+
throw new ArchiveError("Invalid ARJ extended header CRC32");
|
|
87
|
+
}
|
|
88
|
+
cursor += extensionSize + 4;
|
|
89
|
+
assertIndexSize(cursor - offset, options.limits, "header metadata");
|
|
90
|
+
}
|
|
91
|
+
return { bodyStart, bodySize, nextOffset: cursor, metadataSize: cursor - offset, isEnd: false };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class ArjBitReader {
|
|
95
|
+
readonly #bytes: Uint8Array;
|
|
96
|
+
#position = 0;
|
|
97
|
+
|
|
98
|
+
constructor(bytes: Uint8Array) {
|
|
99
|
+
this.#bytes = bytes;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
read(count: number): number {
|
|
103
|
+
if (!Number.isInteger(count) || count < 0 || count > 24 || this.#position + count > this.#bytes.byteLength * 8) {
|
|
104
|
+
throw new ArchiveError("Invalid ARJ method-4 compressed data: truncated bitstream");
|
|
105
|
+
}
|
|
106
|
+
let value = 0;
|
|
107
|
+
for (let index = 0; index < count; index++) {
|
|
108
|
+
const position = this.#position++;
|
|
109
|
+
value = value * 2 + ((this.#bytes[position >>> 3]! >>> (7 - (position & 7))) & 1);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
assertZeroPadding(): void {
|
|
115
|
+
while (this.#position < this.#bytes.byteLength * 8) {
|
|
116
|
+
if (this.read(1) !== 0) throw new ArchiveError("Invalid ARJ method-4 compressed data: non-zero trailing bits");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function decompressArjMethod4(packed: Uint8Array, outSize: number): Uint8Array {
|
|
122
|
+
const reader = new ArjBitReader(packed);
|
|
123
|
+
const output = new Uint8Array(outSize);
|
|
124
|
+
let outputPosition = 0;
|
|
125
|
+
while (outputPosition < outSize) {
|
|
126
|
+
let lengthCode = 0;
|
|
127
|
+
let lengthWidth = 0;
|
|
128
|
+
for (; lengthWidth < 7; lengthWidth++) {
|
|
129
|
+
if (reader.read(1) === 0) break;
|
|
130
|
+
lengthCode += 2 ** lengthWidth;
|
|
131
|
+
}
|
|
132
|
+
if (lengthWidth !== 0) lengthCode += reader.read(lengthWidth);
|
|
133
|
+
if (lengthCode === 0) {
|
|
134
|
+
output[outputPosition++] = reader.read(8);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const length = lengthCode + 2;
|
|
138
|
+
if (length > outSize - outputPosition) {
|
|
139
|
+
throw new ArchiveError("Invalid ARJ method-4 compressed data: match exceeds declared size");
|
|
140
|
+
}
|
|
141
|
+
let positionCode = 0;
|
|
142
|
+
let positionWidth = 9;
|
|
143
|
+
for (; positionWidth < 13; positionWidth++) {
|
|
144
|
+
if (reader.read(1) === 0) break;
|
|
145
|
+
positionCode += 2 ** positionWidth;
|
|
146
|
+
}
|
|
147
|
+
positionCode += reader.read(positionWidth);
|
|
148
|
+
if (positionCode >= 26_624 || positionCode >= outputPosition) {
|
|
149
|
+
throw new ArchiveError("Invalid ARJ method-4 compressed data: history distance is out of range");
|
|
150
|
+
}
|
|
151
|
+
let sourcePosition = outputPosition - positionCode - 1;
|
|
152
|
+
for (let index = 0; index < length; index++) output[outputPosition++] = output[sourcePosition++]!;
|
|
153
|
+
}
|
|
154
|
+
reader.assertZeroPadding();
|
|
155
|
+
return output;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
class ArjMemberSource implements MemberSource {
|
|
159
|
+
readonly #archive: Uint8Array;
|
|
160
|
+
readonly #start: number;
|
|
161
|
+
readonly #packedSize: number;
|
|
162
|
+
readonly #method: number;
|
|
163
|
+
readonly #crc: number;
|
|
164
|
+
|
|
165
|
+
constructor(archive: Uint8Array, start: number, packedSize: number, method: number, crc: number) {
|
|
166
|
+
this.#archive = archive;
|
|
167
|
+
this.#start = start;
|
|
168
|
+
this.#packedSize = packedSize;
|
|
169
|
+
this.#method = method;
|
|
170
|
+
this.#crc = crc;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async read(size: number, memberPath: string): Promise<Uint8Array> {
|
|
174
|
+
const packed = this.#archive.subarray(this.#start, this.#start + this.#packedSize);
|
|
175
|
+
let output: Uint8Array;
|
|
176
|
+
switch (this.#method) {
|
|
177
|
+
case 0:
|
|
178
|
+
if (packed.byteLength !== size)
|
|
179
|
+
throw new ArchiveError(`ARJ member '${memberPath}' has inconsistent stored size`);
|
|
180
|
+
output = packed.slice();
|
|
181
|
+
break;
|
|
182
|
+
case 1:
|
|
183
|
+
case 2:
|
|
184
|
+
case 3:
|
|
185
|
+
output = decompressLhStatic(packed, size, 26_624, 5, 17, `ARJ method ${this.#method}`);
|
|
186
|
+
break;
|
|
187
|
+
case 4:
|
|
188
|
+
output = decompressArjMethod4(packed, size);
|
|
189
|
+
break;
|
|
190
|
+
case 8:
|
|
191
|
+
case 9:
|
|
192
|
+
if (size !== 0 || packed.byteLength !== 0) {
|
|
193
|
+
throw new ArchiveError(`ARJ member '${memberPath}' has invalid no-data method sizes`);
|
|
194
|
+
}
|
|
195
|
+
output = new Uint8Array(0);
|
|
196
|
+
break;
|
|
197
|
+
default:
|
|
198
|
+
throw new ArchiveError(`ARJ member '${memberPath}' uses unsupported compression method ${this.#method}`);
|
|
199
|
+
}
|
|
200
|
+
if (output.byteLength !== size)
|
|
201
|
+
throw new ArchiveError(`ARJ member '${memberPath}' extracted to an unexpected size`);
|
|
202
|
+
if (this.#method !== 8 && crc32(output) !== this.#crc) {
|
|
203
|
+
throw new ArchiveError(`ARJ member '${memberPath}' failed CRC32 verification`);
|
|
204
|
+
}
|
|
205
|
+
return output;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeHostPath(rawPath: string, hostOs: number): string {
|
|
210
|
+
let path = rawPath.replaceAll("\\", "/");
|
|
211
|
+
if (hostOs === 1) path = path.replaceAll(">", "/");
|
|
212
|
+
else if (hostOs === 4) path = path.replaceAll(":", "/");
|
|
213
|
+
return path;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Probe whether bytes begin with a CRC-framed ARJ main header. */
|
|
217
|
+
export function sniffArj(bytes: Uint8Array): boolean {
|
|
218
|
+
if (bytes.byteLength < 34 || bytes[0] !== ARJ_SIGNATURE_0 || bytes[1] !== ARJ_SIGNATURE_1) return false;
|
|
219
|
+
const size = u16(bytes, 2);
|
|
220
|
+
if (size < 30 || size > ARJ_MAX_BASIC_HEADER || bytes.byteLength < size + 8) return false;
|
|
221
|
+
const body = bytes.subarray(4, 4 + size);
|
|
222
|
+
return body[0]! >= 30 && body[0]! <= size && body[6] === 2 && crc32(body) === u32(bytes, 4 + size);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Index an ARJ archive and lazily decode stored, static-Huffman, and fast-LZSS members. */
|
|
226
|
+
export const readArj: FormatReader = async (
|
|
227
|
+
source: ByteSource,
|
|
228
|
+
options: FormatReadOptions,
|
|
229
|
+
): Promise<ArchiveIndexEntry[]> => {
|
|
230
|
+
assertInMemorySize(source.size, options.limits);
|
|
231
|
+
let bytes: Uint8Array;
|
|
232
|
+
try {
|
|
233
|
+
bytes = await readAllBytes(source);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error instanceof ArchiveError) throw error;
|
|
236
|
+
throw new ArchiveError(`Unable to read ARJ archive: ${error instanceof Error ? error.message : String(error)}`);
|
|
237
|
+
}
|
|
238
|
+
if (bytes.byteLength !== source.size) throw new ArchiveError("Invalid ARJ archive: truncated data");
|
|
239
|
+
if (!sniffArj(bytes)) throw new ArchiveError("Invalid ARJ archive header");
|
|
240
|
+
|
|
241
|
+
const main = parseArjBlock(bytes, 0, options);
|
|
242
|
+
if (main.isEnd) throw new ArchiveError("Invalid ARJ archive: missing main header");
|
|
243
|
+
const mainFirstHeaderSize = bytes[main.bodyStart]!;
|
|
244
|
+
if (mainFirstHeaderSize < 30 || mainFirstHeaderSize > main.bodySize || bytes[main.bodyStart + 6] !== 2) {
|
|
245
|
+
throw new ArchiveError("Invalid ARJ main header");
|
|
246
|
+
}
|
|
247
|
+
const mainFlags = bytes[main.bodyStart + 4]!;
|
|
248
|
+
if ((mainFlags & 0x01) !== 0) throw new ArchiveError("Encrypted ARJ archives are unsupported");
|
|
249
|
+
if ((mainFlags & 0x04) !== 0) throw new ArchiveError("Multi-volume ARJ archives are unsupported");
|
|
250
|
+
|
|
251
|
+
const entries: ArchiveIndexEntry[] = [];
|
|
252
|
+
let offset = main.nextOffset;
|
|
253
|
+
let metadataSize = main.metadataSize;
|
|
254
|
+
let parsedCount = 0;
|
|
255
|
+
for (;;) {
|
|
256
|
+
const block = parseArjBlock(bytes, offset, options);
|
|
257
|
+
metadataSize += block.metadataSize;
|
|
258
|
+
assertIndexSize(metadataSize, options.limits, "index");
|
|
259
|
+
if (block.isEnd) break;
|
|
260
|
+
assertEntryCount(++parsedCount, options.limits);
|
|
261
|
+
const firstHeaderSize = bytes[block.bodyStart]!;
|
|
262
|
+
if (firstHeaderSize < 30 || firstHeaderSize > block.bodySize) throw new ArchiveError("Invalid ARJ local header");
|
|
263
|
+
const hostOs = bytes[block.bodyStart + 3]!;
|
|
264
|
+
const flags = bytes[block.bodyStart + 4]!;
|
|
265
|
+
const method = bytes[block.bodyStart + 5]!;
|
|
266
|
+
const fileType = bytes[block.bodyStart + 6]!;
|
|
267
|
+
if ((flags & 0x01) !== 0) throw new ArchiveError("Encrypted ARJ members are unsupported");
|
|
268
|
+
if ((flags & 0x0c) !== 0) throw new ArchiveError("Multi-volume ARJ members are unsupported");
|
|
269
|
+
const packedSize = u32(bytes, block.bodyStart + 12);
|
|
270
|
+
const size = u32(bytes, block.bodyStart + 16);
|
|
271
|
+
const fileCrc = u32(bytes, block.bodyStart + 20);
|
|
272
|
+
const accessMode = u16(bytes, block.bodyStart + 26);
|
|
273
|
+
const filename = readCString(
|
|
274
|
+
bytes,
|
|
275
|
+
block.bodyStart + firstHeaderSize,
|
|
276
|
+
block.bodyStart + block.bodySize,
|
|
277
|
+
"filename",
|
|
278
|
+
);
|
|
279
|
+
readCString(bytes, filename.next, block.bodyStart + block.bodySize, "comment");
|
|
280
|
+
assertArchivePathBytes(
|
|
281
|
+
filename.next - (block.bodyStart + firstHeaderSize) - 1,
|
|
282
|
+
"member path",
|
|
283
|
+
options.limits.maxPathBytes,
|
|
284
|
+
);
|
|
285
|
+
const rawPath = normalizeHostPath(filename.value, hostOs);
|
|
286
|
+
assertArchivePathString(rawPath, "member path", options.limits.maxPathBytes);
|
|
287
|
+
assertArchiveMemberSize(size, rawPath || "<unnamed>", options.limits);
|
|
288
|
+
const dataStart = block.nextOffset;
|
|
289
|
+
assertRange(bytes, dataStart, dataStart + packedSize, "member data");
|
|
290
|
+
offset = dataStart + packedSize;
|
|
291
|
+
const path = normalizeArchiveEntryPath(rawPath);
|
|
292
|
+
if (!path) continue;
|
|
293
|
+
const isDirectory = fileType === 3;
|
|
294
|
+
let mode: number | undefined;
|
|
295
|
+
if ((hostOs === 2 || hostOs === 8) && accessMode !== 0) {
|
|
296
|
+
mode = (isDirectory ? 0x4000 : 0x8000) | (accessMode & 0x0fff);
|
|
297
|
+
}
|
|
298
|
+
const rawMtime = u32(bytes, block.bodyStart + 8);
|
|
299
|
+
const mtimeMs =
|
|
300
|
+
hostOs === 2 || hostOs === 8 ? (rawMtime === 0 ? undefined : rawMtime * 1000) : dosTimeToMs(rawMtime);
|
|
301
|
+
entries.push({
|
|
302
|
+
path,
|
|
303
|
+
isDirectory,
|
|
304
|
+
size: isDirectory ? 0 : size,
|
|
305
|
+
mtimeMs,
|
|
306
|
+
mode,
|
|
307
|
+
storage: isDirectory
|
|
308
|
+
? undefined
|
|
309
|
+
: { type: "member", source: new ArjMemberSource(bytes, dataStart, packedSize, method, fileCrc) },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (parsedCount === 0) throw new ArchiveError("Invalid ARJ archive: no members");
|
|
313
|
+
return entries;
|
|
314
|
+
};
|