- FAT32 VFS driver (fat32.h, fat32.c): reads BPB, FAT table, cluster chains, directory entries (8.3 SFN), file read/write, mount via fat32_mount_device() - mount app: writes 'device mountpoint' to /sys/vfs/mount to trigger FAT32 mount from userspace - VFS sysfs mount namespace in kernel.c: handles mount requests via sysfs write to /sys/vfs/mount, delegates to FAT32 driver - Shell cd: validates target directory exists before updating CWD, resolves relative paths (., .., components) to absolute paths - Shell run_command: resolves ARG1 paths relative to CWD so apps like cat and ls receive absolute paths automatically - Fixed FAT32 root directory access: use fs->root_cluster when VFS mount root node has inode=0
366 lines
10 KiB
C
366 lines
10 KiB
C
/**
|
|
* @file sh.c
|
|
* @brief ClaudeOS shell.
|
|
*
|
|
* A simple interactive shell that reads commands from stdin,
|
|
* supports built-in commands (cd, exit, help, env), and
|
|
* executes external programs via fork+exec.
|
|
*/
|
|
|
|
#include "syscalls.h"
|
|
|
|
/** Maximum command line length. */
|
|
#define CMD_MAX 256
|
|
|
|
/** Read a line from stdin with echo and basic line editing.
|
|
* Returns length of the line (excluding newline). */
|
|
static int readline(char *buf, int maxlen) {
|
|
int pos = 0;
|
|
while (pos < maxlen - 1) {
|
|
char c;
|
|
int n = read(0, &c, 1);
|
|
if (n <= 0) {
|
|
/* No data yet: yield CPU and retry */
|
|
yield();
|
|
continue;
|
|
}
|
|
|
|
if (c == '\n' || c == '\r') {
|
|
putchar('\n');
|
|
break;
|
|
} else if (c == '\b' || c == 127) {
|
|
/* Backspace */
|
|
if (pos > 0) {
|
|
pos--;
|
|
puts("\b \b"); /* Move back, overwrite with space, move back */
|
|
}
|
|
} else if (c >= 32) {
|
|
/* Printable character */
|
|
buf[pos++] = c;
|
|
putchar(c);
|
|
}
|
|
}
|
|
buf[pos] = '\0';
|
|
return pos;
|
|
}
|
|
|
|
/** Skip leading whitespace, return pointer to first non-space. */
|
|
static char *skip_spaces(char *s) {
|
|
while (*s == ' ' || *s == '\t') s++;
|
|
return s;
|
|
}
|
|
|
|
/** Find the next space or end of string. */
|
|
static char *find_space(char *s) {
|
|
while (*s && *s != ' ' && *s != '\t') s++;
|
|
return s;
|
|
}
|
|
|
|
/** String length. */
|
|
static int slen(const char *s) {
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
return n;
|
|
}
|
|
|
|
/** Resolve an absolute path from cwd + a possibly relative path.
|
|
* Handles '..', '.', and trailing slashes. Result is always absolute. */
|
|
static void resolve_path(const char *cwd, const char *arg, char *out, int out_sz) {
|
|
char tmp[256];
|
|
int ti = 0;
|
|
|
|
/* If arg is absolute, start from it; otherwise prepend cwd */
|
|
if (arg[0] == '/') {
|
|
/* copy arg into tmp */
|
|
for (int i = 0; arg[i] && ti < 255; i++)
|
|
tmp[ti++] = arg[i];
|
|
} else {
|
|
/* copy cwd */
|
|
for (int i = 0; cwd[i] && ti < 255; i++)
|
|
tmp[ti++] = cwd[i];
|
|
/* ensure separator */
|
|
if (ti > 0 && tmp[ti - 1] != '/' && ti < 255)
|
|
tmp[ti++] = '/';
|
|
/* append arg */
|
|
for (int i = 0; arg[i] && ti < 255; i++)
|
|
tmp[ti++] = arg[i];
|
|
}
|
|
tmp[ti] = '\0';
|
|
|
|
/* Now canonicalize: split on '/' and process each component */
|
|
/* Stack of component start offsets */
|
|
int comp_starts[64];
|
|
int comp_lens[64];
|
|
int depth = 0;
|
|
|
|
int i = 0;
|
|
while (tmp[i]) {
|
|
/* skip slashes */
|
|
while (tmp[i] == '/') i++;
|
|
if (!tmp[i]) break;
|
|
|
|
/* find end of component */
|
|
int start = i;
|
|
while (tmp[i] && tmp[i] != '/') i++;
|
|
int len = i - start;
|
|
|
|
if (len == 1 && tmp[start] == '.') {
|
|
/* current dir: skip */
|
|
continue;
|
|
} else if (len == 2 && tmp[start] == '.' && tmp[start + 1] == '.') {
|
|
/* parent dir: pop */
|
|
if (depth > 0) depth--;
|
|
} else {
|
|
/* normal component: push */
|
|
if (depth < 64) {
|
|
comp_starts[depth] = start;
|
|
comp_lens[depth] = len;
|
|
depth++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Build output */
|
|
int oi = 0;
|
|
if (depth == 0) {
|
|
if (oi < out_sz - 1) out[oi++] = '/';
|
|
} else {
|
|
for (int d = 0; d < depth && oi < out_sz - 2; d++) {
|
|
out[oi++] = '/';
|
|
for (int j = 0; j < comp_lens[d] && oi < out_sz - 1; j++)
|
|
out[oi++] = tmp[comp_starts[d] + j];
|
|
}
|
|
}
|
|
out[oi] = '\0';
|
|
}
|
|
|
|
/** Built-in: cd <path> */
|
|
static void builtin_cd(char *arg) {
|
|
if (!arg || !*arg) {
|
|
arg = "/";
|
|
}
|
|
|
|
/* Get current working directory */
|
|
char cwd[128];
|
|
if (getenv("CWD", cwd, sizeof(cwd)) < 0) {
|
|
strcpy(cwd, "/");
|
|
}
|
|
|
|
/* Resolve the path (handles relative, .., etc.) */
|
|
char resolved[256];
|
|
resolve_path(cwd, arg, resolved, sizeof(resolved));
|
|
|
|
/* Validate: check if the directory exists by trying to readdir it.
|
|
* Root "/" always exists. For others, readdir index 0 must not fail
|
|
* with -1 unless the dir is simply empty, so we also accept stat-like
|
|
* checks. A simple heuristic: if readdir returns -1 for index 0 AND
|
|
* the path is not "/", we check if the *parent* can list it. */
|
|
if (resolved[0] == '/' && resolved[1] != '\0') {
|
|
/* Try listing the first entry; if the path is a valid directory,
|
|
* readdir(path, 0, ...) returns >= 0 even for an empty dir
|
|
* (it returns -1 meaning "end"), BUT if the path doesn't exist
|
|
* at all, the VFS resolve_path will fail and readdir returns -1.
|
|
*
|
|
* Better approach: try to readdir the parent and look for this
|
|
* entry among its children. */
|
|
char name[128];
|
|
int32_t type = readdir(resolved, 0, name);
|
|
/* type >= 0 means there's at least one entry => directory exists.
|
|
* type == -1 could mean empty dir or non-existent.
|
|
* For empty dirs: the directory itself was resolved successfully.
|
|
* For non-existent: the VFS resolution failed.
|
|
*
|
|
* Our VFS readdir calls resolve_path internally. If the path
|
|
* doesn't exist, it returns -1. If it exists but is empty,
|
|
* it also returns -1. We need to distinguish these.
|
|
* Use a stat-like approach: try to open the path. */
|
|
|
|
/* Actually the simplest check: try readdir on the *parent* and
|
|
* see if our target component exists as a directory entry. */
|
|
char parent[256];
|
|
char target[128];
|
|
|
|
/* Split resolved into parent + target */
|
|
int rlen = slen(resolved);
|
|
int last_slash = 0;
|
|
for (int i = 0; i < rlen; i++) {
|
|
if (resolved[i] == '/') last_slash = i;
|
|
}
|
|
|
|
if (last_slash == 0) {
|
|
/* Parent is root */
|
|
parent[0] = '/';
|
|
parent[1] = '\0';
|
|
strcpy(target, resolved + 1);
|
|
} else {
|
|
memcpy(parent, resolved, (uint32_t)last_slash);
|
|
parent[last_slash] = '\0';
|
|
strcpy(target, resolved + last_slash + 1);
|
|
}
|
|
|
|
/* Search parent directory for target */
|
|
int found = 0;
|
|
uint32_t idx = 0;
|
|
int32_t etype;
|
|
while ((etype = readdir(parent, idx, name)) >= 0) {
|
|
if (strcmp(name, target) == 0) {
|
|
if (etype == 2) {
|
|
found = 1; /* It's a directory */
|
|
} else {
|
|
puts("cd: ");
|
|
puts(resolved);
|
|
puts(": not a directory\n");
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
idx++;
|
|
}
|
|
|
|
if (!found) {
|
|
puts("cd: ");
|
|
puts(resolved);
|
|
puts(": no such directory\n");
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Update CWD environment variable */
|
|
setenv("CWD", resolved);
|
|
}
|
|
|
|
/** Built-in: env - print all known env vars. */
|
|
static void builtin_env(void) {
|
|
char buf[128];
|
|
if (getenv("CWD", buf, sizeof(buf)) >= 0) {
|
|
puts("CWD=");
|
|
puts(buf);
|
|
putchar('\n');
|
|
}
|
|
if (getenv("PATH", buf, sizeof(buf)) >= 0) {
|
|
puts("PATH=");
|
|
puts(buf);
|
|
putchar('\n');
|
|
}
|
|
if (getenv("TEST", buf, sizeof(buf)) >= 0) {
|
|
puts("TEST=");
|
|
puts(buf);
|
|
putchar('\n');
|
|
}
|
|
}
|
|
|
|
/** Built-in: help */
|
|
static void builtin_help(void) {
|
|
puts("ClaudeOS Shell\n");
|
|
puts("Built-in commands:\n");
|
|
puts(" cd <path> - change working directory\n");
|
|
puts(" env - show environment variables\n");
|
|
puts(" help - show this message\n");
|
|
puts(" exit - exit the shell\n");
|
|
puts("External commands are loaded from initrd.\n");
|
|
}
|
|
|
|
/** Check if a string looks like a path (contains / or starts with .) */
|
|
static int looks_like_path(const char *s) {
|
|
if (!s || !*s) return 0;
|
|
if (s[0] == '.' || s[0] == '/') return 1;
|
|
for (int i = 0; s[i]; i++) {
|
|
if (s[i] == '/') return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/** Execute an external command via fork+exec. */
|
|
static void run_command(const char *cmd, const char *arg) {
|
|
int32_t pid = fork();
|
|
if (pid < 0) {
|
|
puts("sh: fork failed\n");
|
|
return;
|
|
}
|
|
|
|
if (pid == 0) {
|
|
/* Child: set ARG1 if there's an argument */
|
|
if (arg && *arg) {
|
|
/* If the argument looks like a path and isn't absolute,
|
|
* resolve it relative to CWD so apps get absolute paths */
|
|
if (looks_like_path(arg) && arg[0] != '/') {
|
|
char cwd[128];
|
|
if (getenv("CWD", cwd, sizeof(cwd)) < 0) {
|
|
strcpy(cwd, "/");
|
|
}
|
|
char resolved_arg[256];
|
|
resolve_path(cwd, arg, resolved_arg, sizeof(resolved_arg));
|
|
setenv("ARG1", resolved_arg);
|
|
} else {
|
|
setenv("ARG1", arg);
|
|
}
|
|
} else {
|
|
setenv("ARG1", "");
|
|
}
|
|
|
|
/* exec the command */
|
|
int32_t ret = exec(cmd);
|
|
if (ret < 0) {
|
|
puts("sh: ");
|
|
puts(cmd);
|
|
puts(": not found\n");
|
|
exit(127);
|
|
}
|
|
/* exec doesn't return on success */
|
|
exit(1);
|
|
}
|
|
|
|
/* Parent: wait for child */
|
|
waitpid(pid);
|
|
}
|
|
|
|
int main(void) {
|
|
puts("ClaudeOS Shell v0.1\n");
|
|
puts("Type 'help' for available commands.\n\n");
|
|
|
|
char cmd[CMD_MAX];
|
|
|
|
for (;;) {
|
|
/* Print prompt with CWD */
|
|
char cwd[128];
|
|
if (getenv("CWD", cwd, sizeof(cwd)) < 0) {
|
|
strcpy(cwd, "/");
|
|
}
|
|
puts(cwd);
|
|
puts("$ ");
|
|
|
|
/* Read command */
|
|
int len = readline(cmd, CMD_MAX);
|
|
if (len == 0) continue;
|
|
|
|
/* Parse command and arguments */
|
|
char *line = skip_spaces(cmd);
|
|
if (*line == '\0') continue;
|
|
|
|
char *arg_start = find_space(line);
|
|
char *arg = (char *)0;
|
|
if (*arg_start) {
|
|
*arg_start = '\0';
|
|
arg = skip_spaces(arg_start + 1);
|
|
if (*arg == '\0') arg = (char *)0;
|
|
}
|
|
|
|
/* Check built-in commands */
|
|
if (strcmp(line, "exit") == 0) {
|
|
puts("Goodbye!\n");
|
|
exit(0);
|
|
} else if (strcmp(line, "cd") == 0) {
|
|
builtin_cd(arg);
|
|
} else if (strcmp(line, "env") == 0) {
|
|
builtin_env();
|
|
} else if (strcmp(line, "help") == 0) {
|
|
builtin_help();
|
|
} else {
|
|
/* External command */
|
|
run_command(line, arg);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|