#!/usr/bin/env python3 """Execute production file readers against SPIFFS-style phantom directories.""" from pathlib import Path import os import shutil import subprocess import tempfile import unittest ROOT = Path(__file__).resolve().parents[1] def section(path, start, end): text = (ROOT / path).read_text(encoding="utf-8") begin = text.index(start) return text[begin:text.index(end, begin)] HARNESS = r''' #include #include #include #include #include #include #include #include #include #include #include #define FILE_O_READ 0 static void check(bool ok, const char* why) { if (!ok) throw std::runtime_error(why); } struct Entry { std::string text; bool directory = false; }; struct FakeFS; struct File { std::shared_ptr entry; size_t position = 0; FakeFS* owner = nullptr; #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) // Actual Adafruit LittleFS requires an owner even for a closed File. File() = delete; #else File() = default; #endif explicit File(FakeFS& fs) : owner(&fs) {} File(FakeFS& fs, std::shared_ptr data) : entry(data), owner(&fs) {} explicit operator bool() const { return bool(entry); } bool isDirectory() const { return entry && entry->directory; } size_t size() const { return entry && !isDirectory() ? entry->text.size() : 0; } size_t available() const { return size() - position; } size_t read(uint8_t* dest, size_t count) { if (!entry || isDirectory()) return 0; count = std::min(count, available()); memcpy(dest, entry->text.data() + position, count); position += count; return count; } void close() { entry.reset(); } }; struct FakeFS { std::map> entries; bool fail_open = false; bool report_directory_exists = false; unsigned missing_opens = 0; unsigned open_calls = 0; bool exists(const char* path) { auto it = entries.find(path); return it != entries.end() && (!it->second->directory || report_directory_exists); } File fixtureOpen(const char* path) { ++open_calls; if (fail_open) return File(*this); auto it = entries.find(path); if (it != entries.end()) return File(*this, it->second); ++missing_opens; return File(*this, std::make_shared(Entry{"", true})); } #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) File open(const char* path, uint8_t = FILE_O_READ) { return fixtureOpen(path); } #elif defined(RP2040_PLATFORM) // Philhower FS requires the mode argument, unlike Arduino ESP32. File open(const char* path, const char*) { return fixtureOpen(path); } #else File open(const char* path, const char* = "r", bool = false) { return fixtureOpen(path); } #endif void put(const char* path, const std::string& content, bool directory = false) { entries[path] = std::make_shared(Entry{content, directory}); } } SPIFFS; FakeFS InternalFS; using FILESYSTEM = FakeFS; class DataStore { public: FILESYSTEM* _fs; explicit DataStore(FILESYSTEM* fs) : _fs(fs) {} File openRead(const char*); File openRead(FILESYSTEM*, const char*); File openDirectory(const char*); File openDirectory(FILESYSTEM*, const char*); }; @DATASTORE@ struct WiFiClient { std::string output; bool connected() const { return true; } size_t write(const uint8_t* data, size_t size) { output.append(reinterpret_cast(data), size); return size; } void printf(const char* format, ...) { char buffer[512]; va_list args; va_start(args, format); int size = vsnprintf(buffer, sizeof(buffer), format, args); va_end(args); check(size >= 0 && static_cast(size) < sizeof(buffer), "HTTP overflow"); output.append(buffer, size); } }; struct LogServer { bool running = true; @RESPONSE@ @SENDLOG@ }; @FLOOD_VERIFY@ int main() { try { FakeFS fs; DataStore store(&fs); File unopened = mesh::emptyFile(&fs); File unavailable = mesh::emptyFile(static_cast(nullptr)); check(!unopened && !unavailable, "empty helper returned an open file"); check(fs.open_calls == 0 && InternalFS.open_calls == 0, "closed file construction must never access either filesystem"); #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) check(unopened.owner == &fs, "closed LittleFS file lost its selected owner"); check(unavailable.owner == &InternalFS, "null filesystem lacks a valid fallback owner"); #endif File phantom = fs.fixtureOpen("/missing"); check(bool(phantom) && phantom.isDirectory(), "fixture must reproduce SPIFFS"); unsigned before = fs.missing_opens; check(!store.openRead("/missing"), "missing file accepted"); check(fs.missing_opens == before, "missing regular file should not be opened"); check(!store.openRead(nullptr, "/file"), "unavailable FS accepted"); check(!store.openRead(nullptr), "null path accepted"); check(!store.openDirectory(nullptr, "/"), "unavailable directory FS accepted"); check(!store.openDirectory(nullptr), "null directory path accepted"); fs.put("/empty", ""); fs.put("/real", "data"); check(bool(store.openRead("/empty")), "real empty file rejected"); File real = store.openRead("/real"); uint8_t bytes[4]; check(real.read(bytes, sizeof(bytes)) == 4 && memcmp(bytes, "data", 4) == 0, "real file changed"); check(!store.openDirectory("/real"), "regular file accepted as directory"); check(store.openDirectory("/").isDirectory(), "SPIFFS root listing broken"); fs.put("/directory", "", true); fs.report_directory_exists = true; check(!store.openRead("/directory"), "directory accepted when exists is true"); check(store.openDirectory("/directory").isDirectory(), "real listing broken"); fs.fail_open = true; check(!store.openRead("/real"), "failed read-open accepted"); fs.fail_open = false; // Empty expected images must not validate a missing file/directory as data. check(!verifyFloodSettingsWrite(&fs, "/missing", 0, 2166136261UL), "missing flood file passed empty verification"); check(!verifyFloodSettingsWrite(&fs, "/directory", 0, 2166136261UL), "directory passed empty verification"); check(verifyFloodSettingsWrite(&fs, "/empty", 0, 2166136261UL), "real empty image should verify"); check(verifyFloodSettingsWrite(&fs, "/real", 4, updateFloodSettingsHash(2166136261UL, reinterpret_cast("data"), 4)), "valid flood image rejected"); LogServer server; WiFiClient missing; server.sendLog(missing); check(missing.output.find("HTTP/1.1 404 Not Found\r\n") == 0, "missing packet log must return HTTP404"); SPIFFS.put("/packet_log", ""); WiFiClient empty; server.sendLog(empty); check(empty.output.find("HTTP/1.1 200 OK\r\n") == 0 && empty.output.find("Content-Length: 0\r\n") != std::string::npos, "existing empty packet log must remain HTTP200"); SPIFFS.put("/packet_log", "packet\n"); WiFiClient log; server.sendLog(log); check(log.output.find("Content-Length: 7\r\n") != std::string::npos && log.output.substr(log.output.size() - 7) == "packet\n", "log download changed"); SPIFFS.put("/packet_log", "", true); SPIFFS.report_directory_exists = true; WiFiClient directory; server.sendLog(directory); check(directory.output.find("HTTP/1.1 404 Not Found\r\n") == 0, "directory log must return HTTP404"); std::cout << "regular-file readers: PASS\n"; return 0; } catch (const std::exception& e) { std::cerr << e.what() << '\n'; return 1; } } ''' class RegularFileReadTest(unittest.TestCase): def test_production_readers(self): compiler = shutil.which("g++") or shutil.which("clang++") if not compiler: self.skipTest("A host C++ compiler is required") program = HARNESS.replace("@DATASTORE@", section( "examples/companion_radio/DataStore.cpp", "File DataStore::openRead(", "bool DataStore::removeFile(")) program = program.replace("@RESPONSE@", section( "src/helpers/ESP32Board.cpp", " static void sendResponse(", " bool readLine(")) program = program.replace("@SENDLOG@", section( "src/helpers/ESP32Board.cpp", " void sendLog(", " void sendUpdateError(")) read_helper = section("examples/simple_repeater/MyMesh.cpp", "static File openFloodSettingsRead(", "static File openFloodSettingsWrite(") verification = section("examples/simple_repeater/MyMesh.cpp", "static uint32_t updateFloodSettingsHash(", "static uint8_t batteryPercentFromMilliVolts(") program = program.replace("@FLOOD_VERIFY@", read_helper + verification) for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM", "RP2040_PLATFORM", "STM32_PLATFORM"): with self.subTest(platform=platform), tempfile.TemporaryDirectory() as tmp: executable = Path(tmp) / ("readers.exe" if os.name == "nt" else "readers") built = subprocess.run( [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-O0", f"-D{platform}", "-I", str(ROOT / "test/fixtures/regular_file_reads"), "-I", str(ROOT / "src"), "-x", "c++", "-", "-o", str(executable)], input=program, text=True, capture_output=True, timeout=60) self.assertEqual(built.returncode, 0, built.stdout + built.stderr) run = subprocess.run([str(executable)], capture_output=True, text=True, timeout=15) self.assertEqual(run.returncode, 0, run.stdout + run.stderr) def test_directory_listing_uses_explicit_directory_api(self): cli = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() self.assertIn("File root = _store->openDirectory(path);", cli) self.assertIn("File root2 = _store->openDirectory(_store->getSecondaryFS(), path);", cli) self.assertIn("File file = _store->openRead(selected_fs, path);", cli) def test_audited_read_helpers_remain_regular_file_only(self): for path, marker, end in ( ("src/helpers/ClientACL.cpp", "static File openRead(", "#if !defined(NRF52_PLATFORM)"), ("examples/simple_room_server/FloodRuleEngine.cpp", "static File openRead(", "static File openWrite("), ): with self.subTest(path=path): self.assertIn("mesh::openFileRead(", section(path, marker, end)) for role in ("simple_repeater", "simple_room_server"): dump = section(f"examples/{role}/MyMesh.cpp", "void MyMesh::dumpLogFile()", "bool MyMesh::hasPendingSerialOutput()") self.assertEqual(dump.count("mesh::openFileRead(_fs, PACKET_LOG_FILE)"), 2) self.assertNotIn("_fs->open(", dump) if __name__ == "__main__": unittest.main()