From ca5331c9db367dd88b63306458f6f72b6fd91cef Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Tue, 4 Aug 2026 08:58:50 +0200 Subject: [PATCH] feat: vfs - add dynamic filesystem mounts and safe unmount lifecycle --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Fs/Boot.cpp | 43 +++- kernel/src/Fs/Ext2.cpp | 235 +++++++++++++----- kernel/src/Fs/Fat32.cpp | 209 +++++++++++----- kernel/src/Fs/FsProbe.cpp | 11 + kernel/src/Fs/Vfs.cpp | 176 +++++++------ kernel/src/Fs/Vfs.hpp | 46 ++-- .../apps/filemanager/filemanager_internal.hpp | 3 +- .../desktop/apps/filemanager/filesystem.cpp | 18 +- programs/src/dialogs/filedialog.hpp | 6 +- programs/src/disks/actions.cpp | 28 ++- programs/src/disks/disks.h | 2 + 12 files changed, 520 insertions(+), 259 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 084f2b1..46dfbd9 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 37 +#define MONTAUK_BUILD_NUMBER 42 diff --git a/kernel/src/Fs/Boot.cpp b/kernel/src/Fs/Boot.cpp index 6535dc5..d37a264 100644 --- a/kernel/src/Fs/Boot.cpp +++ b/kernel/src/Fs/Boot.cpp @@ -54,19 +54,38 @@ namespace Fs { return hasRamdisk; } + // The ramdisk is a singleton, so it has no per-mount state and ignores + // ctx. These shims exist only to match the FsDriver signature. + int RdOpen(void*, const char* p) { return Ramdisk::Open(p); } + int RdRead(void*, int h, uint8_t* b, uint64_t o, uint64_t s) { return Ramdisk::Read(h, b, o, s); } + uint64_t RdGetSize(void*, int h) { return Ramdisk::GetSize(h); } + void RdClose(void*, int h) { Ramdisk::Close(h); } + int RdReadDir(void*, const char* p, const char** o, int m) { return Ramdisk::ReadDir(p, o, m); } + int RdWrite(void*, int h, const uint8_t* b, uint64_t o, uint64_t s) { return Ramdisk::Write(h, b, o, s); } + int RdCreate(void*, const char* p) { return Ramdisk::Create(p); } + int RdDelete(void*, const char* p) { return Ramdisk::Delete(p); } + int RdMkdir(void*, const char* p) { return Ramdisk::Mkdir(p); } + int RdRename(void*, const char* o, const char* n) { return Ramdisk::Rename(o, n); } + 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); } + Vfs::FsDriver g_ramdiskDriver = { - Ramdisk::Open, - Ramdisk::Read, - Ramdisk::GetSize, - Ramdisk::Close, - Ramdisk::ReadDir, - Ramdisk::Write, - Ramdisk::Create, - Ramdisk::Delete, - Ramdisk::Mkdir, - Ramdisk::Rename, - Ramdisk::GetLabel, - Ramdisk::ReadDirAt, + .ctx = nullptr, + .Open = RdOpen, + .Read = RdRead, + .GetSize = RdGetSize, + .Close = RdClose, + .ReadDir = RdReadDir, + .Write = RdWrite, + .Create = RdCreate, + .Delete = RdDelete, + .Mkdir = RdMkdir, + .Rename = RdRename, + .GetLabel = RdGetLabel, + .ReadDirAt = RdReadDirAt, + .Stat = nullptr, + // Statically allocated: nothing to release. + .Unmount = nullptr, }; } diff --git a/kernel/src/Fs/Ext2.cpp b/kernel/src/Fs/Ext2.cpp index 25fb7c3..4f5ee5f 100644 --- a/kernel/src/Fs/Ext2.cpp +++ b/kernel/src/Fs/Ext2.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include using namespace Kt; @@ -20,7 +21,6 @@ namespace Fs::Ext2 { // Constants // ========================================================================= - static constexpr int MaxInstances = 8; static constexpr int MaxFilesPerInstance = 16; static constexpr int MaxDirEntries = 128; static constexpr int MaxNameLen = 256; @@ -148,6 +148,10 @@ namespace Fs::Ext2 { int blockDevIndex; uint64_t partStartLba; + // The vtable handed to the VFS for this mount; owned by the instance + // and freed alongside it in DrvUnmount. + Vfs::FsDriver* driver; + // Superblock fields uint32_t blockSize; // 1024 << s_log_block_size uint32_t inodeSize; @@ -170,8 +174,11 @@ namespace Fs::Ext2 { // Open file handles Ext2File files[MaxFilesPerInstance]; - // ReadDir name cache - char dirNames[MaxDirEntries][MaxNameLen]; + // ReadDir name cache. 32 KiB, allocated on the first listing rather + // than at mount, so volumes that are never enumerated don't pay for + // it. Released on unmount. + char (*dirNames)[MaxNameLen]; + int dirNamesPages; int dirNameCount; }; @@ -179,8 +186,28 @@ namespace Fs::Ext2 { // Instance table // ========================================================================= - static Ext2Instance g_instances[MaxInstances] = {}; - static int g_instanceCount = 0; + // One heap allocation per mounted volume. The table holds pointers rather + // than values so growing it never moves a live instance, and an index stays + // valid for the lifetime of its mount. Unmounting nulls the slot; Mount + // reuses the lowest free one, so hot-plug cycles don't grow the table. + static kcp::vector g_instanceSlots; + + // Resolve an instance index, or nullptr if the slot is out of range, freed, + // or inactive. Every FsDriver entry point validates through this; internal + // helpers may then assume the instance is live, because the VFS holds + // vfsLock across the whole driver call and Unmount runs under that lock. + static Ext2Instance* InstanceAt(int inst) { + if (inst < 0 || (std::size_t)inst >= g_instanceSlots.size()) return nullptr; + Ext2Instance* self = g_instanceSlots[(std::size_t)inst]; + return (self != nullptr && self->active) ? self : nullptr; + } + + // Adapter keeping the `g_instances[inst]` spelling at the call sites that + // index the table by instance number. + struct InstanceTable { + Ext2Instance& operator[](int inst) { return *g_instanceSlots[(std::size_t)inst]; } + }; + static InstanceTable g_instances; // ========================================================================= // Low-level helpers @@ -1168,7 +1195,7 @@ namespace Fs::Ext2 { // ========================================================================= static int OpenImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; uint32_t inodeNum; @@ -1191,7 +1218,7 @@ namespace Fs::Ext2 { static int StatImpl(int inst, const char* path, Vfs::StatInfo* out) { if (!out) return -1; - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; uint32_t inodeNum; @@ -1209,7 +1236,7 @@ namespace Fs::Ext2 { static int ReadImpl(int inst, int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { - if (inst < 0 || inst >= g_instanceCount) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; @@ -1250,22 +1277,36 @@ namespace Fs::Ext2 { } static uint64_t GetSizeImpl(int inst, int handle) { - if (inst < 0 || inst >= g_instanceCount) return 0; + if (InstanceAt(inst) == nullptr) return 0; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return 0; return self.files[handle].inode.i_size; } static void CloseImpl(int inst, int handle) { - if (inst < 0 || inst >= g_instanceCount) return; + if (InstanceAt(inst) == nullptr) return; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance) return; self.files[handle].inUse = false; } + static constexpr int DirNamesPages = + (MaxDirEntries * MaxNameLen + 0xFFF) / 0x1000; + + static bool EnsureDirNames(Ext2Instance& self) { + if (self.dirNames != nullptr) return true; + + self.dirNames = (char(*)[MaxNameLen])Memory::g_pfa->ReallocConsecutive( + nullptr, DirNamesPages); + if (self.dirNames == nullptr) return false; + + self.dirNamesPages = DirNamesPages; + return true; + } + static int ReadDirImpl(int inst, const char* path, const char** outNames, int maxEntries, int startIndex = 0) { - if (inst < 0 || inst >= g_instanceCount) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; uint32_t inodeNum; @@ -1273,6 +1314,8 @@ namespace Fs::Ext2 { if (!TraversePath(self, path, &inodeNum, &inode)) return -1; if ((inode.i_mode & IMODE_TYPE_MASK) != IMODE_DIR) return -1; + if (!EnsureDirNames(self)) return -1; + int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries; int count = ReadDirectoryNames(self, inode, self.dirNames, limit, startIndex); @@ -1286,7 +1329,7 @@ namespace Fs::Ext2 { static int WriteImpl(int inst, int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { - if (inst < 0 || inst >= g_instanceCount) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; @@ -1358,7 +1401,7 @@ namespace Fs::Ext2 { } static int CreateImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; char parentPath[MaxNameLen]; @@ -1448,7 +1491,7 @@ namespace Fs::Ext2 { } static int DeleteImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; char parentPath[MaxNameLen]; @@ -1508,7 +1551,7 @@ namespace Fs::Ext2 { } static int MkdirImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; char parentPath[MaxNameLen]; @@ -1640,7 +1683,7 @@ namespace Fs::Ext2 { // ========================================================================= static int RenameImpl(int inst, const char* oldPath, const char* newPath) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; // Split old path @@ -1765,57 +1808,88 @@ namespace Fs::Ext2 { // ========================================================================= static const char* GetLabelImpl(int inst) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return nullptr; + if (InstanceAt(inst) == nullptr) return nullptr; return g_instances[inst].volumeLabel[0] ? g_instances[inst].volumeLabel : nullptr; } - template struct Thunks { - static int Open(const char* p) { return OpenImpl(N, p); } - static int Read(int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(N, h, b, o, s); } - static uint64_t GetSize(int h) { return GetSizeImpl(N, h); } - static void Close(int h) { CloseImpl(N, h); } - static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); } - static int ReadDirAt(const char* p, const char** o, int m, int s) { return ReadDirImpl(N, p, o, m, s); } - static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); } - static int Create(const char* p) { return CreateImpl(N, p); } - static int Delete(const char* p) { return DeleteImpl(N, p); } - static int Mkdir(const char* p) { return MkdirImpl(N, p); } - static int Rename(const char* o, const char* n) { return RenameImpl(N, o, n); } - static const char* GetLabel() { return GetLabelImpl(N); } - static int Stat(const char* p, Vfs::StatInfo* o) { return StatImpl(N, p, o); } - }; + // ctx carries the instance index. Slots are never compacted, so the index + // stays valid until the mount is torn down. + static int CtxToInst(void* ctx) { return (int)(uintptr_t)ctx; } + static void* InstToCtx(int inst) { return (void*)(uintptr_t)inst; } - template - static Vfs::FsDriver MakeDriver() { - return { - Thunks::Open, - Thunks::Read, - Thunks::GetSize, - Thunks::Close, - Thunks::ReadDir, - Thunks::Write, - Thunks::Create, - Thunks::Delete, - Thunks::Mkdir, - Thunks::Rename, - Thunks::GetLabel, - Thunks::ReadDirAt, - Thunks::Stat, - }; + static int DrvOpen(void* c, const char* p) { return OpenImpl(CtxToInst(c), p); } + static int DrvRead(void* c, int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(CtxToInst(c), h, b, o, s); } + static uint64_t DrvGetSize(void* c, int h) { return GetSizeImpl(CtxToInst(c), h); } + static void DrvClose(void* c, int h) { CloseImpl(CtxToInst(c), h); } + static int DrvReadDir(void* c, const char* p, const char** o, int m) { return ReadDirImpl(CtxToInst(c), p, o, m); } + static int DrvReadDirAt(void* c, const char* p, const char** o, int m, int s) { return ReadDirImpl(CtxToInst(c), p, o, m, s); } + static int DrvWrite(void* c, int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(CtxToInst(c), h, b, o, s); } + static int DrvCreate(void* c, const char* p) { return CreateImpl(CtxToInst(c), p); } + static int DrvDelete(void* c, const char* p) { return DeleteImpl(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 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. + static void DrvUnmount(void* c) { + int inst = CtxToInst(c); + Ext2Instance* self = InstanceAt(inst); + if (self == nullptr) return; + + if (self->blockBuf != nullptr && self->blockBufPages > 0) { + Memory::g_pfa->Free(self->blockBuf, self->blockBufPages); + } + if (self->bgdt != nullptr && self->bgdtPages > 0) { + Memory::g_pfa->Free(self->bgdt, self->bgdtPages); + } + + if (self->dirNames != nullptr && self->dirNamesPages > 0) { + Memory::g_pfa->Free(self->dirNames, self->dirNamesPages); + } + + self->active = false; + g_instanceSlots[(std::size_t)inst] = nullptr; + + Vfs::FsDriver* driver = self->driver; + Memory::g_heap->Free(self); + if (driver != nullptr) Memory::g_heap->Free(driver); } - static Vfs::FsDriver g_drivers[] = { - MakeDriver<0>(), MakeDriver<1>(), MakeDriver<2>(), MakeDriver<3>(), - MakeDriver<4>(), MakeDriver<5>(), MakeDriver<6>(), MakeDriver<7>(), - }; - // ========================================================================= // Superblock validation and mount // ========================================================================= - Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) { - if (g_instanceCount >= MaxInstances) return nullptr; + // Claim the lowest free instance slot, extending the table if all are in + // use. Returns -1 only if the heap is exhausted. + static int AllocateInstanceSlot() { + for (std::size_t i = 0; i < g_instanceSlots.size(); i++) { + if (g_instanceSlots[i] == nullptr) return (int)i; + } + g_instanceSlots.push_back(nullptr); + return (int)g_instanceSlots.size() - 1; + } + // Undo a partially built mount: release whatever was allocated so far and + // free the slot. Only used on the Mount error paths. + static Vfs::FsDriver* AbortMount(int idx) { + Ext2Instance* self = g_instanceSlots[(std::size_t)idx]; + if (self != nullptr) { + if (self->blockBuf != nullptr && self->blockBufPages > 0) { + Memory::g_pfa->Free(self->blockBuf, self->blockBufPages); + } + if (self->bgdt != nullptr && self->bgdtPages > 0) { + Memory::g_pfa->Free(self->bgdt, self->bgdtPages); + } + if (self->driver != nullptr) Memory::g_heap->Free(self->driver); + Memory::g_heap->Free(self); + } + g_instanceSlots[(std::size_t)idx] = nullptr; + return nullptr; + } + + Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) { auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex); if (!dev) return nullptr; @@ -1865,9 +1939,23 @@ namespace Fs::Ext2 { / sb->s_blocks_per_group; if (groupCount == 0) return nullptr; - // Success — initialize instance - int idx = g_instanceCount; - auto& inst = g_instances[idx]; + // Success — allocate and initialize the instance + int idx = AllocateInstanceSlot(); + if (idx < 0) return nullptr; + + auto* instPtr = (Ext2Instance*)Memory::g_heap->Request(sizeof(Ext2Instance)); + if (instPtr == nullptr) return nullptr; + memset(instPtr, 0, sizeof(Ext2Instance)); + + auto* driver = (Vfs::FsDriver*)Memory::g_heap->Request(sizeof(Vfs::FsDriver)); + if (driver == nullptr) { + Memory::g_heap->Free(instPtr); + return nullptr; + } + + g_instanceSlots[(std::size_t)idx] = instPtr; + auto& inst = *instPtr; + inst.driver = driver; inst.active = true; inst.blockDevIndex = blockDevIndex; @@ -1900,8 +1988,7 @@ namespace Fs::Ext2 { nullptr, inst.blockBufPages); } if (!inst.blockBuf) { - inst.active = false; - return nullptr; + return AbortMount(idx); } // Load block group descriptor table @@ -1918,8 +2005,7 @@ namespace Fs::Ext2 { } if (!inst.bgdt) { - inst.active = false; - return nullptr; + return AbortMount(idx); } // Read BGDT blocks @@ -1927,8 +2013,7 @@ namespace Fs::Ext2 { uint8_t* dst = (uint8_t*)inst.bgdt; for (uint32_t b = 0; b < bgdtBlocks; b++) { if (!ReadBlock(inst, bgdtStartBlock + b, inst.blockBuf)) { - inst.active = false; - return nullptr; + return AbortMount(idx); } uint32_t copyLen = bgdtBytes - b * blockSize; if (copyLen > blockSize) copyLen = blockSize; @@ -1940,13 +2025,29 @@ namespace Fs::Ext2 { inst.files[i].inUse = false; } - g_instanceCount++; + *driver = Vfs::FsDriver{ + .ctx = InstToCtx(idx), + .Open = DrvOpen, + .Read = DrvRead, + .GetSize = DrvGetSize, + .Close = DrvClose, + .ReadDir = DrvReadDir, + .Write = DrvWrite, + .Create = DrvCreate, + .Delete = DrvDelete, + .Mkdir = DrvMkdir, + .Rename = DrvRename, + .GetLabel = DrvGetLabel, + .ReadDirAt = DrvReadDirAt, + .Stat = DrvStat, + .Unmount = DrvUnmount, + }; KernelLogStream(OK, "Ext2") << "Mounted volume \"" << inst.volumeLabel << "\" (" << inst.totalBlocks << " blocks, " << blockSize << " bytes/block, " << groupCount << " groups)"; - return &g_drivers[idx]; + return driver; } void RegisterProbe() { diff --git a/kernel/src/Fs/Fat32.cpp b/kernel/src/Fs/Fat32.cpp index 042deaf..0fdecb1 100644 --- a/kernel/src/Fs/Fat32.cpp +++ b/kernel/src/Fs/Fat32.cpp @@ -10,6 +10,7 @@ #include #include #include +#include using namespace Kt; @@ -19,7 +20,6 @@ namespace Fs::Fat32 { // Constants // ========================================================================= - static constexpr int MaxInstances = 8; static constexpr int MaxFilesPerInstance = 16; static constexpr int MaxDirEntries = 128; static constexpr int MaxNameLen = 256; @@ -60,6 +60,10 @@ namespace Fs::Fat32 { int blockDevIndex; uint64_t partStartLba; + // The vtable handed to the VFS for this mount; owned by the instance + // and freed alongside it in DrvUnmount. + Vfs::FsDriver* driver; + // BPB fields uint16_t bytesPerSector; uint8_t sectorsPerCluster; @@ -87,8 +91,11 @@ namespace Fs::Fat32 { // Open file handles Fat32File files[MaxFilesPerInstance]; - // ReadDir name cache - char dirNames[MaxDirEntries][MaxNameLen]; + // ReadDir name cache. 32 KiB, allocated on the first listing rather + // than at mount, so volumes that are never enumerated don't pay for + // it. Released on unmount. + char (*dirNames)[MaxNameLen]; + int dirNamesPages; int dirNameCount; }; @@ -106,8 +113,28 @@ namespace Fs::Fat32 { // Instance table // ========================================================================= - static Fat32Instance g_instances[MaxInstances] = {}; - static int g_instanceCount = 0; + // One heap allocation per mounted volume. The table holds pointers rather + // than values so growing it never moves a live instance, and an index stays + // valid for the lifetime of its mount. Unmounting nulls the slot; Mount + // reuses the lowest free one, so hot-plug cycles don't grow the table. + static kcp::vector g_instanceSlots; + + // Resolve an instance index, or nullptr if the slot is out of range, freed, + // or inactive. Every FsDriver entry point validates through this; internal + // helpers may then assume the instance is live, because the VFS holds + // vfsLock across the whole driver call and Unmount runs under that lock. + static Fat32Instance* InstanceAt(int inst) { + if (inst < 0 || (std::size_t)inst >= g_instanceSlots.size()) return nullptr; + Fat32Instance* self = g_instanceSlots[(std::size_t)inst]; + return (self != nullptr && self->active) ? self : nullptr; + } + + // Adapter keeping the `g_instances[inst]` spelling at the ~40 call sites + // that index the table by instance number. + struct InstanceTable { + Fat32Instance& operator[](int inst) { return *g_instanceSlots[(std::size_t)inst]; } + }; + static InstanceTable g_instances; // ========================================================================= // Low-level helpers @@ -902,7 +929,7 @@ namespace Fs::Fat32 { // ========================================================================= static int OpenImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; ParsedEntry entry; if (!TraversePath(inst, path, &entry)) return -1; @@ -928,7 +955,7 @@ namespace Fs::Fat32 { static int ReadImpl(int inst, int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { - if (inst < 0 || inst >= g_instanceCount) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; @@ -988,28 +1015,44 @@ namespace Fs::Fat32 { } static uint64_t GetSizeImpl(int inst, int handle) { - if (inst < 0 || inst >= g_instanceCount) return 0; + if (InstanceAt(inst) == nullptr) return 0; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return 0; return self.files[handle].fileSize; } static void CloseImpl(int inst, int handle) { - if (inst < 0 || inst >= g_instanceCount) return; + if (InstanceAt(inst) == nullptr) return; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance) return; self.files[handle].inUse = false; } + static constexpr int DirNamesPages = + (MaxDirEntries * MaxNameLen + 0xFFF) / 0x1000; + + static bool EnsureDirNames(Fat32Instance& self) { + if (self.dirNames != nullptr) return true; + + self.dirNames = (char(*)[MaxNameLen])Memory::g_pfa->ReallocConsecutive( + nullptr, DirNamesPages); + if (self.dirNames == nullptr) return false; + + self.dirNamesPages = DirNamesPages; + return true; + } + static int ReadDirImpl(int inst, const char* path, const char** outNames, int maxEntries, int startIndex = 0) { - if (inst < 0 || inst >= g_instanceCount) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; ParsedEntry dirEntry; if (!TraversePath(inst, path, &dirEntry)) return -1; if (!(dirEntry.attributes & ATTR_DIRECTORY)) return -1; + if (!EnsureDirNames(self)) return -1; + int limit = maxEntries < MaxDirEntries ? maxEntries : MaxDirEntries; int count = ReadDirectoryNames(inst, dirEntry.firstCluster, self.dirNames, limit, startIndex); @@ -1023,7 +1066,7 @@ namespace Fs::Fat32 { static int WriteImpl(int inst, int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { - if (inst < 0 || inst >= g_instanceCount) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; if (handle < 0 || handle >= MaxFilesPerInstance || !self.files[handle].inUse) return -1; @@ -1126,7 +1169,7 @@ namespace Fs::Fat32 { } static int CreateImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; // Split path into parent directory and filename @@ -1274,7 +1317,7 @@ namespace Fs::Fat32 { } static int DeleteImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; // Split path into parent directory and filename @@ -1410,7 +1453,7 @@ namespace Fs::Fat32 { // ========================================================================= static int MkdirImpl(int inst, const char* path) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; // Split path into parent directory and new dir name @@ -1540,7 +1583,7 @@ namespace Fs::Fat32 { // ========================================================================= static int RenameImpl(int inst, const char* oldPath, const char* newPath) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return -1; + if (InstanceAt(inst) == nullptr) return -1; auto& self = g_instances[inst]; // Split old path @@ -1754,58 +1797,72 @@ namespace Fs::Fat32 { // ========================================================================= static const char* GetLabelImpl(int inst) { - if (inst < 0 || inst >= g_instanceCount || !g_instances[inst].active) return nullptr; + if (InstanceAt(inst) == nullptr) return nullptr; const char* label = g_instances[inst].volumeLabel; if (label[0] == '\0' || StrEqualNoCase(label, "NO NAME")) return nullptr; return label; } - template struct Thunks { - static int Open(const char* p) { return OpenImpl(N, p); } - static int Read(int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(N, h, b, o, s); } - static uint64_t GetSize(int h) { return GetSizeImpl(N, h); } - static void Close(int h) { CloseImpl(N, h); } - static int ReadDir(const char* p, const char** o, int m) { return ReadDirImpl(N, p, o, m); } - static int ReadDirAt(const char* p, const char** o, int m, int s) { return ReadDirImpl(N, p, o, m, s); } - static int Write(int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(N, h, b, o, s); } - static int Create(const char* p) { return CreateImpl(N, p); } - static int Delete(const char* p) { return DeleteImpl(N, p); } - static int Mkdir(const char* p) { return MkdirImpl(N, p); } - static int Rename(const char* o, const char* n) { return RenameImpl(N, o, n); } - static const char* GetLabel() { return GetLabelImpl(N); } - }; + // ctx carries the instance index. Slots are never compacted, so the index + // stays valid until the mount is torn down. + static int CtxToInst(void* ctx) { return (int)(uintptr_t)ctx; } + static void* InstToCtx(int inst) { return (void*)(uintptr_t)inst; } - template - static Vfs::FsDriver MakeDriver() { - return { - Thunks::Open, - Thunks::Read, - Thunks::GetSize, - Thunks::Close, - Thunks::ReadDir, - Thunks::Write, - Thunks::Create, - Thunks::Delete, - Thunks::Mkdir, - Thunks::Rename, - Thunks::GetLabel, - Thunks::ReadDirAt, - }; + static int DrvOpen(void* c, const char* p) { return OpenImpl(CtxToInst(c), p); } + static int DrvRead(void* c, int h, uint8_t* b, uint64_t o, uint64_t s) { return ReadImpl(CtxToInst(c), h, b, o, s); } + static uint64_t DrvGetSize(void* c, int h) { return GetSizeImpl(CtxToInst(c), h); } + static void DrvClose(void* c, int h) { CloseImpl(CtxToInst(c), h); } + static int DrvReadDir(void* c, const char* p, const char** o, int m) { return ReadDirImpl(CtxToInst(c), p, o, m); } + static int DrvReadDirAt(void* c, const char* p, const char** o, int m, int s) { return ReadDirImpl(CtxToInst(c), p, o, m, s); } + static int DrvWrite(void* c, int h, const uint8_t* b, uint64_t o, uint64_t s) { return WriteImpl(CtxToInst(c), h, b, o, s); } + static int DrvCreate(void* c, const char* p) { return CreateImpl(CtxToInst(c), p); } + static int DrvDelete(void* c, const char* p) { return DeleteImpl(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 const char* DrvGetLabel(void* c) { return GetLabelImpl(CtxToInst(c)); } + + // Release everything the mount owns. The driver is either not registered + // yet, or the VFS has deactivated its drive and drained dispatches. + static void DrvUnmount(void* c) { + int inst = CtxToInst(c); + Fat32Instance* self = InstanceAt(inst); + if (self == nullptr) return; + + if (self->clusterBuf != nullptr && self->clusterBufPages > 0) { + Memory::g_pfa->Free(self->clusterBuf, self->clusterBufPages); + } + if (self->fatCache != nullptr && self->fatCachePages > 0) { + Memory::g_pfa->Free(self->fatCache, self->fatCachePages); + } + + if (self->dirNames != nullptr && self->dirNamesPages > 0) { + Memory::g_pfa->Free(self->dirNames, self->dirNamesPages); + } + + self->active = false; + g_instanceSlots[(std::size_t)inst] = nullptr; + + Vfs::FsDriver* driver = self->driver; + Memory::g_heap->Free(self); + if (driver != nullptr) Memory::g_heap->Free(driver); } - static Vfs::FsDriver g_drivers[] = { - MakeDriver<0>(), MakeDriver<1>(), MakeDriver<2>(), MakeDriver<3>(), - MakeDriver<4>(), MakeDriver<5>(), MakeDriver<6>(), MakeDriver<7>(), - }; - // ========================================================================= // BPB validation and mount // ========================================================================= - Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) { - if (g_instanceCount >= MaxInstances) return nullptr; + // Claim the lowest free instance slot, extending the table if all are in + // use. Returns -1 only if the heap is exhausted. + static int AllocateInstanceSlot() { + for (std::size_t i = 0; i < g_instanceSlots.size(); i++) { + if (g_instanceSlots[i] == nullptr) return (int)i; + } + g_instanceSlots.push_back(nullptr); + return (int)g_instanceSlots.size() - 1; + } + Vfs::FsDriver* Mount(int blockDevIndex, uint64_t startLba, uint64_t sectorCount) { auto* dev = Drivers::Storage::GetBlockDevice(blockDevIndex); if (!dev) return nullptr; @@ -1873,9 +1930,23 @@ namespace Fs::Fat32 { // At least one of: valid cluster count or FS type string if (!hasFat32Str && clusterCount < 65525) return nullptr; - // Success — initialize instance - int idx = g_instanceCount; - auto& inst = g_instances[idx]; + // Success — allocate and initialize the instance + int idx = AllocateInstanceSlot(); + if (idx < 0) return nullptr; + + auto* instPtr = (Fat32Instance*)Memory::g_heap->Request(sizeof(Fat32Instance)); + if (instPtr == nullptr) return nullptr; + memset(instPtr, 0, sizeof(Fat32Instance)); + + auto* driver = (Vfs::FsDriver*)Memory::g_heap->Request(sizeof(Vfs::FsDriver)); + if (driver == nullptr) { + Memory::g_heap->Free(instPtr); + return nullptr; + } + + g_instanceSlots[(std::size_t)idx] = instPtr; + auto& inst = *instPtr; + inst.driver = driver; inst.active = true; inst.blockDevIndex = blockDevIndex; @@ -1924,8 +1995,12 @@ namespace Fs::Fat32 { uint32_t chunk = (remaining > 4096) ? 4096 : (uint32_t)remaining; uint32_t secs = (chunk + bytesPerSector - 1) / bytesPerSector; if (!ReadPartSectors(inst, fatPartSector, secs, dst)) { - // If read fails, disable cache and fall back to per-lookup reads + // If read fails, disable cache and fall back to per-lookup + // reads. Release the pages rather than orphaning them. + Memory::g_pfa->Free(inst.fatCache, inst.fatCachePages); inst.fatCache = nullptr; + inst.fatCachePages = 0; + inst.fatCacheEntries = 0; break; } dst += secs * bytesPerSector; @@ -1939,13 +2014,29 @@ namespace Fs::Fat32 { inst.files[i].inUse = false; } - g_instanceCount++; + *driver = Vfs::FsDriver{ + .ctx = InstToCtx(idx), + .Open = DrvOpen, + .Read = DrvRead, + .GetSize = DrvGetSize, + .Close = DrvClose, + .ReadDir = DrvReadDir, + .Write = DrvWrite, + .Create = DrvCreate, + .Delete = DrvDelete, + .Mkdir = DrvMkdir, + .Rename = DrvRename, + .GetLabel = DrvGetLabel, + .ReadDirAt = DrvReadDirAt, + .Stat = nullptr, + .Unmount = DrvUnmount, + }; KernelLogStream(OK, "FAT32") << "Mounted volume \"" << inst.volumeLabel << "\" (" << clusterCount << " clusters, " << (uint64_t)inst.clusterSize << " bytes/cluster)"; - return &g_drivers[idx]; + return driver; } void RegisterProbe() { diff --git a/kernel/src/Fs/FsProbe.cpp b/kernel/src/Fs/FsProbe.cpp index 5ccfb2d..01b3dd6 100644 --- a/kernel/src/Fs/FsProbe.cpp +++ b/kernel/src/Fs/FsProbe.cpp @@ -17,6 +17,15 @@ namespace Fs::FsProbe { static bool g_mounted[Drivers::Storage::Gpt::MaxPartitions] = {}; static int g_driveForPart[Drivers::Storage::Gpt::MaxPartitions] = {}; + // A successful probe returns an owned mount. Registration transfers that + // ownership to the VFS; if registration loses a drive-slot race, release + // the mount here so its driver, instance, and page allocations do not leak. + static void DiscardUnregisteredDriver(Vfs::FsDriver* driver) { + if (driver != nullptr && driver->Unmount != nullptr) { + driver->Unmount(driver->ctx); + } + } + void Register(ProbeFn fn) { if (g_probeCount < MaxProbes && fn) { g_probes[g_probeCount++] = fn; @@ -60,6 +69,7 @@ namespace Fs::FsProbe { return 1; } + DiscardUnregisteredDriver(driver); return -1; } @@ -135,6 +145,7 @@ namespace Fs::FsProbe { << partIndex << " as drive " << driveNum; return 0; } + DiscardUnregisteredDriver(driver); return -1; } } diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index 134518b..7647eca 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -19,6 +19,11 @@ namespace Fs::Vfs { // flips driveActive so an interrupted dispatch never sees a null driver. static kcp::Mutex vfsLock; + // Upper bound on digits in a drive number. Without it the accumulator in + // ParsePath overflows on a long digit run and wraps into a valid drive -- + // "4294967296:/x" would otherwise resolve to drive 0. + static constexpr int MaxDriveDigits = 4; + // Parse "N:/path" into drive number and local path. // Returns true on success, sets outDrive and outPath. static bool ParsePath(const char* path, int& outDrive, const char*& outPath) { @@ -27,15 +32,15 @@ namespace Fs::Vfs { // Parse decimal drive number before ':' int drive = 0; int i = 0; - bool hasDigit = false; + int digits = 0; while (path[i] >= '0' && path[i] <= '9') { + if (++digits > MaxDriveDigits) return false; drive = drive * 10 + (path[i] - '0'); - hasDigit = true; i++; } - if (!hasDigit) return false; + if (digits == 0) return false; if (path[i] != ':') return false; // Everything after "N:" is the local path @@ -44,6 +49,27 @@ namespace Fs::Vfs { return true; } + // Resolve a drive number to its driver, or nullptr if the slot is out of + // range, unregistered, or empty. Caller must hold vfsLock: the returned + // pointer is only valid for as long as the lock is held. + static FsDriver* DriverForLocked(int driveNumber) { + if (driveNumber < 0 || driveNumber >= MaxDrives) return nullptr; + if (!driveActive[driveNumber]) return nullptr; + return driveTable[driveNumber]; + } + + // As DriverForLocked, but for an already-open handle: also rejects handles + // whose generation stamp is stale (the drive was unmounted and reused). + static FsDriver* DriverForFileLocked(const BackendFile& file) { + if (file.localHandle < 0) return nullptr; + + FsDriver* driver = DriverForLocked(file.driveNumber); + if (driver == nullptr) return nullptr; + if (file.generation != driveGeneration[file.driveNumber]) return nullptr; + + return driver; + } + static void BumpDriveGeneration(int driveNumber) { driveGeneration[driveNumber]++; if (driveGeneration[driveNumber] == 0) { @@ -95,7 +121,16 @@ namespace Fs::Vfs { return -1; } driveActive[driveNumber] = false; + FsDriver* driver = driveTable[driveNumber]; + driveTable[driveNumber] = nullptr; BumpDriveGeneration(driveNumber); + + // Safe to tear down here: every dispatch path runs under vfsLock, so + // holding it means no call into this driver can still be in flight. + // Unmount frees the FsDriver itself, so nothing may touch it after. + if (driver != nullptr && driver->Unmount != nullptr) { + driver->Unmount(driver->ctx); + } vfsLock.Release(); Kt::KernelLogStream(Kt::OK, "VFS") << "Unregistered drive " << driveNumber; @@ -135,12 +170,15 @@ namespace Fs::Vfs { const char* localPath; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[drive]; + FsDriver* driver = DriverForLocked(drive); + if (driver == nullptr || driver->Open == nullptr) { + vfsLock.Release(); + return -1; + } uint32_t generation = driveGeneration[drive]; - int localHandle = (driveActive[drive] && driver) ? driver->Open(localPath) : -1; + int localHandle = driver->Open(driver->ctx, localPath); vfsLock.Release(); if (localHandle < 0) return -1; @@ -152,72 +190,44 @@ namespace Fs::Vfs { int ReadBackendFile(const BackendFile& file, uint8_t* buffer, uint64_t offset, uint64_t size) { vfsLock.Acquire(); - FsDriver* driver = (file.driveNumber >= 0 && file.driveNumber < MaxDrives) - ? driveTable[file.driveNumber] - : nullptr; - if (file.driveNumber < 0 || file.driveNumber >= MaxDrives || !driveActive[file.driveNumber] || - driver == nullptr || - file.generation != driveGeneration[file.driveNumber] || - file.localHandle < 0) { + FsDriver* driver = DriverForFileLocked(file); + if (driver == nullptr || driver->Read == nullptr) { vfsLock.Release(); return -1; } - int result = driver->Read(file.localHandle, buffer, offset, size); + int result = driver->Read(driver->ctx, file.localHandle, buffer, offset, size); vfsLock.Release(); return result; } uint64_t GetBackendFileSize(const BackendFile& file) { vfsLock.Acquire(); - FsDriver* driver = (file.driveNumber >= 0 && file.driveNumber < MaxDrives) - ? driveTable[file.driveNumber] - : nullptr; - if (file.driveNumber < 0 || file.driveNumber >= MaxDrives || !driveActive[file.driveNumber] || - driver == nullptr || - file.generation != driveGeneration[file.driveNumber] || - file.localHandle < 0) { + FsDriver* driver = DriverForFileLocked(file); + if (driver == nullptr || driver->GetSize == nullptr) { vfsLock.Release(); return 0; } - uint64_t result = driver->GetSize(file.localHandle); + uint64_t result = driver->GetSize(driver->ctx, file.localHandle); vfsLock.Release(); return result; } bool BackendFileCanWrite(const BackendFile& file) { vfsLock.Acquire(); - FsDriver* driver = (file.driveNumber >= 0 && file.driveNumber < MaxDrives) - ? driveTable[file.driveNumber] - : nullptr; - bool canWrite = file.driveNumber >= 0 && file.driveNumber < MaxDrives && - driveActive[file.driveNumber] && - driver != nullptr && - file.generation == driveGeneration[file.driveNumber] && - file.localHandle >= 0 && - driver->Write != nullptr; + FsDriver* driver = DriverForFileLocked(file); + bool canWrite = driver != nullptr && driver->Write != nullptr; vfsLock.Release(); return canWrite; } void CloseBackendFile(BackendFile& file) { vfsLock.Acquire(); - FsDriver* driver = (file.driveNumber >= 0 && file.driveNumber < MaxDrives) - ? driveTable[file.driveNumber] - : nullptr; - if (file.driveNumber < 0 || file.driveNumber >= MaxDrives || !driveActive[file.driveNumber] || - driver == nullptr || - file.generation != driveGeneration[file.driveNumber] || - file.localHandle < 0) { - vfsLock.Release(); - file.driveNumber = -1; - file.localHandle = -1; - file.generation = 0; - return; + FsDriver* driver = DriverForFileLocked(file); + if (driver != nullptr && driver->Close != nullptr) { + driver->Close(driver->ctx, file.localHandle); } - - driver->Close(file.localHandle); vfsLock.Release(); file.driveNumber = -1; @@ -227,19 +237,13 @@ namespace Fs::Vfs { int WriteBackendFile(const BackendFile& file, const uint8_t* buffer, uint64_t offset, uint64_t size) { vfsLock.Acquire(); - FsDriver* driver = (file.driveNumber >= 0 && file.driveNumber < MaxDrives) - ? driveTable[file.driveNumber] - : nullptr; - if (file.driveNumber < 0 || file.driveNumber >= MaxDrives || !driveActive[file.driveNumber] || - driver == nullptr || - file.generation != driveGeneration[file.driveNumber] || - file.localHandle < 0) { + FsDriver* driver = DriverForFileLocked(file); + if (driver == nullptr || driver->Write == nullptr) { vfsLock.Release(); return -1; } - if (driver->Write == nullptr) { vfsLock.Release(); return -1; } - int result = driver->Write(file.localHandle, buffer, offset, size); + int result = driver->Write(driver->ctx, file.localHandle, buffer, offset, size); vfsLock.Release(); return result; } @@ -253,15 +257,15 @@ namespace Fs::Vfs { const char* localPath; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; - if (driveTable[drive]->Create == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[drive]; + FsDriver* driver = DriverForLocked(drive); + if (driver == nullptr || driver->Create == nullptr) { + vfsLock.Release(); + return -1; + } uint32_t generation = driveGeneration[drive]; - int localHandle = (driveActive[drive] && driver && driver->Create) - ? driver->Create(localPath) - : -1; + int localHandle = driver->Create(driver->ctx, localPath); vfsLock.Release(); if (localHandle < 0) return -1; @@ -276,13 +280,11 @@ namespace Fs::Vfs { const char* localPath; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; - if (driveTable[drive]->Delete == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[drive]; - int result = (driveActive[drive] && driver && driver->Delete) - ? driver->Delete(localPath) + FsDriver* driver = DriverForLocked(drive); + int result = (driver != nullptr && driver->Delete != nullptr) + ? driver->Delete(driver->ctx, localPath) : -1; vfsLock.Release(); return result; @@ -295,13 +297,11 @@ namespace Fs::Vfs { out = StatInfo{}; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; - if (driveTable[drive]->Stat == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[drive]; - int result = (driveActive[drive] && driver && driver->Stat) - ? driver->Stat(localPath, &out) + FsDriver* driver = DriverForLocked(drive); + int result = (driver != nullptr && driver->Stat != nullptr) + ? driver->Stat(driver->ctx, localPath, &out) : -1; vfsLock.Release(); return result; @@ -312,19 +312,19 @@ namespace Fs::Vfs { const char* localPath; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; - if (driveTable[drive]->Mkdir == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[drive]; - int result = (driveActive[drive] && driver && driver->Mkdir) - ? driver->Mkdir(localPath) + FsDriver* driver = DriverForLocked(drive); + int result = (driver != nullptr && driver->Mkdir != nullptr) + ? driver->Mkdir(driver->ctx, localPath) : -1; vfsLock.Release(); return result; } int VfsDriveList(int* outDrives, int maxEntries) { + if (outDrives == nullptr || maxEntries <= 0) return 0; + vfsLock.Acquire(); int count = 0; for (int i = 0; i < MaxDrives && count < maxEntries; i++) { @@ -341,18 +341,18 @@ namespace Fs::Vfs { outLabel[0] = '\0'; vfsLock.Acquire(); - if (driveNumber < 0 || driveNumber >= MaxDrives || !driveActive[driveNumber] || - driveTable[driveNumber] == nullptr) { + FsDriver* driver = DriverForLocked(driveNumber); + if (driver == nullptr) { vfsLock.Release(); return -1; } - if (driveTable[driveNumber]->GetLabel == nullptr) { + if (driver->GetLabel == nullptr) { vfsLock.Release(); return 0; } - const char* label = driveTable[driveNumber]->GetLabel(); + const char* label = driver->GetLabel(driver->ctx); if (label == nullptr || label[0] == '\0') { vfsLock.Release(); return 0; @@ -379,14 +379,11 @@ namespace Fs::Vfs { // Cross-drive rename not supported if (oldDrive != newDrive) return -1; - if (oldDrive < 0 || oldDrive >= MaxDrives || !driveActive[oldDrive] || - driveTable[oldDrive] == nullptr) return -1; - if (driveTable[oldDrive]->Rename == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[oldDrive]; - int result = (driveActive[oldDrive] && driver && driver->Rename) - ? driver->Rename(oldLocal, newLocal) + FsDriver* driver = DriverForLocked(oldDrive); + int result = (driver != nullptr && driver->Rename != nullptr) + ? driver->Rename(driver->ctx, oldLocal, newLocal) : -1; vfsLock.Release(); return result; @@ -402,18 +399,17 @@ namespace Fs::Vfs { if (startIndex < 0) return -1; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || !driveActive[drive] || driveTable[drive] == nullptr) return -1; vfsLock.Acquire(); - FsDriver* driver = driveTable[drive]; + FsDriver* driver = DriverForLocked(drive); int result; - if (!driveActive[drive] || !driver) { + if (driver == nullptr) { result = -1; } else if (driver->ReadDirAt) { - result = driver->ReadDirAt(localPath, outNames, maxEntries, startIndex); + result = driver->ReadDirAt(driver->ctx, localPath, outNames, maxEntries, startIndex); } else if (driver->ReadDir) { // Driver without pagination support: only the first page is reachable. - result = (startIndex == 0) ? driver->ReadDir(localPath, outNames, maxEntries) : 0; + result = (startIndex == 0) ? driver->ReadDir(driver->ctx, localPath, outNames, maxEntries) : 0; } else { result = -1; } diff --git a/kernel/src/Fs/Vfs.hpp b/kernel/src/Fs/Vfs.hpp index f884cf0..8fd03d8 100644 --- a/kernel/src/Fs/Vfs.hpp +++ b/kernel/src/Fs/Vfs.hpp @@ -10,7 +10,15 @@ namespace Fs::Vfs { - static constexpr int MaxDrives = 16; + // Size of the drive-number namespace. This is a sanity bound, not a + // resource limit: drive numbers arrive as text in paths ("N:/..."), so the + // range has to be bounded somewhere, and the table costs only 13 bytes per + // slot. The number of mountable volumes is limited by memory alone -- the + // FS drivers allocate one instance per mount. + // + // Keep at or below 99: the shell's drive-number formatter + // (programs/src/shell/shell.h) emits at most two digits. + static constexpr int MaxDrives = 64; struct BackendFile { int driveNumber; @@ -29,24 +37,34 @@ namespace Fs::Vfs { bool isDir; // true if the entry is a directory }; + // A mounted filesystem. Every entry point takes the driver's own `ctx` so a + // driver can serve any number of concurrent mounts from one set of function + // pointers; drivers with a single global mount (e.g. the ramdisk) ignore it. struct FsDriver { - int (*Open)(const char* path); - int (*Read)(int handle, uint8_t* buffer, uint64_t offset, uint64_t size); - uint64_t (*GetSize)(int handle); - void (*Close)(int handle); - int (*ReadDir)(const char* path, const char** outNames, int maxEntries); - int (*Write)(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); - int (*Create)(const char* path); - int (*Delete)(const char* path); - int (*Mkdir)(const char* path); - int (*Rename)(const char* oldPath, const char* newPath); - const char* (*GetLabel)(); + void* ctx; + int (*Open)(void* ctx, const char* path); + int (*Read)(void* ctx, int handle, uint8_t* buffer, uint64_t offset, uint64_t size); + uint64_t (*GetSize)(void* ctx, int handle); + void (*Close)(void* ctx, int handle); + int (*ReadDir)(void* ctx, const char* path, const char** outNames, int maxEntries); + int (*Write)(void* ctx, int handle, const uint8_t* buffer, uint64_t offset, uint64_t size); + int (*Create)(void* ctx, const char* path); + int (*Delete)(void* ctx, const char* path); + int (*Mkdir)(void* ctx, const char* path); + int (*Rename)(void* ctx, const char* oldPath, const char* newPath); + const char* (*GetLabel)(void* ctx); // Optional: paginated directory read returning entries [startIndex, startIndex+maxEntries). // Drivers that leave this null are read via ReadDir at startIndex 0 only. - int (*ReadDirAt)(const char* path, const char** outNames, int maxEntries, int startIndex); + int (*ReadDirAt)(void* ctx, const char* path, const char** outNames, int maxEntries, int startIndex); // Optional: fill metadata for a path. Drivers that leave this null do // not support stat and VfsStat returns -1 for their paths. - int (*Stat)(const char* path, StatInfo* out); + int (*Stat)(void* ctx, const char* path, StatInfo* out); + // Optional: release everything this mount owns, including the FsDriver + // itself. Called either when registration fails, before the driver has + // become reachable, or by UnregisterDrive once the slot is deactivated + // and no dispatch can still be in flight. Drivers with statically + // allocated state (e.g. the ramdisk) leave this null. + void (*Unmount)(void* ctx); }; void Initialize(); diff --git a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp index 06fe7bc..43ddf72 100644 --- a/programs/src/desktop/apps/filemanager/filemanager_internal.hpp +++ b/programs/src/desktop/apps/filemanager/filemanager_internal.hpp @@ -11,7 +11,8 @@ namespace filemanager { -inline constexpr int FM_MAX_DRIVES = 16; +// Matches Fs::Vfs::MaxDrives; sizes the buffer passed to montauk::drivelist(). +inline constexpr int FM_MAX_DRIVES = 64; enum FileManagerEntryType : int { FM_ENTRY_FILE = 0, diff --git a/programs/src/desktop/apps/filemanager/filesystem.cpp b/programs/src/desktop/apps/filemanager/filesystem.cpp index f41fc0f..f1ad7b1 100644 --- a/programs/src/desktop/apps/filemanager/filesystem.cpp +++ b/programs/src/desktop/apps/filemanager/filesystem.cpp @@ -238,18 +238,12 @@ void filemanager_read_drives(FileManagerState* fm) { for (int di = 0; di < driveCount; di++) { int d = drives[di]; char probe[8]; - if (d < 10) { - probe[0] = '0' + d; - probe[1] = ':'; - probe[2] = '/'; - probe[3] = '\0'; - } else { - probe[0] = '1'; - probe[1] = '0' + (d - 10); - probe[2] = ':'; - probe[3] = '/'; - probe[4] = '\0'; - } + int pi = 0; + if (d >= 10) probe[pi++] = (char)('0' + (d / 10)); + probe[pi++] = (char)('0' + (d % 10)); + probe[pi++] = ':'; + probe[pi++] = '/'; + probe[pi] = '\0'; char label[64]; montauk::strcpy(label, "Drive "); str_append(label, probe, 64); diff --git a/programs/src/dialogs/filedialog.hpp b/programs/src/dialogs/filedialog.hpp index 4fabce8..2748142 100644 --- a/programs/src/dialogs/filedialog.hpp +++ b/programs/src/dialogs/filedialog.hpp @@ -42,9 +42,11 @@ constexpr int GRID_CELL_H = 80; constexpr int GRID_ICON = 48; constexpr int GRID_PAD = 4; -constexpr int MAX_ENTRIES = 64; constexpr int MAX_HISTORY = 16; -constexpr int MAX_DRIVES = 16; +// Matches Fs::Vfs::MaxDrives; sizes the buffer passed to montauk::drivelist(). +constexpr int MAX_DRIVES = 64; +// The drives root can contain all drives plus the six special home folders. +constexpr int MAX_ENTRIES = MAX_DRIVES + 6; constexpr int BUTTON_H = 30; constexpr int BUTTON_W = 88; diff --git a/programs/src/disks/actions.cpp b/programs/src/disks/actions.cpp index 71210fc..4a72055 100644 --- a/programs/src/disks/actions.cpp +++ b/programs/src/disks/actions.cpp @@ -150,7 +150,33 @@ void do_mount_partition() { int global_idx = part_indices[dt.selected_part]; int driveNum = 1 + global_idx; - if (driveNum >= 16) driveNum = 15; + + int mountedDrives[MAX_DRIVES]; + int mountedCount = montauk::drivelist(mountedDrives, MAX_DRIVES); + bool occupied[MAX_DRIVES] = {}; + for (int i = 0; i < mountedCount; i++) { + int mountedDrive = mountedDrives[i]; + if (mountedDrive >= 0 && mountedDrive < MAX_DRIVES) { + occupied[mountedDrive] = true; + } + } + + // Preserve the stable partition-to-drive mapping when it is available. + // Otherwise use the lowest free non-ramdisk drive in the full namespace. + if (driveNum >= MAX_DRIVES || occupied[driveNum]) { + driveNum = -1; + for (int candidate = 1; candidate < MAX_DRIVES; candidate++) { + if (!occupied[candidate]) { + driveNum = candidate; + break; + } + } + } + + if (driveNum < 0) { + set_status("Mount failed (no free drive slot)"); + return; + } int r = montauk::fs_mount(global_idx, driveNum); if (r < 0) { diff --git a/programs/src/disks/disks.h b/programs/src/disks/disks.h index b4c46c0..8a7b61a 100644 --- a/programs/src/disks/disks.h +++ b/programs/src/disks/disks.h @@ -32,6 +32,8 @@ static constexpr int MAP_H = 48; static constexpr int MAP_PAD = 16; static constexpr int MAX_PARTS = 32; static constexpr int MAX_DISKS = 32; +// Matches Fs::Vfs::MaxDrives; used for selecting a free mount target. +static constexpr int MAX_DRIVES = 64; static constexpr int STATUS_H = 44; static constexpr int TB_BTN_Y = 7;