mirror of
https://github.com/MeshEnvy/lobbs.git
synced 2026-11-03 19:42:25 +01:00
Single LoLog slot on QSPI or nRF flash2; sd/lofs/extra/internal order. Host lolog tests; drop qspi/spare/reserve mount names.
629 lines
16 KiB
C++
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));
|
|
}
|