Files
HaloKeymind/test/fixtures/contact_cache/test_contact_cache.cpp
T
mikecarper acfcfb6008 Keep ESP contact saves responsive on PSRAM boards
Extend the cooperative contact transaction to uncached ESP32 storage,
including the PSRAM policy used by the XIAO and V4. Preserve owned contact
copies, the 152-byte disk format, revision cancellation and synchronous
durability operations. Keep contact capacities unchanged.

Avoid SDK error logs when optional blob or transaction cleanup files are
absent, while preserving metadata and deletion failure handling.

Validate cached ESP/nRF and uncached PSRAM paths under ASan/UBSan,
including bounded writes, mutation cancellation, faults, recovery and
reboot readback. Update the existing uncached production fixture.
2026-10-04 04:01:40 -07:00

782 lines
32 KiB
C++

#include "FakeFilesystem.h"
#define FILESYSTEM FakeFilesystem
#include <helpers/ContactInfo.h>
#include <helpers/ContactSecretCache.h>
#include <helpers/ContactFileTransaction.h>
#if defined(NRF52_PLATFORM)
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
#include <helpers/AtomicFileWriter.h>
#endif
#include "packet_under_test.h"
#define ADV_TYPE_NONE 0
struct Host {
unsigned cache_flushes = 0;
size_t capacity = 350;
std::vector<ContactInfo> contacts;
bool onContactLoaded(const ContactInfo& c) {
if (contacts.size() >= capacity) return false;
contacts.push_back(c);
return true;
}
void onContactCacheFlushed() { ++cache_flushes; }
ContactInfo* getContactForStore(uint32_t index) {
return index < contacts.size() ? &contacts[index] : nullptr;
}
};
using DataStoreHost = Host;
#if defined(NRF52_PLATFORM)
static const char* CONTACT_MIGRATION_MARKER = "/contacts4.mig";
bool contactPathPresence(FakeFilesystem* fs, const char* path, bool& present) {
present = fs->exists(path);
return true;
}
#else
bool companionPathPresence(FakeFilesystem* fs, const char* path, bool& present) {
present = fs->exists(path);
return true;
}
#endif
// The narrow hardware adapter below supplies the same filesystem/host seams.
// Serialization, save/eviction and secret I/O bodies are extracted unchanged
// from DataStore.cpp and compiled below, including their platform branches.
class DataStore : public mesh::ContactPathBackend, public mesh::ContactSecretBackend {
DataStoreHost* _cache_host;
bool _cache_load_incomplete = false;
#if defined(ESP32_PLATFORM)
File _contact_path_reader;
#include "contact_write_state_under_test.h"
FakeFilesystem* _fs = &SPIFFS;
FakeFilesystem* _fsExtra = nullptr;
#endif
uint32_t _secret_retry_at = 0;
#if defined(NRF52_PLATFORM)
uint32_t _contact_page_generations[14] = {};
bool _dirty[14] = {};
bool _contact_load_incomplete = false;
bool _legacy_contacts_pending_cleanup = false, _legacy_migration_ready = false;
uint16_t _legacy_contact_count = 0;
mesh::storage::ContactSlotMap _contact_slots;
mesh::storage::DirtyPageSet _dirty_contact_pages, _unread_contact_pages;
void resetContactPageState() { _contact_slots.clear(); }
bool prepareLegacyContactMigration() { _legacy_migration_ready = true; return true; }
#endif
public:
explicit DataStore(Host& host) : _cache_host(&host) {
mesh::contactPathStorage().attach(this);
}
~DataStore();
FakeFilesystem* _getContactsChannelsFS() const {
#if defined(ESP32_PLATFORM)
return _fsExtra != nullptr ? _fsExtra : _fs;
#else
return &SPIFFS;
#endif
}
File openRead(FakeFilesystem* fs, const char* path) { return fs->open(path); }
bool hasIncompleteContactLoad() const {
return _cache_load_incomplete
#if defined(NRF52_PLATFORM)
|| _contact_load_incomplete || !_unread_contact_pages.empty()
#endif
;
}
void loadContacts(DataStoreHost*);
bool saveContacts(DataStoreHost*, bool (*filter)(const ContactInfo&));
bool flushContactWrites(DataStoreHost*, bool (*filter)(const ContactInfo&));
bool readStoredPath(uint16_t, uint8_t[64]) override;
bool flushCachedPaths() override;
#if defined(ESP32_PLATFORM)
void begin();
void disableSecondaryFS(bool);
void useSecondary(FakeFilesystem* fs) { _fsExtra = fs; }
bool cancelCooperativeWrite() override;
#endif
uint16_t secretSlot(const uint8_t[32]) const;
bool readSavedSecret(const uint8_t[32], const uint8_t[32], uint8_t[32]) override;
bool saveSecret(const uint8_t[32], const uint8_t[32], const uint8_t[32]) override;
#if defined(ESP32_PLATFORM)
bool markContactDirty(const ContactInfo&);
bool releaseContact(const ContactInfo&);
bool hasPendingContactWrites() const;
bool serviceContactWrites(DataStoreHost*, bool (*filter)(const ContactInfo&));
#endif
#if defined(NRF52_PLATFORM)
bool loadContactPages(DataStoreHost*, uint16_t, uint32_t);
bool writeContactPage(DataStoreHost*, uint8_t, bool (*filter)(const ContactInfo&));
bool markContactDirty(const ContactInfo& c) {
if (hasIncompleteContactLoad() || c.storage_slot >= 350) return false;
_dirty[c.storage_slot / 25] = true;
return true;
}
bool hasPendingContactWrites() const {
for (bool dirty : _dirty) if (dirty) return true;
return false;
}
bool serviceContactWrites(DataStoreHost* host, bool (*filter)(const ContactInfo&)) {
if (hasIncompleteContactLoad()) return false;
for (uint8_t i = 0; i < 14; ++i) if (_dirty[i]) {
if (!writeContactPage(host, i, filter)) return false;
_dirty[i] = false;
return true;
}
return true;
}
#endif
};
#include "store_under_test.h"
// Exercise the real Mesh::sendDirect and all request/login callers with only
// the radio queue and datagram constructor replaced. A flash miss must release
// the packet and report failure to the app, then recover on a successful read.
namespace mesh {
class Mesh {
protected:
struct Tables { void markSent(Packet*) {} } tables;
Tables* _tables = &tables;
void maybeScheduleDirectRetry(Packet*, uint8_t) {}
uint8_t getTraceDirectPriority(Packet*) { return 0; }
const char* getLogDateTime() { return "test"; }
public:
Packet packet;
unsigned released = 0, queued = 0;
void releasePacket(Packet*) { ++released; }
bool sendPacket(Packet*, uint8_t, uint32_t) { ++queued; return true; }
bool sendDirect(Packet*, const uint8_t*, uint8_t, uint32_t = 0);
};
}
#define MSG_SEND_FAILED 0
#define MSG_SEND_SENT_FLOOD 1
#define MSG_SEND_SENT_DIRECT 2
#define ADV_TYPE_ROOM 3
class BaseChatMesh : public mesh::Mesh {
uint32_t getTransmitAirtime(const mesh::Packet* packet) const {
return _radio->getEstAirtimeFor(packet->getRawLength());
}
struct Radio { uint32_t getEstAirtimeFor(int) { return 20; } } radio;
struct Clock { uint32_t getCurrentTimeUnique() { return 100; } } clock;
struct Rng { void random(uint8_t* data, size_t size) { memset(data, 42, size); } } rng;
Radio* _radio = &radio;
mesh::LocalIdentity self_id;
Clock* getRTCClock() { return &clock; }
Rng* getRNG() { return &rng; }
mesh::Packet* createDatagram(uint8_t type, const mesh::Identity&,
const uint8_t*, const uint8_t*, size_t length) {
packet = mesh::Packet();
packet.header = type << PH_TYPE_SHIFT;
packet.payload_len = length;
return &packet;
}
mesh::Packet* createAnonDatagram(uint8_t type, const mesh::LocalIdentity&,
const mesh::Identity& peer, const uint8_t* secret, const uint8_t* data, size_t length) {
return createDatagram(type, peer, secret, data, length);
}
bool sendFloodScoped(const ContactInfo&, mesh::Packet* p) { return sendPacket(p, 0, 0); }
uint32_t calcFloodTimeoutMillisFor(uint32_t t) { return t * 2; }
uint32_t calcDirectTimeoutMillisFor(uint32_t t, uint8_t n) { return t * (n + 1); }
public:
int sendLogin(const ContactInfo&, const char*, uint32_t&);
int sendAnonReq(const ContactInfo&, const uint8_t*, uint8_t, uint32_t&, uint32_t&);
virtual bool allowRequestTag(uint32_t) { return true; }
int sendRequest(const ContactInfo&, const uint8_t*, uint8_t, uint32_t&, uint32_t&);
int sendRequest(const ContactInfo&, uint8_t, uint32_t&, uint32_t&);
};
#include "send_under_test.h"
using Path = std::array<uint8_t, 64>;
Path route(unsigned index) {
Path path;
for (size_t i = 0; i < path.size(); ++i) path[i] = index * 19 + i + (index >> 8);
return path;
}
struct Fixture {
Host host;
DataStore store{host};
explicit Fixture(size_t count = 350) {
SPIFFS = FakeFilesystem();
std::vector<uint8_t> records(count * mesh::storage::CONTACT_RECORD_SIZE, 0);
host.contacts.reserve(count);
for (size_t i = 0; i < count; ++i) {
uint8_t* r = records.data() + i * mesh::storage::CONTACT_RECORD_SIZE;
memset(r, 0x21, 32); // identical prefixes exercise full peer-key matching
r[30] = i >> 8;
r[31] = i;
snprintf(reinterpret_cast<char*>(r + 32), 32, "Contact %u", unsigned(i));
r[64] = 1;
r[71] = 63;
const Path path = route(i);
memcpy(r + 76, path.data(), path.size());
}
SPIFFS.files["/contacts3"] = records;
for (size_t i = 0; i < count; ++i) {
ContactInfo c;
assert(deserializeContactRecord(records.data() + i * 152, c, i));
#if defined(NRF52_PLATFORM)
c.storage_slot = i;
#endif
host.contacts.push_back(c);
}
}
void check(unsigned index, const Path& expected) {
const uint8_t* actual = host.contacts[index].getPath();
assert(actual && memcmp(actual, expected.data(), 64) == 0);
}
};
static void routes_survive_eviction_and_full_sync() {
Fixture f;
assert(sizeof(ContactInfo) <= 100); // all 350 records retain compact handles
const unsigned reads = SPIFFS.reads;
f.check(0, route(0));
assert(SPIFFS.reads == reads + 1);
f.check(0, route(0));
assert(SPIFFS.reads == reads + 1); // hot route consumes no flash I/O
for (unsigned i = 1; i < 350; ++i) f.check(i, route(i));
f.check(0, route(0)); // actual LRU miss after 350 different contacts
const auto original = SPIFFS.files.at("/contacts3");
const unsigned opens = SPIFFS.opens;
assert(f.store.saveContacts(&f.host, cachedContactFilter));
assert(SPIFFS.opens - opens == 2); // reuse source; open writer and verifier
assert(SPIFFS.files.at("/contacts3") == original);
for (unsigned i = 0; i < 350; ++i) f.check(i, route(i));
}
static void snapshot_rollback_and_dirty_eviction() {
Fixture f;
ContactInfo pending_response = f.host.contacts[0];
assert(f.host.contacts[0].setRawPath(route(900).data()));
assert(f.store.saveContacts(&f.host, cachedContactFilter));
Path old;
assert(pending_response.copyPathTo(old.data()) && old == route(0));
for (unsigned i = 1; i < 80; ++i) {
assert(f.host.contacts[i].setRawPath(route(900 + i).data()));
}
assert(SPIFFS.writes > 350); // full cache forced safe dirty writeback
for (unsigned i = 0; i < 80; ++i) f.check(i, route(900 + i));
assert(pending_response.copyPathTo(old.data()) && old == route(0));
f.host.contacts[0] = pending_response; // rollback after the old file changed
assert(f.store.saveContacts(&f.host, cachedContactFilter));
f.check(0, route(0));
// Deletion compacts records; every remaining handle follows its new offset.
f.host.contacts.erase(f.host.contacts.begin() + 20);
assert(f.store.saveContacts(&f.host, cachedContactFilter));
for (unsigned i = 20; i < 79; ++i) f.check(i, route(901 + i));
for (unsigned i = 79; i < 349; ++i) f.check(i, route(i + 1));
}
static void storage_failures_preserve_routes() {
Fixture f;
const auto original = SPIFFS.files.at("/contacts3");
ContactInfo previous = f.host.contacts[0];
assert(f.host.contacts[0].setRawPath(route(810).data()));
SPIFFS.max_write = 13;
assert(!f.store.saveContacts(&f.host, cachedContactFilter));
assert(SPIFFS.files.at("/contacts3") == original);
f.check(0, route(810));
f.host.contacts[0] = previous;
f.check(0, route(0));
SPIFFS.max_write = std::numeric_limits<size_t>::max();
for (unsigned fail : {1u, 2u}) {
SPIFFS.renames = 0;
SPIFFS.fail_rename = fail;
assert(!f.store.saveContacts(&f.host, cachedContactFilter));
assert(SPIFFS.files.at("/contacts3") == original);
}
SPIFFS.fail_rename = 0;
SPIFFS.fail_read = "/contacts3";
assert(!f.host.contacts[100].getPath());
assert(!f.store.saveContacts(&f.host, cachedContactFilter));
assert(SPIFFS.files.at("/contacts3") == original);
SPIFFS.fail_read.clear();
SPIFFS.files["/contacts3"][100 * 152 + 76] ^= 1;
assert(!f.host.contacts[100].getPath()); // CRC protects a cold route after boot
assert(!f.store.saveContacts(&f.host, cachedContactFilter));
SPIFFS.files["/contacts3"] = original;
assert(f.store.saveContacts(&f.host, cachedContactFilter));
}
#if defined(ESP32_PLATFORM)
static void rename_gaps_recover_a_complete_contact_file() {
Fixture f(30);
auto old = SPIFFS.files.at("/contacts3");
assert(f.host.contacts[0].setRawPath(route(800).data()));
assert(f.store.saveContacts(&f.host, cachedContactFilter));
auto updated = SPIFFS.files.at("/contacts3");
const auto snapshots = SPIFFS.rename_snapshots;
assert(snapshots.size() == 2);
for (const auto& snapshot : snapshots) {
FakeFilesystem rebooted;
rebooted.files = snapshot;
assert(mesh::ContactFileTransaction::recover(&rebooted, "/contacts3"));
const auto& recovered = rebooted.files.at("/contacts3");
assert(recovered == old || recovered == updated);
}
}
#endif
static void pinned_snapshots_bound_ram_without_losing_updates() {
Fixture f;
std::vector<ContactInfo> snapshots;
snapshots.reserve(16);
for (unsigned i = 0; i < 16; ++i) {
snapshots.push_back(f.host.contacts[i]);
assert(snapshots.back().setRawPath(route(1000 + i).data()));
}
const ContactInfo previous = f.host.contacts[100];
assert(!f.host.contacts[100].setRawPath(route(1500).data()));
Path path;
assert(f.host.contacts[100].copyPathTo(path.data()) && path == route(100));
snapshots.clear();
assert(f.host.contacts[100].setRawPath(route(1500).data()));
assert(f.store.saveContacts(&f.host, cachedContactFilter));
assert(previous.copyPathTo(path.data()) && path == route(100));
}
static void encoded_paths_and_copy_assignment() {
Fixture f(40);
auto& c = f.host.contacts[0];
const Path raw = route(700);
for (uint8_t len : {uint8_t(63), uint8_t(0x60), uint8_t(0x95)}) {
assert(c.setPath(raw.data(), len));
assert(c.out_path_len == len);
const auto* p = c.getPath();
const unsigned bytes = (len & 63) * ((len >> 6) + 1);
assert(p && memcmp(p, raw.data(), bytes) == 0);
}
const ContactInfo old = c;
assert(!c.setPath(raw.data(), 0x61)); // 33 two-byte hashes exceed 64 bytes
assert(!c.setPath(raw.data(), 0xc1)); // reserved hash encoding
assert(c.setPath(nullptr, 0));
assert(c.out_path_len == 0);
assert(c.setPath(nullptr, OUT_PATH_UNKNOWN));
assert(c.out_path_len == OUT_PATH_UNKNOWN);
c = old;
assert(c.out_path_len == old.out_path_len);
assert(f.store.saveContacts(&f.host, cachedContactFilter));
}
static void failed_path_reads_fail_sends_without_leaking_packets() {
Fixture f(30);
mesh::contactSecretCache().attach(&f.store);
BaseChatMesh mesh;
const auto& recipient = f.host.contacts[29];
uint8_t request[] = {1};
uint32_t tag, timeout;
for (bool fail : {true, false}) {
SPIFFS.fail_read = fail ? "/contacts3" : "";
const int expected = fail ? MSG_SEND_FAILED : MSG_SEND_SENT_DIRECT;
assert(mesh.sendLogin(recipient, "test", timeout) == expected);
assert(mesh.sendAnonReq(recipient, request, 1, tag, timeout) == expected);
assert(mesh.sendRequest(recipient, request, 1, tag, timeout) == expected);
assert(mesh.sendRequest(recipient, 1, tag, timeout) == expected);
assert(mesh.released == 4);
assert(mesh.queued == (fail ? 0u : 4u));
if (!fail) assert(memcmp(mesh.packet.path, route(29).data(), 63) == 0);
}
}
static void contact_reload_preserves_disk_on_cache_exhaustion() {
Fixture f;
#if defined(NRF52_PLATFORM)
for (unsigned page = 0; page < 14; ++page)
assert(f.store.writeContactPage(&f.host, page, cachedContactFilter));
SPIFFS.remove("/contacts3");
#endif
f.host.contacts.clear();
f.store.loadContacts(&f.host);
assert(!f.store.hasIncompleteContactLoad() && f.host.contacts.size() == 350);
for (unsigned i = 0; i < 350; ++i) f.check(i, route(i));
f.host.contacts.clear();
const auto durable = SPIFFS.files;
const unsigned writes = SPIFFS.writes;
// Occupy every handle with independent pending values. A failed load must
// not turn a valid persisted record into an empty/invalid contact repair.
std::vector<mesh::ContactPathRef> pending(390);
for (auto& path : pending) assert(path.bind(0, route(0).data()));
f.store.loadContacts(&f.host);
assert(f.store.hasIncompleteContactLoad());
assert(f.host.contacts.empty());
assert(!f.store.saveContacts(&f.host, cachedContactFilter));
assert(SPIFFS.files == durable && SPIFFS.writes == writes);
pending.clear();
f.store.loadContacts(&f.host);
assert(f.host.contacts.empty()); // latched until a fresh boot/store
DataStore rebooted(f.host);
rebooted.loadContacts(&f.host);
assert(!rebooted.hasIncompleteContactLoad() && f.host.contacts.size() == 350);
for (unsigned i = 0; i < 350; ++i) f.check(i, route(i));
}
#if defined(ESP32_PLATFORM)
static unsigned drain_cooperative_save(Fixture& f) {
unsigned serviced_commands = 0;
while (f.store.hasPendingContactWrites()) {
const unsigned before_writes = SPIFFS.writes;
const unsigned before_reads = SPIFFS.reads;
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
// Returning from every bounded store pass lets the production mesh loop
// poll transports. No pass serializes the whole table or verifies it all.
assert(SPIFFS.writes - before_writes <= 1);
assert(SPIFFS.reads - before_reads <= 16); // retained path snapshots
++serviced_commands;
assert(serviced_commands < 3000);
assert(SPIFFS.exists("/contacts3")); // no observable rename gap
for (unsigned i = 0; i < f.host.contacts.size(); i += 47) {
Path path;
assert(f.host.contacts[i].copyPathTo(path.data()));
}
}
return serviced_commands;
}
static void cooperative_save_services_commands_and_preserves_snapshots() {
Fixture f;
const ContactInfo snapshot = f.host.contacts[349];
const auto old = SPIFFS.files.at("/contacts3");
assert(f.host.contacts[349].setRawPath(route(980).data()));
assert(f.store.markContactDirty(f.host.contacts[349]));
assert(f.store.hasPendingContactWrites());
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
assert(f.store.hasPendingContactWrites() && SPIFFS.files.at("/contacts3") == old);
const unsigned polled = drain_cooperative_save(f);
assert(polled > 350); // record writes and CRC readback both yield
assert(SPIFFS.files.at("/contacts3") != old);
f.check(349, route(980));
Path path;
assert(snapshot.copyPathTo(path.data()) && path == route(349));
for (const auto& files : SPIFFS.rename_snapshots) {
FakeFilesystem rebooted;
rebooted.files = files;
assert(mesh::ContactFileTransaction::recover(&rebooted, "/contacts3"));
assert(rebooted.files.at("/contacts3") == old
|| rebooted.files.at("/contacts3") == SPIFFS.files.at("/contacts3"));
}
}
static void cooperative_mutations_never_publish_obsolete_indices() {
for (unsigned mutation_step : {4u, 46u, 110u, 140u}) {
Fixture f(40);
const auto old = SPIFFS.files.at("/contacts3");
assert(f.store.markContactDirty(f.host.contacts[0]));
for (unsigned step = 0; step < mutation_step; ++step) {
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
assert(SPIFFS.files.at("/contacts3") == old);
}
const ContactInfo snapshot = f.host.contacts[3]; // created during save
assert(f.store.releaseContact(f.host.contacts[3]));
f.host.contacts.erase(f.host.contacts.begin() + 3); // shifts all later indices
assert(f.host.contacts[1].setRawPath(route(991).data()));
snprintf(f.host.contacts[1].name, sizeof(f.host.contacts[1].name), "new revision");
assert(f.store.markContactDirty(f.host.contacts[1]));
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
assert(f.store.hasPendingContactWrites()); // abort is not durable success
assert(SPIFFS.files.at("/contacts3") == old);
drain_cooperative_save(f);
const auto& bytes = SPIFFS.files.at("/contacts3");
assert(bytes.size() == 39 * 152);
for (unsigned i = 0; i < 39; ++i) {
assert(memcmp(bytes.data() + i * 152, f.host.contacts[i].id.pub_key, 32) == 0);
Path path;
assert(f.host.contacts[i].copyPathTo(path.data()));
const Path expected = route(i == 1 ? 991 : i >= 3 ? i + 1 : i);
assert(path == expected);
}
Path path;
assert(snapshot.copyPathTo(path.data()) && path == route(3));
}
}
static void cooperative_failures_keep_the_old_file_and_pending_mutations() {
for (unsigned fault = 0; fault < 4; ++fault) {
Fixture f(30);
const auto old = SPIFFS.files.at("/contacts3");
assert(f.host.contacts[0].setRawPath(route(993).data()));
assert(f.store.markContactDirty(f.host.contacts[0]));
if (fault == 0) SPIFFS.max_write = 17;
if (fault == 1) SPIFFS.fail_read = "/contacts3.tmp";
if (fault == 2) SPIFFS.fail_rename = SPIFFS.renames + 2;
bool failed = false;
for (unsigned step = 0; step < 1000 && !failed; ++step) {
if (fault == 3 && step == 35) SPIFFS.files.at("/contacts3.tmp")[100] ^= 1;
failed = !f.store.serviceContactWrites(&f.host, cachedContactFilter);
}
assert(failed && f.store.hasPendingContactWrites());
assert(SPIFFS.files.at("/contacts3") == old);
assert(!SPIFFS.exists("/contacts3.tmp"));
f.check(0, route(993));
SPIFFS.max_write = std::numeric_limits<size_t>::max();
SPIFFS.fail_read.clear(); SPIFFS.fail_rename = 0;
drain_cooperative_save(f);
assert(SPIFFS.files.at("/contacts3") != old);
f.check(0, route(993));
}
}
static void synchronous_flush_cancels_pending_jobs_and_stays_durable() {
Fixture f(40);
const ContactInfo snapshot = f.host.contacts[0];
assert(f.host.contacts[0].setRawPath(route(994).data()));
assert(f.store.markContactDirty(f.host.contacts[0]));
for (unsigned i = 0; i < 8; ++i)
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
// Update, reorder, then force explicit durability instead of draining a
// snapshot from an earlier revision. The rename callback sees a full file.
std::swap(f.host.contacts[0], f.host.contacts[3]);
assert(f.store.markContactDirty(f.host.contacts[0]));
assert(f.store.flushContactWrites(&f.host, cachedContactFilter));
assert(!f.store.hasPendingContactWrites() && !SPIFFS.exists("/contacts3.tmp"));
assert(memcmp(SPIFFS.files.at("/contacts3").data(), f.host.contacts[0].id.pub_key, 32) == 0);
f.check(3, route(994));
Path path;
assert(snapshot.copyPathTo(path.data()) && path == route(0));
}
static void interleaved_path_pressure_preserves_guarded_eviction() {
Fixture f(40);
for (unsigned i = 0; i < 26; ++i) {
// Keep a real lazy job open while more than all16 resident path slots are
// replaced by app updates. makeRoom must abandon only that cooperative
// job before the pre-existing synchronous eviction flush obtains room.
if (!f.store.hasPendingContactWrites())
assert(f.store.markContactDirty(f.host.contacts[0]));
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
assert(f.host.contacts[i].setRawPath(route(1200 + i).data()));
assert(f.store.markContactDirty(f.host.contacts[i]));
}
assert(f.host.cache_flushes > 0); // actual cache exhaustion, no mocked bypass
drain_cooperative_save(f);
for (unsigned i = 0; i < 40; ++i)
f.check(i, route(i < 26 ? 1200 + i : i));
}
static void lifecycle_cancels_jobs_and_closes_prior_filesystem_readers() {
{
Fixture f(30);
assert(f.store.markContactDirty(f.host.contacts[0]));
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
f.check(29, route(29)); // opens the original cold-path reader
f.store.begin();
assert(f.store.hasPendingContactWrites());
assert(!SPIFFS.exists("/contacts3.tmp"));
const unsigned opens = SPIFFS.opens;
f.check(28, route(28));
assert(SPIFFS.opens == opens + 1); // reinitialization reopened its reader
drain_cooperative_save(f);
}
{
Fixture f(30);
FakeFilesystem alternate;
alternate.files = SPIFFS.files;
f.store.useSecondary(&alternate);
assert(f.store.markContactDirty(f.host.contacts[0]));
assert(f.store.serviceContactWrites(&f.host, cachedContactFilter));
f.check(29, route(29));
const unsigned opens = SPIFFS.opens;
f.store.disableSecondaryFS(false);
assert(f.store.hasPendingContactWrites());
assert(!alternate.exists("/contacts3.tmp"));
f.check(28, route(28));
assert(SPIFFS.opens == opens + 1); // reads follow the primary route
drain_cooperative_save(f);
// No lazy job remains tied to alternate's shorter lifetime.
}
}
static void partial_loads_cannot_replace_the_complete_file() {
for (bool capacity_failure : {false, true}) {
Fixture f(40);
f.host.contacts.clear();
const auto durable = SPIFFS.files;
if (capacity_failure) f.host.capacity = 20;
else SPIFFS.fail_read = "/contacts3";
f.store.loadContacts(&f.host);
assert(f.store.hasIncompleteContactLoad());
assert(!f.store.saveContacts(&f.host, cachedContactFilter));
assert(SPIFFS.files == durable);
}
}
#endif
static void secrets_load_from_flash_instead_of_recalculating() {
Fixture f;
mesh::LocalIdentity self;
self.private_key[0] = 42;
self.pub_key[0] = 23;
mesh::ContactSecretCache cache;
cache.attach(&f.store);
std::array<uint8_t, 32> first;
memcpy(first.data(), cache.get(self, f.host.contacts[0].id.pub_key), 32);
assert(self.derivations == 1);
const unsigned writes = SPIFFS.writes;
assert(memcmp(first.data(), cache.get(self, f.host.contacts[0].id.pub_key), 32) == 0);
assert(self.derivations == 1 && SPIFFS.writes == writes);
for (unsigned i = 1; i <= 20; ++i) cache.get(self, f.host.contacts[i].id.pub_key);
const unsigned before_miss = self.derivations, before_writes = SPIFFS.writes;
assert(memcmp(first.data(), cache.get(self, f.host.contacts[0].id.pub_key), 32) == 0);
assert(self.derivations == before_miss && SPIFFS.writes == before_writes);
assert(cache.flash_hits == 1);
// Reboot loses the RAM cache; the persisted identity-bound key is reusable.
mesh::ContactSecretCache rebooted;
rebooted.attach(&f.store);
assert(memcmp(first.data(), rebooted.get(self, f.host.contacts[0].id.pub_key), 32) == 0);
assert(self.derivations == before_miss);
self.private_key[1] ^= 2; // also invalidate when public bytes did not change
assert(memcmp(first.data(), rebooted.get(self, f.host.contacts[0].id.pub_key), 32) != 0);
assert(self.derivations == before_miss + 1);
self.pub_key[3] ^= 4;
rebooted.get(self, f.host.contacts[0].id.pub_key);
assert(self.derivations == before_miss + 2);
}
static void persisted_secret_matches_the_real_peer_key_exchange() {
Fixture f(30);
mesh::LocalIdentity self, peer;
uint8_t seed[32] = {1};
ed25519_create_keypair(self.pub_key, self.private_key, seed);
seed[0] = 2;
ed25519_create_keypair(peer.pub_key, peer.private_key, seed);
f.host.contacts[0].id = peer;
uint8_t reciprocal[32];
peer.calcSharedSecret(reciprocal, self.pub_key);
mesh::ContactSecretCache cache;
cache.attach(&f.store);
assert(memcmp(reciprocal, cache.get(self, peer.pub_key), 32) == 0);
for (unsigned i = 1; i < 30; ++i) cache.get(self, f.host.contacts[i].id.pub_key);
const unsigned derivations = self.derivations;
assert(memcmp(reciprocal, cache.get(self, peer.pub_key), 32) == 0);
assert(self.derivations == derivations && cache.flash_hits == 1);
}
static void secret_corruption_reordering_and_full_storage() {
Fixture f(40);
mesh::LocalIdentity self;
mesh::ContactSecretCache cache;
cache.attach(&f.store);
std::array<uint8_t, 32> first;
memcpy(first.data(), cache.get(self, f.host.contacts[0].id.pub_key), 32);
SPIFFS.files["/csecret000"][36 + 32] ^= 4;
mesh::ContactSecretCache cold;
cold.attach(&f.store);
assert(memcmp(first.data(), cold.get(self, f.host.contacts[0].id.pub_key), 32) == 0);
assert(self.derivations == 2); // corrupted key was recomputed, never used
std::swap(f.host.contacts[0], f.host.contacts[1]);
mesh::ContactSecretCache moved;
moved.attach(&f.store);
assert(memcmp(first.data(), moved.get(self, f.host.contacts[0].id.pub_key), 32) != 0);
assert(self.derivations == 3); // same slot, different full peer key
SPIFFS.capacity = SPIFFS.usedBytes() + 10000;
const unsigned writes = SPIFFS.writes;
moved.get(self, f.host.contacts[30].id.pub_key);
assert(self.derivations == 4 && SPIFFS.writes == writes);
moved.get(self, f.host.contacts[30].id.pub_key);
assert(self.derivations == 4); // still cached in RAM when flash has no reserve
assert(f.store.saveContacts(&f.host, cachedContactFilter));
}
#if defined(NRF52_PLATFORM)
static void nrf52_secret_cache_leaves_contact_storage_reserve() {
Fixture f;
for (unsigned page = 0; page < 14; ++page)
assert(f.store.writeContactPage(&f.host, page, cachedContactFilter));
SPIFFS.remove("/contacts3");
const auto contacts = SPIFFS.files;
SPIFFS.setBlockCount(25); // 100 KiB ExtraFS
mesh::LocalIdentity self;
mesh::ContactSecretCache cache;
cache.attach(&f.store);
for (unsigned i = 0; i < 350; i += 56) cache.get(self, f.host.contacts[i].id.pub_key);
assert(cache.save_failures > 0); // low space remains a best-effort cache
assert(_getLfsUsedBlockCount(&SPIFFS) <= 21); // keep at least four blocks
for (const auto& file : contacts) assert(SPIFFS.files.at(file.first) == file.second);
assert(f.host.contacts[0].setRawPath(route(999).data()));
assert(f.store.saveContacts(&f.host, cachedContactFilter));
f.check(0, route(999));
}
static void nrf52_migration_snapshots_and_failed_page_replacement() {
Fixture f;
ContactInfo snapshot = f.host.contacts[349];
assert(f.host.contacts[349].setRawPath(route(950).data()));
// Commit one page at a time from the legacy tail, exactly as migration does.
for (int page = 13; page >= 0; --page) {
assert(f.store.writeContactPage(&f.host, page, cachedContactFilter));
SPIFFS.files["/contacts3"].resize(page * 25 * 152);
for (unsigned i = page * 25; i < 350; ++i)
f.check(i, route(i == 349 ? 950 : i));
}
SPIFFS.remove("/contacts3");
Path path;
assert(snapshot.copyPathTo(path.data()) && path == route(349));
const auto original = SPIFFS.files.at("/contacts4_00");
assert(f.host.contacts[0].setRawPath(route(990).data()));
SPIFFS.max_write = 13;
assert(!f.store.writeContactPage(&f.host, 0, cachedContactFilter));
assert(SPIFFS.files.at("/contacts4_00") == original);
f.check(0, route(990));
SPIFFS.max_write = std::numeric_limits<size_t>::max();
assert(f.store.saveContacts(&f.host, cachedContactFilter));
f.check(0, route(990));
assert(snapshot.copyPathTo(path.data()) && path == route(349));
for (unsigned i = 1; i < 350; ++i) f.check(i, route(i == 349 ? 950 : i));
SPIFFS.files["/contacts4_00"][20 + 3 * 152 + 76] ^= 8;
assert(!f.host.contacts[3].getPath());
assert(!f.store.writeContactPage(&f.host, 0, cachedContactFilter));
}
#endif
static void contact_tx_policy_round_trips_without_growing_records() {
Fixture f(1);
auto& contact = f.host.contacts[0];
assert(contact.tx_radio == mesh::RADIO_TX_AUTO);
for (uint8_t policy = mesh::RADIO_TX_AUTO; policy <= mesh::RADIO_TX_OFF; ++policy) {
contact.tx_radio = policy;
contact.shared_secret_valid = true;
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
assert(serializeContactRecord(contact, record));
assert(sizeof(record) == 152 && record[66] == mesh::encodeRadioTxPolicy(policy));
ContactInfo restored;
assert(deserializeContactRecord(record, restored, 0));
assert(restored.tx_radio == policy && !restored.shared_secret_valid);
assert(contact.shared_secret_valid && contact.tx_radio == policy);
}
}
int main() {
contact_tx_policy_round_trips_without_growing_records();
#if defined(ESP32_PLATFORM)
cooperative_save_services_commands_and_preserves_snapshots();
cooperative_mutations_never_publish_obsolete_indices();
cooperative_failures_keep_the_old_file_and_pending_mutations();
synchronous_flush_cancels_pending_jobs_and_stays_durable();
interleaved_path_pressure_preserves_guarded_eviction();
lifecycle_cancels_jobs_and_closes_prior_filesystem_readers();
routes_survive_eviction_and_full_sync();
snapshot_rollback_and_dirty_eviction();
storage_failures_preserve_routes();
rename_gaps_recover_a_complete_contact_file();
pinned_snapshots_bound_ram_without_losing_updates();
encoded_paths_and_copy_assignment();
failed_path_reads_fail_sends_without_leaking_packets();
contact_reload_preserves_disk_on_cache_exhaustion();
partial_loads_cannot_replace_the_complete_file();
secrets_load_from_flash_instead_of_recalculating();
persisted_secret_matches_the_real_peer_key_exchange();
secret_corruption_reordering_and_full_storage();
#else
nrf52_secret_cache_leaves_contact_storage_reserve();
nrf52_migration_snapshots_and_failed_page_replacement();
encoded_paths_and_copy_assignment();
failed_path_reads_fail_sends_without_leaking_packets();
contact_reload_preserves_disk_on_cache_exhaustion();
secrets_load_from_flash_instead_of_recalculating();
persisted_secret_matches_the_real_peer_key_exchange();
#endif
return 0;
}