feat: real stat metadata across all filesystems, plus mkdir and rmdir
This commit is contained in:
@@ -23,8 +23,9 @@ extern "C" {
|
||||
#define S_ISSOCK(mode) (0)
|
||||
#define S_ISBLK(mode) (0)
|
||||
|
||||
/* Permission bits are accepted and preserved by the API for POSIX
|
||||
compatibility, but the Montauk VFS does not store them. */
|
||||
/* stat() reports real permission bits: ext2 stores them natively, the
|
||||
ramdisk takes them from the USTAR header, and FAT32 synthesizes them from
|
||||
its read-only attribute. Changing them (chmod) is still not supported. */
|
||||
#define S_IRWXU 0700
|
||||
#define S_IRUSR 0400
|
||||
#define S_IWUSR 0200
|
||||
|
||||
+51
-19
@@ -116,6 +116,7 @@ static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) {
|
||||
#define SYS_GETPID 3
|
||||
#define SYS_DUPHANDLE 98
|
||||
#define SYS_KILL 62
|
||||
#define SYS_STAT 152
|
||||
|
||||
/* ========================================================================
|
||||
errno
|
||||
@@ -136,6 +137,17 @@ struct _mtk_datetime {
|
||||
uint8_t second;
|
||||
};
|
||||
|
||||
/* Mirrors montauk::abi::FileStat, the SYS_STAT output layout. Kept in step
|
||||
with Api/Syscall.hpp by hand, like the syscall numbers above. */
|
||||
struct _mtk_filestat {
|
||||
uint64_t size;
|
||||
int64_t mtime;
|
||||
int64_t ctime;
|
||||
int64_t atime;
|
||||
uint32_t mode; /* POSIX i_mode bits: type + permissions */
|
||||
uint32_t isDir;
|
||||
};
|
||||
|
||||
struct _DIR {
|
||||
int count;
|
||||
int index;
|
||||
@@ -2311,6 +2323,19 @@ int mkdir(const char *path, unsigned int mode) {
|
||||
return (int)_zos_syscall1(SYS_FMKDIR, (long)path);
|
||||
}
|
||||
|
||||
/* The VFS exposes no inode numbers, so derive a stable id from the path and
|
||||
let distinct paths compare as distinct files. GCC's include-path setup
|
||||
deduplicates directories by (st_dev, st_ino); leaving this 0 made every
|
||||
directory "the same" and all but one include dir was silently dropped.
|
||||
SYS_STAT supplies no inode either, so this stays in use regardless. */
|
||||
static ino_t _path_fake_ino(const char *path) {
|
||||
unsigned long ino = 5381;
|
||||
for (const char *p = path; *p; p++) {
|
||||
ino = ino * 33 + (unsigned char)*p;
|
||||
}
|
||||
return (ino_t)(ino | 1);
|
||||
}
|
||||
|
||||
int stat(const char *path, struct stat *buf) {
|
||||
if (path == NULL || buf == NULL) {
|
||||
errno = EINVAL;
|
||||
@@ -2318,21 +2343,35 @@ int stat(const char *path, struct stat *buf) {
|
||||
}
|
||||
|
||||
memset(buf, 0, sizeof(*buf));
|
||||
buf->st_ino = _path_fake_ino(path);
|
||||
buf->st_nlink = 1;
|
||||
buf->st_blksize = 4096;
|
||||
|
||||
/* Preferred path: real metadata from the filesystem driver. */
|
||||
struct _mtk_filestat st;
|
||||
if (_zos_syscall2(SYS_STAT, (long)path, (long)&st) == 0) {
|
||||
buf->st_mode = (mode_t)st.mode;
|
||||
/* A driver that reports no type bits still tells us whether the
|
||||
entry is a directory; without this S_ISREG/S_ISDIR both fail. */
|
||||
if ((buf->st_mode & S_IFMT) == 0) {
|
||||
buf->st_mode |= st.isDir ? S_IFDIR : S_IFREG;
|
||||
}
|
||||
buf->st_size = (off_t)st.size;
|
||||
buf->st_atime = (time_t)st.atime;
|
||||
buf->st_mtime = (time_t)st.mtime;
|
||||
buf->st_ctime = (time_t)st.ctime;
|
||||
buf->st_blocks = (blkcnt_t)((buf->st_size + 511) / 512);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Fallback for a filesystem whose driver has no Stat entry point: probe
|
||||
with open/readdir as before. Size is recoverable this way, timestamps
|
||||
and permissions are not. */
|
||||
int h = (int)_zos_syscall1(SYS_OPEN, (long)path);
|
||||
if (h >= 0) {
|
||||
buf->st_mode = S_IFREG;
|
||||
buf->st_size = (off_t)_zos_syscall1(SYS_GETSIZE, (long)h);
|
||||
_zos_syscall1(SYS_CLOSE, (long)h);
|
||||
/* No inodes in the VFS API: derive a stable id from the path
|
||||
so that distinct paths compare as distinct files. */
|
||||
unsigned long ino = 5381;
|
||||
for (const char *p = path; *p; p++) {
|
||||
ino = ino * 33 + (unsigned char)*p;
|
||||
}
|
||||
buf->st_ino = ino | 1;
|
||||
buf->st_nlink = 1;
|
||||
buf->st_blksize = 4096;
|
||||
buf->st_blocks = (blkcnt_t)((buf->st_size + 511) / 512);
|
||||
return 0;
|
||||
}
|
||||
@@ -2340,16 +2379,6 @@ int stat(const char *path, struct stat *buf) {
|
||||
if (_path_is_directory(path)) {
|
||||
buf->st_mode = S_IFDIR;
|
||||
buf->st_size = 0;
|
||||
/* Directories need distinct fake inodes too: GCC's include
|
||||
path setup deduplicates directories by (st_dev, st_ino), so
|
||||
leaving 0 here made every directory "the same" and all but
|
||||
one include dir was silently dropped. */
|
||||
unsigned long ino = 5381;
|
||||
for (const char *p = path; *p; p++) {
|
||||
ino = ino * 33 + (unsigned char)*p;
|
||||
}
|
||||
buf->st_ino = ino | 1;
|
||||
buf->st_nlink = 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2363,6 +2392,9 @@ int fstat(int fd, struct stat *buf) {
|
||||
return -1;
|
||||
}
|
||||
memset(buf, 0, sizeof(*buf));
|
||||
/* SYS_STAT is path-based and handles do not map back to paths, so this
|
||||
cannot report real timestamps or permissions the way stat() does.
|
||||
Callers needing those must stat the path instead. */
|
||||
buf->st_mode = S_IFREG;
|
||||
buf->st_size = (off_t)_zos_syscall1(SYS_GETSIZE, (long)fd);
|
||||
/* Handles do not map back to paths, so fake a per-handle inode in a
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user