@secure-exec/core 0.1.1-rc.3 → 0.2.0-rc.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/dist/esm-compiler.d.ts +5 -1
- package/dist/esm-compiler.js +5 -1
- package/dist/fs-helpers.d.ts +1 -1
- package/dist/generated/isolate-runtime.d.ts +15 -15
- package/dist/generated/isolate-runtime.js +15 -15
- package/dist/index.d.ts +24 -5
- package/dist/index.js +23 -3
- package/dist/isolate-runtime/apply-custom-global-policy.js +3 -3
- package/dist/isolate-runtime/apply-timing-mitigation-freeze.js +2 -2
- package/dist/isolate-runtime/apply-timing-mitigation-off.js +2 -2
- package/dist/isolate-runtime/bridge-attach.js +2 -2
- package/dist/isolate-runtime/bridge-initial-globals.js +145 -6
- package/dist/isolate-runtime/eval-script-result.js +1 -1
- package/dist/isolate-runtime/global-exposure-helpers.js +2 -2
- package/dist/isolate-runtime/init-commonjs-module-globals.js +2 -2
- package/dist/isolate-runtime/override-process-cwd.js +1 -1
- package/dist/isolate-runtime/override-process-env.js +1 -1
- package/dist/isolate-runtime/require-setup.js +1600 -338
- package/dist/isolate-runtime/set-commonjs-file-globals.js +2 -2
- package/dist/isolate-runtime/set-stdin-data.js +1 -1
- package/dist/isolate-runtime/setup-dynamic-import.js +47 -19
- package/dist/isolate-runtime/setup-fs-facade.js +62 -23
- package/dist/kernel/command-registry.d.ts +44 -0
- package/dist/kernel/command-registry.js +114 -0
- package/dist/kernel/device-layer.d.ts +12 -0
- package/dist/kernel/device-layer.js +262 -0
- package/dist/kernel/dns-cache.d.ts +29 -0
- package/dist/kernel/dns-cache.js +52 -0
- package/dist/kernel/fd-table.d.ts +84 -0
- package/dist/kernel/fd-table.js +278 -0
- package/dist/kernel/file-lock.d.ts +34 -0
- package/dist/kernel/file-lock.js +123 -0
- package/dist/kernel/host-adapter.d.ts +50 -0
- package/dist/kernel/host-adapter.js +8 -0
- package/dist/kernel/index.d.ts +36 -0
- package/dist/kernel/index.js +34 -0
- package/dist/kernel/inode-table.d.ts +43 -0
- package/dist/kernel/inode-table.js +85 -0
- package/dist/kernel/kernel.d.ts +9 -0
- package/dist/kernel/kernel.js +1396 -0
- package/dist/kernel/permissions.d.ts +27 -0
- package/dist/kernel/permissions.js +118 -0
- package/dist/kernel/pipe-manager.d.ts +64 -0
- package/dist/kernel/pipe-manager.js +267 -0
- package/dist/kernel/proc-layer.d.ts +11 -0
- package/dist/kernel/proc-layer.js +501 -0
- package/dist/kernel/process-table.d.ts +124 -0
- package/dist/kernel/process-table.js +631 -0
- package/dist/kernel/pty.d.ts +108 -0
- package/dist/kernel/pty.js +541 -0
- package/dist/kernel/socket-table.d.ts +305 -0
- package/dist/kernel/socket-table.js +1124 -0
- package/dist/kernel/timer-table.d.ts +54 -0
- package/dist/kernel/timer-table.js +108 -0
- package/dist/kernel/types.d.ts +500 -0
- package/dist/kernel/types.js +89 -0
- package/dist/kernel/user.d.ts +29 -0
- package/dist/kernel/user.js +35 -0
- package/dist/kernel/vfs.d.ts +54 -0
- package/dist/kernel/vfs.js +8 -0
- package/dist/kernel/wait.d.ts +45 -0
- package/dist/kernel/wait.js +112 -0
- package/dist/kernel/wstatus.d.ts +21 -0
- package/dist/kernel/wstatus.js +33 -0
- package/dist/module-resolver.d.ts +4 -0
- package/dist/module-resolver.js +4 -0
- package/dist/package-bundler.d.ts +6 -1
- package/dist/runtime-driver.d.ts +3 -1
- package/dist/shared/bridge-contract.d.ts +329 -20
- package/dist/shared/bridge-contract.js +60 -5
- package/dist/shared/console-formatter.js +8 -4
- package/dist/shared/global-exposure.js +269 -19
- package/dist/shared/in-memory-fs.d.ts +30 -11
- package/dist/shared/in-memory-fs.js +383 -109
- package/dist/shared/permissions.d.ts +4 -6
- package/dist/shared/permissions.js +19 -39
- package/dist/types.d.ts +8 -159
- package/dist/types.js +5 -0
- package/package.json +12 -22
- package/dist/bridge/active-handles.d.ts +0 -22
- package/dist/bridge/active-handles.js +0 -55
- package/dist/bridge/child-process.d.ts +0 -99
- package/dist/bridge/child-process.js +0 -670
- package/dist/bridge/fs.d.ts +0 -281
- package/dist/bridge/fs.js +0 -2235
- package/dist/bridge/index.d.ts +0 -10
- package/dist/bridge/index.js +0 -41
- package/dist/bridge/module.d.ts +0 -75
- package/dist/bridge/module.js +0 -308
- package/dist/bridge/network.d.ts +0 -350
- package/dist/bridge/network.js +0 -2050
- package/dist/bridge/os.d.ts +0 -13
- package/dist/bridge/os.js +0 -256
- package/dist/bridge/polyfills.d.ts +0 -2
- package/dist/bridge/polyfills.js +0 -11
- package/dist/bridge/process.d.ts +0 -89
- package/dist/bridge/process.js +0 -1015
- package/dist/bridge.js +0 -12496
- package/dist/python-runtime.d.ts +0 -16
- package/dist/python-runtime.js +0 -45
- package/dist/runtime.d.ts +0 -31
- package/dist/runtime.js +0 -69
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// isolate-runtime/src/common/global-exposure.ts
|
|
3
|
+
// ../core/isolate-runtime/src/common/global-exposure.ts
|
|
4
4
|
function defineRuntimeGlobalBinding(name, value, mutable) {
|
|
5
5
|
Object.defineProperty(globalThis, name, {
|
|
6
6
|
value,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
return createRuntimeGlobalExposer(true);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// isolate-runtime/src/inject/set-commonjs-file-globals.ts
|
|
24
|
+
// ../core/isolate-runtime/src/inject/set-commonjs-file-globals.ts
|
|
25
25
|
var __runtimeExposeMutableGlobal = getRuntimeExposeMutableGlobal();
|
|
26
26
|
var __commonJsFileConfig = globalThis.__runtimeCommonJsFileConfig ?? {};
|
|
27
27
|
var __filePath = typeof __commonJsFileConfig.filePath === "string" ? __commonJsFileConfig.filePath : "/<entry>.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// isolate-runtime/src/inject/set-stdin-data.ts
|
|
3
|
+
// ../core/isolate-runtime/src/inject/set-stdin-data.ts
|
|
4
4
|
if (typeof globalThis._stdinData !== "undefined") {
|
|
5
5
|
globalThis._stdinData = globalThis.__runtimeStdinData;
|
|
6
6
|
globalThis._stdinPosition = 0;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// isolate-runtime/src/common/global-access.ts
|
|
3
|
+
// ../core/isolate-runtime/src/common/global-access.ts
|
|
4
4
|
function isObjectLike(value) {
|
|
5
5
|
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
// isolate-runtime/src/common/global-exposure.ts
|
|
8
|
+
// ../core/isolate-runtime/src/common/global-exposure.ts
|
|
9
9
|
function defineRuntimeGlobalBinding(name, value, mutable) {
|
|
10
10
|
Object.defineProperty(globalThis, name, {
|
|
11
11
|
value,
|
|
@@ -26,30 +26,57 @@
|
|
|
26
26
|
return createRuntimeGlobalExposer(false);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// isolate-runtime/src/inject/setup-dynamic-import.ts
|
|
29
|
+
// ../core/isolate-runtime/src/inject/setup-dynamic-import.ts
|
|
30
30
|
var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();
|
|
31
31
|
var __dynamicImportConfig = globalThis.__runtimeDynamicImportConfig ?? {};
|
|
32
32
|
var __fallbackReferrer = typeof __dynamicImportConfig.referrerPath === "string" && __dynamicImportConfig.referrerPath.length > 0 ? __dynamicImportConfig.referrerPath : "/";
|
|
33
|
-
var
|
|
33
|
+
var __dynamicImportCache = /* @__PURE__ */ new Map();
|
|
34
|
+
var __resolveDynamicImportPath = function(request, referrer) {
|
|
35
|
+
if (!request.startsWith("./") && !request.startsWith("../") && !request.startsWith("/")) {
|
|
36
|
+
return request;
|
|
37
|
+
}
|
|
38
|
+
const baseDir = referrer.endsWith("/") ? referrer : referrer.slice(0, referrer.lastIndexOf("/")) || "/";
|
|
39
|
+
const segments = baseDir.split("/").filter(Boolean);
|
|
40
|
+
for (const part of request.split("/")) {
|
|
41
|
+
if (part === "." || part.length === 0) continue;
|
|
42
|
+
if (part === "..") {
|
|
43
|
+
segments.pop();
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
segments.push(part);
|
|
47
|
+
}
|
|
48
|
+
return `/${segments.join("/")}`;
|
|
49
|
+
};
|
|
50
|
+
var __dynamicImportHandler = function(specifier, fromPath) {
|
|
34
51
|
const request = String(specifier);
|
|
35
52
|
const referrer = typeof fromPath === "string" && fromPath.length > 0 ? fromPath : __fallbackReferrer;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (namespace !== null) {
|
|
43
|
-
return namespace;
|
|
53
|
+
let resolved = null;
|
|
54
|
+
if (typeof globalThis._resolveModuleSync !== "undefined") {
|
|
55
|
+
resolved = globalThis._resolveModuleSync.applySync(
|
|
56
|
+
void 0,
|
|
57
|
+
[request, referrer, "import"]
|
|
58
|
+
);
|
|
44
59
|
}
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
const resolvedPath = typeof resolved === "string" && resolved.length > 0 ? resolved : __resolveDynamicImportPath(request, referrer);
|
|
61
|
+
const cacheKey = typeof resolved === "string" && resolved.length > 0 ? resolved : `${referrer}\0${request}`;
|
|
62
|
+
const cached = __dynamicImportCache.get(cacheKey);
|
|
63
|
+
if (cached) return Promise.resolve(cached);
|
|
64
|
+
if (typeof globalThis._requireFrom !== "function") {
|
|
65
|
+
throw new Error("Cannot load module: " + resolvedPath);
|
|
47
66
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
67
|
+
let mod;
|
|
68
|
+
try {
|
|
69
|
+
mod = globalThis._requireFrom(resolved ?? request, referrer);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
if (error && typeof error === "object" && "code" in error && error.code === "MODULE_NOT_FOUND") {
|
|
73
|
+
throw new Error("Cannot load module: " + resolvedPath);
|
|
74
|
+
}
|
|
75
|
+
if (message.startsWith("Cannot find module ")) {
|
|
76
|
+
throw new Error("Cannot load module: " + resolvedPath);
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
51
79
|
}
|
|
52
|
-
const mod = runtimeRequire(request);
|
|
53
80
|
const namespaceFallback = { default: mod };
|
|
54
81
|
if (isObjectLike(mod)) {
|
|
55
82
|
for (const key of Object.keys(mod)) {
|
|
@@ -58,7 +85,8 @@
|
|
|
58
85
|
}
|
|
59
86
|
}
|
|
60
87
|
}
|
|
61
|
-
|
|
88
|
+
__dynamicImportCache.set(cacheKey, namespaceFallback);
|
|
89
|
+
return Promise.resolve(namespaceFallback);
|
|
62
90
|
};
|
|
63
91
|
__runtimeExposeCustomGlobal("__dynamicImport", __dynamicImportHandler);
|
|
64
92
|
})();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
-
// isolate-runtime/src/common/global-exposure.ts
|
|
3
|
+
// ../core/isolate-runtime/src/common/global-exposure.ts
|
|
4
4
|
function defineRuntimeGlobalBinding(name, value, mutable) {
|
|
5
5
|
Object.defineProperty(globalThis, name, {
|
|
6
6
|
value,
|
|
@@ -21,28 +21,67 @@
|
|
|
21
21
|
return createRuntimeGlobalExposer(false);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
// isolate-runtime/src/inject/setup-fs-facade.ts
|
|
24
|
+
// ../core/isolate-runtime/src/inject/setup-fs-facade.ts
|
|
25
25
|
var __runtimeExposeCustomGlobal = getRuntimeExposeCustomGlobal();
|
|
26
|
-
var __fsFacade = {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
var __fsFacade = {};
|
|
27
|
+
Object.defineProperties(__fsFacade, {
|
|
28
|
+
readFile: { get() {
|
|
29
|
+
return globalThis._fsReadFile;
|
|
30
|
+
}, enumerable: true },
|
|
31
|
+
writeFile: { get() {
|
|
32
|
+
return globalThis._fsWriteFile;
|
|
33
|
+
}, enumerable: true },
|
|
34
|
+
readFileBinary: { get() {
|
|
35
|
+
return globalThis._fsReadFileBinary;
|
|
36
|
+
}, enumerable: true },
|
|
37
|
+
writeFileBinary: { get() {
|
|
38
|
+
return globalThis._fsWriteFileBinary;
|
|
39
|
+
}, enumerable: true },
|
|
40
|
+
readDir: { get() {
|
|
41
|
+
return globalThis._fsReadDir;
|
|
42
|
+
}, enumerable: true },
|
|
43
|
+
mkdir: { get() {
|
|
44
|
+
return globalThis._fsMkdir;
|
|
45
|
+
}, enumerable: true },
|
|
46
|
+
rmdir: { get() {
|
|
47
|
+
return globalThis._fsRmdir;
|
|
48
|
+
}, enumerable: true },
|
|
49
|
+
exists: { get() {
|
|
50
|
+
return globalThis._fsExists;
|
|
51
|
+
}, enumerable: true },
|
|
52
|
+
stat: { get() {
|
|
53
|
+
return globalThis._fsStat;
|
|
54
|
+
}, enumerable: true },
|
|
55
|
+
unlink: { get() {
|
|
56
|
+
return globalThis._fsUnlink;
|
|
57
|
+
}, enumerable: true },
|
|
58
|
+
rename: { get() {
|
|
59
|
+
return globalThis._fsRename;
|
|
60
|
+
}, enumerable: true },
|
|
61
|
+
chmod: { get() {
|
|
62
|
+
return globalThis._fsChmod;
|
|
63
|
+
}, enumerable: true },
|
|
64
|
+
chown: { get() {
|
|
65
|
+
return globalThis._fsChown;
|
|
66
|
+
}, enumerable: true },
|
|
67
|
+
link: { get() {
|
|
68
|
+
return globalThis._fsLink;
|
|
69
|
+
}, enumerable: true },
|
|
70
|
+
symlink: { get() {
|
|
71
|
+
return globalThis._fsSymlink;
|
|
72
|
+
}, enumerable: true },
|
|
73
|
+
readlink: { get() {
|
|
74
|
+
return globalThis._fsReadlink;
|
|
75
|
+
}, enumerable: true },
|
|
76
|
+
lstat: { get() {
|
|
77
|
+
return globalThis._fsLstat;
|
|
78
|
+
}, enumerable: true },
|
|
79
|
+
truncate: { get() {
|
|
80
|
+
return globalThis._fsTruncate;
|
|
81
|
+
}, enumerable: true },
|
|
82
|
+
utimes: { get() {
|
|
83
|
+
return globalThis._fsUtimes;
|
|
84
|
+
}, enumerable: true }
|
|
85
|
+
});
|
|
47
86
|
__runtimeExposeCustomGlobal("_fs", __fsFacade);
|
|
48
87
|
})();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps command names to runtime drivers. When a process calls
|
|
5
|
+
* spawn("grep", ...), the registry resolves "grep" to the WasmVM driver.
|
|
6
|
+
* Also populates /bin in the VFS so shell PATH lookup succeeds.
|
|
7
|
+
*/
|
|
8
|
+
import type { RuntimeDriver } from "./types.js";
|
|
9
|
+
import type { VirtualFileSystem } from "./vfs.js";
|
|
10
|
+
export declare class CommandRegistry {
|
|
11
|
+
/** command name → RuntimeDriver */
|
|
12
|
+
private commands;
|
|
13
|
+
/** Warning log for command overrides. */
|
|
14
|
+
private warnings;
|
|
15
|
+
/**
|
|
16
|
+
* Register all commands from a driver.
|
|
17
|
+
* Last-mounted driver wins on conflicts (allows override with warning).
|
|
18
|
+
*/
|
|
19
|
+
register(driver: RuntimeDriver): void;
|
|
20
|
+
/** Get recorded warnings (for testing). */
|
|
21
|
+
getWarnings(): readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* Register a single command to a driver.
|
|
24
|
+
* Used for on-demand dynamic registration (e.g. after tryResolve).
|
|
25
|
+
*/
|
|
26
|
+
registerCommand(command: string, driver: RuntimeDriver): void;
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a command name to a driver. Returns null if unknown.
|
|
29
|
+
* Supports path-based lookup: '/bin/ls' resolves to the driver for 'ls'.
|
|
30
|
+
*/
|
|
31
|
+
resolve(command: string): RuntimeDriver | null;
|
|
32
|
+
/** List all registered commands. Returns command → driver name. */
|
|
33
|
+
list(): Map<string, string>;
|
|
34
|
+
/**
|
|
35
|
+
* Create a single /bin stub entry for a command.
|
|
36
|
+
* Used for on-demand registration after tryResolve discovers a new command.
|
|
37
|
+
*/
|
|
38
|
+
populateBinEntry(vfs: VirtualFileSystem, command: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Populate /bin in the VFS with stub entries for all registered commands.
|
|
41
|
+
* This enables brush-shell's PATH lookup to find commands.
|
|
42
|
+
*/
|
|
43
|
+
populateBin(vfs: VirtualFileSystem): Promise<void>;
|
|
44
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps command names to runtime drivers. When a process calls
|
|
5
|
+
* spawn("grep", ...), the registry resolves "grep" to the WasmVM driver.
|
|
6
|
+
* Also populates /bin in the VFS so shell PATH lookup succeeds.
|
|
7
|
+
*/
|
|
8
|
+
export class CommandRegistry {
|
|
9
|
+
/** command name → RuntimeDriver */
|
|
10
|
+
commands = new Map();
|
|
11
|
+
/** Warning log for command overrides. */
|
|
12
|
+
warnings = [];
|
|
13
|
+
/**
|
|
14
|
+
* Register all commands from a driver.
|
|
15
|
+
* Last-mounted driver wins on conflicts (allows override with warning).
|
|
16
|
+
*/
|
|
17
|
+
register(driver) {
|
|
18
|
+
for (const cmd of driver.commands) {
|
|
19
|
+
const existing = this.commands.get(cmd);
|
|
20
|
+
if (existing) {
|
|
21
|
+
const msg = `command "${cmd}" overridden: ${existing.name} → ${driver.name}`;
|
|
22
|
+
this.warnings.push(msg);
|
|
23
|
+
console.warn(`[CommandRegistry] ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
this.commands.set(cmd, driver);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Get recorded warnings (for testing). */
|
|
29
|
+
getWarnings() {
|
|
30
|
+
return this.warnings;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Register a single command to a driver.
|
|
34
|
+
* Used for on-demand dynamic registration (e.g. after tryResolve).
|
|
35
|
+
*/
|
|
36
|
+
registerCommand(command, driver) {
|
|
37
|
+
const existing = this.commands.get(command);
|
|
38
|
+
if (existing) {
|
|
39
|
+
const msg = `command "${command}" overridden: ${existing.name} → ${driver.name}`;
|
|
40
|
+
this.warnings.push(msg);
|
|
41
|
+
console.warn(`[CommandRegistry] ${msg}`);
|
|
42
|
+
}
|
|
43
|
+
this.commands.set(command, driver);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a command name to a driver. Returns null if unknown.
|
|
47
|
+
* Supports path-based lookup: '/bin/ls' resolves to the driver for 'ls'.
|
|
48
|
+
*/
|
|
49
|
+
resolve(command) {
|
|
50
|
+
// Direct name lookup
|
|
51
|
+
const direct = this.commands.get(command);
|
|
52
|
+
if (direct)
|
|
53
|
+
return direct;
|
|
54
|
+
// Path-based: extract basename and retry
|
|
55
|
+
if (command.includes("/")) {
|
|
56
|
+
const basename = command.split("/").pop();
|
|
57
|
+
if (basename)
|
|
58
|
+
return this.commands.get(basename) ?? null;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** List all registered commands. Returns command → driver name. */
|
|
63
|
+
list() {
|
|
64
|
+
const result = new Map();
|
|
65
|
+
for (const [cmd, driver] of this.commands) {
|
|
66
|
+
result.set(cmd, driver.name);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create a single /bin stub entry for a command.
|
|
72
|
+
* Used for on-demand registration after tryResolve discovers a new command.
|
|
73
|
+
*/
|
|
74
|
+
async populateBinEntry(vfs, command) {
|
|
75
|
+
if (!(await vfs.exists("/bin"))) {
|
|
76
|
+
await vfs.mkdir("/bin", { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
const path = `/bin/${command}`;
|
|
79
|
+
if (!(await vfs.exists(path))) {
|
|
80
|
+
const stub = new TextEncoder().encode("#!/bin/sh\n# kernel command stub\n");
|
|
81
|
+
await vfs.writeFile(path, stub);
|
|
82
|
+
try {
|
|
83
|
+
await vfs.chmod(path, 0o755);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// chmod may not be supported by all VFS backends
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Populate /bin in the VFS with stub entries for all registered commands.
|
|
92
|
+
* This enables brush-shell's PATH lookup to find commands.
|
|
93
|
+
*/
|
|
94
|
+
async populateBin(vfs) {
|
|
95
|
+
// Ensure /bin exists
|
|
96
|
+
if (!(await vfs.exists("/bin"))) {
|
|
97
|
+
await vfs.mkdir("/bin", { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
// Create a stub file for each command
|
|
100
|
+
const stub = new TextEncoder().encode("#!/bin/sh\n# kernel command stub\n");
|
|
101
|
+
for (const cmd of this.commands.keys()) {
|
|
102
|
+
const path = `/bin/${cmd}`;
|
|
103
|
+
if (!(await vfs.exists(path))) {
|
|
104
|
+
await vfs.writeFile(path, stub);
|
|
105
|
+
try {
|
|
106
|
+
await vfs.chmod(path, 0o755);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// chmod may not be supported by all VFS backends
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device layer.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts device node paths (/dev/*) before they reach the VFS backend.
|
|
5
|
+
* Wraps a VirtualFileSystem and handles device-specific read/write semantics.
|
|
6
|
+
*/
|
|
7
|
+
import type { VirtualFileSystem } from "./vfs.js";
|
|
8
|
+
/**
|
|
9
|
+
* Wrap a VFS with device node interception.
|
|
10
|
+
* Device paths are handled directly; all other paths pass through.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createDeviceLayer(vfs: VirtualFileSystem): VirtualFileSystem;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device layer.
|
|
3
|
+
*
|
|
4
|
+
* Intercepts device node paths (/dev/*) before they reach the VFS backend.
|
|
5
|
+
* Wraps a VirtualFileSystem and handles device-specific read/write semantics.
|
|
6
|
+
*/
|
|
7
|
+
import { KernelError } from "./types.js";
|
|
8
|
+
const DEVICE_PATHS = new Set([
|
|
9
|
+
"/dev/null",
|
|
10
|
+
"/dev/zero",
|
|
11
|
+
"/dev/stdin",
|
|
12
|
+
"/dev/stdout",
|
|
13
|
+
"/dev/stderr",
|
|
14
|
+
"/dev/urandom",
|
|
15
|
+
"/dev/random",
|
|
16
|
+
"/dev/tty",
|
|
17
|
+
"/dev/console",
|
|
18
|
+
"/dev/full",
|
|
19
|
+
"/dev/ptmx",
|
|
20
|
+
]);
|
|
21
|
+
const DEVICE_INO = {
|
|
22
|
+
"/dev/null": 0xffff_0001,
|
|
23
|
+
"/dev/zero": 0xffff_0002,
|
|
24
|
+
"/dev/stdin": 0xffff_0003,
|
|
25
|
+
"/dev/stdout": 0xffff_0004,
|
|
26
|
+
"/dev/stderr": 0xffff_0005,
|
|
27
|
+
"/dev/urandom": 0xffff_0006,
|
|
28
|
+
"/dev/random": 0xffff_0007,
|
|
29
|
+
"/dev/tty": 0xffff_0008,
|
|
30
|
+
"/dev/console": 0xffff_0009,
|
|
31
|
+
"/dev/full": 0xffff_000a,
|
|
32
|
+
"/dev/ptmx": 0xffff_000b,
|
|
33
|
+
};
|
|
34
|
+
/** Device pseudo-directories that contain dynamic entries. */
|
|
35
|
+
const DEVICE_DIRS = new Set(["/dev/fd", "/dev/pts", "/dev/shm"]);
|
|
36
|
+
function isDevicePath(path) {
|
|
37
|
+
return DEVICE_PATHS.has(path) || path.startsWith("/dev/fd/") || path.startsWith("/dev/pts/");
|
|
38
|
+
}
|
|
39
|
+
function isDeviceDir(path) {
|
|
40
|
+
return path === "/dev" || DEVICE_DIRS.has(path);
|
|
41
|
+
}
|
|
42
|
+
function deviceStat(path) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
return {
|
|
45
|
+
mode: 0o666,
|
|
46
|
+
size: 0,
|
|
47
|
+
isDirectory: false,
|
|
48
|
+
isSymbolicLink: false,
|
|
49
|
+
atimeMs: now,
|
|
50
|
+
mtimeMs: now,
|
|
51
|
+
ctimeMs: now,
|
|
52
|
+
birthtimeMs: now,
|
|
53
|
+
ino: DEVICE_INO[path] ?? 0xffff_0000,
|
|
54
|
+
nlink: 1,
|
|
55
|
+
uid: 0,
|
|
56
|
+
gid: 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const DEV_DIR_ENTRIES = [
|
|
60
|
+
{ name: "null", isDirectory: false },
|
|
61
|
+
{ name: "zero", isDirectory: false },
|
|
62
|
+
{ name: "stdin", isDirectory: false },
|
|
63
|
+
{ name: "stdout", isDirectory: false },
|
|
64
|
+
{ name: "stderr", isDirectory: false },
|
|
65
|
+
{ name: "urandom", isDirectory: false },
|
|
66
|
+
{ name: "random", isDirectory: false },
|
|
67
|
+
{ name: "tty", isDirectory: false },
|
|
68
|
+
{ name: "console", isDirectory: false },
|
|
69
|
+
{ name: "full", isDirectory: false },
|
|
70
|
+
{ name: "ptmx", isDirectory: false },
|
|
71
|
+
{ name: "fd", isDirectory: true },
|
|
72
|
+
{ name: "pts", isDirectory: true },
|
|
73
|
+
{ name: "shm", isDirectory: true },
|
|
74
|
+
];
|
|
75
|
+
/**
|
|
76
|
+
* Wrap a VFS with device node interception.
|
|
77
|
+
* Device paths are handled directly; all other paths pass through.
|
|
78
|
+
*/
|
|
79
|
+
export function createDeviceLayer(vfs) {
|
|
80
|
+
const wrapped = {
|
|
81
|
+
prepareOpenSync(path, flags) {
|
|
82
|
+
if (isDevicePath(path) || isDeviceDir(path))
|
|
83
|
+
return false;
|
|
84
|
+
const syncVfs = vfs;
|
|
85
|
+
return syncVfs.prepareOpenSync?.(path, flags) ?? false;
|
|
86
|
+
},
|
|
87
|
+
async readFile(path) {
|
|
88
|
+
if (path === "/dev/null" || path === "/dev/full")
|
|
89
|
+
return new Uint8Array(0);
|
|
90
|
+
if (path === "/dev/zero")
|
|
91
|
+
return new Uint8Array(4096);
|
|
92
|
+
if (path === "/dev/urandom" || path === "/dev/random") {
|
|
93
|
+
const buf = new Uint8Array(4096);
|
|
94
|
+
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
|
95
|
+
globalThis.crypto.getRandomValues(buf);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
for (let i = 0; i < buf.length; i++) {
|
|
99
|
+
buf[i] = (Math.random() * 256) | 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return buf;
|
|
103
|
+
}
|
|
104
|
+
if (path === "/dev/tty" || path === "/dev/console" || path === "/dev/ptmx")
|
|
105
|
+
return new Uint8Array(0);
|
|
106
|
+
return vfs.readFile(path);
|
|
107
|
+
},
|
|
108
|
+
async pread(path, offset, length) {
|
|
109
|
+
if (path === "/dev/null" || path === "/dev/full")
|
|
110
|
+
return new Uint8Array(0);
|
|
111
|
+
if (path === "/dev/zero")
|
|
112
|
+
return new Uint8Array(length);
|
|
113
|
+
if (path === "/dev/urandom" || path === "/dev/random") {
|
|
114
|
+
const buf = new Uint8Array(length);
|
|
115
|
+
if (typeof globalThis.crypto?.getRandomValues === "function") {
|
|
116
|
+
globalThis.crypto.getRandomValues(buf);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
for (let i = 0; i < buf.length; i++) {
|
|
120
|
+
buf[i] = (Math.random() * 256) | 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return buf;
|
|
124
|
+
}
|
|
125
|
+
if (path === "/dev/tty" || path === "/dev/console" || path === "/dev/ptmx")
|
|
126
|
+
return new Uint8Array(0);
|
|
127
|
+
return vfs.pread(path, offset, length);
|
|
128
|
+
},
|
|
129
|
+
async readTextFile(path) {
|
|
130
|
+
if (path === "/dev/null")
|
|
131
|
+
return "";
|
|
132
|
+
const bytes = await this.readFile(path);
|
|
133
|
+
return new TextDecoder().decode(bytes);
|
|
134
|
+
},
|
|
135
|
+
async readDir(path) {
|
|
136
|
+
if (path === "/dev") {
|
|
137
|
+
return DEV_DIR_ENTRIES.map((e) => e.name);
|
|
138
|
+
}
|
|
139
|
+
// /dev/fd and /dev/pts are dynamic — return empty at VFS level
|
|
140
|
+
if (DEVICE_DIRS.has(path))
|
|
141
|
+
return [];
|
|
142
|
+
return vfs.readDir(path);
|
|
143
|
+
},
|
|
144
|
+
async readDirWithTypes(path) {
|
|
145
|
+
if (path === "/dev") {
|
|
146
|
+
return DEV_DIR_ENTRIES;
|
|
147
|
+
}
|
|
148
|
+
if (DEVICE_DIRS.has(path))
|
|
149
|
+
return [];
|
|
150
|
+
return vfs.readDirWithTypes(path);
|
|
151
|
+
},
|
|
152
|
+
async writeFile(path, content) {
|
|
153
|
+
// /dev/full always returns ENOSPC on write (POSIX behavior)
|
|
154
|
+
if (path === "/dev/full")
|
|
155
|
+
throw new KernelError("ENOSPC", "No space left on device");
|
|
156
|
+
// Discard writes to sink devices
|
|
157
|
+
if (path === "/dev/null" || path === "/dev/zero" || path === "/dev/urandom"
|
|
158
|
+
|| path === "/dev/random" || path === "/dev/tty" || path === "/dev/console"
|
|
159
|
+
|| path === "/dev/ptmx")
|
|
160
|
+
return;
|
|
161
|
+
return vfs.writeFile(path, content);
|
|
162
|
+
},
|
|
163
|
+
async createDir(path) {
|
|
164
|
+
if (isDeviceDir(path))
|
|
165
|
+
return;
|
|
166
|
+
return vfs.createDir(path);
|
|
167
|
+
},
|
|
168
|
+
async mkdir(path, options) {
|
|
169
|
+
if (isDeviceDir(path))
|
|
170
|
+
return;
|
|
171
|
+
return vfs.mkdir(path, options);
|
|
172
|
+
},
|
|
173
|
+
async exists(path) {
|
|
174
|
+
if (isDevicePath(path) || isDeviceDir(path))
|
|
175
|
+
return true;
|
|
176
|
+
return vfs.exists(path);
|
|
177
|
+
},
|
|
178
|
+
async stat(path) {
|
|
179
|
+
if (isDevicePath(path))
|
|
180
|
+
return deviceStat(path);
|
|
181
|
+
if (isDeviceDir(path)) {
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
return {
|
|
184
|
+
mode: 0o755,
|
|
185
|
+
size: 0,
|
|
186
|
+
isDirectory: true,
|
|
187
|
+
isSymbolicLink: false,
|
|
188
|
+
atimeMs: now,
|
|
189
|
+
mtimeMs: now,
|
|
190
|
+
ctimeMs: now,
|
|
191
|
+
birthtimeMs: now,
|
|
192
|
+
ino: DEVICE_INO[path] ?? 0xffff_0000,
|
|
193
|
+
nlink: 2,
|
|
194
|
+
uid: 0,
|
|
195
|
+
gid: 0,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return vfs.stat(path);
|
|
199
|
+
},
|
|
200
|
+
async removeFile(path) {
|
|
201
|
+
if (isDevicePath(path))
|
|
202
|
+
throw new KernelError("EPERM", "cannot remove device");
|
|
203
|
+
return vfs.removeFile(path);
|
|
204
|
+
},
|
|
205
|
+
async removeDir(path) {
|
|
206
|
+
if (isDeviceDir(path))
|
|
207
|
+
throw new KernelError("EPERM", `cannot remove ${path}`);
|
|
208
|
+
return vfs.removeDir(path);
|
|
209
|
+
},
|
|
210
|
+
async rename(oldPath, newPath) {
|
|
211
|
+
if (isDevicePath(oldPath) || isDevicePath(newPath)) {
|
|
212
|
+
throw new KernelError("EPERM", "cannot rename device");
|
|
213
|
+
}
|
|
214
|
+
return vfs.rename(oldPath, newPath);
|
|
215
|
+
},
|
|
216
|
+
async realpath(path) {
|
|
217
|
+
if (isDevicePath(path) || isDeviceDir(path))
|
|
218
|
+
return path;
|
|
219
|
+
return vfs.realpath(path);
|
|
220
|
+
},
|
|
221
|
+
// Passthrough for POSIX extensions
|
|
222
|
+
async symlink(target, linkPath) {
|
|
223
|
+
return vfs.symlink(target, linkPath);
|
|
224
|
+
},
|
|
225
|
+
async readlink(path) {
|
|
226
|
+
return vfs.readlink(path);
|
|
227
|
+
},
|
|
228
|
+
async lstat(path) {
|
|
229
|
+
if (isDevicePath(path))
|
|
230
|
+
return deviceStat(path);
|
|
231
|
+
if (isDeviceDir(path))
|
|
232
|
+
return this.stat(path);
|
|
233
|
+
return vfs.lstat(path);
|
|
234
|
+
},
|
|
235
|
+
async link(oldPath, newPath) {
|
|
236
|
+
if (isDevicePath(oldPath))
|
|
237
|
+
throw new KernelError("EPERM", "cannot link device");
|
|
238
|
+
return vfs.link(oldPath, newPath);
|
|
239
|
+
},
|
|
240
|
+
async chmod(path, mode) {
|
|
241
|
+
if (isDevicePath(path))
|
|
242
|
+
return;
|
|
243
|
+
return vfs.chmod(path, mode);
|
|
244
|
+
},
|
|
245
|
+
async chown(path, uid, gid) {
|
|
246
|
+
if (isDevicePath(path))
|
|
247
|
+
return;
|
|
248
|
+
return vfs.chown(path, uid, gid);
|
|
249
|
+
},
|
|
250
|
+
async utimes(path, atime, mtime) {
|
|
251
|
+
if (isDevicePath(path))
|
|
252
|
+
return;
|
|
253
|
+
return vfs.utimes(path, atime, mtime);
|
|
254
|
+
},
|
|
255
|
+
async truncate(path, length) {
|
|
256
|
+
if (isDevicePath(path))
|
|
257
|
+
return;
|
|
258
|
+
return vfs.truncate(path, length);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
return wrapped;
|
|
262
|
+
}
|