@linxiraos/pi-utils 1.4.7 → 1.4.10
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 -8
- package/dist/types/acp/schema.d.ts +1 -1
- package/dist/types/ar/codecs/xz.d.ts +4 -0
- package/dist/types/ar/lzh.d.ts +2 -0
- package/dist/types/async.d.ts +5 -0
- package/dist/types/bytes.d.ts +7 -0
- package/dist/types/color.d.ts +2 -0
- package/dist/types/env.d.ts +1 -0
- package/dist/types/file-lock.d.ts +6 -0
- package/dist/types/held-file.d.ts +5 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/pid.d.ts +8 -0
- package/dist/types/postmortem.d.ts +3 -2
- package/dist/types/sqlite.d.ts +12 -0
- package/package.json +3 -2
- package/src/acp/schema.ts +53 -107
- package/src/ar/arj.ts +13 -32
- package/src/ar/asar.ts +5 -8
- package/src/ar/codecs/xz.ts +7 -8
- package/src/ar/lzh.ts +21 -27
- package/src/ar/sevenzip/decode.ts +1 -56
- package/src/ar/tar.ts +1 -8
- package/src/async.ts +13 -0
- package/src/bytes.ts +14 -0
- package/src/color.ts +14 -2
- package/src/env.ts +46 -2
- package/src/file-lock.ts +9 -0
- package/src/held-file.ts +22 -0
- package/src/index.ts +2 -0
- package/src/logger.ts +2 -10
- package/src/pid.ts +16 -0
- package/src/postmortem.ts +61 -33
- package/src/runtime-install.ts +1 -4
- package/src/sqlite.ts +32 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [1.4.
|
|
4
|
-
|
|
5
|
-
- Synced with upstream OMP v18.14.0; no package-specific user-visible changes.
|
|
3
|
+
## [1.4.10] - 2026-11-05
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
- Legacy OMP dotenv files are no longer loaded by default; set `ZETA_OMP_COMPAT=1` to opt into the read-only fallback. Zeta `.zeta` dotenv files keep precedence.
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
## [1.4.9] - 2026-11-03
|
|
10
8
|
|
|
11
|
-
-
|
|
9
|
+
- Synced with upstream OMP v18.16.0; no package-specific user-visible changes.
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
## [1.4.7] - 2026-10-30
|
|
14
12
|
|
|
15
|
-
-
|
|
13
|
+
- Synced with upstream OMP v18.14.0; no package-specific user-visible changes.
|
|
16
14
|
|
|
17
15
|
## [1.4.5] - 2026-10-28
|
|
18
16
|
|
|
@@ -2,7 +2,7 @@ import type { ForkSessionResponse, LoadSessionResponse, NewSessionResponse, Prom
|
|
|
2
2
|
/** Validation failure compatible with the used Zod error surface. */
|
|
3
3
|
export interface ValidationError {
|
|
4
4
|
issues: Array<{
|
|
5
|
-
path:
|
|
5
|
+
path: PropertyKey[];
|
|
6
6
|
message: string;
|
|
7
7
|
}>;
|
|
8
8
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Undo the delta filter (shared by XZ and 7z) in place. */
|
|
2
|
+
export declare function deltaDecode(bytes: Uint8Array, distance: number): void;
|
|
3
|
+
/** Undo the x86 BCJ filter (shared by XZ and 7z) in place; `startOffset` is the stream position of `bytes[0]`. */
|
|
4
|
+
export declare function x86Decode(bytes: Uint8Array, startOffset: number): void;
|
|
1
5
|
/** Whether bytes begin with the XZ stream-header magic. */
|
|
2
6
|
export declare function isXz(bytes: Uint8Array): boolean;
|
|
3
7
|
/** Decompress all concatenated streams in an XZ container within `maxOutput`. */
|
package/dist/types/ar/lzh.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { FormatReader } from "./types.js";
|
|
2
2
|
/** @internal Decode the static-Huffman LZSS stream shared by LZH and ARJ methods 1-3. */
|
|
3
3
|
export declare function decompressLhStatic(packed: Uint8Array, outSize: number, dictionarySize: number, positionBits: number, positionSymbols: number, label: string): Uint8Array;
|
|
4
|
+
/** MS-DOS packed date/time as UTC milliseconds; `undefined` for 0 or an out-of-range field. */
|
|
5
|
+
export declare function dosTimeToMs(value: number): number | undefined;
|
|
4
6
|
/** Probe whether bytes begin with an LZH/LHA member header. */
|
|
5
7
|
export declare function sniffLzh(bytes: Uint8Array): boolean;
|
|
6
8
|
/** Index an LZH/LHA archive and lazily decode its members from the bounded archive buffer. */
|
package/dist/types/async.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export declare const MAX_TIMER_DELAY_MS = 2147483647;
|
|
|
16
16
|
* the unelapsed duration instead of shortening the requested sleep.
|
|
17
17
|
*/
|
|
18
18
|
export declare function sleepLong(delayMs: number, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Wait for a wall-clock deadline, including time spent suspended. Some platforms
|
|
21
|
+
* exclude suspend from relative timers, so recheck at least once per second.
|
|
22
|
+
*/
|
|
23
|
+
export declare function sleepUntil(deadlineMs: number, signal?: AbortSignal): Promise<void>;
|
|
19
24
|
/**
|
|
20
25
|
* Wrap a promise with a timeout and optional abort signal.
|
|
21
26
|
* Rejects with the given error or a new error containing the given message if
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow `bytes` to a view over its own whole `ArrayBuffer`, which is what
|
|
3
|
+
* `crypto.subtle.{digest,sign,importKey,encrypt,decrypt}` require under the
|
|
4
|
+
* strict TS DOM typings. Returns `bytes` itself when it already is one,
|
|
5
|
+
* otherwise a copy.
|
|
6
|
+
*/
|
|
7
|
+
export declare function asStrictBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer>;
|
package/dist/types/color.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export interface OKLCH {
|
|
|
99
99
|
* hues, so carrying them between colors preserves the palette's "weight".
|
|
100
100
|
*/
|
|
101
101
|
export declare function hexToOklch(hex: string): OKLCH;
|
|
102
|
+
/** Perceptual distance between two hex colors: Euclidean OKLab, 0 when identical, 1 from black to white. */
|
|
103
|
+
export declare function oklabDistance(a: string, b: string): number;
|
|
102
104
|
/**
|
|
103
105
|
* The sRGB gamut cusp for an OKLCH hue: the lightness/chroma point where the
|
|
104
106
|
* hue reaches its maximum chroma inside sRGB.
|
package/dist/types/env.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export declare function getDotenvEnvValues(cwd?: string, env?: Record<string, st
|
|
|
44
44
|
* only shell-identifier names and spawn-safe values.
|
|
45
45
|
*/
|
|
46
46
|
export declare function parseEnvFile(filePath: string): Record<string, string>;
|
|
47
|
+
export declare function isOmpCompatEnabled(env?: Record<string, string | undefined>): boolean;
|
|
47
48
|
/**
|
|
48
49
|
* Intentional re-export of Bun.env.
|
|
49
50
|
*
|
|
@@ -16,6 +16,12 @@ declare function getLockPath(filePath: string): string;
|
|
|
16
16
|
declare function tryAcquireLock(lockPath: string): NativeFileLock | null;
|
|
17
17
|
/** Acquire an exclusive lease without waiting; `null` while another holder owns it. */
|
|
18
18
|
export declare function tryAcquireFileLock(filePath: string): FileLockHandle | null;
|
|
19
|
+
/**
|
|
20
|
+
* Whether any holder, including this process, owns the lease, observed without
|
|
21
|
+
* taking it, so a concurrent `tryAcquireFileLock` never meets the probe.
|
|
22
|
+
* Throws when the lock cannot be probed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isFileLockHeld(filePath: string): boolean;
|
|
19
25
|
/** Acquire an exclusive lease; callers must release it when their operation ends. */
|
|
20
26
|
export declare function acquireFileLock(filePath: string, options?: FileLockOptions): Promise<FileLockHandle>;
|
|
21
27
|
/** Run `fn` while holding an OS-backed exclusive lock for `filePath`. */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { once, untilAborted } from "./abortable.js";
|
|
2
2
|
export * from "./async.js";
|
|
3
3
|
export * from "./binary.js";
|
|
4
|
+
export * from "./bytes.js";
|
|
4
5
|
export * from "./color.js";
|
|
5
6
|
export * from "./dirs.js";
|
|
6
7
|
export * from "./env.js";
|
|
@@ -23,6 +24,7 @@ export * from "./path.js";
|
|
|
23
24
|
export * from "./path-tree.js";
|
|
24
25
|
export * from "./ports.js";
|
|
25
26
|
export * from "./peek-file.js";
|
|
27
|
+
export * from "./pid.js";
|
|
26
28
|
export * as postmortem from "./postmortem.js";
|
|
27
29
|
export * from "./process-name.js";
|
|
28
30
|
export * as procmgr from "./procmgr.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal-0 liveness probe: true unless the OS reports the pid as gone
|
|
3
|
+
* (`ESRCH`) or invalid (`EINVAL`). Any other failure, notably `EPERM` (alive
|
|
4
|
+
* but owned by another user), reads as alive, so callers never reclaim a
|
|
5
|
+
* resource a live process may still hold. Unlike `procmgr.isPidRunning`, this
|
|
6
|
+
* does not consult the process table's run state.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
@@ -178,6 +178,8 @@ export declare function registerFatalRecoveryHint(provider: FatalRecoveryHintPro
|
|
|
178
178
|
* Reports a caught top-level failure after terminal owners restore their display, then exits.
|
|
179
179
|
*/
|
|
180
180
|
export declare function fatal(error: unknown): Promise<never>;
|
|
181
|
+
/** Installs OMP's process-wide signal and fatal handlers for a CLI-owned process. */
|
|
182
|
+
export declare function installProcessHandlers(): void;
|
|
181
183
|
/** Controls when a registered cleanup callback participates in cleanup passes. */
|
|
182
184
|
export interface CleanupRegistrationOptions {
|
|
183
185
|
/**
|
|
@@ -201,8 +203,7 @@ export interface CleanupRegistrationOptions {
|
|
|
201
203
|
waitMessage?: string;
|
|
202
204
|
}
|
|
203
205
|
/**
|
|
204
|
-
* Registers a cleanup callback for
|
|
205
|
-
* repeatable manual cleanup passes.
|
|
206
|
+
* Registers a cleanup callback for manual passes and, when installed, process shutdown.
|
|
206
207
|
*
|
|
207
208
|
* Registrations persist across keep-alive {@link cleanup} passes and run at
|
|
208
209
|
* most once per pass. Set `exitOnly` for resources the continuing process still
|
package/dist/types/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** Shared SQLite opening, error attribution, and result-code classification for persistent stores. */
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
+
/**
|
|
4
|
+
* Moves this process's SQLite locks onto open-file-description locks (Linux), once.
|
|
5
|
+
*
|
|
6
|
+
* SQLite's POSIX record locks belong to the process, so any close of another
|
|
7
|
+
* descriptor of a database or its `-shm` released them, including Bun's own file
|
|
8
|
+
* APIs and the bash tool's builtins; a peer omp then unlinked the WAL under the live
|
|
9
|
+
* connection (issue #15586). Call before the process opens its first database: a
|
|
10
|
+
* connection opened earlier keeps POSIX locks. Returns whether the shim is active; a
|
|
11
|
+
* Linux build where it cannot be installed (Bun's SQLite layout changed) logs a warning
|
|
12
|
+
* once, and callers with a UI surface it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function ensureSqliteLockShim(): boolean;
|
|
3
15
|
/**
|
|
4
16
|
* A store's identity as `dev:ino:birthtimeMs`, `null` when the path holds no
|
|
5
17
|
* file, or `undefined` when the stat itself failed and no conclusion is safe.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxiraos/pi-utils",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.10",
|
|
4
4
|
"description": "Shared utilities for pi packages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"utilities",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@linxiraos/pi-natives": "1.4.
|
|
57
|
+
"@linxiraos/pi-natives": "1.4.10",
|
|
58
|
+
"@linxiraos/pi-omptype": "1.4.10"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/bun": "1.4.4"
|
package/src/acp/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Type, type } from "@linxiraos/pi-omptype";
|
|
1
2
|
import type {
|
|
2
3
|
ForkSessionResponse,
|
|
3
4
|
LoadSessionResponse,
|
|
@@ -8,7 +9,7 @@ import type {
|
|
|
8
9
|
|
|
9
10
|
/** Validation failure compatible with the used Zod error surface. */
|
|
10
11
|
export interface ValidationError {
|
|
11
|
-
issues: Array<{ path:
|
|
12
|
+
issues: Array<{ path: PropertyKey[]; message: string }>;
|
|
12
13
|
}
|
|
13
14
|
/** Successful validation result. */
|
|
14
15
|
export interface ValidationSuccess<T> {
|
|
@@ -26,129 +27,74 @@ export interface Validator<T> {
|
|
|
26
27
|
parse(value: unknown): T;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Wraps a structural schema in the Zod call shape. Schemas check the fields
|
|
32
|
+
* ACP consumers read and keep every other key, so the result is the full
|
|
33
|
+
* protocol value.
|
|
34
|
+
*/
|
|
35
|
+
function validator<T>(schema: Type, label: string): Validator<T> {
|
|
30
36
|
return {
|
|
31
37
|
safeParse(value) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
const result = schema(value);
|
|
39
|
+
if (!(result instanceof type.errors)) return { success: true, data: result as T };
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
error: { issues: result.map(issue => ({ path: [...issue.path], message: issue.message })) },
|
|
43
|
+
};
|
|
35
44
|
},
|
|
36
45
|
parse(value) {
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const result = schema(value);
|
|
47
|
+
if (result instanceof type.errors) throw new Error(`Invalid ${label}: ${result.summary}`);
|
|
48
|
+
return result as T;
|
|
39
49
|
},
|
|
40
50
|
};
|
|
41
51
|
}
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validModes(value: unknown): boolean {
|
|
48
|
-
if (value === undefined || value === null) return true;
|
|
49
|
-
if (typeof value !== "object" || value === null) return false;
|
|
50
|
-
const modes = value as Record<string, unknown>;
|
|
51
|
-
return (
|
|
52
|
-
typeof modes.currentModeId === "string" &&
|
|
53
|
-
Array.isArray(modes.availableModes) &&
|
|
54
|
-
modes.availableModes.every(mode => objectWithString(mode, "id") && objectWithString(mode, "name"))
|
|
55
|
-
);
|
|
56
|
-
}
|
|
53
|
+
const Modes = type({
|
|
54
|
+
currentModeId: "string",
|
|
55
|
+
availableModes: type({ id: "string", name: "string" }).array(),
|
|
56
|
+
}).or("null | undefined");
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
response.configOptions === undefined || response.configOptions === null || Array.isArray(response.configOptions)
|
|
65
|
-
);
|
|
66
|
-
}
|
|
58
|
+
// Responses and notifications are also checked in-process, where absent optionals may be explicit `undefined`.
|
|
59
|
+
const sessionResponseFields = { "modes?": Modes, "configOptions?": "unknown[] | null | undefined" } as const;
|
|
60
|
+
const notArray = (value: object): boolean => !Array.isArray(value);
|
|
61
|
+
const SessionResponse = type(sessionResponseFields).narrow(notArray);
|
|
62
|
+
const IdentifiedSessionResponse = type({ sessionId: "string", ...sessionResponseFields }).narrow(notArray);
|
|
67
63
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
case "text":
|
|
73
|
-
return typeof block.text === "string";
|
|
74
|
-
case "image":
|
|
75
|
-
case "audio":
|
|
76
|
-
return typeof block.data === "string" && typeof block.mimeType === "string";
|
|
77
|
-
case "resource_link":
|
|
78
|
-
return typeof block.uri === "string" && typeof block.name === "string";
|
|
79
|
-
case "resource":
|
|
80
|
-
return typeof block.resource === "object" && block.resource !== null;
|
|
81
|
-
default:
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
64
|
+
const ContentBlock = type({ type: "'text'", text: "string" })
|
|
65
|
+
.or({ type: "'image' | 'audio'", data: "string", mimeType: "string" })
|
|
66
|
+
.or({ type: "'resource_link'", uri: "string", name: "string" })
|
|
67
|
+
.or({ type: "'resource'", resource: "object" });
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
case "agent_message_chunk":
|
|
99
|
-
case "agent_thought_chunk":
|
|
100
|
-
case "user_message_chunk":
|
|
101
|
-
return contentBlock(update.content);
|
|
102
|
-
case "tool_call":
|
|
103
|
-
return typeof update.toolCallId === "string" && typeof update.title === "string";
|
|
104
|
-
case "tool_call_update":
|
|
105
|
-
return typeof update.toolCallId === "string";
|
|
106
|
-
case "plan":
|
|
107
|
-
return Array.isArray(update.entries);
|
|
108
|
-
case "current_mode_update":
|
|
109
|
-
return typeof update.currentModeId === "string";
|
|
110
|
-
case "available_commands_update":
|
|
111
|
-
return Array.isArray(update.availableCommands);
|
|
112
|
-
case "config_option_update":
|
|
113
|
-
return Array.isArray(update.configOptions);
|
|
114
|
-
case "session_info_update":
|
|
115
|
-
return update.title === undefined || update.title === null || typeof update.title === "string";
|
|
116
|
-
case "usage_update":
|
|
117
|
-
return typeof update.size === "number" && typeof update.used === "number";
|
|
118
|
-
default:
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
69
|
+
const SessionUpdate = type({
|
|
70
|
+
sessionUpdate: "'agent_message_chunk' | 'agent_thought_chunk' | 'user_message_chunk'",
|
|
71
|
+
content: ContentBlock,
|
|
72
|
+
})
|
|
73
|
+
.or({ sessionUpdate: "'tool_call'", toolCallId: "string", title: "string" })
|
|
74
|
+
.or({ sessionUpdate: "'tool_call_update'", toolCallId: "string" })
|
|
75
|
+
.or({ sessionUpdate: "'plan'", entries: "unknown[]" })
|
|
76
|
+
.or({ sessionUpdate: "'current_mode_update'", currentModeId: "string" })
|
|
77
|
+
.or({ sessionUpdate: "'available_commands_update'", availableCommands: "unknown[]" })
|
|
78
|
+
.or({ sessionUpdate: "'config_option_update'", configOptions: "unknown[]" })
|
|
79
|
+
.or({ sessionUpdate: "'session_info_update'", "title?": "string | null | undefined" })
|
|
80
|
+
.or({ sessionUpdate: "'usage_update'", size: "number", used: "number" });
|
|
122
81
|
|
|
123
82
|
/** Validator for new-session responses. */
|
|
124
|
-
export const zNewSessionResponse = validator<NewSessionResponse>(
|
|
125
|
-
value => sessionResponse(value, true),
|
|
126
|
-
"new session response",
|
|
127
|
-
);
|
|
83
|
+
export const zNewSessionResponse = validator<NewSessionResponse>(IdentifiedSessionResponse, "new session response");
|
|
128
84
|
/** Validator for load-session responses. */
|
|
129
|
-
export const zLoadSessionResponse = validator<LoadSessionResponse>(
|
|
130
|
-
value => sessionResponse(value, false),
|
|
131
|
-
"load session response",
|
|
132
|
-
);
|
|
85
|
+
export const zLoadSessionResponse = validator<LoadSessionResponse>(SessionResponse, "load session response");
|
|
133
86
|
/** Validator for fork-session responses. */
|
|
134
|
-
export const zForkSessionResponse = validator<ForkSessionResponse>(
|
|
135
|
-
value => sessionResponse(value, true),
|
|
136
|
-
"fork session response",
|
|
137
|
-
);
|
|
87
|
+
export const zForkSessionResponse = validator<ForkSessionResponse>(IdentifiedSessionResponse, "fork session response");
|
|
138
88
|
/** Validator for prompt responses. */
|
|
139
|
-
export const zPromptResponse = validator<PromptResponse>(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
stopReason === "end_turn" ||
|
|
144
|
-
stopReason === "max_tokens" ||
|
|
145
|
-
stopReason === "max_turn_requests" ||
|
|
146
|
-
stopReason === "refusal" ||
|
|
147
|
-
stopReason === "cancelled"
|
|
148
|
-
);
|
|
149
|
-
}, "prompt response");
|
|
89
|
+
export const zPromptResponse = validator<PromptResponse>(
|
|
90
|
+
type({ stopReason: "'end_turn' | 'max_tokens' | 'max_turn_requests' | 'refusal' | 'cancelled'" }),
|
|
91
|
+
"prompt response",
|
|
92
|
+
);
|
|
150
93
|
/** Validator for session notifications. */
|
|
151
|
-
export const zSessionNotification = validator<SessionNotification>(
|
|
94
|
+
export const zSessionNotification = validator<SessionNotification>(
|
|
95
|
+
type({ sessionId: "string", update: SessionUpdate }).narrow(notArray),
|
|
96
|
+
"session notification",
|
|
97
|
+
);
|
|
152
98
|
|
|
153
99
|
/** ACP runtime validators. */
|
|
154
100
|
export const schema = {
|
package/src/ar/arj.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { readUInt16LE, readUInt32LE } from "./bytes";
|
|
1
2
|
import { crc32 } from "./checksums";
|
|
2
3
|
import { ArchiveError } from "./error";
|
|
3
4
|
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize, assertInMemorySize } from "./limits";
|
|
4
|
-
import { decompressLhStatic } from "./lzh";
|
|
5
|
+
import { decompressLhStatic, dosTimeToMs } from "./lzh";
|
|
5
6
|
import { assertArchivePathBytes, assertArchivePathString, normalizeArchiveEntryPath } from "./paths";
|
|
6
7
|
import { type ByteSource, readAllBytes } from "./source";
|
|
7
8
|
import type { ArchiveIndexEntry, FormatReader, FormatReadOptions, MemberSource } from "./types";
|
|
@@ -11,14 +12,6 @@ const ARJ_SIGNATURE_1 = 0xea;
|
|
|
11
12
|
const ARJ_MAX_BASIC_HEADER = 2600;
|
|
12
13
|
const LEGACY_DECODER = new TextDecoder("windows-1252");
|
|
13
14
|
|
|
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
15
|
function assertRange(bytes: Uint8Array, start: number, end: number, what: string): void {
|
|
23
16
|
if (
|
|
24
17
|
!Number.isSafeInteger(start) ||
|
|
@@ -31,18 +24,6 @@ function assertRange(bytes: Uint8Array, start: number, end: number, what: string
|
|
|
31
24
|
}
|
|
32
25
|
}
|
|
33
26
|
|
|
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
27
|
function readCString(bytes: Uint8Array, start: number, end: number, field: string): { value: string; next: number } {
|
|
47
28
|
let terminator = start;
|
|
48
29
|
while (terminator < end && bytes[terminator] !== 0) terminator++;
|
|
@@ -63,26 +44,26 @@ function parseArjBlock(bytes: Uint8Array, offset: number, options: FormatReadOpt
|
|
|
63
44
|
if (bytes[offset] !== ARJ_SIGNATURE_0 || bytes[offset + 1] !== ARJ_SIGNATURE_1) {
|
|
64
45
|
throw new ArchiveError("Invalid ARJ header signature");
|
|
65
46
|
}
|
|
66
|
-
const bodySize =
|
|
47
|
+
const bodySize = readUInt16LE(bytes, offset + 2);
|
|
67
48
|
if (bodySize === 0)
|
|
68
49
|
return { bodyStart: offset + 4, bodySize: 0, nextOffset: offset + 4, metadataSize: 4, isEnd: true };
|
|
69
50
|
if (bodySize < 30 || bodySize > ARJ_MAX_BASIC_HEADER) throw new ArchiveError("Invalid ARJ basic header size");
|
|
70
51
|
const bodyStart = offset + 4;
|
|
71
52
|
const bodyEnd = bodyStart + bodySize;
|
|
72
53
|
assertRange(bytes, bodyStart, bodyEnd + 4, "basic header");
|
|
73
|
-
if (crc32(bytes.subarray(bodyStart, bodyEnd)) !==
|
|
54
|
+
if (crc32(bytes.subarray(bodyStart, bodyEnd)) !== readUInt32LE(bytes, bodyEnd)) {
|
|
74
55
|
throw new ArchiveError("Invalid ARJ basic header CRC32");
|
|
75
56
|
}
|
|
76
57
|
let cursor = bodyEnd + 4;
|
|
77
58
|
let extensionCount = 0;
|
|
78
59
|
for (;;) {
|
|
79
60
|
assertRange(bytes, cursor, cursor + 2, "extended header size");
|
|
80
|
-
const extensionSize =
|
|
61
|
+
const extensionSize = readUInt16LE(bytes, cursor);
|
|
81
62
|
cursor += 2;
|
|
82
63
|
if (extensionSize === 0) break;
|
|
83
64
|
if (++extensionCount > 65_535) throw new ArchiveError("Invalid ARJ archive: too many extended headers");
|
|
84
65
|
assertRange(bytes, cursor, cursor + extensionSize + 4, "extended header");
|
|
85
|
-
if (crc32(bytes.subarray(cursor, cursor + extensionSize)) !==
|
|
66
|
+
if (crc32(bytes.subarray(cursor, cursor + extensionSize)) !== readUInt32LE(bytes, cursor + extensionSize)) {
|
|
86
67
|
throw new ArchiveError("Invalid ARJ extended header CRC32");
|
|
87
68
|
}
|
|
88
69
|
cursor += extensionSize + 4;
|
|
@@ -216,10 +197,10 @@ function normalizeHostPath(rawPath: string, hostOs: number): string {
|
|
|
216
197
|
/** Probe whether bytes begin with a CRC-framed ARJ main header. */
|
|
217
198
|
export function sniffArj(bytes: Uint8Array): boolean {
|
|
218
199
|
if (bytes.byteLength < 34 || bytes[0] !== ARJ_SIGNATURE_0 || bytes[1] !== ARJ_SIGNATURE_1) return false;
|
|
219
|
-
const size =
|
|
200
|
+
const size = readUInt16LE(bytes, 2);
|
|
220
201
|
if (size < 30 || size > ARJ_MAX_BASIC_HEADER || bytes.byteLength < size + 8) return false;
|
|
221
202
|
const body = bytes.subarray(4, 4 + size);
|
|
222
|
-
return body[0]! >= 30 && body[0]! <= size && body[6] === 2 && crc32(body) ===
|
|
203
|
+
return body[0]! >= 30 && body[0]! <= size && body[6] === 2 && crc32(body) === readUInt32LE(bytes, 4 + size);
|
|
223
204
|
}
|
|
224
205
|
|
|
225
206
|
/** Index an ARJ archive and lazily decode stored, static-Huffman, and fast-LZSS members. */
|
|
@@ -266,10 +247,10 @@ export const readArj: FormatReader = async (
|
|
|
266
247
|
const fileType = bytes[block.bodyStart + 6]!;
|
|
267
248
|
if ((flags & 0x01) !== 0) throw new ArchiveError("Encrypted ARJ members are unsupported");
|
|
268
249
|
if ((flags & 0x0c) !== 0) throw new ArchiveError("Multi-volume ARJ members are unsupported");
|
|
269
|
-
const packedSize =
|
|
270
|
-
const size =
|
|
271
|
-
const fileCrc =
|
|
272
|
-
const accessMode =
|
|
250
|
+
const packedSize = readUInt32LE(bytes, block.bodyStart + 12);
|
|
251
|
+
const size = readUInt32LE(bytes, block.bodyStart + 16);
|
|
252
|
+
const fileCrc = readUInt32LE(bytes, block.bodyStart + 20);
|
|
253
|
+
const accessMode = readUInt16LE(bytes, block.bodyStart + 26);
|
|
273
254
|
const filename = readCString(
|
|
274
255
|
bytes,
|
|
275
256
|
block.bodyStart + firstHeaderSize,
|
|
@@ -295,7 +276,7 @@ export const readArj: FormatReader = async (
|
|
|
295
276
|
if ((hostOs === 2 || hostOs === 8) && accessMode !== 0) {
|
|
296
277
|
mode = (isDirectory ? 0x4000 : 0x8000) | (accessMode & 0x0fff);
|
|
297
278
|
}
|
|
298
|
-
const rawMtime =
|
|
279
|
+
const rawMtime = readUInt32LE(bytes, block.bodyStart + 8);
|
|
299
280
|
const mtimeMs =
|
|
300
281
|
hostOs === 2 || hostOs === 8 ? (rawMtime === 0 ? undefined : rawMtime * 1000) : dosTimeToMs(rawMtime);
|
|
301
282
|
entries.push({
|
package/src/ar/asar.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import { readUInt32LE, writeUInt32LE } from "./bytes";
|
|
3
3
|
import { ArchiveError } from "./error";
|
|
4
4
|
import { assertArchiveMemberSize, assertEntryCount, assertIndexSize } from "./limits";
|
|
5
|
+
import { isRecord } from "../type-guards";
|
|
5
6
|
import {
|
|
6
7
|
assertArchivePathBytes,
|
|
7
8
|
assertArchivePathString,
|
|
@@ -55,14 +56,10 @@ function alignAsarPayload(size: number): number {
|
|
|
55
56
|
return size + ((4 - (size % 4)) % 4);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function isArchiveRecord(value: unknown): value is Record<string, unknown> {
|
|
59
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
59
|
function parseIntegrity(value: unknown, entryPath: string, size: number): AsarIntegrity | undefined {
|
|
63
60
|
if (value === undefined) return undefined;
|
|
64
61
|
const label = formatArchivePathForError(entryPath);
|
|
65
|
-
if (!
|
|
62
|
+
if (!isRecord(value)) {
|
|
66
63
|
throw invalidAsar(`file '${label}' has an invalid integrity record`);
|
|
67
64
|
}
|
|
68
65
|
if (value.algorithm !== "SHA256") {
|
|
@@ -228,7 +225,7 @@ async function readAsarIndex(source: ByteSource, options: FormatReadOptions): Pr
|
|
|
228
225
|
} catch {
|
|
229
226
|
throw invalidAsar("header is not valid JSON");
|
|
230
227
|
}
|
|
231
|
-
if (!
|
|
228
|
+
if (!isRecord(header) || !isRecord(header.files)) {
|
|
232
229
|
throw invalidAsar("root must contain a files object");
|
|
233
230
|
}
|
|
234
231
|
|
|
@@ -257,7 +254,7 @@ async function readAsarIndex(source: ByteSource, options: FormatReadOptions): Pr
|
|
|
257
254
|
) {
|
|
258
255
|
throw invalidAsar(`invalid entry name '${formatArchivePathForError(current.name)}'`);
|
|
259
256
|
}
|
|
260
|
-
if (!
|
|
257
|
+
if (!isRecord(current.node)) {
|
|
261
258
|
throw invalidAsar(`entry '${formatArchivePathForError(current.name)}' must be an object`);
|
|
262
259
|
}
|
|
263
260
|
const entryPathBytes = current.parentPathBytes + (current.parentPath ? 1 : 0) + nameBytes;
|
|
@@ -295,7 +292,7 @@ async function readAsarIndex(source: ByteSource, options: FormatReadOptions): Pr
|
|
|
295
292
|
}
|
|
296
293
|
|
|
297
294
|
if ("files" in current.node) {
|
|
298
|
-
if (!
|
|
295
|
+
if (!isRecord(current.node.files)) {
|
|
299
296
|
throw invalidAsar(`directory '${entryLabel}' must contain a files object`);
|
|
300
297
|
}
|
|
301
298
|
entries.push({ path: entryPath, isDirectory: true, size: 0 });
|
package/src/ar/codecs/xz.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readUInt32BE } from "../bytes";
|
|
1
2
|
import { crc32, crc64 } from "../checksums";
|
|
2
3
|
import { ArchiveError } from "../error";
|
|
3
4
|
import { lzma2Decompress } from "./lzma";
|
|
@@ -16,10 +17,6 @@ function write32LE(bytes: Uint8Array, offset: number, value: number): void {
|
|
|
16
17
|
bytes[offset + 3] = value >>> 24;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function read32BE(bytes: Uint8Array, offset: number): number {
|
|
20
|
-
return ((bytes[offset]! << 24) | (bytes[offset + 1]! << 16) | (bytes[offset + 2]! << 8) | bytes[offset + 3]!) >>> 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
20
|
function write32BE(bytes: Uint8Array, offset: number, value: number): void {
|
|
24
21
|
bytes[offset] = value >>> 24;
|
|
25
22
|
bytes[offset + 1] = (value >>> 16) & 0xff;
|
|
@@ -155,7 +152,8 @@ interface XzFilter {
|
|
|
155
152
|
properties: Uint8Array;
|
|
156
153
|
}
|
|
157
154
|
|
|
158
|
-
|
|
155
|
+
/** Undo the delta filter (shared by XZ and 7z) in place. */
|
|
156
|
+
export function deltaDecode(bytes: Uint8Array, distance: number): void {
|
|
159
157
|
const history = new Uint8Array(256);
|
|
160
158
|
let position = 0;
|
|
161
159
|
for (let index = 0; index < bytes.byteLength; index++) {
|
|
@@ -166,7 +164,8 @@ function deltaDecode(bytes: Uint8Array, distance: number): void {
|
|
|
166
164
|
}
|
|
167
165
|
}
|
|
168
166
|
|
|
169
|
-
|
|
167
|
+
/** Undo the x86 BCJ filter (shared by XZ and 7z) in place; `startOffset` is the stream position of `bytes[0]`. */
|
|
168
|
+
export function x86Decode(bytes: Uint8Array, startOffset: number): void {
|
|
170
169
|
const maskToBitNumber = [0, 1, 2, 2, 3] as const;
|
|
171
170
|
let previousMask = 0;
|
|
172
171
|
let previousPosition = 0xfffffffb;
|
|
@@ -267,7 +266,7 @@ function sparcDecode(bytes: Uint8Array, startOffset: number): void {
|
|
|
267
266
|
)
|
|
268
267
|
)
|
|
269
268
|
continue;
|
|
270
|
-
const source = (
|
|
269
|
+
const source = (readUInt32BE(bytes, index) << 2) >>> 0;
|
|
271
270
|
let destination = (source - startOffset - index) >>> 2;
|
|
272
271
|
destination = (((0 - ((destination >>> 22) & 1)) << 22) & 0x3fffffff) | (destination & 0x3fffff) | 0x40000000;
|
|
273
272
|
write32BE(bytes, index, destination >>> 0);
|
|
@@ -356,7 +355,7 @@ function riscvDecode(bytes: Uint8Array, startOffset: number): void {
|
|
|
356
355
|
index += 2;
|
|
357
356
|
continue;
|
|
358
357
|
}
|
|
359
|
-
const address = (
|
|
358
|
+
const address = (readUInt32BE(bytes, index + 4) - startOffset - index) >>> 0;
|
|
360
359
|
instruction2 = ((instruction >>> 12) | (address << 20)) >>> 0;
|
|
361
360
|
instruction = (0x17 | (register << 7) | ((address + 0x800) & 0xfffff000)) >>> 0;
|
|
362
361
|
}
|