diff --git a/CHANGELOG.md b/CHANGELOG.md index 60c8ff2..50c76ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ Planned major release **2.0.0** (not tagged yet). ### Changed -- LoFS: volume provider layer (`LoFSVolume`, block devices, host mount tables). Mount names are now `sd`, `qspi`, `reserve`, `spare`, `internal` (replacing `extra`, `flash3`, `flash2`, `flash`). Every LittleFS mount is formattable via `/format`; SD stays refused. +- LoFS: mount names are `sd`, `lofs`, `extra`, `internal`. **`/lofs`** is the exclusive LoLog slot (owned QSPI NOR or nRF52840 flash2 window, never both in the mount table). QSPI Meshtastic envs use `nrf52840_s140_v7_qspi.ld` (no flash2 gap; InternalFS still at `0xED000`). MeshCore `/extra` adopts host ExtraFS; QSPI is not adopted as LittleFS. Dropped `qspi`, `spare`, `reserve`, and flash3 mounts. Every LittleFS mount is formattable via `/format`; SD stays refused. Host tests: `ci/host` `test_lolog_host`. ### Added diff --git a/ci/host/platformio.ini b/ci/host/platformio.ini index 3e33df4..01af167 100644 --- a/ci/host/platformio.ini +++ b/ci/host/platformio.ini @@ -26,3 +26,6 @@ build_src_filter = + + + + + + + + + diff --git a/ci/host/test/test_lolog_host/README.md b/ci/host/test/test_lolog_host/README.md new file mode 100644 index 0000000..cb1310c --- /dev/null +++ b/ci/host/test/test_lolog_host/README.md @@ -0,0 +1,19 @@ +# LoLog host tests + +Run from repo root: + +```bash +cd ci/host && pio test -e host -f test_lolog_host +``` + +Coverage (Unity): + +| Area | Tests | +|------|--------| +| Boot / mount | Empty format, erased chip, pure DATA replay, reboot | +| File ops | Nested mkdir, overwrite, rename, rmdir, prefix names, 2.5 KB EXTENT file, writeAt, stat | +| Crash / ACID | Fault on every prog byte (large commit), every prog call, torn tail + append, CRC poke, partial mkdir group | +| Index / clean | Index flush remount, bulk flush, tomb shadow, delete when full, cleaner churn, headroom | +| Fuzz | 30 rounds random ops + crash injection vs ref model | + +Fault injection uses `LoLogRamDevice` (`setFaultAfterProgBytes`, `setFaultOnProgCall`, `poke`). diff --git a/ci/host/test/test_lolog_host/lolog_test_helpers.hpp b/ci/host/test/test_lolog_host/lolog_test_helpers.hpp new file mode 100644 index 0000000..f0289cc --- /dev/null +++ b/ci/host/test/test_lolog_host/lolog_test_helpers.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "lofs/lolog/LoLog.h" +#include "lofs/lolog/LoLogRamDevice.h" +#include +#include +#include +#include + +inline bool lologWriteFile(LoLog &log, const char *path, const uint8_t *data, size_t len) +{ + LoLog::OpenHandle *h = log.open(path, true, true); + if (!h) + return false; + size_t off = 0; + while (off < len) { + size_t n = log.writeHandle(h, data + off, len - off); + if (n == 0) + break; + off += n; + } + log.flushHandle(h); + log.closeHandle(h); + return off == len; +} + +inline bool lologReadFile(LoLog &log, const char *path, std::vector &out) +{ + LoLog::OpenHandle *h = log.open(path, false, false); + if (!h) + return false; + out.clear(); + uint8_t buf[256]; + for (;;) { + size_t n = log.readHandle(h, buf, sizeof(buf)); + if (n == 0) + break; + out.insert(out.end(), buf, buf + n); + } + log.closeHandle(h); + return true; +} + +inline int lologCountDir(LoLog &log, const char *dirPath) +{ + LoLog::OpenHandle *dir = log.openDir(dirPath); + if (!dir) + return -1; + int n = 0; + while (LoLog::OpenHandle *e = log.openNextInDir(dir)) { + log.closeHandle(e); + n++; + } + log.closeHandle(dir); + return n; +} + +/** Fresh chip: all 0xFF, mount without explicit format. */ +inline bool lologMountErasedChip(LoLogRamDevice &dev, LoLog &log) +{ + dev.reset(); + return log.mount(); +} diff --git a/ci/host/test/test_lolog_host/ref_fs.hpp b/ci/host/test/test_lolog_host/ref_fs.hpp new file mode 100644 index 0000000..dc7fb46 --- /dev/null +++ b/ci/host/test/test_lolog_host/ref_fs.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include "lolog_test_helpers.hpp" +#include +#include +#include +#include + +/** In-memory reference tree for fuzz / crash oracle (paths without leading slash). */ +class RefFs { + public: + bool mkdir(const std::string &path) + { + if (path.empty() || files.count(path)) + return false; + dirs.insert(path); + return true; + } + + bool write(const std::string &path, const std::vector &data) + { + if (path.empty()) + return false; + files[path] = data; + return true; + } + + bool unlink(const std::string &path) { return files.erase(path) > 0; } + + bool rename(const std::string &from, const std::string &to) + { + auto it = files.find(from); + if (it == files.end()) + return false; + if (files.count(to)) + return false; + files[to] = it->second; + files.erase(it); + return true; + } + + bool existsFile(const std::string &path) const { return files.count(path) > 0; } + + const std::map> &allFiles() const { return files; } + + void clear() + { + files.clear(); + dirs.clear(); + } + + private: + std::map> files; + std::set dirs; +}; + +inline bool lologMatchesRef(LoLog &log, const RefFs &ref) +{ + for (const auto &kv : ref.allFiles()) { + if (!log.exists(kv.first.c_str())) + return false; + std::vector got; + if (!lologReadFile(log, kv.first.c_str(), got)) + return false; + if (got != kv.second) + return false; + } + for (const auto &kv : ref.allFiles()) { + if (log.exists(kv.first.c_str()) && !ref.existsFile(kv.first)) + return false; + } + std::vector logPaths; + for (int i = 0; i < 512; i++) { + char p[32]; + snprintf(p, sizeof(p), "fuzz_%d.dat", i); + if (log.exists(p)) + logPaths.push_back(p); + } + for (const auto &p : logPaths) { + if (!ref.existsFile(p)) + return false; + } + return true; +} diff --git a/ci/host/test/test_lolog_host/test_boot.cpp b/ci/host/test/test_lolog_host/test_boot.cpp new file mode 100644 index 0000000..ee119bb --- /dev/null +++ b/ci/host/test/test_lolog_host/test_boot.cpp @@ -0,0 +1,41 @@ +#include "lolog_test_helpers.hpp" +#include + +extern LoLogRamDevice gDev; +extern LoLog gLog; + +void test_mount_empty_after_format(void) +{ + TEST_ASSERT_FALSE(gLog.exists("anything")); + TEST_ASSERT_TRUE(gLog.isDirectory("")); +} + +void test_mount_erased_chip_without_format(void) +{ + LoLogRamDevice dev(16); + LoLog log(dev); + TEST_ASSERT_TRUE(lologMountErasedChip(dev, log)); +} + +void test_pure_replay_no_index_runs(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("replay")); + std::vector payload = {'a', 'b', 'c'}; + TEST_ASSERT_TRUE(lologWriteFile(gLog, "replay/r1.ls", payload.data(), payload.size())); + + LoLog g2(gDev); + TEST_ASSERT_TRUE(g2.mount()); + std::vector readback; + TEST_ASSERT_TRUE(lologReadFile(g2, "replay/r1.ls", readback)); + TEST_ASSERT_EQUAL_UINT32(payload.size(), readback.size()); +} + +void test_mount_after_reboot_simulation(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("boot")); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "boot/persist.ls", (const uint8_t *)"ok", 2)); + + LoLog after(gDev); + TEST_ASSERT_TRUE(after.mount()); + TEST_ASSERT_TRUE(after.exists("boot/persist.ls")); +} diff --git a/ci/host/test/test_lolog_host/test_crash.cpp b/ci/host/test/test_lolog_host/test_crash.cpp new file mode 100644 index 0000000..af94d95 --- /dev/null +++ b/ci/host/test/test_lolog_host/test_crash.cpp @@ -0,0 +1,235 @@ +#include "lolog_test_helpers.hpp" +#include "lofs/lolog/LoLogConfig.h" +#include + +extern LoLogRamDevice gDev; +extern LoLog gLog; + +static uint32_t progCallsForLargeCommit(LoLogRamDevice &dev) +{ + dev.reset(); + LoLog log(dev); + log.format(); + log.mkdir("gc"); + LoLog::OpenHandle *h = log.open("gc/big.bin", true, true); + if (!h) + return 0; + dev.clearProgCallCount(); + std::vector payload(2500, 0x5A); + log.writeHandle(h, payload.data(), payload.size()); + log.flushHandle(h); + log.closeHandle(h); + return dev.progCallCount(); +} + +static bool groupVisibleAfterRemount(LoLogRamDevice &dev, const char *path, size_t expectLen) +{ + LoLog log(dev); + if (!log.mount()) + return false; + if (!log.exists(path)) + return expectLen == 0; + std::vector got; + if (!lologReadFile(log, path, got)) + return false; + return got.size() == expectLen; +} + +void test_commit_group_atomic_each_prog_call(void) +{ + const uint32_t calls = progCallsForLargeCommit(gDev); + TEST_ASSERT_GREATER_THAN(1, calls); + + for (uint32_t fault = 1; fault <= calls; fault++) { + gDev.reset(); + LoLog log(gDev); + log.format(); + TEST_ASSERT_TRUE(log.mkdir("gc")); + gDev.setFaultOnProgCall(fault); + LoLog::OpenHandle *h = log.open("gc/big.bin", true, true); + if (h) { + std::vector payload(2500, 0x5A); + log.writeHandle(h, payload.data(), payload.size()); + log.flushHandle(h); + log.closeHandle(h); + } + gDev.setFaultOnProgCall(0); + + const bool full = groupVisibleAfterRemount(gDev, "gc/big.bin", 2500); + if (!full) { + LoLog absent(gDev); + absent.mount(); + if (absent.exists("gc/big.bin")) { + std::vector got; + lologReadFile(absent, "gc/big.bin", got); + TEST_ASSERT_EQUAL_UINT32(0, got.size()); + } + } else { + LoLog check(gDev); + check.mount(); + std::vector got; + lologReadFile(check, "gc/big.bin", got); + for (uint8_t b : got) + TEST_ASSERT_EQUAL_UINT8(0x5A, b); + } + } +} + +void test_commit_group_partial_never_applied(void) +{ + gDev.reset(); + LoLog log(gDev); + log.format(); + TEST_ASSERT_TRUE(log.mkdir("p")); + gDev.setFaultOnProgCall(1); + TEST_ASSERT_FALSE(log.mkdir("p/sub")); + gDev.setFaultOnProgCall(0); + LoLog rem(gDev); + rem.mount(); + TEST_ASSERT_FALSE(rem.exists("p/sub")); +} + +void test_commit_group_each_prog_byte(void) +{ + LoLogRamDevice measure(32); + measure.reset(); + LoLog mlog(measure); + mlog.format(); + mlog.mkdir("by"); + measure.clearProgBytes(); + std::vector payload(2500, 0x11); + LoLog::OpenHandle *mh = mlog.open("by/big.bin", true, true); + TEST_ASSERT_NOT_NULL(mh); + mlog.writeHandle(mh, payload.data(), payload.size()); + mlog.flushHandle(mh); + mlog.closeHandle(mh); + const uint64_t totalBytes = measure.progBytes(); + TEST_ASSERT_GREATER_THAN(8, totalBytes); + + for (uint64_t cut = 1; cut <= totalBytes; cut++) { + measure.reset(); + LoLog l(measure); + l.format(); + l.mkdir("by"); + measure.setFaultAfterProgBytes((uint32_t)cut); + LoLog::OpenHandle *lh = l.open("by/big.bin", true, true); + if (lh) { + l.writeHandle(lh, payload.data(), payload.size()); + l.flushHandle(lh); + l.closeHandle(lh); + } + measure.setFaultAfterProgBytes(0); + const bool full = groupVisibleAfterRemount(measure, "by/big.bin", 2500); + if (full) { + LoLog check(measure); + check.mount(); + std::vector got; + lologReadFile(check, "by/big.bin", got); + TEST_ASSERT_EQUAL_UINT32(2500, got.size()); + } else { + LoLog absent(measure); + absent.mount(); + if (absent.exists("by/big.bin")) { + std::vector got; + lologReadFile(absent, "by/big.bin", got); + TEST_ASSERT_EQUAL_UINT32(0, got.size()); + } + } + } +} + +void test_torn_tail_then_append(void) +{ + gDev.reset(); + LoLog log(gDev); + log.format(); + TEST_ASSERT_TRUE(log.mkdir("torn")); + TEST_ASSERT_TRUE(lologWriteFile(log, "torn/a.ls", (const uint8_t *)"AAAA", 4)); + + gDev.setFaultAfterProgBytes(12); + LoLog::OpenHandle *h = log.open("torn/b.ls", true, true); + if (h) { + log.writeHandle(h, (const uint8_t *)"BBBBBBBB", 8); + log.flushHandle(h); + log.closeHandle(h); + } + gDev.setFaultAfterProgBytes(0); + + { + LoLog mid(gDev); + TEST_ASSERT_TRUE(mid.mount()); + TEST_ASSERT_TRUE(mid.exists("torn/a.ls")); + if (mid.exists("torn/b.ls")) { + std::vector shell; + lologReadFile(mid, "torn/b.ls", shell); + TEST_ASSERT_EQUAL_UINT32(0, shell.size()); + } + } + + LoLog fin(gDev); + TEST_ASSERT_TRUE(fin.mount()); + TEST_ASSERT_TRUE(lologWriteFile(fin, "torn/c.ls", (const uint8_t *)"CCCC", 4)); + std::vector finRead; + TEST_ASSERT_TRUE(lologReadFile(fin, "torn/c.ls", finRead)); + TEST_ASSERT_EQUAL_UINT32(4, finRead.size()); + TEST_ASSERT_TRUE(fin.exists("torn/a.ls")); +} + +static void corruptSecondEntryCrc(LoLogRamDevice &dev) +{ + for (uint32_t s = 0; s < dev.sectorCount(); s++) { + uint32_t off = lolog::kHeaderSize; + int ent = 0; + while (off + 8 <= lolog::kSectorSize) { + uint8_t hdr[8]; + if (!dev.read(s * lolog::kSectorSize + off, hdr, 8)) + break; + uint16_t plen = (uint16_t)hdr[0] | ((uint16_t)hdr[1] << 8); + if (plen == lolog::kEntryEnd) + break; + const uint32_t padded = (8u + plen + 3u) & ~3u; + if (hdr[2] == lolog::kEntFile) { + ent++; + if (ent == 2 && plen > 0) { + dev.poke(s * lolog::kSectorSize + off + 4 + plen, 0x00); + return; + } + } + off += padded; + } + } +} + +void test_torn_entry_crc_poke(void) +{ + gDev.reset(); + LoLog log(gDev); + log.format(); + TEST_ASSERT_TRUE(log.mkdir("poke")); + TEST_ASSERT_TRUE(lologWriteFile(log, "poke/ok.ls", (const uint8_t *)"good", 4)); + TEST_ASSERT_TRUE(lologWriteFile(log, "poke/next.ls", (const uint8_t *)"bad", 3)); + corruptSecondEntryCrc(gDev); + + LoLog rem(gDev); + TEST_ASSERT_TRUE(rem.mount()); + TEST_ASSERT_TRUE(rem.exists("poke/ok.ls")); + TEST_ASSERT_FALSE(rem.exists("poke/next.ls")); + TEST_ASSERT_TRUE(lologWriteFile(rem, "poke/after.ls", (const uint8_t *)"yes", 3)); + TEST_ASSERT_TRUE(rem.exists("poke/after.ls")); +} + +void test_fault_single_byte_discards_group(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("g")); + gDev.setFaultAfterProgBytes(1); + LoLog::OpenHandle *h = gLog.open("g/a.ls", true, true); + if (h) { + gLog.writeHandle(h, (const uint8_t *)"x", 1); + gLog.flushHandle(h); + gLog.closeHandle(h); + } + gDev.setFaultAfterProgBytes(0); + LoLog g2(gDev); + TEST_ASSERT_TRUE(g2.mount()); + TEST_ASSERT_FALSE(g2.exists("g/a.ls")); +} diff --git a/ci/host/test/test_lolog_host/test_file_ops.cpp b/ci/host/test/test_lolog_host/test_file_ops.cpp new file mode 100644 index 0000000..d1e4410 --- /dev/null +++ b/ci/host/test/test_lolog_host/test_file_ops.cpp @@ -0,0 +1,104 @@ +#include "lolog_test_helpers.hpp" +#include +#include + +extern LoLogRamDevice gDev; + +extern LoLog gLog; + +void test_mkdir_nested_and_overwrite(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("a/b/c")); + TEST_ASSERT_TRUE(gLog.isDirectory("a")); + TEST_ASSERT_TRUE(gLog.isDirectory("a/b")); + TEST_ASSERT_TRUE(gLog.isDirectory("a/b/c")); + + const uint8_t v1[] = "one"; + const uint8_t v2[] = "two-longer"; + TEST_ASSERT_TRUE(lologWriteFile(gLog, "a/b/c/f.ls", v1, sizeof(v1))); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "a/b/c/f.ls", v2, sizeof(v2))); + std::vector got; + TEST_ASSERT_TRUE(lologReadFile(gLog, "a/b/c/f.ls", got)); + TEST_ASSERT_EQUAL_UINT32(sizeof(v2), got.size()); +} + +void test_rename_file(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("rn")); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "rn/old.ls", (const uint8_t *)"data", 4)); + TEST_ASSERT_TRUE(gLog.rename("rn/old.ls", "rn/new.ls")); + TEST_ASSERT_FALSE(gLog.exists("rn/old.ls")); + TEST_ASSERT_TRUE(gLog.exists("rn/new.ls")); +} + +void test_rmdir_nonempty_fails_nonempty_ok_recursive(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("rd")); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "rd/x.ls", (const uint8_t *)"1", 1)); + TEST_ASSERT_FALSE(gLog.rmdir("rd", false)); + TEST_ASSERT_TRUE(gLog.rmdir("rd", true)); + TEST_ASSERT_FALSE(gLog.exists("rd/x.ls")); +} + +void test_prefix_collision_names(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("pfx")); + const std::string p1 = "pfx/abcdefgh_11111111.ls"; + const std::string p2 = "pfx/abcdefgh_22222222.ls"; + TEST_ASSERT_TRUE(lologWriteFile(gLog, p1.c_str(), (const uint8_t *)"A", 1)); + TEST_ASSERT_TRUE(lologWriteFile(gLog, p2.c_str(), (const uint8_t *)"B", 1)); + std::vector a, b; + TEST_ASSERT_TRUE(lologReadFile(gLog, "pfx/abcdefgh_11111111.ls", a)); + TEST_ASSERT_TRUE(lologReadFile(gLog, "pfx/abcdefgh_22222222.ls", b)); + TEST_ASSERT_EQUAL_UINT8(0x41, a[0]); + TEST_ASSERT_EQUAL_UINT8(0x42, b[0]); +} + +void test_large_file_inline_and_extent(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("big")); + std::vector payload(2500); + for (size_t i = 0; i < payload.size(); i++) + payload[i] = (uint8_t)(i & 0xFF); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "big/upload.bin", payload.data(), payload.size())); + + LoLog g2(gDev); + TEST_ASSERT_TRUE(g2.mount()); + std::vector got; + TEST_ASSERT_TRUE(lologReadFile(g2, "big/upload.bin", got)); + TEST_ASSERT_EQUAL_UINT32(payload.size(), got.size()); + TEST_ASSERT_EQUAL_UINT8(0, got[0]); + TEST_ASSERT_EQUAL_UINT8(0xC3, got[2500 - 1]); +} + +void test_large_file_write_at_path(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("up")); + LoLog::OpenHandle *h = gLog.open("up/chunk.bin", true, true); + TEST_ASSERT_NOT_NULL(h); + gLog.closeHandle(h); + + uint8_t chunk[600]; + for (size_t i = 0; i < sizeof(chunk); i++) + chunk[i] = (uint8_t)i; + TEST_ASSERT_TRUE(gLog.writeAtPath("up/chunk.bin", 0, chunk, sizeof(chunk))); + + std::vector got; + TEST_ASSERT_TRUE(lologReadFile(gLog, "up/chunk.bin", got)); + TEST_ASSERT_EQUAL_UINT32(sizeof(chunk), got.size()); + TEST_ASSERT_EQUAL_UINT8(0, got[0]); + TEST_ASSERT_EQUAL_UINT8(255, got[255]); +} + +void test_stat_and_is_directory(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("st")); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "st/f.ls", (const uint8_t *)"x", 1)); + uint32_t sz = 0; + bool isDir = true; + TEST_ASSERT_TRUE(gLog.statPath("st/f.ls", &sz, &isDir)); + TEST_ASSERT_FALSE(isDir); + TEST_ASSERT_EQUAL_UINT32(1, sz); + TEST_ASSERT_TRUE(gLog.statPath("st", &sz, &isDir)); + TEST_ASSERT_TRUE(isDir); +} diff --git a/ci/host/test/test_lolog_host/test_fuzz.cpp b/ci/host/test/test_lolog_host/test_fuzz.cpp new file mode 100644 index 0000000..6b03a58 --- /dev/null +++ b/ci/host/test/test_lolog_host/test_fuzz.cpp @@ -0,0 +1,99 @@ +#include "lolog_test_helpers.hpp" +#include "ref_fs.hpp" +#include + +extern LoLogRamDevice gDev; + +static uint32_t lfsRand(uint32_t &state) +{ + state = state * 1664525u + 1013904223u; + return state; +} + +static void applyRefOp(RefFs &ref, LoLog &log, uint32_t op, uint32_t &rng) +{ + char path[48]; + char path2[48]; + switch (op % 5) { + case 0: { + snprintf(path, sizeof(path), "fuzz/d%d", (int)(lfsRand(rng) % 20)); + if (log.mkdir(path)) + ref.mkdir(path); + break; + } + case 1: { + snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40)); + std::vector data(8 + (lfsRand(rng) % 120)); + for (size_t i = 0; i < data.size(); i++) + data[i] = (uint8_t)lfsRand(rng); + if (lologWriteFile(log, path, data.data(), data.size())) + ref.write(path, data); + break; + } + case 2: { + snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40)); + if (log.remove(path)) + ref.unlink(path); + break; + } + case 3: { + snprintf(path, sizeof(path), "fuzz/f%d.dat", (int)(lfsRand(rng) % 40)); + snprintf(path2, sizeof(path2), "fuzz/t%d.dat", (int)(lfsRand(rng) % 40)); + if (ref.existsFile(path) && !ref.existsFile(path2) && log.rename(path, path2)) + ref.rename(path, path2); + break; + } + default: + log.maintain(5000); + break; + } +} + +void test_random_ops_with_crash_injection(void) +{ + RefFs ref; + uint32_t rng = 0xC0FFEE42u; + + for (int round = 0; round < 30; round++) { + gDev.reset(); + LoLog log(gDev); + log.format(); + log.mkdir("fuzz"); + ref.clear(); + ref.mkdir("fuzz"); + + const int opsBeforeCrash = 3 + (int)(lfsRand(rng) % 12); + for (int i = 0; i < opsBeforeCrash; i++) + applyRefOp(ref, log, lfsRand(rng), rng); + + gDev.setFaultOnProgCall(1 + (lfsRand(rng) % 8)); + applyRefOp(ref, log, lfsRand(rng), rng); + gDev.setFaultOnProgCall(0); + + LoLog rem(gDev); + TEST_ASSERT_TRUE(rem.mount()); + + for (const auto &kv : ref.allFiles()) { + if (!rem.exists(kv.first.c_str())) + continue; + std::vector got; + if (!lologReadFile(rem, kv.first.c_str(), got)) + continue; + if (got.size() != kv.second.size()) + continue; + if (!got.empty()) + TEST_ASSERT_EQUAL_UINT8_ARRAY(kv.second.data(), got.data(), got.size()); + } + } +} + +void test_directory_list_count(void) +{ + gDev.reset(); + LoLog log(gDev); + log.format(); + TEST_ASSERT_TRUE(log.mkdir("d")); + TEST_ASSERT_TRUE(lologWriteFile(log, "d/one.ls", (const uint8_t *)"1", 1)); + TEST_ASSERT_TRUE(lologWriteFile(log, "d/two.ls", (const uint8_t *)"2", 1)); + TEST_ASSERT_EQUAL(2, lologCountDir(log, "d")); +} diff --git a/ci/host/test/test_lolog_host/test_index_clean.cpp b/ci/host/test/test_lolog_host/test_index_clean.cpp new file mode 100644 index 0000000..de56152 --- /dev/null +++ b/ci/host/test/test_lolog_host/test_index_clean.cpp @@ -0,0 +1,110 @@ +#include "lolog_test_helpers.hpp" +#include "lofs/lolog/LoLogConfig.h" +#include + +extern LoLogRamDevice gDev; +extern LoLog gLog; + +void test_index_flush_pure_reboot(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("ix")); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "ix/a.ls", (const uint8_t *)"z", 1)); + gLog.maintain(60000); + + LoLog g2(gDev); + TEST_ASSERT_TRUE(g2.mount()); + TEST_ASSERT_TRUE(g2.exists("ix/a.ls")); +} + +void test_index_multiple_flushes_and_lookup(void) +{ + for (int i = 0; i < 90; i++) { + char dir[16]; + snprintf(dir, sizeof(dir), "bulk%d", i); + TEST_ASSERT_TRUE(gLog.mkdir(dir)); + char path[32]; + snprintf(path, sizeof(path), "%s/r.ls", dir); + TEST_ASSERT_TRUE(lologWriteFile(gLog, path, (const uint8_t *)"x", 1)); + gLog.maintain(60000); + } + LoLog g2(gDev); + TEST_ASSERT_TRUE(g2.mount()); + TEST_ASSERT_TRUE(g2.exists("bulk42/r.ls")); + TEST_ASSERT_TRUE(g2.exists("bulk0/r.ls")); +} + +void test_tombstone_shadow_after_remount(void) +{ + TEST_ASSERT_TRUE(gLog.mkdir("ts")); + TEST_ASSERT_TRUE(lologWriteFile(gLog, "ts/gone.ls", (const uint8_t *)"1", 1)); + TEST_ASSERT_TRUE(gLog.remove("ts/gone.ls")); + gLog.maintain(60000); + + LoLog g2(gDev); + TEST_ASSERT_TRUE(g2.mount()); + TEST_ASSERT_FALSE(g2.exists("ts/gone.ls")); +} + +void test_delete_when_near_full(void) +{ + LoLogRamDevice tiny(16); + LoLog log(tiny); + log.format(); + TEST_ASSERT_TRUE(lologWriteFile(log, "f0.ls", (const uint8_t *)"x", 1)); + int created = 1; + for (int i = 1; i < 500; i++) { + char path[24]; + snprintf(path, sizeof(path), "f%d.ls", i); + if (!lologWriteFile(log, path, (const uint8_t *)"x", 1)) + break; + created++; + } + TEST_ASSERT_GREATER_THAN(3, created); + TEST_ASSERT_TRUE(log.remove("f0.ls")); + char extra[24]; + snprintf(extra, sizeof(extra), "f%d.ls", created + 10); + TEST_ASSERT_TRUE(lologWriteFile(log, extra, (const uint8_t *)"n", 1)); +} + +void test_cleaner_recycles_sectors(void) +{ + LoLogRamDevice dev(12); + LoLog log(dev); + log.format(); + for (int i = 0; i < 80; i++) { + char path[20]; + snprintf(path, sizeof(path), "c%d.ls", i); + if (!lologWriteFile(log, path, (const uint8_t *)"xy", 2)) + break; + } + for (int i = 0; i < 40; i++) { + char path[20]; + snprintf(path, sizeof(path), "c%d.ls", i); + log.remove(path); + } + for (int m = 0; m < 60; m++) + log.maintain(60000); + uint32_t usedAfter = (uint32_t)log.usedBytes(); + (void)usedAfter; + TEST_ASSERT_TRUE(log.exists("c79.ls")); + std::vector tail; + TEST_ASSERT_TRUE(lologReadFile(log, "c79.ls", tail)); + TEST_ASSERT_EQUAL_UINT8(0x78, tail[0]); +} + +void test_headroom_blocks_before_starve(void) +{ + LoLogRamDevice tiny(6); + LoLog log(tiny); + log.format(); + for (int i = 0; i < 200; i++) { + char path[32]; + snprintf(path, sizeof(path), "f%d.ls", i); + LoLog::OpenHandle *h = log.open(path, true, true); + if (!h) + break; + log.closeHandle(h); + } + LoLog::OpenHandle *last = log.open("overflow.ls", true, true); + TEST_ASSERT_NULL(last); +} diff --git a/ci/host/test/test_lolog_host/test_main.cpp b/ci/host/test/test_lolog_host/test_main.cpp new file mode 100644 index 0000000..d22ab26 --- /dev/null +++ b/ci/host/test/test_lolog_host/test_main.cpp @@ -0,0 +1,85 @@ +#include "lofs/lolog/LoLogRamDevice.h" +#include "lofs/lolog/LoLog.h" +#include + +LoLogRamDevice gDev(32); +LoLog gLog(gDev); + +void setUp(void) +{ + gDev.reset(); + gLog.format(); +} + +void tearDown(void) {} + +#define DECL(name) void name(void) + +DECL(test_mount_empty_after_format); +DECL(test_mount_erased_chip_without_format); +DECL(test_pure_replay_no_index_runs); +DECL(test_mount_after_reboot_simulation); + +DECL(test_mkdir_nested_and_overwrite); +DECL(test_rename_file); +DECL(test_rmdir_nonempty_fails_nonempty_ok_recursive); +DECL(test_prefix_collision_names); +DECL(test_large_file_inline_and_extent); +DECL(test_large_file_write_at_path); +DECL(test_stat_and_is_directory); + +DECL(test_commit_group_atomic_each_prog_call); +DECL(test_commit_group_each_prog_byte); +DECL(test_torn_tail_then_append); +DECL(test_torn_entry_crc_poke); +DECL(test_fault_single_byte_discards_group); +DECL(test_commit_group_partial_never_applied); + +DECL(test_index_flush_pure_reboot); +DECL(test_index_multiple_flushes_and_lookup); +DECL(test_tombstone_shadow_after_remount); +DECL(test_delete_when_near_full); +DECL(test_cleaner_recycles_sectors); +DECL(test_headroom_blocks_before_starve); + +DECL(test_random_ops_with_crash_injection); +DECL(test_directory_list_count); + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + UNITY_BEGIN(); + + RUN_TEST(test_mount_empty_after_format); + RUN_TEST(test_mount_erased_chip_without_format); + RUN_TEST(test_pure_replay_no_index_runs); + RUN_TEST(test_mount_after_reboot_simulation); + + RUN_TEST(test_mkdir_nested_and_overwrite); + RUN_TEST(test_rename_file); + RUN_TEST(test_rmdir_nonempty_fails_nonempty_ok_recursive); + RUN_TEST(test_prefix_collision_names); + RUN_TEST(test_large_file_inline_and_extent); + RUN_TEST(test_large_file_write_at_path); + RUN_TEST(test_stat_and_is_directory); + + RUN_TEST(test_commit_group_atomic_each_prog_call); + RUN_TEST(test_commit_group_each_prog_byte); + RUN_TEST(test_torn_tail_then_append); + RUN_TEST(test_torn_entry_crc_poke); + RUN_TEST(test_fault_single_byte_discards_group); + RUN_TEST(test_commit_group_partial_never_applied); + + RUN_TEST(test_index_flush_pure_reboot); + RUN_TEST(test_index_multiple_flushes_and_lookup); + RUN_TEST(test_tombstone_shadow_after_remount); + RUN_TEST(test_delete_when_near_full); + RUN_TEST(test_cleaner_recycles_sectors); + RUN_TEST(test_headroom_blocks_before_starve); + + RUN_TEST(test_random_ops_with_crash_injection); + RUN_TEST(test_directory_list_count); + + return UNITY_END(); +} diff --git a/core/LoBBSInstall.h b/core/LoBBSInstall.h index caac0db..f128226 100644 --- a/core/LoBBSInstall.h +++ b/core/LoBBSInstall.h @@ -21,7 +21,7 @@ const char *lobbsInstallOfflineHome(const LoBBSKernel &kernel); void lobbsInstallInit(LoBBSKernel &kernel); void lobbsInstallDatabaseOpened(LoBBSKernel &kernel); -/** `install_mounts` names joined with `|`, e.g. `sd|qspi|reserve|spare|internal`. */ +/** `install_mounts` names joined with `|`, e.g. `sd|lofs|extra|internal`. */ void lobbsInstallMountList(LoBBSCommandCtx &ctx, char *out, size_t cap); /** `/lobbs`, e.g. `/extra/lobbs`. */ diff --git a/docs/lobbs-internals.md b/docs/lobbs-internals.md index c5cd1e3..fa8c6cb 100644 --- a/docs/lobbs-internals.md +++ b/docs/lobbs-internals.md @@ -95,13 +95,13 @@ Machine: records encoded as LoScalar lines, sliced into pages with `ok [n:ma ## LoFS and install -LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `//…`. Slot names in install preference order: `sd`, `qspi`, `reserve`, `spare`, `internal`. Each slot maps to a `LoFSVolume` provider (adopted host LittleFS or LoBBS-mounted block device). Host composition lives in `lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp` and `lofs/platforms/meshcore/LoFSMountsMeshcore.cpp`. Mount roots cannot be removed, renamed, or used as copy sources. Cross-mount `mv` copies files then deletes the source. Directories require same-mount rename. +LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `//…`. Slot names in install preference order: `sd`, `lofs`, `extra`, `internal`. Each slot maps to a `LoFSVolume` provider (adopted host LittleFS, LoLog on `/lofs`, or SD). Host composition lives in `lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp` and `lofs/platforms/meshcore/LoFSMountsMeshcore.cpp`. Mount roots cannot be removed, renamed, or used as copy sources. Cross-mount `mv` copies files then deletes the source. Directories require same-mount rename. -`/spare` is the middle nRF52840 internal window (100 KiB at `0xD4000`–`0xED000`, weak symbols `__flash2_start` / `__flash2_end`). Meshtastic mounts it with `LoFSNrfFlashBlockDevice` when the linker provides the symbols. MeshCore adopts host `ExtraFS` on the same range when `EXTRAFS` is enabled. +`/lofs` is the exclusive LoLog slot. On boards with owned QSPI NOR (`LOFS_BOARD_HAS_QSPI`), LoLog uses `LoFSQspiNorBlockDevice` (about 2 MiB). Meshtastic QSPI envs link `nrf52840_s140_v7_qspi.ld`: application flash runs to `0xED000` with no `__flash2` gap. Other nRF52840 Meshtastic builds use `nrf52840_s140_v7.ld`, which stops the app at `0xD4000` and provides `__flash2_start` / `__flash2_end` (100 KiB) for `/lofs` via `LoFSNrfFlashRawDevice`. Meshtastic `InternalFS` stays at the Adafruit default (`0xED000` on nRF52840). ESP32 and boards with neither QSPI nor flash2 have no `/lofs`. -`/reserve` is the low window (`0xCC000`–`0xD4000`, `__flash3_*`) when the opt-in MeshCore linker script or Meshtastic layout leaves it empty. LoBBS mounts it with the same block device helper. +`/lofs` uses **LoLog** (`lofs/lolog/`, `LoFSLoLogVolume`): append-only data segments, on-flash index runs, background clean/flush via `LoFS::maintain()` from the Meshtastic module `runOnce` and `lobbsMeshCoreLoop`. `/format lofs` erases the volume and starts a new epoch. There is no LittleFS on `/lofs`. MeshCore does not adopt host QSPI LittleFS: when `QSPIFLASH` is set, the chip is `/lofs` LoLog like Meshtastic. -`/qspi` is onboard NOR (2 MiB). Meshtastic always mounts via `LoFSQspiNorBlockDevice` when `LOFS_ENABLE_EXTRA_QSPI`. MeshCore adopts `QSPIFlash` when `QSPIFLASH`, otherwise LoBBS mounts the chip when present. +`/extra` is MeshCore only: host `ExtraFS` LittleFS at `0xD4000` when `EXTRAFS` is enabled and the board has no QSPI (`!QSPIFLASH`). It is shared with the radio. Do not mount `/lofs` on the same flash range as `/extra`. `/internal` is the host primary filesystem (`InternalFS` / `FSCom` on Meshtastic, the filesystem pointer passed to `lobbsMeshCoreInit` on MeshCore). It is shared: LoBBS keeps a reserve on writes. `/format internal` on Meshtastic runs `FSCom.format()` then `nodeDB->saveToDisk()`. Formatting the install mount reruns `lobbsInstallInit`. @@ -109,7 +109,7 @@ LoFS exposes `/` as a virtual root listing mounts. Writable paths start with `/< Each mount has `dbSafe` (computed at boot: non-shared volumes are always db-safe; shared volumes are db-safe only when no higher-preference mount is present) and `formattable`. LittleFS mounts are formattable; `sd` is not. Install points come from the `install_mounts` list filter in preference order above. -MeshCore nRF52840 can opt into `/reserve` with `boards/nrf52840_s140_v7_lobbs_flash3.ld`. Example env: `RAK_4631_terminal_chat` in `lobbs-meshcore-firmware`. `lobbsMeshCoreInit` calls `lofsMeshcoreSetHostFilesystem` before `LoFS::begin`. +`lobbsMeshCoreInit` calls `lofsMeshcoreSetHostFilesystem` before `LoFS::begin`. Shared mounts keep reserve bytes free (16 KiB nRF52, 128 KiB other hardware, 0 Portduino). `LoFS::hasRoom(path, bytes)` rounds `bytes` up to the block size, adds slack for metadata, adds the mount reserve, and compares with free space. It returns true when the mount reports no size. LoDB checks it before every record write and returns `LODB_ERR_FULL`. FsCommands checks it for `/mkdir`, `/cp`, `/upload`, and cross-mount `/mv`. Apps map `LODB_ERR_FULL` to `Disk full.` via `lobbsDbErrorText`. Deletes are never blocked. diff --git a/docs/lobbs-sysop.md b/docs/lobbs-sysop.md index 3ea440f..d8feeb7 100644 --- a/docs/lobbs-sysop.md +++ b/docs/lobbs-sysop.md @@ -14,11 +14,11 @@ A new LoBBS node has no database until you install it. Only `/help` and `/instal | Hardware | Install mounts | | ------------------------ | ---------------------------------------------------- | -| nRF52840 Meshtastic | `sd`, `qspi`, `reserve` (if linked), `spare` (100 KB LoBBS) | -| nRF52840 MeshCore | `qspi`, `reserve` (opt-in linker), `spare` (host ExtraFS), `internal` | -| ESP32 and other hardware | `sd`, `qspi`, `internal` | +| nRF52840 Meshtastic | `sd`, `lofs` (QSPI or 100 KB flash2), `internal` | +| nRF52840 MeshCore | `lofs` (QSPI boards), `extra`, `internal` | +| ESP32 and other hardware | `sd`, `internal` | -Mounts are listed in preference order. `sd` appears when a card is present. `qspi` (about 2 MB) appears on Seeed XIAO nRF52840 kit and SenseCAP Solar Node builds (`LOBBS_EXTRA_QSPI=1`). On nRF52840 Meshtastic, `/internal` is the radio's own filesystem and is only offered when no better mount exists. +Mounts are listed in preference order. `sd` appears when a card is present. `/lofs` (LoLog, about 2 MB on QSPI or 100 KB on flash2) appears when that hardware exists. On nRF52840 Meshtastic, `/internal` is the radio's own filesystem and is only offered when no better mount exists. `/install ` creates the database on that mount and makes `` the SysOp. Use it from the local client (phone app connected over serial/BLE) or from a remote DM encrypted with a configured admin key. If the target already holds a LoBBS database, the same command adopts it and you must supply an existing SysOp username and password. Once installed, `/install` replies `Already installed at /.` @@ -31,7 +31,7 @@ Everything LoBBS stores lives under `//lobbs`: | `//lobbs/apps//` | Reserved for per-app files | | `//lobbs/home//` | Reserved for user files | -On boot LoBBS checks the install mounts in preference order and uses the first one holding `lobbs/install.ls`. Nothing is stored on `/internal`, so a Meshtastic wipe of `/internal` does not lose the install when it lives on `spare`, `qspi`, or `sd`. Pulling the SD card that holds it makes the node look new until the card is back. +On boot LoBBS checks the install mounts in preference order and uses the first one holding `lobbs/install.ls`. Nothing is stored on `/internal`, so a Meshtastic wipe of `/internal` does not lose the install when it lives on `lofs` or `sd`. Pulling the SD card that holds it makes the node look new until the card is back. ## Demo builds @@ -69,16 +69,18 @@ Config keys (uint32, defaults in parentheses): `session.max` (16), `session.idle | `/upload path [offset:b62]` | Chunked file write at byte offset, or show current size | | `/commit src dst` | Move when file CRC32 matches the 8-hex segment in the source name | | `/stat path` | `file N` or `dir` | -| `/df` | One line per mount, e.g. `qspi: 1.8M free of 2.0M (12% used) [lobbs]`. Free space excludes the `/internal` reserve. `[lobbs]` marks the install mount | +| `/df` | One line per mount, e.g. `lofs: 1.8M free of 2.0M (12% used) [lobbs]`. Free space excludes the `/internal` reserve. `[lobbs]` marks the install mount | | `/format mount [code]` | Erase a mount. The first call replies with a code; repeat with the code within 2 minutes from the same node. On `/internal` the radio settings are saved again from memory (BLE pairings are lost). On the install mount LoBBS goes blank until `/install`. `sd` is refused (format cards on a PC) | SysOps painting the wall or adding yarn do not use quota. ## Files -Paths use mount prefixes: `/internal/...`, `/spare/...` on nRF52840, `/sd/...`, `/qspi/...` on builds with `LOBBS_EXTRA_QSPI=1` and a QSPI flash chip. `/` lists mounts. Do not `/rmtree` `/lobbs` on your install mount unless you intend to wipe the BBS. +Paths use mount prefixes: `/internal/...`, `/extra/...` on MeshCore nRF52840, `/sd/...`, `/lofs/...` when that slot exists. `/` lists mounts. Do not `/rmtree` `/lobbs` on your install mount unless you intend to wipe the BBS. -`/internal` is shared with Meshtastic's own settings and node database. LoBBS leaves a reserve free there (16 KB on nRF52, 128 KB elsewhere). Posts, mail, uploads, and copies that would cut into it reply `Disk full.`. Deleting files always works. `/spare`, `/sd`, and `/qspi` have no reserve. +`/internal` is shared with Meshtastic's own settings and node database. LoBBS leaves a reserve free there (16 KB on nRF52, 128 KB elsewhere). Posts, mail, uploads, and copies that would cut into it reply `Disk full.`. Deleting files always works. `/extra` and `/sd` have no LoBBS reserve. `/lofs` uses LoLog with cleaner headroom held back so `Disk full.` appears before the log can deadlock; used bytes on `/df` are live data only. Other tools cannot read a LoLog-formatted volume. + +After upgrading from builds that used mount names `qspi`, `spare`, or LittleFS on QSPI, run `/format lofs` once (2-minute confirm) before `/install lofs`. Old data is not migrated. Relative paths work under your session working directory for `ls`, `cat`, `hex`, `stat`, `mkdir`, `cp`, `mv`, `upload`, and `commit`. `rm`, `rmdir`, and `rmtree` require absolute paths. diff --git a/lofs/LoFS.cpp b/lofs/LoFS.cpp index 24e176b..150c0f7 100644 --- a/lofs/LoFS.cpp +++ b/lofs/LoFS.cpp @@ -124,6 +124,16 @@ void LoFS::eachPresentMount(void (*fn)(void *ctx, const char *name), void *ctx) } } +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 = {}; diff --git a/lofs/LoFS.h b/lofs/LoFS.h index de6838c..6ad549c 100644 --- a/lofs/LoFS.h +++ b/lofs/LoFS.h @@ -5,7 +5,7 @@ #include "LoFile.h" /** - * Unified filesystem with a mount table in install preference order: /sd, /qspi, /reserve, /spare, /internal. + * Unified filesystem with a mount table in install preference order: /sd, /lofs, /extra, /internal. * "/" is a virtual root that lists mounts. Every other path must start with "//..." or "/". */ enum class LoFSMoveResult : uint8_t { @@ -67,6 +67,9 @@ class LoFS typedef bool (*ListCallback)(void *ctx, const char *basename, bool isDirectory, uint32_t size); static bool list(const char *dirpath, void *ctx, ListCallback fn); + /** Calls maintain() on each mounted volume (LoLog cleaning on lofs). */ + static void maintain(uint32_t budgetMs); + private: struct Mount { const char *name; diff --git a/lofs/lolog/LoLog.cpp b/lofs/lolog/LoLog.cpp new file mode 100644 index 0000000..7180216 --- /dev/null +++ b/lofs/lolog/LoLog.cpp @@ -0,0 +1,1263 @@ +#include "LoLog.h" +#include "lofs/lolog/LoLogConfig.h" +#include +#include +#include + +#if defined(LOBBS_PLATFORM_MESHTASTIC) || defined(LOBBS_PLATFORM_MESHCORE) +#include "platforms/LoPlatform.h" +#else +static uint32_t lologStubMs() +{ + static uint32_t t; + return t++; +} +#endif + +static uint32_t lologNowMs() +{ +#if defined(LOBBS_PLATFORM_MESHTASTIC) || defined(LOBBS_PLATFORM_MESHCORE) + return lobbsPlatformMillis(); +#else + return lologStubMs(); +#endif +} + +bool LoLog::NameKey::operator<(const NameKey &o) const +{ + if (dirId != o.dirId) + return dirId < o.dirId; + return name < o.name; +} + +LoLog::LoLog(LoFSRawDevice &device) : dev_(device) {} + +uint32_t LoLog::crc32(const uint8_t *data, size_t len) const +{ + uint32_t crc = 0xFFFFFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int b = 0; b < 8; b++) + crc = (crc >> 1) ^ (0xEDB88320 & (~((crc & 1) - 1))); + } + return ~crc; +} + +bool LoLog::readBytes(uint32_t addr, void *buf, size_t len) +{ + return dev_.read(addr, buf, len); +} + +bool LoLog::progBytes(uint32_t addr, const void *buf, size_t len) +{ + if ((addr & 3) != 0 || (len & 3) != 0) + return false; + return dev_.prog(addr, buf, len); +} + +bool LoLog::readSectorHeader(uint32_t sector, uint32_t &magic, uint32_t &epoch, uint32_t &seq, uint8_t &kind) +{ + uint8_t hdr[lolog::kHeaderSize]; + if (!readBytes(sector * lolog::kSectorSize, hdr, sizeof(hdr))) + return false; + magic = (uint32_t)hdr[0] | ((uint32_t)hdr[1] << 8) | ((uint32_t)hdr[2] << 16) | ((uint32_t)hdr[3] << 24); + epoch = (uint32_t)hdr[4] | ((uint32_t)hdr[5] << 8) | ((uint32_t)hdr[6] << 16) | ((uint32_t)hdr[7] << 24); + seq = (uint32_t)hdr[8] | ((uint32_t)hdr[9] << 8) | ((uint32_t)hdr[10] << 16) | ((uint32_t)hdr[11] << 24); + kind = hdr[12]; + uint32_t hcrc = (uint32_t)hdr[13] | ((uint32_t)hdr[14] << 8) | ((uint32_t)hdr[15] << 16); + uint32_t calc = crc32(hdr, 13) & 0xFFFFFF; + if (magic != lolog::kMagicUsed || calc != hcrc) + return false; + return true; +} + +bool LoLog::writeSectorHeader(uint32_t sector, uint32_t seq, uint8_t kind) +{ + uint8_t hdr[lolog::kHeaderSize]; + memset(hdr, 0xFF, sizeof(hdr)); + hdr[0] = (uint8_t)(lolog::kMagicUsed); + hdr[1] = (uint8_t)(lolog::kMagicUsed >> 8); + hdr[2] = (uint8_t)(lolog::kMagicUsed >> 16); + hdr[3] = (uint8_t)(lolog::kMagicUsed >> 24); + hdr[4] = (uint8_t)(epoch_); + hdr[5] = (uint8_t)(epoch_ >> 8); + hdr[6] = (uint8_t)(epoch_ >> 16); + hdr[7] = (uint8_t)(epoch_ >> 24); + hdr[8] = (uint8_t)(seq); + hdr[9] = (uint8_t)(seq >> 8); + hdr[10] = (uint8_t)(seq >> 16); + hdr[11] = (uint8_t)(seq >> 24); + hdr[12] = kind; + uint32_t hcrc = crc32(hdr, 13) & 0xFFFFFF; + hdr[13] = (uint8_t)hcrc; + hdr[14] = (uint8_t)(hcrc >> 8); + hdr[15] = (uint8_t)(hcrc >> 16); + return progBytes(sector * lolog::kSectorSize, hdr, sizeof(hdr)); +} + +bool LoLog::allocDataSector() +{ + const uint32_t n = dev_.sectorCount(); + if (n == 0) + return false; + static uint32_t rr = 0; + for (uint32_t i = 0; i < n; i++) { + uint32_t s = (rr + i) % n; + if (s >= sectorUsed_.size() || sectorUsed_[s]) + continue; + if (!dev_.eraseSector(s)) + continue; + sectorUsed_[s] = 1; + liveInSector_[s] = lolog::kHeaderSize; + rr = (s + 1) % n; + activeSector_ = (uint16_t)s; + activeOff_ = lolog::kHeaderSize; + if (!writeSectorHeader(s, nextSeq_++, lolog::kSegData)) + return false; + return true; + } + return false; +} + +bool LoLog::appendRaw(const uint8_t *payload, size_t payloadLen, uint8_t type, uint8_t flags) +{ + const uint32_t entLen = (uint32_t)(8 + payloadLen); + const uint32_t padded = (entLen + 3) & ~3u; + if (activeSector_ == 0xFFFF || activeOff_ + padded > lolog::kSectorSize) { + if (!allocDataSector()) + return false; + } + if (!hasRoomFor(padded + lolog::kHeadroomSectors * lolog::kSectorSize)) + return false; + + uint8_t stack[8 + lolog::kInlineMax]; + if (payloadLen > lolog::kInlineMax) + return false; + stack[0] = (uint8_t)(entLen - 8); + stack[1] = (uint8_t)((entLen - 8) >> 8); + stack[2] = type; + stack[3] = flags; + memcpy(stack + 4, payload, payloadLen); + uint32_t c = crc32(stack + 4, payloadLen); + memcpy(stack + 4 + payloadLen, &c, 4); + size_t pad = padded - entLen; + if (pad) + memset(stack + entLen, 0xFF, pad); + + uint32_t addr = activeSector_ * lolog::kSectorSize + activeOff_; + if (!progBytes(addr, stack, padded)) + return false; + activeOff_ += padded; + liveInSector_[activeSector_] = activeOff_; + liveBytes_ += padded; + lastActivityMs_ = nowMs(); + return true; +} + +void LoLog::beginGroup() +{ + pendingGroup_.clear(); +} + +bool LoLog::endGroup() +{ + for (size_t i = 0; i < pendingGroup_.size(); i++) { + uint8_t fl = (i + 1 == pendingGroup_.size()) ? lolog::kFlagGroupEnd : 0; + if (!appendRaw(pendingGroup_[i].payload.data(), pendingGroup_[i].payload.size(), pendingGroup_[i].type, fl)) + return false; + } + pendingGroup_.clear(); + return true; +} + +void LoLog::applyEntry(uint8_t type, const uint8_t *payload, size_t len) +{ + if (type == lolog::kEntDir && len >= 8) { + uint32_t parent = (uint32_t)payload[0] | ((uint32_t)payload[1] << 8) | ((uint32_t)payload[2] << 16) | + ((uint32_t)payload[3] << 24); + uint32_t dirId = (uint32_t)payload[4] | ((uint32_t)payload[5] << 8) | ((uint32_t)payload[6] << 16) | + ((uint32_t)payload[7] << 24); + const char *name = (const char *)(payload + 8); + if (dirId >= dirParent_.size()) + dirParent_.resize(dirId + 1, lolog::kRootDirId); + dirParent_[dirId] = parent; + upsertName(parent, name, NameVal{true, dirId}); + } else if (type == lolog::kEntFile && len >= 16) { + uint32_t dirId = (uint32_t)payload[0] | ((uint32_t)payload[1] << 8) | ((uint32_t)payload[2] << 16) | + ((uint32_t)payload[3] << 24); + uint32_t inode = (uint32_t)payload[4] | ((uint32_t)payload[5] << 8) | ((uint32_t)payload[6] << 16) | + ((uint32_t)payload[7] << 24); + uint32_t size = (uint32_t)payload[8] | ((uint32_t)payload[9] << 8) | ((uint32_t)payload[10] << 16) | + ((uint32_t)payload[11] << 24); + uint32_t mtime = (uint32_t)payload[12] | ((uint32_t)payload[13] << 8) | ((uint32_t)payload[14] << 16) | + ((uint32_t)payload[15] << 24); + const char *name = (const char *)(payload + 16); + size_t nameLen = strnlen(name, len - 16); + size_t inlineOff = 16 + nameLen + 1; + FileMeta meta; + meta.dirId = dirId; + meta.name.assign(name, nameLen); + meta.isDir = false; + meta.inodeId = inode; + meta.size = size; + meta.mtime = mtime; + if (inlineOff < len) + meta.data.assign(payload + inlineOff, payload + len); + if (inode >= byInode_.size()) + byInode_.resize(inode + 1); + byInode_[inode] = meta; + upsertName(dirId, meta.name.c_str(), NameVal{false, inode}); + } else if (type == lolog::kEntExtent && len >= 8) { + uint32_t inode = (uint32_t)payload[0] | ((uint32_t)payload[1] << 8) | ((uint32_t)payload[2] << 16) | + ((uint32_t)payload[3] << 24); + uint32_t offset = (uint32_t)payload[4] | ((uint32_t)payload[5] << 8) | ((uint32_t)payload[6] << 16) | + ((uint32_t)payload[7] << 24); + const uint8_t *data = payload + 8; + size_t dlen = len - 8; + FileMeta *m = metaByInode(inode); + if (!m) + return; + if (m->data.size() < offset + dlen) + m->data.resize(offset + dlen); + memcpy(m->data.data() + offset, data, dlen); + if (offset + dlen > m->size) + m->size = (uint32_t)(offset + dlen); + } else if (type == lolog::kEntTomb && len >= 4) { + uint32_t dirId = (uint32_t)payload[0] | ((uint32_t)payload[1] << 8) | ((uint32_t)payload[2] << 16) | + ((uint32_t)payload[3] << 24); + if (len == 4) { + deadDirIds_.push_back(dirId); + tombDir(dirId); + } else { + const char *name = (const char *)(payload + 4); + eraseName(dirId, name); + } + } +} + +void LoLog::replaySector(uint32_t sector) +{ + uint32_t magic, epoch, seq; + uint8_t kind = 0; + if (!readSectorHeader(sector, magic, epoch, seq, kind) || kind != lolog::kSegData) + return; + if (epoch != epoch_) + return; + if (seq <= coveredSeq_) + return; + + uint32_t off = lolog::kHeaderSize; + std::vector group; + while (off + 8 <= lolog::kSectorSize) { + uint8_t hdr[8]; + if (!readBytes(sector * lolog::kSectorSize + off, hdr, 8)) + break; + uint16_t plen = (uint16_t)hdr[0] | ((uint16_t)hdr[1] << 8); + if (plen == lolog::kEntryEnd) + break; + uint8_t type = hdr[2]; + uint8_t flags = hdr[3]; + uint32_t entLen = 8 + plen; + uint32_t padded = (entLen + 3) & ~3u; + if (off + padded > lolog::kSectorSize) + break; + std::vector payload(plen); + if (plen > 0 && !readBytes(sector * lolog::kSectorSize + off + 4, payload.data(), plen)) + break; + uint32_t stored; + if (!readBytes(sector * lolog::kSectorSize + off + 4 + plen, &stored, 4)) + break; + if (crc32(payload.data(), plen) != stored) + break; + PendingEnt pe{type, std::move(payload)}; + group.push_back(std::move(pe)); + off += padded; + if (flags & lolog::kFlagGroupEnd) { + for (auto &e : group) + applyEntry(e.type, e.payload.data(), e.payload.size()); + group.clear(); + } + } +} + +void LoLog::rebuildIndexFromFlash() +{ + nameIndex_.clear(); + byInode_.clear(); + dirParent_.clear(); + dirParent_.resize(1); + dirParent_[0] = 0; + sectorUsed_.assign(dev_.sectorCount(), 0); + liveInSector_.assign(dev_.sectorCount(), 0); + liveBytes_ = 0; + epoch_ = 0; + coveredSeq_ = 0; + activeSector_ = 0xFFFF; + + uint32_t maxEpoch = 0; + for (uint32_t s = 0; s < dev_.sectorCount(); s++) { + uint32_t magic, ep, seq; + uint8_t kind = 0; + if (!readSectorHeader(s, magic, ep, seq, kind)) + continue; + if (ep > maxEpoch) + maxEpoch = ep; + } + if (maxEpoch == 0) { + uint8_t probe; + if (readBytes(0, &probe, 1) && probe == 0xFF) + return; + } + epoch_ = maxEpoch; + indexRunSeqs_.clear(); + + for (uint32_t s = 0; s < dev_.sectorCount(); s++) { + uint32_t magic, ep, seq; + uint8_t kind = 0; + if (!readSectorHeader(s, magic, ep, seq, kind)) + continue; + if (ep != epoch_) + continue; + sectorUsed_[s] = 1; + uint32_t end = lolog::kSectorSize; + uint8_t scan[8]; + uint32_t o = lolog::kHeaderSize; + while (o + 8 <= lolog::kSectorSize) { + if (!readBytes(s * lolog::kSectorSize + o, scan, 8)) + break; + uint16_t plen = (uint16_t)scan[0] | ((uint16_t)scan[1] << 8); + if (plen == lolog::kEntryEnd) { + end = o; + break; + } + uint32_t padded = (8 + plen + 3) & ~3u; + if (o + padded > lolog::kSectorSize) + break; + o += padded; + } + liveInSector_[s] = end; + liveBytes_ += end; + if (kind == lolog::kSegIndex) + indexRunSeqs_.push_back(seq); + } + + const bool hadIndex = loadNewestIndexRun(); + const uint32_t replayFrom = coveredSeq_; + for (uint32_t s = 0; s < dev_.sectorCount(); s++) { + uint32_t magic, ep, seq; + uint8_t kind = 0; + if (!readSectorHeader(s, magic, ep, seq, kind)) + continue; + if (ep != epoch_ || kind != lolog::kSegData) + continue; + if (hadIndex && seq <= replayFrom) + continue; + replaySector(s); + } + + for (uint32_t s = 0; s < dev_.sectorCount(); s++) { + uint32_t magic, ep, seq; + uint8_t kind = 0; + if (readSectorHeader(s, magic, ep, seq, kind) && kind == lolog::kSegData && ep == epoch_) { + if (activeSector_ == 0xFFFF || seq >= nextSeq_) { + activeSector_ = (uint16_t)s; + activeOff_ = liveInSector_[s]; + nextSeq_ = seq + 1; + } + } + } +} + +bool LoLog::mount() +{ + if (!dev_.begin()) + return false; + mounted_ = true; + rebuildIndexFromFlash(); + nextDirId_ = std::max(nextDirId_, (uint32_t)dirParent_.size()); + for (const auto &m : byInode_) + if (m.inodeId >= nextInodeId_) + nextInodeId_ = m.inodeId + 1; + return true; +} + +bool LoLog::format() +{ + if (!dev_.begin()) + return false; + for (uint32_t s = 0; s < dev_.sectorCount(); s++) + dev_.eraseSector(s); + epoch_ = (uint32_t)lologNowMs() | 1; + nextSeq_ = 1; + nextDirId_ = 1; + nextInodeId_ = 1; + coveredSeq_ = 0; + nameIndex_.clear(); + byInode_.clear(); + dirParent_.clear(); + dirParent_.resize(1); + dirParent_[0] = 0; + sectorUsed_.assign(dev_.sectorCount(), 0); + liveInSector_.assign(dev_.sectorCount(), 0); + liveBytes_ = 0; + activeSector_ = 0xFFFF; + mounted_ = true; + return allocDataSector(); +} + +uint32_t LoLog::nowMs() const +{ + return lologNowMs(); +} + +uint64_t LoLog::totalBytes() const +{ + return (uint64_t)dev_.sectorCount() * dev_.sectorSize(); +} + +uint64_t LoLog::usedBytes() const +{ + return liveBytes_; +} + +void LoLog::spaceHint(uint32_t *blockOut, uint32_t *slackOut) const +{ + if (blockOut) + *blockOut = 256; + if (slackOut) + *slackOut = (lolog::kHeadroomSectors + 1) * lolog::kSectorSize; +} + +uint32_t LoLog::freeSectors() const +{ + uint32_t n = 0; + for (uint32_t s = 0; s < sectorUsed_.size(); s++) + if (!sectorUsed_[s]) + n++; + return n; +} + +bool LoLog::hasRoomFor(uint32_t bytes) const +{ + const uint64_t reserve = (uint64_t)(lolog::kHeadroomSectors + 1) * lolog::kSectorSize; + const uint64_t cap = totalBytes(); + return liveBytes_ + bytes + reserve <= cap; +} + +bool LoLog::lookupName(uint32_t dirId, const char *name, NameVal &out) const +{ + NameKey k{dirId, name ? name : ""}; + for (const auto &p : nameIndex_) { + if (p.first.dirId == k.dirId && p.first.name == k.name) { + out = p.second; + return true; + } + } + return false; +} + +void LoLog::upsertName(uint32_t dirId, const char *name, const NameVal &val) +{ + NameKey k{dirId, name ? name : ""}; + for (auto &p : nameIndex_) { + if (p.first.dirId == k.dirId && p.first.name == k.name) { + p.second = val; + return; + } + } + nameIndex_.push_back({k, val}); +} + +void LoLog::eraseName(uint32_t dirId, const char *name) +{ + NameKey k{dirId, name ? name : ""}; + nameIndex_.erase( + std::remove_if(nameIndex_.begin(), nameIndex_.end(), + [&](const std::pair &p) { + return p.first.dirId == k.dirId && p.first.name == k.name; + }), + nameIndex_.end()); +} + +LoLog::FileMeta *LoLog::metaByInode(uint32_t inodeId) +{ + return inodeId < byInode_.size() ? &byInode_[inodeId] : nullptr; +} + +const LoLog::FileMeta *LoLog::metaByInode(uint32_t inodeId) const +{ + return inodeId < byInode_.size() ? &byInode_[inodeId] : nullptr; +} + +void LoLog::tombDir(uint32_t dirId) +{ + nameIndex_.erase( + std::remove_if(nameIndex_.begin(), nameIndex_.end(), + [&](const std::pair &p) { + return p.second.isDir && p.second.id == dirId; + }), + nameIndex_.end()); +} + +bool LoLog::dirEmpty(uint32_t dirId) const +{ + for (const auto &p : nameIndex_) { + if (p.first.dirId == dirId) + return false; + } + return true; +} + +static void splitPath(const char *rel, std::vector &parts) +{ + parts.clear(); + if (!rel) + return; + while (*rel == '/') + rel++; + std::string cur; + for (; *rel; rel++) { + if (*rel == '/') { + if (!cur.empty()) { + parts.push_back(cur); + cur.clear(); + } + } else + cur += *rel; + } + if (!cur.empty()) + parts.push_back(cur); +} + +bool LoLog::resolvePath(const char *relPath, uint32_t &dirIdOut, std::string &nameOut, bool createDirs) +{ + std::vector parts; + splitPath(relPath, parts); + if (parts.empty()) + return false; + uint32_t dirId = lolog::kRootDirId; + for (size_t i = 0; i + 1 < parts.size(); i++) { + NameVal v; + if (!lookupName(dirId, parts[i].c_str(), v) || !v.isDir) + return false; + dirId = v.id; + } + nameOut = parts.back(); + dirIdOut = dirId; + if (createDirs) { + (void)createDirs; + } + return true; +} + +bool LoLog::exists(const char *relPath) +{ + uint32_t dirId; + std::string name; + if (!resolvePath(relPath, dirId, name, false)) + return strcmp(relPath, "/") == 0 || (relPath && relPath[0] == '\0'); + NameVal v; + return lookupName(dirId, name.c_str(), v); +} + +bool LoLog::isDirectory(const char *relPath) +{ + uint32_t dirId; + std::string name; + if (!resolvePath(relPath, dirId, name, false)) + return relPath && (strcmp(relPath, "/") == 0 || relPath[0] == '\0'); + NameVal v; + if (!lookupName(dirId, name.c_str(), v)) + return false; + return v.isDir; +} + +bool LoLog::statPath(const char *relPath, uint32_t *sizeOut, bool *isDirOut) +{ + if (sizeOut) + *sizeOut = 0; + if (isDirOut) + *isDirOut = false; + if (relPath && (strcmp(relPath, "/") == 0 || relPath[0] == '\0')) { + if (isDirOut) + *isDirOut = true; + return true; + } + uint32_t dirId; + std::string name; + if (!resolvePath(relPath, dirId, name, false)) + return false; + NameVal v; + if (!lookupName(dirId, name.c_str(), v)) + return false; + if (isDirOut) + *isDirOut = v.isDir; + if (!v.isDir && sizeOut) { + const FileMeta *m = metaByInode(v.id); + if (m) + *sizeOut = m->size; + } + return true; +} + +bool LoLog::mkdir(const char *relPath) +{ + std::vector parts; + splitPath(relPath, parts); + if (parts.empty()) + return false; + uint32_t dirId = lolog::kRootDirId; + for (size_t i = 0; i + 1 < parts.size(); i++) { + NameVal v; + if (!lookupName(dirId, parts[i].c_str(), v)) { + std::string sub; + for (size_t j = 0; j <= i; j++) { + if (j) + sub += '/'; + sub += parts[j]; + } + if (!mkdir(sub.c_str())) + return false; + if (!lookupName(dirId, parts[i].c_str(), v) || !v.isDir) + return false; + } + dirId = v.id; + } + uint32_t parentDir = dirId; + std::string name = parts.back(); + NameVal existing; + if (lookupName(parentDir, name.c_str(), existing)) + return false; + uint32_t newDir = nextDirId_++; + if (newDir >= dirParent_.size()) + dirParent_.resize(newDir + 1, lolog::kRootDirId); + dirParent_[newDir] = parentDir; + beginGroup(); + uint8_t pl[8 + 64]; + pl[0] = (uint8_t)parentDir; + pl[1] = (uint8_t)(parentDir >> 8); + pl[2] = (uint8_t)(parentDir >> 16); + pl[3] = (uint8_t)(parentDir >> 24); + pl[4] = (uint8_t)newDir; + pl[5] = (uint8_t)(newDir >> 8); + pl[6] = (uint8_t)(newDir >> 16); + pl[7] = (uint8_t)(newDir >> 24); + strncpy((char *)(pl + 8), name.c_str(), 63); + pendingGroup_.push_back({lolog::kEntDir, std::vector(pl, pl + 8 + strlen((char *)(pl + 8)) + 1)}); + if (!endGroup()) + return false; + upsertName(parentDir, name.c_str(), NameVal{true, newDir}); + return true; +} + +bool LoLog::remove(const char *relPath) +{ + uint32_t dirId; + std::string name; + if (!resolvePath(relPath, dirId, name, false)) + return false; + NameVal v; + if (!lookupName(dirId, name.c_str(), v) || v.isDir) + return false; + beginGroup(); + uint8_t pl[4 + 64]; + pl[0] = (uint8_t)dirId; + pl[1] = (uint8_t)(dirId >> 8); + pl[2] = (uint8_t)(dirId >> 16); + pl[3] = (uint8_t)(dirId >> 24); + strncpy((char *)(pl + 4), name.c_str(), 63); + pendingGroup_.push_back( + {lolog::kEntTomb, std::vector(pl, pl + 4 + strlen((char *)(pl + 4)) + 1)}); + if (!endGroup()) + return false; + eraseName(dirId, name.c_str()); + return true; +} + +bool LoLog::rename(const char *oldPath, const char *newPath) +{ + uint32_t oDir, nDir; + std::string oName, nName; + if (!resolvePath(oldPath, oDir, oName, false) || !resolvePath(newPath, nDir, nName, false)) + return false; + NameVal v; + if (!lookupName(oDir, oName.c_str(), v)) + return false; + if (lookupName(nDir, nName.c_str(), v)) + return false; + if (v.isDir) { + if (!mkdir(newPath)) + return false; + return rmdir(oldPath, true); + } + FileMeta *m = metaByInode(v.id); + if (!m) + return false; + FileMeta copy = *m; + copy.dirId = nDir; + copy.name = nName; + if (!commitFileMeta(copy)) + return false; + remove(oldPath); + return true; +} + +bool LoLog::rmdir(const char *relPath, bool recursive) +{ + uint32_t dirId; + std::string name; + if (!resolvePath(relPath, dirId, name, false)) + return false; + NameVal v; + if (!lookupName(dirId, name.c_str(), v) || !v.isDir) + return false; + if (!recursive && !dirEmpty(v.id)) + return false; + beginGroup(); + uint8_t pl[4]; + pl[0] = (uint8_t)v.id; + pl[1] = (uint8_t)(v.id >> 8); + pl[2] = (uint8_t)(v.id >> 16); + pl[3] = (uint8_t)(v.id >> 24); + pendingGroup_.push_back({lolog::kEntTomb, std::vector(pl, pl + 4)}); + if (!endGroup()) + return false; + tombDir(v.id); + eraseName(dirId, name.c_str()); + return true; +} + +static uint32_t paddedEntrySize(size_t payloadLen) +{ + const uint32_t entLen = (uint32_t)(8 + payloadLen); + return (entLen + 3u) & ~3u; +} + +bool LoLog::commitFileMeta(FileMeta &meta) +{ + const bool large = meta.data.size() > lolog::kInlineMax; + const size_t inlineBytes = large ? 0 : meta.data.size(); + uint32_t groupNeed = paddedEntrySize(16 + meta.name.size() + 1 + inlineBytes); + if (large) { + size_t off = 0; + while (off < meta.data.size()) { + const size_t remain = meta.data.size() - off; + const size_t chunk = remain < 512 ? remain : 512; + groupNeed += paddedEntrySize(8 + chunk); + off += chunk; + } + } + if (!hasRoomFor(groupNeed)) + return false; + if (meta.inodeId == 0) { + meta.inodeId = nextInodeId_++; + if (meta.inodeId >= byInode_.size()) + byInode_.resize(meta.inodeId + 1); + } + meta.size = (uint32_t)meta.data.size(); + meta.mtime = nowMs(); + beginGroup(); + std::vector pl(16 + meta.name.size() + 1 + inlineBytes); + pl[0] = (uint8_t)meta.dirId; + pl[1] = (uint8_t)(meta.dirId >> 8); + pl[2] = (uint8_t)(meta.dirId >> 16); + pl[3] = (uint8_t)(meta.dirId >> 24); + pl[4] = (uint8_t)meta.inodeId; + pl[5] = (uint8_t)(meta.inodeId >> 8); + pl[6] = (uint8_t)(meta.inodeId >> 16); + pl[7] = (uint8_t)(meta.inodeId >> 24); + pl[8] = (uint8_t)meta.size; + pl[9] = (uint8_t)(meta.size >> 8); + pl[10] = (uint8_t)(meta.size >> 16); + pl[11] = (uint8_t)(meta.size >> 24); + pl[12] = (uint8_t)meta.mtime; + pl[13] = (uint8_t)(meta.mtime >> 8); + pl[14] = (uint8_t)(meta.mtime >> 16); + pl[15] = (uint8_t)(meta.mtime >> 24); + memcpy(pl.data() + 16, meta.name.c_str(), meta.name.size() + 1); + if (inlineBytes) + memcpy(pl.data() + 16 + meta.name.size() + 1, meta.data.data(), inlineBytes); + pendingGroup_.push_back({lolog::kEntFile, std::move(pl)}); + if (large) { + size_t off = 0; + while (off < meta.data.size()) { + const size_t remain = meta.data.size() - off; + size_t chunk = remain < 512 ? remain : 512; + std::vector ep(8 + chunk); + ep[0] = (uint8_t)meta.inodeId; + ep[1] = (uint8_t)(meta.inodeId >> 8); + ep[2] = (uint8_t)(meta.inodeId >> 16); + ep[3] = (uint8_t)(meta.inodeId >> 24); + ep[4] = (uint8_t)off; + ep[5] = (uint8_t)(off >> 8); + ep[6] = (uint8_t)(off >> 16); + ep[7] = (uint8_t)(off >> 24); + memcpy(ep.data() + 8, meta.data.data() + off, chunk); + pendingGroup_.push_back({lolog::kEntExtent, std::move(ep)}); + off += chunk; + } + } + if (!endGroup()) + return false; + byInode_[meta.inodeId] = meta; + upsertName(meta.dirId, meta.name.c_str(), NameVal{false, meta.inodeId}); + return true; +} + +LoLog::OpenHandle *LoLog::open(const char *relPath, bool write, bool create) +{ + uint32_t dirId; + std::string name; + if (!resolvePath(relPath, dirId, name, false)) { + if (!create || !relPath) + return nullptr; + if (!resolvePath(relPath, dirId, name, false)) + return nullptr; + } + NameVal v; + if (!lookupName(dirId, name.c_str(), v)) { + if (!write || !create) + return nullptr; + FileMeta meta; + meta.dirId = dirId; + meta.name = name; + meta.data.clear(); + if (!commitFileMeta(meta)) + return nullptr; + v = NameVal{false, meta.inodeId}; + } + if (v.isDir) + return nullptr; + auto *h = new OpenHandle(); + h->inodeId = v.id; + h->write = write; + h->pos = 0; + FileMeta *m = metaByInode(v.id); + if (m && write) + h->writeBuf = m->data; + strncpy(h->nameBuf, name.c_str(), sizeof(h->nameBuf) - 1); + return h; +} + +void LoLog::closeHandle(OpenHandle *h) +{ + if (!h) + return; + if (h->write && h->dirty) { + FileMeta *m = metaByInode(h->inodeId); + if (m) { + m->data = h->writeBuf; + commitFileMeta(*m); + } + } + delete h; +} + +size_t LoLog::readHandle(OpenHandle *h, uint8_t *buf, size_t len) +{ + if (!h || h->isDirIter || !buf) + return 0; + const FileMeta *m = metaByInode(h->inodeId); + if (!m) + return 0; + if (h->pos >= m->data.size()) + return 0; + const size_t avail = m->data.size() - (size_t)h->pos; + size_t n = len < avail ? len : avail; + memcpy(buf, m->data.data() + h->pos, n); + h->pos += (uint32_t)n; + return n; +} + +size_t LoLog::writeHandle(OpenHandle *h, const uint8_t *buf, size_t len) +{ + if (!h || !h->write || !buf) + return 0; + static constexpr size_t kWriteBufMax = 65536; + if (h->pos + len > kWriteBufMax) + len = kWriteBufMax - h->pos; + if (h->writeBuf.size() < h->pos + len) + h->writeBuf.resize(h->pos + len); + memcpy(h->writeBuf.data() + h->pos, buf, len); + h->pos += (uint32_t)len; + h->dirty = true; + return len; +} + +bool LoLog::seekHandle(OpenHandle *h, uint32_t pos) +{ + if (!h || h->isDirIter) + return false; + h->pos = pos; + return true; +} + +uint32_t LoLog::sizeHandle(OpenHandle *h) const +{ + if (!h || h->isDirIter) + return 0; + const FileMeta *m = metaByInode(h->inodeId); + return m ? m->size : 0; +} + +bool LoLog::isDirHandle(OpenHandle *h) const +{ + return h && h->isDirIter; +} + +void LoLog::flushHandle(OpenHandle *h) +{ + if (!h || !h->write || !h->dirty) + return; + FileMeta *m = metaByInode(h->inodeId); + if (m) { + m->data = h->writeBuf; + commitFileMeta(*m); + h->dirty = false; + } +} + +const char *LoLog::nameHandle(OpenHandle *h) const +{ + return h ? h->nameBuf : ""; +} + +LoLog::OpenHandle *LoLog::openDir(const char *relPath) +{ + uint32_t dirId = lolog::kRootDirId; + std::vector parts; + splitPath(relPath, parts); + for (const auto &p : parts) { + NameVal v; + if (!lookupName(dirId, p.c_str(), v) || !v.isDir) + return nullptr; + dirId = v.id; + } + auto *h = new OpenHandle(); + h->isDirIter = true; + h->dirId = dirId; + h->dirIdx = 0; + return h; +} + +LoLog::OpenHandle *LoLog::openNextInDir(OpenHandle *dir) +{ + if (!dir || !dir->isDirIter) + return nullptr; + while (dir->dirIdx < nameIndex_.size()) { + const auto &p = nameIndex_[dir->dirIdx++]; + if (p.first.dirId != dir->dirId) + continue; + auto *f = new OpenHandle(); + f->isDirIter = false; + f->inodeId = p.second.isDir ? 0 : p.second.id; + if (p.second.isDir) + f->isDirIter = true; + strncpy(f->nameBuf, p.first.name.c_str(), sizeof(f->nameBuf) - 1); + if (p.second.isDir) { + f->dirId = p.second.id; + f->isDirIter = true; + } + return f; + } + return nullptr; +} + +bool LoLog::writeAtPath(const char *relPath, uint32_t offset, const uint8_t *data, size_t len) +{ + OpenHandle *h = open(relPath, true, true); + if (!h) + return false; + if (offset > 0) + seekHandle(h, offset); + size_t w = writeHandle(h, data, len); + flushHandle(h); + closeHandle(h); + return w == len; +} + +static constexpr uint32_t kIndexBodyTag = 0x49524958; // IRIX + +bool LoLog::allocIndexSector(uint32_t §orOut, uint32_t runSeq) +{ + const uint32_t n = dev_.sectorCount(); + if (n == 0) + return false; + static uint32_t rrIdx = 0; + for (uint32_t i = 0; i < n; i++) { + uint32_t s = (rrIdx + i) % n; + if (sectorUsed_[s]) + continue; + uint32_t ec = dev_.eraseCount(s); + (void)ec; + if (!dev_.eraseSector(s)) + continue; + sectorUsed_[s] = 1; + liveInSector_[s] = lolog::kHeaderSize; + rrIdx = (s + 1) % n; + if (!writeSectorHeader(s, runSeq, lolog::kSegIndex)) + return false; + sectorOut = s; + return true; + } + return false; +} + +bool LoLog::flushIndexRun() +{ + uint32_t sector = 0; + const uint32_t runSeq = nextSeq_++; + if (!allocIndexSector(sector, runSeq)) + return false; + + std::vector body; + body.resize(32); + body[0] = (uint8_t)kIndexBodyTag; + body[1] = (uint8_t)(kIndexBodyTag >> 8); + body[2] = (uint8_t)(kIndexBodyTag >> 16); + body[3] = (uint8_t)(kIndexBodyTag >> 24); + const uint32_t cov = nextSeq_ > 1 ? nextSeq_ - 1 : 0; + memcpy(body.data() + 4, &cov, 4); + memcpy(body.data() + 8, &nextDirId_, 4); + memcpy(body.data() + 12, &nextInodeId_, 4); + memcpy(body.data() + 16, &liveBytes_, 8); + const uint32_t nNames = (uint32_t)nameIndex_.size(); + const uint32_t nDead = (uint32_t)deadDirIds_.size(); + memcpy(body.data() + 24, &nNames, 4); + memcpy(body.data() + 28, &nDead, 4); + + for (const auto &p : nameIndex_) { + const uint32_t dirId = p.first.dirId; + const uint32_t nameLen = (uint32_t)p.first.name.size(); + const uint8_t isDir = p.second.isDir ? 1 : 0; + const uint32_t id = p.second.id; + size_t o = body.size(); + body.resize(o + 4 + 4 + nameLen + 1 + 1 + 4); + memcpy(body.data() + o, &dirId, 4); + o += 4; + memcpy(body.data() + o, &nameLen, 4); + o += 4; + memcpy(body.data() + o, p.first.name.c_str(), nameLen + 1); + o += nameLen + 1; + body.data()[o++] = isDir; + memcpy(body.data() + o, &id, 4); + } + for (uint32_t d : deadDirIds_) { + size_t o = body.size(); + body.resize(o + 4); + memcpy(body.data() + o, &d, 4); + } + uint32_t nDirParent = 0; + for (size_t i = 1; i < dirParent_.size(); i++) + nDirParent++; + size_t oDp = body.size(); + body.resize(oDp + 4); + memcpy(body.data() + oDp, &nDirParent, 4); + for (size_t i = 1; i < dirParent_.size(); i++) { + uint32_t dirId = (uint32_t)i; + uint32_t parent = dirParent_[i]; + size_t o = body.size(); + body.resize(o + 8); + memcpy(body.data() + o, &dirId, 4); + memcpy(body.data() + o + 4, &parent, 4); + } + + const uint32_t bodyCrc = crc32(body.data(), body.size()); + body.resize(body.size() + 4); + memcpy(body.data() + body.size() - 4, &bodyCrc, 4); + + uint32_t off = lolog::kHeaderSize; + const uint32_t padded = (uint32_t)((body.size() + 3) & ~3u); + if (off + padded > lolog::kSectorSize) + return false; + std::vector pad(padded, 0xFF); + memcpy(pad.data(), body.data(), body.size()); + if (!progBytes(sector * lolog::kSectorSize + off, pad.data(), padded)) + return false; + liveInSector_[sector] = off + padded; + liveBytes_ += padded; + coveredSeq_ = cov; + indexRunSeqs_.push_back(runSeq); + memtableBytes_ = 0; + lastFlushMs_ = nowMs(); + mergeIndexRunsIfNeeded(); + return true; +} + +void LoLog::mergeIndexRunsIfNeeded() +{ + if (indexRunSeqs_.size() <= lolog::kMaxRuns) + return; + const uint32_t keep = indexRunSeqs_.back(); + indexRunSeqs_.clear(); + indexRunSeqs_.push_back(keep); +} + +bool LoLog::loadNewestIndexRun() +{ + uint32_t bestSeq = 0; + uint32_t bestSec = 0xFFFFFFFF; + for (uint32_t s = 0; s < dev_.sectorCount(); s++) { + uint32_t magic, ep, seq; + uint8_t kind = 0; + if (!readSectorHeader(s, magic, ep, seq, kind)) + continue; + if (ep != epoch_ || kind != lolog::kSegIndex) + continue; + if (seq >= bestSeq) { + bestSeq = seq; + bestSec = s; + } + } + if (bestSec == 0xFFFFFFFF) + return false; + + std::vector body(lolog::kSectorSize - lolog::kHeaderSize); + if (!readBytes(bestSec * lolog::kSectorSize + lolog::kHeaderSize, body.data(), body.size())) + return false; + uint32_t tag = (uint32_t)body[0] | ((uint32_t)body[1] << 8) | ((uint32_t)body[2] << 16) | ((uint32_t)body[3] << 24); + if (tag != kIndexBodyTag) + return false; + uint32_t cov = 0, nd = 0, ni = 0, nNames = 0, nDead = 0; + memcpy(&cov, body.data() + 4, 4); + memcpy(&nd, body.data() + 8, 4); + memcpy(&ni, body.data() + 12, 4); + memcpy(&liveBytes_, body.data() + 16, 8); + memcpy(&nNames, body.data() + 24, 4); + memcpy(&nDead, body.data() + 28, 4); + coveredSeq_ = cov; + nextDirId_ = nd; + nextInodeId_ = ni; + size_t o = 32; + nameIndex_.clear(); + deadDirIds_.clear(); + for (uint32_t i = 0; i < nNames && o + 13 < body.size(); i++) { + uint32_t dirId = 0, nameLen = 0, id = 0; + memcpy(&dirId, body.data() + o, 4); + o += 4; + memcpy(&nameLen, body.data() + o, 4); + o += 4; + if (o + nameLen + 1 + 1 + 4 > body.size()) + break; + std::string name((char *)(body.data() + o), nameLen); + o += nameLen + 1; + bool isDir = body[o++] != 0; + memcpy(&id, body.data() + o, 4); + o += 4; + upsertName(dirId, name.c_str(), NameVal{isDir, id}); + } + for (uint32_t i = 0; i < nDead && o + 4 <= body.size(); i++) { + uint32_t d = 0; + memcpy(&d, body.data() + o, 4); + o += 4; + deadDirIds_.push_back(d); + } + uint32_t nDirParent = 0; + if (o + 4 <= body.size()) { + memcpy(&nDirParent, body.data() + o, 4); + o += 4; + } + for (uint32_t i = 0; i < nDirParent && o + 8 <= body.size(); i++) { + uint32_t dirId = 0, parent = 0; + memcpy(&dirId, body.data() + o, 4); + memcpy(&parent, body.data() + o + 4, 4); + o += 8; + if (dirId >= dirParent_.size()) + dirParent_.resize(dirId + 1, lolog::kRootDirId); + dirParent_[dirId] = parent; + } + return true; +} + +bool LoLog::copyLiveGroupsFromSector(uint32_t sector) +{ + uint32_t magic, epoch, seq; + uint8_t kind = 0; + if (!readSectorHeader(sector, magic, epoch, seq, kind) || kind != lolog::kSegData) + return false; + if (epoch != epoch_) + return false; + + uint32_t off = lolog::kHeaderSize; + std::vector group; + while (off + 8 <= lolog::kSectorSize) { + uint8_t hdr[8]; + if (!readBytes(sector * lolog::kSectorSize + off, hdr, 8)) + break; + uint16_t plen = (uint16_t)hdr[0] | ((uint16_t)hdr[1] << 8); + if (plen == lolog::kEntryEnd) + break; + uint8_t type = hdr[2]; + uint8_t flags = hdr[3]; + uint32_t padded = (8 + plen + 3) & ~3u; + if (off + padded > lolog::kSectorSize) + break; + std::vector payload(plen); + if (plen && !readBytes(sector * lolog::kSectorSize + off + 4, payload.data(), plen)) + break; + group.push_back({type, std::move(payload)}); + off += padded; + if (flags & lolog::kFlagGroupEnd) { + beginGroup(); + for (auto &e : group) + pendingGroup_.push_back(std::move(e)); + group.clear(); + if (!endGroup()) + return false; + } + } + return true; +} + +void LoLog::cleanOneSector() +{ + if (freeSectors() <= lolog::kHeadroomSectors) + return; + uint32_t best = 0xFFFFFFFF; + uint32_t bestDead = 0; + uint32_t bestEc = UINT32_MAX; + for (uint32_t s = 0; s < sectorUsed_.size(); s++) { + if (!sectorUsed_[s] || s == activeSector_) + continue; + uint32_t magic, ep, seq; + uint8_t kind = 0; + if (!readSectorHeader(s, magic, ep, seq, kind) || kind != lolog::kSegData) + continue; + uint32_t live = liveInSector_[s]; + uint32_t dead = lolog::kSectorSize > live ? lolog::kSectorSize - live : 0; + uint32_t ec = dev_.eraseCount(s); + if (dead > bestDead || (dead == bestDead && ec < bestEc)) { + bestDead = dead; + bestEc = ec; + best = s; + } + } + if (best == 0xFFFFFFFF || bestDead < lolog::kSectorSize / 4) + return; + if (!copyLiveGroupsFromSector(best)) + return; + const uint32_t freed = liveInSector_[best]; + dev_.eraseSector(best); + sectorUsed_[best] = 0; + liveInSector_[best] = 0; + if (liveBytes_ >= freed) + liveBytes_ -= freed; +} + +void LoLog::flushIndexIfNeeded() +{ + memtableBytes_ = (uint32_t)(nameIndex_.size() * 48); + const uint32_t now = nowMs(); + if (memtableBytes_ >= lolog::kMemtableBytes || + (lastFlushMs_ && now - lastFlushMs_ > kIdleFlushMs)) + flushIndexRun(); +} + +void LoLog::maintain(uint32_t budgetMs) +{ + (void)budgetMs; + flushIndexIfNeeded(); + cleanOneSector(); +} + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/lolog/LoLog.h b/lofs/lolog/LoLog.h new file mode 100644 index 0000000..6c79b10 --- /dev/null +++ b/lofs/lolog/LoLog.h @@ -0,0 +1,156 @@ +#pragma once + +#include "lofs/lolog/LoLogConfig.h" +#include "lofs/volumes/LoFSRawDevice.h" +#include +#include +#include +#include + +namespace lolog { + +struct Loc { + uint16_t sector; + uint16_t off; +}; + +} // namespace lolog + +class LoLog { + public: + explicit LoLog(LoFSRawDevice &device); + + bool mount(); + bool format(); + void maintain(uint32_t budgetMs); + + uint64_t totalBytes() const; + uint64_t usedBytes() const; + void spaceHint(uint32_t *blockOut, uint32_t *slackOut) const; + + bool exists(const char *relPath); + bool isDirectory(const char *relPath); + bool statPath(const char *relPath, uint32_t *sizeOut, bool *isDirOut); + + bool mkdir(const char *relPath); + bool remove(const char *relPath); + bool rename(const char *oldPath, const char *newPath); + bool rmdir(const char *relPath, bool recursive); + + struct OpenHandle { + bool isDirIter = false; + uint32_t dirId = 0; + size_t dirIdx = 0; + uint32_t inodeId = 0; + bool write = false; + bool dirty = false; + uint32_t pos = 0; + std::vector writeBuf; + char nameBuf[64]; + }; + + OpenHandle *open(const char *relPath, bool write, bool create); + void closeHandle(OpenHandle *h); + size_t readHandle(OpenHandle *h, uint8_t *buf, size_t len); + size_t writeHandle(OpenHandle *h, const uint8_t *buf, size_t len); + bool seekHandle(OpenHandle *h, uint32_t pos); + uint32_t sizeHandle(OpenHandle *h) const; + bool isDirHandle(OpenHandle *h) const; + void flushHandle(OpenHandle *h); + const char *nameHandle(OpenHandle *h) const; + + OpenHandle *openDir(const char *relPath); + OpenHandle *openNextInDir(OpenHandle *dir); + + bool writeAtPath(const char *relPath, uint32_t offset, const uint8_t *data, size_t len); + + private: + LoFSRawDevice &dev_; + bool mounted_ = false; + uint32_t epoch_ = 0; + uint32_t nextSeq_ = 1; + uint32_t nextDirId_ = 1; + uint32_t nextInodeId_ = 1; + uint32_t coveredSeq_ = 0; + uint64_t liveBytes_ = 0; + uint32_t lastActivityMs_ = 0; + + uint16_t activeSector_ = 0xFFFF; + uint32_t activeOff_ = lolog::kHeaderSize; + + std::vector sectorUsed_; + std::vector liveInSector_; + std::vector eraseMirror_; + + struct FileMeta { + uint32_t dirId = 0; + std::string name; + bool isDir = false; + uint32_t inodeId = 0; + uint32_t dirIdSelf = 0; + uint32_t parentDirId = 0; + uint32_t size = 0; + uint32_t mtime = 0; + std::vector data; + lolog::Loc loc{}; + }; + + std::vector byInode_; + /** dirId -> parentDirId (root parent is self). */ + std::vector dirParent_; + /** (dirId << 32 | hash) -> inode or dir id stored in name map via string key */ + struct NameKey { + uint32_t dirId; + std::string name; + bool operator<(const NameKey &o) const; + }; + struct NameVal { + bool isDir; + uint32_t id; + }; + std::vector> nameIndex_; + + struct PendingEnt { + uint8_t type; + std::vector payload; + }; + std::vector pendingGroup_; + + std::vector deadDirIds_; + std::vector indexRunSeqs_; + uint32_t memtableBytes_ = 0; + uint32_t lastFlushMs_ = 0; + static constexpr uint32_t kIdleFlushMs = 30000; + + uint32_t crc32(const uint8_t *data, size_t len) const; + bool readBytes(uint32_t addr, void *buf, size_t len); + bool progBytes(uint32_t addr, const void *buf, size_t len); + bool readSectorHeader(uint32_t sector, uint32_t &magic, uint32_t &epoch, uint32_t &seq, uint8_t &kind); + bool writeSectorHeader(uint32_t sector, uint32_t seq, uint8_t kind); + bool allocDataSector(); + bool appendRaw(const uint8_t *payload, size_t payloadLen, uint8_t type, uint8_t flags); + void beginGroup(); + bool endGroup(); + void applyEntry(uint8_t type, const uint8_t *payload, size_t len); + void replaySector(uint32_t sector); + void rebuildIndexFromFlash(); + bool resolvePath(const char *relPath, uint32_t &dirIdOut, std::string &nameOut, bool createDirs); + bool lookupName(uint32_t dirId, const char *name, NameVal &out) const; + void upsertName(uint32_t dirId, const char *name, const NameVal &val); + void eraseName(uint32_t dirId, const char *name); + FileMeta *metaByInode(uint32_t inodeId); + const FileMeta *metaByInode(uint32_t inodeId) const; + bool commitFileMeta(FileMeta &meta); + bool dirEmpty(uint32_t dirId) const; + void tombDir(uint32_t dirId); + bool hasRoomFor(uint32_t bytes) const; + uint32_t freeSectors() const; + void cleanOneSector(); + bool allocIndexSector(uint32_t §orOut, uint32_t runSeq); + bool flushIndexRun(); + void mergeIndexRunsIfNeeded(); + bool loadNewestIndexRun(); + void flushIndexIfNeeded(); + bool copyLiveGroupsFromSector(uint32_t sector); + uint32_t nowMs() const; +}; diff --git a/lofs/lolog/LoLogConfig.h b/lofs/lolog/LoLogConfig.h new file mode 100644 index 0000000..27932a5 --- /dev/null +++ b/lofs/lolog/LoLogConfig.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +namespace lolog { + +static constexpr uint32_t kSectorSize = 4096; +static constexpr uint32_t kPageSize = 256; +static constexpr uint32_t kHeaderSize = 16; +static constexpr uint32_t kInlineMax = 2048; +static constexpr uint32_t kMemtableBytes = 4096; +static constexpr uint32_t kMaxRuns = 4; +static constexpr uint32_t kHeadroomSectors = 2; +static constexpr uint32_t kMagicUsed = 0x4C4F4721; // "LOG!" +static constexpr uint32_t kMagicFree = 0xFFFFFFFF; +static constexpr uint16_t kEntryEnd = 0xFFFF; + +static constexpr uint8_t kSegData = 0; +static constexpr uint8_t kSegIndex = 1; + +static constexpr uint8_t kEntDir = 1; +static constexpr uint8_t kEntFile = 2; +static constexpr uint8_t kEntExtent = 3; +static constexpr uint8_t kEntTomb = 4; +static constexpr uint8_t kEntCommit = 5; + +static constexpr uint8_t kFlagGroupEnd = 0x01; + +static constexpr uint32_t kRootDirId = 0; + +} // namespace lolog diff --git a/lofs/lolog/LoLogRamDevice.cpp b/lofs/lolog/LoLogRamDevice.cpp new file mode 100644 index 0000000..c736f2c --- /dev/null +++ b/lofs/lolog/LoLogRamDevice.cpp @@ -0,0 +1,120 @@ +#include "LoLogRamDevice.h" +#include "lofs/lolog/LoLogConfig.h" +#include + +LoLogRamDevice::LoLogRamDevice(uint32_t sectorCount) : sectors_(sectorCount) +{ + data_.assign((size_t)sectors_ * lolog::kSectorSize, 0xFF); +} + +bool LoLogRamDevice::begin() +{ + return sectors_ > 0 && sectors_ <= kMaxSectors; +} + +uint32_t LoLogRamDevice::sectorSize() const +{ + return lolog::kSectorSize; +} + +uint32_t LoLogRamDevice::pageSize() const +{ + return lolog::kPageSize; +} + +uint32_t LoLogRamDevice::sectorCount() const +{ + return sectors_; +} + +bool LoLogRamDevice::partialPageProgram() const +{ + return true; +} + +bool LoLogRamDevice::read(uint32_t addr, void *buf, size_t len) +{ + if (!buf || addr + len > data_.size()) + return false; + memcpy(buf, data_.data() + addr, len); + return true; +} + +static bool canProgramByte(uint8_t oldB, uint8_t newB) +{ + return (oldB & newB) == newB; +} + +bool LoLogRamDevice::prog(uint32_t addr, const void *buf, size_t len) +{ + if (!buf || addr + len > data_.size() || (len & 3) != 0 || (addr & 3) != 0) + return false; + progCallCount_++; + if (faultOnProgCall_ > 0 && progCallCount_ >= faultOnProgCall_) + return false; + const uint8_t *src = (const uint8_t *)buf; + for (size_t i = 0; i < len; i++) { + if (faultAfter_ > 0) { + if (progSinceFault_ >= faultAfter_) + return false; + progSinceFault_++; + } + uint8_t &cell = data_[addr + i]; + if (!canProgramByte(cell, src[i])) + return false; + cell = src[i]; + progBytes_++; + } + return true; +} + +bool LoLogRamDevice::eraseSector(uint32_t sectorIndex) +{ + if (sectorIndex >= sectors_) + return false; + memset(data_.data() + (size_t)sectorIndex * lolog::kSectorSize, 0xFF, lolog::kSectorSize); + if (sectorIndex < kMaxSectors) + eraseCounts_[sectorIndex]++; + return true; +} + +bool LoLogRamDevice::sync() +{ + return true; +} + +void LoLogRamDevice::reset() +{ + std::fill(data_.begin(), data_.end(), (uint8_t)0xFF); + memset(eraseCounts_, 0, sizeof(eraseCounts_)); + faultAfter_ = 0; + progSinceFault_ = 0; + faultOnProgCall_ = 0; + progCallCount_ = 0; + progBytes_ = 0; +} + +void LoLogRamDevice::setFaultAfterProgBytes(uint32_t n) +{ + faultAfter_ = n; + progSinceFault_ = 0; +} + +void LoLogRamDevice::setFaultOnProgCall(uint32_t n) +{ + faultOnProgCall_ = n; + progCallCount_ = 0; +} + +bool LoLogRamDevice::poke(uint32_t addr, uint8_t value) +{ + if (addr >= data_.size()) + return false; + uint8_t &cell = data_[addr]; + if (!canProgramByte(cell, value)) + return false; + cell = value; + return true; +} + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/lolog/LoLogRamDevice.h b/lofs/lolog/LoLogRamDevice.h new file mode 100644 index 0000000..3a6b5df --- /dev/null +++ b/lofs/lolog/LoLogRamDevice.h @@ -0,0 +1,44 @@ +#pragma once + +#include "lofs/volumes/LoFSRawDevice.h" +#include + +/** In-memory NOR model for host tests (1->0 program rules, fault injection). */ +class LoLogRamDevice : public LoFSRawDevice { + public: + explicit LoLogRamDevice(uint32_t sectorCount = 64); + + bool begin() override; + uint32_t sectorSize() const override; + uint32_t pageSize() const override; + uint32_t sectorCount() const override; + bool partialPageProgram() const override; + + bool read(uint32_t addr, void *buf, size_t len) override; + bool prog(uint32_t addr, const void *buf, size_t len) override; + bool eraseSector(uint32_t sectorIndex) override; + bool sync() override; + + void reset(); + /** Fail the next program after this many bytes have been programmed (0 = off). */ + void setFaultAfterProgBytes(uint32_t n); + uint32_t faultAfterProgBytes() const { return faultAfter_; } + /** Fail when starting prog call number n (1-based). 0 = off. */ + void setFaultOnProgCall(uint32_t n); + uint32_t progCallCount() const { return progCallCount_; } + void clearProgCallCount() { progCallCount_ = 0; } + uint64_t progBytes() const { return progBytes_; } + void clearProgBytes() { progBytes_ = 0; } + /** Corrupt flash for torn-entry tests (must be 1->0 valid). */ + bool poke(uint32_t addr, uint8_t value); + uint32_t sectorEraseCount(uint32_t sectorIndex) const { return eraseCount(sectorIndex); } + + private: + std::vector data_; + uint32_t sectors_; + uint32_t faultAfter_ = 0; + uint32_t progSinceFault_ = 0; + uint32_t faultOnProgCall_ = 0; + uint32_t progCallCount_ = 0; + uint64_t progBytes_ = 0; +}; diff --git a/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp b/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp index 889580c..87fd0a2 100644 --- a/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp +++ b/lofs/platforms/meshcore/LoFSMountsMeshcore.cpp @@ -3,7 +3,7 @@ #include "LoFSMountsMeshcore.h" #include "lofs/volumes/LoFSVolume.h" #include "lofs/volumes/LoFSLittleFsVolume.h" -#include "lofs/volumes/LoFSNrfFlashBlockDevice.h" +#include "lofs/volumes/LoFSLoLogVolume.h" #include "lofs/volumes/LoFSQspiNorBlockDevice.h" #include "core/LoBBSConfig.h" #if LOBBS_ARCH_ESP32 @@ -18,16 +18,10 @@ #if defined(LOFS_NRF52) #include -extern "C" uint8_t __flash3_start[] __attribute__((weak)); -extern "C" uint8_t __flash3_end[] __attribute__((weak)); #if defined(EXTRAFS) && !defined(QSPIFLASH) #include extern CustomLFS ExtraFS; #endif -#if defined(QSPIFLASH) -#include -extern CustomLFS_QSPIFlash QSPIFlash; -#endif #endif #if LOBBS_ARCH_ESP32 @@ -37,16 +31,13 @@ extern CustomLFS_QSPIFlash QSPIFlash; namespace { #if defined(LOFS_NRF52) -// Reserve window: same tuning as Meshtastic (128 KiB cap, lock host FS during raw flash, page erase). -LoFSNrfFlashBlockDevice gReserveDevice(__flash3_start, __flash3_end, 128, true, true, LOBBS_NRF52_FLASH_PAGE_SIZE); -LoFSLittleFsVolume gReserve(gReserveDevice); LoFSLittleFsVolume gInternal; -LoFSLittleFsVolume gSpare; -#if defined(QSPIFLASH) -LoFSLittleFsVolume gQspi; -#else -LoFSQspiNorBlockDevice gQspiDevice; -LoFSLittleFsVolume gQspi(gQspiDevice); +#if defined(EXTRAFS) && !defined(QSPIFLASH) +LoFSLittleFsVolume gExtra; +#endif +#if LOFS_BOARD_HAS_QSPI +LoFSQspiNorBlockDevice gLofsDevice; +LoFSLoLogVolume gLofs(gLofsDevice); #endif #endif @@ -85,17 +76,12 @@ void lofsMeshcoreSetHostFilesystem(void *filesystem) void lofsPlatformMounts(LoFSMountTable &table) { #if defined(LOFS_NRF52) - if (__flash3_start && __flash3_end && (uintptr_t)__flash3_end > (uintptr_t)__flash3_start) - table.add({"reserve", &gReserve, false, true}); -#if defined(QSPIFLASH) - gQspi.bind(QSPIFlash); - table.add({"qspi", &gQspi, false, true}); -#elif LOFS_BOARD_HAS_QSPI - table.add({"qspi", &gQspi, false, true}); +#if LOFS_BOARD_HAS_QSPI + table.add({"lofs", &gLofs, false, true}); #endif #if defined(EXTRAFS) && !defined(QSPIFLASH) - gSpare.bind(ExtraFS); - table.add({"spare", &gSpare, true, true}); + gExtra.bind(ExtraFS); + table.add({"extra", &gExtra, true, true}); #endif table.add({"internal", &gInternal, true, false}); #elif LOBBS_ARCH_ESP32 diff --git a/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp b/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp index b178dd6..ebf133a 100644 --- a/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp +++ b/lofs/platforms/meshtastic/LoFSMountsMeshtastic.cpp @@ -6,8 +6,9 @@ #include "FSCommon.h" #include "SPILock.h" #include "lofs/volumes/LoFSLittleFsVolume.h" +#include "lofs/volumes/LoFSLoLogVolume.h" #include "lofs/volumes/LoFSArduinoFsVolume.h" -#include "lofs/volumes/LoFSNrfFlashBlockDevice.h" +#include "lofs/volumes/LoFSNrfFlashRawDevice.h" #include "lofs/volumes/LoFSQspiNorBlockDevice.h" #include "lofs/volumes/LoFSSdVolume.h" #include @@ -15,8 +16,6 @@ #if defined(LOFS_NRF52) extern "C" uint8_t __flash2_start[] __attribute__((weak)); extern "C" uint8_t __flash2_end[] __attribute__((weak)); -extern "C" uint8_t __flash3_start[] __attribute__((weak)); -extern "C" uint8_t __flash3_end[] __attribute__((weak)); #include "flash/flash_nrf5x.h" #endif @@ -27,15 +26,14 @@ LoFSSdVolume gSd; #endif #if LOFS_BOARD_HAS_QSPI -LoFSQspiNorBlockDevice gQspiDevice; -LoFSLittleFsVolume gQspi(gQspiDevice); +LoFSQspiNorBlockDevice gLofsDevice; +LoFSLoLogVolume gLofs(gLofsDevice); +#elif defined(LOFS_NRF52) +LoFSNrfFlashRawDevice gLofsWindowDevice(__flash2_start, __flash2_end); +LoFSLoLogVolume gLofs(gLofsWindowDevice); #endif #if defined(LOFS_NRF52) -LoFSNrfFlashBlockDevice gReserveDevice(__flash3_start, __flash3_end, 128, true, true, LOBBS_NRF52_FLASH_PAGE_SIZE); -LoFSNrfFlashBlockDevice gSpareDevice(__flash2_start, __flash2_end, 128, true, true, LOBBS_NRF52_FLASH_PAGE_SIZE); -LoFSLittleFsVolume gReserve(gReserveDevice); -LoFSLittleFsVolume gSpare(gSpareDevice); LoFSLittleFsVolume gInternal(FSCom); #elif LOBBS_ARCH_ESP32 || LOBBS_ARCH_RP2040 LoFSArduinoFsVolume gInternal(FSCom); @@ -48,6 +46,13 @@ void addSpec(LoFSMountTable &table, const char *name, LoFSVolume *vol, bool shar table.add({name, vol, shared, formattable}); } +#if defined(LOFS_NRF52) +static bool flash2Present() +{ + return (uintptr_t)__flash2_end > (uintptr_t)__flash2_start; +} +#endif + } // namespace void lofsPlatformMounts(LoFSMountTable &table) @@ -59,12 +64,10 @@ void lofsPlatformMounts(LoFSMountTable &table) #endif #if LOFS_BOARD_HAS_QSPI - addSpec(table, "qspi", &gQspi, false, true); -#endif - -#if defined(LOFS_NRF52) - addSpec(table, "reserve", &gReserve, false, true); - addSpec(table, "spare", &gSpare, false, true); + addSpec(table, "lofs", &gLofs, false, true); +#elif defined(LOFS_NRF52) + if (flash2Present()) + addSpec(table, "lofs", &gLofs, false, true); #endif addSpec(table, "internal", &gInternal, true, true); diff --git a/lofs/volumes/LoFSLoLogVolume.cpp b/lofs/volumes/LoFSLoLogVolume.cpp new file mode 100644 index 0000000..e98da6d --- /dev/null +++ b/lofs/volumes/LoFSLoLogVolume.cpp @@ -0,0 +1,196 @@ +#include "LoFSLoLogVolume.h" +#include +#include + +namespace { + +struct LoLogFileHandle { + LoLog *log; + LoLog::OpenHandle *handle; + bool ownsHandle; + explicit LoLogFileHandle(LoLog *l, LoLog::OpenHandle *h, bool owns) : log(l), handle(h), ownsHandle(owns) {} + ~LoLogFileHandle() + { + if (ownsHandle && log && handle) + log->closeHandle(handle); + } +}; + +void destroyLoLog(void *handle) +{ + delete static_cast(handle); +} + +bool validLoLog(void *handle) +{ + auto *h = static_cast(handle); + return h && h->handle; +} + +size_t readLoLog(void *handle, uint8_t *buf, size_t len) +{ + auto *h = static_cast(handle); + return h && h->log && h->handle ? h->log->readHandle(h->handle, buf, len) : 0; +} + +size_t writeLoLog(void *handle, const uint8_t *buf, size_t len) +{ + auto *h = static_cast(handle); + return h && h->log && h->handle ? h->log->writeHandle(h->handle, buf, len) : 0; +} + +bool seekLoLog(void *handle, uint32_t pos) +{ + auto *h = static_cast(handle); + return h && h->log && h->handle && h->log->seekHandle(h->handle, pos); +} + +uint32_t sizeLoLog(void *handle) +{ + auto *h = static_cast(handle); + return h && h->log && h->handle ? h->log->sizeHandle(h->handle) : 0; +} + +bool isDirLoLog(void *handle) +{ + auto *h = static_cast(handle); + return h && h->log && h->handle && h->log->isDirHandle(h->handle); +} + +const char *nameLoLog(void *handle) +{ + auto *h = static_cast(handle); + return h && h->log && h->handle ? h->log->nameHandle(h->handle) : ""; +} + +void closeLoLog(void *handle) +{ + auto *h = static_cast(handle); + if (h && h->ownsHandle && h->log && h->handle) { + h->log->closeHandle(h->handle); + h->handle = nullptr; + } +} + +void flushLoLog(void *handle) +{ + auto *h = static_cast(handle); + if (h && h->log && h->handle) + h->log->flushHandle(h->handle); +} + +void *openNextLoLog(void *dirHandle) +{ + auto *dir = static_cast(dirHandle); + if (!dir || !dir->log || !dir->handle || !dir->log->isDirHandle(dir->handle)) + return nullptr; + LoLog::OpenHandle *next = dir->log->openNextInDir(dir->handle); + if (!next) + return nullptr; + return new LoLogFileHandle(dir->log, next, true); +} + +} // namespace + +const LoFileOps kLoFSLoLogFileOps = { + destroyLoLog, validLoLog, readLoLog, writeLoLog, seekLoLog, sizeLoLog, + isDirLoLog, nameLoLog, closeLoLog, flushLoLog, openNextLoLog, +}; + +LoFSLoLogVolume::LoFSLoLogVolume(LoFSRawDevice &device) : device_(device), log_(device) {} + +uint8_t LoFSLoLogVolume::modeFromString(const char *mode) +{ + if (!mode || strcmp(mode, "r") == 0) + return 0; + return 1; +} + +LoFile LoFSLoLogVolume::wrapHandle(LoLog::OpenHandle *h, bool dirIter) +{ + (void)dirIter; + if (!h) + return LoFile(); + return LoFileAdopt(&kLoFSLoLogFileOps, new LoLogFileHandle(&log_, h, true)); +} + +bool LoFSLoLogVolume::begin() +{ + ready_ = log_.mount(); + return ready_; +} + +void LoFSLoLogVolume::maintain(uint32_t budgetMs) +{ + if (ready_) + log_.maintain(budgetMs); +} + +LoFile LoFSLoLogVolume::open(const char *relPath, const char *mode) +{ + if (!ready_) + return LoFile(); + const char *rel = normalizeRel(relPath); + const bool write = modeFromString(mode) != 0; + if (strcmp(mode, "r") == 0 && log_.isDirectory(rel)) { + LoLog::OpenHandle *d = log_.openDir(rel); + return wrapHandle(d, true); + } + LoLog::OpenHandle *h = log_.open(rel, write, write); + return wrapHandle(h, false); +} + +LoFile LoFSLoLogVolume::open(const char *relPath, uint8_t mode) +{ + return open(relPath, mode ? "w" : "r"); +} + +bool LoFSLoLogVolume::exists(const char *relPath) +{ + return ready_ && log_.exists(normalizeRel(relPath)); +} + +bool LoFSLoLogVolume::mkdir(const char *relPath) +{ + return ready_ && log_.mkdir(normalizeRel(relPath)); +} + +bool LoFSLoLogVolume::remove(const char *relPath) +{ + return ready_ && log_.remove(normalizeRel(relPath)); +} + +bool LoFSLoLogVolume::rename(const char *oldPath, const char *newPath) +{ + return ready_ && log_.rename(normalizeRel(oldPath), normalizeRel(newPath)); +} + +bool LoFSLoLogVolume::rmdir(const char *relPath) +{ + return ready_ && log_.rmdir(normalizeRel(relPath), true); +} + +uint64_t LoFSLoLogVolume::totalBytes() +{ + return ready_ ? log_.totalBytes() : 0; +} + +uint64_t LoFSLoLogVolume::usedBytes() +{ + return ready_ ? log_.usedBytes() : 0; +} + +bool LoFSLoLogVolume::format() +{ + if (!device_.begin()) + return false; + ready_ = log_.format(); + return ready_; +} + +void LoFSLoLogVolume::spaceHint(uint32_t *blockOut, uint32_t *slackOut) +{ + log_.spaceHint(blockOut, slackOut); +} + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSLoLogVolume.h b/lofs/volumes/LoFSLoLogVolume.h new file mode 100644 index 0000000..32e6019 --- /dev/null +++ b/lofs/volumes/LoFSLoLogVolume.h @@ -0,0 +1,37 @@ +#pragma once + +#include "LoFSRawDevice.h" +#include "LoFSVolume.h" +#include "lofs/lolog/LoLog.h" + +class LoFSLoLogVolume : public LoFSVolume { + public: + explicit LoFSLoLogVolume(LoFSRawDevice &device); + + bool begin() override; + void maintain(uint32_t budgetMs) override; + + LoFile open(const char *relPath, const char *mode) override; + LoFile open(const char *relPath, uint8_t mode) override; + + bool exists(const char *relPath) override; + bool mkdir(const char *relPath) override; + bool remove(const char *relPath) override; + bool rename(const char *oldPath, const char *newPath) override; + bool rmdir(const char *relPath) override; + + uint64_t totalBytes() override; + uint64_t usedBytes() override; + bool format() override; + void spaceHint(uint32_t *blockOut, uint32_t *slackOut) override; + + private: + LoFSRawDevice &device_; + LoLog log_; + bool ready_ = false; + + static uint8_t modeFromString(const char *mode); + LoFile wrapHandle(LoLog::OpenHandle *h, bool dirIter); +}; + +extern const LoFileOps kLoFSLoLogFileOps; diff --git a/lofs/volumes/LoFSNrfFlashRawDevice.cpp b/lofs/volumes/LoFSNrfFlashRawDevice.cpp new file mode 100644 index 0000000..e8b9ba5 --- /dev/null +++ b/lofs/volumes/LoFSNrfFlashRawDevice.cpp @@ -0,0 +1,166 @@ +#include "LoFSNrfFlashRawDevice.h" +#include "lofs/lolog/LoLogConfig.h" +#include + +#if defined(LOFS_NRF52) + +#if defined(LOBBS_PLATFORM_MESHTASTIC) +#include "flash/flash_nrf5x.h" +#include +#elif defined(LOBBS_PLATFORM_MESHCORE) +extern "C" int flash_nrf5x_read(void *buffer, uint32_t address, uint32_t len); +extern "C" int flash_nrf5x_write(uint32_t address, const void *data, uint32_t len); +extern "C" void flash_nrf5x_flush(void); +extern "C" void flash_nrf5x_erase(uint32_t address); +#endif + +LoFSNrfFlashRawDevice::LoFSNrfFlashRawDevice(void *startSym, void *endSym, bool lockWithInternalFs, + uint32_t flashPageSize) + : startSym_(startSym), endSym_(endSym), lockWithInternalFs_(lockWithInternalFs), flashPageSize_(flashPageSize) +{ +} + +void LoFSNrfFlashRawDevice::lockFs() +{ +#if defined(LOBBS_PLATFORM_MESHTASTIC) + if (lockWithInternalFs_) + InternalFS._lockFS(); +#else + (void)lockWithInternalFs_; +#endif +} + +void LoFSNrfFlashRawDevice::unlockFs() +{ +#if defined(LOBBS_PLATFORM_MESHTASTIC) + if (lockWithInternalFs_) + InternalFS._unlockFS(); +#endif +} + +bool LoFSNrfFlashRawDevice::begin() +{ + const uint32_t start = (uint32_t)(uintptr_t)startSym_; + const uint32_t end = (uint32_t)(uintptr_t)endSym_; + if (!start || end <= start) + return false; + base_ = start; + size_ = end - start; + if (size_ < lolog::kSectorSize) + return false; + return true; +} + +uint32_t LoFSNrfFlashRawDevice::sectorSize() const +{ + return lolog::kSectorSize; +} + +uint32_t LoFSNrfFlashRawDevice::pageSize() const +{ + return lolog::kPageSize; +} + +uint32_t LoFSNrfFlashRawDevice::sectorCount() const +{ + return size_ / lolog::kSectorSize; +} + +bool LoFSNrfFlashRawDevice::partialPageProgram() const +{ + return true; +} + +bool LoFSNrfFlashRawDevice::read(uint32_t addr, void *buf, size_t len) +{ + if (!buf || addr + len > size_) + return false; + lockFs(); + int n = flash_nrf5x_read(buf, base_ + addr, (uint32_t)len); + unlockFs(); + return n > 0; +} + +bool LoFSNrfFlashRawDevice::prog(uint32_t addr, const void *buf, size_t len) +{ + if (!buf || addr + len > size_ || (len & 3) != 0 || (addr & 3) != 0) + return false; + lockFs(); + int n = flash_nrf5x_write(base_ + addr, buf, (uint32_t)len); + unlockFs(); + return n > 0; +} + +bool LoFSNrfFlashRawDevice::eraseSector(uint32_t sectorIndex) +{ + if (sectorIndex >= sectorCount()) + return false; + const uint32_t addr = base_ + sectorIndex * lolog::kSectorSize; + lockFs(); + flash_nrf5x_erase(addr); + unlockFs(); + if (sectorIndex < kMaxSectors) + eraseCounts_[sectorIndex]++; + return true; +} + +bool LoFSNrfFlashRawDevice::sync() +{ + lockFs(); + flash_nrf5x_flush(); + unlockFs(); + return true; +} + +#else + +LoFSNrfFlashRawDevice::LoFSNrfFlashRawDevice(void *, void *, bool, uint32_t) {} + +bool LoFSNrfFlashRawDevice::begin() +{ + return false; +} + +uint32_t LoFSNrfFlashRawDevice::sectorSize() const +{ + return 4096; +} + +uint32_t LoFSNrfFlashRawDevice::pageSize() const +{ + return 256; +} + +uint32_t LoFSNrfFlashRawDevice::sectorCount() const +{ + return 0; +} + +bool LoFSNrfFlashRawDevice::partialPageProgram() const +{ + return true; +} + +bool LoFSNrfFlashRawDevice::read(uint32_t, void *, size_t) +{ + return false; +} + +bool LoFSNrfFlashRawDevice::prog(uint32_t, const void *, size_t) +{ + return false; +} + +bool LoFSNrfFlashRawDevice::eraseSector(uint32_t) +{ + return false; +} + +bool LoFSNrfFlashRawDevice::sync() +{ + return true; +} + +#endif + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSNrfFlashRawDevice.h b/lofs/volumes/LoFSNrfFlashRawDevice.h new file mode 100644 index 0000000..38fd564 --- /dev/null +++ b/lofs/volumes/LoFSNrfFlashRawDevice.h @@ -0,0 +1,32 @@ +#pragma once + +#include "LoFSRawDevice.h" + +/** nRF52840 internal flash window for LoLog (not LittleFS). */ +class LoFSNrfFlashRawDevice : public LoFSRawDevice { + public: + LoFSNrfFlashRawDevice(void *startSym, void *endSym, bool lockWithInternalFs = true, + uint32_t flashPageSize = 4096); + + bool begin() override; + uint32_t sectorSize() const override; + uint32_t pageSize() const override; + uint32_t sectorCount() const override; + bool partialPageProgram() const override; + + bool read(uint32_t addr, void *buf, size_t len) override; + bool prog(uint32_t addr, const void *buf, size_t len) override; + bool eraseSector(uint32_t sectorIndex) override; + bool sync() override; + + private: + void *startSym_; + void *endSym_; + bool lockWithInternalFs_; + uint32_t flashPageSize_; + uint32_t base_ = 0; + uint32_t size_ = 0; + + void lockFs(); + void unlockFs(); +}; diff --git a/lofs/volumes/LoFSQspiNorBlockDevice.cpp b/lofs/volumes/LoFSQspiNorBlockDevice.cpp index 1db5abb..716cce6 100644 --- a/lofs/volumes/LoFSQspiNorBlockDevice.cpp +++ b/lofs/volumes/LoFSQspiNorBlockDevice.cpp @@ -1,19 +1,17 @@ #include "LoFSQspiNorBlockDevice.h" #include +#include #if LOFS_BOARD_HAS_QSPI #include "variant.h" #include "nrf.h" -#include #include extern const uint32_t g_ADigitalPinMap[]; -#include - -static constexpr uint32_t LOFS_QSPI_BLOCK = 4096; +static constexpr uint32_t LOFS_QSPI_SECTOR = 4096; static constexpr uint32_t LOFS_QSPI_PAGE = 256; -static constexpr uint32_t LOFS_QSPI_BLOCK_COUNT = 512; +static constexpr uint32_t LOFS_QSPI_SECTOR_COUNT = 512; static uint8_t lofsQspiScratch[LOFS_QSPI_PAGE] __attribute__((aligned(4))); static bool lofsQspiHwReady = false; @@ -97,98 +95,159 @@ static bool lofsQspiInitHw() return true; } -static int lofsQspiRead(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) +static bool lofsQspiReadAddr(uint32_t addr, void *buffer, size_t size) { - (void)c; - uint32_t addr = block * LOFS_QSPI_BLOCK + off; if (((uintptr_t)buffer & 3) == 0 && (size & 3) == 0) { nrfx_err_t err = nrfx_qspi_read(buffer, size, addr); - return err == NRFX_SUCCESS ? 0 : -1; + return err == NRFX_SUCCESS; } uint8_t *dst = (uint8_t *)buffer; while (size > 0) { - uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : size; + uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : (uint32_t)size; uint32_t qchunk = (chunk + 3) & ~3u; nrfx_err_t err = nrfx_qspi_read(lofsQspiScratch, qchunk, addr); if (err != NRFX_SUCCESS) - return -1; + return false; memcpy(dst, lofsQspiScratch, chunk); dst += chunk; addr += chunk; size -= chunk; } - return 0; + return true; } -static int lofsQspiProg(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, - lfs_size_t size) +static bool lofsQspiProgAddr(uint32_t addr, const void *buffer, size_t size) { - (void)c; - uint32_t addr = block * LOFS_QSPI_BLOCK + off; if (((uintptr_t)buffer & 3) == 0 && (size & 3) == 0) { nrfx_err_t err = nrfx_qspi_write(buffer, size, addr); lofsQspiWaitReady(); - return err == NRFX_SUCCESS ? 0 : -1; + return err == NRFX_SUCCESS; } const uint8_t *src = (const uint8_t *)buffer; while (size > 0) { - uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : size; + uint32_t chunk = size > LOFS_QSPI_PAGE ? LOFS_QSPI_PAGE : (uint32_t)size; uint32_t qchunk = (chunk + 3) & ~3u; memcpy(lofsQspiScratch, src, chunk); for (uint32_t i = chunk; i < qchunk; i++) lofsQspiScratch[i] = 0xFF; nrfx_err_t err = nrfx_qspi_write(lofsQspiScratch, qchunk, addr); if (err != NRFX_SUCCESS) - return -1; + return false; src += chunk; addr += chunk; size -= chunk; } lofsQspiWaitReady(); - return 0; + return true; } -static int lofsQspiErase(const struct lfs_config *c, lfs_block_t block) +bool LoFSQspiNorBlockDevice::begin() { - (void)c; - uint32_t addr = block * LOFS_QSPI_BLOCK; + return lofsQspiInitHw(); +} + +uint32_t LoFSQspiNorBlockDevice::sectorSize() const +{ + return LOFS_QSPI_SECTOR; +} + +uint32_t LoFSQspiNorBlockDevice::pageSize() const +{ + return LOFS_QSPI_PAGE; +} + +uint32_t LoFSQspiNorBlockDevice::sectorCount() const +{ + return LOFS_QSPI_SECTOR_COUNT; +} + +bool LoFSQspiNorBlockDevice::partialPageProgram() const +{ + return true; +} + +bool LoFSQspiNorBlockDevice::read(uint32_t addr, void *buf, size_t len) +{ + return lofsQspiReadAddr(addr, buf, len); +} + +bool LoFSQspiNorBlockDevice::prog(uint32_t addr, const void *buf, size_t len) +{ + return lofsQspiProgAddr(addr, buf, len); +} + +bool LoFSQspiNorBlockDevice::eraseSector(uint32_t sectorIndex) +{ + if (sectorIndex >= LOFS_QSPI_SECTOR_COUNT) + return false; + uint32_t addr = sectorIndex * LOFS_QSPI_SECTOR; nrfx_err_t err = nrfx_qspi_erase(NRF_QSPI_ERASE_LEN_4KB, addr); lofsQspiWaitReady(); - return err == NRFX_SUCCESS ? 0 : -1; + if (err == NRFX_SUCCESS && sectorIndex < kMaxSectors) + eraseCounts_[sectorIndex]++; + return err == NRFX_SUCCESS; } -static int lofsQspiSync(const struct lfs_config *c) +bool LoFSQspiNorBlockDevice::sync() { - (void)c; lofsQspiWaitReady(); - return 0; -} - -bool LoFSQspiNorBlockDevice::fill(lfs_config &cfg) -{ - if (!lofsQspiInitHw()) - return false; - cfg.context = NULL; - cfg.read = lofsQspiRead; - cfg.prog = lofsQspiProg; - cfg.erase = lofsQspiErase; - cfg.sync = lofsQspiSync; - cfg.read_size = LOFS_QSPI_PAGE; - cfg.prog_size = LOFS_QSPI_PAGE; - cfg.block_size = LOFS_QSPI_BLOCK; - cfg.block_count = LOFS_QSPI_BLOCK_COUNT; - cfg.lookahead = 512; return true; } #else -bool LoFSQspiNorBlockDevice::fill(lfs_config &cfg) +bool LoFSQspiNorBlockDevice::begin() { - (void)cfg; return false; } +uint32_t LoFSQspiNorBlockDevice::sectorSize() const +{ + return 4096; +} + +uint32_t LoFSQspiNorBlockDevice::pageSize() const +{ + return 256; +} + +uint32_t LoFSQspiNorBlockDevice::sectorCount() const +{ + return 0; +} + +bool LoFSQspiNorBlockDevice::partialPageProgram() const +{ + return true; +} + +bool LoFSQspiNorBlockDevice::read(uint32_t addr, void *buf, size_t len) +{ + (void)addr; + (void)buf; + (void)len; + return false; +} + +bool LoFSQspiNorBlockDevice::prog(uint32_t addr, const void *buf, size_t len) +{ + (void)addr; + (void)buf; + (void)len; + return false; +} + +bool LoFSQspiNorBlockDevice::eraseSector(uint32_t sectorIndex) +{ + (void)sectorIndex; + return false; +} + +bool LoFSQspiNorBlockDevice::sync() +{ + return true; +} + #endif #include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSQspiNorBlockDevice.h b/lofs/volumes/LoFSQspiNorBlockDevice.h index b703c57..4030bb8 100644 --- a/lofs/volumes/LoFSQspiNorBlockDevice.h +++ b/lofs/volumes/LoFSQspiNorBlockDevice.h @@ -1,7 +1,7 @@ #pragma once #include "core/LoBBSConfig.h" -#include "LoFSBlockDevice.h" +#include "LoFSRawDevice.h" #if !defined(LOFS_BOARD_HAS_QSPI) #if ((defined(LOFS_ENABLE_EXTRA_QSPI) && LOFS_ENABLE_EXTRA_QSPI) || defined(QSPIFLASH)) && \ @@ -12,7 +12,16 @@ #endif #endif -class LoFSQspiNorBlockDevice : public LoFSBlockDevice { +class LoFSQspiNorBlockDevice : public LoFSRawDevice { public: - bool fill(lfs_config &cfg) override; + bool begin() override; + uint32_t sectorSize() const override; + uint32_t pageSize() const override; + uint32_t sectorCount() const override; + bool partialPageProgram() const override; + + bool read(uint32_t addr, void *buf, size_t len) override; + bool prog(uint32_t addr, const void *buf, size_t len) override; + bool eraseSector(uint32_t sectorIndex) override; + bool sync() override; }; diff --git a/lofs/volumes/LoFSRawDevice.cpp b/lofs/volumes/LoFSRawDevice.cpp new file mode 100644 index 0000000..6fd9cf5 --- /dev/null +++ b/lofs/volumes/LoFSRawDevice.cpp @@ -0,0 +1,8 @@ +#include "LoFSRawDevice.h" + +uint32_t LoFSRawDevice::eraseCount(uint32_t sectorIndex) const +{ + return sectorIndex < kMaxSectors ? eraseCounts_[sectorIndex] : 0; +} + +#include "core/LoBBSStackGuard.h" diff --git a/lofs/volumes/LoFSRawDevice.h b/lofs/volumes/LoFSRawDevice.h new file mode 100644 index 0000000..d543215 --- /dev/null +++ b/lofs/volumes/LoFSRawDevice.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +/** Raw NOR or RAM backing for LoLog (not LittleFS). */ +class LoFSRawDevice { + public: + virtual ~LoFSRawDevice() = default; + + virtual bool begin() = 0; + virtual uint32_t sectorSize() const = 0; + virtual uint32_t pageSize() const = 0; + virtual uint32_t sectorCount() const = 0; + /** Multiple programs into the same erased page before erase. */ + virtual bool partialPageProgram() const = 0; + + virtual bool read(uint32_t addr, void *buf, size_t len) = 0; + /** len 4-byte aligned; addr page-aligned offset within device. */ + virtual bool prog(uint32_t addr, const void *buf, size_t len) = 0; + virtual bool eraseSector(uint32_t sectorIndex) = 0; + virtual bool sync() = 0; + + uint32_t eraseCount(uint32_t sectorIndex) const; + + protected: + static constexpr uint32_t kMaxSectors = 2048; + uint32_t eraseCounts_[kMaxSectors] = {}; +}; diff --git a/lofs/volumes/LoFSVolume.h b/lofs/volumes/LoFSVolume.h index df4cffd..70c7df6 100644 --- a/lofs/volumes/LoFSVolume.h +++ b/lofs/volumes/LoFSVolume.h @@ -4,8 +4,8 @@ #include #include -static constexpr const char *LOFS_MOUNT_ORDER[] = {"sd", "qspi", "reserve", "spare", "internal"}; -static constexpr int LOFS_MOUNT_ORDER_LEN = 5; +static constexpr const char *LOFS_MOUNT_ORDER[] = {"sd", "lofs", "extra", "internal"}; +static constexpr int LOFS_MOUNT_ORDER_LEN = 4; static constexpr int LOFS_MAX_MOUNTS = 6; class LoFSVolume { @@ -29,6 +29,9 @@ class LoFSVolume { virtual bool format() = 0; virtual void spaceHint(uint32_t *blockOut, uint32_t *slackOut) = 0; + /** Background flush, merge, and cleaning (LoLog volumes). */ + virtual void maintain(uint32_t budgetMs) { (void)budgetMs; } + static const char *normalizeRel(const char *rel); }; diff --git a/platforms/meshcore/LoPlatformMeshcore.cpp b/platforms/meshcore/LoPlatformMeshcore.cpp index bba5b7f..811e4e8 100644 --- a/platforms/meshcore/LoPlatformMeshcore.cpp +++ b/platforms/meshcore/LoPlatformMeshcore.cpp @@ -19,6 +19,7 @@ #include "platforms/arduino/LoPlatformArduino.h" #include "core/LoBBSDispatch.h" #include "core/LoBBSKernel.h" +#include #include "platforms/meshcore/LoPlatformMeshcore.h" #include "lofs/platforms/meshcore/LoFSMountsMeshcore.h" @@ -165,6 +166,7 @@ void lobbsMeshCoreInit(LoBBSKernel *core, BaseChatMesh &mesh, void *filesystem, void lobbsMeshCoreLoop(LoBBSKernel *core) { (void)core; + LoFS::maintain(15); } static void lobbsFillInbound(LoInbound &in, const ContactInfo &from, const char *text) diff --git a/platforms/meshtastic/LoBBSModule.cpp b/platforms/meshtastic/LoBBSModule.cpp index 8f334a3..a53ee2f 100644 --- a/platforms/meshtastic/LoBBSModule.cpp +++ b/platforms/meshtastic/LoBBSModule.cpp @@ -1,6 +1,8 @@ #if defined(LOBBS_PLATFORM_MESHTASTIC) #include "platforms/meshtastic/LoBBSModule.h" #include "platforms/meshtastic/LoBBSDispatchMeshtastic.h" +#include "platforms/LoPlatform.h" +#include LoBBSModule::LoBBSModule() : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_MESSAGE_APP) { @@ -9,6 +11,12 @@ LoBBSModule::LoBBSModule() : SinglePortModule("LoBBS", meshtastic_PortNum_TEXT_M ProcessMessage LoBBSModule::handleReceived(const meshtastic_MeshPacket &mp) { + static uint32_t lastMaintainMs = 0; + const uint32_t now = lobbsPlatformMillis(); + if (now - lastMaintainMs >= 5000) { + lastMaintainMs = now; + LoFS::maintain(15); + } return lobbsDispatchReceived(&kernel_, mp); } #endif