feat: fs - real stat metadata across all filesystems, plus mkdir and rmdir
This commit is contained in:
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 42
|
#define MONTAUK_BUILD_NUMBER 46
|
||||||
|
|||||||
+18
-1
@@ -69,6 +69,23 @@ namespace Fs {
|
|||||||
const char* RdGetLabel(void*) { return Ramdisk::GetLabel(); }
|
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 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 = {
|
Vfs::FsDriver g_ramdiskDriver = {
|
||||||
.ctx = nullptr,
|
.ctx = nullptr,
|
||||||
.Open = RdOpen,
|
.Open = RdOpen,
|
||||||
@@ -83,7 +100,7 @@ namespace Fs {
|
|||||||
.Rename = RdRename,
|
.Rename = RdRename,
|
||||||
.GetLabel = RdGetLabel,
|
.GetLabel = RdGetLabel,
|
||||||
.ReadDirAt = RdReadDirAt,
|
.ReadDirAt = RdReadDirAt,
|
||||||
.Stat = nullptr,
|
.Stat = RdStat,
|
||||||
// Statically allocated: nothing to release.
|
// Statically allocated: nothing to release.
|
||||||
.Unmount = nullptr,
|
.Unmount = nullptr,
|
||||||
};
|
};
|
||||||
|
|||||||
+71
-1
@@ -11,6 +11,7 @@
|
|||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Memory/PageFrameAllocator.hpp>
|
#include <Memory/PageFrameAllocator.hpp>
|
||||||
#include <CppLib/Vector.hpp>
|
#include <CppLib/Vector.hpp>
|
||||||
|
#include <Timekeeping/Time.hpp>
|
||||||
|
|
||||||
using namespace Kt;
|
using namespace Kt;
|
||||||
|
|
||||||
@@ -107,6 +108,13 @@ namespace Fs::Fat32 {
|
|||||||
// Location of the SFN entry on disk (for write support)
|
// Location of the SFN entry on disk (for write support)
|
||||||
uint64_t sfnPartSector;
|
uint64_t sfnPartSector;
|
||||||
uint32_t sfnOffInSector;
|
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;
|
out->firstCluster = ((uint32_t)clHi << 16) | (uint32_t)clLo;
|
||||||
memcpy(&out->fileSize, e + 28, 4);
|
memcpy(&out->fileSize, e + 28, 4);
|
||||||
out->attributes = attr;
|
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;
|
int j = 0;
|
||||||
while (entryName[j] && j < MaxNameLen - 1) {
|
while (entryName[j] && j < MaxNameLen - 1) {
|
||||||
out->name[j] = entryName[j]; j++;
|
out->name[j] = entryName[j]; j++;
|
||||||
@@ -888,6 +903,12 @@ namespace Fs::Fat32 {
|
|||||||
out->attributes = ATTR_DIRECTORY;
|
out->attributes = ATTR_DIRECTORY;
|
||||||
out->name[0] = '/';
|
out->name[0] = '/';
|
||||||
out->name[1] = '\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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1578,6 +1599,54 @@ namespace Fs::Fat32 {
|
|||||||
return 0;
|
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
|
// 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 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 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 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
|
// Release everything the mount owns. The driver is either not registered
|
||||||
// yet, or the VFS has deactivated its drive and drained dispatches.
|
// yet, or the VFS has deactivated its drive and drained dispatches.
|
||||||
@@ -2028,7 +2098,7 @@ namespace Fs::Fat32 {
|
|||||||
.Rename = DrvRename,
|
.Rename = DrvRename,
|
||||||
.GetLabel = DrvGetLabel,
|
.GetLabel = DrvGetLabel,
|
||||||
.ReadDirAt = DrvReadDirAt,
|
.ReadDirAt = DrvReadDirAt,
|
||||||
.Stat = nullptr,
|
.Stat = DrvStat,
|
||||||
.Unmount = DrvUnmount,
|
.Unmount = DrvUnmount,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,20 @@
|
|||||||
#include <Libraries/String.hpp>
|
#include <Libraries/String.hpp>
|
||||||
#include <Libraries/Memory.hpp>
|
#include <Libraries/Memory.hpp>
|
||||||
#include <Memory/Heap.hpp>
|
#include <Memory/Heap.hpp>
|
||||||
|
#include <Timekeeping/Time.hpp>
|
||||||
|
|
||||||
namespace Fs::Ramdisk {
|
namespace Fs::Ramdisk {
|
||||||
|
|
||||||
static FileEntry fileTable[MaxFiles];
|
static FileEntry fileTable[MaxFiles];
|
||||||
static int fileCount = 0;
|
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) {
|
static uint64_t OctalToUint(const char* str, int len) {
|
||||||
uint64_t result = 0;
|
uint64_t result = 0;
|
||||||
for (int i = 0; i < len && str[i] != '\0' && str[i] != ' '; i++) {
|
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.
|
// the name lands as a bogus root-level entry.
|
||||||
const char* name = (const char*)ptr;
|
const char* name = (const char*)ptr;
|
||||||
const char* namePrefix = (const char*)(ptr + 345);
|
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)
|
// File size at offset 124 (12 bytes, octal ASCII)
|
||||||
uint64_t size = OctalToUint((const char*)(ptr + 124), 12);
|
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
|
// Type flag at offset 156
|
||||||
char typeFlag = (char)ptr[156];
|
char typeFlag = (char)ptr[156];
|
||||||
|
|
||||||
@@ -154,6 +166,13 @@ namespace Fs::Ramdisk {
|
|||||||
entry.size = size;
|
entry.size = size;
|
||||||
entry.capacity = size;
|
entry.capacity = size;
|
||||||
entry.heapAllocated = false;
|
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
|
// Data starts at next 512-byte block
|
||||||
entry.data = ptr + 512;
|
entry.data = ptr + 512;
|
||||||
@@ -396,6 +415,7 @@ namespace Fs::Ramdisk {
|
|||||||
}
|
}
|
||||||
entry.size = 0;
|
entry.size = 0;
|
||||||
entry.isDirectory = false;
|
entry.isDirectory = false;
|
||||||
|
entry.mtime = RamdiskNow();
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +438,8 @@ namespace Fs::Ramdisk {
|
|||||||
entry.capacity = 256;
|
entry.capacity = 256;
|
||||||
entry.isDirectory = false;
|
entry.isDirectory = false;
|
||||||
entry.heapAllocated = true;
|
entry.heapAllocated = true;
|
||||||
|
entry.mode = DefaultFileMode;
|
||||||
|
entry.mtime = RamdiskNow();
|
||||||
|
|
||||||
return fileCount++;
|
return fileCount++;
|
||||||
}
|
}
|
||||||
@@ -504,11 +526,27 @@ namespace Fs::Ramdisk {
|
|||||||
entry.capacity = 0;
|
entry.capacity = 0;
|
||||||
entry.isDirectory = true;
|
entry.isDirectory = true;
|
||||||
entry.heapAllocated = false;
|
entry.heapAllocated = false;
|
||||||
|
entry.mode = DefaultDirMode;
|
||||||
|
entry.mtime = RamdiskNow();
|
||||||
|
|
||||||
fileCount++;
|
fileCount++;
|
||||||
return 0;
|
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) {
|
int Rename(const char* oldPath, const char* newPath) {
|
||||||
char oldNorm[MaxNameLen];
|
char oldNorm[MaxNameLen];
|
||||||
char newNorm[MaxNameLen];
|
char newNorm[MaxNameLen];
|
||||||
|
|||||||
@@ -21,10 +21,20 @@ namespace Fs::Ramdisk {
|
|||||||
uint8_t* data;
|
uint8_t* data;
|
||||||
uint64_t size;
|
uint64_t size;
|
||||||
uint64_t capacity;
|
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 isDirectory;
|
||||||
bool heapAllocated;
|
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);
|
void Initialize(void* moduleData, uint64_t moduleSize);
|
||||||
|
|
||||||
int Open(const char* path);
|
int Open(const char* path);
|
||||||
@@ -42,4 +52,17 @@ namespace Fs::Ramdisk {
|
|||||||
const char* GetLabel();
|
const char* GetLabel();
|
||||||
int GetFileCount();
|
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ namespace Fs::Vfs {
|
|||||||
uint32_t generation;
|
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
|
// 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.
|
// unix seconds; a filesystem that does not record a given time leaves it 0.
|
||||||
struct StatInfo {
|
struct StatInfo {
|
||||||
|
|||||||
@@ -84,6 +84,13 @@ int64_t Timekeeping::GetUnixTimestamp() {
|
|||||||
return g_bootEpoch + (int64_t)(Timekeeping::GetMilliseconds() / 1000);
|
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) {
|
bool Timekeeping::SetUnixTimestamp(int64_t unixSeconds) {
|
||||||
if (unixSeconds < 0 || unixSeconds > 4102444799LL)
|
if (unixSeconds < 0 || unixSeconds > 4102444799LL)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -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);
|
void Init(uint16_t Year, uint8_t Month, uint8_t Day, uint8_t Hour, uint8_t Minute, uint8_t Second);
|
||||||
int64_t GetUnixTimestamp();
|
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();
|
DateTime GetDateTime();
|
||||||
bool SetUnixTimestamp(int64_t unixSeconds);
|
bool SetUnixTimestamp(int64_t unixSeconds);
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ extern "C" {
|
|||||||
#define S_ISSOCK(mode) (0)
|
#define S_ISSOCK(mode) (0)
|
||||||
#define S_ISBLK(mode) (0)
|
#define S_ISBLK(mode) (0)
|
||||||
|
|
||||||
/* Permission bits are accepted and preserved by the API for POSIX
|
/* stat() reports real permission bits: ext2 stores them natively, the
|
||||||
compatibility, but the Montauk VFS does not store them. */
|
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_IRWXU 0700
|
||||||
#define S_IRUSR 0400
|
#define S_IRUSR 0400
|
||||||
#define S_IWUSR 0200
|
#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_GETPID 3
|
||||||
#define SYS_DUPHANDLE 98
|
#define SYS_DUPHANDLE 98
|
||||||
#define SYS_KILL 62
|
#define SYS_KILL 62
|
||||||
|
#define SYS_STAT 152
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
errno
|
errno
|
||||||
@@ -136,6 +137,17 @@ struct _mtk_datetime {
|
|||||||
uint8_t second;
|
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 {
|
struct _DIR {
|
||||||
int count;
|
int count;
|
||||||
int index;
|
int index;
|
||||||
@@ -2311,6 +2323,19 @@ int mkdir(const char *path, unsigned int mode) {
|
|||||||
return (int)_zos_syscall1(SYS_FMKDIR, (long)path);
|
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) {
|
int stat(const char *path, struct stat *buf) {
|
||||||
if (path == NULL || buf == NULL) {
|
if (path == NULL || buf == NULL) {
|
||||||
errno = EINVAL;
|
errno = EINVAL;
|
||||||
@@ -2318,21 +2343,35 @@ int stat(const char *path, struct stat *buf) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
memset(buf, 0, sizeof(*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);
|
int h = (int)_zos_syscall1(SYS_OPEN, (long)path);
|
||||||
if (h >= 0) {
|
if (h >= 0) {
|
||||||
buf->st_mode = S_IFREG;
|
buf->st_mode = S_IFREG;
|
||||||
buf->st_size = (off_t)_zos_syscall1(SYS_GETSIZE, (long)h);
|
buf->st_size = (off_t)_zos_syscall1(SYS_GETSIZE, (long)h);
|
||||||
_zos_syscall1(SYS_CLOSE, (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);
|
buf->st_blocks = (blkcnt_t)((buf->st_size + 511) / 512);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -2340,16 +2379,6 @@ int stat(const char *path, struct stat *buf) {
|
|||||||
if (_path_is_directory(path)) {
|
if (_path_is_directory(path)) {
|
||||||
buf->st_mode = S_IFDIR;
|
buf->st_mode = S_IFDIR;
|
||||||
buf->st_size = 0;
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2363,6 +2392,9 @@ int fstat(int fd, struct stat *buf) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
memset(buf, 0, sizeof(*buf));
|
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_mode = S_IFREG;
|
||||||
buf->st_size = (off_t)_zos_syscall1(SYS_GETSIZE, (long)fd);
|
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
|
/* 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("Built-in variables:\n");
|
||||||
montauk::print(" $USER $HOME $PWD $?\n");
|
montauk::print(" $USER $HOME $PWD $?\n");
|
||||||
montauk::print("\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("System commands:\n");
|
||||||
montauk::print(" man <topic> View manual pages\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(" edit [file] Text editor\n");
|
||||||
montauk::print(" whoami Print current username\n");
|
montauk::print(" whoami Print current username\n");
|
||||||
montauk::print(" info Show system information\n");
|
montauk::print(" info Show system information\n");
|
||||||
|
|||||||
Reference in New Issue
Block a user