diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 46dfbd9..e25e10d 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 42 +#define MONTAUK_BUILD_NUMBER 46 diff --git a/kernel/src/Fs/Boot.cpp b/kernel/src/Fs/Boot.cpp index d37a264..326bffb 100644 --- a/kernel/src/Fs/Boot.cpp +++ b/kernel/src/Fs/Boot.cpp @@ -69,6 +69,23 @@ namespace Fs { const char* RdGetLabel(void*) { return Ramdisk::GetLabel(); } int RdReadDirAt(void*, const char* p, const char** o, int m, int s) { return Ramdisk::ReadDirAt(p, o, m, s); } + int RdStat(void*, const char* p, Vfs::StatInfo* out) { + if (out == nullptr) return -1; + + Ramdisk::StatResult st; + if (Ramdisk::Stat(p, &st) != 0) return -1; + + out->size = st.size; + // The USTAR header carries a single timestamp; report it for all + // three rather than leaving ctime/atime at the epoch. + out->mtime = st.mtime; + out->ctime = st.mtime; + out->atime = st.mtime; + out->mode = (st.isDirectory ? Vfs::ModeDir : Vfs::ModeReg) | (st.mode & 07777); + out->isDir = st.isDirectory; + return 0; + } + Vfs::FsDriver g_ramdiskDriver = { .ctx = nullptr, .Open = RdOpen, @@ -83,7 +100,7 @@ namespace Fs { .Rename = RdRename, .GetLabel = RdGetLabel, .ReadDirAt = RdReadDirAt, - .Stat = nullptr, + .Stat = RdStat, // Statically allocated: nothing to release. .Unmount = nullptr, }; diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp index 0fdecb1..5a8c56b 100644 --- a/kernel/src/Fs/Fat32.cpp +++ b/kernel/src/Fs/Fat32.cpp @@ -11,6 +11,7 @@ #include #include #include +#include using namespace Kt; @@ -107,6 +108,13 @@ namespace Fs::Fat32 { // Location of the SFN entry on disk (for write support) uint64_t sfnPartSector; uint32_t sfnOffInSector; + // Packed FAT date/time words straight from the SFN entry, decoded + // on demand by Stat. Zero means "not recorded". + uint16_t writeTime; + uint16_t writeDate; + uint16_t createTime; + uint16_t createDate; + uint16_t accessDate; }; // ========================================================================= @@ -853,6 +861,13 @@ namespace Fs::Fat32 { out->firstCluster = ((uint32_t)clHi << 16) | (uint32_t)clLo; memcpy(&out->fileSize, e + 28, 4); out->attributes = attr; + // SFN timestamp words: creation at 14/16, last access + // date at 18, last write at 22/24. + memcpy(&out->createTime, e + 14, 2); + memcpy(&out->createDate, e + 16, 2); + memcpy(&out->accessDate, e + 18, 2); + memcpy(&out->writeTime, e + 22, 2); + memcpy(&out->writeDate, e + 24, 2); int j = 0; while (entryName[j] && j < MaxNameLen - 1) { out->name[j] = entryName[j]; j++; @@ -888,6 +903,12 @@ namespace Fs::Fat32 { out->attributes = ATTR_DIRECTORY; out->name[0] = '/'; out->name[1] = '\0'; + // The root has no directory entry, so it has no timestamps. + out->writeTime = 0; + out->writeDate = 0; + out->createTime = 0; + out->createDate = 0; + out->accessDate = 0; return true; } @@ -1578,6 +1599,54 @@ namespace Fs::Fat32 { return 0; } + // ========================================================================= + // Stat — metadata for a single path + // ========================================================================= + + // Decode a packed FAT date/time pair into a Unix timestamp. + // date: bits 15-9 year since 1980, 8-5 month (1-12), 4-0 day (1-31) + // time: bits 15-11 hour, 10-5 minute, 4-0 seconds/2 + // FAT stores local time with no recorded offset, so it is taken as UTC. + // A zero date means the field was never written; report 0 rather than + // inventing 1980-00-00. + static int64_t FatDateTimeToEpoch(uint16_t date, uint16_t time) { + if (date == 0) return 0; + + int year = 1980 + ((date >> 9) & 0x7F); + int month = (date >> 5) & 0x0F; + int day = date & 0x1F; + + int hour = (time >> 11) & 0x1F; + int minute = (time >> 5) & 0x3F; + int second = (time & 0x1F) * 2; + + return Timekeeping::DateToUnixTimestamp(year, month, day, hour, minute, second); + } + + static int StatImpl(int inst, const char* path, Vfs::StatInfo* out) { + if (out == nullptr) return -1; + if (InstanceAt(inst) == nullptr) return -1; + + ParsedEntry entry; + if (!TraversePath(inst, path, &entry)) return -1; + + bool isDir = (entry.attributes & ATTR_DIRECTORY) != 0; + + out->size = isDir ? 0 : entry.fileSize; + out->mtime = FatDateTimeToEpoch(entry.writeDate, entry.writeTime); + out->ctime = FatDateTimeToEpoch(entry.createDate, entry.createTime); + // Access date has no time-of-day component on FAT. + out->atime = FatDateTimeToEpoch(entry.accessDate, 0); + // FAT carries no POSIX permissions; synthesize them from the + // read-only attribute so callers see a plausible mode. + uint32_t perms = (entry.attributes & ATTR_READ_ONLY) + ? (isDir ? 0555u : 0444u) + : (isDir ? 0755u : 0644u); + out->mode = (isDir ? Vfs::ModeDir : Vfs::ModeReg) | perms; + out->isDir = isDir; + return 0; + } + // ========================================================================= // Rename — atomic directory entry move // ========================================================================= @@ -1821,6 +1890,7 @@ namespace Fs::Fat32 { static int DrvMkdir(void* c, const char* p) { return MkdirImpl(CtxToInst(c), p); } static int DrvRename(void* c, const char* o, const char* n) { return RenameImpl(CtxToInst(c), o, n); } static const char* DrvGetLabel(void* c) { return GetLabelImpl(CtxToInst(c)); } + static int DrvStat(void* c, const char* p, Vfs::StatInfo* o) { return StatImpl(CtxToInst(c), p, o); } // Release everything the mount owns. The driver is either not registered // yet, or the VFS has deactivated its drive and drained dispatches. @@ -2028,7 +2098,7 @@ namespace Fs::Fat32 { .Rename = DrvRename, .GetLabel = DrvGetLabel, .ReadDirAt = DrvReadDirAt, - .Stat = nullptr, + .Stat = DrvStat, .Unmount = DrvUnmount, }; diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp index b39a744..e86d35c 100644 --- a/kernel/src/Fs/Ramdisk.cpp +++ b/kernel/src/Fs/Ramdisk.cpp @@ -9,12 +9,20 @@ #include #include #include +#include namespace Fs::Ramdisk { static FileEntry fileTable[MaxFiles]; static int fileCount = 0; + // Wall-clock stamp for entries created after boot. Negative (clock not yet + // available) collapses to 0 rather than a nonsense pre-epoch timestamp. + static int64_t RamdiskNow() { + int64_t ts = Timekeeping::GetUnixTimestamp(); + return ts < 0 ? 0 : ts; + } + static uint64_t OctalToUint(const char* str, int len) { uint64_t result = 0; for (int i = 0; i < len && str[i] != '\0' && str[i] != ' '; i++) { @@ -114,8 +122,12 @@ namespace Fs::Ramdisk { // the name lands as a bogus root-level entry. const char* name = (const char*)ptr; const char* namePrefix = (const char*)(ptr + 345); + // File mode at offset 100 (8 bytes, octal ASCII) + uint32_t mode = (uint32_t)OctalToUint((const char*)(ptr + 100), 8); // File size at offset 124 (12 bytes, octal ASCII) uint64_t size = OctalToUint((const char*)(ptr + 124), 12); + // Modification time at offset 136 (12 bytes, octal ASCII Unix epoch) + int64_t mtime = (int64_t)OctalToUint((const char*)(ptr + 136), 12); // Type flag at offset 156 char typeFlag = (char)ptr[156]; @@ -154,6 +166,13 @@ namespace Fs::Ramdisk { entry.size = size; entry.capacity = size; entry.heapAllocated = false; + // Keep permission bits only; some archivers stash type bits here + // too, and the type is already carried by isDirectory. + entry.mode = mode & 07777; + if (entry.mode == 0) { + entry.mode = entry.isDirectory ? DefaultDirMode : DefaultFileMode; + } + entry.mtime = mtime; // Data starts at next 512-byte block entry.data = ptr + 512; @@ -396,6 +415,7 @@ namespace Fs::Ramdisk { } entry.size = 0; entry.isDirectory = false; + entry.mtime = RamdiskNow(); return i; } } @@ -418,6 +438,8 @@ namespace Fs::Ramdisk { entry.capacity = 256; entry.isDirectory = false; entry.heapAllocated = true; + entry.mode = DefaultFileMode; + entry.mtime = RamdiskNow(); return fileCount++; } @@ -504,11 +526,27 @@ namespace Fs::Ramdisk { entry.capacity = 0; entry.isDirectory = true; entry.heapAllocated = false; + entry.mode = DefaultDirMode; + entry.mtime = RamdiskNow(); fileCount++; return 0; } + int Stat(const char* path, StatResult* out) { + if (path == nullptr || out == nullptr) return -1; + + int idx = FindEntryByPath(path); + if (idx < 0) return -1; + + const FileEntry& entry = fileTable[idx]; + out->size = entry.isDirectory ? 0 : entry.size; + out->mtime = entry.mtime; + out->mode = entry.mode; + out->isDirectory = entry.isDirectory; + return 0; + } + int Rename(const char* oldPath, const char* newPath) { char oldNorm[MaxNameLen]; char newNorm[MaxNameLen]; diff --git a/kernel/src/Fs/Ramdisk.hpp b/kernel/src/Fs/Ramdisk.hpp index f5f76f9..46917b4 100644 --- a/kernel/src/Fs/Ramdisk.hpp +++ b/kernel/src/Fs/Ramdisk.hpp @@ -21,10 +21,20 @@ namespace Fs::Ramdisk { uint8_t* data; uint64_t size; uint64_t capacity; + // POSIX permission bits (no type bits) and modification time, both + // taken from the USTAR header so shipped files report real metadata. + // Runtime-created entries get sensible defaults instead. + uint32_t mode; + int64_t mtime; bool isDirectory; bool heapAllocated; }; + // Default permissions for entries created at runtime, where the archive + // has no header to read them from. + static constexpr uint32_t DefaultFileMode = 0644; + static constexpr uint32_t DefaultDirMode = 0755; + void Initialize(void* moduleData, uint64_t moduleSize); int Open(const char* path); @@ -42,4 +52,17 @@ namespace Fs::Ramdisk { const char* GetLabel(); int GetFileCount(); + // Metadata for a single path. Deliberately free of VFS types so the + // ramdisk stays independent of the layer above it; the caller maps this + // onto Vfs::StatInfo and adds the POSIX type bits. + struct StatResult { + uint64_t size; + int64_t mtime; + uint32_t mode; // permission bits only, no type bits + bool isDirectory; + }; + + // Returns 0 and fills *out on success, -1 if the path does not exist. + int Stat(const char* path, StatResult* out); + } diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index 8fd03d8..57f854e 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -26,6 +26,12 @@ namespace Fs::Vfs { uint32_t generation; }; + // POSIX file-type bits carried in StatInfo::mode. These match the ext2 + // on-disk i_mode encoding, which drivers without native POSIX modes + // (ramdisk, FAT32) synthesize so every filesystem reports mode alike. + static constexpr uint32_t ModeDir = 0x4000; + static constexpr uint32_t ModeReg = 0x8000; + // Metadata for a single path, filled by FsDriver::Stat. Timestamps are UTC // unix seconds; a filesystem that does not record a given time leaves it 0. struct StatInfo { diff --git a/kernel/src/Timekeeping/Time.cpp b/kernel/src/Timekeeping/Time.cpp index 2f729ac..389bced 100644 --- a/kernel/src/Timekeeping/Time.cpp +++ b/kernel/src/Timekeeping/Time.cpp @@ -84,6 +84,13 @@ int64_t Timekeeping::GetUnixTimestamp() { return g_bootEpoch + (int64_t)(Timekeeping::GetMilliseconds() / 1000); } +int64_t Timekeeping::DateToUnixTimestamp(int year, int month, int day, + int hour, int minute, int second) { + if (year < 1970 || month < 1 || month > 12 || day < 1 || day > 31) return 0; + if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 60) return 0; + return DateToEpoch(year, month, day, hour, minute, second); +} + bool Timekeeping::SetUnixTimestamp(int64_t unixSeconds) { if (unixSeconds < 0 || unixSeconds > 4102444799LL) return false; diff --git a/kernel/src/Timekeeping/Time.hpp b/kernel/src/Timekeeping/Time.hpp index 67edfa6..c0df46f 100644 --- a/kernel/src/Timekeeping/Time.hpp +++ b/kernel/src/Timekeeping/Time.hpp @@ -52,6 +52,12 @@ namespace Timekeeping { void Init(uint16_t Year, uint8_t Month, uint8_t Day, uint8_t Hour, uint8_t Minute, uint8_t Second); int64_t GetUnixTimestamp(); + + // Convert a UTC calendar date to a Unix timestamp. Returns 0 for dates + // outside the representable range. Used by filesystem drivers that store + // on-disk timestamps as calendar fields (e.g. FAT32). + int64_t DateToUnixTimestamp(int year, int month, int day, int hour, int minute, int second); + DateTime GetDateTime(); bool SetUnixTimestamp(int64_t unixSeconds); diff --git a/programs/include/libc/sys/stat.h b/programs/include/libc/sys/stat.h index 34b30e0..7dc995d 100644 --- a/programs/include/libc/sys/stat.h +++ b/programs/include/libc/sys/stat.h @@ -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 diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index c450c01..6bd7876 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -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 diff --git a/programs/lib/libc/obj/libc.o b/programs/lib/libc/obj/libc.o index 7e82445..addf4aa 100644 Binary files a/programs/lib/libc/obj/libc.o and b/programs/lib/libc/obj/libc.o differ diff --git a/programs/src/mkdir/main.cpp b/programs/src/mkdir/main.cpp new file mode 100644 index 0000000..f95643d --- /dev/null +++ b/programs/src/mkdir/main.cpp @@ -0,0 +1,155 @@ +/* + * main.cpp + * mkdir - command to create directories + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include + +// 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 ...]\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); +} diff --git a/programs/src/rmdir/main.cpp b/programs/src/rmdir/main.cpp new file mode 100644 index 0000000..ace4b7d --- /dev/null +++ b/programs/src/rmdir/main.cpp @@ -0,0 +1,169 @@ +/* + * main.cpp + * rmdir - command to remove empty directories + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include + +// 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 ...]\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); +} diff --git a/programs/src/shell/builtins.cpp b/programs/src/shell/builtins.cpp index 2591df8..8c1bb5c 100644 --- a/programs/src/shell/builtins.cpp +++ b/programs/src/shell/builtins.cpp @@ -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 Display file contents\n"); + montauk::print(" touch Create an empty file\n"); + montauk::print(" copy Copy a file\n"); + montauk::print(" move Move or rename a file\n"); + montauk::print(" rm Remove a file\n"); + montauk::print(" mkdir [-p] Create a directory\n"); + montauk::print(" rmdir [-p] Remove an empty directory\n"); + montauk::print("\n"); montauk::print("System commands:\n"); montauk::print(" man View manual pages\n"); - montauk::print(" cat 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");