feat: real stat metadata across all filesystems, plus mkdir and rmdir

This commit is contained in:
2026-08-04 13:03:08 +02:00
parent ca5331c9db
commit edc3452d61
14 changed files with 557 additions and 25 deletions
+155
View File
@@ -0,0 +1,155 @@
/*
* main.cpp
* mkdir - command to create directories
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
// The kernel resolves paths into a 256-byte buffer, so anything longer is
// rejected up front rather than acted on after silent truncation.
static constexpr int MaxPath = 256;
static int g_failures = 0;
static void report(const char* path, const char* reason) {
montauk::print("mkdir: ");
montauk::print(path);
montauk::print(": ");
montauk::print(reason);
montauk::putchar('\n');
g_failures++;
}
static void usage() {
montauk::print("usage: mkdir [-p] <directory> [directory ...]\n");
montauk::print(" -p create missing parent directories, and succeed\n");
montauk::print(" when the directory already exists\n");
}
// Copy the next space-delimited token out of *p, advancing *p past it.
// Returns false once the input is exhausted.
static bool next_token(const char** p, char* out, int outMax) {
const char* s = montauk::skip_spaces(*p);
if (*s == '\0') {
*p = s;
return false;
}
int n = 0;
while (*s != '\0' && *s != ' ') {
if (n < outMax - 1) out[n++] = *s;
s++;
}
out[n] = '\0';
*p = s;
return true;
}
// Length of a leading drive prefix ("0:", "12:"), or 0 when there is none.
// The prefix names the root, which always exists and is never created.
static int drive_prefix_len(const char* s) {
int i = 0;
while (s[i] >= '0' && s[i] <= '9') i++;
return (i > 0 && s[i] == ':') ? i + 1 : 0;
}
static bool path_exists(const char* path, bool& isDir) {
montauk::abi::FileStat st;
if (montauk::stat(path, &st) < 0) return false;
isDir = st.isDir != 0;
return true;
}
// Create every missing component of the path. fmkdir already succeeds on a
// directory that exists, so each prefix can be created unconditionally; it
// fails only when a component is in the way as a regular file.
static bool make_with_parents(const char* path) {
char partial[MaxPath];
int i = drive_prefix_len(path);
// Leading slashes belong to the root, not to any component.
while (path[i] == '/') i++;
for (int k = 0; k < i; k++) partial[k] = path[k];
int n = i;
while (path[i] != '\0') {
while (path[i] != '\0' && path[i] != '/') partial[n++] = path[i++];
partial[n] = '\0';
// Collapse repeated separators; a trailing slash ends the path.
while (path[i] == '/') i++;
if (montauk::fmkdir(partial) < 0) {
report(partial, "cannot create directory");
return false;
}
if (path[i] != '\0') partial[n++] = '/';
}
return true;
}
static void make_dir(const char* path, bool parents) {
if (montauk::slen(path) >= MaxPath) {
report(path, "path too long");
return;
}
bool isDir = false;
if (path_exists(path, isDir)) {
// -p treats an existing directory as success, but never a file.
if (parents && isDir) return;
report(path, isDir ? "directory already exists" : "file already exists");
return;
}
if (parents) {
make_with_parents(path);
return;
}
if (montauk::fmkdir(path) < 0) {
report(path, "cannot create directory");
}
}
extern "C" void _start() {
char args[1024];
montauk::getargs(args, sizeof(args));
const char* p = args;
// Sized past MaxPath so an over-long operand survives tokenizing intact
// and is caught by the length check instead of being truncated into a
// different, possibly valid, path.
char tok[MaxPath + 2];
bool parents = false;
int operands = 0;
while (next_token(&p, tok, sizeof(tok))) {
// Options are recognized only before the first operand, so a
// directory named "-p" stays reachable once one has been given.
if (operands == 0 && tok[0] == '-' && tok[1] != '\0') {
if (montauk::streq(tok, "-p")) {
parents = true;
continue;
}
montauk::print("mkdir: unknown option: ");
montauk::print(tok);
montauk::putchar('\n');
usage();
montauk::exit(2);
}
operands++;
make_dir(tok, parents);
}
if (operands == 0) {
usage();
montauk::exit(2);
}
montauk::exit(g_failures > 0 ? 1 : 0);
}
+169
View File
@@ -0,0 +1,169 @@
/*
* main.cpp
* rmdir - command to remove empty directories
* Copyright (c) 2026 Daniel Hammer
*/
#include <montauk/syscall.h>
#include <montauk/string.h>
// The kernel resolves paths into a 256-byte buffer, so anything longer is
// rejected up front rather than acted on after silent truncation.
static constexpr int MaxPath = 256;
static int g_failures = 0;
static void report(const char* path, const char* reason) {
montauk::print("rmdir: ");
montauk::print(path);
montauk::print(": ");
montauk::print(reason);
montauk::putchar('\n');
g_failures++;
}
static void usage() {
montauk::print("usage: rmdir [-p] <directory> [directory ...]\n");
montauk::print(" -p also remove each parent directory that becomes\n");
montauk::print(" empty, stopping at the first one that does not\n");
}
// Copy the next space-delimited token out of *p, advancing *p past it.
// Returns false once the input is exhausted.
static bool next_token(const char** p, char* out, int outMax) {
const char* s = montauk::skip_spaces(*p);
if (*s == '\0') {
*p = s;
return false;
}
int n = 0;
while (*s != '\0' && *s != ' ') {
if (n < outMax - 1) out[n++] = *s;
s++;
}
out[n] = '\0';
*p = s;
return true;
}
// Length of a leading drive prefix ("0:", "12:"), or 0 when there is none.
static int drive_prefix_len(const char* s) {
int i = 0;
while (s[i] >= '0' && s[i] <= '9') i++;
return (i > 0 && s[i] == ':') ? i + 1 : 0;
}
// Write the parent of `path` into `out`. Returns false when the path has no
// removable parent left, which is how the -p walk knows to stop: the drive
// root ("0:/"), the absolute root ("/"), and a bare relative name all end it.
static bool parent_of(const char* path, char* out, int outMax) {
int len = montauk::slen(path);
if (len >= outMax) return false;
// Trailing slashes are not part of the last component.
while (len > 0 && path[len - 1] == '/') len--;
int base = drive_prefix_len(path);
int cut = -1;
for (int i = base; i < len; i++) {
if (path[i] == '/') cut = i;
}
if (cut < 0) return false;
int end = cut;
while (end > base && path[end - 1] == '/') end--;
if (end == base) return false;
for (int i = 0; i < end; i++) out[i] = path[i];
out[end] = '\0';
return true;
}
static bool remove_one(const char* path) {
montauk::abi::FileStat st;
if (montauk::stat(path, &st) < 0) {
report(path, "no such directory");
return false;
}
// The guard that matters: fdelete removes regular files just as happily
// as empty directories, so rmdir must refuse anything that is not one.
if (st.isDir == 0) {
report(path, "not a directory");
return false;
}
// The drivers refuse a non-empty directory as well, but cannot say why;
// checking here turns the common failure into a precise message.
const char* names[1];
if (montauk::readdir(path, names, 1) > 0) {
report(path, "directory not empty");
return false;
}
if (montauk::fdelete(path) < 0) {
report(path, "failed to remove directory");
return false;
}
return true;
}
static void remove_dir(const char* path, bool parents) {
if (montauk::slen(path) >= MaxPath) {
report(path, "path too long");
return;
}
if (!remove_one(path)) return;
if (!parents) return;
char current[MaxPath];
char parent[MaxPath];
montauk::strncpy(current, path, MaxPath);
while (parent_of(current, parent, MaxPath)) {
if (!remove_one(parent)) return;
montauk::strncpy(current, parent, MaxPath);
}
}
extern "C" void _start() {
char args[1024];
montauk::getargs(args, sizeof(args));
const char* p = args;
// Sized past MaxPath so an over-long operand survives tokenizing intact
// and is caught by the length check instead of being truncated into a
// different, possibly valid, path.
char tok[MaxPath + 2];
bool parents = false;
int operands = 0;
while (next_token(&p, tok, sizeof(tok))) {
// Options are recognized only before the first operand, so a
// directory named "-p" stays reachable once one has been given.
if (operands == 0 && tok[0] == '-' && tok[1] != '\0') {
if (montauk::streq(tok, "-p")) {
parents = true;
continue;
}
montauk::print("rmdir: unknown option: ");
montauk::print(tok);
montauk::putchar('\n');
usage();
montauk::exit(2);
}
operands++;
remove_dir(tok, parents);
}
if (operands == 0) {
usage();
montauk::exit(2);
}
montauk::exit(g_failures > 0 ? 1 : 0);
}
+9 -1
View File
@@ -48,9 +48,17 @@ void cmd_help() {
montauk::print("Built-in variables:\n");
montauk::print(" $USER $HOME $PWD $?\n");
montauk::print("\n");
montauk::print("File commands:\n");
montauk::print(" cat <file> Display file contents\n");
montauk::print(" touch <file> Create an empty file\n");
montauk::print(" copy <src> <dst> Copy a file\n");
montauk::print(" move <src> <dst> Move or rename a file\n");
montauk::print(" rm <file> Remove a file\n");
montauk::print(" mkdir [-p] <dir> Create a directory\n");
montauk::print(" rmdir [-p] <dir> Remove an empty directory\n");
montauk::print("\n");
montauk::print("System commands:\n");
montauk::print(" man <topic> View manual pages\n");
montauk::print(" cat <file> Display file contents\n");
montauk::print(" edit [file] Text editor\n");
montauk::print(" whoami Print current username\n");
montauk::print(" info Show system information\n");