typescript-virtual-container 1.5.11 → 1.6.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/README.md +236 -456
- package/dist/.tsbuildinfo +1 -1
- package/dist/Honeypot/index.d.ts +9 -0
- package/dist/Honeypot/index.js +57 -0
- package/dist/SSHMimic/exec.d.ts +4 -0
- package/dist/SSHMimic/exec.js +4 -0
- package/dist/SSHMimic/executor.d.ts +10 -1
- package/dist/SSHMimic/executor.js +18 -8
- package/dist/SSHMimic/hostKey.d.ts +5 -0
- package/dist/SSHMimic/hostKey.js +5 -0
- package/dist/SSHMimic/loginBanner.d.ts +7 -0
- package/dist/SSHMimic/loginBanner.js +4 -0
- package/dist/SSHMimic/loginFormat.d.ts +4 -0
- package/dist/SSHMimic/loginFormat.js +4 -0
- package/dist/SSHMimic/prompt.d.ts +9 -0
- package/dist/SSHMimic/prompt.js +9 -0
- package/dist/SSHMimic/scp.d.ts +18 -0
- package/dist/SSHMimic/scp.js +14 -0
- package/dist/VirtualFileSystem/binaryPack.d.ts +7 -3
- package/dist/VirtualFileSystem/binaryPack.js +32 -10
- package/dist/VirtualFileSystem/index.d.ts +29 -0
- package/dist/VirtualFileSystem/index.js +126 -5
- package/dist/VirtualFileSystem/internalTypes.d.ts +4 -0
- package/dist/VirtualFileSystem/journal.d.ts +10 -4
- package/dist/VirtualFileSystem/journal.js +12 -2
- package/dist/VirtualFileSystem/path.d.ts +23 -1
- package/dist/VirtualFileSystem/path.js +23 -3
- package/dist/VirtualPackageManager/index.js +1 -1
- package/dist/VirtualShell/index.d.ts +3 -0
- package/dist/VirtualShell/index.js +12 -3
- package/dist/VirtualUserManager/index.d.ts +20 -1
- package/dist/VirtualUserManager/index.js +52 -15
- package/dist/commands/bc.d.ts +5 -0
- package/dist/commands/bc.js +5 -0
- package/dist/commands/cat.js +2 -2
- package/dist/commands/chgrp.d.ts +7 -0
- package/dist/commands/chgrp.js +42 -0
- package/dist/commands/chown.d.ts +7 -0
- package/dist/commands/chown.js +79 -0
- package/dist/commands/cp.js +4 -3
- package/dist/commands/dd.d.ts +7 -0
- package/dist/commands/dd.js +60 -0
- package/dist/commands/declare.js +0 -2
- package/dist/commands/expr.d.ts +7 -0
- package/dist/commands/expr.js +63 -0
- package/dist/commands/fun.d.ts +5 -0
- package/dist/commands/fun.js +5 -1
- package/dist/commands/help.d.ts +5 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/helpers.d.ts +43 -0
- package/dist/commands/helpers.js +61 -0
- package/dist/commands/id.d.ts +5 -0
- package/dist/commands/id.js +5 -0
- package/dist/commands/ip.d.ts +1 -0
- package/dist/commands/ip.js +50 -23
- package/dist/commands/jobs.js +43 -9
- package/dist/commands/kill.d.ts +1 -0
- package/dist/commands/kill.js +13 -5
- package/dist/commands/last.js +1 -1
- package/dist/commands/ln.d.ts +5 -0
- package/dist/commands/ln.js +5 -0
- package/dist/commands/ls.d.ts +5 -0
- package/dist/commands/ls.js +19 -4
- package/dist/commands/lsb-release.js +1 -1
- package/dist/commands/man.d.ts +5 -0
- package/dist/commands/man.js +5 -0
- package/dist/commands/manuals-bundle.js +242 -0
- package/dist/commands/miscutils.d.ts +43 -0
- package/dist/commands/miscutils.js +233 -0
- package/dist/commands/mkdir.js +3 -2
- package/dist/commands/mv.js +4 -3
- package/dist/commands/netcat.d.ts +7 -0
- package/dist/commands/netcat.js +64 -0
- package/dist/commands/nice.d.ts +7 -0
- package/dist/commands/nice.js +22 -0
- package/dist/commands/nohup.d.ts +7 -0
- package/dist/commands/nohup.js +18 -0
- package/dist/commands/ping.d.ts +2 -1
- package/dist/commands/ping.js +46 -8
- package/dist/commands/procUtils.d.ts +13 -0
- package/dist/commands/procUtils.js +72 -0
- package/dist/commands/pwd.d.ts +5 -0
- package/dist/commands/pwd.js +5 -0
- package/dist/commands/python.js +0 -4
- package/dist/commands/read.js +0 -1
- package/dist/commands/registry.d.ts +37 -0
- package/dist/commands/registry.js +73 -0
- package/dist/commands/rm.js +3 -2
- package/dist/commands/runtime.d.ts +47 -1
- package/dist/commands/runtime.js +60 -5
- package/dist/commands/sh.d.ts +5 -0
- package/dist/commands/sh.js +5 -0
- package/dist/commands/stat.js +3 -2
- package/dist/commands/strace.js +0 -1
- package/dist/commands/sysinfo.d.ts +19 -0
- package/dist/commands/sysinfo.js +73 -0
- package/dist/commands/test.d.ts +5 -0
- package/dist/commands/test.js +5 -0
- package/dist/commands/textutils.d.ts +25 -0
- package/dist/commands/textutils.js +171 -0
- package/dist/commands/top.d.ts +7 -0
- package/dist/commands/top.js +54 -0
- package/dist/commands/touch.js +6 -2
- package/dist/commands/tr.d.ts +5 -0
- package/dist/commands/tr.js +5 -0
- package/dist/commands/w.js +1 -1
- package/dist/commands/which.d.ts +5 -0
- package/dist/commands/which.js +5 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +4 -0
- package/dist/modules/VirtualNetworkManager.d.ts +54 -0
- package/dist/modules/VirtualNetworkManager.js +144 -0
- package/dist/modules/linuxRootfs.d.ts +4 -3
- package/dist/modules/linuxRootfs.js +115 -74
- package/dist/modules/neofetch.d.ts +2 -0
- package/dist/modules/neofetch.js +3 -2
- package/dist/modules/pacmanGame.d.ts +2 -0
- package/dist/modules/pacmanGame.js +1 -0
- package/dist/modules/shellInteractive.d.ts +2 -0
- package/dist/modules/shellInteractive.js +2 -0
- package/dist/modules/shellRuntime.d.ts +7 -0
- package/dist/modules/shellRuntime.js +6 -0
- package/dist/modules/webTermRenderer.js +0 -7
- package/dist/types/commands.d.ts +1 -1
- package/dist/types/vfs.d.ts +8 -0
- package/dist/utils/argv.d.ts +22 -3
- package/dist/utils/argv.js +22 -3
- package/dist/utils/perfLogger.d.ts +10 -2
- package/dist/utils/perfLogger.js +7 -14
- package/dist/utils/shellSession.d.ts +35 -0
- package/dist/utils/shellSession.js +35 -0
- package/package.json +1 -1
|
@@ -29,6 +29,8 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
29
29
|
activeProcesses = new Map();
|
|
30
30
|
nextTty = 0;
|
|
31
31
|
nextPid = 1000;
|
|
32
|
+
nextUid = 1001;
|
|
33
|
+
nextGid = 1001;
|
|
32
34
|
/**
|
|
33
35
|
* Creates a user manager instance backed by a virtual filesystem.
|
|
34
36
|
*
|
|
@@ -59,14 +61,6 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
59
61
|
changed = true;
|
|
60
62
|
}
|
|
61
63
|
this.sudoers.add("root");
|
|
62
|
-
// Auto-create current system user for easier authentication
|
|
63
|
-
// const currentUser = process.env.USER || process.env.USERNAME;
|
|
64
|
-
// if (currentUser && currentUser !== "root" && !this.users.has(currentUser)) {
|
|
65
|
-
// const userPassword = this.defaultRootPassword;
|
|
66
|
-
// this.users.set(currentUser, this.createRecord(currentUser, userPassword));
|
|
67
|
-
// this.sudoers.add(currentUser);
|
|
68
|
-
// changed = true;
|
|
69
|
-
// }
|
|
70
64
|
const homePath = "/root";
|
|
71
65
|
if (!this.vfs.exists(homePath)) {
|
|
72
66
|
this.vfs.mkdir(homePath, 0o755);
|
|
@@ -403,11 +397,19 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
403
397
|
listUsers() {
|
|
404
398
|
return Array.from(this.users.keys()).sort();
|
|
405
399
|
}
|
|
400
|
+
/** Returns the numeric UID for a username, or 0 if unknown. */
|
|
401
|
+
getUid(username) {
|
|
402
|
+
return this.users.get(username)?.uid ?? 0;
|
|
403
|
+
}
|
|
404
|
+
/** Returns the primary GID for a username, or 0 if unknown. */
|
|
405
|
+
getGid(username) {
|
|
406
|
+
return this.users.get(username)?.gid ?? 0;
|
|
407
|
+
}
|
|
406
408
|
/**
|
|
407
409
|
* Registers a running command as a virtual process.
|
|
408
410
|
* Returns the assigned PID so the caller can deregister on completion.
|
|
409
411
|
*/
|
|
410
|
-
registerProcess(username, command, argv, tty) {
|
|
412
|
+
registerProcess(username, command, argv, tty, abortController) {
|
|
411
413
|
const pid = this.nextPid++;
|
|
412
414
|
this.activeProcesses.set(pid, {
|
|
413
415
|
pid,
|
|
@@ -416,6 +418,8 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
416
418
|
argv,
|
|
417
419
|
tty,
|
|
418
420
|
startedAt: new Date().toISOString(),
|
|
421
|
+
status: "running",
|
|
422
|
+
abortController,
|
|
419
423
|
});
|
|
420
424
|
return pid;
|
|
421
425
|
}
|
|
@@ -423,10 +427,27 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
423
427
|
unregisterProcess(pid) {
|
|
424
428
|
this.activeProcesses.delete(pid);
|
|
425
429
|
}
|
|
430
|
+
/** Marks a process as done (keeps it in the table briefly for jobs/ps). */
|
|
431
|
+
markProcessDone(pid) {
|
|
432
|
+
const proc = this.activeProcesses.get(pid);
|
|
433
|
+
if (proc)
|
|
434
|
+
proc.status = "done";
|
|
435
|
+
}
|
|
426
436
|
/** Returns all currently running processes sorted by PID. */
|
|
427
437
|
listProcesses() {
|
|
428
438
|
return Array.from(this.activeProcesses.values()).sort((a, b) => a.pid - b.pid);
|
|
429
439
|
}
|
|
440
|
+
/** Terminate a process by PID. Returns true if the process was found and signalled. */
|
|
441
|
+
killProcess(pid) {
|
|
442
|
+
const proc = this.activeProcesses.get(pid);
|
|
443
|
+
if (!proc)
|
|
444
|
+
return false;
|
|
445
|
+
if (proc.abortController) {
|
|
446
|
+
proc.abortController.abort();
|
|
447
|
+
}
|
|
448
|
+
proc.status = "stopped";
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
430
451
|
loadFromVfs() {
|
|
431
452
|
this.users.clear();
|
|
432
453
|
if (!this.vfs.exists(this.usersPath)) {
|
|
@@ -442,11 +463,23 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
442
463
|
if (parts.length < 3) {
|
|
443
464
|
continue;
|
|
444
465
|
}
|
|
445
|
-
|
|
446
|
-
if (
|
|
447
|
-
|
|
466
|
+
// Format: username:uid:gid:salt:passwordHash (new) or username:salt:passwordHash (legacy)
|
|
467
|
+
if (parts.length >= 5) {
|
|
468
|
+
const [username, uidStr, gidStr, salt, passwordHash] = parts;
|
|
469
|
+
if (!username || !salt || !passwordHash)
|
|
470
|
+
continue;
|
|
471
|
+
const uid = parseInt(uidStr ?? "1001", 10);
|
|
472
|
+
const gid = parseInt(gidStr ?? "1001", 10);
|
|
473
|
+
this.users.set(username, { username, uid, gid, salt, passwordHash });
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
const [username, salt, passwordHash] = parts;
|
|
477
|
+
if (!username || !salt || !passwordHash)
|
|
478
|
+
continue;
|
|
479
|
+
const uid = username === "root" ? 0 : this.nextUid++;
|
|
480
|
+
const gid = username === "root" ? 0 : this.nextGid++;
|
|
481
|
+
this.users.set(username, { username, uid, gid, salt, passwordHash });
|
|
448
482
|
}
|
|
449
|
-
this.users.set(username, { username, salt, passwordHash });
|
|
450
483
|
}
|
|
451
484
|
}
|
|
452
485
|
loadSudoersFromVfs() {
|
|
@@ -487,7 +520,7 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
487
520
|
}
|
|
488
521
|
const authContent = Array.from(this.users.values())
|
|
489
522
|
.sort((left, right) => left.username.localeCompare(right.username))
|
|
490
|
-
.map((record) => [record.username, record.salt, record.passwordHash].join(":"))
|
|
523
|
+
.map((record) => [record.username, record.uid, record.gid, record.salt, record.passwordHash].join(":"))
|
|
491
524
|
.join("\n");
|
|
492
525
|
const sudoersContent = Array.from(this.sudoers.values()).sort().join("\n");
|
|
493
526
|
const quotasContent = Array.from(this.quotas.entries())
|
|
@@ -516,7 +549,9 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
516
549
|
this.vfs.writeFile(targetPath, content, { mode });
|
|
517
550
|
return true;
|
|
518
551
|
}
|
|
519
|
-
createRecord(username, password) {
|
|
552
|
+
createRecord(username, password, uid, gid) {
|
|
553
|
+
const assignedUid = uid ?? (username === "root" ? 0 : this.nextUid++);
|
|
554
|
+
const assignedGid = gid ?? (username === "root" ? 0 : this.nextGid++);
|
|
520
555
|
// Cache key is a hash of the inputs — never store plaintext password in memory
|
|
521
556
|
const cacheKey = createHash("sha256").update(username).update(":").update(password).digest("hex");
|
|
522
557
|
const cached = VirtualUserManager.recordCache.get(cacheKey);
|
|
@@ -526,6 +561,8 @@ export class VirtualUserManager extends EventEmitter {
|
|
|
526
561
|
const salt = randomBytes(16).toString("hex");
|
|
527
562
|
const record = {
|
|
528
563
|
username,
|
|
564
|
+
uid: assignedUid,
|
|
565
|
+
gid: assignedGid,
|
|
529
566
|
salt,
|
|
530
567
|
// Hash uses the generated salt — verifyPassword must use record.salt
|
|
531
568
|
passwordHash: this.hashPassword(password, salt),
|
package/dist/commands/bc.d.ts
CHANGED
package/dist/commands/bc.js
CHANGED
package/dist/commands/cat.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ifFlag } from "./command-helpers";
|
|
2
|
-
import {
|
|
2
|
+
import { checkFilePermission, resolveReadablePath } from "./helpers";
|
|
3
3
|
/**
|
|
4
4
|
* Concatenate and print files to stdout.
|
|
5
5
|
* @category files
|
|
@@ -23,7 +23,7 @@ export const catCommand = {
|
|
|
23
23
|
const parts = [];
|
|
24
24
|
for (const fileArg of fileArgs) {
|
|
25
25
|
const target = resolveReadablePath(shell.vfs, cwd, fileArg);
|
|
26
|
-
|
|
26
|
+
checkFilePermission(shell.vfs, shell.users, authUser, target, 4);
|
|
27
27
|
parts.push(shell.vfs.readFile(target));
|
|
28
28
|
}
|
|
29
29
|
const combined = parts.join("");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
2
|
+
/**
|
|
3
|
+
* Change group ownership.
|
|
4
|
+
* @category files
|
|
5
|
+
* @params ["<group> <file>"]
|
|
6
|
+
*/
|
|
7
|
+
export const chgrpCommand = {
|
|
8
|
+
name: "chgrp",
|
|
9
|
+
description: "Change group ownership",
|
|
10
|
+
category: "files",
|
|
11
|
+
params: ["<group> <file>"],
|
|
12
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
13
|
+
const [groupArg, fileArg] = args;
|
|
14
|
+
if (!groupArg || !fileArg) {
|
|
15
|
+
return { stderr: "chgrp: missing operand", exitCode: 1 };
|
|
16
|
+
}
|
|
17
|
+
if (authUser !== "root") {
|
|
18
|
+
return { stderr: "chgrp: changing group: Operation not permitted", exitCode: 1 };
|
|
19
|
+
}
|
|
20
|
+
const filePath = resolvePath(cwd, fileArg);
|
|
21
|
+
try {
|
|
22
|
+
assertPathAccess(authUser, filePath, "chgrp");
|
|
23
|
+
if (!shell.vfs.exists(filePath)) {
|
|
24
|
+
return {
|
|
25
|
+
stderr: `chgrp: ${fileArg}: No such file or directory`,
|
|
26
|
+
exitCode: 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const gid = parseInt(groupArg, 10);
|
|
30
|
+
if (Number.isNaN(gid)) {
|
|
31
|
+
return { stderr: `chgrp: invalid group: ${groupArg}`, exitCode: 1 };
|
|
32
|
+
}
|
|
33
|
+
const current = shell.vfs.getOwner(filePath);
|
|
34
|
+
shell.vfs.chown(filePath, current.uid, gid);
|
|
35
|
+
return { exitCode: 0 };
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return { stderr: `chgrp: ${msg}`, exitCode: 1 };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { assertPathAccess, resolvePath } from "./helpers";
|
|
2
|
+
/**
|
|
3
|
+
* Change file owner and group.
|
|
4
|
+
* @category files
|
|
5
|
+
* @params ["<owner>[:<group>] <file>"]
|
|
6
|
+
*/
|
|
7
|
+
export const chownCommand = {
|
|
8
|
+
name: "chown",
|
|
9
|
+
description: "Change file owner and group",
|
|
10
|
+
category: "files",
|
|
11
|
+
params: ["<owner>[:<group>] <file>"],
|
|
12
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
13
|
+
const [ownerArg, fileArg] = args;
|
|
14
|
+
if (!ownerArg || !fileArg) {
|
|
15
|
+
return { stderr: "chown: missing operand", exitCode: 1 };
|
|
16
|
+
}
|
|
17
|
+
if (authUser !== "root") {
|
|
18
|
+
return { stderr: "chown: changing ownership: Operation not permitted", exitCode: 1 };
|
|
19
|
+
}
|
|
20
|
+
const filePath = resolvePath(cwd, fileArg);
|
|
21
|
+
try {
|
|
22
|
+
assertPathAccess(authUser, filePath, "chown");
|
|
23
|
+
if (!shell.vfs.exists(filePath)) {
|
|
24
|
+
return {
|
|
25
|
+
stderr: `chown: ${fileArg}: No such file or directory`,
|
|
26
|
+
exitCode: 1,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let uid = null;
|
|
30
|
+
let gid = null;
|
|
31
|
+
const colonIdx = ownerArg.indexOf(":");
|
|
32
|
+
if (colonIdx === -1) {
|
|
33
|
+
// Just a user name
|
|
34
|
+
uid = resolveUser(shell, ownerArg);
|
|
35
|
+
if (uid === null) {
|
|
36
|
+
return { stderr: `chown: invalid user: ${ownerArg}`, exitCode: 1 };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const userPart = ownerArg.slice(0, colonIdx);
|
|
41
|
+
const groupPart = ownerArg.slice(colonIdx + 1);
|
|
42
|
+
if (userPart) {
|
|
43
|
+
uid = resolveUser(shell, userPart);
|
|
44
|
+
if (uid === null) {
|
|
45
|
+
return { stderr: `chown: invalid user: ${userPart}`, exitCode: 1 };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (groupPart) {
|
|
49
|
+
gid = resolveGroup(shell, groupPart);
|
|
50
|
+
if (gid === null) {
|
|
51
|
+
return { stderr: `chown: invalid group: ${groupPart}`, exitCode: 1 };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const current = shell.vfs.getOwner(filePath);
|
|
56
|
+
shell.vfs.chown(filePath, uid ?? current.uid, gid ?? current.gid);
|
|
57
|
+
return { exitCode: 0 };
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
61
|
+
return { stderr: `chown: ${msg}`, exitCode: 1 };
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
function resolveUser(shell, name) {
|
|
66
|
+
const users = shell.users.listUsers();
|
|
67
|
+
if (users.includes(name))
|
|
68
|
+
return shell.users.getUid(name);
|
|
69
|
+
const num = parseInt(name, 10);
|
|
70
|
+
if (!Number.isNaN(num))
|
|
71
|
+
return num;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function resolveGroup(_shell, name) {
|
|
75
|
+
const num = parseInt(name, 10);
|
|
76
|
+
if (!Number.isNaN(num))
|
|
77
|
+
return num;
|
|
78
|
+
return 0; // fallback: root group
|
|
79
|
+
}
|
package/dist/commands/cp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import { ifFlag } from "./command-helpers";
|
|
2
|
-
import {
|
|
3
|
+
import { checkFilePermission, resolvePath } from "./helpers";
|
|
3
4
|
/**
|
|
4
5
|
* Copy files or directories inside the virtual filesystem.
|
|
5
6
|
* @category files
|
|
@@ -20,8 +21,8 @@ export const cpCommand = {
|
|
|
20
21
|
const srcPath = resolvePath(cwd, srcArg);
|
|
21
22
|
const destPath = resolvePath(cwd, destArg);
|
|
22
23
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
checkFilePermission(shell.vfs, shell.users, authUser, srcPath, 4);
|
|
25
|
+
checkFilePermission(shell.vfs, shell.users, authUser, path.posix.dirname(destPath), 2);
|
|
25
26
|
if (!shell.vfs.exists(srcPath)) {
|
|
26
27
|
return {
|
|
27
28
|
stderr: `cp: ${srcArg}: No such file or directory`,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { resolvePath } from "./helpers";
|
|
2
|
+
/**
|
|
3
|
+
* Convert and copy a file
|
|
4
|
+
* @category files
|
|
5
|
+
* @params ["if=<file> of=<file> [bs=1024] [count=N]"]
|
|
6
|
+
*/
|
|
7
|
+
export const ddCommand = {
|
|
8
|
+
name: "dd",
|
|
9
|
+
description: "Convert and copy a file",
|
|
10
|
+
category: "files",
|
|
11
|
+
params: ["if=<file> of=<file> [bs=1024] [count=N]"],
|
|
12
|
+
run: ({ authUser, shell, cwd, args }) => {
|
|
13
|
+
const kv = {};
|
|
14
|
+
for (const arg of args) {
|
|
15
|
+
const eqIdx = arg.indexOf("=");
|
|
16
|
+
if (eqIdx !== -1) {
|
|
17
|
+
kv[arg.slice(0, eqIdx)] = arg.slice(eqIdx + 1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const ifPath = kv.if ? resolvePath(cwd, kv.if) : undefined;
|
|
21
|
+
const ofPath = kv.of ? resolvePath(cwd, kv.of) : undefined;
|
|
22
|
+
if (!ifPath || !ofPath) {
|
|
23
|
+
return { stderr: "dd: missing 'if' or 'of' operand\n", exitCode: 1 };
|
|
24
|
+
}
|
|
25
|
+
if (!shell.vfs.exists(ifPath)) {
|
|
26
|
+
return { stderr: `dd: ${kv.if}: No such file or directory\n`, exitCode: 1 };
|
|
27
|
+
}
|
|
28
|
+
const bs = parseInt(kv.bs || "512", 10);
|
|
29
|
+
const content = shell.vfs.readFile(ifPath);
|
|
30
|
+
const skipBlocks = parseInt(kv.skip || "0", 10);
|
|
31
|
+
const seekBlocks = parseInt(kv.seek || "0", 10);
|
|
32
|
+
const countBlocks = kv.count !== undefined ? parseInt(kv.count, 10) : undefined;
|
|
33
|
+
const startByte = skipBlocks * bs;
|
|
34
|
+
const available = content.slice(startByte);
|
|
35
|
+
const totalBytes = countBlocks !== undefined
|
|
36
|
+
? Math.min(available.length, countBlocks * bs)
|
|
37
|
+
: available.length;
|
|
38
|
+
const data = available.slice(0, totalBytes);
|
|
39
|
+
let output;
|
|
40
|
+
try {
|
|
41
|
+
output = shell.vfs.readFile(ofPath);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
output = "";
|
|
45
|
+
}
|
|
46
|
+
const seekByte = seekBlocks * bs;
|
|
47
|
+
if (seekByte > 0) {
|
|
48
|
+
if (output.length < seekByte) {
|
|
49
|
+
output = output.padEnd(seekByte, "\0");
|
|
50
|
+
}
|
|
51
|
+
output = output.slice(0, seekByte) + data + output.slice(seekByte + data.length);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
output = data;
|
|
55
|
+
}
|
|
56
|
+
shell.writeFileAsUser(authUser, ofPath, output);
|
|
57
|
+
const recordsIn = Math.ceil(data.length / bs);
|
|
58
|
+
return { stdout: `${recordsIn}+0 records in\n${recordsIn}+0 records out\n`, exitCode: 0 };
|
|
59
|
+
},
|
|
60
|
+
};
|
package/dist/commands/declare.js
CHANGED
|
@@ -14,8 +14,6 @@ export const declareCommand = {
|
|
|
14
14
|
if (!env)
|
|
15
15
|
return { exitCode: 0 };
|
|
16
16
|
const integer = ifFlag(args, ["-i"]);
|
|
17
|
-
const _readonly = ifFlag(args, ["-r"]);
|
|
18
|
-
const _export_ = ifFlag(args, ["-x"]);
|
|
19
17
|
const printAll = args.filter((a) => !a.startsWith("-")).length === 0;
|
|
20
18
|
if (printAll) {
|
|
21
19
|
const lines = Object.entries(env.vars).map(([k, v]) => `declare -- ${k}="${v}"`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate expressions
|
|
3
|
+
* @category shell
|
|
4
|
+
* @params ["<expression>"]
|
|
5
|
+
*/
|
|
6
|
+
export const exprCommand = {
|
|
7
|
+
name: "expr",
|
|
8
|
+
description: "Evaluate expressions",
|
|
9
|
+
category: "shell",
|
|
10
|
+
params: ["<expression>"],
|
|
11
|
+
run: ({ args }) => {
|
|
12
|
+
const colonIdx = args.indexOf(":");
|
|
13
|
+
if (colonIdx > 0 && colonIdx <= args.length - 2) {
|
|
14
|
+
const str = args[colonIdx - 1];
|
|
15
|
+
const pattern = args[colonIdx + 1];
|
|
16
|
+
try {
|
|
17
|
+
const re = new RegExp(pattern);
|
|
18
|
+
const match = str.match(re);
|
|
19
|
+
if (match && match.index !== undefined) {
|
|
20
|
+
return { stdout: `${match[0].length}\n`, exitCode: 0 };
|
|
21
|
+
}
|
|
22
|
+
return { stdout: "0\n", exitCode: 1 };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return { stderr: "expr: invalid regex\n", exitCode: 2 };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (args.length >= 3) {
|
|
29
|
+
const left = parseInt(args[0], 10);
|
|
30
|
+
const op = args[1];
|
|
31
|
+
const right = parseInt(args[2], 10);
|
|
32
|
+
if (isNaN(left) || isNaN(right)) {
|
|
33
|
+
return { stderr: "expr: non-integer argument\n", exitCode: 1 };
|
|
34
|
+
}
|
|
35
|
+
let result;
|
|
36
|
+
switch (op) {
|
|
37
|
+
case "+":
|
|
38
|
+
result = left + right;
|
|
39
|
+
break;
|
|
40
|
+
case "-":
|
|
41
|
+
result = left - right;
|
|
42
|
+
break;
|
|
43
|
+
case "*":
|
|
44
|
+
result = left * right;
|
|
45
|
+
break;
|
|
46
|
+
case "/":
|
|
47
|
+
if (right === 0)
|
|
48
|
+
return { stderr: "expr: division by zero\n", exitCode: 2 };
|
|
49
|
+
result = Math.trunc(left / right);
|
|
50
|
+
break;
|
|
51
|
+
case "%":
|
|
52
|
+
if (right === 0)
|
|
53
|
+
return { stderr: "expr: division by zero\n", exitCode: 2 };
|
|
54
|
+
result = left % right;
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
return { stderr: "expr: syntax error\n", exitCode: 2 };
|
|
58
|
+
}
|
|
59
|
+
return { stdout: `${result}\n`, exitCode: 0 };
|
|
60
|
+
}
|
|
61
|
+
return { stderr: "expr: syntax error\n", exitCode: 2 };
|
|
62
|
+
},
|
|
63
|
+
};
|
package/dist/commands/fun.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export declare const fortuneCommand: ShellModule;
|
|
|
17
17
|
* @params ["[message]"]
|
|
18
18
|
*/
|
|
19
19
|
export declare const cowsayCommand: ShellModule;
|
|
20
|
+
/**
|
|
21
|
+
* Generate ASCII cow thinking.
|
|
22
|
+
* @category misc
|
|
23
|
+
* @params ["[message]"]
|
|
24
|
+
*/
|
|
20
25
|
export declare const cowthinkCommand: ShellModule;
|
|
21
26
|
/**
|
|
22
27
|
* Show falling characters in the terminal like the Matrix movie.
|
package/dist/commands/fun.js
CHANGED
|
@@ -57,7 +57,6 @@ export const fortuneCommand = {
|
|
|
57
57
|
function cowsay(message, dead = false) {
|
|
58
58
|
const lines = message.split("\n");
|
|
59
59
|
const maxLen = Math.max(...lines.map(l => l.length));
|
|
60
|
-
const _border = "-".repeat(maxLen + 2);
|
|
61
60
|
const body = lines.length === 1
|
|
62
61
|
? `< ${lines[0]} >`
|
|
63
62
|
: lines.map((l, i) => {
|
|
@@ -95,6 +94,11 @@ export const cowsayCommand = {
|
|
|
95
94
|
return { stdout: cowsay(msg), exitCode: 0 };
|
|
96
95
|
},
|
|
97
96
|
};
|
|
97
|
+
/**
|
|
98
|
+
* Generate ASCII cow thinking.
|
|
99
|
+
* @category misc
|
|
100
|
+
* @params ["[message]"]
|
|
101
|
+
*/
|
|
98
102
|
export const cowthinkCommand = {
|
|
99
103
|
name: "cowthink",
|
|
100
104
|
description: "Generate ASCII cow thinking",
|
package/dist/commands/help.d.ts
CHANGED
package/dist/commands/help.js
CHANGED
|
@@ -98,6 +98,11 @@ function renderDetail(mod) {
|
|
|
98
98
|
return lines.join("\n");
|
|
99
99
|
}
|
|
100
100
|
// ─── export ───────────────────────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* List all commands, or show usage for a specific command.
|
|
103
|
+
* @category shell
|
|
104
|
+
* @params ["[command]"]
|
|
105
|
+
*/
|
|
101
106
|
export function createHelpCommand(_getNames) {
|
|
102
107
|
return {
|
|
103
108
|
name: "help",
|
|
@@ -1,8 +1,51 @@
|
|
|
1
1
|
import type VirtualFileSystem from "../VirtualFileSystem";
|
|
2
2
|
import type { VirtualPackageManager } from "../VirtualPackageManager";
|
|
3
3
|
import type { VirtualShell } from "../VirtualShell";
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a path string against the virtual file system.
|
|
6
|
+
* Supports `~` as shorthand for the home directory. If `inputPath` is
|
|
7
|
+
* absolute it is returned as-is; otherwise it is joined to `cwd`.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd - The current working directory
|
|
10
|
+
* @param inputPath - The path string to resolve
|
|
11
|
+
* @param homeDir - The home directory to use for `~` expansion (defaults to `/root`)
|
|
12
|
+
* @returns The normalized absolute path
|
|
13
|
+
*/
|
|
4
14
|
export declare function resolvePath(cwd: string, inputPath: string, homeDir?: string): string;
|
|
5
15
|
export declare function assertPathAccess(authUser: string, targetPath: string, operation: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Strips the filename from a URL path, extracting the last path segment.
|
|
18
|
+
* Removes query strings and fragments first. Returns `"index.html"` when
|
|
19
|
+
* no segment is found.
|
|
20
|
+
*
|
|
21
|
+
* @param url - The URL or path string to process
|
|
22
|
+
* @returns The extracted filename, or "index.html" as a fallback
|
|
23
|
+
*/
|
|
6
24
|
export declare function stripUrlFilename(url: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves a path with readable error messages by attempting an exact match
|
|
27
|
+
* first, then falling back to case-insensitive matching, and finally to a
|
|
28
|
+
* Levenshtein-distance (≤1) fuzzy match against sibling entries.
|
|
29
|
+
*
|
|
30
|
+
* @param vfs - The virtual file system instance
|
|
31
|
+
* @param cwd - The current working directory
|
|
32
|
+
* @param inputPath - The path string to resolve
|
|
33
|
+
* @returns The best matching path (exact, case-insensitive, or fuzzy)
|
|
34
|
+
*/
|
|
7
35
|
export declare function resolveReadablePath(vfs: VirtualFileSystem, cwd: string, inputPath: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Returns the active package manager associated with the given shell.
|
|
38
|
+
*
|
|
39
|
+
* @param shell - The VirtualShell instance
|
|
40
|
+
* @returns The VirtualPackageManager, or undefined if none is configured
|
|
41
|
+
*/
|
|
8
42
|
export declare function getPackageManager(shell: VirtualShell): VirtualPackageManager | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* POSIX-style permission check: does `authUser` have `want` access to `filePath`?
|
|
45
|
+
* `want` is a bitmask: 4=R_OK, 2=W_OK, 1=X_OK, 0=F_OK (check existence).
|
|
46
|
+
* Root bypasses all checks. Throws on EACCES.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkFilePermission(vfs: VirtualFileSystem, users: {
|
|
49
|
+
getUid: (u: string) => number;
|
|
50
|
+
getGid: (u: string) => number;
|
|
51
|
+
}, authUser: string, filePath: string, want: number): void;
|