454 lines
14 KiB
C++
454 lines
14 KiB
C++
/*
|
|
* Vfs.cpp
|
|
* Virtual File System with numerical logical drive identifiers
|
|
* Copyright (c) 2025 Daniel Hammer
|
|
*/
|
|
|
|
#include "Vfs.hpp"
|
|
#include <Terminal/Terminal.hpp>
|
|
#include <CppLib/Spinlock.hpp>
|
|
|
|
namespace Fs::Vfs {
|
|
|
|
static FsDriver* driveTable[MaxDrives];
|
|
static bool driveActive[MaxDrives];
|
|
static uint32_t driveGeneration[MaxDrives];
|
|
static bool initialized = false;
|
|
|
|
// Protects driver dispatch from concurrent CPU access. Hot-unplug only
|
|
// 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) {
|
|
if (path == nullptr) return false;
|
|
|
|
// Parse decimal drive number before ':'
|
|
int drive = 0;
|
|
int i = 0;
|
|
int digits = 0;
|
|
|
|
while (path[i] >= '0' && path[i] <= '9') {
|
|
if (++digits > MaxDriveDigits) return false;
|
|
drive = drive * 10 + (path[i] - '0');
|
|
i++;
|
|
}
|
|
|
|
if (digits == 0) return false;
|
|
if (path[i] != ':') return false;
|
|
|
|
// Everything after "N:" is the local path
|
|
outDrive = drive;
|
|
outPath = &path[i + 1];
|
|
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) {
|
|
driveGeneration[driveNumber] = 1;
|
|
}
|
|
}
|
|
|
|
void Initialize() {
|
|
for (int i = 0; i < MaxDrives; i++) {
|
|
driveTable[i] = nullptr;
|
|
driveActive[i] = false;
|
|
driveGeneration[i] = 1;
|
|
}
|
|
|
|
initialized = true;
|
|
Kt::KernelLogStream(Kt::OK, "VFS") << "Initialized (" << MaxDrives << " drives)";
|
|
}
|
|
|
|
bool IsInitialized() {
|
|
return initialized;
|
|
}
|
|
|
|
int RegisterDrive(int driveNumber, FsDriver* driver) {
|
|
if (driveNumber < 0 || driveNumber >= MaxDrives) return -1;
|
|
if (driver == nullptr) return -1;
|
|
if (!initialized) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
if (driveActive[driveNumber]) {
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
driveTable[driveNumber] = driver;
|
|
BumpDriveGeneration(driveNumber);
|
|
driveActive[driveNumber] = true;
|
|
vfsLock.Release();
|
|
|
|
Kt::KernelLogStream(Kt::OK, "VFS") << "Registered drive " << driveNumber;
|
|
return 0;
|
|
}
|
|
|
|
int UnregisterDrive(int driveNumber) {
|
|
if (!initialized) return -1;
|
|
if (driveNumber < 0 || driveNumber >= MaxDrives) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
if (!driveActive[driveNumber]) {
|
|
vfsLock.Release();
|
|
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;
|
|
return 0;
|
|
}
|
|
|
|
bool IsDriveRegistered(int driveNumber) {
|
|
if (driveNumber < 0 || driveNumber >= MaxDrives) return false;
|
|
vfsLock.Acquire();
|
|
bool registered = driveActive[driveNumber];
|
|
vfsLock.Release();
|
|
return registered;
|
|
}
|
|
|
|
int FindLowestAvailableDrive(int firstDrive) {
|
|
if (!initialized) return -1;
|
|
if (firstDrive < 0) firstDrive = 0;
|
|
|
|
vfsLock.Acquire();
|
|
for (int i = firstDrive; i < MaxDrives; i++) {
|
|
if (!driveActive[i]) {
|
|
vfsLock.Release();
|
|
return i;
|
|
}
|
|
}
|
|
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
|
|
int OpenBackendFile(const char* path, BackendFile& outFile) {
|
|
outFile.driveNumber = -1;
|
|
outFile.localHandle = -1;
|
|
outFile.generation = 0;
|
|
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(drive);
|
|
if (driver == nullptr || driver->Open == nullptr) {
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
uint32_t generation = driveGeneration[drive];
|
|
int localHandle = driver->Open(driver->ctx, localPath);
|
|
vfsLock.Release();
|
|
if (localHandle < 0) return -1;
|
|
|
|
outFile.driveNumber = drive;
|
|
outFile.localHandle = localHandle;
|
|
outFile.generation = generation;
|
|
return 0;
|
|
}
|
|
|
|
int ReadBackendFile(const BackendFile& file, uint8_t* buffer, uint64_t offset, uint64_t size) {
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForFileLocked(file);
|
|
if (driver == nullptr || driver->Read == nullptr) {
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
|
|
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 = DriverForFileLocked(file);
|
|
if (driver == nullptr || driver->GetSize == nullptr) {
|
|
vfsLock.Release();
|
|
return 0;
|
|
}
|
|
|
|
uint64_t result = driver->GetSize(driver->ctx, file.localHandle);
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
bool BackendFileCanWrite(const BackendFile& file) {
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForFileLocked(file);
|
|
bool canWrite = driver != nullptr && driver->Write != nullptr;
|
|
vfsLock.Release();
|
|
return canWrite;
|
|
}
|
|
|
|
void CloseBackendFile(BackendFile& file) {
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForFileLocked(file);
|
|
if (driver != nullptr && driver->Close != nullptr) {
|
|
driver->Close(driver->ctx, file.localHandle);
|
|
}
|
|
vfsLock.Release();
|
|
|
|
file.driveNumber = -1;
|
|
file.localHandle = -1;
|
|
file.generation = 0;
|
|
}
|
|
|
|
int WriteBackendFile(const BackendFile& file, const uint8_t* buffer, uint64_t offset, uint64_t size) {
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForFileLocked(file);
|
|
if (driver == nullptr || driver->Write == nullptr) {
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
|
|
int result = driver->Write(driver->ctx, file.localHandle, buffer, offset, size);
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
int CreateBackendFile(const char* path, BackendFile& outFile) {
|
|
outFile.driveNumber = -1;
|
|
outFile.localHandle = -1;
|
|
outFile.generation = 0;
|
|
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(drive);
|
|
if (driver == nullptr || driver->Create == nullptr) {
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
uint32_t generation = driveGeneration[drive];
|
|
int localHandle = driver->Create(driver->ctx, localPath);
|
|
vfsLock.Release();
|
|
if (localHandle < 0) return -1;
|
|
|
|
outFile.driveNumber = drive;
|
|
outFile.localHandle = localHandle;
|
|
outFile.generation = generation;
|
|
return 0;
|
|
}
|
|
|
|
int VfsDelete(const char* path) {
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(drive);
|
|
int result = (driver != nullptr && driver->Delete != nullptr)
|
|
? driver->Delete(driver->ctx, localPath)
|
|
: -1;
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
int VfsStat(const char* path, StatInfo& out) {
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
out = StatInfo{};
|
|
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(drive);
|
|
int result = (driver != nullptr && driver->Stat != nullptr)
|
|
? driver->Stat(driver->ctx, localPath, &out)
|
|
: -1;
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
int VfsSetTimes(const char* path, int64_t atime, int64_t mtime) {
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(drive);
|
|
int result = (driver != nullptr && driver->SetTimes != nullptr)
|
|
? driver->SetTimes(driver->ctx, localPath, atime, mtime)
|
|
: -2;
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
int VfsMkdir(const char* path) {
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
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++) {
|
|
if (driveActive[i]) {
|
|
outDrives[count++] = i;
|
|
}
|
|
}
|
|
vfsLock.Release();
|
|
return count;
|
|
}
|
|
static int CopyDriveString(int driveNumber, const char* (*FsDriver::*hook)(void*),
|
|
char* out, int maxLen) {
|
|
if (out == nullptr || maxLen <= 0) return -1;
|
|
out[0] = '\0';
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(driveNumber);
|
|
if (driver == nullptr) {
|
|
vfsLock.Release();
|
|
return -1;
|
|
}
|
|
|
|
if (driver->*hook == nullptr) {
|
|
vfsLock.Release();
|
|
return 0;
|
|
}
|
|
|
|
const char* value = (driver->*hook)(driver->ctx);
|
|
if (value == nullptr || value[0] == '\0') {
|
|
vfsLock.Release();
|
|
return 0;
|
|
}
|
|
|
|
int copied = 0;
|
|
while (value[copied] != '\0' && copied < maxLen - 1) {
|
|
out[copied] = value[copied];
|
|
copied++;
|
|
}
|
|
out[copied] = '\0';
|
|
|
|
vfsLock.Release();
|
|
return copied;
|
|
}
|
|
|
|
int VfsDriveLabel(int driveNumber, char* outLabel, int maxLen) {
|
|
return CopyDriveString(driveNumber, &FsDriver::GetLabel, outLabel, maxLen);
|
|
}
|
|
|
|
int VfsDriveFsType(int driveNumber, char* outType, int maxLen) {
|
|
return CopyDriveString(driveNumber, &FsDriver::GetFsType, outType, maxLen);
|
|
}
|
|
|
|
int VfsGrow(int driveNumber, uint64_t partSectors, bool apply, GrowStatus* out) {
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(driveNumber);
|
|
int result = (driver != nullptr && driver->Grow != nullptr)
|
|
? driver->Grow(driver->ctx, partSectors, apply, out)
|
|
: -2;
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
int VfsRename(const char* oldPath, const char* newPath) {
|
|
int oldDrive, newDrive;
|
|
const char* oldLocal;
|
|
const char* newLocal;
|
|
|
|
if (!ParsePath(oldPath, oldDrive, oldLocal)) return -1;
|
|
if (!ParsePath(newPath, newDrive, newLocal)) return -1;
|
|
|
|
// Cross-drive rename not supported
|
|
if (oldDrive != newDrive) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(oldDrive);
|
|
int result = (driver != nullptr && driver->Rename != nullptr)
|
|
? driver->Rename(driver->ctx, oldLocal, newLocal)
|
|
: -1;
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
int VfsReadDir(const char* path, const char** outNames, int maxEntries) {
|
|
return VfsReadDirAt(path, outNames, maxEntries, 0);
|
|
}
|
|
|
|
int VfsReadDirAt(const char* path, const char** outNames, int maxEntries, int startIndex) {
|
|
int drive;
|
|
const char* localPath;
|
|
|
|
if (startIndex < 0) return -1;
|
|
if (!ParsePath(path, drive, localPath)) return -1;
|
|
|
|
vfsLock.Acquire();
|
|
FsDriver* driver = DriverForLocked(drive);
|
|
int result;
|
|
if (driver == nullptr) {
|
|
result = -1;
|
|
} else if (driver->ReadDirAt) {
|
|
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(driver->ctx, localPath, outNames, maxEntries) : 0;
|
|
} else {
|
|
result = -1;
|
|
}
|
|
vfsLock.Release();
|
|
return result;
|
|
}
|
|
|
|
}
|