novac 2.0.1 → 2.2.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/LICENSE +1 -1
- package/README.md +1574 -597
- package/bin/novac +468 -171
- package/bin/nvc +522 -0
- package/bin/nvml +78 -17
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitansi/kitdef.js +1402 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitformat/kitdef.js +1485 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmatrix/ex.js +19 -0
- package/kits/kitmatrix/kitdef.js +960 -0
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitnovacweb/README.md +1416 -143
- package/kits/kitnovacweb/kitdef.js +92 -2
- package/kits/kitnovacweb/nvml/executor.js +578 -176
- package/kits/kitnovacweb/nvml/index.js +2 -2
- package/kits/kitnovacweb/nvml/lexer.js +72 -69
- package/kits/kitnovacweb/nvml/parser.js +328 -159
- package/kits/kitnovacweb/nvml/renderer.js +770 -270
- package/kits/kitparse/kitdef.js +1688 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitregex++/kitdef.js +1353 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/kitx11/kitdef.js +1 -0
- package/kits/kitx11/kitx11.js +2472 -0
- package/kits/kitx11/kitx11_conn.js +948 -0
- package/kits/kitx11/kitx11_worker.js +121 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/ex.js +285 -0
- package/kits/libterm/kitdef.js +1927 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libtea/tf.js +2691 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +6 -3
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +838 -362
- package/src/core/executor.js +2578 -170
- package/src/core/lexer.js +502 -54
- package/src/core/nova_builtins.js +21 -3
- package/src/core/parser.js +413 -72
- package/src/core/types.js +30 -2
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,2691 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tea's F — Virtual Filesystem (v2.0.0)
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - POSIX-style path mode (default)
|
|
6
|
+
* - Windows-style path mode: mode: 'win' → C:\Users\...
|
|
7
|
+
* - URL-style path mode: mode: 'url' → http://host/path/...
|
|
8
|
+
* - Snapshots saved to ./snaps/ (not /tmp)
|
|
9
|
+
* - Mount appends into /mnt/ and fixes duplicate-segment collisions
|
|
10
|
+
* - Real watcher events: 'change', 'rename', 'unlink', 'mkdir', 'write'
|
|
11
|
+
* - Permission enforcement (uid/gid/mode checks)
|
|
12
|
+
* - exec: shell interpreter (sh-compatible lexer + parser + evaluator)
|
|
13
|
+
* - ENV variables, PATH lookup, pipelines, redirections, &&/||/;
|
|
14
|
+
* - Built-ins: cd, echo, export, unset, env, pwd, alias, type, which, exit
|
|
15
|
+
* - Executable files in bin/, she-bang (#!) support
|
|
16
|
+
* - REPL: tf.repl() → interactive shell session object
|
|
17
|
+
* - TeaBin: custom binary executable format
|
|
18
|
+
* - Registers: R0–R15, SP, PC, FLAGS
|
|
19
|
+
* - Opcodes: MOV, ADD, SUB, MUL, DIV, MOD, AND, OR, XOR, NOT, SHL, SHR,
|
|
20
|
+
* CMP, JMP, JE, JNE, JL, JG, JLE, JGE, CALL, RET, PUSH, POP,
|
|
21
|
+
* LOAD, STORE, ALLOC, FREE, IN, OUT, HALT, NOP, INT
|
|
22
|
+
* - Heap memory (Uint8Array), stack, syscall interrupts
|
|
23
|
+
*
|
|
24
|
+
* Architecture:
|
|
25
|
+
* The entire FS is one JS object:
|
|
26
|
+
* {
|
|
27
|
+
* meta: { created, label, version },
|
|
28
|
+
* tree: { "/": { type:"dir", mode:0o755, uid:0, gid:0, atime, mtime, ctime, children:{} }, ... },
|
|
29
|
+
* inodeCounter: 3
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Path resolution always starts from "/" (or drive root in win mode).
|
|
33
|
+
* Hard links, symlinks, permissions, mounts all supported.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
"use strict";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Constants
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const S_IFREG = 0o100000;
|
|
43
|
+
const S_IFDIR = 0o040000;
|
|
44
|
+
const S_IFLNK = 0o120000;
|
|
45
|
+
|
|
46
|
+
const S_IRUSR = 0o400; const S_IWUSR = 0o200; const S_IXUSR = 0o100;
|
|
47
|
+
const S_IRGRP = 0o040; const S_IWGRP = 0o020; const S_IXGRP = 0o010;
|
|
48
|
+
const S_IROTH = 0o004; const S_IWOTH = 0o002; const S_IXOTH = 0o001;
|
|
49
|
+
|
|
50
|
+
const O_RDONLY = 0;
|
|
51
|
+
const O_WRONLY = 1;
|
|
52
|
+
const O_RDWR = 2;
|
|
53
|
+
const O_CREAT = 64;
|
|
54
|
+
const O_EXCL = 128;
|
|
55
|
+
const O_TRUNC = 512;
|
|
56
|
+
const O_APPEND = 1024;
|
|
57
|
+
|
|
58
|
+
const COPYFILE_EXCL = 1;
|
|
59
|
+
const VERSION = "2.0.0";
|
|
60
|
+
const SNAP_DIR = "./snaps";
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Errors
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
class TFError extends Error {
|
|
67
|
+
constructor(code, message, path) {
|
|
68
|
+
super(message + (path ? ` '${path}'` : ""));
|
|
69
|
+
this.code = code; this.path = path; this.name = "TFError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function err(code, path) {
|
|
74
|
+
const messages = {
|
|
75
|
+
ENOENT: "no such file or directory", EEXIST: "file already exists",
|
|
76
|
+
ENOTDIR: "not a directory", EISDIR: "is a directory",
|
|
77
|
+
ENOTEMPTY: "directory not empty", EACCES: "permission denied",
|
|
78
|
+
EBADF: "bad file descriptor", EINVAL: "invalid argument",
|
|
79
|
+
ELOOP: "too many levels of symbolic links", EPERM: "operation not permitted",
|
|
80
|
+
ENOTSUP: "operation not supported", ENOTEXEC: "exec format error",
|
|
81
|
+
ENOMEM: "out of memory", EFAULT: "bad address",
|
|
82
|
+
};
|
|
83
|
+
return new TFError(code, messages[code] || code, path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Path mode adapters
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* PathAdapter: normalises between posix / win / url path representations.
|
|
92
|
+
* Internally we always use posix "/"-separated paths.
|
|
93
|
+
* The adapter converts user-supplied paths → internal, and internal → display.
|
|
94
|
+
*/
|
|
95
|
+
class PathAdapter {
|
|
96
|
+
constructor(mode = "posix") {
|
|
97
|
+
this.mode = mode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Convert external path (user-supplied) to internal posix path
|
|
101
|
+
toInternal(p) {
|
|
102
|
+
if (typeof p !== "string") throw err("EINVAL");
|
|
103
|
+
switch (this.mode) {
|
|
104
|
+
case "win": return this._winToInternal(p);
|
|
105
|
+
case "url": return this._urlToInternal(p);
|
|
106
|
+
default: return p; // posix: pass through
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Convert internal posix path to external display form
|
|
111
|
+
toExternal(p) {
|
|
112
|
+
switch (this.mode) {
|
|
113
|
+
case "win": return this._internalToWin(p);
|
|
114
|
+
case "url": return this._internalToUrl(p);
|
|
115
|
+
default: return p;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_winToInternal(p) {
|
|
120
|
+
// "C:\Users\foo\bar" → "/C/Users/foo/bar"
|
|
121
|
+
// "C:/" → "/C"
|
|
122
|
+
// "\foo\bar" → "/foo/bar"
|
|
123
|
+
// "foo\bar" → "/foo/bar"
|
|
124
|
+
let s = p.replace(/\\/g, "/");
|
|
125
|
+
const driveMatch = s.match(/^([A-Za-z]):(\/.*)?$/);
|
|
126
|
+
if (driveMatch) {
|
|
127
|
+
const drive = driveMatch[1].toUpperCase();
|
|
128
|
+
const rest = driveMatch[2] || "/";
|
|
129
|
+
s = "/" + drive + rest;
|
|
130
|
+
} else if (!s.startsWith("/")) {
|
|
131
|
+
s = "/" + s;
|
|
132
|
+
}
|
|
133
|
+
return s;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_internalToWin(p) {
|
|
137
|
+
// "/C/Users/foo" → "C:\Users\foo"
|
|
138
|
+
const m = p.match(/^\/([A-Z])(\/.*)?$/);
|
|
139
|
+
if (m) {
|
|
140
|
+
return m[1] + ":" + (m[2] || "\\").replace(/\//g, "\\");
|
|
141
|
+
}
|
|
142
|
+
return p.replace(/\//g, "\\");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_urlToInternal(p) {
|
|
146
|
+
// "http://hostname/path/to/file" → "/http/hostname/path/to/file"
|
|
147
|
+
// "https://example.com/a/b" → "/https/example.com/a/b"
|
|
148
|
+
// "/path" → "/path"
|
|
149
|
+
const m = p.match(/^([a-zA-Z][a-zA-Z0-9+\-.]*):\/\/([^/]*)(\/.*)?$/);
|
|
150
|
+
if (m) {
|
|
151
|
+
const scheme = m[1].toLowerCase();
|
|
152
|
+
const host = m[2];
|
|
153
|
+
const rest = m[3] || "/";
|
|
154
|
+
return "/" + scheme + "/" + host + rest;
|
|
155
|
+
}
|
|
156
|
+
return p.startsWith("/") ? p : "/" + p;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
_internalToUrl(p) {
|
|
160
|
+
// "/http/example.com/a/b" → "http://example.com/a/b"
|
|
161
|
+
const m = p.match(/^\/([a-z][a-z0-9+\-.]*)\/([^/]+)(\/.*)?$/);
|
|
162
|
+
if (m) {
|
|
163
|
+
return m[1] + "://" + m[2] + (m[3] || "/");
|
|
164
|
+
}
|
|
165
|
+
return p;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Utilities
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
function now() { return new Date().toISOString(); }
|
|
174
|
+
|
|
175
|
+
function normPath(p) {
|
|
176
|
+
if (typeof p !== "string") throw err("EINVAL");
|
|
177
|
+
const abs = p.startsWith("/") ? p : "/" + p;
|
|
178
|
+
const parts = abs.split("/").filter(Boolean);
|
|
179
|
+
const stack = [];
|
|
180
|
+
for (const part of parts) {
|
|
181
|
+
if (part === ".") continue;
|
|
182
|
+
if (part === "..") stack.pop();
|
|
183
|
+
else stack.push(part);
|
|
184
|
+
}
|
|
185
|
+
return "/" + stack.join("/");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function splitPath(p) {
|
|
189
|
+
const n = normPath(p);
|
|
190
|
+
if (n === "/") return { dir: "/", base: "" };
|
|
191
|
+
const idx = n.lastIndexOf("/");
|
|
192
|
+
return { dir: idx === 0 ? "/" : n.slice(0, idx), base: n.slice(idx + 1) };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function matchGlob(pattern, str) {
|
|
196
|
+
const re = pattern
|
|
197
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
198
|
+
.replace(/\*\*/g, "§DOUBLE§").replace(/\*/g, "[^/]*")
|
|
199
|
+
.replace(/§DOUBLE§/g, ".*").replace(/\?/g, "[^/]");
|
|
200
|
+
return new RegExp("^" + re + "$").test(str);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Simple EventEmitter
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
class EventEmitter {
|
|
208
|
+
constructor() { this._events = {}; }
|
|
209
|
+
on(ev, fn) { (this._events[ev] = this._events[ev] || []).push(fn); return this; }
|
|
210
|
+
off(ev, fn) { if (this._events[ev]) this._events[ev] = this._events[ev].filter(f => f !== fn); return this; }
|
|
211
|
+
emit(ev, ...args) { (this._events[ev] || []).slice().forEach(fn => { try { fn(...args); } catch(_) {} }); }
|
|
212
|
+
once(ev, fn) {
|
|
213
|
+
const w = (...args) => { fn(...args); this.off(ev, w); };
|
|
214
|
+
return this.on(ev, w);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Stats
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
class Stats {
|
|
223
|
+
constructor(node, nodeId) {
|
|
224
|
+
this.dev = 1;
|
|
225
|
+
this.ino = parseInt(nodeId.replace(/\D/g, ""), 10) || 0;
|
|
226
|
+
this.mode = (node.mode & 0o7777) | (node.type === "dir" ? S_IFDIR : node.type === "symlink" ? S_IFLNK : S_IFREG);
|
|
227
|
+
this.nlink = node.nlink || 1;
|
|
228
|
+
this.uid = node.uid || 0;
|
|
229
|
+
this.gid = node.gid || 0;
|
|
230
|
+
this.rdev = 0;
|
|
231
|
+
this.size = node.type === "file" ? (node.data || "").length : node.type === "symlink" ? (node.link || "").length : 4096;
|
|
232
|
+
this.blksize = 4096;
|
|
233
|
+
this.blocks = Math.ceil(this.size / 512);
|
|
234
|
+
this.atimeMs = new Date(node.atime || node.ctime).getTime();
|
|
235
|
+
this.mtimeMs = new Date(node.mtime || node.ctime).getTime();
|
|
236
|
+
this.ctimeMs = new Date(node.ctime).getTime();
|
|
237
|
+
this.birthtimeMs = new Date(node.ctime).getTime();
|
|
238
|
+
this.atime = new Date(this.atimeMs); this.mtime = new Date(this.mtimeMs);
|
|
239
|
+
this.ctime = new Date(this.ctimeMs); this.birthtime = new Date(this.birthtimeMs);
|
|
240
|
+
this._type = node.type;
|
|
241
|
+
}
|
|
242
|
+
isFile() { return this._type === "file"; }
|
|
243
|
+
isDirectory() { return this._type === "dir"; }
|
|
244
|
+
isSymbolicLink() { return this._type === "symlink"; }
|
|
245
|
+
isBlockDevice() { return false; }
|
|
246
|
+
isCharacterDevice() { return false; }
|
|
247
|
+
isFIFO() { return false; }
|
|
248
|
+
isSocket() { return false; }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ===========================================================================
|
|
252
|
+
// TeaBin — Custom Binary Executable Format + VM
|
|
253
|
+
// ===========================================================================
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Binary format (little-endian):
|
|
257
|
+
* Magic: 0x54 0x45 0x41 0x42 ("TEAB")
|
|
258
|
+
* Version: 1 byte
|
|
259
|
+
* Data segment size: 4 bytes (uint32)
|
|
260
|
+
* Data segment: N bytes
|
|
261
|
+
* Code segment size: 4 bytes (uint32)
|
|
262
|
+
* Code segment: each instruction = 6 bytes:
|
|
263
|
+
* [opcode:1][mode:1][arg0:2][arg1:2] (uint16 args)
|
|
264
|
+
*
|
|
265
|
+
* Registers: R0–R15 (index 0–15), SP (16), PC (17)
|
|
266
|
+
* FLAGS: ZF (zero), SF (sign), OF (overflow), CF (carry)
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
const OP = {
|
|
270
|
+
NOP: 0x00,
|
|
271
|
+
// Data movement
|
|
272
|
+
MOV: 0x01, // MOV dst_reg, src (reg or imm)
|
|
273
|
+
LOAD: 0x02, // LOAD dst_reg, addr_reg — load word from memory[addr]
|
|
274
|
+
STORE: 0x03, // STORE addr_reg, src_reg — store word to memory[addr]
|
|
275
|
+
PUSH: 0x04, // PUSH src_reg
|
|
276
|
+
POP: 0x05, // POP dst_reg
|
|
277
|
+
// Arithmetic
|
|
278
|
+
ADD: 0x10,
|
|
279
|
+
SUB: 0x11,
|
|
280
|
+
MUL: 0x12,
|
|
281
|
+
DIV: 0x13,
|
|
282
|
+
MOD: 0x14,
|
|
283
|
+
// Bitwise
|
|
284
|
+
AND: 0x20,
|
|
285
|
+
OR: 0x21,
|
|
286
|
+
XOR: 0x22,
|
|
287
|
+
NOT: 0x23,
|
|
288
|
+
SHL: 0x24,
|
|
289
|
+
SHR: 0x25,
|
|
290
|
+
// Compare / branch
|
|
291
|
+
CMP: 0x30, // CMP reg, reg/imm — sets FLAGS
|
|
292
|
+
JMP: 0x31, // JMP addr (imm = instruction index)
|
|
293
|
+
JE: 0x32, // Jump if ZF
|
|
294
|
+
JNE: 0x33,
|
|
295
|
+
JL: 0x34, // Jump if SF != OF
|
|
296
|
+
JG: 0x35, // Jump if ZF=0 and SF=OF
|
|
297
|
+
JLE: 0x36,
|
|
298
|
+
JGE: 0x37,
|
|
299
|
+
// Subroutines
|
|
300
|
+
CALL: 0x40, // CALL addr — pushes PC, jumps
|
|
301
|
+
RET: 0x41, // RET — pops PC
|
|
302
|
+
// Memory management
|
|
303
|
+
ALLOC: 0x50, // ALLOC dst_reg, size_reg — malloc(size) → pointer in dst
|
|
304
|
+
FREE: 0x51, // FREE addr_reg
|
|
305
|
+
// I/O
|
|
306
|
+
IN: 0x60, // IN dst_reg, port
|
|
307
|
+
OUT: 0x61, // OUT port, src_reg
|
|
308
|
+
// System
|
|
309
|
+
INT: 0x70, // INT vector — software interrupt / syscall
|
|
310
|
+
HALT: 0xFF,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Arg mode flags (encoded in 'mode' byte of instruction)
|
|
314
|
+
const ARG_REG = 0; // argument is register index
|
|
315
|
+
const ARG_IMM = 1; // argument is immediate value
|
|
316
|
+
// mode byte: bits[0]=arg0 is imm, bits[1]=arg1 is imm
|
|
317
|
+
|
|
318
|
+
const REG_COUNT = 18; // R0–R15 + SP(16) + PC(17)
|
|
319
|
+
const SP_IDX = 16;
|
|
320
|
+
const PC_IDX = 17;
|
|
321
|
+
const HEAP_SIZE = 1024 * 64; // 64 KB
|
|
322
|
+
|
|
323
|
+
class TeaBinVM {
|
|
324
|
+
constructor(opts = {}) {
|
|
325
|
+
this.regs = new Int32Array(REG_COUNT); // registers
|
|
326
|
+
this.mem = new Uint8Array(HEAP_SIZE); // flat heap
|
|
327
|
+
this.stack = []; // JS-array stack (separate from heap)
|
|
328
|
+
this.flags = { ZF: false, SF: false, OF: false, CF: false };
|
|
329
|
+
this._heapAllocs = new Map(); // ptr → size
|
|
330
|
+
this._heapTop = 0;
|
|
331
|
+
this.halted = false;
|
|
332
|
+
this.exitCode = 0;
|
|
333
|
+
this._ports = opts.ports || {}; // port → {read, write} handlers
|
|
334
|
+
this._intHandlers = opts.interrupts || {}; // vector → fn(vm)
|
|
335
|
+
this.stdout = [];
|
|
336
|
+
this.stdin = [];
|
|
337
|
+
this._maxCycles = opts.maxCycles || 1_000_000;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Load a TEAB binary (ArrayBuffer or Uint8Array) */
|
|
341
|
+
load(binary) {
|
|
342
|
+
const buf = binary instanceof Uint8Array ? binary : new Uint8Array(binary);
|
|
343
|
+
let off = 0;
|
|
344
|
+
|
|
345
|
+
// Magic
|
|
346
|
+
if (buf[0] !== 0x54 || buf[1] !== 0x45 || buf[2] !== 0x41 || buf[3] !== 0x42)
|
|
347
|
+
throw new Error("Invalid TEAB magic");
|
|
348
|
+
off = 4;
|
|
349
|
+
|
|
350
|
+
const _version = buf[off++]; // reserved
|
|
351
|
+
|
|
352
|
+
// Data segment
|
|
353
|
+
const dataSize = buf[off] | (buf[off+1]<<8) | (buf[off+2]<<16) | (buf[off+3]<<24);
|
|
354
|
+
off += 4;
|
|
355
|
+
this.mem.set(buf.subarray(off, off + dataSize), 0);
|
|
356
|
+
this._heapTop = dataSize;
|
|
357
|
+
off += dataSize;
|
|
358
|
+
|
|
359
|
+
// Code segment
|
|
360
|
+
const codeSize = buf[off] | (buf[off+1]<<8) | (buf[off+2]<<16) | (buf[off+3]<<24);
|
|
361
|
+
off += 4;
|
|
362
|
+
this._instructions = [];
|
|
363
|
+
for (let i = 0; i < codeSize; i += 6) {
|
|
364
|
+
const opcode = buf[off + i];
|
|
365
|
+
const mode = buf[off + i + 1];
|
|
366
|
+
const arg0 = buf[off + i + 2] | (buf[off + i + 3] << 8);
|
|
367
|
+
const arg1 = buf[off + i + 4] | (buf[off + i + 5] << 8);
|
|
368
|
+
this._instructions.push({ opcode, mode, arg0, arg1 });
|
|
369
|
+
}
|
|
370
|
+
this.regs[SP_IDX] = HEAP_SIZE - 1; // stack grows down in heap (but we use JS stack)
|
|
371
|
+
this.regs[PC_IDX] = 0;
|
|
372
|
+
return this;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Build a TEAB binary from an assembly-like source string */
|
|
376
|
+
static assemble(src) {
|
|
377
|
+
const lines = src.split(/\n/).map(l => l.replace(/;.*/, "").trim()).filter(Boolean);
|
|
378
|
+
const labels = {};
|
|
379
|
+
// First pass: collect labels
|
|
380
|
+
let instrIdx = 0;
|
|
381
|
+
for (const line of lines) {
|
|
382
|
+
if (line.endsWith(":")) { labels[line.slice(0, -1)] = instrIdx; continue; }
|
|
383
|
+
if (line.startsWith(".data")) continue;
|
|
384
|
+
instrIdx++;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const instructions = [];
|
|
388
|
+
const dataBytes = [];
|
|
389
|
+
let inData = false;
|
|
390
|
+
const dataLabels = {};
|
|
391
|
+
|
|
392
|
+
for (const line of lines) {
|
|
393
|
+
if (line.startsWith(".data")) { inData = true; continue; }
|
|
394
|
+
if (line.startsWith(".code")) { inData = false; continue; }
|
|
395
|
+
if (line.endsWith(":")) continue; // label
|
|
396
|
+
|
|
397
|
+
if (inData) {
|
|
398
|
+
// .data label "string" or .data label 0x...
|
|
399
|
+
const m = line.match(/^(\w+)\s+"(.*)"$/) || line.match(/^(\w+)\s+(.+)$/);
|
|
400
|
+
if (m) {
|
|
401
|
+
dataLabels[m[1]] = dataBytes.length;
|
|
402
|
+
const val = m[2];
|
|
403
|
+
if (val.startsWith('"')) {
|
|
404
|
+
for (const ch of val.slice(1, -1)) dataBytes.push(ch.charCodeAt(0));
|
|
405
|
+
dataBytes.push(0); // null terminate
|
|
406
|
+
} else {
|
|
407
|
+
const n = parseInt(val, 0);
|
|
408
|
+
dataBytes.push(n & 0xFF, (n>>8)&0xFF, (n>>16)&0xFF, (n>>24)&0xFF);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const parts = line.split(/[\s,]+/);
|
|
415
|
+
const mnemo = parts[0].toUpperCase();
|
|
416
|
+
const opcode = OP[mnemo];
|
|
417
|
+
if (opcode === undefined) throw new Error(`Unknown opcode: ${mnemo}`);
|
|
418
|
+
|
|
419
|
+
const parseArg = (s) => {
|
|
420
|
+
if (!s) return { val: 0, imm: false };
|
|
421
|
+
if (/^R\d+$/i.test(s)) return { val: parseInt(s.slice(1)), imm: false };
|
|
422
|
+
if (s.toUpperCase() === "SP") return { val: SP_IDX, imm: false };
|
|
423
|
+
if (s.toUpperCase() === "PC") return { val: PC_IDX, imm: false };
|
|
424
|
+
if (labels[s] !== undefined) return { val: labels[s], imm: true };
|
|
425
|
+
if (dataLabels[s] !== undefined) return { val: dataLabels[s], imm: true };
|
|
426
|
+
return { val: parseInt(s, 0) & 0xFFFF, imm: true };
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const a0 = parseArg(parts[1]);
|
|
430
|
+
const a1 = parseArg(parts[2]);
|
|
431
|
+
const mode = (a0.imm ? 1 : 0) | (a1.imm ? 2 : 0);
|
|
432
|
+
instructions.push({ opcode, mode, arg0: a0.val & 0xFFFF, arg1: a1.val & 0xFFFF });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Encode
|
|
436
|
+
const dataSize = dataBytes.length;
|
|
437
|
+
const codeSize = instructions.length * 6;
|
|
438
|
+
const total = 4 + 1 + 4 + dataSize + 4 + codeSize;
|
|
439
|
+
const buf = new Uint8Array(total);
|
|
440
|
+
let off = 0;
|
|
441
|
+
buf[off++]=0x54; buf[off++]=0x45; buf[off++]=0x41; buf[off++]=0x42; // TEAB
|
|
442
|
+
buf[off++]=1; // version
|
|
443
|
+
buf[off++]=dataSize&0xFF; buf[off++]=(dataSize>>8)&0xFF;
|
|
444
|
+
buf[off++]=(dataSize>>16)&0xFF; buf[off++]=(dataSize>>24)&0xFF;
|
|
445
|
+
for (const b of dataBytes) buf[off++]=b;
|
|
446
|
+
buf[off++]=codeSize&0xFF; buf[off++]=(codeSize>>8)&0xFF;
|
|
447
|
+
buf[off++]=(codeSize>>16)&0xFF; buf[off++]=(codeSize>>24)&0xFF;
|
|
448
|
+
for (const ins of instructions) {
|
|
449
|
+
buf[off++]=ins.opcode; buf[off++]=ins.mode;
|
|
450
|
+
buf[off++]=ins.arg0&0xFF; buf[off++]=(ins.arg0>>8)&0xFF;
|
|
451
|
+
buf[off++]=ins.arg1&0xFF; buf[off++]=(ins.arg1>>8)&0xFF;
|
|
452
|
+
}
|
|
453
|
+
return buf;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
_getReg(idx) { return this.regs[idx] | 0; }
|
|
457
|
+
_setReg(idx, val) {
|
|
458
|
+
this.regs[idx] = val | 0;
|
|
459
|
+
if (idx === PC_IDX) return;
|
|
460
|
+
this.flags.ZF = (val === 0);
|
|
461
|
+
this.flags.SF = (val < 0);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
_arg(val, isImm) { return isImm ? (val | 0) : this._getReg(val); }
|
|
465
|
+
|
|
466
|
+
_memReadWord(addr) {
|
|
467
|
+
addr = addr & 0xFFFF;
|
|
468
|
+
return this.mem[addr] | (this.mem[addr+1]<<8) | (this.mem[addr+2]<<16) | (this.mem[addr+3]<<24);
|
|
469
|
+
}
|
|
470
|
+
_memWriteWord(addr, val) {
|
|
471
|
+
addr = addr & 0xFFFF;
|
|
472
|
+
this.mem[addr]=val&0xFF; this.mem[addr+1]=(val>>8)&0xFF;
|
|
473
|
+
this.mem[addr+2]=(val>>16)&0xFF; this.mem[addr+3]=(val>>24)&0xFF;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
_alloc(size) {
|
|
477
|
+
const ptr = this._heapTop;
|
|
478
|
+
if (ptr + size > HEAP_SIZE) throw err("ENOMEM");
|
|
479
|
+
this._heapAllocs.set(ptr, size);
|
|
480
|
+
this._heapTop += size;
|
|
481
|
+
return ptr;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_free(ptr) { this._heapAllocs.delete(ptr); }
|
|
485
|
+
|
|
486
|
+
_setFlags(val) {
|
|
487
|
+
this.flags.ZF = (val === 0);
|
|
488
|
+
this.flags.SF = (val < 0);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
run() {
|
|
492
|
+
let cycles = 0;
|
|
493
|
+
const ins = this._instructions;
|
|
494
|
+
while (!this.halted && this.regs[PC_IDX] < ins.length && cycles < this._maxCycles) {
|
|
495
|
+
const pc = this.regs[PC_IDX]++;
|
|
496
|
+
const { opcode, mode, arg0, arg1 } = ins[pc];
|
|
497
|
+
const imm0 = (mode & 1) !== 0;
|
|
498
|
+
const imm1 = (mode & 2) !== 0;
|
|
499
|
+
const v0 = this._arg(arg0, imm0);
|
|
500
|
+
const v1 = this._arg(arg1, imm1);
|
|
501
|
+
|
|
502
|
+
switch (opcode) {
|
|
503
|
+
case OP.NOP: break;
|
|
504
|
+
case OP.HALT: this.halted = true; this.exitCode = v0; break;
|
|
505
|
+
|
|
506
|
+
case OP.MOV: this._setReg(arg0, v1); break;
|
|
507
|
+
|
|
508
|
+
case OP.LOAD: {
|
|
509
|
+
const word = this._memReadWord(v1);
|
|
510
|
+
this._setReg(arg0, word);
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
case OP.STORE: this._memWriteWord(v0, v1); break;
|
|
514
|
+
|
|
515
|
+
case OP.PUSH:
|
|
516
|
+
this.stack.push(v0);
|
|
517
|
+
break;
|
|
518
|
+
case OP.POP:
|
|
519
|
+
if (this.stack.length === 0) throw new Error("Stack underflow");
|
|
520
|
+
this._setReg(arg0, this.stack.pop());
|
|
521
|
+
break;
|
|
522
|
+
|
|
523
|
+
case OP.ADD: { const r = (v0 + v1)|0; this._setFlags(r); this.flags.CF = (v0+v1) > 0x7FFFFFFF; this._setReg(arg0, r); break; }
|
|
524
|
+
case OP.SUB: { const r = (v0 - v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
525
|
+
case OP.MUL: { const r = Math.imul(v0, v1); this._setFlags(r); this._setReg(arg0, r); break; }
|
|
526
|
+
case OP.DIV: { if (v1===0) throw new Error("Division by zero"); const r=(v0/v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
527
|
+
case OP.MOD: { if (v1===0) throw new Error("Division by zero"); const r=v0%v1; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
528
|
+
|
|
529
|
+
case OP.AND: { const r=v0&v1; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
530
|
+
case OP.OR: { const r=v0|v1; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
531
|
+
case OP.XOR: { const r=v0^v1; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
532
|
+
case OP.NOT: { const r=~v0; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
533
|
+
case OP.SHL: { const r=(v0<<v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
534
|
+
case OP.SHR: { const r=(v0>>v1)|0; this._setFlags(r); this._setReg(arg0, r); break; }
|
|
535
|
+
|
|
536
|
+
case OP.CMP: {
|
|
537
|
+
const r = (v0 - v1)|0;
|
|
538
|
+
this.flags.ZF = (r === 0);
|
|
539
|
+
this.flags.SF = (r < 0);
|
|
540
|
+
this.flags.OF = ((v0 >= 0 && v1 < 0 && r < 0) || (v0 < 0 && v1 >= 0 && r >= 0));
|
|
541
|
+
this.flags.CF = (v0 < v1);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
case OP.JMP: this.regs[PC_IDX] = v0; break;
|
|
546
|
+
case OP.JE: if (this.flags.ZF) this.regs[PC_IDX] = v0; break;
|
|
547
|
+
case OP.JNE: if (!this.flags.ZF) this.regs[PC_IDX] = v0; break;
|
|
548
|
+
case OP.JL: if (this.flags.SF !== this.flags.OF) this.regs[PC_IDX] = v0; break;
|
|
549
|
+
case OP.JG: if (!this.flags.ZF && this.flags.SF === this.flags.OF) this.regs[PC_IDX] = v0; break;
|
|
550
|
+
case OP.JLE: if (this.flags.ZF || this.flags.SF !== this.flags.OF) this.regs[PC_IDX] = v0; break;
|
|
551
|
+
case OP.JGE: if (this.flags.SF === this.flags.OF) this.regs[PC_IDX] = v0; break;
|
|
552
|
+
|
|
553
|
+
case OP.CALL:
|
|
554
|
+
this.stack.push(this.regs[PC_IDX]);
|
|
555
|
+
this.regs[PC_IDX] = v0;
|
|
556
|
+
break;
|
|
557
|
+
case OP.RET:
|
|
558
|
+
if (this.stack.length === 0) { this.halted = true; break; }
|
|
559
|
+
this.regs[PC_IDX] = this.stack.pop();
|
|
560
|
+
break;
|
|
561
|
+
|
|
562
|
+
case OP.ALLOC: {
|
|
563
|
+
const ptr = this._alloc(v1);
|
|
564
|
+
this._setReg(arg0, ptr);
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
case OP.FREE: this._free(v0); break;
|
|
568
|
+
|
|
569
|
+
case OP.IN: {
|
|
570
|
+
const port = v1;
|
|
571
|
+
const handler = this._ports[port];
|
|
572
|
+
const val = handler ? handler.read() : (this.stdin.shift() || 0);
|
|
573
|
+
this._setReg(arg0, val);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
case OP.OUT: {
|
|
577
|
+
const port = v0;
|
|
578
|
+
const val = v1;
|
|
579
|
+
const handler = this._ports[port];
|
|
580
|
+
if (handler) { handler.write(val); }
|
|
581
|
+
else { this.stdout.push(val); }
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case OP.INT: {
|
|
586
|
+
const vec = v0;
|
|
587
|
+
const handler = this._intHandlers[vec];
|
|
588
|
+
if (handler) handler(this);
|
|
589
|
+
// Syscall table for INT 0x80 (Linux-style)
|
|
590
|
+
else if (vec === 0x80) this._syscall();
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
default:
|
|
595
|
+
throw new Error(`Unknown opcode: 0x${opcode.toString(16)}`);
|
|
596
|
+
}
|
|
597
|
+
cycles++;
|
|
598
|
+
}
|
|
599
|
+
if (cycles >= this._maxCycles) throw new Error("CPU cycle limit exceeded");
|
|
600
|
+
return this.exitCode;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
_syscall() {
|
|
604
|
+
// R0 = syscall number, R1–R5 = args
|
|
605
|
+
const num = this._getReg(0);
|
|
606
|
+
switch (num) {
|
|
607
|
+
case 1: // write(fd, buf_ptr, len) → writes string to stdout
|
|
608
|
+
{
|
|
609
|
+
const ptr = this._getReg(2);
|
|
610
|
+
const len = this._getReg(3);
|
|
611
|
+
let s = "";
|
|
612
|
+
for (let i = 0; i < len; i++) s += String.fromCharCode(this.mem[(ptr + i) & 0xFFFF] || 0);
|
|
613
|
+
this.stdout.push(s);
|
|
614
|
+
this._setReg(0, len);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
case 60: // exit(code)
|
|
618
|
+
this.exitCode = this._getReg(1);
|
|
619
|
+
this.halted = true;
|
|
620
|
+
break;
|
|
621
|
+
default:
|
|
622
|
+
this._setReg(0, -1); // ENOSYS
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Read a null-terminated string from memory at addr */
|
|
627
|
+
readString(addr) {
|
|
628
|
+
let s = "";
|
|
629
|
+
let i = addr;
|
|
630
|
+
while (i < HEAP_SIZE && this.mem[i] !== 0) s += String.fromCharCode(this.mem[i++]);
|
|
631
|
+
return s;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ===========================================================================
|
|
636
|
+
// Shell (sh interpreter)
|
|
637
|
+
// ===========================================================================
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Sh — a minimal but real sh interpreter
|
|
641
|
+
* Supports: variables, ENV, export, PATH lookup, pipelines (|), redirections
|
|
642
|
+
* (>, >>, <), background (&), &&, ||, ;, subshells $(), quoting, globs,
|
|
643
|
+
* built-ins, #! scripts, and TeaBin executables.
|
|
644
|
+
*/
|
|
645
|
+
|
|
646
|
+
class Sh {
|
|
647
|
+
constructor(fs, opts = {}) {
|
|
648
|
+
this._fs = fs;
|
|
649
|
+
this.env = Object.assign({
|
|
650
|
+
PATH: "/bin:/usr/bin:/usr/local/bin",
|
|
651
|
+
HOME: "/root",
|
|
652
|
+
USER: "root",
|
|
653
|
+
SHELL: "/bin/sh",
|
|
654
|
+
PWD: "/",
|
|
655
|
+
PS1: "$ ",
|
|
656
|
+
IFS: " \t\n",
|
|
657
|
+
}, opts.env || {});
|
|
658
|
+
this.aliases = {};
|
|
659
|
+
this._exitCode = 0;
|
|
660
|
+
this._uid = opts.uid !== undefined ? opts.uid : 0;
|
|
661
|
+
this._gid = opts.gid !== undefined ? opts.gid : 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// -- Public API -----------------------------------------------------------
|
|
665
|
+
|
|
666
|
+
/** Execute a shell script string. Returns { stdout, stderr, code }. */
|
|
667
|
+
exec(script, opts = {}) {
|
|
668
|
+
const ctx = {
|
|
669
|
+
stdin: opts.stdin || [],
|
|
670
|
+
stdout: opts.stdout || [],
|
|
671
|
+
stderr: opts.stderr || [],
|
|
672
|
+
vars: Object.assign({}, this.env),
|
|
673
|
+
};
|
|
674
|
+
try {
|
|
675
|
+
const tokens = this._lex(script);
|
|
676
|
+
const ast = this._parse(tokens);
|
|
677
|
+
this._execList(ast, ctx);
|
|
678
|
+
} catch(e) {
|
|
679
|
+
if (e && e._exit) { this._exitCode = e.code; return { stdout: ctx.stdout.join(""), stderr: ctx.stderr.join(""), code: e.code }; }
|
|
680
|
+
ctx.stderr.push(e.message + "\n");
|
|
681
|
+
this._exitCode = 1;
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
stdout: ctx.stdout.join(""),
|
|
685
|
+
stderr: ctx.stderr.join(""),
|
|
686
|
+
code: this._exitCode,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// -- Lexer ----------------------------------------------------------------
|
|
691
|
+
|
|
692
|
+
_lex(src) {
|
|
693
|
+
const tokens = [];
|
|
694
|
+
let i = 0;
|
|
695
|
+
while (i < src.length) {
|
|
696
|
+
const ch = src[i];
|
|
697
|
+
// skip whitespace (but not newlines — they're separators)
|
|
698
|
+
if (ch === " " || ch === "\t") { i++; continue; }
|
|
699
|
+
if (ch === "\n" || ch === ";") { tokens.push({ type: "SEP" }); i++; continue; }
|
|
700
|
+
if (ch === "#") { while (i < src.length && src[i] !== "\n") i++; continue; }
|
|
701
|
+
if (ch === "&" && src[i+1] === "&") { tokens.push({ type: "AND" }); i+=2; continue; }
|
|
702
|
+
if (ch === "|" && src[i+1] === "|") { tokens.push({ type: "OR" }); i+=2; continue; }
|
|
703
|
+
if (ch === "|") { tokens.push({ type: "PIPE" }); i++; continue; }
|
|
704
|
+
if (ch === "&") { tokens.push({ type: "BG" }); i++; continue; }
|
|
705
|
+
if (ch === ">") {
|
|
706
|
+
if (src[i+1] === ">") { tokens.push({ type: "REDIR_APPEND" }); i+=2; }
|
|
707
|
+
else { tokens.push({ type: "REDIR_OUT" }); i++; }
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (ch === "<") { tokens.push({ type: "REDIR_IN" }); i++; continue; }
|
|
711
|
+
if (ch === "(") { tokens.push({ type: "LPAREN" }); i++; continue; }
|
|
712
|
+
if (ch === ")") { tokens.push({ type: "RPAREN" }); i++; continue; }
|
|
713
|
+
if (ch === "{") { tokens.push({ type: "LBRACE" }); i++; continue; }
|
|
714
|
+
if (ch === "}") { tokens.push({ type: "RBRACE" }); i++; continue; }
|
|
715
|
+
|
|
716
|
+
// String / word
|
|
717
|
+
let word = "";
|
|
718
|
+
while (i < src.length) {
|
|
719
|
+
const c = src[i];
|
|
720
|
+
if (" \t\n;|&<>(){}".includes(c)) break;
|
|
721
|
+
if (c === "\\") { i++; word += src[i] || ""; i++; continue; }
|
|
722
|
+
if (c === "'") {
|
|
723
|
+
i++;
|
|
724
|
+
while (i < src.length && src[i] !== "'") word += src[i++];
|
|
725
|
+
i++;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (c === '"') {
|
|
729
|
+
i++;
|
|
730
|
+
while (i < src.length && src[i] !== '"') {
|
|
731
|
+
if (src[i] === "\\" && '"$`\\'.includes(src[i+1])) { i++; word += src[i++]; }
|
|
732
|
+
else word += src[i++];
|
|
733
|
+
}
|
|
734
|
+
i++;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
word += c; i++;
|
|
738
|
+
}
|
|
739
|
+
if (word) tokens.push({ type: "WORD", value: word });
|
|
740
|
+
}
|
|
741
|
+
return tokens;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// -- Parser ---------------------------------------------------------------
|
|
745
|
+
// Produces a list of command nodes
|
|
746
|
+
|
|
747
|
+
_parse(tokens) {
|
|
748
|
+
// Flatten into a list of "statements" separated by SEP/AND/OR/PIPE/BG
|
|
749
|
+
return this._parseList(tokens, 0).nodes;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
_parseList(tokens, pos) {
|
|
753
|
+
const nodes = [];
|
|
754
|
+
while (pos < tokens.length) {
|
|
755
|
+
const { node, pos: newPos } = this._parseCmd(tokens, pos);
|
|
756
|
+
pos = newPos;
|
|
757
|
+
if (node) nodes.push(node);
|
|
758
|
+
if (pos >= tokens.length) break;
|
|
759
|
+
const sep = tokens[pos];
|
|
760
|
+
if (sep.type === "RPAREN" || sep.type === "RBRACE") break;
|
|
761
|
+
if (sep.type === "SEP") { nodes.push({ type: "SEP" }); pos++; continue; }
|
|
762
|
+
if (sep.type === "AND") { nodes.push({ type: "AND" }); pos++; continue; }
|
|
763
|
+
if (sep.type === "OR") { nodes.push({ type: "OR" }); pos++; continue; }
|
|
764
|
+
if (sep.type === "PIPE") { nodes.push({ type: "PIPE" }); pos++; continue; }
|
|
765
|
+
if (sep.type === "BG") { nodes.push({ type: "BG" }); pos++; continue; }
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
return { nodes, pos };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
_parseCmd(tokens, pos) {
|
|
772
|
+
const words = [];
|
|
773
|
+
const redirs = [];
|
|
774
|
+
while (pos < tokens.length) {
|
|
775
|
+
const tok = tokens[pos];
|
|
776
|
+
if (["SEP","AND","OR","PIPE","BG","RPAREN","RBRACE"].includes(tok.type)) break;
|
|
777
|
+
if (tok.type === "REDIR_OUT" || tok.type === "REDIR_APPEND" || tok.type === "REDIR_IN") {
|
|
778
|
+
const kind = tok.type;
|
|
779
|
+
pos++;
|
|
780
|
+
const file = tokens[pos];
|
|
781
|
+
if (!file || file.type !== "WORD") throw new Error("sh: expected filename after redirect");
|
|
782
|
+
redirs.push({ kind, file: file.value });
|
|
783
|
+
pos++;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
if (tok.type === "LPAREN") {
|
|
787
|
+
// subshell
|
|
788
|
+
pos++;
|
|
789
|
+
const { nodes, pos: endPos } = this._parseList(tokens, pos);
|
|
790
|
+
pos = endPos + 1; // skip RPAREN
|
|
791
|
+
return { node: { type: "SUBSHELL", body: nodes }, pos };
|
|
792
|
+
}
|
|
793
|
+
if (tok.type === "WORD") { words.push(tok.value); pos++; continue; }
|
|
794
|
+
pos++;
|
|
795
|
+
}
|
|
796
|
+
if (words.length === 0 && redirs.length === 0) return { node: null, pos };
|
|
797
|
+
return { node: { type: "CMD", words, redirs }, pos };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// -- Executor -------------------------------------------------------------
|
|
801
|
+
|
|
802
|
+
_execList(nodes, ctx) {
|
|
803
|
+
let i = 0;
|
|
804
|
+
while (i < nodes.length) {
|
|
805
|
+
const node = nodes[i];
|
|
806
|
+
if (!node || node.type === "SEP") { i++; continue; }
|
|
807
|
+
|
|
808
|
+
// Collect a pipeline
|
|
809
|
+
const pipeline = [node];
|
|
810
|
+
i++;
|
|
811
|
+
while (i < nodes.length && nodes[i] && nodes[i].type === "PIPE") {
|
|
812
|
+
i++; // skip PIPE
|
|
813
|
+
if (i < nodes.length && nodes[i] && nodes[i].type !== "SEP")
|
|
814
|
+
pipeline.push(nodes[i++]);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
this._execPipeline(pipeline, ctx);
|
|
818
|
+
|
|
819
|
+
// Check && / ||
|
|
820
|
+
if (i < nodes.length && nodes[i]) {
|
|
821
|
+
if (nodes[i].type === "AND") { i++; if (this._exitCode !== 0) { while (i < nodes.length && nodes[i].type !== "SEP") i++; } continue; }
|
|
822
|
+
if (nodes[i].type === "OR") { i++; if (this._exitCode === 0) { while (i < nodes.length && nodes[i].type !== "SEP") i++; } continue; }
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
_execPipeline(pipeline, ctx) {
|
|
828
|
+
if (pipeline.length === 1) {
|
|
829
|
+
this._execNode(pipeline[0], ctx);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
// Build piped buffers
|
|
833
|
+
let prevOut = ctx.stdin;
|
|
834
|
+
for (let i = 0; i < pipeline.length; i++) {
|
|
835
|
+
const isLast = (i === pipeline.length - 1);
|
|
836
|
+
const pipeOut = isLast ? ctx.stdout : [];
|
|
837
|
+
const pipeCtx = {
|
|
838
|
+
stdin: prevOut,
|
|
839
|
+
stdout: pipeOut,
|
|
840
|
+
stderr: ctx.stderr,
|
|
841
|
+
vars: ctx.vars,
|
|
842
|
+
};
|
|
843
|
+
this._execNode(pipeline[i], pipeCtx);
|
|
844
|
+
prevOut = pipeOut;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
_execNode(node, ctx) {
|
|
849
|
+
if (!node) return;
|
|
850
|
+
if (node.type === "SEP") return;
|
|
851
|
+
if (node.type === "SUBSHELL") {
|
|
852
|
+
const subCtx = { stdin: [...ctx.stdin], stdout: ctx.stdout, stderr: ctx.stderr, vars: { ...ctx.vars } };
|
|
853
|
+
this._execList(node.body, subCtx);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (node.type === "CMD") {
|
|
857
|
+
// Expand vars and perform globbing
|
|
858
|
+
const words = node.words.map(w => this._expand(w, ctx));
|
|
859
|
+
const expanded = [];
|
|
860
|
+
for (const w of words) {
|
|
861
|
+
const globs = this._glob(w, ctx.vars.PWD || "/");
|
|
862
|
+
expanded.push(...globs);
|
|
863
|
+
}
|
|
864
|
+
if (expanded.length === 0) return;
|
|
865
|
+
|
|
866
|
+
// Handle redirections
|
|
867
|
+
let stdin = ctx.stdin;
|
|
868
|
+
let stdout = ctx.stdout;
|
|
869
|
+
for (const r of node.redirs) {
|
|
870
|
+
const file = this._expand(r.file, ctx);
|
|
871
|
+
if (r.kind === "REDIR_IN") {
|
|
872
|
+
try { stdin = [this._fs.readFile(file, "utf8")]; } catch(_) { stdin = []; }
|
|
873
|
+
} else if (r.kind === "REDIR_OUT") {
|
|
874
|
+
const buf = [];
|
|
875
|
+
stdout = buf;
|
|
876
|
+
// after exec, write buf to file
|
|
877
|
+
const flush = () => this._fs.writeFile(file, buf.join(""));
|
|
878
|
+
ctx._flushes = ctx._flushes || [];
|
|
879
|
+
ctx._flushes.push(flush);
|
|
880
|
+
} else if (r.kind === "REDIR_APPEND") {
|
|
881
|
+
const buf = [];
|
|
882
|
+
stdout = buf;
|
|
883
|
+
const flush = () => this._fs.appendFile(file, buf.join(""));
|
|
884
|
+
ctx._flushes = ctx._flushes || [];
|
|
885
|
+
ctx._flushes.push(flush);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const cmdCtx = { stdin, stdout, stderr: ctx.stderr, vars: ctx.vars };
|
|
890
|
+
this._runCommand(expanded[0], expanded.slice(1), cmdCtx);
|
|
891
|
+
|
|
892
|
+
// Flush redirections
|
|
893
|
+
for (const f of (ctx._flushes || [])) f();
|
|
894
|
+
ctx._flushes = [];
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
_expand(word, ctx) {
|
|
900
|
+
// $VAR, ${VAR}, $?, $$
|
|
901
|
+
return word.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*|\?|\$|\d+)/g, (_, a, b) => {
|
|
902
|
+
const name = a || b;
|
|
903
|
+
if (name === "?") return String(this._exitCode);
|
|
904
|
+
if (name === "$") return "$$";
|
|
905
|
+
return ctx.vars[name] !== undefined ? ctx.vars[name] : "";
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
_glob(pattern, cwd) {
|
|
910
|
+
if (!/[*?]/.test(pattern)) return [pattern];
|
|
911
|
+
try {
|
|
912
|
+
const hits = this._fs.glob(pattern, { cwd });
|
|
913
|
+
return hits.length ? hits : [pattern];
|
|
914
|
+
} catch(_) { return [pattern]; }
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
_runCommand(cmd, args, ctx) {
|
|
918
|
+
// Alias expansion
|
|
919
|
+
if (this.aliases[cmd]) {
|
|
920
|
+
const aliased = this._lex(this.aliases[cmd] + " " + args.join(" "));
|
|
921
|
+
const ast = this._parse(aliased);
|
|
922
|
+
this._execList(ast, ctx);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Variable assignment (FOO=bar)
|
|
927
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(cmd)) {
|
|
928
|
+
const eq = cmd.indexOf("=");
|
|
929
|
+
const key = cmd.slice(0, eq);
|
|
930
|
+
const val = this._expand(cmd.slice(eq + 1), ctx);
|
|
931
|
+
ctx.vars[key] = val;
|
|
932
|
+
this.env[key] = val;
|
|
933
|
+
this._exitCode = 0;
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Built-ins
|
|
938
|
+
if (this._builtin(cmd, args, ctx)) return;
|
|
939
|
+
|
|
940
|
+
// PATH lookup
|
|
941
|
+
const bin = this._findBin(cmd, ctx.vars.PATH || this.env.PATH);
|
|
942
|
+
if (!bin) {
|
|
943
|
+
ctx.stderr.push(`sh: ${cmd}: command not found\n`);
|
|
944
|
+
this._exitCode = 127;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
this._execFile(bin, [cmd, ...args], ctx);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
_findBin(name, PATH) {
|
|
952
|
+
if (name.startsWith("/") || name.startsWith("./") || name.startsWith("../")) {
|
|
953
|
+
return this._fs.exists(name) ? name : null;
|
|
954
|
+
}
|
|
955
|
+
for (const dir of (PATH || "").split(":")) {
|
|
956
|
+
const p = dir.endsWith("/") ? dir + name : dir + "/" + name;
|
|
957
|
+
if (this._fs.exists(p)) return p;
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
_execFile(path, argv, ctx) {
|
|
963
|
+
let data;
|
|
964
|
+
try {
|
|
965
|
+
data = this._fs.readFile(path, "utf8");
|
|
966
|
+
} catch(e) {
|
|
967
|
+
ctx.stderr.push(`sh: ${path}: ${e.message}\n`);
|
|
968
|
+
this._exitCode = 126;
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Check execute permission
|
|
973
|
+
try {
|
|
974
|
+
const st = this._fs.stat(path);
|
|
975
|
+
const canExec = (st.uid === this._uid && (st.mode & S_IXUSR)) ||
|
|
976
|
+
(st.gid === this._gid && (st.mode & S_IXGRP)) ||
|
|
977
|
+
(this._uid === 0) ||
|
|
978
|
+
(st.mode & S_IXOTH);
|
|
979
|
+
if (!canExec) { ctx.stderr.push(`sh: ${path}: permission denied\n`); this._exitCode = 126; return; }
|
|
980
|
+
} catch(_) {}
|
|
981
|
+
|
|
982
|
+
// She-bang
|
|
983
|
+
if (data.startsWith("#!")) {
|
|
984
|
+
const firstLine = data.split("\n")[0].slice(2).trim();
|
|
985
|
+
const interp = firstLine.split(/\s+/);
|
|
986
|
+
this._runCommand(interp[0], [...interp.slice(1), path, ...argv.slice(1)], ctx);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// TEAB binary magic (base64-encoded in the file data, or raw bytes check)
|
|
991
|
+
if (data.startsWith("TEAB") || (data.charCodeAt(0) === 0x54 && data.charCodeAt(1) === 0x45)) {
|
|
992
|
+
this._execTeaBin(data, argv, ctx);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Treat as shell script
|
|
997
|
+
const saved = { ...this.env };
|
|
998
|
+
ctx.vars["0"] = argv[0];
|
|
999
|
+
argv.slice(1).forEach((a, i) => { ctx.vars[String(i+1)] = a; });
|
|
1000
|
+
this.exec(data, { stdin: ctx.stdin, stdout: ctx.stdout, stderr: ctx.stderr });
|
|
1001
|
+
Object.assign(this.env, saved);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
_execTeaBin(data, argv, ctx) {
|
|
1005
|
+
try {
|
|
1006
|
+
// Encode file data as Uint8Array (assuming it's stored as a binary string)
|
|
1007
|
+
const bytes = new Uint8Array(data.length);
|
|
1008
|
+
for (let i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i) & 0xFF;
|
|
1009
|
+
const vm = new TeaBinVM({
|
|
1010
|
+
interrupts: {
|
|
1011
|
+
0x80: (vm) => {
|
|
1012
|
+
// Write syscall: R0=1 → stdout
|
|
1013
|
+
if (vm._getReg(0) === 1) {
|
|
1014
|
+
const ptr = vm._getReg(2);
|
|
1015
|
+
const len = vm._getReg(3);
|
|
1016
|
+
let s = "";
|
|
1017
|
+
for (let i = 0; i < len; i++) s += String.fromCharCode(vm.mem[(ptr+i)&0xFFFF]);
|
|
1018
|
+
ctx.stdout.push(s);
|
|
1019
|
+
vm._setReg(0, len);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
vm.load(bytes);
|
|
1025
|
+
this._exitCode = vm.run();
|
|
1026
|
+
for (const o of vm.stdout) ctx.stdout.push(typeof o === "number" ? String.fromCharCode(o) : String(o));
|
|
1027
|
+
} catch(e) {
|
|
1028
|
+
ctx.stderr.push(`${argv[0]}: ${e.message}\n`);
|
|
1029
|
+
this._exitCode = 1;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
_builtin(cmd, args, ctx) {
|
|
1034
|
+
switch (cmd) {
|
|
1035
|
+
case "echo": {
|
|
1036
|
+
let newline = true, i = 0;
|
|
1037
|
+
if (args[0] === "-n") { newline = false; i = 1; }
|
|
1038
|
+
ctx.stdout.push(args.slice(i).join(" ") + (newline ? "\n" : ""));
|
|
1039
|
+
this._exitCode = 0;
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
case "printf": {
|
|
1043
|
+
let fmt = args[0] || "";
|
|
1044
|
+
let ai = 1;
|
|
1045
|
+
const out = fmt.replace(/%[diouxXeEfgGsqcb%]/g, (m) => {
|
|
1046
|
+
if (m === "%%") return "%";
|
|
1047
|
+
const a = args[ai++] || "";
|
|
1048
|
+
if (m === "%d" || m === "%i") return String(parseInt(a,10)||0);
|
|
1049
|
+
if (m === "%s") return a;
|
|
1050
|
+
if (m === "%x") return (parseInt(a,10)||0).toString(16);
|
|
1051
|
+
if (m === "%o") return (parseInt(a,10)||0).toString(8);
|
|
1052
|
+
return a;
|
|
1053
|
+
}).replace(/\\n/g,"\n").replace(/\\t/g,"\t").replace(/\\r/g,"\r");
|
|
1054
|
+
ctx.stdout.push(out);
|
|
1055
|
+
this._exitCode = 0;
|
|
1056
|
+
return true;
|
|
1057
|
+
}
|
|
1058
|
+
case "cd": {
|
|
1059
|
+
const target = args[0] || ctx.vars.HOME || "/";
|
|
1060
|
+
const p = target.startsWith("/") ? target : (ctx.vars.PWD || "/") + "/" + target;
|
|
1061
|
+
const norm = normPath(p);
|
|
1062
|
+
if (!this._fs.exists(norm)) {
|
|
1063
|
+
ctx.stderr.push(`cd: ${target}: No such file or directory\n`);
|
|
1064
|
+
this._exitCode = 1;
|
|
1065
|
+
} else {
|
|
1066
|
+
ctx.vars.PWD = norm;
|
|
1067
|
+
this.env.PWD = norm;
|
|
1068
|
+
this._exitCode = 0;
|
|
1069
|
+
}
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
case "pwd": { ctx.stdout.push((ctx.vars.PWD || "/") + "\n"); this._exitCode = 0; return true; }
|
|
1073
|
+
case "export": {
|
|
1074
|
+
for (const a of args) {
|
|
1075
|
+
if (a.includes("=")) {
|
|
1076
|
+
const [k, ...v] = a.split("=");
|
|
1077
|
+
ctx.vars[k] = v.join("=");
|
|
1078
|
+
this.env[k] = ctx.vars[k];
|
|
1079
|
+
} else {
|
|
1080
|
+
this.env[a] = ctx.vars[a] || "";
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
this._exitCode = 0;
|
|
1084
|
+
return true;
|
|
1085
|
+
}
|
|
1086
|
+
case "unset": { for (const a of args) { delete ctx.vars[a]; delete this.env[a]; } this._exitCode = 0; return true; }
|
|
1087
|
+
case "env":
|
|
1088
|
+
case "printenv": {
|
|
1089
|
+
if (args.length) { ctx.stdout.push((ctx.vars[args[0]] || "") + "\n"); }
|
|
1090
|
+
else { for (const [k,v] of Object.entries(ctx.vars)) ctx.stdout.push(`${k}=${v}\n`); }
|
|
1091
|
+
this._exitCode = 0; return true;
|
|
1092
|
+
}
|
|
1093
|
+
case "set": {
|
|
1094
|
+
for (const [k,v] of Object.entries(ctx.vars)) ctx.stdout.push(`${k}=${v}\n`);
|
|
1095
|
+
this._exitCode = 0; return true;
|
|
1096
|
+
}
|
|
1097
|
+
case "alias": {
|
|
1098
|
+
if (args.length === 0) {
|
|
1099
|
+
for (const [k,v] of Object.entries(this.aliases)) ctx.stdout.push(`alias ${k}='${v}'\n`);
|
|
1100
|
+
} else {
|
|
1101
|
+
for (const a of args) {
|
|
1102
|
+
const eq = a.indexOf("=");
|
|
1103
|
+
if (eq < 0) { ctx.stdout.push(`alias ${a}='${this.aliases[a] || ""}'\n`); }
|
|
1104
|
+
else { this.aliases[a.slice(0, eq)] = a.slice(eq + 1).replace(/^'|'$/g, ""); }
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
this._exitCode = 0; return true;
|
|
1108
|
+
}
|
|
1109
|
+
case "unalias": { for (const a of args) delete this.aliases[a]; this._exitCode = 0; return true; }
|
|
1110
|
+
case "type":
|
|
1111
|
+
case "which": {
|
|
1112
|
+
for (const a of args) {
|
|
1113
|
+
if (this.aliases[a]) { ctx.stdout.push(`${a} is aliased to '${this.aliases[a]}'\n`); continue; }
|
|
1114
|
+
if (["echo","printf","cd","pwd","export","unset","env","alias","type","which","true","false","exit","read","test","[","source",".","sleep","kill"].includes(a)) {
|
|
1115
|
+
ctx.stdout.push(`${a} is a shell builtin\n`); continue;
|
|
1116
|
+
}
|
|
1117
|
+
const bin = this._findBin(a, ctx.vars.PATH);
|
|
1118
|
+
if (bin) ctx.stdout.push(bin + "\n");
|
|
1119
|
+
else ctx.stderr.push(`${a}: not found\n`);
|
|
1120
|
+
}
|
|
1121
|
+
this._exitCode = 0; return true;
|
|
1122
|
+
}
|
|
1123
|
+
case "true": { this._exitCode = 0; return true; }
|
|
1124
|
+
case "false": { this._exitCode = 1; return true; }
|
|
1125
|
+
case "exit": { throw { _exit: true, code: parseInt(args[0], 10) || 0 }; }
|
|
1126
|
+
case "source":
|
|
1127
|
+
case ".": {
|
|
1128
|
+
const path = args[0];
|
|
1129
|
+
if (!path) { ctx.stderr.push("source: filename argument required\n"); this._exitCode = 1; return true; }
|
|
1130
|
+
try {
|
|
1131
|
+
const script = this._fs.readFile(path, "utf8");
|
|
1132
|
+
const r = this.exec(script, { stdin: ctx.stdin, stdout: ctx.stdout, stderr: ctx.stderr });
|
|
1133
|
+
this._exitCode = r.code;
|
|
1134
|
+
} catch(e) { ctx.stderr.push(`source: ${path}: ${e.message}\n`); this._exitCode = 1; }
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
case "read": {
|
|
1138
|
+
const varName = args[0] || "REPLY";
|
|
1139
|
+
const line = (ctx.stdin.shift() || "").replace(/\n$/, "");
|
|
1140
|
+
ctx.vars[varName] = line;
|
|
1141
|
+
this._exitCode = 0; return true;
|
|
1142
|
+
}
|
|
1143
|
+
case "test":
|
|
1144
|
+
case "[": {
|
|
1145
|
+
// Basic test expressions
|
|
1146
|
+
const a = args.slice(0, args[args.length-1] === "]" ? -1 : undefined);
|
|
1147
|
+
this._exitCode = this._test(a, ctx) ? 0 : 1;
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
case "sleep": {
|
|
1151
|
+
// no-op in virtual fs
|
|
1152
|
+
this._exitCode = 0; return true;
|
|
1153
|
+
}
|
|
1154
|
+
case "cat": {
|
|
1155
|
+
if (args.length === 0) {
|
|
1156
|
+
ctx.stdout.push(...ctx.stdin); this._exitCode = 0; return true;
|
|
1157
|
+
}
|
|
1158
|
+
for (const f of args) {
|
|
1159
|
+
try { ctx.stdout.push(this._fs.readFile(f, "utf8")); }
|
|
1160
|
+
catch(e) { ctx.stderr.push(`cat: ${f}: ${e.message}\n`); this._exitCode = 1; return true; }
|
|
1161
|
+
}
|
|
1162
|
+
this._exitCode = 0; return true;
|
|
1163
|
+
}
|
|
1164
|
+
case "ls": {
|
|
1165
|
+
const dir = args.find(a => !a.startsWith("-")) || ctx.vars.PWD || "/";
|
|
1166
|
+
const showHidden = args.includes("-a") || args.includes("-la") || args.includes("-al");
|
|
1167
|
+
const long = args.includes("-l") || args.includes("-la") || args.includes("-al");
|
|
1168
|
+
try {
|
|
1169
|
+
const entries = this._fs.readdir(dir, { withFileTypes: true });
|
|
1170
|
+
for (const e of entries) {
|
|
1171
|
+
if (!showHidden && e.name.startsWith(".")) continue;
|
|
1172
|
+
if (long) {
|
|
1173
|
+
const st = this._fs.stat(dir === "/" ? "/" + e.name : dir + "/" + e.name);
|
|
1174
|
+
const type = e.isDirectory() ? "d" : e.isSymbolicLink() ? "l" : "-";
|
|
1175
|
+
const mode = this._formatMode(st.mode);
|
|
1176
|
+
ctx.stdout.push(`${type}${mode} ${st.nlink} ${st.uid} ${st.gid} ${String(st.size).padStart(6)} ${e.name}\n`);
|
|
1177
|
+
} else {
|
|
1178
|
+
ctx.stdout.push(e.name + " ");
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
if (!long) ctx.stdout.push("\n");
|
|
1182
|
+
this._exitCode = 0;
|
|
1183
|
+
} catch(e) { ctx.stderr.push(`ls: ${dir}: ${e.message}\n`); this._exitCode = 1; }
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
case "mkdir": {
|
|
1187
|
+
const recursive = args.includes("-p");
|
|
1188
|
+
const dirs = args.filter(a => !a.startsWith("-"));
|
|
1189
|
+
for (const d of dirs) {
|
|
1190
|
+
try { this._fs.mkdir(d, { recursive }); }
|
|
1191
|
+
catch(e) { ctx.stderr.push(`mkdir: ${d}: ${e.message}\n`); this._exitCode = 1; return true; }
|
|
1192
|
+
}
|
|
1193
|
+
this._exitCode = 0; return true;
|
|
1194
|
+
}
|
|
1195
|
+
case "rm": {
|
|
1196
|
+
const recursive = args.includes("-r") || args.includes("-rf") || args.includes("-fr");
|
|
1197
|
+
const force = args.includes("-f") || args.includes("-rf") || args.includes("-fr");
|
|
1198
|
+
const files = args.filter(a => !a.startsWith("-"));
|
|
1199
|
+
for (const f of files) {
|
|
1200
|
+
try { this._fs.rm(f, { recursive, force }); }
|
|
1201
|
+
catch(e) { if (!force) { ctx.stderr.push(`rm: ${f}: ${e.message}\n`); this._exitCode = 1; return true; } }
|
|
1202
|
+
}
|
|
1203
|
+
this._exitCode = 0; return true;
|
|
1204
|
+
}
|
|
1205
|
+
case "cp": {
|
|
1206
|
+
const recursive = args.includes("-r") || args.includes("-R");
|
|
1207
|
+
const paths = args.filter(a => !a.startsWith("-"));
|
|
1208
|
+
if (paths.length < 2) { ctx.stderr.push("cp: missing file operand\n"); this._exitCode = 1; return true; }
|
|
1209
|
+
try { this._fs.cp(paths[0], paths[1], { recursive }); this._exitCode = 0; }
|
|
1210
|
+
catch(e) { ctx.stderr.push(`cp: ${e.message}\n`); this._exitCode = 1; }
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
case "mv": {
|
|
1214
|
+
if (args.length < 2) { ctx.stderr.push("mv: missing file operand\n"); this._exitCode = 1; return true; }
|
|
1215
|
+
try { this._fs.rename(args[0], args[1]); this._exitCode = 0; }
|
|
1216
|
+
catch(e) { ctx.stderr.push(`mv: ${e.message}\n`); this._exitCode = 1; }
|
|
1217
|
+
return true;
|
|
1218
|
+
}
|
|
1219
|
+
case "chmod": {
|
|
1220
|
+
if (args.length < 2) { ctx.stderr.push("chmod: missing operand\n"); this._exitCode = 1; return true; }
|
|
1221
|
+
const mode = parseInt(args[0], 8);
|
|
1222
|
+
try { this._fs.chmod(args[1], mode); this._exitCode = 0; }
|
|
1223
|
+
catch(e) { ctx.stderr.push(`chmod: ${e.message}\n`); this._exitCode = 1; }
|
|
1224
|
+
return true;
|
|
1225
|
+
}
|
|
1226
|
+
case "touch": {
|
|
1227
|
+
for (const f of args) {
|
|
1228
|
+
try {
|
|
1229
|
+
if (!this._fs.exists(f)) this._fs.writeFile(f, "");
|
|
1230
|
+
else this._fs.utimes(f, new Date(), new Date());
|
|
1231
|
+
} catch(e) { ctx.stderr.push(`touch: ${f}: ${e.message}\n`); this._exitCode = 1; return true; }
|
|
1232
|
+
}
|
|
1233
|
+
this._exitCode = 0; return true;
|
|
1234
|
+
}
|
|
1235
|
+
case "grep": {
|
|
1236
|
+
const pattern = args[0];
|
|
1237
|
+
const files = args.slice(1);
|
|
1238
|
+
if (!pattern) { ctx.stderr.push("grep: usage: grep PATTERN [FILE...]\n"); this._exitCode = 1; return true; }
|
|
1239
|
+
const re = new RegExp(pattern);
|
|
1240
|
+
let found = false;
|
|
1241
|
+
const search = (text, label) => {
|
|
1242
|
+
for (const line of text.split("\n")) {
|
|
1243
|
+
if (re.test(line)) { ctx.stdout.push((files.length > 1 ? label + ":" : "") + line + "\n"); found = true; }
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
if (files.length === 0) { search(ctx.stdin.join(""), "stdin"); }
|
|
1247
|
+
else {
|
|
1248
|
+
for (const f of files) {
|
|
1249
|
+
try { search(this._fs.readFile(f, "utf8"), f); }
|
|
1250
|
+
catch(e) { ctx.stderr.push(`grep: ${f}: ${e.message}\n`); }
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
this._exitCode = found ? 0 : 1; return true;
|
|
1254
|
+
}
|
|
1255
|
+
case "find": {
|
|
1256
|
+
const path = args[0] || ctx.vars.PWD || "/";
|
|
1257
|
+
const opts = {};
|
|
1258
|
+
for (let i = 1; i < args.length; i++) {
|
|
1259
|
+
if (args[i] === "-type" && args[i+1]) { opts.type = args[++i]; }
|
|
1260
|
+
if (args[i] === "-name" && args[i+1]) { opts.name = args[++i]; }
|
|
1261
|
+
}
|
|
1262
|
+
try {
|
|
1263
|
+
const results = this._fs.find(path, opts);
|
|
1264
|
+
for (const r of results) ctx.stdout.push(r + "\n");
|
|
1265
|
+
this._exitCode = 0;
|
|
1266
|
+
} catch(e) { ctx.stderr.push(`find: ${e.message}\n`); this._exitCode = 1; }
|
|
1267
|
+
return true;
|
|
1268
|
+
}
|
|
1269
|
+
case "wc": {
|
|
1270
|
+
const text = args.length > 0 ? this._fs.readFile(args[0], "utf8") : ctx.stdin.join("");
|
|
1271
|
+
const lines = (text.match(/\n/g) || []).length;
|
|
1272
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
1273
|
+
const chars = text.length;
|
|
1274
|
+
ctx.stdout.push(` ${lines} ${words} ${chars}\n`);
|
|
1275
|
+
this._exitCode = 0; return true;
|
|
1276
|
+
}
|
|
1277
|
+
case "head": {
|
|
1278
|
+
const n = args.includes("-n") ? parseInt(args[args.indexOf("-n")+1]) : 10;
|
|
1279
|
+
const src = args.find(a => !a.startsWith("-")) || null;
|
|
1280
|
+
const text = src ? this._fs.readFile(src, "utf8") : ctx.stdin.join("");
|
|
1281
|
+
ctx.stdout.push(text.split("\n").slice(0, n).join("\n") + "\n");
|
|
1282
|
+
this._exitCode = 0; return true;
|
|
1283
|
+
}
|
|
1284
|
+
case "tail": {
|
|
1285
|
+
const n = args.includes("-n") ? parseInt(args[args.indexOf("-n")+1]) : 10;
|
|
1286
|
+
const src = args.find(a => !a.startsWith("-")) || null;
|
|
1287
|
+
const text = src ? this._fs.readFile(src, "utf8") : ctx.stdin.join("");
|
|
1288
|
+
const lines = text.split("\n");
|
|
1289
|
+
ctx.stdout.push(lines.slice(-n).join("\n") + "\n");
|
|
1290
|
+
this._exitCode = 0; return true;
|
|
1291
|
+
}
|
|
1292
|
+
case "sort": {
|
|
1293
|
+
const text = ctx.stdin.join("");
|
|
1294
|
+
const sorted = text.split("\n").filter(Boolean).sort().join("\n") + "\n";
|
|
1295
|
+
ctx.stdout.push(sorted); this._exitCode = 0; return true;
|
|
1296
|
+
}
|
|
1297
|
+
case "uniq": {
|
|
1298
|
+
const text = ctx.stdin.join("");
|
|
1299
|
+
const lines = text.split("\n");
|
|
1300
|
+
const out = [lines[0]];
|
|
1301
|
+
for (let i = 1; i < lines.length; i++) if (lines[i] !== lines[i-1]) out.push(lines[i]);
|
|
1302
|
+
ctx.stdout.push(out.join("\n") + "\n"); this._exitCode = 0; return true;
|
|
1303
|
+
}
|
|
1304
|
+
case "tr": {
|
|
1305
|
+
const [set1, set2] = args;
|
|
1306
|
+
let text = ctx.stdin.join("");
|
|
1307
|
+
if (set1 && set2) {
|
|
1308
|
+
for (let i = 0; i < Math.min(set1.length, set2.length); i++)
|
|
1309
|
+
text = text.split(set1[i]).join(set2[i]);
|
|
1310
|
+
}
|
|
1311
|
+
ctx.stdout.push(text); this._exitCode = 0; return true;
|
|
1312
|
+
}
|
|
1313
|
+
case "sed": {
|
|
1314
|
+
// Basic s/pat/rep/g support
|
|
1315
|
+
const expr = args[0] || "";
|
|
1316
|
+
const src = args[1] || null;
|
|
1317
|
+
let text = src ? this._fs.readFile(src, "utf8") : ctx.stdin.join("");
|
|
1318
|
+
const m = expr.match(/^s\/(.+)\/(.*)\/([gim]*)$/);
|
|
1319
|
+
if (m) {
|
|
1320
|
+
const re = new RegExp(m[1], m[3] || "");
|
|
1321
|
+
text = text.replace(re, m[2]);
|
|
1322
|
+
}
|
|
1323
|
+
ctx.stdout.push(text); this._exitCode = 0; return true;
|
|
1324
|
+
}
|
|
1325
|
+
case "awk": {
|
|
1326
|
+
// Very minimal: print $N support
|
|
1327
|
+
const prog = args[0] || "{ print }";
|
|
1328
|
+
const text = ctx.stdin.join("");
|
|
1329
|
+
for (const line of text.split("\n").filter(Boolean)) {
|
|
1330
|
+
const fields = line.split(/\s+/);
|
|
1331
|
+
const out = prog
|
|
1332
|
+
.replace(/\$(\d+)/g, (_, n) => fields[parseInt(n)-1] || "")
|
|
1333
|
+
.replace(/\$0/g, line)
|
|
1334
|
+
.replace(/print\s*/g, "");
|
|
1335
|
+
ctx.stdout.push(out.trim() + "\n");
|
|
1336
|
+
}
|
|
1337
|
+
this._exitCode = 0; return true;
|
|
1338
|
+
}
|
|
1339
|
+
default: return false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
_test(args, ctx) {
|
|
1344
|
+
if (args.length === 0) return false;
|
|
1345
|
+
if (args[0] === "!" ) return !this._test(args.slice(1), ctx);
|
|
1346
|
+
if (args[1] === "-a") return this._test(args.slice(0,1), ctx) && this._test(args.slice(2), ctx);
|
|
1347
|
+
if (args[1] === "-o") return this._test(args.slice(0,1), ctx) || this._test(args.slice(2), ctx);
|
|
1348
|
+
if (args[0] === "-f") return this._fs.exists(args[1]) && this._fs.stat(args[1]).isFile();
|
|
1349
|
+
if (args[0] === "-d") return this._fs.exists(args[1]) && this._fs.stat(args[1]).isDirectory();
|
|
1350
|
+
if (args[0] === "-e") return this._fs.exists(args[1]);
|
|
1351
|
+
if (args[0] === "-n") return (args[1] || "").length > 0;
|
|
1352
|
+
if (args[0] === "-z") return (args[1] || "").length === 0;
|
|
1353
|
+
if (args[1] === "=") return args[0] === args[2];
|
|
1354
|
+
if (args[1] === "!=") return args[0] !== args[2];
|
|
1355
|
+
if (args[1] === "-eq") return parseInt(args[0]) === parseInt(args[2]);
|
|
1356
|
+
if (args[1] === "-ne") return parseInt(args[0]) !== parseInt(args[2]);
|
|
1357
|
+
if (args[1] === "-lt") return parseInt(args[0]) < parseInt(args[2]);
|
|
1358
|
+
if (args[1] === "-le") return parseInt(args[0]) <= parseInt(args[2]);
|
|
1359
|
+
if (args[1] === "-gt") return parseInt(args[0]) > parseInt(args[2]);
|
|
1360
|
+
if (args[1] === "-ge") return parseInt(args[0]) >= parseInt(args[2]);
|
|
1361
|
+
return args[0].length > 0;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
_formatMode(mode) {
|
|
1365
|
+
const bits = "rwxrwxrwx";
|
|
1366
|
+
return bits.split("").map((b, i) => (mode & (1 << (8-i))) ? b : "-").join("");
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// ===========================================================================
|
|
1371
|
+
// REPL
|
|
1372
|
+
// ===========================================================================
|
|
1373
|
+
|
|
1374
|
+
class ShRepl extends EventEmitter {
|
|
1375
|
+
constructor(sh) {
|
|
1376
|
+
super();
|
|
1377
|
+
this._sh = sh;
|
|
1378
|
+
this._history = [];
|
|
1379
|
+
this._histIdx = -1;
|
|
1380
|
+
this.running = true;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/** Feed a line of input, returns { stdout, stderr, code } */
|
|
1384
|
+
line(input) {
|
|
1385
|
+
if (!this.running) return { stdout: "", stderr: "Session closed\n", code: 1 };
|
|
1386
|
+
this._history.push(input);
|
|
1387
|
+
this._histIdx = this._history.length;
|
|
1388
|
+
const result = this._sh.exec(input);
|
|
1389
|
+
this.emit("result", result);
|
|
1390
|
+
return result;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
historyUp() { if (this._histIdx > 0) this._histIdx--; return this._history[this._histIdx] || ""; }
|
|
1394
|
+
historyDown() { if (this._histIdx < this._history.length) this._histIdx++; return this._history[this._histIdx] || ""; }
|
|
1395
|
+
close() { this.running = false; this.emit("close"); }
|
|
1396
|
+
prompt() { return this._sh.env.PS1 || "$ "; }
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// ===========================================================================
|
|
1400
|
+
// Main class: TeaF
|
|
1401
|
+
// ===========================================================================
|
|
1402
|
+
|
|
1403
|
+
class TeaF extends EventEmitter {
|
|
1404
|
+
/**
|
|
1405
|
+
* @param {Object} opts
|
|
1406
|
+
* @param {string} opts.label
|
|
1407
|
+
* @param {number} opts.umask
|
|
1408
|
+
* @param {string} opts.mode - 'posix' (default) | 'win' | 'url'
|
|
1409
|
+
* @param {number} opts.uid - current uid for permission checks (0 = root)
|
|
1410
|
+
* @param {number} opts.gid
|
|
1411
|
+
* @param {boolean} opts.enforcePermissions
|
|
1412
|
+
*/
|
|
1413
|
+
constructor(opts = {}) {
|
|
1414
|
+
super();
|
|
1415
|
+
this._db = null;
|
|
1416
|
+
this._fds = new Map();
|
|
1417
|
+
this._fdCounter = 3;
|
|
1418
|
+
this._watchers = new Map();
|
|
1419
|
+
this._mounts = new Map();
|
|
1420
|
+
this._umask = opts.umask !== undefined ? opts.umask : 0o022;
|
|
1421
|
+
this._uid = opts.uid !== undefined ? opts.uid : 0;
|
|
1422
|
+
this._gid = opts.gid !== undefined ? opts.gid : 0;
|
|
1423
|
+
this._enforcePerms = opts.enforcePermissions !== false; // default true
|
|
1424
|
+
this._pathAdapter = new PathAdapter(opts.mode || "posix");
|
|
1425
|
+
this._sh = null; // lazy-init shell
|
|
1426
|
+
this._init(opts.label || "tea/f");
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// -------------------------------------------------------------------------
|
|
1430
|
+
// Internal bootstrap
|
|
1431
|
+
// -------------------------------------------------------------------------
|
|
1432
|
+
|
|
1433
|
+
_init(label) {
|
|
1434
|
+
const t = now();
|
|
1435
|
+
this._db = {
|
|
1436
|
+
meta: { created: t, label, version: VERSION },
|
|
1437
|
+
tree: {
|
|
1438
|
+
"/": { type: "dir", mode: 0o755, uid: 0, gid: 0, atime: t, mtime: t, ctime: t, nlink: 2, children: {} },
|
|
1439
|
+
},
|
|
1440
|
+
inodeCounter: 1,
|
|
1441
|
+
};
|
|
1442
|
+
// Create standard directories
|
|
1443
|
+
for (const d of ["bin", "etc", "usr", "usr/bin", "usr/local", "usr/local/bin", "home", "root", "tmp", "var", "lib", "mnt", "proc", "dev"]) {
|
|
1444
|
+
this._mkdirOne("/" + d, 0o755, ...this._parentAndBase("/" + d));
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
_parentAndBase(p) {
|
|
1449
|
+
const { dir, base } = splitPath(p);
|
|
1450
|
+
const parentId = this._resolve(dir);
|
|
1451
|
+
return [parentId, base];
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
_newId() { this._db.inodeCounter++; return `node_${this._db.inodeCounter}`; }
|
|
1455
|
+
|
|
1456
|
+
// -------------------------------------------------------------------------
|
|
1457
|
+
// Path translation (mode-aware)
|
|
1458
|
+
// -------------------------------------------------------------------------
|
|
1459
|
+
|
|
1460
|
+
_p(path) {
|
|
1461
|
+
const internal = this._pathAdapter.toInternal(path);
|
|
1462
|
+
return normPath(internal);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
_ext(path) { return this._pathAdapter.toExternal(path); }
|
|
1466
|
+
|
|
1467
|
+
// -------------------------------------------------------------------------
|
|
1468
|
+
// Permission checking
|
|
1469
|
+
// -------------------------------------------------------------------------
|
|
1470
|
+
|
|
1471
|
+
_checkPerm(node, need) {
|
|
1472
|
+
if (!this._enforcePerms || this._uid === 0) return; // root bypasses
|
|
1473
|
+
const mode = node.mode & 0o777;
|
|
1474
|
+
let allowed = mode & 0o007; // other
|
|
1475
|
+
if (node.gid === this._gid) allowed |= (mode >> 3) & 0o007;
|
|
1476
|
+
if (node.uid === this._uid) allowed |= (mode >> 6) & 0o007;
|
|
1477
|
+
// need: 4=read, 2=write, 1=exec
|
|
1478
|
+
if ((allowed & need) !== need) throw err("EACCES");
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
_checkRead(node) { this._checkPerm(node, 4); }
|
|
1482
|
+
_checkWrite(node) { this._checkPerm(node, 2); }
|
|
1483
|
+
_checkExec(node) { this._checkPerm(node, 1); }
|
|
1484
|
+
|
|
1485
|
+
// -------------------------------------------------------------------------
|
|
1486
|
+
// Node resolution
|
|
1487
|
+
// -------------------------------------------------------------------------
|
|
1488
|
+
|
|
1489
|
+
_resolve(path, { followSymlinks = true, maxLinks = 40 } = {}) {
|
|
1490
|
+
const p = normPath(typeof path === "string" ? path : "/");
|
|
1491
|
+
if (p === "/") return "/";
|
|
1492
|
+
const parts = p.split("/").filter(Boolean);
|
|
1493
|
+
let cur = "/";
|
|
1494
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1495
|
+
// Check for mount point at current position
|
|
1496
|
+
const curPath = "/" + parts.slice(0, i).join("/") || "/";
|
|
1497
|
+
if (this._mounts.has(curPath)) {
|
|
1498
|
+
// Delegate to the mounted fs
|
|
1499
|
+
const mounted = this._mounts.get(curPath);
|
|
1500
|
+
const subPath = "/" + parts.slice(i).join("/");
|
|
1501
|
+
return "__mount__:" + curPath + ":" + subPath;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
const node = this._db.tree[cur];
|
|
1505
|
+
if (!node) throw err("ENOENT", path);
|
|
1506
|
+
if (node.type !== "dir") throw err("ENOTDIR", path);
|
|
1507
|
+
this._checkExec(node);
|
|
1508
|
+
const childId = node.children[parts[i]];
|
|
1509
|
+
if (!childId) throw err("ENOENT", path);
|
|
1510
|
+
const childNode = this._db.tree[childId];
|
|
1511
|
+
if (!childNode) throw err("ENOENT", path);
|
|
1512
|
+
if (childNode.type === "symlink" && (followSymlinks || i < parts.length - 1)) {
|
|
1513
|
+
if (maxLinks <= 0) throw err("ELOOP", path);
|
|
1514
|
+
const target = normPath(childNode.link.startsWith("/")
|
|
1515
|
+
? childNode.link
|
|
1516
|
+
: "/" + parts.slice(0, i).join("/") + "/" + childNode.link);
|
|
1517
|
+
cur = this._resolve(target, { followSymlinks: true, maxLinks: maxLinks - 1 });
|
|
1518
|
+
} else {
|
|
1519
|
+
cur = childId;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return cur;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
_isMountRef(id) { return typeof id === "string" && id.startsWith("__mount__:"); }
|
|
1526
|
+
|
|
1527
|
+
_mountedOp(id, method, args) {
|
|
1528
|
+
const [, mountPath, subPath] = id.split(":");
|
|
1529
|
+
const mountedFs = this._mounts.get(mountPath);
|
|
1530
|
+
if (!mountedFs) throw err("ENOENT");
|
|
1531
|
+
return mountedFs[method](subPath, ...args);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
_node(nodeId) { return this._db.tree[nodeId]; }
|
|
1535
|
+
|
|
1536
|
+
_getNode(path, opts) {
|
|
1537
|
+
const normP = this._p(path);
|
|
1538
|
+
const id = this._resolve(normP, opts);
|
|
1539
|
+
if (this._isMountRef(id)) {
|
|
1540
|
+
const [, , subPath] = id.split(":");
|
|
1541
|
+
const [, mountPath] = id.split(":");
|
|
1542
|
+
const mountedFs = this._mounts.get(mountPath);
|
|
1543
|
+
return { id, node: { type: "mount", _fs: mountedFs, _sub: subPath } };
|
|
1544
|
+
}
|
|
1545
|
+
const node = this._db.tree[id];
|
|
1546
|
+
if (!node) throw err("ENOENT", path);
|
|
1547
|
+
return { id, node };
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
_touch(nodeId, fields = ["atime", "mtime", "ctime"]) {
|
|
1551
|
+
if (this._isMountRef(nodeId)) return;
|
|
1552
|
+
const n = now();
|
|
1553
|
+
const node = this._db.tree[nodeId];
|
|
1554
|
+
if (!node) return;
|
|
1555
|
+
for (const f of fields) node[f] = n;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// -------------------------------------------------------------------------
|
|
1559
|
+
// Watcher notification (real events)
|
|
1560
|
+
// -------------------------------------------------------------------------
|
|
1561
|
+
|
|
1562
|
+
_notify(nodeId, eventType, filePath) {
|
|
1563
|
+
for (const [, w] of this._watchers) {
|
|
1564
|
+
// Check if this nodeId matches watcher's watched node or is a descendant
|
|
1565
|
+
const matches = (w.id === nodeId) || this._isDescendantById(nodeId, w.id);
|
|
1566
|
+
if (matches) {
|
|
1567
|
+
try { w.emitter.emit(eventType, eventType, filePath || w.path); } catch(_) {}
|
|
1568
|
+
if (w.listener) try { w.listener(eventType, filePath || w.path); } catch(_) {}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
// Also fire top-level FS events
|
|
1572
|
+
this.emit(eventType, filePath || nodeId);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
_isDescendantById(childId, ancestorId) {
|
|
1576
|
+
if (childId === ancestorId) return false;
|
|
1577
|
+
// Walk all dirs to see if ancestorId is a parent
|
|
1578
|
+
const ancestorNode = this._db.tree[ancestorId];
|
|
1579
|
+
if (!ancestorNode || ancestorNode.type !== "dir") return false;
|
|
1580
|
+
for (const cid of Object.values(ancestorNode.children)) {
|
|
1581
|
+
if (cid === childId) return true;
|
|
1582
|
+
if (this._isDescendantById(childId, cid)) return true;
|
|
1583
|
+
}
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// -------------------------------------------------------------------------
|
|
1588
|
+
// Path utilities
|
|
1589
|
+
// -------------------------------------------------------------------------
|
|
1590
|
+
|
|
1591
|
+
join(...parts) { return this._ext(normPath(parts.map(p => this._p(p)).join("/"))); }
|
|
1592
|
+
resolve(...parts) { return this._ext(normPath(parts.map(p => this._p(p)).join("/"))); }
|
|
1593
|
+
dirname(path) { return this._ext(splitPath(this._p(path)).dir); }
|
|
1594
|
+
basename(path, ext) {
|
|
1595
|
+
let b = splitPath(this._p(path)).base || "/";
|
|
1596
|
+
if (ext && b.endsWith(ext)) b = b.slice(0, -ext.length);
|
|
1597
|
+
return b;
|
|
1598
|
+
}
|
|
1599
|
+
extname(path) { const b = this.basename(path); const dot = b.lastIndexOf("."); return dot > 0 ? b.slice(dot) : ""; }
|
|
1600
|
+
relative(from, to) {
|
|
1601
|
+
const f = this._p(from).split("/").filter(Boolean);
|
|
1602
|
+
const t = this._p(to).split("/").filter(Boolean);
|
|
1603
|
+
let i = 0;
|
|
1604
|
+
while (i < f.length && i < t.length && f[i] === t[i]) i++;
|
|
1605
|
+
return [...f.slice(i).map(() => ".."), ...t.slice(i)].join("/") || ".";
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// -------------------------------------------------------------------------
|
|
1609
|
+
// exists / access / stat
|
|
1610
|
+
// -------------------------------------------------------------------------
|
|
1611
|
+
|
|
1612
|
+
exists(path) { try { this._resolve(this._p(path)); return true; } catch(_) { return false; } }
|
|
1613
|
+
|
|
1614
|
+
access(path, mode = 0) {
|
|
1615
|
+
const { node } = this._getNode(path);
|
|
1616
|
+
if (mode !== 0) this._checkPerm(node, mode);
|
|
1617
|
+
return true;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
stat(path) {
|
|
1621
|
+
const p = this._p(path);
|
|
1622
|
+
const { id, node } = this._getNode(p, { followSymlinks: true });
|
|
1623
|
+
if (node.type === "mount") return node._fs.stat(node._sub);
|
|
1624
|
+
this._checkRead(node);
|
|
1625
|
+
this._touch(id, ["atime"]);
|
|
1626
|
+
return new Stats(node, id);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
lstat(path) {
|
|
1630
|
+
const p = this._p(path);
|
|
1631
|
+
const { id, node } = this._getNode(p, { followSymlinks: false });
|
|
1632
|
+
if (node.type === "mount") return node._fs.lstat(node._sub);
|
|
1633
|
+
return new Stats(node, id);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
fstat(fd) {
|
|
1637
|
+
const handle = this._fds.get(fd);
|
|
1638
|
+
if (!handle) throw err("EBADF");
|
|
1639
|
+
const node = this._db.tree[handle.nodeId];
|
|
1640
|
+
if (!node) throw err("EBADF");
|
|
1641
|
+
return new Stats(node, handle.nodeId);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// -------------------------------------------------------------------------
|
|
1645
|
+
// mkdir / rmdir / readdir
|
|
1646
|
+
// -------------------------------------------------------------------------
|
|
1647
|
+
|
|
1648
|
+
mkdir(path, opts = {}) {
|
|
1649
|
+
const mode = (typeof opts === "number" ? opts : (opts.mode || (0o777 & ~this._umask)));
|
|
1650
|
+
const recursive = opts.recursive || false;
|
|
1651
|
+
const p = this._p(path);
|
|
1652
|
+
|
|
1653
|
+
if (recursive) {
|
|
1654
|
+
const parts = p.split("/").filter(Boolean);
|
|
1655
|
+
let cur = "/";
|
|
1656
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1657
|
+
const parentNode = this._db.tree[cur];
|
|
1658
|
+
if (!parentNode.children[parts[i]]) {
|
|
1659
|
+
const partial = "/" + parts.slice(0, i + 1).join("/");
|
|
1660
|
+
const { dir: pdir, base: pbase } = splitPath(partial);
|
|
1661
|
+
const parentId = this._resolve(pdir);
|
|
1662
|
+
this._mkdirOne(partial, mode, parentId, pbase);
|
|
1663
|
+
}
|
|
1664
|
+
cur = this._db.tree[cur].children[parts[i]];
|
|
1665
|
+
}
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const { dir, base } = splitPath(p);
|
|
1670
|
+
if (!base) throw err("EEXIST", path);
|
|
1671
|
+
const { id: parentId, node: parentNode } = this._getNode(dir, { followSymlinks: true });
|
|
1672
|
+
if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
|
|
1673
|
+
this._checkWrite(parentNode);
|
|
1674
|
+
if (parentNode.children[base]) throw err("EEXIST", path);
|
|
1675
|
+
this._mkdirOne(p, mode, parentId, base);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
_mkdirOne(path, mode, parentId, base) {
|
|
1679
|
+
const t = now();
|
|
1680
|
+
const id = this._newId();
|
|
1681
|
+
this._db.tree[id] = { type: "dir", mode, uid: this._uid, gid: this._gid, atime: t, mtime: t, ctime: t, nlink: 2, children: {} };
|
|
1682
|
+
this._db.tree[parentId].children[base] = id;
|
|
1683
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1684
|
+
this._notify(id, "mkdir", path);
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
rmdir(path, opts = {}) {
|
|
1688
|
+
const p = this._p(path);
|
|
1689
|
+
const { id, node } = this._getNode(p, { followSymlinks: true });
|
|
1690
|
+
if (node.type !== "dir") throw err("ENOTDIR", path);
|
|
1691
|
+
if (opts.recursive) { this._rmRecursive(p); return; }
|
|
1692
|
+
if (Object.keys(node.children).length > 0) throw err("ENOTEMPTY", path);
|
|
1693
|
+
const { dir, base } = splitPath(p);
|
|
1694
|
+
const { id: parentId, node: parentNode } = this._getNode(dir);
|
|
1695
|
+
this._checkWrite(parentNode);
|
|
1696
|
+
delete this._db.tree[parentId].children[base];
|
|
1697
|
+
delete this._db.tree[id];
|
|
1698
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1699
|
+
this._notify(parentId, "rmdir", path);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
readdir(path, opts = {}) {
|
|
1703
|
+
const { id, node } = this._getNode(path, { followSymlinks: true });
|
|
1704
|
+
if (node.type === "mount") return node._fs.readdir(node._sub, opts);
|
|
1705
|
+
if (node.type !== "dir") throw err("ENOTDIR", path);
|
|
1706
|
+
this._checkRead(node);
|
|
1707
|
+
this._touch(id, ["atime"]);
|
|
1708
|
+
const names = Object.keys(node.children);
|
|
1709
|
+
if (opts.withFileTypes) {
|
|
1710
|
+
return names.map(name => ({
|
|
1711
|
+
name,
|
|
1712
|
+
isFile: () => this._db.tree[node.children[name]]?.type === "file",
|
|
1713
|
+
isDirectory: () => this._db.tree[node.children[name]]?.type === "dir",
|
|
1714
|
+
isSymbolicLink:() => this._db.tree[node.children[name]]?.type === "symlink",
|
|
1715
|
+
}));
|
|
1716
|
+
}
|
|
1717
|
+
return names;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// -------------------------------------------------------------------------
|
|
1721
|
+
// readFile / writeFile / appendFile / truncate
|
|
1722
|
+
// -------------------------------------------------------------------------
|
|
1723
|
+
|
|
1724
|
+
readFile(path, opts = {}) {
|
|
1725
|
+
const encoding = typeof opts === "string" ? opts : opts.encoding;
|
|
1726
|
+
const { id, node } = this._getNode(path, { followSymlinks: true });
|
|
1727
|
+
if (node.type === "mount") return node._fs.readFile(node._sub, opts);
|
|
1728
|
+
if (node.type === "dir") throw err("EISDIR", path);
|
|
1729
|
+
this._checkRead(node);
|
|
1730
|
+
this._touch(id, ["atime"]);
|
|
1731
|
+
const data = node.data || "";
|
|
1732
|
+
return encoding ? data : (typeof Buffer !== "undefined" ? Buffer.from(data) : data);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
writeFile(path, data, opts = {}) {
|
|
1736
|
+
const mode = typeof opts === "object" ? (opts.mode || (0o666 & ~this._umask)) : (0o666 & ~this._umask);
|
|
1737
|
+
const flag = typeof opts === "object" ? (opts.flag || "w") : "w";
|
|
1738
|
+
const p = this._p(path);
|
|
1739
|
+
const str = (data !== null && data !== undefined)
|
|
1740
|
+
? (typeof data === "object" && data.toString ? data.toString() : String(data))
|
|
1741
|
+
: "";
|
|
1742
|
+
|
|
1743
|
+
let id, node;
|
|
1744
|
+
try {
|
|
1745
|
+
({ id, node } = this._getNode(p, { followSymlinks: true }));
|
|
1746
|
+
if (node.type === "mount") { node._fs.writeFile(node._sub, data, opts); return; }
|
|
1747
|
+
if (node.type === "dir") throw err("EISDIR", path);
|
|
1748
|
+
if (flag === "wx" || flag === "ax") throw err("EEXIST", path);
|
|
1749
|
+
this._checkWrite(node);
|
|
1750
|
+
} catch(e) {
|
|
1751
|
+
if (e.code !== "ENOENT") throw e;
|
|
1752
|
+
const { dir, base } = splitPath(p);
|
|
1753
|
+
const { id: parentId, node: parentNode } = this._getNode(dir, { followSymlinks: true });
|
|
1754
|
+
if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
|
|
1755
|
+
this._checkWrite(parentNode);
|
|
1756
|
+
id = this._newId();
|
|
1757
|
+
const t = now();
|
|
1758
|
+
this._db.tree[id] = { type: "file", mode, uid: this._uid, gid: this._gid, atime: t, mtime: t, ctime: t, nlink: 1, data: "" };
|
|
1759
|
+
this._db.tree[parentId].children[base] = id;
|
|
1760
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1761
|
+
node = this._db.tree[id];
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
node.data = (flag === "a" || flag === "ax") ? (node.data || "") + str : str;
|
|
1765
|
+
this._touch(id, ["mtime", "ctime"]);
|
|
1766
|
+
this._notify(id, "write", p);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
appendFile(path, data, opts = {}) {
|
|
1770
|
+
const o = typeof opts === "string" ? { encoding: opts } : opts;
|
|
1771
|
+
this.writeFile(path, data, { ...o, flag: "a" });
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
truncate(path, len = 0) {
|
|
1775
|
+
const { id, node } = this._getNode(path, { followSymlinks: true });
|
|
1776
|
+
if (node.type !== "file") throw err("EINVAL", path);
|
|
1777
|
+
this._checkWrite(node);
|
|
1778
|
+
const d = node.data || "";
|
|
1779
|
+
node.data = d.length >= len ? d.slice(0, len) : d.padEnd(len, "\0");
|
|
1780
|
+
this._touch(id, ["mtime", "ctime"]);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
ftruncate(fd, len = 0) {
|
|
1784
|
+
const handle = this._fds.get(fd);
|
|
1785
|
+
if (!handle) throw err("EBADF");
|
|
1786
|
+
const node = this._db.tree[handle.nodeId];
|
|
1787
|
+
if (!node || node.type !== "file") throw err("EBADF");
|
|
1788
|
+
if (!handle.writable) throw err("EBADF");
|
|
1789
|
+
const d = node.data || "";
|
|
1790
|
+
node.data = d.length >= len ? d.slice(0, len) : d.padEnd(len, "\0");
|
|
1791
|
+
this._touch(handle.nodeId, ["mtime", "ctime"]);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// -------------------------------------------------------------------------
|
|
1795
|
+
// rename / unlink / rm
|
|
1796
|
+
// -------------------------------------------------------------------------
|
|
1797
|
+
|
|
1798
|
+
rename(src, dst) {
|
|
1799
|
+
const sp = this._p(src); const dp = this._p(dst);
|
|
1800
|
+
if (sp === dp) return;
|
|
1801
|
+
const { id: srcId } = this._getNode(sp, { followSymlinks: false });
|
|
1802
|
+
const { dir: srcDir, base: srcBase } = splitPath(sp);
|
|
1803
|
+
const { dir: dstDir, base: dstBase } = splitPath(dp);
|
|
1804
|
+
const { id: srcParentId, node: srcParentNode } = this._getNode(srcDir);
|
|
1805
|
+
this._checkWrite(srcParentNode);
|
|
1806
|
+
const { id: dstParentId, node: dstParentNode } = this._getNode(dstDir);
|
|
1807
|
+
this._checkWrite(dstParentNode);
|
|
1808
|
+
if (dstParentNode.type !== "dir") throw err("ENOTDIR", dstDir);
|
|
1809
|
+
if (dstParentNode.children[dstBase]) {
|
|
1810
|
+
const existId = dstParentNode.children[dstBase];
|
|
1811
|
+
const existNode = this._db.tree[existId];
|
|
1812
|
+
if (existNode.type === "dir" && Object.keys(existNode.children).length > 0) throw err("ENOTEMPTY", dst);
|
|
1813
|
+
if (existNode.type === "dir" && this._db.tree[srcId].type !== "dir") throw err("EISDIR", dst);
|
|
1814
|
+
delete this._db.tree[existId];
|
|
1815
|
+
}
|
|
1816
|
+
dstParentNode.children[dstBase] = srcId;
|
|
1817
|
+
delete this._db.tree[srcParentId].children[srcBase];
|
|
1818
|
+
this._touch(srcParentId, ["mtime", "ctime"]);
|
|
1819
|
+
this._touch(dstParentId, ["mtime", "ctime"]);
|
|
1820
|
+
this._touch(srcId, ["ctime"]);
|
|
1821
|
+
this._notify(srcId, "rename", dp);
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
unlink(path) {
|
|
1825
|
+
const p = this._p(path);
|
|
1826
|
+
const { id, node } = this._getNode(p, { followSymlinks: false });
|
|
1827
|
+
if (node.type === "dir") throw err("EISDIR", path);
|
|
1828
|
+
const { dir, base } = splitPath(p);
|
|
1829
|
+
const { id: parentId, node: parentNode } = this._getNode(dir);
|
|
1830
|
+
this._checkWrite(parentNode);
|
|
1831
|
+
node.nlink = (node.nlink || 1) - 1;
|
|
1832
|
+
delete this._db.tree[parentId].children[base];
|
|
1833
|
+
if (node.nlink <= 0) delete this._db.tree[id];
|
|
1834
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1835
|
+
this._notify(parentId, "unlink", p);
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
rm(path, opts = {}) {
|
|
1839
|
+
const p = this._p(path);
|
|
1840
|
+
try {
|
|
1841
|
+
const { node } = this._getNode(p, { followSymlinks: false });
|
|
1842
|
+
if (node.type === "dir") {
|
|
1843
|
+
if (!opts.recursive) throw err("EISDIR", path);
|
|
1844
|
+
this._rmRecursive(p);
|
|
1845
|
+
} else {
|
|
1846
|
+
this.unlink(p);
|
|
1847
|
+
}
|
|
1848
|
+
} catch(e) {
|
|
1849
|
+
if (opts.force && e.code === "ENOENT") return;
|
|
1850
|
+
throw e;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
_rmRecursive(path) {
|
|
1855
|
+
const p = normPath(path);
|
|
1856
|
+
const { id, node } = this._getNode(p, { followSymlinks: false });
|
|
1857
|
+
if (node.type === "dir") {
|
|
1858
|
+
for (const child of Object.keys(node.children))
|
|
1859
|
+
this._rmRecursive(p === "/" ? "/" + child : p + "/" + child);
|
|
1860
|
+
if (p !== "/") {
|
|
1861
|
+
const { dir, base } = splitPath(p);
|
|
1862
|
+
const { id: parentId } = this._getNode(dir);
|
|
1863
|
+
delete this._db.tree[parentId].children[base];
|
|
1864
|
+
delete this._db.tree[id];
|
|
1865
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1866
|
+
}
|
|
1867
|
+
} else {
|
|
1868
|
+
this.unlink(p);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
// -------------------------------------------------------------------------
|
|
1873
|
+
// Links
|
|
1874
|
+
// -------------------------------------------------------------------------
|
|
1875
|
+
|
|
1876
|
+
link(existingPath, newPath) {
|
|
1877
|
+
const { id } = this._getNode(existingPath, { followSymlinks: true });
|
|
1878
|
+
const node = this._db.tree[id];
|
|
1879
|
+
if (node.type === "dir") throw err("EPERM", existingPath);
|
|
1880
|
+
const dp = this._p(newPath);
|
|
1881
|
+
const { dir, base } = splitPath(dp);
|
|
1882
|
+
const { id: parentId, node: parentNode } = this._getNode(dir);
|
|
1883
|
+
this._checkWrite(parentNode);
|
|
1884
|
+
if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
|
|
1885
|
+
if (parentNode.children[base]) throw err("EEXIST", newPath);
|
|
1886
|
+
parentNode.children[base] = id;
|
|
1887
|
+
node.nlink = (node.nlink || 1) + 1;
|
|
1888
|
+
this._touch(id, ["ctime"]);
|
|
1889
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
symlink(target, path) {
|
|
1893
|
+
const p = this._p(path);
|
|
1894
|
+
const { dir, base } = splitPath(p);
|
|
1895
|
+
const { id: parentId, node: parentNode } = this._getNode(dir);
|
|
1896
|
+
this._checkWrite(parentNode);
|
|
1897
|
+
if (parentNode.type !== "dir") throw err("ENOTDIR", dir);
|
|
1898
|
+
if (parentNode.children[base]) throw err("EEXIST", path);
|
|
1899
|
+
const t = now();
|
|
1900
|
+
const id = this._newId();
|
|
1901
|
+
this._db.tree[id] = { type: "symlink", mode: 0o777, uid: this._uid, gid: this._gid, atime: t, mtime: t, ctime: t, nlink: 1, link: target };
|
|
1902
|
+
parentNode.children[base] = id;
|
|
1903
|
+
this._touch(parentId, ["mtime", "ctime"]);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
readlink(path) {
|
|
1907
|
+
const { node } = this._getNode(path, { followSymlinks: false });
|
|
1908
|
+
if (node.type !== "symlink") throw err("EINVAL", path);
|
|
1909
|
+
return node.link;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
realpath(path) { return this._ext(normPath(this._p(path))); }
|
|
1913
|
+
|
|
1914
|
+
// -------------------------------------------------------------------------
|
|
1915
|
+
// Permissions / ownership / timestamps
|
|
1916
|
+
// -------------------------------------------------------------------------
|
|
1917
|
+
|
|
1918
|
+
chmod(path, mode) {
|
|
1919
|
+
const { id, node } = this._getNode(path, { followSymlinks: true });
|
|
1920
|
+
if (this._enforcePerms && this._uid !== 0 && node.uid !== this._uid) throw err("EPERM", path);
|
|
1921
|
+
node.mode = mode;
|
|
1922
|
+
this._touch(id, ["ctime"]);
|
|
1923
|
+
}
|
|
1924
|
+
lchmod(path, mode) {
|
|
1925
|
+
const { id, node } = this._getNode(path, { followSymlinks: false });
|
|
1926
|
+
if (this._enforcePerms && this._uid !== 0 && node.uid !== this._uid) throw err("EPERM", path);
|
|
1927
|
+
node.mode = mode; this._touch(id, ["ctime"]);
|
|
1928
|
+
}
|
|
1929
|
+
fchmod(fd, mode) {
|
|
1930
|
+
const handle = this._fds.get(fd);
|
|
1931
|
+
if (!handle) throw err("EBADF");
|
|
1932
|
+
const node = this._db.tree[handle.nodeId];
|
|
1933
|
+
if (!node) throw err("EBADF");
|
|
1934
|
+
if (this._enforcePerms && this._uid !== 0 && node.uid !== this._uid) throw err("EPERM");
|
|
1935
|
+
node.mode = mode; this._touch(handle.nodeId, ["ctime"]);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
chown(path, uid, gid) {
|
|
1939
|
+
const { id, node } = this._getNode(path, { followSymlinks: true });
|
|
1940
|
+
if (this._enforcePerms && this._uid !== 0) throw err("EPERM", path);
|
|
1941
|
+
node.uid = uid; node.gid = gid; this._touch(id, ["ctime"]);
|
|
1942
|
+
}
|
|
1943
|
+
lchown(path, uid, gid) {
|
|
1944
|
+
const { id, node } = this._getNode(path, { followSymlinks: false });
|
|
1945
|
+
if (this._enforcePerms && this._uid !== 0) throw err("EPERM", path);
|
|
1946
|
+
node.uid = uid; node.gid = gid; this._touch(id, ["ctime"]);
|
|
1947
|
+
}
|
|
1948
|
+
fchown(fd, uid, gid) {
|
|
1949
|
+
const handle = this._fds.get(fd);
|
|
1950
|
+
if (!handle) throw err("EBADF");
|
|
1951
|
+
const node = this._db.tree[handle.nodeId];
|
|
1952
|
+
if (!node) throw err("EBADF");
|
|
1953
|
+
if (this._enforcePerms && this._uid !== 0) throw err("EPERM");
|
|
1954
|
+
node.uid = uid; node.gid = gid; this._touch(handle.nodeId, ["ctime"]);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
utimes(path, atime, mtime) {
|
|
1958
|
+
const { id, node } = this._getNode(path, { followSymlinks: true });
|
|
1959
|
+
this._checkWrite(node);
|
|
1960
|
+
node.atime = new Date(atime).toISOString();
|
|
1961
|
+
node.mtime = new Date(mtime).toISOString();
|
|
1962
|
+
node.ctime = now();
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// -------------------------------------------------------------------------
|
|
1966
|
+
// File descriptors
|
|
1967
|
+
// -------------------------------------------------------------------------
|
|
1968
|
+
|
|
1969
|
+
open(path, flags, mode) {
|
|
1970
|
+
const p = this._p(path);
|
|
1971
|
+
const flagStr = typeof flags === "string" ? flags : String(flags);
|
|
1972
|
+
const isCreate = flagStr.includes("w") || flagStr.includes("a") || (typeof flags === "number" && flags & O_CREAT);
|
|
1973
|
+
const isExcl = flagStr.includes("x") || (typeof flags === "number" && flags & O_EXCL);
|
|
1974
|
+
const isAppend = flagStr.includes("a") || (typeof flags === "number" && flags & O_APPEND);
|
|
1975
|
+
const isTrunc = (flagStr.includes("w") && !isAppend) || (typeof flags === "number" && flags & O_TRUNC);
|
|
1976
|
+
const isWrite = flagStr.includes("w") || flagStr.includes("a") || flagStr.includes("+") || (typeof flags === "number" && (flags & (O_WRONLY | O_RDWR)));
|
|
1977
|
+
const isRead = !flagStr.includes("w") || flagStr.includes("+") || flagStr.includes("r") || (typeof flags === "number" && !(flags & O_WRONLY));
|
|
1978
|
+
|
|
1979
|
+
let nodeId;
|
|
1980
|
+
try {
|
|
1981
|
+
nodeId = this._resolve(p);
|
|
1982
|
+
if (isExcl) throw err("EEXIST", path);
|
|
1983
|
+
const node = this._db.tree[nodeId];
|
|
1984
|
+
if (isRead) this._checkRead(node);
|
|
1985
|
+
if (isWrite) this._checkWrite(node);
|
|
1986
|
+
if (isTrunc) { node.data = ""; this._touch(nodeId, ["mtime", "ctime"]); }
|
|
1987
|
+
} catch(e) {
|
|
1988
|
+
if (e.code !== "ENOENT" || !isCreate) throw e;
|
|
1989
|
+
this.writeFile(p, "", { mode: mode || (0o666 & ~this._umask) });
|
|
1990
|
+
nodeId = this._resolve(p);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
const fd = this._fdCounter++;
|
|
1994
|
+
this._fds.set(fd, {
|
|
1995
|
+
nodeId, flags: flagStr,
|
|
1996
|
+
pos: isAppend ? (this._db.tree[nodeId].data || "").length : 0,
|
|
1997
|
+
readable: isRead, writable: isWrite, append: isAppend,
|
|
1998
|
+
});
|
|
1999
|
+
return fd;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
close(fd) { if (!this._fds.has(fd)) throw err("EBADF"); this._fds.delete(fd); }
|
|
2003
|
+
|
|
2004
|
+
read(fd, buffer, offset = 0, length, position = null) {
|
|
2005
|
+
const handle = this._fds.get(fd);
|
|
2006
|
+
if (!handle || !handle.readable) throw err("EBADF");
|
|
2007
|
+
const node = this._db.tree[handle.nodeId];
|
|
2008
|
+
if (!node || node.type !== "file") throw err("EBADF");
|
|
2009
|
+
const len = length !== undefined ? length : (buffer ? buffer.length : 0);
|
|
2010
|
+
const pos = position !== null ? position : handle.pos;
|
|
2011
|
+
const data = node.data || "";
|
|
2012
|
+
const chunk = data.slice(pos, pos + len);
|
|
2013
|
+
const bytesRead = chunk.length;
|
|
2014
|
+
if (typeof buffer === "object" && buffer !== null && !Array.isArray(buffer)) {
|
|
2015
|
+
for (let i = 0; i < bytesRead; i++) buffer[offset + i] = chunk.charCodeAt(i);
|
|
2016
|
+
}
|
|
2017
|
+
if (position === null) handle.pos += bytesRead;
|
|
2018
|
+
this._touch(handle.nodeId, ["atime"]);
|
|
2019
|
+
return { bytesRead, buffer: chunk };
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
write(fd, buffer, offset = 0, length, position = null) {
|
|
2023
|
+
const handle = this._fds.get(fd);
|
|
2024
|
+
if (!handle || !handle.writable) throw err("EBADF");
|
|
2025
|
+
const node = this._db.tree[handle.nodeId];
|
|
2026
|
+
if (!node || node.type !== "file") throw err("EBADF");
|
|
2027
|
+
const str = typeof buffer === "string"
|
|
2028
|
+
? buffer.slice(offset, length != null ? offset + length : undefined)
|
|
2029
|
+
: (typeof buffer === "object" && buffer.toString ? buffer.toString().slice(offset, length != null ? offset + length : undefined) : String(buffer));
|
|
2030
|
+
const pos = handle.append ? (node.data || "").length : (position !== null ? position : handle.pos);
|
|
2031
|
+
const d = node.data || "";
|
|
2032
|
+
node.data = d.slice(0, pos) + str + d.slice(pos + str.length);
|
|
2033
|
+
const bytesWritten = str.length;
|
|
2034
|
+
if (!handle.append && position === null) handle.pos += bytesWritten;
|
|
2035
|
+
this._touch(handle.nodeId, ["mtime", "ctime"]);
|
|
2036
|
+
this._notify(handle.nodeId, "write", null);
|
|
2037
|
+
return { bytesWritten };
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
fsync(fd) { if (!this._fds.has(fd)) throw err("EBADF"); }
|
|
2041
|
+
fdatasync(fd) { if (!this._fds.has(fd)) throw err("EBADF"); }
|
|
2042
|
+
|
|
2043
|
+
// -------------------------------------------------------------------------
|
|
2044
|
+
// Streams
|
|
2045
|
+
// -------------------------------------------------------------------------
|
|
2046
|
+
|
|
2047
|
+
createReadStream(path, opts = {}) {
|
|
2048
|
+
const self = this; const em = new EventEmitter();
|
|
2049
|
+
const start = opts.start || 0; const end = opts.end;
|
|
2050
|
+
em.pipe = (dest) => {
|
|
2051
|
+
try {
|
|
2052
|
+
const { node } = self._getNode(path, { followSymlinks: true });
|
|
2053
|
+
if (node.type !== "file") { em.emit("error", err("EISDIR", path)); return dest; }
|
|
2054
|
+
const data = (node.data || "").slice(start, end != null ? end + 1 : undefined);
|
|
2055
|
+
em.emit("data", data); em.emit("end");
|
|
2056
|
+
if (dest && typeof dest.write === "function") dest.write(data);
|
|
2057
|
+
} catch(e) { em.emit("error", e); }
|
|
2058
|
+
return dest;
|
|
2059
|
+
};
|
|
2060
|
+
setTimeout(() => em.pipe(null), 0);
|
|
2061
|
+
return em;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
createWriteStream(path, opts = {}) {
|
|
2065
|
+
const self = this; const flag = opts.flags || "w"; const em = new EventEmitter();
|
|
2066
|
+
em.write = (chunk) => { self.appendFile(path, chunk); return true; };
|
|
2067
|
+
em.end = (chunk) => { if (chunk) em.write(chunk); em.emit("finish"); em.emit("close"); };
|
|
2068
|
+
if (flag === "w") try { self.writeFile(path, "", opts); } catch(_) {}
|
|
2069
|
+
return em;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// -------------------------------------------------------------------------
|
|
2073
|
+
// copyFile / cp
|
|
2074
|
+
// -------------------------------------------------------------------------
|
|
2075
|
+
|
|
2076
|
+
copyFile(src, dst, flags = 0) {
|
|
2077
|
+
const excl = flags & COPYFILE_EXCL;
|
|
2078
|
+
if (excl && this.exists(dst)) throw err("EEXIST", dst);
|
|
2079
|
+
const { node: srcNode } = this._getNode(src, { followSymlinks: true });
|
|
2080
|
+
this._checkRead(srcNode);
|
|
2081
|
+
if (srcNode.type === "dir") throw err("EISDIR", src);
|
|
2082
|
+
this.writeFile(dst, srcNode.data || "");
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
cp(src, dst, opts = {}) {
|
|
2086
|
+
const sp = this._p(src); const dp = this._p(dst);
|
|
2087
|
+
const { node: srcNode } = this._getNode(sp, { followSymlinks: true });
|
|
2088
|
+
if (srcNode.type === "file") { this.copyFile(sp, dp, opts.errorOnExist ? COPYFILE_EXCL : 0); return; }
|
|
2089
|
+
if (srcNode.type === "dir") {
|
|
2090
|
+
if (!opts.recursive) throw err("EISDIR", src);
|
|
2091
|
+
if (!this.exists(dp)) this.mkdir(dp, { recursive: true });
|
|
2092
|
+
for (const child of Object.keys(srcNode.children)) this.cp(`${sp}/${child}`, `${dp}/${child}`, opts);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// -------------------------------------------------------------------------
|
|
2097
|
+
// Watchers (real EventEmitter-based with actual events)
|
|
2098
|
+
// -------------------------------------------------------------------------
|
|
2099
|
+
|
|
2100
|
+
watch(path, opts = {}, listener) {
|
|
2101
|
+
if (typeof opts === "function") { listener = opts; opts = {}; }
|
|
2102
|
+
const p = this._p(path);
|
|
2103
|
+
const id = this._resolve(p);
|
|
2104
|
+
const watcher = new EventEmitter();
|
|
2105
|
+
const key = Symbol();
|
|
2106
|
+
this._watchers.set(key, { id, path: p, emitter: watcher, listener: listener || null });
|
|
2107
|
+
watcher.close = () => { this._watchers.delete(key); watcher.emit("close"); };
|
|
2108
|
+
return watcher;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
watchFile(path, opts = {}, listener) {
|
|
2112
|
+
if (typeof opts === "function") { listener = opts; opts = {}; }
|
|
2113
|
+
const p = this._p(path);
|
|
2114
|
+
let lastStat;
|
|
2115
|
+
try { lastStat = this.stat(p); } catch(_) { lastStat = null; }
|
|
2116
|
+
const key = Symbol();
|
|
2117
|
+
const watcher = { path: p, listener: listener || (() => {}), stop: () => this._watchers.delete(key), id: null, emitter: new EventEmitter() };
|
|
2118
|
+
// Hook into notify
|
|
2119
|
+
this._watchers.set(key, watcher);
|
|
2120
|
+
return { stop: watcher.stop };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
unwatchFile(path) {
|
|
2124
|
+
const p = this._p(path);
|
|
2125
|
+
for (const [k, v] of this._watchers) { if (v.path === p) this._watchers.delete(k); }
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// -------------------------------------------------------------------------
|
|
2129
|
+
// Glob / find
|
|
2130
|
+
// -------------------------------------------------------------------------
|
|
2131
|
+
|
|
2132
|
+
glob(pattern, opts = {}) {
|
|
2133
|
+
const root = normPath(this._p(opts.cwd || "/"));
|
|
2134
|
+
const results = [];
|
|
2135
|
+
const search = (dir) => {
|
|
2136
|
+
let node;
|
|
2137
|
+
try { ({ node } = this._getNode(dir, { followSymlinks: true })); } catch(_) { return; }
|
|
2138
|
+
if (node.type !== "dir") return;
|
|
2139
|
+
for (const child of Object.keys(node.children)) {
|
|
2140
|
+
const childPath = dir === "/" ? "/" + child : dir + "/" + child;
|
|
2141
|
+
const rel = this.relative(root, childPath);
|
|
2142
|
+
if (matchGlob(pattern, rel) || matchGlob(pattern, child)) results.push(this._ext(childPath));
|
|
2143
|
+
search(childPath);
|
|
2144
|
+
}
|
|
2145
|
+
};
|
|
2146
|
+
search(root);
|
|
2147
|
+
return results;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
find(path, opts = {}) {
|
|
2151
|
+
const results = [];
|
|
2152
|
+
const typeFilter = opts.type; const nameFilter = opts.name;
|
|
2153
|
+
const search = (p) => {
|
|
2154
|
+
let node;
|
|
2155
|
+
try { ({ node } = this._getNode(p, { followSymlinks: false })); } catch(_) { return; }
|
|
2156
|
+
const typeMatch = !typeFilter || (typeFilter === "f" && node.type === "file") || (typeFilter === "d" && node.type === "dir") || (typeFilter === "l" && node.type === "symlink");
|
|
2157
|
+
const nameMatch = !nameFilter || matchGlob(nameFilter, this.basename(p));
|
|
2158
|
+
if (typeMatch && nameMatch) results.push(this._ext(p));
|
|
2159
|
+
if (node.type === "dir") {
|
|
2160
|
+
for (const child of Object.keys(node.children)) search(p === "/" ? "/" + child : p + "/" + child);
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
search(normPath(this._p(path)));
|
|
2164
|
+
return results;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// -------------------------------------------------------------------------
|
|
2168
|
+
// df / du / tree
|
|
2169
|
+
// -------------------------------------------------------------------------
|
|
2170
|
+
|
|
2171
|
+
df() {
|
|
2172
|
+
let files = 0, dirs = 0, symlinks = 0, totalBytes = 0;
|
|
2173
|
+
for (const [, node] of Object.entries(this._db.tree)) {
|
|
2174
|
+
if (node.type === "file") { files++; totalBytes += (node.data || "").length; }
|
|
2175
|
+
if (node.type === "dir") dirs++;
|
|
2176
|
+
if (node.type === "symlink") symlinks++;
|
|
2177
|
+
}
|
|
2178
|
+
return { files, dirs, symlinks, totalBytes, nodes: Object.keys(this._db.tree).length };
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
du(path) {
|
|
2182
|
+
const p = normPath(this._p(path));
|
|
2183
|
+
let bytes = 0;
|
|
2184
|
+
const measure = (pt) => {
|
|
2185
|
+
let node;
|
|
2186
|
+
try { ({ node } = this._getNode(pt, { followSymlinks: false })); } catch(_) { return; }
|
|
2187
|
+
if (node.type === "file") bytes += (node.data || "").length;
|
|
2188
|
+
if (node.type === "dir") for (const child of Object.keys(node.children)) measure(pt === "/" ? "/" + child : pt + "/" + child);
|
|
2189
|
+
};
|
|
2190
|
+
measure(p);
|
|
2191
|
+
return bytes;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
tree(path = "/", opts = {}) {
|
|
2195
|
+
const indent = opts._indent || "";
|
|
2196
|
+
const isLast = opts._isLast !== false;
|
|
2197
|
+
const p = normPath(this._p(path));
|
|
2198
|
+
const name = this.basename(p) || "/";
|
|
2199
|
+
const connector = indent === "" ? "" : (isLast ? "└── " : "├── ");
|
|
2200
|
+
let out = indent + connector + name + "\n";
|
|
2201
|
+
let node;
|
|
2202
|
+
try { ({ node } = this._getNode(p, { followSymlinks: false })); } catch(_) { return out; }
|
|
2203
|
+
if (node.type === "symlink") return indent + connector + name + " -> " + node.link + "\n";
|
|
2204
|
+
if (node.type === "dir") {
|
|
2205
|
+
const children = Object.keys(node.children);
|
|
2206
|
+
children.forEach((child, i) => {
|
|
2207
|
+
const childPath = p === "/" ? "/" + child : p + "/" + child;
|
|
2208
|
+
const last = i === children.length - 1;
|
|
2209
|
+
const newIndent = indent + (isLast || indent === "" ? " " : "│ ");
|
|
2210
|
+
out += this.tree(childPath, { _indent: newIndent, _isLast: last });
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
return out;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// -------------------------------------------------------------------------
|
|
2217
|
+
// Mount / unmount (properly appends into /mnt/)
|
|
2218
|
+
// -------------------------------------------------------------------------
|
|
2219
|
+
|
|
2220
|
+
/**
|
|
2221
|
+
* Mount another TeaF instance at path (defaults to /mnt/<label>).
|
|
2222
|
+
* Handles duplicate-segment collisions by appending a counter.
|
|
2223
|
+
* Always creates mount point inside /mnt/ if a relative name is given.
|
|
2224
|
+
*/
|
|
2225
|
+
mount(mountPath, otherTF) {
|
|
2226
|
+
if (!(otherTF instanceof TeaF)) throw new Error("mount: must provide another TeaF instance");
|
|
2227
|
+
let p;
|
|
2228
|
+
if (mountPath.startsWith("/")) {
|
|
2229
|
+
p = normPath(mountPath);
|
|
2230
|
+
} else {
|
|
2231
|
+
// Treat as a name inside /mnt/
|
|
2232
|
+
// Avoid collision with existing /mnt/ children
|
|
2233
|
+
const mntNode = this._db.tree[this._resolve("/mnt")];
|
|
2234
|
+
let name = mountPath;
|
|
2235
|
+
let counter = 0;
|
|
2236
|
+
while (mntNode && mntNode.children[name]) {
|
|
2237
|
+
counter++;
|
|
2238
|
+
name = mountPath + "_" + counter;
|
|
2239
|
+
}
|
|
2240
|
+
p = "/mnt/" + name;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
// Ensure path exists
|
|
2244
|
+
if (!this.exists(p)) this.mkdir(p, { recursive: true });
|
|
2245
|
+
this._mounts.set(p, otherTF);
|
|
2246
|
+
this.emit("mount", p);
|
|
2247
|
+
return p;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
unmount(path) {
|
|
2251
|
+
const p = path.startsWith("/") ? normPath(path) : "/mnt/" + path;
|
|
2252
|
+
if (!this._mounts.has(p)) throw new Error("unmount: not a mount point: " + p);
|
|
2253
|
+
this._mounts.delete(p);
|
|
2254
|
+
this.emit("unmount", p);
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// -------------------------------------------------------------------------
|
|
2258
|
+
// exec — run sh command/script in this FS
|
|
2259
|
+
// -------------------------------------------------------------------------
|
|
2260
|
+
|
|
2261
|
+
/**
|
|
2262
|
+
* Execute a shell command string in this filesystem.
|
|
2263
|
+
* @param {string} cmd - command or sh script
|
|
2264
|
+
* @param {Object} opts - { env, stdin, cwd, uid, gid }
|
|
2265
|
+
* @returns {{ stdout: string, stderr: string, code: number }}
|
|
2266
|
+
*/
|
|
2267
|
+
exec(cmd, opts = {}) {
|
|
2268
|
+
if (!this._sh) {
|
|
2269
|
+
this._sh = new Sh(this, { uid: this._uid, gid: this._gid });
|
|
2270
|
+
}
|
|
2271
|
+
if (opts.env) Object.assign(this._sh.env, opts.env);
|
|
2272
|
+
if (opts.cwd) this._sh.env.PWD = normPath(this._p(opts.cwd));
|
|
2273
|
+
if (opts.uid !== undefined) this._sh._uid = opts.uid;
|
|
2274
|
+
if (opts.gid !== undefined) this._sh._gid = opts.gid;
|
|
2275
|
+
return this._sh.exec(cmd, { stdin: opts.stdin || [], stdout: opts.stdout, stderr: opts.stderr });
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/** Get or create the shell instance */
|
|
2279
|
+
get sh() {
|
|
2280
|
+
if (!this._sh) this._sh = new Sh(this, { uid: this._uid, gid: this._gid });
|
|
2281
|
+
return this._sh;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
/**
|
|
2285
|
+
* Create an interactive REPL session for this FS.
|
|
2286
|
+
* @returns {ShRepl}
|
|
2287
|
+
*/
|
|
2288
|
+
repl(opts = {}) {
|
|
2289
|
+
const sh = new Sh(this, { uid: this._uid, gid: this._gid, env: opts.env });
|
|
2290
|
+
return new ShRepl(sh);
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// -------------------------------------------------------------------------
|
|
2294
|
+
// TeaBin helpers (install binary onto filesystem)
|
|
2295
|
+
// -------------------------------------------------------------------------
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* Assemble TeaBin source and install as an executable at path.
|
|
2299
|
+
* @param {string} path - e.g. "/bin/hello"
|
|
2300
|
+
* @param {string} src - assembly source
|
|
2301
|
+
*/
|
|
2302
|
+
installBin(path, src) {
|
|
2303
|
+
const bytes = TeaBinVM.assemble(src);
|
|
2304
|
+
// Store as a binary string in the file
|
|
2305
|
+
let str = "";
|
|
2306
|
+
for (let i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
|
|
2307
|
+
this.writeFile(path, str);
|
|
2308
|
+
this.chmod(path, 0o755);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* Run a TeaBin binary file from the filesystem.
|
|
2313
|
+
* @param {string} path
|
|
2314
|
+
* @param {Object} opts - { ports, interrupts, maxCycles }
|
|
2315
|
+
* @returns {{ stdout: string, exitCode: number }}
|
|
2316
|
+
*/
|
|
2317
|
+
runBin(path, opts = {}) {
|
|
2318
|
+
const data = this.readFile(path, "utf8");
|
|
2319
|
+
const bytes = new Uint8Array(data.length);
|
|
2320
|
+
for (let i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i) & 0xFF;
|
|
2321
|
+
const vm = new TeaBinVM(opts);
|
|
2322
|
+
vm.load(bytes);
|
|
2323
|
+
const exitCode = vm.run();
|
|
2324
|
+
const stdout = vm.stdout.map(o => typeof o === "number" ? String.fromCharCode(o) : String(o)).join("");
|
|
2325
|
+
return { stdout, exitCode };
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// -------------------------------------------------------------------------
|
|
2329
|
+
// JSON persistence (snaps/ directory)
|
|
2330
|
+
// -------------------------------------------------------------------------
|
|
2331
|
+
|
|
2332
|
+
toJSON() { return JSON.stringify(this._db, null, 2); }
|
|
2333
|
+
|
|
2334
|
+
fromJSON(json) {
|
|
2335
|
+
const data = typeof json === "string" ? JSON.parse(json) : json;
|
|
2336
|
+
if (!data.tree || !data.meta) throw new Error("Invalid tea/f snapshot");
|
|
2337
|
+
this._db = data;
|
|
2338
|
+
return this;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
/**
|
|
2342
|
+
* Save snapshot to ./snaps/<filename>.json (Node.js only).
|
|
2343
|
+
* @param {string} name - just the base name (no path needed), e.g. "my-fs"
|
|
2344
|
+
* or a full path starting with /
|
|
2345
|
+
*/
|
|
2346
|
+
saveSnapshot(name) {
|
|
2347
|
+
if (typeof require === "undefined") throw new Error("saveSnapshot requires Node.js");
|
|
2348
|
+
const fs = require("fs");
|
|
2349
|
+
const path = require("path");
|
|
2350
|
+
|
|
2351
|
+
let filePath;
|
|
2352
|
+
if (name.startsWith("/") || name.includes("\\") || name.includes(":")) {
|
|
2353
|
+
filePath = name; // absolute path given
|
|
2354
|
+
} else {
|
|
2355
|
+
// Ensure ./snaps/ exists
|
|
2356
|
+
if (!fs.existsSync(SNAP_DIR)) fs.mkdirSync(SNAP_DIR, { recursive: true });
|
|
2357
|
+
const base = name.endsWith(".json") ? name : name + ".json";
|
|
2358
|
+
filePath = path.join(SNAP_DIR, base);
|
|
2359
|
+
}
|
|
2360
|
+
fs.writeFileSync(filePath, this.toJSON(), "utf8");
|
|
2361
|
+
return filePath;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
/** Alias kept for compatibility */
|
|
2365
|
+
saveToFile(filePath) { return this.saveSnapshot(filePath); }
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* Load snapshot from ./snaps/<name>.json or an absolute path (Node.js only).
|
|
2369
|
+
*/
|
|
2370
|
+
loadSnapshot(name) {
|
|
2371
|
+
if (typeof require === "undefined") throw new Error("loadSnapshot requires Node.js");
|
|
2372
|
+
const fs = require("fs");
|
|
2373
|
+
const path = require("path");
|
|
2374
|
+
let filePath;
|
|
2375
|
+
// If it looks like a full/relative OS path (contains path sep or starts with . or /), use as-is
|
|
2376
|
+
if (name.startsWith("/") || name.startsWith("./") || name.startsWith("../") ||
|
|
2377
|
+
name.includes("\\") || name.includes(":") || name.includes(path.sep)) {
|
|
2378
|
+
filePath = name;
|
|
2379
|
+
} else {
|
|
2380
|
+
const base = name.endsWith(".json") ? name : name + ".json";
|
|
2381
|
+
filePath = path.join(SNAP_DIR, base);
|
|
2382
|
+
}
|
|
2383
|
+
const json = fs.readFileSync(filePath, "utf8");
|
|
2384
|
+
return this.fromJSON(json);
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
/** Alias */
|
|
2388
|
+
loadFromFile(filePath) { return this.loadSnapshot(filePath); }
|
|
2389
|
+
|
|
2390
|
+
// -------------------------------------------------------------------------
|
|
2391
|
+
// Async wrappers
|
|
2392
|
+
// -------------------------------------------------------------------------
|
|
2393
|
+
|
|
2394
|
+
get promises() {
|
|
2395
|
+
const wrap = (method) => (...args) => {
|
|
2396
|
+
try { return Promise.resolve(this[method](...args)); }
|
|
2397
|
+
catch(e) { return Promise.reject(e); }
|
|
2398
|
+
};
|
|
2399
|
+
return {
|
|
2400
|
+
readFile: wrap("readFile"), writeFile: wrap("writeFile"),
|
|
2401
|
+
appendFile: wrap("appendFile"), truncate: wrap("truncate"),
|
|
2402
|
+
mkdir: wrap("mkdir"), rmdir: wrap("rmdir"), readdir: wrap("readdir"),
|
|
2403
|
+
stat: wrap("stat"), lstat: wrap("lstat"), rename: wrap("rename"),
|
|
2404
|
+
unlink: wrap("unlink"), rm: wrap("rm"), link: wrap("link"),
|
|
2405
|
+
symlink: wrap("symlink"), readlink: wrap("readlink"), realpath: wrap("realpath"),
|
|
2406
|
+
chmod: wrap("chmod"), lchmod: wrap("lchmod"), chown: wrap("chown"),
|
|
2407
|
+
lchown: wrap("lchown"), utimes: wrap("utimes"), copyFile: wrap("copyFile"),
|
|
2408
|
+
cp: wrap("cp"), open: wrap("open"), access: wrap("access"),
|
|
2409
|
+
exec: wrap("exec"),
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// ===========================================================================
|
|
2415
|
+
// Factory
|
|
2416
|
+
// ===========================================================================
|
|
2417
|
+
|
|
2418
|
+
function createFS(opts) { return new TeaF(opts); }
|
|
2419
|
+
createFS.TeaF = TeaF;
|
|
2420
|
+
createFS.Stats = Stats;
|
|
2421
|
+
createFS.TFError = TFError;
|
|
2422
|
+
createFS.TeaBinVM = TeaBinVM;
|
|
2423
|
+
createFS.Sh = Sh;
|
|
2424
|
+
createFS.ShRepl = ShRepl;
|
|
2425
|
+
createFS.PathAdapter = PathAdapter;
|
|
2426
|
+
createFS.COPYFILE_EXCL = COPYFILE_EXCL;
|
|
2427
|
+
createFS.O_RDONLY = O_RDONLY;
|
|
2428
|
+
createFS.O_WRONLY = O_WRONLY;
|
|
2429
|
+
createFS.O_RDWR = O_RDWR;
|
|
2430
|
+
createFS.O_CREAT = O_CREAT;
|
|
2431
|
+
createFS.O_EXCL = O_EXCL;
|
|
2432
|
+
createFS.O_TRUNC = O_TRUNC;
|
|
2433
|
+
createFS.O_APPEND = O_APPEND;
|
|
2434
|
+
createFS.OP = OP;
|
|
2435
|
+
createFS.VERSION = VERSION;
|
|
2436
|
+
|
|
2437
|
+
if (typeof module !== "undefined" && module.exports) module.exports = { kitdef: createFS };
|
|
2438
|
+
if (typeof window !== "undefined") window.tf = createFS;
|
|
2439
|
+
|
|
2440
|
+
// ===========================================================================
|
|
2441
|
+
// Self-test
|
|
2442
|
+
// ===========================================================================
|
|
2443
|
+
|
|
2444
|
+
if (typeof process !== "undefined" && process.argv[1] && process.argv[1].endsWith("tea-f.js")) {
|
|
2445
|
+
const tf = createFS({ label: "test-fs", enforcePermissions: false });
|
|
2446
|
+
const assert = (cond, msg) => { if (!cond) throw new Error("FAIL: " + msg); };
|
|
2447
|
+
console.log("=== tea's F v2 — self-test ===\n");
|
|
2448
|
+
|
|
2449
|
+
// mkdir / exists
|
|
2450
|
+
tf.mkdir("/etc/config", { recursive: true }); // /etc pre-exists from _init
|
|
2451
|
+
tf.mkdir("/usr/local/bin", { recursive: true });
|
|
2452
|
+
assert(tf.exists("/etc/config"), "mkdir /etc/config");
|
|
2453
|
+
assert(tf.exists("/usr/local/bin"), "mkdir recursive");
|
|
2454
|
+
console.log("✓ mkdir + exists");
|
|
2455
|
+
|
|
2456
|
+
// writeFile / readFile
|
|
2457
|
+
tf.writeFile("/etc/hosts", "127.0.0.1 localhost\n");
|
|
2458
|
+
assert(tf.readFile("/etc/hosts", "utf8") === "127.0.0.1 localhost\n", "writeFile/readFile");
|
|
2459
|
+
console.log("✓ writeFile / readFile");
|
|
2460
|
+
|
|
2461
|
+
// appendFile
|
|
2462
|
+
tf.appendFile("/etc/hosts", "::1 localhost\n");
|
|
2463
|
+
assert(tf.readFile("/etc/hosts", "utf8").includes("::1"), "appendFile");
|
|
2464
|
+
console.log("✓ appendFile");
|
|
2465
|
+
|
|
2466
|
+
// watcher events
|
|
2467
|
+
const watched = [];
|
|
2468
|
+
const watcher = tf.watch("/etc");
|
|
2469
|
+
watcher.on("write", (ev, f) => watched.push(ev));
|
|
2470
|
+
tf.writeFile("/etc/test-watch", "hello");
|
|
2471
|
+
assert(watched.length > 0, "watcher received event");
|
|
2472
|
+
watcher.close();
|
|
2473
|
+
console.log("✓ watcher real events");
|
|
2474
|
+
|
|
2475
|
+
// stat
|
|
2476
|
+
const s = tf.stat("/etc/hosts");
|
|
2477
|
+
assert(s.isFile(), "stat isFile");
|
|
2478
|
+
assert(s.size > 0, "stat size");
|
|
2479
|
+
console.log("✓ stat");
|
|
2480
|
+
|
|
2481
|
+
// symlink / readlink / lstat
|
|
2482
|
+
tf.symlink("/etc/hosts", "/etc/hosts.bak");
|
|
2483
|
+
const ls = tf.lstat("/etc/hosts.bak");
|
|
2484
|
+
assert(ls.isSymbolicLink(), "lstat symlink");
|
|
2485
|
+
assert(tf.readlink("/etc/hosts.bak") === "/etc/hosts", "readlink");
|
|
2486
|
+
console.log("✓ symlink / readlink / lstat");
|
|
2487
|
+
|
|
2488
|
+
// hard link
|
|
2489
|
+
tf.link("/etc/hosts", "/etc/hosts2");
|
|
2490
|
+
assert(tf.readFile("/etc/hosts2", "utf8") === tf.readFile("/etc/hosts", "utf8"), "hard link");
|
|
2491
|
+
console.log("✓ hard link");
|
|
2492
|
+
|
|
2493
|
+
// rename
|
|
2494
|
+
tf.rename("/etc/hosts2", "/etc/hosts.old");
|
|
2495
|
+
assert(!tf.exists("/etc/hosts2"), "rename src gone");
|
|
2496
|
+
assert(tf.exists("/etc/hosts.old"), "rename dst exists");
|
|
2497
|
+
console.log("✓ rename");
|
|
2498
|
+
|
|
2499
|
+
// unlink
|
|
2500
|
+
tf.unlink("/etc/hosts.old");
|
|
2501
|
+
assert(!tf.exists("/etc/hosts.old"), "unlink");
|
|
2502
|
+
console.log("✓ unlink");
|
|
2503
|
+
|
|
2504
|
+
// readdir
|
|
2505
|
+
const entries = tf.readdir("/etc");
|
|
2506
|
+
assert(entries.includes("hosts"), "readdir includes hosts");
|
|
2507
|
+
console.log("✓ readdir");
|
|
2508
|
+
|
|
2509
|
+
// chmod / chown
|
|
2510
|
+
tf.chmod("/etc/hosts", 0o644);
|
|
2511
|
+
assert(tf.stat("/etc/hosts").mode & 0o644, "chmod");
|
|
2512
|
+
tf.chown("/etc/hosts", 1000, 1000);
|
|
2513
|
+
assert(tf.stat("/etc/hosts").uid === 1000, "chown uid");
|
|
2514
|
+
console.log("✓ chmod / chown");
|
|
2515
|
+
|
|
2516
|
+
// truncate
|
|
2517
|
+
tf.truncate("/etc/hosts", 5);
|
|
2518
|
+
assert(tf.readFile("/etc/hosts", "utf8").length === 5, "truncate");
|
|
2519
|
+
console.log("✓ truncate");
|
|
2520
|
+
|
|
2521
|
+
// File descriptors
|
|
2522
|
+
tf.writeFile("/tmp/test.txt", "hello world");
|
|
2523
|
+
const fd = tf.open("/tmp/test.txt", "r");
|
|
2524
|
+
const { bytesRead } = tf.read(fd, new Uint8Array(5), 0, 5, 0);
|
|
2525
|
+
assert(bytesRead === 5, "read bytesRead");
|
|
2526
|
+
tf.close(fd);
|
|
2527
|
+
const fd2 = tf.open("/tmp/test2.txt", "w");
|
|
2528
|
+
tf.write(fd2, "abc");
|
|
2529
|
+
tf.close(fd2);
|
|
2530
|
+
assert(tf.readFile("/tmp/test2.txt", "utf8") === "abc", "write via fd");
|
|
2531
|
+
console.log("✓ open / read / write / close");
|
|
2532
|
+
|
|
2533
|
+
// copyFile / cp recursive
|
|
2534
|
+
tf.writeFile("/etc/hosts", "127.0.0.1 localhost\n");
|
|
2535
|
+
tf.copyFile("/etc/hosts", "/etc/hosts.copy");
|
|
2536
|
+
assert(tf.exists("/etc/hosts.copy"), "copyFile");
|
|
2537
|
+
tf.mkdir("/backup");
|
|
2538
|
+
tf.cp("/etc", "/backup/etc", { recursive: true });
|
|
2539
|
+
assert(tf.exists("/backup/etc/hosts"), "cp recursive");
|
|
2540
|
+
tf.rm("/backup", { recursive: true });
|
|
2541
|
+
assert(!tf.exists("/backup"), "rm recursive");
|
|
2542
|
+
console.log("✓ copyFile / cp / rm");
|
|
2543
|
+
|
|
2544
|
+
// glob / find / path helpers
|
|
2545
|
+
tf.writeFile("/usr/local/bin/node", "#!/usr/bin/env node\n");
|
|
2546
|
+
tf.writeFile("/usr/local/bin/npm", "#!/usr/bin/env node\n");
|
|
2547
|
+
const hits = tf.glob("**/bin/*");
|
|
2548
|
+
assert(hits.length >= 2, "glob **");
|
|
2549
|
+
const found = tf.find("/usr", { type: "f" });
|
|
2550
|
+
assert(found.length >= 2, "find type:f");
|
|
2551
|
+
assert(tf.join("/usr", "local", "bin") === "/usr/local/bin", "join");
|
|
2552
|
+
assert(tf.dirname("/usr/local/bin") === "/usr/local", "dirname");
|
|
2553
|
+
assert(tf.basename("/usr/local/bin/node") === "node", "basename");
|
|
2554
|
+
assert(tf.extname("/etc/hosts.copy") === ".copy", "extname");
|
|
2555
|
+
assert(tf.relative("/usr", "/usr/local/bin") === "local/bin", "relative");
|
|
2556
|
+
console.log("✓ glob / find / path helpers");
|
|
2557
|
+
|
|
2558
|
+
// df / du / tree
|
|
2559
|
+
const dfStats = tf.df();
|
|
2560
|
+
assert(dfStats.files > 0, "df files");
|
|
2561
|
+
const usage = tf.du("/etc");
|
|
2562
|
+
assert(usage >= 0, "du");
|
|
2563
|
+
const treeStr = tf.tree("/");
|
|
2564
|
+
assert(treeStr.includes("etc"), "tree includes etc");
|
|
2565
|
+
console.log("✓ df / du / tree");
|
|
2566
|
+
|
|
2567
|
+
// Win mode
|
|
2568
|
+
const tfWin = createFS({ mode: "win", label: "win-test" });
|
|
2569
|
+
tfWin.mkdir("C:\\Users\\Alice", { recursive: true });
|
|
2570
|
+
tfWin.writeFile("C:\\Users\\Alice\\notes.txt", "hello windows");
|
|
2571
|
+
assert(tfWin.readFile("C:\\Users\\Alice\\notes.txt", "utf8") === "hello windows", "win mode");
|
|
2572
|
+
console.log("✓ win mode (C:\\...)");
|
|
2573
|
+
|
|
2574
|
+
// URL mode
|
|
2575
|
+
const tfUrl = createFS({ mode: "url", label: "url-test" });
|
|
2576
|
+
tfUrl.mkdir("http://example.com/api/v1", { recursive: true });
|
|
2577
|
+
tfUrl.writeFile("http://example.com/api/v1/data.json", '{"ok":true}');
|
|
2578
|
+
assert(tfUrl.readFile("http://example.com/api/v1/data.json", "utf8") === '{"ok":true}', "url mode read");
|
|
2579
|
+
console.log("✓ url mode (http://...)");
|
|
2580
|
+
|
|
2581
|
+
// Mount with /mnt/ appending and duplicate prevention
|
|
2582
|
+
const tf2 = createFS({ label: "sub-fs" });
|
|
2583
|
+
tf2.writeFile("/hello.txt", "from mounted fs");
|
|
2584
|
+
const mountAt = tf.mount("sub", tf2);
|
|
2585
|
+
assert(mountAt === "/mnt/sub", "mount path");
|
|
2586
|
+
// Mount duplicate
|
|
2587
|
+
const tf3 = createFS({ label: "sub-fs-2" });
|
|
2588
|
+
const mountAt2 = tf.mount("sub", tf3);
|
|
2589
|
+
assert(mountAt2 === "/mnt/sub_1", "mount collision resolved");
|
|
2590
|
+
console.log("✓ mount (into /mnt/, collision-safe)");
|
|
2591
|
+
|
|
2592
|
+
// Shell exec
|
|
2593
|
+
tf.writeFile("/etc/motd", "Welcome to tea/f!\n");
|
|
2594
|
+
const r1 = tf.exec("cat /etc/motd");
|
|
2595
|
+
assert(r1.stdout.includes("Welcome"), "exec cat");
|
|
2596
|
+
const r2 = tf.exec("echo hello world");
|
|
2597
|
+
assert(r2.stdout.trim() === "hello world", "exec echo");
|
|
2598
|
+
const r3 = tf.exec("ls /etc");
|
|
2599
|
+
assert(r3.stdout.includes("hosts"), "exec ls");
|
|
2600
|
+
const r4 = tf.exec("echo foo > /tmp/redir.txt && cat /tmp/redir.txt");
|
|
2601
|
+
assert(tf.readFile("/tmp/redir.txt", "utf8").trim() === "foo", "exec redirect");
|
|
2602
|
+
const r5 = tf.exec("echo a | grep a");
|
|
2603
|
+
assert(r5.stdout.trim() === "a", "exec pipe+grep");
|
|
2604
|
+
console.log("✓ shell exec (cat, echo, ls, redirect, pipe)");
|
|
2605
|
+
|
|
2606
|
+
// REPL
|
|
2607
|
+
const repl = tf.repl();
|
|
2608
|
+
const res = repl.line("echo repl-works");
|
|
2609
|
+
assert(res.stdout.trim() === "repl-works", "repl line");
|
|
2610
|
+
repl.close();
|
|
2611
|
+
console.log("✓ repl");
|
|
2612
|
+
|
|
2613
|
+
// TeaBin VM — assemble + run simple program
|
|
2614
|
+
const asmSrc = `
|
|
2615
|
+
.code
|
|
2616
|
+
MOV R0, 42
|
|
2617
|
+
OUT 0, R0
|
|
2618
|
+
HALT 0
|
|
2619
|
+
`;
|
|
2620
|
+
const bytes = TeaBinVM.assemble(asmSrc);
|
|
2621
|
+
const vm = new TeaBinVM();
|
|
2622
|
+
vm.load(bytes);
|
|
2623
|
+
vm.run();
|
|
2624
|
+
assert(vm.stdout[0] === 42, "TeaBin VM OUT");
|
|
2625
|
+
console.log("✓ TeaBin VM (assemble + run + OUT)");
|
|
2626
|
+
|
|
2627
|
+
// installBin + runBin
|
|
2628
|
+
const helloBinSrc = `
|
|
2629
|
+
.data
|
|
2630
|
+
msg "Hi from bin!\n"
|
|
2631
|
+
.code
|
|
2632
|
+
MOV R2, 0
|
|
2633
|
+
MOV R3, 14
|
|
2634
|
+
MOV R0, 1
|
|
2635
|
+
INT 0x80
|
|
2636
|
+
HALT 0
|
|
2637
|
+
`;
|
|
2638
|
+
tf.installBin("/bin/hello", helloBinSrc);
|
|
2639
|
+
assert(tf.exists("/bin/hello"), "installBin");
|
|
2640
|
+
const binResult = tf.runBin("/bin/hello");
|
|
2641
|
+
assert(binResult.stdout.includes("Hi"), "runBin stdout");
|
|
2642
|
+
console.log("✓ installBin / runBin");
|
|
2643
|
+
|
|
2644
|
+
// JSON round-trip
|
|
2645
|
+
const json = tf.toJSON();
|
|
2646
|
+
const tf4 = createFS();
|
|
2647
|
+
tf4.fromJSON(json);
|
|
2648
|
+
assert(tf4.exists("/etc/hosts"), "JSON round-trip");
|
|
2649
|
+
console.log("✓ toJSON / fromJSON");
|
|
2650
|
+
|
|
2651
|
+
// saveSnapshot / loadSnapshot
|
|
2652
|
+
const snapPath = tf.saveSnapshot("test-snap");
|
|
2653
|
+
const tf5 = createFS();
|
|
2654
|
+
tf5.loadSnapshot(snapPath);
|
|
2655
|
+
assert(tf5.exists("/usr/local/bin/node"), "loadSnapshot");
|
|
2656
|
+
// Cleanup
|
|
2657
|
+
try { require("fs").unlinkSync(snapPath); require("fs").rmdirSync(SNAP_DIR); } catch(_) {}
|
|
2658
|
+
console.log("✓ saveSnapshot / loadSnapshot (./snaps/)");
|
|
2659
|
+
|
|
2660
|
+
// Permission enforcement
|
|
2661
|
+
const tfPerms = createFS({ uid: 0, gid: 0, enforcePermissions: true });
|
|
2662
|
+
// As root: create a home dir for uid 1000 and a secret file
|
|
2663
|
+
tfPerms.mkdir("/home/alice", { recursive: true });
|
|
2664
|
+
tfPerms.chown("/home/alice", 1000, 1000);
|
|
2665
|
+
tfPerms.chmod("/home/alice", 0o700);
|
|
2666
|
+
tfPerms.writeFile("/home/alice/secret.txt", "private");
|
|
2667
|
+
tfPerms.chown("/home/alice/secret.txt", 1000, 1000);
|
|
2668
|
+
tfPerms.chmod("/home/alice/secret.txt", 0o600);
|
|
2669
|
+
// Now act as uid 1000 — owner can read
|
|
2670
|
+
tfPerms._uid = 1000; tfPerms._gid = 1000;
|
|
2671
|
+
try {
|
|
2672
|
+
tfPerms.readFile("/home/alice/secret.txt", "utf8");
|
|
2673
|
+
console.log("✓ permission: owner can read 0o600 file");
|
|
2674
|
+
} catch(e) { throw new Error("FAIL: owner should read own file: " + e.message); }
|
|
2675
|
+
// Act as uid 2000 — should be denied
|
|
2676
|
+
tfPerms._uid = 2000; tfPerms._gid = 2000;
|
|
2677
|
+
let denied = false;
|
|
2678
|
+
try { tfPerms.readFile("/home/alice/secret.txt", "utf8"); }
|
|
2679
|
+
catch(e) { denied = e.code === "EACCES"; }
|
|
2680
|
+
assert(denied, "permission: other user denied reading 0o600 file");
|
|
2681
|
+
console.log("✓ permission enforcement");
|
|
2682
|
+
|
|
2683
|
+
tf.promises.readFile("/usr/local/bin/node", "utf8").then(v => {
|
|
2684
|
+
assert(v.includes("node"), "promises.readFile");
|
|
2685
|
+
console.log("✓ promises API");
|
|
2686
|
+
console.log("\n=== all tests passed ===\n");
|
|
2687
|
+
console.log("Filesystem snapshot:\n");
|
|
2688
|
+
console.log(tf.tree("/"));
|
|
2689
|
+
console.log("df:", tf.df());
|
|
2690
|
+
});
|
|
2691
|
+
}
|