Files
HaloKeymind/examples/companion_radio/DataStore.cpp
T

3134 lines
120 KiB
C++

#include <Arduino.h>
#include <stdlib.h>
#include "DataStore.h"
#include <helpers/FileRead.h>
#include <helpers/AdvertDataHelpers.h>
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
#include <helpers/ContactFileTransaction.h>
#endif
#if COMPANION_FEATURE_JOHN
#include <helpers/bible/JohnBookmarkFiles.h>
#endif
#if defined(NRF52_PLATFORM)
#include <helpers/AtomicFileWriter.h>
#if defined(EXTRAFS) && !defined(QSPIFLASH)
#include <helpers/nrf52/InternalSecondaryFsRepair.h>
#endif
#endif
// Linked presence of this symbol is the authoritative signal that this firmware actually mounts the
// internal 0xD4000 ExtraFS. OTA layout code references it weakly, so non-companion roles can reclaim the
// reserved range even though nrf52_base defines EXTRAFS globally.
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
extern "C" __attribute__((used)) const uint8_t g_meshcore_internal_extrafs = 1u;
extern "C" uint32_t __flash_arduino_end[];
#endif
#if defined(EXTRAFS) || defined(QSPIFLASH)
#define MAX_BLOBRECS 100
#else
#define MAX_BLOBRECS 20
#endif
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr),
_configuredFsExtra(nullptr), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else
identity_store(fs, "/identity")
#endif
{
}
#if defined(EXTRAFS) || defined(QSPIFLASH)
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra),
_configuredFsExtra(&fsExtra), _clock(&clock),
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
identity_store(fs, "")
#elif defined(RP2040_PLATFORM)
identity_store(fs, "/identity")
#else
identity_store(fs, "/identity")
#endif
{
}
#endif
static File openWrite(FILESYSTEM* fs, const char* filename) {
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
fs->remove(filename);
return fs->open(filename, FILE_O_WRITE);
#elif defined(RP2040_PLATFORM)
return fs->open(filename, "w");
#else
return fs->open(filename, "w", true);
#endif
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
static bool validateLfsFilesystem(FILESYSTEM* fs);
#endif
#if defined(NRF52_PLATFORM)
static void cleanupAtomicTempFiles(FILESYSTEM* fs);
static bool contactPathPresence(FILESYSTEM* fs, const char* path,
bool& present, uint32_t* size = nullptr);
#endif
void DataStore::begin() {
#if defined(RP2040_PLATFORM)
identity_store.begin();
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#if defined(NRF52_PLATFORM)
resetContactPageState(true);
if (_secondary_authority_unknown) {
_contact_load_incomplete = true;
_prefs_load_incomplete = true;
}
if (_primary_storage_unavailable) {
// The base mount already failed. Do not traverse or open this unmounted
// filesystem; both can re-enter corrupt metadata before explicit repair.
_identity_creation_blocked = true;
_contact_load_incomplete = true;
_fsExtra = nullptr;
return;
}
bool primary_ready = validateLfsFilesystem(_fs);
if (!primary_ready) {
// A traversal error can be transient, and formatting here would erase the
// only identity before MyMesh can distinguish recovery from a fresh boot.
// Preserve the filesystem and fail closed; a reboot may recover it, while
// an explicit factory reset remains the destructive recovery operation.
MESH_DEBUG_PRINTLN(
"DataStore: primary LittleFS metadata is unavailable; preserving it and blocking startup writes");
_primary_storage_unavailable = true;
_identity_creation_blocked = true;
_contact_load_incomplete = true;
_prefs_load_incomplete = true;
_fsExtra = nullptr;
return;
}
#if defined(EXTRAFS) && !defined(QSPIFLASH)
// Validate primary first: automatic secondary recovery must never hide a
// primary/identity fault. Retry or rebuild before loading any RAM state.
recoverInternalExtraFSOnBoot();
#else
if (_fsExtra != nullptr && !validateLfsFilesystem(_fsExtra)) {
// Automatic destructive recovery is restricted to reserved internal
// ExtraFS. Removable/external QSPI still requires an explicit repair.
MESH_DEBUG_PRINTLN("DataStore: secondary LittleFS metadata is corrupt; preserving it and using primary storage");
disableSecondaryFS(true);
}
#endif
if (primary_ready) cleanupAtomicTempFiles(_fs);
if (_fsExtra != nullptr) cleanupAtomicTempFiles(_fsExtra);
#endif
#if defined(EXTRAFS) || defined(QSPIFLASH)
if (_fsExtra != nullptr && !migrateToSecondaryFS()) {
MESH_DEBUG_PRINTLN("DataStore: one or more secondary filesystem migrations remain pending");
}
#endif
#if defined(NRF52_PLATFORM)
// A journal-stat failure leaves the contact/channel authority unknowable.
// Do not mutate the reconstructable advert cache on either candidate store
// while that fail-closed latch is active.
if (!_contact_load_incomplete) checkAdvBlobFile();
#else
checkAdvBlobFile();
#endif
#else
// init 'blob store' support
_fs->mkdir("/bl");
#endif
}
#if defined(ESP32)
#include <SPIFFS.h>
#include <nvs_flash.h>
#elif defined(RP2040_PLATFORM)
#include <LittleFS.h>
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#if defined(QSPIFLASH)
#include <CustomLFS_QSPIFlash.h>
#elif defined(EXTRAFS)
#include <CustomLFS.h>
#else
#include <InternalFileSystem.h>
#endif
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
struct LfsTraversalState {
lfs_size_t visited;
lfs_size_t block_count;
};
int _countLfsBlock(void *p, lfs_block_t block){
LfsTraversalState* state = (LfsTraversalState*)p;
// Valid blocks are [0, block_count). A traversal longer than block_count
// indicates a metadata cycle even if each individual block number is valid.
if (block >= state->block_count || state->visited >= state->block_count) {
MESH_DEBUG_PRINTLN("ERROR: LittleFS traversal out of bounds/cyclic at block %lu",
(unsigned long)block);
return LFS_ERR_CORRUPT;
}
state->visited++;
return 0;
}
lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) {
LfsTraversalState state = {0, fs->_getFS()->cfg->block_count};
int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &state);
if (err) {
MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err);
return -1;
}
return state.visited;
}
static bool validateLfsFilesystem(FILESYSTEM* fs) {
return fs != nullptr && _getLfsUsedBlockCount(fs) >= 0;
}
#endif
#if defined(NRF52_PLATFORM)
static void cleanupAtomicTempFiles(FILESYSTEM* fs) {
if (fs == nullptr) return;
static const char* fixed_temp_paths[] = {
"/_main.id.tmp", "/new_prefs.tmp", "/channels2.tmp",
"/contacts3.tmp", "/contacts4.mig.tmp", "/adv_blobs.tmp",
"/.extrafs.mig.tmp"};
for (size_t i = 0; i < sizeof(fixed_temp_paths) / sizeof(fixed_temp_paths[0]); i++) {
if (fs->exists(fixed_temp_paths[i])) fs->remove(fixed_temp_paths[i]);
}
// Include the old ten-bucket range as well as the current five-bucket
// layout so interrupted preview builds cannot strand full LittleFS blocks.
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[28];
snprintf(path, sizeof(path), "/contacts4_%02u.tmp", (unsigned)page);
if (fs->exists(path)) fs->remove(path);
}
for (uint8_t bucket = 0; bucket < 10; bucket++) {
char path[24];
snprintf(path, sizeof(path), "/adv4_%02u.tmp", (unsigned)bucket);
if (fs->exists(path)) fs->remove(path);
}
}
#if defined(EXTRAFS) && !defined(QSPIFLASH)
bool DataStore::recoverInternalExtraFSOnBoot() {
if (_configuredFsExtra == nullptr) return false;
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
if (_primary_storage_unavailable
|| !mesh::storage::isExpectedInternalExtraFsGeometry(
extra->getFlashAddr(), extra->getFlashSize(), extra->getBlockSize())
|| !mesh::storage::isInternalExtraFsReservedByApplication(
(uint32_t)(uintptr_t)__flash_arduino_end)) {
disableSecondaryFS(true);
MESH_DEBUG_PRINTLN("DataStore: refusing automatic ExtraFS recovery outside reserved 100 KiB region");
return false;
}
const mesh::storage::InternalSecondaryFsRecoveryResult result =
mesh::storage::recoverInternalSecondaryFilesystem(
[this]() -> bool {
return _fsExtra == _configuredFsExtra
&& validateLfsFilesystem(_fsExtra);
},
[this, extra]() -> bool {
MESH_DEBUG_PRINTLN("DataStore: retrying internal ExtraFS mount before recovery");
_fsExtra = nullptr;
extra->end();
if (!extra->Adafruit_LittleFS::begin()) return false;
_fsExtra = _configuredFsExtra;
return true;
},
[this]() -> bool {
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS remains unusable; rebuilding 100 KiB (secondary-only data may be lost)");
return reinitializeInternalExtraFS();
});
if (result == mesh::storage::InternalSecondaryFsRecoveryResult::Failed) {
disableSecondaryFS(true);
return false;
}
// This runs only before migration and user-data loading. Clear the initial
// mount quarantine, not errors from a later incomplete contact/prefs load.
// Migration below establishes authority again and can re-latch any error.
_secondary_authority_unknown = false;
_contact_load_incomplete = false;
_identity_creation_blocked = false;
_prefs_load_incomplete = false;
return true;
}
bool DataStore::reinitializeInternalExtraFS() {
if (_configuredFsExtra == nullptr) return false;
CustomLFS* extra = static_cast<CustomLFS*>(_configuredFsExtra);
if (!mesh::storage::isExpectedInternalExtraFsGeometry(
extra->getFlashAddr(), extra->getFlashSize(),
extra->getBlockSize())
|| !mesh::storage::isInternalExtraFsReservedByApplication(
(uint32_t)(uintptr_t)__flash_arduino_end)) {
_fsExtra = nullptr;
MESH_DEBUG_PRINTLN("DataStore: refusing internal ExtraFS repair with unexpected geometry");
return false;
}
const mesh::storage::InternalSecondaryFsRepairResult result =
mesh::storage::repairInternalSecondaryFilesystem(
[extra]() -> bool {
// Avoid CustomLFS::formatRegion(): it probes corrupt metadata with
// open("/") before deciding whether to unmount. end() is safe for
// both mounted and unmounted Adafruit LittleFS instances.
extra->end();
return extra->format();
},
[extra]() -> bool {
// Call the base mount explicitly. CustomLFS::begin() would erase
// the complete region again if this mount failed.
return extra->Adafruit_LittleFS::begin();
},
[this]() -> bool {
return validateLfsFilesystem(_configuredFsExtra);
});
if (result != mesh::storage::InternalSecondaryFsRepairResult::Repaired) {
_fsExtra = nullptr;
switch (result) {
case mesh::storage::InternalSecondaryFsRepairResult::FormatFailed:
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair format failed");
break;
case mesh::storage::InternalSecondaryFsRepairResult::MountFailed:
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair mount failed");
break;
case mesh::storage::InternalSecondaryFsRepairResult::ValidationFailed:
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repair validation failed");
break;
default:
break;
}
return false;
}
_fsExtra = _configuredFsExtra;
MESH_DEBUG_PRINTLN("DataStore: internal ExtraFS repaired and reactivated");
return true;
}
#endif
#endif
void DataStore::markPrimaryFSUnavailable() {
#if defined(NRF52_PLATFORM)
_primary_storage_unavailable = true;
_identity_creation_blocked = true;
_contact_load_incomplete = true;
_prefs_load_incomplete = true;
#endif
}
void DataStore::disableSecondaryFS(bool authority_unknown) {
_fsExtra = nullptr;
#if defined(NRF52_PLATFORM)
if (authority_unknown) {
// Without reading the secondary journal, primary may be a retired source
// rather than an empty fresh-install store. Keep this fact across begin()'s
// runtime-state reset and block every authority-changing write.
_secondary_authority_unknown = true;
_contact_load_incomplete = true;
_identity_creation_blocked = true;
_prefs_load_incomplete = true;
}
#else
(void)authority_unknown;
#endif
}
uint32_t DataStore::getStorageUsedKb() const {
#if defined(ESP32)
return SPIFFS.usedBytes() / 1024;
#elif defined(RP2040_PLATFORM)
FSInfo info;
info.usedBytes = 0;
_fs->info(info);
return info.usedBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
if (usedBlockCount < 0) return 0;
int usedBytes = config->block_size * usedBlockCount;
return usedBytes / 1024;
#else
return 0;
#endif
}
uint32_t DataStore::getStorageTotalKb() const {
#if defined(ESP32)
return SPIFFS.totalBytes() / 1024;
#elif defined(RP2040_PLATFORM)
FSInfo info;
info.totalBytes = 0;
_fs->info(info);
return info.totalBytes / 1024;
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
int totalBytes = config->block_size * config->block_count;
return totalBytes / 1024;
#else
return 0;
#endif
}
File DataStore::openRead(const char* filename) {
return openRead(_fs, filename);
}
#if COMPANION_FEATURE_JOHN
namespace {
class JohnBookmarkFiles {
FILESYSTEM* _fs;
public:
explicit JohnBookmarkFiles(FILESYSTEM* fs) : _fs(fs) {}
bool exists(const char* path) {
#if defined(NRF52_PLATFORM)
bool present = false;
// Unknown presence must take the read/abort path, never the new-file path.
return !contactPathPresence(_fs, path, present) || present;
#else
return _fs->exists(path);
#endif
}
bool remove(const char* path) {
#if defined(NRF52_PLATFORM)
bool present = false;
if (!contactPathPresence(_fs, path, present)) return false;
return !present || _fs->remove(path);
#else
return !exists(path) || _fs->remove(path);
#endif
}
bool rename(const char* from, const char* to) { return _fs->rename(from, to); }
bool read(const char* path, uint8_t (&data)[mesh::bible::kBookmarkBytes]) {
File file = mesh::openFileRead(_fs, path);
if (!file) return false;
if (file.size() != sizeof(data)) {
// Known malformed record, not an I/O failure. Let the codec reject it
// and permit a later verified replacement instead of trapping resume.
memset(data, 0, sizeof(data));
file.close();
return true;
}
const bool ok = file.read(data, sizeof(data)) == sizeof(data);
file.close();
return ok;
}
bool write(const char* path, const uint8_t (&data)[mesh::bible::kBookmarkBytes]) {
File file = openWrite(_fs, path);
if (!file) return false;
const bool ok = file.write(data, sizeof(data)) == sizeof(data);
file.flush();
file.close();
return ok;
}
};
} // namespace
bool DataStore::loadJohnBookmark(mesh::bible::Position& pos) {
pos = mesh::bible::Position{};
#if defined(NRF52_PLATFORM)
if (_primary_storage_unavailable) return false;
#endif
JohnBookmarkFiles files(_fs);
return mesh::bible::loadReaderBookmark(files, pos);
}
bool DataStore::saveJohnBookmark(mesh::bible::Position pos) {
#if defined(NRF52_PLATFORM)
if (_primary_storage_unavailable) return false;
#endif
JohnBookmarkFiles files(_fs);
return mesh::bible::saveReaderBookmark(files, pos);
}
#endif
File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
return mesh::openFileRead(fs, filename);
}
File DataStore::openDirectory(const char* path) {
return openDirectory(_fs, path);
}
File DataStore::openDirectory(FILESYSTEM* fs, const char* path) {
if (fs == nullptr || path == nullptr) return mesh::emptyFile(fs);
// SPIFFS has virtual directories: do not use the regular-file existence
// check here. Only the explicit listing API may return directory handles.
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
File directory = fs->open(path, FILE_O_READ);
#elif defined(RP2040_PLATFORM)
File directory = fs->open(path, "r");
#else
File directory = fs->open(path, "r", false);
#endif
if (directory && !directory.isDirectory()) {
directory.close();
return mesh::emptyFile(fs);
}
return directory;
}
bool DataStore::removeFile(const char* filename) {
return _fs->remove(filename);
}
bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
return fs->remove(filename);
}
bool DataStore::formatFileSystem() {
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
_contact_path_reader.close();
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#if defined(NRF52_PLATFORM)
resetContactPageState();
#endif
const bool primary_success = _fs->format();
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
// Factory reset/rebuild is already an explicit destructive operation. Use
// the configured pointer so it also clears and reactivates an ExtraFS which
// normal boot deliberately quarantined after failed traversal validation.
const bool secondary_success = _configuredFsExtra == nullptr
|| reinitializeInternalExtraFS();
#else
const bool secondary_success = _fsExtra == nullptr || _fsExtra->format();
#endif
const bool success = primary_success && secondary_success;
#if defined(NRF52_PLATFORM)
// A successful explicit erase creates a new empty authoritative store. A
// failed erase must keep the boot's load quarantine latched.
if (success) {
_contact_load_incomplete = false;
#if MESH_CONTACT_CACHE
_cache_load_incomplete = false;
#endif
_identity_creation_blocked = false;
_prefs_load_incomplete = false;
}
#endif
return success;
#elif defined(RP2040_PLATFORM)
return LittleFS.format();
#elif defined(ESP32)
bool fs_success = ((fs::SPIFFSFS *)_fs)->format();
esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot
return fs_success && (nvs_err == ESP_OK);
#else
#error "need to implement format()"
#endif
}
bool DataStore::repairInternalExtraFS() {
#if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH)
// A healthy active ExtraFS must never be erased. Re-running the explicit
// command is still useful, though: it retries any verified migration which
// failed after a previously repaired filesystem was activated.
if (_fsExtra != nullptr) {
CustomLFS* extra = static_cast<CustomLFS*>(_fsExtra);
if (_fsExtra != _configuredFsExtra
|| !mesh::storage::isExpectedInternalExtraFsGeometry(
extra->getFlashAddr(), extra->getFlashSize(), extra->getBlockSize())
|| !mesh::storage::isInternalExtraFsReservedByApplication(
(uint32_t)(uintptr_t)__flash_arduino_end)) {
MESH_DEBUG_PRINTLN("DataStore: refusing internal ExtraFS migration with unexpected geometry");
return false;
}
cleanupAtomicTempFiles(_fsExtra);
return migrateToSecondaryFS();
}
if (!reinitializeInternalExtraFS()) return false;
cleanupAtomicTempFiles(_fsExtra);
// The fallback primary remains intact until each destination file has been
// copied, read back byte-for-byte, and committed by migrateToSecondaryFS().
return migrateToSecondaryFS();
#else
return false;
#endif
}
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
#if defined(NRF52_PLATFORM)
if (_primary_storage_unavailable) return false;
bool identity_exists = false;
if (!contactPathPresence(_fs, "/_main.id", identity_exists)) {
_identity_creation_blocked = true;
return false;
}
if (!identity_exists) return false;
if (!identity_store.load("_main", identity)) {
// A present identity which cannot be read or decoded is not a fresh
// install. Never replace it with a newly generated key this boot.
_identity_creation_blocked = true;
return false;
}
// A verified primary identity is canonical even when only contact/channel
// authority on an unavailable secondary remains unknown.
_identity_creation_blocked = false;
return true;
#else
return identity_store.load("_main", identity);
#endif
}
bool DataStore::canCreateMainIdentity() const {
#if defined(NRF52_PLATFORM)
return !_identity_creation_blocked;
#else
return true;
#endif
}
bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
#if defined(NRF52_PLATFORM)
if (_identity_creation_blocked) return false;
#endif
return identity_store.save("_main", identity);
}
bool DataStore::loadPrefs(CompanionNodePrefs& prefs, double& node_lat,
double& node_lon) {
#if defined(NRF52_PLATFORM)
if (_primary_storage_unavailable
|| (_prefs_load_incomplete && !_secondary_authority_unknown)) {
_prefs_load_incomplete = true;
return false;
}
bool new_prefs_exists = false;
if (!contactPathPresence(_fs, "/new_prefs", new_prefs_exists)) {
_prefs_load_incomplete = true;
return false;
}
if (new_prefs_exists) {
const bool loaded = loadPrefsInt(
"/new_prefs", prefs, node_lat, node_lon);
_prefs_load_incomplete = !loaded;
return loaded;
}
bool legacy_prefs_exists = false;
if (!contactPathPresence(_fs, "/node_prefs", legacy_prefs_exists)) {
_prefs_load_incomplete = true;
return false;
}
if (!legacy_prefs_exists) {
if (_secondary_authority_unknown) {
_prefs_load_incomplete = true;
return false;
}
return true;
}
#else
if (_fs->exists("/new_prefs")) {
return loadPrefsInt("/new_prefs", prefs, node_lat, node_lon);
}
if (!_fs->exists("/node_prefs")) return true;
#endif
if (!loadPrefsInt("/node_prefs", prefs, node_lat, node_lon)) {
#if defined(NRF52_PLATFORM)
_prefs_load_incomplete = true;
#endif
return false;
}
#if defined(NRF52_PLATFORM)
// A verified primary legacy image is authoritative even if an unavailable
// secondary might contain a duplicate. Permit its atomic in-place upgrade.
_prefs_load_incomplete = false;
#endif
if (savePrefs(prefs, node_lat, node_lon)) {
_fs->remove("/node_prefs"); // remove old only after verified replacement
}
return true;
}
bool DataStore::loadPrefsInt(const char *filename,
CompanionNodePrefs& _prefs, double& node_lat,
double& node_lon) {
File file = openRead(_fs, filename);
if (file) {
CompanionNodePrefs loaded_prefs = _prefs;
double loaded_lat = node_lat;
double loaded_lon = node_lon;
// The original image ended after ble_pin at byte 84. Later releases only
// appended fields, sometimes as an indivisible group. Accept every format
// actually emitted by those releases, but reject a truncated field/group
// or an unknown tail before any value reaches the live preferences.
static const uint32_t MIN_PREFS_SIZE = 84;
static const uint32_t KNOWN_PREFS_SIZES[] = {
84, 85, 90, 91, 92, 93, 140, 141, 142, 143, 144, 155,
156, 157, 158, 159,
159 + sizeof(loaded_prefs.bluetooth_name),
161 + sizeof(loaded_prefs.bluetooth_name),
168 + sizeof(loaded_prefs.bluetooth_name),
168 + sizeof(loaded_prefs.bluetooth_name)
+ sizeof(loaded_prefs.bluetooth_mac_mode)
+ sizeof(loaded_prefs.bluetooth_mac),
168 + sizeof(loaded_prefs.bluetooth_name)
+ sizeof(loaded_prefs.bluetooth_mac_mode)
+ sizeof(loaded_prefs.bluetooth_mac)
+ sizeof(loaded_prefs.bluetooth_stealth_peer_type)
+ sizeof(loaded_prefs.bluetooth_stealth_peer),
168 + sizeof(loaded_prefs.bluetooth_name)
+ sizeof(loaded_prefs.bluetooth_mac_mode)
+ sizeof(loaded_prefs.bluetooth_mac)
+ sizeof(loaded_prefs.bluetooth_stealth_peer_type)
+ sizeof(loaded_prefs.bluetooth_stealth_peer)
+ sizeof(loaded_prefs.bluetooth_stealth_mode),
};
const uint32_t prefs_size = file.size();
bool known_size = false;
for (size_t i = 0;
i < sizeof(KNOWN_PREFS_SIZES) / sizeof(KNOWN_PREFS_SIZES[0]); i++) {
if (prefs_size == KNOWN_PREFS_SIZES[i]) {
known_size = true;
break;
}
}
if (prefs_size < MIN_PREFS_SIZE || !known_size) {
file.close();
return false;
}
bool success = true;
auto readField = [&file, &success](void* dest, size_t size) -> bool {
if (!success
|| file.read(static_cast<uint8_t*>(dest), size) != size) {
success = false;
return false;
}
return true;
};
auto readOptionalField = [&file, &readField](void* dest,
size_t size) -> bool {
return file.available() == 0 || readField(dest, size);
};
uint8_t pad[8];
readField(&loaded_prefs.airtime_factor, sizeof(float)); // 0
readField(loaded_prefs.node_name, sizeof(loaded_prefs.node_name)); // 4
readField(pad, 4); // 36
readField(&loaded_lat, sizeof(loaded_lat)); // 40
readField(&loaded_lon, sizeof(loaded_lon)); // 48
readField(&loaded_prefs.freq, sizeof(loaded_prefs.freq)); // 56
readField(&loaded_prefs.sf, sizeof(loaded_prefs.sf)); // 60
readField(&loaded_prefs.cr, sizeof(loaded_prefs.cr)); // 61
readField(&loaded_prefs.client_repeat, sizeof(loaded_prefs.client_repeat)); // 62
readField(&loaded_prefs.manual_add_contacts, sizeof(loaded_prefs.manual_add_contacts)); // 63
readField(&loaded_prefs.bw, sizeof(loaded_prefs.bw)); // 64
readField(&loaded_prefs.tx_power_dbm, sizeof(loaded_prefs.tx_power_dbm)); // 68
readField(&loaded_prefs.telemetry_mode_base, sizeof(loaded_prefs.telemetry_mode_base)); // 69
readField(&loaded_prefs.telemetry_mode_loc, sizeof(loaded_prefs.telemetry_mode_loc)); // 70
readField(&loaded_prefs.telemetry_mode_env, sizeof(loaded_prefs.telemetry_mode_env)); // 71
readField(&loaded_prefs.rx_delay_base, sizeof(loaded_prefs.rx_delay_base)); // 72
readField(&loaded_prefs.advert_loc_policy, sizeof(loaded_prefs.advert_loc_policy)); // 76
readField(&loaded_prefs.multi_acks, sizeof(loaded_prefs.multi_acks)); // 77
readField(&loaded_prefs.path_hash_mode, sizeof(loaded_prefs.path_hash_mode)); // 78
readField(pad, 1); // 79
readField(&loaded_prefs.ble_pin, sizeof(loaded_prefs.ble_pin)); // 80
readOptionalField(&loaded_prefs.buzzer_quiet,
sizeof(loaded_prefs.buzzer_quiet)); // 84
readOptionalField(&loaded_prefs.gps_enabled,
sizeof(loaded_prefs.gps_enabled)); // 85
readOptionalField(&loaded_prefs.gps_interval,
sizeof(loaded_prefs.gps_interval)); // 86
readOptionalField(&loaded_prefs.autoadd_config,
sizeof(loaded_prefs.autoadd_config)); // 90
readOptionalField(&loaded_prefs.autoadd_max_hops,
sizeof(loaded_prefs.autoadd_max_hops)); // 91
readOptionalField(&loaded_prefs.rx_boosted_gain,
sizeof(loaded_prefs.rx_boosted_gain)); // 92
readOptionalField(loaded_prefs.default_scope_name,
sizeof(loaded_prefs.default_scope_name)); // 93
readOptionalField(loaded_prefs.default_scope_key,
sizeof(loaded_prefs.default_scope_key)); // 124
readOptionalField(&loaded_prefs.radio_fem_rxgain,
sizeof(loaded_prefs.radio_fem_rxgain)); // 140
readOptionalField(&loaded_prefs.radio_fem_rxgain_override,
sizeof(loaded_prefs.radio_fem_rxgain_override)); // 141
readOptionalField(&loaded_prefs.vibe_quiet,
sizeof(loaded_prefs.vibe_quiet)); // 142
readOptionalField(&loaded_prefs.radio_fem_txgain,
sizeof(loaded_prefs.radio_fem_txgain)); // 143
readOptionalField(&loaded_prefs.rx_powersaving_enabled,
sizeof(loaded_prefs.rx_powersaving_enabled)); // 144
readOptionalField(&loaded_prefs.rx_ps_rx_us,
sizeof(loaded_prefs.rx_ps_rx_us)); // 145
readOptionalField(&loaded_prefs.rx_ps_sleep_us,
sizeof(loaded_prefs.rx_ps_sleep_us)); // 149
readOptionalField(&loaded_prefs.rx_ps_level,
sizeof(loaded_prefs.rx_ps_level)); // 153
readOptionalField(&loaded_prefs.rx_ps_preamble,
sizeof(loaded_prefs.rx_ps_preamble)); // 154
readOptionalField(&loaded_prefs.powersaving_enabled,
sizeof(loaded_prefs.powersaving_enabled)); // 155
readOptionalField(&loaded_prefs.wifi_enabled,
sizeof(loaded_prefs.wifi_enabled)); // 156
readOptionalField(&loaded_prefs.powersaving_policy_version,
sizeof(loaded_prefs.powersaving_policy_version)); // 157
readOptionalField(&loaded_prefs.usb_logging_enabled,
sizeof(loaded_prefs.usb_logging_enabled)); // 158
readOptionalField(loaded_prefs.bluetooth_name,
sizeof(loaded_prefs.bluetooth_name)); // 159
readOptionalField(&loaded_prefs.display_rotation_degrees,
sizeof(loaded_prefs.display_rotation_degrees)); // 191
readOptionalField(&loaded_prefs.cad_enabled,
sizeof(loaded_prefs.cad_enabled)); // 193
readOptionalField(&loaded_prefs.cad_scan_timeout_ms,
sizeof(loaded_prefs.cad_scan_timeout_ms)); // 194
readOptionalField(&loaded_prefs.cad_retry_delay_ms,
sizeof(loaded_prefs.cad_retry_delay_ms)); // 196
readOptionalField(&loaded_prefs.cad_max_duration_ms,
sizeof(loaded_prefs.cad_max_duration_ms)); // 198
readOptionalField(&loaded_prefs.bluetooth_mac_mode,
sizeof(loaded_prefs.bluetooth_mac_mode)); // 200
readOptionalField(loaded_prefs.bluetooth_mac,
sizeof(loaded_prefs.bluetooth_mac)); // 201
readOptionalField(&loaded_prefs.bluetooth_stealth_peer_type,
sizeof(loaded_prefs.bluetooth_stealth_peer_type)); // 207
readOptionalField(loaded_prefs.bluetooth_stealth_peer,
sizeof(loaded_prefs.bluetooth_stealth_peer)); // 208
readOptionalField(&loaded_prefs.bluetooth_stealth_mode,
sizeof(loaded_prefs.bluetooth_stealth_mode)); // 214
// Any bytes left over form only part of a historically appended field.
// Preserve the file and defaults rather than treating that tail as EOF.
success = success && file.available() == 0;
file.close();
if (!success) return false;
_prefs = loaded_prefs;
node_lat = loaded_lat;
node_lon = loaded_lon;
return true;
}
return false;
}
bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, double node_lon) {
#if defined(NRF52_PLATFORM)
if (_prefs_load_incomplete) return false;
if (_primary_storage_unavailable) return false;
mesh::AtomicFileWriter file(_fs, "/new_prefs");
#else
File file = openWrite(_fs, "/new_prefs");
#endif
if (file) {
uint8_t pad[8];
memset(pad, 0, sizeof(pad));
bool success = file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)) == sizeof(float); // 0
success = success && file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)) == sizeof(_prefs.node_name); // 4
success = success && file.write(pad, 4) == 4; // 36
success = success && file.write((uint8_t *)&node_lat, sizeof(node_lat)) == sizeof(node_lat); // 40
success = success && file.write((uint8_t *)&node_lon, sizeof(node_lon)) == sizeof(node_lon); // 48
success = success && file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)) == sizeof(_prefs.freq); // 56
success = success && file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)) == sizeof(_prefs.sf); // 60
success = success && file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)) == sizeof(_prefs.cr); // 61
success = success && file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)) == sizeof(_prefs.client_repeat); // 62
success = success && file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)) == sizeof(_prefs.manual_add_contacts); // 63
success = success && file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)) == sizeof(_prefs.bw); // 64
success = success && file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)) == sizeof(_prefs.tx_power_dbm); // 68
success = success && file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)) == sizeof(_prefs.telemetry_mode_base); // 69
success = success && file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)) == sizeof(_prefs.telemetry_mode_loc); // 70
success = success && file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)) == sizeof(_prefs.telemetry_mode_env); // 71
success = success && file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)) == sizeof(_prefs.rx_delay_base); // 72
success = success && file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)) == sizeof(_prefs.advert_loc_policy); // 76
success = success && file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)) == sizeof(_prefs.multi_acks); // 77
success = success && file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)) == sizeof(_prefs.path_hash_mode); // 78
success = success && file.write(pad, 1) == 1; // 79
success = success && file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)) == sizeof(_prefs.ble_pin); // 80
success = success && file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)) == sizeof(_prefs.buzzer_quiet); // 84
success = success && file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)) == sizeof(_prefs.gps_enabled); // 85
success = success && file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)) == sizeof(_prefs.gps_interval); // 86
success = success && file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)) == sizeof(_prefs.autoadd_config); // 87
success = success && file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)) == sizeof(_prefs.autoadd_max_hops); // 88
success = success && file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)) == sizeof(_prefs.rx_boosted_gain); // 89
success = success && file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)) == sizeof(_prefs.default_scope_name); // 90
success = success && file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)) == sizeof(_prefs.default_scope_key); // 121
success = success && file.write((uint8_t *)&_prefs.radio_fem_rxgain, sizeof(_prefs.radio_fem_rxgain)) == sizeof(_prefs.radio_fem_rxgain); // 122
success = success && file.write((uint8_t *)&_prefs.radio_fem_rxgain_override,
sizeof(_prefs.radio_fem_rxgain_override)) == sizeof(_prefs.radio_fem_rxgain_override); // 123
success = success && file.write((uint8_t *)&_prefs.vibe_quiet,
sizeof(_prefs.vibe_quiet)) == sizeof(_prefs.vibe_quiet); // 124
success = success && file.write((uint8_t *)&_prefs.radio_fem_txgain,
sizeof(_prefs.radio_fem_txgain)) == sizeof(_prefs.radio_fem_txgain); // 125
success = success && file.write((uint8_t *)&_prefs.rx_powersaving_enabled,
sizeof(_prefs.rx_powersaving_enabled)) == sizeof(_prefs.rx_powersaving_enabled); // 126
success = success && file.write((uint8_t *)&_prefs.rx_ps_rx_us,
sizeof(_prefs.rx_ps_rx_us)) == sizeof(_prefs.rx_ps_rx_us); // 127
success = success && file.write((uint8_t *)&_prefs.rx_ps_sleep_us,
sizeof(_prefs.rx_ps_sleep_us)) == sizeof(_prefs.rx_ps_sleep_us); // 131
success = success && file.write((uint8_t *)&_prefs.rx_ps_level,
sizeof(_prefs.rx_ps_level)) == sizeof(_prefs.rx_ps_level); // 135
success = success && file.write((uint8_t *)&_prefs.rx_ps_preamble,
sizeof(_prefs.rx_ps_preamble)) == sizeof(_prefs.rx_ps_preamble); // 136
success = success && file.write((uint8_t *)&_prefs.powersaving_enabled,
sizeof(_prefs.powersaving_enabled)) == sizeof(_prefs.powersaving_enabled); // 137
success = success && file.write((uint8_t *)&_prefs.wifi_enabled,
sizeof(_prefs.wifi_enabled)) == sizeof(_prefs.wifi_enabled); // 138
success = success && file.write((uint8_t *)&_prefs.powersaving_policy_version,
sizeof(_prefs.powersaving_policy_version))
== sizeof(_prefs.powersaving_policy_version); // 139
success = success && file.write((uint8_t *)&_prefs.usb_logging_enabled,
sizeof(_prefs.usb_logging_enabled))
== sizeof(_prefs.usb_logging_enabled); // 140
success = success && file.write((uint8_t *)_prefs.bluetooth_name,
sizeof(_prefs.bluetooth_name)) == sizeof(_prefs.bluetooth_name); // 141
success = success && file.write(
(uint8_t *)&_prefs.display_rotation_degrees,
sizeof(_prefs.display_rotation_degrees))
== sizeof(_prefs.display_rotation_degrees);
success = success && file.write((uint8_t *)&_prefs.cad_enabled,
sizeof(_prefs.cad_enabled)) == sizeof(_prefs.cad_enabled);
success = success && file.write((uint8_t *)&_prefs.cad_scan_timeout_ms,
sizeof(_prefs.cad_scan_timeout_ms))
== sizeof(_prefs.cad_scan_timeout_ms);
success = success && file.write((uint8_t *)&_prefs.cad_retry_delay_ms,
sizeof(_prefs.cad_retry_delay_ms))
== sizeof(_prefs.cad_retry_delay_ms);
success = success && file.write((uint8_t *)&_prefs.cad_max_duration_ms,
sizeof(_prefs.cad_max_duration_ms))
== sizeof(_prefs.cad_max_duration_ms);
success = success && file.write((uint8_t *)&_prefs.bluetooth_mac_mode,
sizeof(_prefs.bluetooth_mac_mode))
== sizeof(_prefs.bluetooth_mac_mode);
success = success && file.write((uint8_t *)_prefs.bluetooth_mac,
sizeof(_prefs.bluetooth_mac)) == sizeof(_prefs.bluetooth_mac);
success = success && file.write(
(uint8_t *)&_prefs.bluetooth_stealth_peer_type,
sizeof(_prefs.bluetooth_stealth_peer_type))
== sizeof(_prefs.bluetooth_stealth_peer_type);
success = success && file.write(
(uint8_t *)_prefs.bluetooth_stealth_peer,
sizeof(_prefs.bluetooth_stealth_peer))
== sizeof(_prefs.bluetooth_stealth_peer);
success = success && file.write(
(uint8_t *)&_prefs.bluetooth_stealth_mode,
sizeof(_prefs.bluetooth_stealth_mode))
== sizeof(_prefs.bluetooth_stealth_mode);
#if defined(NRF52_PLATFORM)
success = file.commit(success);
if (!success) MESH_DEBUG_PRINTLN("DataStore: atomic preferences write failed");
#else
file.close();
#endif
return success;
}
return false;
}
static bool serializeContactRecord(const ContactInfo& c,
uint8_t out[mesh::storage::CONTACT_RECORD_SIZE]) {
size_t offset = 0;
const uint8_t unused = 0;
memcpy(&out[offset], c.id.pub_key, 32); offset += 32;
memcpy(&out[offset], c.name, 32); offset += 32;
out[offset++] = c.type;
out[offset++] = c.flags;
out[offset++] = unused;
memcpy(&out[offset], &c.sync_since, 4); offset += 4;
out[offset++] = c.out_path_len;
memcpy(&out[offset], &c.last_advert_timestamp, 4); offset += 4;
if (!c.copyPathTo(&out[offset])) return false;
offset += 64;
memcpy(&out[offset], &c.lastmod, 4); offset += 4;
memcpy(&out[offset], &c.gps_lat, 4); offset += 4;
memcpy(&out[offset], &c.gps_lon, 4);
return true;
}
static bool deserializeContactRecord(
const uint8_t in[mesh::storage::CONTACT_RECORD_SIZE], ContactInfo& c,
uint16_t path_source, bool* path_unavailable = nullptr) {
if (path_unavailable) *path_unavailable = false;
size_t offset = 0;
uint8_t pub_key[32];
memcpy(pub_key, &in[offset], 32); offset += 32;
memcpy(c.name, &in[offset], 32); offset += 32;
c.name[sizeof(c.name) - 1] = 0;
c.type = in[offset++];
c.flags = in[offset++];
offset++; // reserved
memcpy(&c.sync_since, &in[offset], 4); offset += 4;
c.out_path_len = in[offset++];
memcpy(&c.last_advert_timestamp, &in[offset], 4); offset += 4;
#if MESH_CONTACT_CACHE
if (!c.path_ref.bind(path_source, &in[offset])) {
if (path_unavailable) *path_unavailable = true;
return false;
}
#else
(void)path_source;
if (!c.setRawPath(&in[offset])) return false;
#endif
offset += 64;
memcpy(&c.lastmod, &in[offset], 4); offset += 4;
memcpy(&c.gps_lat, &in[offset], 4); offset += 4;
memcpy(&c.gps_lon, &in[offset], 4);
c.id = mesh::Identity(pub_key);
c.shared_secret_valid = false;
return c.out_path_len == OUT_PATH_UNKNOWN
|| mesh::Packet::isValidPathLen(c.out_path_len);
}
#if defined(NRF52_PLATFORM)
static const char* CONTACT_MIGRATION_MARKER = "/contacts4.mig";
static const char* SECONDARY_MIGRATION_JOURNAL = "/.extrafs.mig";
static const uint8_t SECONDARY_MIGRATION_PENDING = 1;
static const uint8_t SECONDARY_MIGRATION_COMMITTED = 2;
#if defined(MESHCORE_EXTRAFS_HIL)
static const char* HIL_CONTACT_PAGE_FAILURE_MARKER = "/__hil.readfail";
static const uint8_t HIL_CONTACT_STAT_FAILURE_FLAG = 0x80;
#endif
static mesh::storage::ContactPathState statContactPath(
FILESYSTEM* fs, const char* path, uint32_t* size) {
if (fs == nullptr || path == nullptr) {
return mesh::storage::ContactPathState::IO_ERROR;
}
struct lfs_info info;
fs->_lockFS();
const int result = lfs_stat(fs->_getFS(), path, &info);
fs->_unlockFS();
const mesh::storage::ContactPathState state =
mesh::storage::classifyContactPathStat(result, LFS_ERR_NOENT);
if (state == mesh::storage::ContactPathState::PRESENT && size != nullptr) {
*size = info.size;
}
if (state == mesh::storage::ContactPathState::IO_ERROR) {
MESH_DEBUG_PRINTLN(
"DataStore: storage path stat failed for %s: %d", path, result);
}
return state;
}
static bool contactPathPresence(FILESYSTEM* fs, const char* path,
bool& present, uint32_t* size) {
const mesh::storage::ContactPathState state = statContactPath(fs, path, size);
present = state == mesh::storage::ContactPathState::PRESENT;
return state != mesh::storage::ContactPathState::IO_ERROR;
}
static void makeContactPagePath(uint8_t page, char path[24]) {
snprintf(path, 24, "/contacts4_%02u", (unsigned)page);
}
static void discardInvalidContactPage(FILESYSTEM* fs, const char* path,
uint8_t page) {
// The page has already failed size/format/CRC validation and cannot be a
// recovery source. Remove it so the atomic replacement only needs one free
// page; retaining full-size .bad copies can otherwise exhaust a 100 KiB
// ExtraFS and make self-repair impossible.
if (!fs->remove(path)) {
MESH_DEBUG_PRINTLN("DataStore: could not remove invalid contact page %u", page);
}
}
void DataStore::resetContactPageState(bool clear_incomplete) {
_contact_slots.clear();
_dirty_contact_pages.clearAll();
_unread_contact_pages.clearAll();
if (clear_incomplete) {
_contact_load_incomplete = false;
#if MESH_CONTACT_CACHE
_cache_load_incomplete = false;
#endif
}
memset(_contact_page_generations, 0, sizeof(_contact_page_generations));
_legacy_contacts_pending_cleanup = false;
_legacy_migration_ready = false;
_legacy_contact_count = 0;
}
bool DataStore::prepareLegacyContactMigration() {
FILESYSTEM* fs = _getContactsChannelsFS();
bool marker_exists = false;
if (!contactPathPresence(fs, CONTACT_MIGRATION_MARKER, marker_exists)) {
return false;
}
if (marker_exists) {
_legacy_migration_ready = true;
return true;
}
// With no marker, page files can be leftovers from a newer firmware followed
// by a downgrade that rewrote /contacts3. The complete legacy file remains
// authoritative while these are removed, so a reset at any point is safe.
bool clean = true;
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[24];
makeContactPagePath(page, path);
bool path_exists = false;
if (!contactPathPresence(fs, path, path_exists)) return false;
if (path_exists && !fs->remove(path)) clean = false;
char temp_path[28];
snprintf(temp_path, sizeof(temp_path), "%s.tmp", path);
bool temp_exists = false;
if (!contactPathPresence(fs, temp_path, temp_exists)) return false;
if (temp_exists && !fs->remove(temp_path)) clean = false;
}
if (!clean) {
MESH_DEBUG_PRINTLN("DataStore: could not clear stale contact pages before migration");
return false;
}
mesh::AtomicFileWriter marker(fs, CONTACT_MIGRATION_MARKER);
_legacy_migration_ready = marker.commit(true);
if (!_legacy_migration_ready) {
MESH_DEBUG_PRINTLN("DataStore: could not create contact migration marker");
}
return _legacy_migration_ready;
}
bool DataStore::loadContactPages(DataStoreHost* host, uint16_t minimum_slot,
uint32_t expected_page_mask) {
bool any_page_file = false;
FILESYSTEM* fs = _getContactsChannelsFS();
auto quarantineUnreadPage = [this](uint8_t page) {
// Fail closed if an otherwise present page cannot be read. Reserving its
// slots prevents a new contact from overwriting records which a later boot
// may recover successfully.
for (uint8_t index = 0;
index < mesh::storage::CONTACTS_PER_PAGE; index++) {
_contact_slots.reserve(
(uint16_t)page * mesh::storage::CONTACTS_PER_PAGE + index);
}
_unread_contact_pages.mark(page);
_contact_load_incomplete = true;
};
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
if ((expected_page_mask & (1UL << page)) == 0) continue;
char path[24];
makeContactPagePath(page, path);
// The first source-selection pass proved this page existed. Do not ask
// exists() a second time and silently reinterpret a transient failure as
// an empty slot range; opening the captured page either succeeds or puts
// that range into fail-closed quarantine.
any_page_file = true;
#if defined(MESHCORE_EXTRAFS_HIL)
// Direct-attached HIL can request one fail-closed boot without damaging
// the page. Consume the marker before quarantine so the next reboot
// exercises normal recovery from the untouched source.
if (fs->exists(HIL_CONTACT_PAGE_FAILURE_MARKER)) {
File marker = openRead(fs, HIL_CONTACT_PAGE_FAILURE_MARKER);
const int fail_page = marker ? marker.read() : -1;
if (marker) marker.close();
if (fail_page == page && fs->remove(HIL_CONTACT_PAGE_FAILURE_MARKER)
&& !fs->exists(HIL_CONTACT_PAGE_FAILURE_MARKER)) {
MESH_DEBUG_PRINTLN(
"DataStore: HIL quarantining contact page %u", page);
quarantineUnreadPage(page);
continue;
}
}
#endif
File file = openRead(fs, path);
if (!file) {
MESH_DEBUG_PRINTLN("DataStore: contact page %u could not be opened", page);
quarantineUnreadPage(page);
continue;
}
if (file.size() != mesh::storage::CONTACT_PAGE_FILE_SIZE) {
MESH_DEBUG_PRINTLN("DataStore: ignoring invalid contact page %u", page);
file.close();
discardInvalidContactPage(fs, path, page);
_dirty_contact_pages.mark(page);
continue;
}
uint8_t raw_header[mesh::storage::CONTACT_PAGE_HEADER_SIZE];
mesh::storage::ContactPageHeader header;
const bool header_read =
file.read(raw_header, sizeof(raw_header)) == sizeof(raw_header);
const bool header_valid = header_read
&& mesh::storage::decodeContactPageHeader(raw_header, page, header);
if (!header_read) {
// A short read is not evidence that the atomically committed page is
// corrupt. Leave it untouched so a later boot can retry it.
MESH_DEBUG_PRINTLN(
"DataStore: contact page %u header could not be read", page);
file.close();
quarantineUnreadPage(page);
continue;
}
if (!header_valid) {
MESH_DEBUG_PRINTLN("DataStore: contact page %u failed CRC/format validation", page);
file.close();
discardInvalidContactPage(fs, path, page);
_dirty_contact_pages.mark(page);
continue;
}
// Keep a verified page snapshot in heap memory while constructing the RAM
// table. A second series of filesystem seeks could fail after only part of
// the page had been loaded; later rewriting that partial RAM view would
// permanently delete otherwise valid contacts.
uint8_t* payload =
(uint8_t*)malloc(mesh::storage::CONTACT_PAGE_PAYLOAD_SIZE);
if (payload == nullptr) {
MESH_DEBUG_PRINTLN(
"DataStore: no memory to load contact page %u", page);
file.close();
quarantineUnreadPage(page);
continue;
}
const bool payload_read =
file.read(payload, mesh::storage::CONTACT_PAGE_PAYLOAD_SIZE)
== mesh::storage::CONTACT_PAGE_PAYLOAD_SIZE;
file.close();
if (!payload_read) {
MESH_DEBUG_PRINTLN(
"DataStore: contact page %u payload could not be read", page);
free(payload);
quarantineUnreadPage(page);
continue;
}
const uint32_t crc = mesh::storage::updateCRC32(
0xFFFFFFFFUL, payload, mesh::storage::CONTACT_PAGE_PAYLOAD_SIZE);
if (crc != header.payload_crc) {
MESH_DEBUG_PRINTLN("DataStore: contact page %u failed CRC/format validation", page);
free(payload);
discardInvalidContactPage(fs, path, page);
_dirty_contact_pages.mark(page);
continue;
}
if ((header.occupied & ~mesh::storage::contactPageValidSlotMask()) != 0) {
MESH_DEBUG_PRINTLN(
"DataStore: repairing contact page %u high occupancy bits", page);
_dirty_contact_pages.mark(page);
}
_contact_page_generations[page] = header.generation;
for (uint8_t index = 0; index < mesh::storage::CONTACTS_PER_PAGE; index++) {
const uint16_t slot = (uint16_t)page * mesh::storage::CONTACTS_PER_PAGE + index;
const uint8_t* record =
&payload[(uint16_t)index * mesh::storage::CONTACT_RECORD_SIZE];
// The payload CRC covers every record, so occupancy can be reconstructed
// without trusting the separate header bitmask. This recovers a contact
// hidden by a flipped mask bit and ignores an empty slot exposed by one;
// the dirty page is later rewritten with the repaired mask.
const bool has_data = mesh::storage::contactRecordHasData(record);
const bool marked_occupied =
(header.occupied & (1UL << index)) != 0;
if (has_data != marked_occupied) {
MESH_DEBUG_PRINTLN(
"DataStore: repairing contact page %u occupancy slot %u",
page, index);
_dirty_contact_pages.mark(page);
}
if (!has_data
|| !mesh::storage::loadSlotFromMigratedPage(slot, minimum_slot)) {
continue;
}
ContactInfo contact;
bool path_unavailable = false;
const bool valid = deserializeContactRecord(record, contact,
0x8000 | slot, &path_unavailable);
if (path_unavailable) {
// Cache exhaustion is not a corrupt record. Keep the complete page
// on disk and refuse changes until it can be loaded after reboot.
_contact_load_incomplete = true;
_dirty_contact_pages.clearAll();
free(payload);
return true;
}
if (!valid || !_contact_slots.reserve(slot)) {
MESH_DEBUG_PRINTLN("DataStore: contact page %u slot %u is invalid/duplicate", page, index);
_dirty_contact_pages.mark(page);
continue;
}
contact.storage_slot = slot;
if (!host->onContactLoaded(contact)) {
_contact_slots.release(slot);
// A host capacity refusal is not a filesystem read error, but the RAM
// table is still not a complete representation of the durable page.
// Preserve the page and veto every mutation/rewrite until reboot.
MESH_DEBUG_PRINTLN(
"DataStore: contact host refused page %u slot %u; load incomplete",
page, index);
_contact_load_incomplete = true;
_dirty_contact_pages.clearAll();
free(payload);
return true;
}
}
free(payload);
}
if (hasIncompleteContactLoad()) {
// Occupancy/header repairs discovered elsewhere in this pass must wait for
// a clean reboot too. Rewriting any page from an incomplete contact table
// can turn a transient read failure into durable cross-page data loss.
_dirty_contact_pages.clearAll();
}
return any_page_file;
}
bool DataStore::writeContactPage(DataStoreHost* host, uint8_t page,
bool (*filter)(const ContactInfo& c)) {
if (page >= mesh::storage::CONTACT_PAGE_COUNT) return false;
// The nRF52 Arduino loop task has only a 4 KiB stack. Keep pointers to this
// page's contacts and stream one 152-byte record at a time instead of
// allocating the complete 3.8 KiB payload on that stack.
ContactInfo* page_contacts[mesh::storage::CONTACTS_PER_PAGE];
memset(page_contacts, 0, sizeof(page_contacts));
uint32_t occupied = 0;
for (uint32_t index = 0;; index++) {
ContactInfo* contact = host->getContactForStore(index);
if (contact == NULL) break;
if ((filter && !filter(*contact))
|| contact->storage_slot == mesh::storage::CONTACT_SLOT_NONE
|| contact->storage_slot / mesh::storage::CONTACTS_PER_PAGE != page) {
continue;
}
const uint8_t page_slot = contact->storage_slot % mesh::storage::CONTACTS_PER_PAGE;
page_contacts[page_slot] = contact;
occupied |= 1UL << page_slot;
}
#if MESH_CONTACT_CACHE
auto& paths = mesh::contactPathStorage();
paths.beginCommit();
for (auto* contact : page_contacts) if (contact) paths.mark(contact->path_ref.handle());
const uint16_t first_slot = page * mesh::storage::CONTACTS_PER_PAGE;
if (!paths.preserveSnapshots(0x8000 | first_slot, mesh::storage::CONTACTS_PER_PAGE)
|| !paths.preserveSnapshots(first_slot, mesh::storage::CONTACTS_PER_PAGE)) {
paths.endCommit(false);
return false;
}
#endif
uint32_t payload_crc = 0xFFFFFFFFUL;
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
for (uint8_t slot = 0; slot < mesh::storage::CONTACTS_PER_PAGE; slot++) {
memset(record, 0, sizeof(record));
if (page_contacts[slot] != nullptr) {
if (!serializeContactRecord(*page_contacts[slot], record)) {
#if MESH_CONTACT_CACHE
mesh::contactPathStorage().endCommit(false);
#endif
return false;
}
}
payload_crc = mesh::storage::updateCRC32(payload_crc, record, sizeof(record));
}
mesh::storage::ContactPageHeader header;
header.page_index = page;
header.occupied = occupied;
header.generation = _contact_page_generations[page] + 1;
header.payload_crc = payload_crc;
uint8_t raw_header[mesh::storage::CONTACT_PAGE_HEADER_SIZE];
mesh::storage::encodeContactPageHeader(raw_header, header);
char path[24];
makeContactPagePath(page, path);
mesh::AtomicFileWriter writer(_getContactsChannelsFS(), path);
bool wrote = writer
&& writer.write(raw_header, sizeof(raw_header)) == sizeof(raw_header);
for (uint8_t slot = 0; wrote && slot < mesh::storage::CONTACTS_PER_PAGE; slot++) {
memset(record, 0, sizeof(record));
if (page_contacts[slot] != nullptr) {
if (!serializeContactRecord(*page_contacts[slot], record)) {
#if MESH_CONTACT_CACHE
mesh::contactPathStorage().endCommit(false);
#endif
return false;
}
}
wrote = writer.write(record, sizeof(record)) == sizeof(record);
}
if (!writer.commit(wrote)) {
#if MESH_CONTACT_CACHE
paths.endCommit(false);
#endif
MESH_DEBUG_PRINTLN("DataStore: atomic contact page %u write failed", page);
return false;
}
#if MESH_CONTACT_CACHE
for (auto* contact : page_contacts) if (contact)
paths.publish(contact->path_ref.handle(), 0x8000 | contact->storage_slot);
paths.endCommit(true);
#endif
_contact_page_generations[page] = header.generation;
return true;
}
#endif
void DataStore::loadContacts(DataStoreHost* host) {
#if MESH_CONTACT_CACHE
_cache_host = host;
mesh::contactPathStorage().attach(this);
#if MESH_CONTACT_SECRET_FLASH_CACHE
mesh::contactSecretCache().attach(this);
#endif
if (_cache_load_incomplete) return;
#if defined(ESP32_PLATFORM)
_contact_path_reader.close();
if (!mesh::ContactFileTransaction::recover(_getContactsChannelsFS(), "/contacts3")) {
MESH_DEBUG_PRINTLN("DataStore: contact transaction recovery failed");
_cache_load_incomplete = true;
return;
}
#endif
#endif
#if defined(NRF52_PLATFORM)
// loadContacts() is also used after an identity import. Rebuild runtime
// slot ownership from disk so stale pointers/slots from the previous in-RAM
// contact table cannot collide with the reload.
resetContactPageState();
if (_contact_load_incomplete) {
// A failed load is latched for this boot. loadContacts() is also called by
// identity import, but only a reboot may retry storage and re-enable
// migration/mutation; otherwise an unrelated command could clear the veto.
MESH_DEBUG_PRINTLN(
"DataStore: contact load remains quarantined until reboot");
return;
}
FILESYSTEM* contacts_fs = _getContactsChannelsFS();
#if defined(MESHCORE_EXTRAFS_HIL)
// Reuse the read-failure marker's high bit for a one-boot source-discovery
// stat failure. Consume and verify the marker before injecting the error so
// the untouched page is recoverable on the very next reboot. Low-bit values
// remain armed for loadContactPages(), preserving the existing read hook.
int hil_stat_failure_page = -1;
bool hil_failure_marker_exists = false;
if (!contactPathPresence(contacts_fs, HIL_CONTACT_PAGE_FAILURE_MARKER,
hil_failure_marker_exists)) {
_contact_load_incomplete = true;
return;
}
if (hil_failure_marker_exists) {
File marker = openRead(contacts_fs, HIL_CONTACT_PAGE_FAILURE_MARKER);
const int encoded_failure = marker && marker.size() == 1
? marker.read() : -1;
if (marker) marker.close();
if (encoded_failure >= 0
&& (encoded_failure & HIL_CONTACT_STAT_FAILURE_FLAG) != 0) {
const uint8_t page = static_cast<uint8_t>(
encoded_failure & ~HIL_CONTACT_STAT_FAILURE_FLAG);
bool marker_remains = true;
const bool removed = contacts_fs->remove(
HIL_CONTACT_PAGE_FAILURE_MARKER);
const bool verified = contactPathPresence(
contacts_fs, HIL_CONTACT_PAGE_FAILURE_MARKER, marker_remains);
if (page >= mesh::storage::CONTACT_PAGE_COUNT || !removed || !verified
|| marker_remains) {
MESH_DEBUG_PRINTLN(
"DataStore: HIL stat-failure marker could not be consumed safely");
_contact_load_incomplete = true;
return;
}
hil_stat_failure_page = page;
}
}
#endif
uint32_t contact_page_presence = 0;
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[24];
makeContactPagePath(page, path);
bool page_exists = false;
#if defined(MESHCORE_EXTRAFS_HIL)
if (page == hil_stat_failure_page) {
MESH_DEBUG_PRINTLN(
"DataStore: HIL injecting contact page %u stat failure", page);
_contact_load_incomplete = true;
return;
}
#endif
if (!contactPathPresence(contacts_fs, path, page_exists)) {
_contact_load_incomplete = true;
return;
}
if (page_exists) {
contact_page_presence |= 1UL << page;
}
}
bool legacy_exists = false;
bool migration_marker_exists = false;
if (!contactPathPresence(contacts_fs, "/contacts3", legacy_exists)
|| !contactPathPresence(contacts_fs, CONTACT_MIGRATION_MARKER,
migration_marker_exists)) {
_contact_load_incomplete = true;
return;
}
const mesh::storage::ContactStoreSource source =
mesh::storage::chooseContactStoreSource(
legacy_exists, contact_page_presence != 0);
if (source == mesh::storage::ContactStoreSource::PAGED) {
loadContactPages(host, 0, contact_page_presence);
// A reset after the final legacy removal may leave this harmless marker.
// Retire it only after every page captured above loaded successfully; an
// unread page must leave all recovery metadata untouched for reboot retry.
if (!hasIncompleteContactLoad() && migration_marker_exists) {
contacts_fs->remove(CONTACT_MIGRATION_MARKER);
}
return;
}
if (source == mesh::storage::ContactStoreSource::EMPTY) {
if (migration_marker_exists) {
contacts_fs->remove(CONTACT_MIGRATION_MARKER);
}
return;
}
// Migrate /contacts3 from the tail so the legacy prefix and completed pages
// never need enough room to coexist in full. A page is committed first,
// then the corresponding legacy tail is truncated. On a reset between
// those operations the still-present legacy prefix wins overlapping slots.
_legacy_contacts_pending_cleanup = true;
_legacy_migration_ready = migration_marker_exists;
#endif
File file = openRead(_getContactsChannelsFS(), "/contacts3");
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
if (!file && _getContactsChannelsFS()->exists("/contacts3")) {
_cache_load_incomplete = true;
return;
}
#endif
#if defined(NRF52_PLATFORM)
if (!file) {
// Never delete, truncate, or write alongside a legacy source that could
// not be opened. A later reboot can retry the untouched authoritative
// file, but this boot must not accept a mutation into a competing page.
MESH_DEBUG_PRINTLN("DataStore: legacy contacts exist but could not be read");
_contact_load_incomplete = true;
return;
}
const size_t legacy_size = file.size();
if (!mesh::storage::isValidLegacyContactFileSize(legacy_size)) {
MESH_DEBUG_PRINTLN(
"DataStore: legacy contacts size %lu is invalid; preserving source",
(unsigned long)legacy_size);
file.close();
_contact_load_incomplete = true;
return;
}
#endif
if (file) {
bool full = false;
uint16_t record_index = 0;
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
if (file.size() % mesh::storage::CONTACT_RECORD_SIZE != 0) {
_cache_load_incomplete = true;
file.close();
return;
}
#endif
#if defined(NRF52_PLATFORM)
_legacy_contact_count =
mesh::storage::legacyContactCountForSize(legacy_size);
bool legacy_read_failed = false;
bool legacy_host_refused = false;
#endif
while (!full
#if MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
&& record_index < file.size() / mesh::storage::CONTACT_RECORD_SIZE
#endif
#if defined(NRF52_PLATFORM)
&& record_index < _legacy_contact_count
#endif
) {
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
if (file.read(record, sizeof(record)) != sizeof(record)) {
#if MESH_CONTACT_CACHE
_cache_load_incomplete = true;
#endif
#if defined(NRF52_PLATFORM)
legacy_read_failed = true;
#endif
break;
}
ContactInfo contact;
bool path_unavailable = false;
if (!deserializeContactRecord(record, contact, record_index, &path_unavailable)) {
if (path_unavailable) {
#if MESH_CONTACT_CACHE
_cache_load_incomplete = true;
#endif
#if defined(NRF52_PLATFORM)
legacy_read_failed = true;
#endif
break;
}
// Preserve the contact while containing corrupt legacy routing data.
contact.out_path_len = OUT_PATH_UNKNOWN;
}
#if defined(NRF52_PLATFORM)
const uint16_t slot = record_index;
if (!_contact_slots.reserve(slot)) {
legacy_read_failed = true;
break;
}
contact.storage_slot = slot;
#endif
++record_index;
if (!host->onContactLoaded(contact)) {
full = true;
#if MESH_CONTACT_CACHE
_cache_load_incomplete = true;
#endif
#if defined(NRF52_PLATFORM)
_contact_slots.release(slot);
legacy_host_refused = true;
#endif
}
}
file.close();
#if defined(NRF52_PLATFORM)
if (legacy_read_failed || legacy_host_refused
|| record_index != _legacy_contact_count) {
if (legacy_host_refused) {
// Capacity refusal is semantically distinct from an I/O error, but the
// durable source still contains contacts absent from RAM. Preserve it
// and use the same mutation/migration veto until a clean load succeeds.
MESH_DEBUG_PRINTLN(
"DataStore: contact host capacity refused legacy record %u; load incomplete",
(unsigned)record_index);
} else {
MESH_DEBUG_PRINTLN(
"DataStore: short/incomplete legacy contact read at record %u of %u",
(unsigned)record_index, (unsigned)_legacy_contact_count);
}
_contact_load_incomplete = true;
return;
}
// Only modify stale page artifacts after the complete authoritative legacy
// file has been loaded. A transient read failure above therefore leaves
// every possible recovery source untouched.
if (!mesh::storage::trustMigratedContactPages(
true, _legacy_migration_ready)) {
if (!prepareLegacyContactMigration()) {
_contact_load_incomplete = true;
return;
}
// prepareLegacyContactMigration() intentionally removed every stale page
// observed in the first pass before committing the marker.
contact_page_presence = 0;
}
#endif
}
#if defined(NRF52_PLATFORM)
// Pages at and beyond the remaining legacy prefix have already committed.
// Loading both sources this way resumes safely after every possible reset
// point, including a reset after page rename but before legacy truncation.
if (_legacy_migration_ready) {
loadContactPages(host, _legacy_contact_count, contact_page_presence);
}
#endif
}
bool DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
if (hasIncompleteContactLoad()) return false;
#if defined(NRF52_PLATFORM)
bool success = true;
for (uint32_t idx = 0;; idx++) {
ContactInfo* contact = host->getContactForStore(idx);
if (contact == NULL) break;
if (filter && !filter(*contact)) continue;
success = markContactDirty(*contact) && success;
}
return flushContactWrites(host, filter) && success;
#elif MESH_CONTACT_CACHE && defined(ESP32_PLATFORM)
auto& paths = mesh::contactPathStorage();
paths.beginCommit();
for (uint32_t i = 0;; ++i) {
auto* c = host->getContactForStore(i);
if (!c) break;
if (!filter || filter(*c)) paths.mark(c->path_ref.handle());
}
if (!paths.preserveSnapshots(0, 0x8000)) {
paths.endCommit(false);
return false;
}
// The cold-path reader reuses one SPIFFS File during this streaming write.
// Close it before replacing the original name so later reads reopen the
// committed file. No complete contact-table copy is needed in RAM.
mesh::ContactFileTransaction writer(_getContactsChannelsFS(), "/contacts3");
bool success = writer;
uint8_t record[mesh::storage::CONTACT_RECORD_SIZE];
for (uint32_t i = 0; success; ++i) {
auto* c = host->getContactForStore(i);
if (!c) break;
if (filter && !filter(*c)) continue;
success = serializeContactRecord(*c, record)
&& writer.write(record, sizeof(record)) == sizeof(record);
}
_contact_path_reader.close();
success = writer.commit(success);
if (success) {
uint16_t index = 0;
for (uint32_t i = 0;; ++i) {
auto* c = host->getContactForStore(i);
if (!c) break;
if (filter && !filter(*c)) continue;
paths.publish(c->path_ref.handle(), index++);
}
}
paths.endCommit(success);
return success;
#else
File file = openWrite(_getContactsChannelsFS(), "/contacts3");
bool success = (bool)file;
if (file) {
uint32_t idx = 0;
ContactInfo c;
uint8_t unused = 0;
while (host->getContactForSave(idx, c)) {
if (filter && !filter(c)) {
idx++; // advance to next contact
continue;
}
success = (file.write(c.id.pub_key, 32) == 32);
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
success = success && (file.write(&c.type, 1) == 1);
success = success && (file.write(&c.flags, 1) == 1);
success = success && (file.write(&unused, 1) == 1);
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
uint8_t path[64];
success = success && c.copyPathTo(path) && (file.write(path, 64) == 64);
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
if (!success) break; // write failed
idx++; // advance to next contact
}
file.close();
}
return success;
#endif
}
bool DataStore::markContactDirty(const ContactInfo& contact) {
#if defined(NRF52_PLATFORM)
if (hasIncompleteContactLoad()) return false;
uint16_t slot = contact.storage_slot;
if (!_contact_slots.isUsed(slot)) {
slot = _contact_slots.allocate();
if (slot == mesh::storage::CONTACT_SLOT_NONE) return false;
contact.storage_slot = slot;
}
return _dirty_contact_pages.mark(slot / mesh::storage::CONTACTS_PER_PAGE);
#else
(void)contact;
return true;
#endif
}
bool DataStore::releaseContact(const ContactInfo& contact) {
#if defined(NRF52_PLATFORM)
if (hasIncompleteContactLoad()) return false;
const uint16_t slot = contact.storage_slot;
if (!_contact_slots.release(slot)) return false;
contact.storage_slot = mesh::storage::CONTACT_SLOT_NONE;
return _dirty_contact_pages.mark(slot / mesh::storage::CONTACTS_PER_PAGE);
#else
(void)contact;
return true;
#endif
}
bool DataStore::restoreContactSlot(const ContactInfo& contact, uint16_t slot) {
#if defined(NRF52_PLATFORM)
if (hasIncompleteContactLoad()) return false;
// Roll back a release into the exact slot it vacated. Allocating the first
// free slot here could move the record to another page; a reset between the
// two resulting page writes would then leave duplicate records on disk.
if (!_contact_slots.reserve(slot)) return false;
if (!_dirty_contact_pages.mark(
slot / mesh::storage::CONTACTS_PER_PAGE)) {
_contact_slots.release(slot);
return false;
}
contact.storage_slot = slot;
return true;
#else
(void)contact;
(void)slot;
return true;
#endif
}
#if defined(NRF52_PLATFORM)
bool DataStore::truncateLegacyContacts(uint16_t remaining_contacts) {
FILESYSTEM* fs = _getContactsChannelsFS();
if (remaining_contacts == 0) {
bool legacy_exists = false;
if (!contactPathPresence(fs, "/contacts3", legacy_exists)) return false;
if (legacy_exists && !fs->remove("/contacts3")) return false;
bool marker_exists = false;
if (!contactPathPresence(fs, CONTACT_MIGRATION_MARKER, marker_exists)) {
return false;
}
if (marker_exists && !fs->remove(CONTACT_MIGRATION_MARKER)) return false;
_legacy_contact_count = 0;
_legacy_contacts_pending_cleanup = false;
_legacy_migration_ready = false;
return true;
}
File file = fs->open("/contacts3", FILE_O_WRITE);
if (!file) return false;
const uint32_t expected_size =
(uint32_t)remaining_contacts * mesh::storage::CONTACT_RECORD_SIZE;
const bool truncated = file.truncate(expected_size);
if (truncated) file.flush();
file.close();
if (!truncated) return false;
// Verify the committed length before advancing the in-memory transaction.
// If verification itself fails, retrying the same page/truncate is harmless.
File verify = openRead(fs, "/contacts3");
const bool valid = verify && verify.size() == expected_size;
if (verify) verify.close();
if (!valid) return false;
_legacy_contact_count = remaining_contacts;
return true;
}
#endif
bool DataStore::serviceContactWrites(DataStoreHost* host,
bool (*filter)(const ContactInfo& c)) {
#if defined(NRF52_PLATFORM)
if (hasIncompleteContactLoad()) {
// Reboot is the recovery operation. Never migrate or rewrite from a RAM
// table which is known not to represent every durable contact.
return false;
}
if (_legacy_contacts_pending_cleanup) {
FILESYSTEM* fs = _getContactsChannelsFS();
bool legacy_exists = false;
if (!contactPathPresence(fs, "/contacts3", legacy_exists)) return false;
if (!legacy_exists) {
bool marker_exists = false;
if (!contactPathPresence(fs, CONTACT_MIGRATION_MARKER,
marker_exists)) {
return false;
}
if (marker_exists && !fs->remove(CONTACT_MIGRATION_MARKER)) return false;
_legacy_contacts_pending_cleanup = false;
_legacy_migration_ready = false;
_legacy_contact_count = 0;
} else if (!_legacy_migration_ready
&& !prepareLegacyContactMigration()) {
return false;
} else if (_legacy_contact_count == 0) {
return truncateLegacyContacts(0);
} else {
const uint8_t page =
mesh::storage::legacyMigrationPage(_legacy_contact_count);
if ((_unread_contact_pages.bits() & (1UL << page)) != 0) {
// The legacy prefix cannot reconstruct post-prefix records from an
// unread committed page. Keep both sources untouched for reboot retry.
return false;
}
if (page >= mesh::storage::CONTACT_PAGE_COUNT
|| !writeContactPage(host, page, filter)) {
return false;
}
const uint16_t remaining =
mesh::storage::legacyCountAfterMigratingPage(page);
if (!truncateLegacyContacts(remaining)) return false;
_dirty_contact_pages.clear(page);
return true;
}
}
const int page = _dirty_contact_pages.first();
if (page < 0) return true;
if (!writeContactPage(host, (uint8_t)page, filter)) return false;
_dirty_contact_pages.clear((uint8_t)page);
return true;
#else
return saveContacts(host, filter);
#endif
}
bool DataStore::flushContactWrites(DataStoreHost* host,
bool (*filter)(const ContactInfo& c)) {
if (hasIncompleteContactLoad()) return false;
#if defined(NRF52_PLATFORM)
while (hasPendingContactWrites()) {
if (!serviceContactWrites(host, filter)) return false;
}
return true;
#else
return saveContacts(host, filter);
#endif
}
bool DataStore::hasPendingContactWrites() const {
#if defined(NRF52_PLATFORM)
return !hasIncompleteContactLoad()
&& (!_dirty_contact_pages.empty() || _legacy_contacts_pending_cleanup);
#else
return false;
#endif
}
bool DataStore::hasIncompleteContactLoad() const {
#if MESH_CONTACT_CACHE
if (_cache_load_incomplete) return true;
#endif
#if defined(NRF52_PLATFORM)
return _contact_load_incomplete || !_unread_contact_pages.empty();
#else
return false;
#endif
}
void DataStore::loadChannels(DataStoreHost* host) {
#if defined(NRF52_PLATFORM)
if (_contact_load_incomplete) {
MESH_DEBUG_PRINTLN(
"DataStore: channel load quarantined after storage I/O failure");
return;
}
FILESYSTEM* contacts_fs = _getContactsChannelsFS();
bool channels_exist = false;
uint32_t channels_size = 0;
if (!contactPathPresence(contacts_fs, "/channels2", channels_exist,
&channels_size)) {
_contact_load_incomplete = true;
return;
}
if (!channels_exist) return;
static const uint32_t CHANNEL_RECORD_SIZE = 4 + 32 + 32;
if ((channels_size % CHANNEL_RECORD_SIZE) != 0
|| channels_size / CHANNEL_RECORD_SIZE > MAX_GROUP_CHANNELS) {
MESH_DEBUG_PRINTLN(
"DataStore: invalid channels file size; storage quarantined");
_contact_load_incomplete = true;
return;
}
#endif
File file = openRead(_getContactsChannelsFS(), "/channels2");
#if defined(NRF52_PLATFORM)
if (!file || file.size() != channels_size) {
if (file) file.close();
MESH_DEBUG_PRINTLN(
"DataStore: channels file could not be opened consistently; storage quarantined");
_contact_load_incomplete = true;
return;
}
const uint8_t channel_count =
static_cast<uint8_t>(channels_size / CHANNEL_RECORD_SIZE);
ChannelDetails* loaded = channel_count == 0 ? nullptr
: static_cast<ChannelDetails*>(
malloc(sizeof(ChannelDetails) * channel_count));
if (channel_count != 0 && loaded == nullptr) {
file.close();
MESH_DEBUG_PRINTLN(
"DataStore: no memory for channels snapshot; storage quarantined");
_contact_load_incomplete = true;
return;
}
bool success = true;
for (uint8_t channel_idx = 0; channel_idx < channel_count; channel_idx++) {
uint8_t unused[4];
success = file.read(unused, sizeof(unused)) == sizeof(unused)
&& file.read(reinterpret_cast<uint8_t*>(loaded[channel_idx].name),
sizeof(loaded[channel_idx].name))
== sizeof(loaded[channel_idx].name)
&& file.read(loaded[channel_idx].channel.secret,
sizeof(loaded[channel_idx].channel.secret))
== sizeof(loaded[channel_idx].channel.secret);
if (!success) break;
}
file.close();
if (!success) {
free(loaded);
MESH_DEBUG_PRINTLN(
"DataStore: channels file read failed; storage quarantined");
_contact_load_incomplete = true;
return;
}
for (uint8_t channel_idx = 0; channel_idx < channel_count; channel_idx++) {
if (!host->onChannelLoaded(channel_idx, loaded[channel_idx])) {
free(loaded);
MESH_DEBUG_PRINTLN(
"DataStore: channel host refused durable record; storage quarantined");
_contact_load_incomplete = true;
return;
}
}
free(loaded);
#else
if (file) {
bool full = false;
uint8_t channel_idx = 0;
while (!full) {
ChannelDetails ch;
uint8_t unused[4];
bool success = (file.read(unused, 4) == 4);
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
if (!success) break; // EOF
if (host->onChannelLoaded(channel_idx, ch)) {
channel_idx++;
} else {
full = true;
}
}
file.close();
}
#endif
}
bool DataStore::saveChannels(DataStoreHost* host) {
#if defined(NRF52_PLATFORM)
if (_contact_load_incomplete) return false;
#endif
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter file(_getContactsChannelsFS(), "/channels2");
#else
File file = openWrite(_getContactsChannelsFS(), "/channels2");
#endif
bool success = (bool)file;
if (file) {
uint8_t channel_idx = 0;
ChannelDetails ch;
uint8_t unused[4];
memset(unused, 0, 4);
while (success && host->getChannelForSave(channel_idx, ch)) {
success = (file.write(unused, 4) == 4);
success = success && (file.write((uint8_t *)ch.name, 32) == 32);
success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32);
if (!success) break; // write failed
channel_idx++;
}
#if defined(NRF52_PLATFORM)
success = file.commit(success);
if (!success) MESH_DEBUG_PRINTLN("DataStore: atomic channels write failed");
#else
file.close();
#endif
}
return success;
}
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)
struct BlobRec {
uint32_t timestamp;
uint8_t key[7];
uint8_t len;
uint8_t data[MAX_ADVERT_PKT_LEN];
};
#if !defined(NRF52_PLATFORM)
static void normalizeBlobKey(const uint8_t key[], int key_len, uint8_t normalized[7]) {
memset(normalized, 0, 7);
if (key == NULL || key_len <= 0) return;
if (key_len > 7) key_len = 7;
memcpy(normalized, key, key_len);
}
#endif
void DataStore::checkAdvBlobFile() {
#if defined(NRF52_PLATFORM)
// Advert packets are a disposable cache and are learned again over the air.
// Retaining the old 18 KiB monolithic cache alongside atomic buckets can
// exhaust the 100 KiB ExtraFS and prevent a contact-page commit, so retire
// it once on upgrade. Contact records and identity data are not affected.
if (_fs->exists("/adv_blobs") && !_fs->remove("/adv_blobs")) {
MESH_DEBUG_PRINTLN("DataStore: could not retire internal legacy advert cache");
}
if (_fsExtra != nullptr && _fsExtra->exists("/adv_blobs")
&& !_fsExtra->remove("/adv_blobs")) {
MESH_DEBUG_PRINTLN("DataStore: could not retire secondary legacy advert cache");
}
return;
#else
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
File file = openWrite(_getContactsChannelsFS(), "/adv_blobs");
if (file) {
BlobRec zeroes;
memset(&zeroes, 0, sizeof(zeroes));
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
file.write((uint8_t *) &zeroes, sizeof(zeroes));
}
file.close();
}
}
#endif
}
bool DataStore::migrateToSecondaryFS() {
if (_fsExtra == nullptr) return false;
// Implemented below through verified copy transactions. On nRF52, all
// primary contact/channel files are copied before an atomic journal commit;
// no source is removed before that commit makes the complete secondary
// snapshot authoritative across a reset.
#if defined(NRF52_PLATFORM)
// /adv_blobs is a reconstructable cache retired by checkAdvBlobFile(); do
// not spend time and temporary space atomically copying it first.
static const char* to_secondary[] = {
"/contacts3", "/contacts4.mig", "/channels2"};
#else
static const char* to_secondary[] = {"/adv_blobs", "/contacts3", "/channels2"};
#endif
static const char* to_primary[] = {"/_main.id", "/new_prefs"};
auto filesEqual = [this](FILESYSTEM* left_fs, FILESYSTEM* right_fs,
const char* path) -> bool {
File left = openRead(left_fs, path);
File right = openRead(right_fs, path);
const uint32_t expected_size = left ? left.size() : 0;
if (!left || !right || expected_size != right.size()) {
if (left) left.close();
if (right) right.close();
return false;
}
uint8_t left_buf[64], right_buf[64];
bool equal = true;
uint32_t compared = 0;
while (equal) {
int left_count = left.read(left_buf, sizeof(left_buf));
int right_count = right.read(right_buf, sizeof(right_buf));
if (left_count < 0 || right_count < 0) {
equal = false;
} else if (left_count != right_count) {
equal = false;
} else if (left_count <= 0) {
break;
} else if (memcmp(left_buf, right_buf, left_count) != 0) {
equal = false;
} else {
compared += (uint32_t)left_count;
}
}
left.close();
right.close();
return equal && compared == expected_size;
};
auto copy = [this, &filesEqual](FILESYSTEM* source_fs, FILESYSTEM* dest_fs,
const char* path,
bool exact_snapshot) -> bool {
#if defined(NRF52_PLATFORM)
bool source_exists = false;
if (!contactPathPresence(source_fs, path, source_exists)) return false;
if (!source_exists) {
if (!exact_snapshot) return true;
// A reset during a previous Pending attempt can leave a destination-only
// file. Primary is still authoritative until COMMITTED, so the retry
// must remove that stale file rather than blessing it into the snapshot.
bool destination_exists = false;
if (!contactPathPresence(dest_fs, path, destination_exists)) return false;
if (!destination_exists) return true;
if (!dest_fs->remove(path)) return false;
bool destination_remains = true;
return contactPathPresence(dest_fs, path, destination_remains)
&& !destination_remains;
}
bool destination_exists = false;
if (!contactPathPresence(dest_fs, path, destination_exists)) return false;
#else
if (!source_fs->exists(path)) return true;
const bool destination_exists = dest_fs->exists(path);
#endif
if (destination_exists) {
if (filesEqual(source_fs, dest_fs, path)) {
return true;
}
if (!exact_snapshot) {
MESH_DEBUG_PRINTLN("DataStore: migration conflict for %s; preserving both copies", path);
return false;
}
// Pending migration destinations are non-authoritative. Fall through to
// AtomicFileWriter so this stale copy is replaced as one transaction.
}
File source = openRead(source_fs, path);
if (!source) return false;
const uint32_t expected_size = source.size();
bool success = true;
uint8_t buf[64];
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter destination(dest_fs, path);
success = (bool)destination;
while (success) {
int count = source.read(buf, sizeof(buf));
if (count < 0) {
success = false;
} else if (count == 0) {
break;
} else {
success = destination.write(buf, count) == (size_t)count;
}
}
source.close();
success = destination.commit(success && destination.bytesWritten() == expected_size);
#else
char temp_path[64];
snprintf(temp_path, sizeof(temp_path), "%s.tmp", path);
File destination = openWrite(dest_fs, temp_path);
success = (bool)destination;
uint32_t written = 0;
while (success) {
int count = source.read(buf, sizeof(buf));
if (count < 0) {
success = false;
} else if (count == 0) {
break;
} else {
success = destination.write(buf, count) == (size_t)count;
written += success ? count : 0;
}
}
source.close();
if (destination) destination.close();
success = success && written == expected_size;
if (success) {
File verify = openRead(dest_fs, temp_path);
success = verify && verify.size() == expected_size;
if (verify) verify.close();
}
if (success) success = dest_fs->rename(temp_path, path);
if (!success) dest_fs->remove(temp_path);
#endif
if (!success || !filesEqual(source_fs, dest_fs, path)) {
MESH_DEBUG_PRINTLN("DataStore: verified migration failed for %s", path);
return false;
}
return true;
};
#if !defined(NRF52_PLATFORM)
auto move = [&copy](FILESYSTEM* source_fs, FILESYSTEM* dest_fs,
const char* path) -> bool {
if (!source_fs->exists(path)) return true;
return copy(source_fs, dest_fs, path, false) && source_fs->remove(path);
};
#endif
auto migratePrimarySources = [this, &copy, &filesEqual]() -> bool {
bool success = true;
for (size_t i = 0; i < sizeof(to_primary) / sizeof(to_primary[0]); i++) {
const char* path = to_primary[i];
#if defined(NRF52_PLATFORM)
const bool is_identity = strcmp(path, "/_main.id") == 0;
const bool is_preferences = strcmp(path, "/new_prefs") == 0;
#endif
bool primary_exists = false;
#if defined(NRF52_PLATFORM)
if (!contactPathPresence(_fs, path, primary_exists)) {
if (is_identity) _identity_creation_blocked = true;
if (is_preferences) _prefs_load_incomplete = true;
return false;
}
bool secondary_exists = false;
if (!contactPathPresence(_fsExtra, path, secondary_exists)) {
// Identity and preferences on primary are canonical. If that copy is
// already present, a secondary metadata error only prevents optional
// duplicate cleanup and is irrelevant to their availability.
if (primary_exists) continue;
if (is_identity) _identity_creation_blocked = true;
if (is_preferences) _prefs_load_incomplete = true;
return false;
}
#else
primary_exists = _fs->exists(path);
const bool secondary_exists = _fsExtra->exists(path);
#endif
if (!secondary_exists) continue;
if (primary_exists) {
if (!filesEqual(_fsExtra, _fs, path)) {
// Identity/preferences are canonical on primary. A differing legacy
// secondary copy is useful forensic data, but it must not prevent
// contact/channel migration or make secondary storage disappear.
MESH_DEBUG_PRINTLN("DataStore: preserving conflicting legacy secondary %s; primary remains authoritative", path);
continue;
}
} else {
// The required source can disappear between its discovery probe and
// copy()'s own stat. Only a verified destination proves recovery.
bool recovered_primary = copy(_fsExtra, _fs, path, false);
#if defined(NRF52_PLATFORM)
bool recovered_primary_exists = false;
recovered_primary = recovered_primary
&& contactPathPresence(_fs, path, recovered_primary_exists)
&& recovered_primary_exists;
#else
recovered_primary = recovered_primary && _fs->exists(path);
#endif
if (!recovered_primary) {
// Without any primary copy MyMesh would generate a replacement
// identity or persist default preferences, so this is the only
// primary-source condition which must block the storage handoff.
#if defined(NRF52_PLATFORM)
if (is_identity) _identity_creation_blocked = true;
if (is_preferences) _prefs_load_incomplete = true;
#endif
success = false;
continue;
}
}
// At this point primary has an exact verified copy. Failure to retire a
// duplicate is harmless and can be retried on a later boot.
if (!_fsExtra->remove(path)) {
MESH_DEBUG_PRINTLN("DataStore: could not retire verified legacy secondary %s", path);
}
}
return success;
};
#if defined(NRF52_PLATFORM)
enum class MigrationJournalState : uint8_t {
None,
Pending,
Committed,
Invalid,
IoError,
};
static const uint8_t journal_magic[] = {'M', 'C', 'X', 'F', 1};
auto readJournal = [this]() -> MigrationJournalState {
bool journal_exists = false;
if (!contactPathPresence(_fsExtra, SECONDARY_MIGRATION_JOURNAL,
journal_exists)) {
return MigrationJournalState::IoError;
}
if (!journal_exists) {
return MigrationJournalState::None;
}
File journal = openRead(_fsExtra, SECONDARY_MIGRATION_JOURNAL);
uint8_t payload[sizeof(journal_magic) + 1];
const bool valid_size = journal && journal.size() == sizeof(payload);
const int count = valid_size ? journal.read(payload, sizeof(payload)) : 0;
if (journal) journal.close();
if (count != (int)sizeof(payload)
|| memcmp(payload, journal_magic, sizeof(journal_magic)) != 0) {
return MigrationJournalState::Invalid;
}
if (payload[sizeof(journal_magic)] == SECONDARY_MIGRATION_PENDING) {
return MigrationJournalState::Pending;
}
if (payload[sizeof(journal_magic)] == SECONDARY_MIGRATION_COMMITTED) {
return MigrationJournalState::Committed;
}
return MigrationJournalState::Invalid;
};
auto writeJournal = [this](uint8_t state) -> bool {
uint8_t payload[sizeof(journal_magic) + 1];
memcpy(payload, journal_magic, sizeof(journal_magic));
payload[sizeof(journal_magic)] = state;
mesh::AtomicFileWriter journal(_fsExtra, SECONDARY_MIGRATION_JOURNAL);
return journal
&& journal.write(payload, sizeof(payload)) == sizeof(payload)
&& journal.commit();
};
auto hasSecondarySources = [this](bool& present) -> bool {
present = false;
for (size_t i = 0; i < sizeof(to_secondary) / sizeof(to_secondary[0]); i++) {
bool path_exists = false;
if (!contactPathPresence(_fs, to_secondary[i], path_exists)) return false;
if (path_exists) {
present = true;
return true;
}
}
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[24];
makeContactPagePath(page, path);
bool path_exists = false;
if (!contactPathPresence(_fs, path, path_exists)) return false;
if (path_exists) {
present = true;
return true;
}
}
for (uint8_t bucket = 0; bucket < 10; bucket++) {
char path[20];
snprintf(path, sizeof(path), "/adv4_%02u", (unsigned)bucket);
bool path_exists = false;
if (!contactPathPresence(_fs, path, path_exists)) return false;
if (path_exists) {
present = true;
return true;
}
}
return true;
};
auto copySecondarySources = [this, &copy]() -> bool {
bool success = true;
for (size_t i = 0; i < sizeof(to_secondary) / sizeof(to_secondary[0]); i++) {
if (!copy(_fs, _fsExtra, to_secondary[i], true)) success = false;
}
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[24];
makeContactPagePath(page, path);
if (!copy(_fs, _fsExtra, path, true)) success = false;
}
for (uint8_t bucket = 0; bucket < 10; bucket++) {
char path[20];
snprintf(path, sizeof(path), "/adv4_%02u", (unsigned)bucket);
if (!copy(_fs, _fsExtra, path, true)) success = false;
}
return success;
};
auto retireSecondarySources = [this]() -> bool {
bool fixed_present[sizeof(to_secondary) / sizeof(to_secondary[0])] = {};
uint32_t page_presence = 0;
uint16_t bucket_presence = 0;
// Capture the complete removal set before deleting anything. A metadata
// I/O error must leave every committed primary source in place for the
// next reboot, rather than being mistaken for an absent file midway
// through cleanup.
for (size_t i = 0; i < sizeof(to_secondary) / sizeof(to_secondary[0]); i++) {
if (!contactPathPresence(_fs, to_secondary[i], fixed_present[i])) {
return false;
}
}
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[24];
makeContactPagePath(page, path);
bool path_exists = false;
if (!contactPathPresence(_fs, path, path_exists)) return false;
if (path_exists) page_presence |= 1UL << page;
}
for (uint8_t bucket = 0; bucket < 10; bucket++) {
char path[20];
snprintf(path, sizeof(path), "/adv4_%02u", (unsigned)bucket);
bool path_exists = false;
if (!contactPathPresence(_fs, path, path_exists)) return false;
if (path_exists) bucket_presence |= (uint16_t)1U << bucket;
}
bool success = true;
auto retire = [this, &success](const char* path, bool path_exists) {
if (!path_exists) return;
// The committed journal is the authority switch: every destination was
// already verified before it was written. Do not compare again here,
// because the secondary may legitimately advance while cleanup retries.
if (!_fs->remove(path)) success = false;
};
for (size_t i = 0; i < sizeof(to_secondary) / sizeof(to_secondary[0]); i++) {
retire(to_secondary[i], fixed_present[i]);
}
for (uint8_t page = 0; page < mesh::storage::CONTACT_PAGE_COUNT; page++) {
char path[24];
makeContactPagePath(page, path);
retire(path, (page_presence & (1UL << page)) != 0);
}
for (uint8_t bucket = 0; bucket < 10; bucket++) {
char path[20];
snprintf(path, sizeof(path), "/adv4_%02u", (unsigned)bucket);
retire(path, (bucket_presence & ((uint16_t)1U << bucket)) != 0);
}
return success;
};
MigrationJournalState journal_state = readJournal();
// Identity and preferences are always read from the primary filesystem.
// Recover legacy/test-layout copies before any journal result can select or
// quarantine the contact/channel store. The journal has already been read,
// however, so a failure here can never silently select the wrong authority.
if (!migratePrimarySources()) {
_contact_load_incomplete = true;
MESH_DEBUG_PRINTLN(
"DataStore: primary identity/preferences migration failed; storage quarantined until reboot");
return false;
}
if (journal_state == MigrationJournalState::IoError) {
// The journal is the authority switch. If its presence cannot be
// classified, neither filesystem can safely be selected for contact or
// channel mutations this boot: Pending means primary is authoritative,
// while Committed means secondary may already be the only complete copy.
_contact_load_incomplete = true;
MESH_DEBUG_PRINTLN(
"DataStore: ExtraFS migration journal stat failed; storage quarantined until reboot");
return false;
}
if (journal_state == MigrationJournalState::Invalid) {
// Atomic journal contents should always be one of the two valid states.
// Corruption cannot reveal whether source retirement had begun, so it has
// the same authority-unknown policy as a journal I/O error.
_contact_load_incomplete = true;
MESH_DEBUG_PRINTLN(
"DataStore: invalid ExtraFS migration journal; storage quarantined until explicit erase");
return false;
}
bool secondary_sources_present = false;
if (journal_state == MigrationJournalState::None
&& !hasSecondarySources(secondary_sources_present)) {
// A clean post-migration boot also has no journal and no primary sources,
// so a source-stat error makes authority unknowable. Preserve both stores
// and block contact/channel use until a reboot can classify every path.
_contact_load_incomplete = true;
MESH_DEBUG_PRINTLN(
"DataStore: could not inspect primary migration sources; storage quarantined until reboot");
return false;
}
if (journal_state != MigrationJournalState::Committed
&& (journal_state != MigrationJournalState::None
|| secondary_sources_present)) {
if (journal_state == MigrationJournalState::None
&& !writeJournal(SECONDARY_MIGRATION_PENDING)) {
MESH_DEBUG_PRINTLN("DataStore: could not start ExtraFS migration transaction");
_fsExtra = nullptr;
return false;
}
if (!copySecondarySources()
|| !writeJournal(SECONDARY_MIGRATION_COMMITTED)) {
// Pending means no source has been retired, so primary remains a complete
// fallback even if the destination contains harmless partial copies.
MESH_DEBUG_PRINTLN("DataStore: ExtraFS migration copy incomplete; using primary storage");
_fsExtra = nullptr;
return false;
}
journal_state = MigrationJournalState::Committed;
}
if (journal_state == MigrationJournalState::Committed) {
if (!retireSecondarySources()) {
// The committed secondary contains the full verified snapshot. Keep it
// active and keep the journal so cleanup can resume without copying a
// stale primary source over newer secondary data.
MESH_DEBUG_PRINTLN("DataStore: ExtraFS migration committed; source cleanup remains pending");
return false;
}
// readJournal() found it, or writeJournal() committed it in this call, so
// a second presence probe cannot add safety. Remove the known journal
// directly after all committed sources have been retired.
if (!_fsExtra->remove(SECONDARY_MIGRATION_JOURNAL)) {
MESH_DEBUG_PRINTLN("DataStore: ExtraFS migration journal cleanup failed");
return false;
}
}
#else
bool success = true;
for (size_t i = 0; i < sizeof(to_secondary) / sizeof(to_secondary[0]); i++) {
if (!move(_fs, _fsExtra, to_secondary[i])) success = false;
}
if (!success) return false;
return migratePrimarySources();
#endif
return true;
}
#if defined(NRF52_PLATFORM)
// Keep every bucket below one 4 KiB LittleFS block. Five 20-record buckets use
// about the same flash as the old 100-record file; smaller buckets would each
// consume a full block and leave no room for contact-page transactions.
static const uint8_t BLOB_BUCKET_COUNT = MAX_BLOBRECS > 20 ? 5 : 1;
static const uint8_t BLOB_BUCKET_SLOTS =
(MAX_BLOBRECS + BLOB_BUCKET_COUNT - 1) / BLOB_BUCKET_COUNT;
static const uint8_t BLOB_BUCKET_HEADER_SIZE = 16;
static const uint8_t BLOB_BUCKET_MAGIC[4] = {'M', 'C', 'B', '4'};
static_assert(BLOB_BUCKET_HEADER_SIZE + sizeof(BlobRec) * BLOB_BUCKET_SLOTS < 4096,
"advert bucket must fit in one LittleFS block");
static void normalizeBlobKey(const uint8_t key[], int key_len, uint8_t normalized[7]) {
memset(normalized, 0, 7);
if (key == NULL || key_len <= 0) return;
if (key_len > 7) key_len = 7;
memcpy(normalized, key, key_len);
}
static uint8_t blobBucketFor(const uint8_t key[7]) {
uint32_t hash = 2166136261UL;
for (uint8_t i = 0; i < 7; i++) {
hash ^= key[i];
hash *= 16777619UL;
}
return hash % BLOB_BUCKET_COUNT;
}
static void makeBlobBucketPath(uint8_t bucket, char path[20]) {
snprintf(path, 20, "/adv4_%02u", (unsigned)bucket);
}
static bool loadBlobBucket(FILESYSTEM* fs, uint8_t bucket,
BlobRec records[BLOB_BUCKET_SLOTS]) {
memset(records, 0, sizeof(BlobRec) * BLOB_BUCKET_SLOTS);
char path[20];
makeBlobBucketPath(bucket, path);
if (!fs->exists(path)) return true;
File file = fs->open(path, FILE_O_READ);
if (!file) return false;
const size_t payload_size = sizeof(BlobRec) * BLOB_BUCKET_SLOTS;
if (file.size() != BLOB_BUCKET_HEADER_SIZE + payload_size) {
file.close();
return false;
}
uint8_t header[BLOB_BUCKET_HEADER_SIZE];
bool valid = file.read(header, sizeof(header)) == sizeof(header)
&& memcmp(header, BLOB_BUCKET_MAGIC, sizeof(BLOB_BUCKET_MAGIC)) == 0
&& header[4] == 1 && header[5] == bucket
&& header[6] == BLOB_BUCKET_SLOTS
&& mesh::storage::readLE16(&header[8]) == sizeof(BlobRec)
&& file.read((uint8_t*)records, payload_size) == (int)payload_size;
file.close();
if (!valid) return false;
const uint32_t expected_crc = mesh::storage::readLE32(&header[12]);
const uint32_t actual_crc = mesh::storage::updateCRC32(
0xFFFFFFFFUL, (const uint8_t*)records, payload_size);
if (actual_crc != expected_crc) return false;
for (uint8_t i = 0; i < BLOB_BUCKET_SLOTS; i++) {
if (records[i].len > MAX_ADVERT_PKT_LEN) return false;
}
return true;
}
static bool saveBlobBucket(FILESYSTEM* fs, uint8_t bucket,
const BlobRec records[BLOB_BUCKET_SLOTS]) {
const size_t payload_size = sizeof(BlobRec) * BLOB_BUCKET_SLOTS;
uint8_t header[BLOB_BUCKET_HEADER_SIZE];
memset(header, 0, sizeof(header));
memcpy(header, BLOB_BUCKET_MAGIC, sizeof(BLOB_BUCKET_MAGIC));
header[4] = 1;
header[5] = bucket;
header[6] = BLOB_BUCKET_SLOTS;
mesh::storage::writeLE16(&header[8], sizeof(BlobRec));
mesh::storage::writeLE32(&header[12], mesh::storage::updateCRC32(
0xFFFFFFFFUL, (const uint8_t*)records, payload_size));
char path[20];
makeBlobBucketPath(bucket, path);
mesh::AtomicFileWriter writer(fs, path);
const bool wrote = writer
&& writer.write(header, sizeof(header)) == sizeof(header)
&& writer.write((const uint8_t*)records, payload_size) == payload_size;
return writer.commit(wrote);
}
static bool findBlobInBucket(FILESYSTEM* fs, const uint8_t key[7],
uint8_t dest_buf[], uint8_t& length) {
// Twenty records are roughly 3.6 KiB, too large for the nRF Arduino loop's
// 4 KiB stack once callers are included. Use a short-lived heap buffer.
BlobRec* records = (BlobRec*)malloc(sizeof(BlobRec) * BLOB_BUCKET_SLOTS);
if (records == nullptr) return false;
const uint8_t bucket = blobBucketFor(key);
if (!loadBlobBucket(fs, bucket, records)) {
free(records);
return false;
}
for (uint8_t i = 0; i < BLOB_BUCKET_SLOTS; i++) {
if (memcmp(records[i].key, key, sizeof(records[i].key)) == 0
&& (records[i].timestamp != 0 || records[i].len != 0)) {
length = records[i].len; // zero is an intentional tombstone
if (length > 0) memcpy(dest_buf, records[i].data, length);
free(records);
return true;
}
}
free(records);
return false;
}
// Once a v4 bucket contains a key, the old monolithic cache entry is no
// longer needed. Clear just that disposable legacy record so an eventually
// evicted bucket/tombstone can never expose stale advert data again. This is
// intentionally a bounded in-place write: atomically rewriting the complete
// legacy cache is the multi-second operation the bucket format avoids.
static bool clearLegacyBlobRecord(FILESYSTEM* fs, const uint8_t key[7]) {
if (!fs->exists("/adv_blobs")) return true;
File file = fs->open("/adv_blobs", FILE_O_WRITE);
if (!file) return false;
BlobRec record;
uint32_t position = 0;
bool success = true;
bool found = false;
file.seek(0);
while (file.read((uint8_t*)&record, sizeof(record)) == sizeof(record)) {
if (record.len <= MAX_ADVERT_PKT_LEN
&& memcmp(record.key, key, sizeof(record.key)) == 0
&& (record.timestamp != 0 || record.len != 0)) {
found = true;
memset(&record, 0, sizeof(record));
success = file.seek(position)
&& file.write((uint8_t*)&record, sizeof(record)) == sizeof(record);
if (success) file.flush();
break;
}
position += sizeof(record);
}
file.close();
return !found || success;
}
#endif
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
#if defined(NRF52_PLATFORM)
uint8_t normalized[7], length = 0;
normalizeBlobKey(key, key_len, normalized);
if (findBlobInBucket(_getContactsChannelsFS(), normalized, dest_buf, length)) {
return length;
}
// checkAdvBlobFile() retires the old monolithic nRF cache at boot. If that
// best-effort removal failed, do not fall back to it: a durable bucket
// tombstone must remain authoritative even when its bucket is temporarily
// unreadable, and adverts are a reconstructable cache.
return 0;
#endif
#if !defined(NRF52_PLATFORM)
File file = openRead(_getContactsChannelsFS(), "/adv_blobs");
uint8_t len = 0; // 0 = not found
if (file) {
BlobRec tmp;
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
uint8_t normalized[7];
normalizeBlobKey(key, key_len, normalized);
if (tmp.len <= MAX_ADVERT_PKT_LEN
&& memcmp(normalized, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
len = tmp.len;
memcpy(dest_buf, tmp.data, len);
break;
}
}
file.close();
}
return len;
#endif
}
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
#if defined(NRF52_PLATFORM)
uint8_t normalized[7];
normalizeBlobKey(key, key_len, normalized);
const uint8_t bucket = blobBucketFor(normalized);
BlobRec* records = (BlobRec*)malloc(sizeof(BlobRec) * BLOB_BUCKET_SLOTS);
if (records == nullptr) return false;
if (!loadBlobBucket(_getContactsChannelsFS(), bucket, records)) {
MESH_DEBUG_PRINTLN("DataStore: advert bucket %u corrupt; replacing on next write", bucket);
memset(records, 0, sizeof(BlobRec) * BLOB_BUCKET_SLOTS);
}
uint8_t selected = 0;
uint32_t oldest = 0xFFFFFFFFUL;
for (uint8_t i = 0; i < BLOB_BUCKET_SLOTS; i++) {
if (memcmp(records[i].key, normalized, sizeof(records[i].key)) == 0
&& (records[i].timestamp != 0 || records[i].len != 0)) {
selected = i;
break;
}
if (records[i].timestamp < oldest) {
oldest = records[i].timestamp;
selected = i;
}
}
BlobRec& record = records[selected];
memset(&record, 0, sizeof(record));
memcpy(record.key, normalized, sizeof(record.key));
memcpy(record.data, src_buf, len);
record.len = len;
record.timestamp = _clock->getCurrentTime();
if (record.timestamp == 0) record.timestamp = 1;
const bool saved = saveBlobBucket(_getContactsChannelsFS(), bucket, records);
free(records);
if (!saved) return false;
if (!clearLegacyBlobRecord(_getContactsChannelsFS(), normalized)) {
// The nRF reader never falls back to this reconstructable legacy cache.
MESH_DEBUG_PRINTLN(
"DataStore: advert saved; legacy cache cleanup deferred");
}
return true;
#else
checkAdvBlobFile();
File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE);
if (file) {
uint32_t pos = 0, found_pos = 0;
uint32_t min_timestamp = 0xFFFFFFFF;
// search for matching key OR evict by oldest timestamp
BlobRec tmp;
file.seek(0);
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
found_pos = pos;
break;
}
if (tmp.timestamp < min_timestamp) {
min_timestamp = tmp.timestamp;
found_pos = pos;
}
pos += sizeof(tmp);
}
memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key
memcpy(tmp.data, src_buf, len);
tmp.len = len;
tmp.timestamp = _clock->getCurrentTime();
file.seek(found_pos);
file.write((uint8_t *) &tmp, sizeof(tmp));
file.close();
return true;
}
return false; // error
#endif
}
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
#if defined(NRF52_PLATFORM)
uint8_t normalized[7];
normalizeBlobKey(key, key_len, normalized);
const uint8_t bucket = blobBucketFor(normalized);
BlobRec* records = (BlobRec*)malloc(sizeof(BlobRec) * BLOB_BUCKET_SLOTS);
if (records == nullptr) return false;
const bool bucket_valid = loadBlobBucket(
_getContactsChannelsFS(), bucket, records);
if (!bucket_valid) {
memset(records, 0, sizeof(BlobRec) * BLOB_BUCKET_SLOTS);
}
uint8_t selected = 0;
uint32_t oldest = 0xFFFFFFFFUL;
bool found = false;
for (uint8_t i = 0; i < BLOB_BUCKET_SLOTS; i++) {
if (memcmp(records[i].key, normalized, sizeof(records[i].key)) == 0
&& (records[i].timestamp != 0 || records[i].len != 0)) {
selected = i;
found = true;
break;
}
if (records[i].timestamp < oldest) {
oldest = records[i].timestamp;
selected = i;
}
}
const bool legacy_exists =
_getContactsChannelsFS()->exists("/adv_blobs");
if (!legacy_exists && bucket_valid
&& (!found || records[selected].len == 0)) {
// Only a successfully verified bucket can prove the key is absent or
// already tombstoned. A failed bucket read may be transient and must not
// turn a stale cached advert into an acknowledged deletion.
free(records);
return true;
}
BlobRec& tombstone = records[selected];
memset(&tombstone, 0, sizeof(tombstone));
memcpy(tombstone.key, normalized, sizeof(tombstone.key));
tombstone.timestamp = _clock->getCurrentTime();
if (tombstone.timestamp == 0) tombstone.timestamp = 1;
const bool saved = saveBlobBucket(_getContactsChannelsFS(), bucket, records);
free(records);
if (!saved) return false;
if (!clearLegacyBlobRecord(_getContactsChannelsFS(), normalized)) {
// The durable zero-length bucket record already masks the legacy entry,
// and checkAdvBlobFile() retires the reconstructable legacy cache on the
// next boot. The cache mutation cannot be rolled back after this commit,
// so report deletion success and keep the live contact table consistent
// with the authoritative tombstone.
MESH_DEBUG_PRINTLN(
"DataStore: deleted advert tombstoned; legacy cleanup deferred");
}
return true;
#else
return true; // this is just a stub on NRF52/STM32 platforms
#endif
}
#else
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) {
char fname[18];
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
mesh::Utils::toHex(fname, key, key_len);
sprintf(path, "/bl/%s", fname);
}
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
if (_fs->exists(path)) {
File f = openRead(_fs, path);
if (f) {
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
f.close();
return len;
}
}
return 0; // not found
}
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
File f = openWrite(_fs, path);
if (f) {
int n = f.write(src_buf, len);
f.close();
if (n == len) return true; // success!
_fs->remove(path); // blob was only partially written!
}
return false; // error
}
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
char path[64];
makeBlobPath(key, key_len, path, sizeof(path));
_fs->remove(path);
return true; // return true even if file did not exist
}
#endif
#if MESH_CONTACT_CACHE
namespace {
bool cachedContactFilter(const ContactInfo& c) { return c.type != ADV_TYPE_NONE; }
#if MESH_CONTACT_SECRET_FLASH_CACHE
#if defined(NRF52_PLATFORM)
// ExtraFS allocates 4 KiB data blocks. Pack derived keys into almost a full
// block; tiny pages would consume the available flash after only a few peers.
constexpr size_t SECRET_PAGE_SLOTS = 56;
#else
constexpr size_t SECRET_PAGE_SLOTS = 8;
#endif
constexpr size_t SECRET_HEADER_SIZE = 36;
constexpr size_t SECRET_RECORD_SIZE = 68;
void secretPagePath(uint16_t slot, char path[24]) {
snprintf(path, 24, "/csecret%03x", slot / SECRET_PAGE_SLOTS);
}
void secretHeader(uint8_t header[SECRET_HEADER_SIZE], const uint8_t identity[32]) {
memcpy(header, "MCS1", 4);
memcpy(header + 4, identity, 32);
}
bool validSecretRecord(const uint8_t record[SECRET_RECORD_SIZE]) {
return mesh::storage::readLE32(record + 64) ==
mesh::storage::updateCRC32(0xffffffff, record, 64);
}
#endif
}
bool DataStore::readStoredPath(uint16_t source, uint8_t path[64]) {
if (source == mesh::ContactPathStorage::NONE || hasIncompleteContactLoad()) return false;
char filename[24] = "/contacts3";
size_t offset = static_cast<size_t>(source) * mesh::storage::CONTACT_RECORD_SIZE;
#if defined(ESP32_PLATFORM)
if (!(source & mesh::ContactPathStorage::PAGED)) {
// SPIFFS open scans metadata. Doing it for every contact made bulk saves
// and app synchronization take tens of seconds. Share one read handle;
// loadContacts/saveContacts close it before recovery or replacement.
if (!_contact_path_reader)
_contact_path_reader = openRead(_getContactsChannelsFS(), filename);
File& file = _contact_path_reader;
const bool ok = file && file.size() >= offset + mesh::storage::CONTACT_RECORD_SIZE
&& file.seek(offset + 76) && file.read(path, 64) == 64;
if (!ok) file.close();
return ok;
}
#endif
if (source & mesh::ContactPathStorage::PAGED) {
#if defined(NRF52_PLATFORM)
const uint16_t slot = source & ~mesh::ContactPathStorage::PAGED;
if (slot >= mesh::storage::CONTACT_PAGE_COUNT * mesh::storage::CONTACTS_PER_PAGE) return false;
makeContactPagePath(slot / mesh::storage::CONTACTS_PER_PAGE, filename);
offset = mesh::storage::CONTACT_PAGE_HEADER_SIZE +
(slot % mesh::storage::CONTACTS_PER_PAGE) * mesh::storage::CONTACT_RECORD_SIZE;
#else
return false;
#endif
}
File file = openRead(_getContactsChannelsFS(), filename);
const bool ok = file && file.size() >= offset + mesh::storage::CONTACT_RECORD_SIZE
&& file.seek(offset + 76) && file.read(path, 64) == 64;
if (file) file.close();
// The pool checks the CRC captured when this immutable path value was bound.
return ok;
}
bool DataStore::flushCachedPaths() {
if (!_cache_host || hasIncompleteContactLoad()
|| !flushContactWrites(_cache_host, cachedContactFilter)) return false;
_cache_host->onContactCacheFlushed();
return true;
}
#if MESH_CONTACT_SECRET_FLASH_CACHE
uint16_t DataStore::secretSlot(const uint8_t peer[32]) const {
if (!_cache_host || hasIncompleteContactLoad()) return mesh::storage::CONTACT_SLOT_NONE;
uint16_t persistent_index = 0;
for (uint32_t i = 0;; ++i) {
const auto* c = _cache_host->getContactForStore(i);
if (!c) break;
if (c->type == ADV_TYPE_NONE) continue;
if (memcmp(c->id.pub_key, peer, 32) == 0) {
#if defined(NRF52_PLATFORM)
return c->storage_slot;
#else
return persistent_index;
#endif
}
++persistent_index;
}
return mesh::storage::CONTACT_SLOT_NONE;
}
bool DataStore::readSavedSecret(const uint8_t peer[32], const uint8_t identity[32],
uint8_t secret[32]) {
const uint16_t slot = secretSlot(peer);
if (slot == mesh::storage::CONTACT_SLOT_NONE) return false;
char path[24];
secretPagePath(slot, path);
#if defined(ESP32_PLATFORM)
if (!mesh::ContactFileTransaction::recover(_getContactsChannelsFS(), path)) return false;
#endif
File file = openRead(_getContactsChannelsFS(), path);
uint8_t expected[SECRET_HEADER_SIZE], header[SECRET_HEADER_SIZE];
secretHeader(expected, identity);
uint8_t record[SECRET_RECORD_SIZE];
const bool ok = file && file.size() == SECRET_HEADER_SIZE + SECRET_PAGE_SLOTS * SECRET_RECORD_SIZE
&& file.read(header, sizeof(header)) == sizeof(header)
&& memcmp(header, expected, sizeof(header)) == 0
&& file.seek(SECRET_HEADER_SIZE + (slot % SECRET_PAGE_SLOTS) * SECRET_RECORD_SIZE)
&& file.read(record, sizeof(record)) == sizeof(record)
&& memcmp(record, peer, 32) == 0 && validSecretRecord(record);
if (file) file.close();
if (ok) memcpy(secret, record + 32, 32);
return ok;
}
bool DataStore::saveSecret(const uint8_t peer[32], const uint8_t identity[32],
const uint8_t secret[32]) {
const uint16_t slot = secretSlot(peer);
if (slot == mesh::storage::CONTACT_SLOT_NONE ||
(_secret_retry_at && static_cast<int32_t>(millis() - _secret_retry_at) < 0)) return false;
// Derived keys are expendable. Keep room for contact replacement, preferences
// and filesystem metadata instead of letting this cache prevent a real save.
size_t free_bytes = 0;
size_t reserve_bytes = 0;
#if defined(NRF52_PLATFORM)
const auto* config = _getContactsChannelsFS()->_getFS()->cfg;
const int used = _getLfsUsedBlockCount(_getContactsChannelsFS());
if (used < 0 || static_cast<uint32_t>(used) > config->block_count) return false;
free_bytes = (config->block_count - used) * config->block_size;
reserve_bytes = 4 * config->block_size;
#else
const size_t total = SPIFFS.totalBytes(), used = SPIFFS.usedBytes();
if (used > total) return false;
free_bytes = total - used;
reserve_bytes = MAX_CONTACTS * mesh::storage::CONTACT_RECORD_SIZE + 16384;
#endif
if (free_bytes < reserve_bytes + 2 * (SECRET_HEADER_SIZE + SECRET_PAGE_SLOTS * SECRET_RECORD_SIZE)) {
_secret_retry_at = millis() + 30000;
return false;
}
char path[24];
secretPagePath(slot, path);
uint8_t header[SECRET_HEADER_SIZE], previous_header[SECRET_HEADER_SIZE];
secretHeader(header, identity);
#if defined(ESP32_PLATFORM)
if (!mesh::ContactFileTransaction::recover(_getContactsChannelsFS(), path)) return false;
#endif
File old = openRead(_getContactsChannelsFS(), path);
bool reuse = old && old.size() == SECRET_HEADER_SIZE + SECRET_PAGE_SLOTS * SECRET_RECORD_SIZE
&& old.read(previous_header, sizeof(previous_header)) == sizeof(previous_header)
&& memcmp(previous_header, header, sizeof(header)) == 0;
#if defined(NRF52_PLATFORM)
mesh::AtomicFileWriter writer(_getContactsChannelsFS(), path);
#else
mesh::ContactFileTransaction writer(_getContactsChannelsFS(), path);
#endif
bool ok = writer && writer.write(header, sizeof(header)) == sizeof(header);
uint8_t record[SECRET_RECORD_SIZE];
for (size_t i = 0; ok && i < SECRET_PAGE_SLOTS; ++i) {
memset(record, 0, sizeof(record));
if (reuse && (old.read(record, sizeof(record)) != sizeof(record) || !validSecretRecord(record)))
memset(record, 0, sizeof(record));
if (i == slot % SECRET_PAGE_SLOTS) {
memcpy(record, peer, 32);
memcpy(record + 32, secret, 32);
mesh::storage::writeLE32(record + 64,
mesh::storage::updateCRC32(0xffffffff, record, 64));
}
ok = writer.write(record, sizeof(record)) == sizeof(record);
}
if (old) old.close();
ok = writer.commit(ok);
if (!ok) _secret_retry_at = millis() + 30000;
return ok;
}
#endif
#endif