Files
lobbs/lofs/LoFS.cpp
T
Ben Allfree 22c8567c4f feat(lofs): exclusive /lofs mount with LoLog
Single LoLog slot on QSPI or nRF flash2; sd/lofs/extra/internal order.
Host lolog tests; drop qspi/spare/reserve mount names.
2026-10-07 16:53:38 -07:00

629 lines
16 KiB
C++

#include "LoFS.h"
#include "volumes/LoFSVolume.h"
#include <cstring>
#include <stdio.h>
#include <string>
#include "core/LoBBSStackGuard.h"
LoFS::Mount LoFS::mounts[6];
int LoFS::mountCount = 0;
bool LoFS::begun = false;
static uint32_t lofsCrc32Update(uint32_t crc, const uint8_t *data, size_t len)
{
for (size_t i = 0; data && i < len; i++) {
crc ^= data[i];
for (int b = 0; b < 8; b++)
crc = (crc >> 1) ^ (0xedb88320 & (~((crc & 1) - 1)));
}
return crc;
}
static int lofsMountPreferenceRank(const char *name)
{
if (!name)
return 999;
for (int i = 0; i < LOFS_MOUNT_ORDER_LEN; i++) {
if (strcmp(name, LOFS_MOUNT_ORDER[i]) == 0)
return i;
}
return 999;
}
int LoFS::mountPreferenceRank(const char *name)
{
return lofsMountPreferenceRank(name);
}
void LoFS::begin()
{
if (begun)
return;
mountCount = 0;
LoFSMountTable table;
lofsPlatformMounts(table);
for (int o = 0; o < LOFS_MOUNT_ORDER_LEN && mountCount < LOFS_MAX_MOUNTS; o++) {
const char *want = LOFS_MOUNT_ORDER[o];
for (int i = 0; i < table.count; i++) {
if (strcmp(table.specs[i].name, want) != 0)
continue;
LoFSMountSpec &spec = table.specs[i];
if (!spec.volume || !spec.volume->begin())
break;
mounts[mountCount++] = {spec.name, spec.volume, true, spec.shared, false, spec.formattable};
break;
}
}
for (int i = 0; i < mountCount; i++) {
if (!mounts[i].shared) {
mounts[i].dbSafe = true;
continue;
}
mounts[i].dbSafe = true;
const int selfRank = lofsMountPreferenceRank(mounts[i].name);
for (int j = 0; j < mountCount; j++) {
if (j == i || !mounts[j].present)
continue;
if (lofsMountPreferenceRank(mounts[j].name) < selfRank) {
mounts[i].dbSafe = false;
break;
}
}
}
begun = true;
}
#ifdef PIO_UNIT_TESTING
void LoFS::resetForTests()
{
begun = false;
mountCount = 0;
}
#endif
LoFS::Mount *LoFS::findByName(const char *name, size_t len)
{
for (int i = 0; i < mountCount; i++) {
if (strncmp(mounts[i].name, name, len) == 0 && mounts[i].name[len] == '\0' && mounts[i].present)
return &mounts[i];
}
return nullptr;
}
bool LoFS::mountPresent(const char *name)
{
return findByName(name, strlen(name)) != nullptr;
}
bool LoFS::mountDbSafe(const char *name)
{
Mount *m = name ? findByName(name, strlen(name)) : nullptr;
return m && m->dbSafe;
}
bool LoFS::format(const char *name)
{
Mount *m = name ? findByName(name, strlen(name)) : nullptr;
if (!m || !m->formattable || !m->volume)
return false;
return m->volume->format();
}
void LoFS::eachPresentMount(void (*fn)(void *ctx, const char *name), void *ctx)
{
if (!fn)
return;
for (int i = 0; i < mountCount; i++) {
if (mounts[i].present)
fn(ctx, mounts[i].name);
}
}
void LoFS::maintain(uint32_t budgetMs)
{
if (!begun)
return;
for (int i = 0; i < mountCount; i++) {
if (mounts[i].present && mounts[i].volume)
mounts[i].volume->maintain(budgetMs);
}
}
bool LoFS::resolve(const char *filepath, Resolved &out)
{
out = {};
if (!filepath || filepath[0] != '/')
return false;
if (filepath[1] == '\0') {
out.kind = PathKind::VirtualRoot;
return true;
}
const char *p = filepath + 1;
const char *slash = strchr(p, '/');
size_t nameLen = slash ? (size_t)(slash - p) : strlen(p);
if (nameLen == 0 || nameLen >= 16)
return false;
Mount *m = findByName(p, nameLen);
if (!m)
return false;
out.volume = m->volume;
if (!slash || slash[1] == '\0') {
out.kind = PathKind::MountRoot;
out.rel = "/";
return true;
}
out.rel = slash;
out.kind = PathKind::Normal;
return true;
}
const char *LoFS::backendPath(const Resolved &r)
{
return r.rel;
}
bool LoFS::isMountPoint(const char *path)
{
if (!path)
return false;
if (strcmp(path, "/") == 0)
return true;
Resolved r;
if (!resolve(path, r))
return false;
return r.kind == PathKind::MountRoot;
}
const char *LoFS::mountNameForPath(const char *path)
{
if (!path || path[0] != '/')
return nullptr;
if (path[1] == '\0')
return nullptr;
const char *p = path + 1;
const char *slash = strchr(p, '/');
size_t nameLen = slash ? (size_t)(slash - p) : strlen(p);
Mount *m = findByName(p, nameLen);
return m ? m->name : nullptr;
}
bool LoFS::isDirectory(const char *path)
{
Resolved r;
if (!resolve(path, r))
return false;
if (r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot)
return true;
LoFile f = open(path, "r");
if (!f)
return false;
bool isDir = f.isDirectory();
f.close();
return isDir;
}
bool LoFS::refuseMountPointMutation(const char *filepath)
{
return isMountPoint(filepath);
}
LoFile LoFS::open(const char *filepath, uint8_t mode)
{
Resolved r;
if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || !r.volume)
return LoFile();
return r.volume->open(backendPath(r), mode);
}
LoFile LoFS::open(const char *filepath, const char *mode)
{
Resolved r;
if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || !r.volume)
return LoFile();
return r.volume->open(backendPath(r), mode);
}
bool LoFS::exists(const char *filepath)
{
Resolved r;
if (!resolve(filepath, r))
return false;
if (r.kind == PathKind::VirtualRoot)
return true;
if (r.kind == PathKind::MountRoot)
return true;
return r.volume && r.volume->exists(backendPath(r));
}
bool LoFS::mkdir(const char *filepath)
{
if (refuseMountPointMutation(filepath))
return false;
Resolved r;
if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || !r.volume)
return false;
return r.volume->mkdir(backendPath(r));
}
bool LoFS::remove(const char *filepath)
{
if (refuseMountPointMutation(filepath))
return false;
Resolved r;
if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot || !r.volume)
return false;
return r.volume->remove(backendPath(r));
}
bool LoFS::rename(const char *oldfilepath, const char *newfilepath)
{
if (refuseMountPointMutation(oldfilepath) || refuseMountPointMutation(newfilepath))
return false;
Resolved oldR;
Resolved newR;
if (!resolve(oldfilepath, oldR) || !resolve(newfilepath, newR) || !oldR.volume || !newR.volume)
return false;
if (oldR.kind == PathKind::VirtualRoot || newR.kind == PathKind::VirtualRoot)
return false;
if (oldR.volume != newR.volume)
return false;
return oldR.volume->rename(backendPath(oldR), backendPath(newR));
}
static bool lobfsEachDirEntry(LoFile &dir, void *ctx, LoFS::ListCallback fn)
{
while (true) {
LoFile file = dir.openNextFile();
if (!file)
break;
std::string pathFromFile = file.name();
bool isDir = file.isDirectory();
uint32_t size = isDir ? 0 : (uint32_t)file.size();
file.close();
size_t lastSlash = pathFromFile.rfind('/');
std::string entryName = (lastSlash != std::string::npos) ? pathFromFile.substr(lastSlash + 1) : pathFromFile;
if (entryName == "." || entryName == "..")
continue;
if (!fn(ctx, entryName.c_str(), isDir, size))
return false;
}
return true;
}
bool LoFS::list(const char *dirpath, void *ctx, ListCallback fn)
{
if (!dirpath || !fn)
return false;
Resolved r;
if (!resolve(dirpath, r))
return false;
if (r.kind == PathKind::VirtualRoot) {
for (int i = 0; i < mountCount; i++) {
if (!mounts[i].present)
continue;
if (!fn(ctx, mounts[i].name, true, 0))
return false;
}
return true;
}
LoFile dir = open(dirpath, "r");
if (!dir)
return false;
if (!dir.isDirectory()) {
dir.close();
return false;
}
bool ok = lobfsEachDirEntry(dir, ctx, fn);
dir.close();
return ok;
}
bool LoFS::stat(const char *filepath, uint32_t *sizeOut, bool *isDirOut)
{
if (sizeOut)
*sizeOut = 0;
if (isDirOut)
*isDirOut = false;
Resolved r;
if (!resolve(filepath, r))
return false;
if (r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot) {
if (isDirOut)
*isDirOut = true;
return true;
}
LoFile f = open(filepath, "r");
if (!f)
return false;
if (isDirOut)
*isDirOut = f.isDirectory();
if (sizeOut && !f.isDirectory())
*sizeOut = (uint32_t)f.size();
f.close();
return true;
}
bool LoFS::writeAt(const char *filepath, uint32_t offset, const uint8_t *data, size_t len)
{
if (!filepath || !data || len == 0)
return false;
if (refuseMountPointMutation(filepath))
return false;
Resolved r;
if (!resolve(filepath, r) || r.kind != PathKind::Normal)
return false;
const bool creating = !exists(filepath);
bool appendAtEnd = false;
LoFile f = open(filepath, creating ? "w" : "r+");
if (!f && !creating && offset > 0) {
f = open(filepath, "a");
appendAtEnd = true;
}
if (!f)
return false;
if (f.isDirectory()) {
f.close();
return false;
}
bool ok = false;
if (!appendAtEnd && offset > 0 && !f.seek(offset)) {
f.close();
return false;
}
size_t w = f.write(data, len);
ok = (w == len);
f.flush();
f.close();
return ok;
}
bool LoFS::copy(const char *src, const char *dst)
{
if (refuseMountPointMutation(src) || refuseMountPointMutation(dst))
return false;
if (exists(dst))
return false;
Resolved srcR;
Resolved dstR;
if (!resolve(src, srcR) || !resolve(dst, dstR))
return false;
if (srcR.kind == PathKind::VirtualRoot || srcR.kind == PathKind::MountRoot)
return false;
if (dstR.kind == PathKind::VirtualRoot || dstR.kind == PathKind::MountRoot)
return false;
LoFile srcFile = open(src, "r");
if (!srcFile)
return false;
if (srcFile.isDirectory()) {
srcFile.close();
return false;
}
LoFile dstFile = open(dst, "w");
if (!dstFile) {
srcFile.close();
return false;
}
unsigned char buffer[128];
bool ok = true;
while (true) {
size_t n = srcFile.read(buffer, sizeof(buffer));
if (n == 0)
break;
size_t w = dstFile.write(buffer, n);
if (w != n) {
ok = false;
break;
}
}
dstFile.flush();
dstFile.close();
srcFile.close();
if (!ok)
remove(dst);
return ok;
}
LoFSMoveResult LoFS::move(const char *src, const char *dst)
{
if (!src || !dst)
return LoFSMoveResult::Failed;
if (isMountPoint(src) || isMountPoint(dst))
return LoFSMoveResult::RefusedMount;
if (exists(dst))
return LoFSMoveResult::DstExists;
const char *srcMount = mountNameForPath(src);
const char *dstMount = mountNameForPath(dst);
if (srcMount && dstMount && strcmp(srcMount, dstMount) == 0) {
return rename(src, dst) ? LoFSMoveResult::Ok : LoFSMoveResult::Failed;
}
bool isDir = false;
uint32_t sz = 0;
if (!stat(src, &sz, &isDir))
return LoFSMoveResult::SrcMissing;
if (isDir)
return LoFSMoveResult::CrossMountDir;
if (!hasRoom(dst, sz))
return LoFSMoveResult::NoSpace;
if (!copy(src, dst))
return LoFSMoveResult::CopyFailed;
if (remove(src))
return LoFSMoveResult::Ok;
return LoFSMoveResult::SrcNotRemoved;
}
bool LoFS::crc32File(const char *filepath, uint32_t *crcOut)
{
if (!filepath || !crcOut)
return false;
LoFile f = open(filepath, "r");
if (!f)
return false;
if (f.isDirectory()) {
f.close();
return false;
}
uint32_t crc = 0xffffffff;
uint8_t chunk[64];
while (true) {
size_t n = f.read(chunk, sizeof(chunk));
if (n == 0)
break;
crc = lofsCrc32Update(crc, chunk, n);
}
f.close();
*crcOut = ~crc;
return true;
}
LoFSMoveResult LoFS::moveIfCrc32Matches(const char *src, const char *dst, uint32_t expectedCrc)
{
uint32_t crc = 0;
if (!crc32File(src, &crc))
return LoFSMoveResult::SrcMissing;
if (crc != expectedCrc)
return LoFSMoveResult::CrcMismatch;
return move(src, dst);
}
uint64_t LoFS::totalBytes(const char *mountRoot)
{
Resolved r;
if (!resolve(mountRoot, r) || r.kind != PathKind::MountRoot || !r.volume)
return 0;
return r.volume->totalBytes();
}
uint64_t LoFS::usedBytes(const char *mountRoot)
{
Resolved r;
if (!resolve(mountRoot, r) || r.kind != PathKind::MountRoot || !r.volume)
return 0;
return r.volume->usedBytes();
}
uint64_t LoFS::freeBytes(const char *mountRoot)
{
uint64_t total = totalBytes(mountRoot);
uint64_t used = usedBytes(mountRoot);
if (total == 0 || used > total)
return 0;
return total - used;
}
uint32_t LoFS::mountReserve(const char *name)
{
Mount *m = name ? findByName(name, strlen(name)) : nullptr;
return (m && m->shared) ? lofsPlatformSharedReserve(name) : 0;
}
bool LoFS::hasRoom(const char *path, uint32_t bytes)
{
Resolved r;
const char *name = mountNameForPath(path);
if (!name || !resolve(path, r) || !r.volume)
return true;
char root[20];
snprintf(root, sizeof(root), "/%s", name);
const uint64_t total = totalBytes(root);
if (total == 0)
return true;
const uint64_t used = usedBytes(root);
const uint64_t freeB = used < total ? total - used : 0;
uint32_t block = 4096;
uint32_t slack = 8192;
r.volume->spaceHint(&block, &slack);
const uint64_t need = (((uint64_t)bytes + block - 1) / block) * block + slack + mountReserve(name);
return freeB >= need;
}
bool LoFS::rmdir(const char *filepath, bool recursive)
{
if (refuseMountPointMutation(filepath))
return false;
if (!exists(filepath))
return true;
if (recursive) {
LoFile dir = open(filepath, "r");
if (!dir)
return false;
if (!dir.isDirectory()) {
dir.close();
return remove(filepath);
}
bool result = true;
while (true) {
LoFile file = dir.openNextFile();
if (!file)
break;
std::string pathFromFile = file.name();
bool isDir = file.isDirectory();
file.close();
size_t lastSlash = pathFromFile.rfind('/');
std::string entryName = (lastSlash != std::string::npos) ? pathFromFile.substr(lastSlash + 1) : pathFromFile;
if (entryName == "." || entryName == "..")
continue;
std::string fullPath = std::string(filepath) + "/" + entryName;
if (isDir) {
if (!rmdir(fullPath.c_str(), true))
result = false;
} else {
if (!remove(fullPath.c_str()))
result = false;
}
}
dir.close();
if (!result)
return false;
}
Resolved r;
if (!resolve(filepath, r) || r.kind == PathKind::VirtualRoot || r.kind == PathKind::MountRoot || !r.volume)
return false;
return r.volume->rmdir(backendPath(r));
}