Files
HaloKeymind/src/helpers/PersistentStoreFormat.h
T

222 lines
7.1 KiB
C++

#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
namespace mesh {
namespace storage {
// Contact pages are deliberately smaller than a 4 KiB LittleFS block. A
// single contact update therefore never rewrites the complete contact list.
static const uint8_t CONTACTS_PER_PAGE = 25;
static const uint8_t CONTACT_PAGE_COUNT = 14;
static const uint16_t CONTACT_RECORD_SIZE = 152;
static const uint16_t CONTACT_PAGE_HEADER_SIZE = 20;
static const uint16_t CONTACT_PAGE_PAYLOAD_SIZE =
CONTACTS_PER_PAGE * CONTACT_RECORD_SIZE;
static const uint16_t CONTACT_PAGE_FILE_SIZE =
CONTACT_PAGE_HEADER_SIZE + CONTACT_PAGE_PAYLOAD_SIZE;
static const uint16_t CONTACT_SLOT_NONE = 0xFFFF;
static const uint8_t CONTACT_PAGE_VERSION = 1;
static const uint8_t CONTACT_PAGE_MAGIC[4] = {'M', 'C', 'P', '4'};
struct ContactPageHeader {
uint8_t page_index;
uint32_t occupied;
uint32_t generation;
uint32_t payload_crc;
};
enum class ContactStoreSource : uint8_t {
EMPTY,
LEGACY,
PAGED,
};
// A retained legacy file means migration is still in progress. Its complete
// record count is the authoritative prefix boundary; page records at or above
// that boundary have already committed.
inline ContactStoreSource chooseContactStoreSource(bool legacy_exists,
bool page_exists) {
if (legacy_exists) return ContactStoreSource::LEGACY;
if (page_exists) return ContactStoreSource::PAGED;
return ContactStoreSource::EMPTY;
}
// When a legacy file returns after a downgrade, old page files may describe a
// different contact list. Only a marker created after stale pages were removed
// makes those pages valid participants in an in-progress migration.
inline bool trustMigratedContactPages(bool legacy_exists,
bool migration_marker_exists) {
return !legacy_exists || migration_marker_exists;
}
inline uint16_t legacyContactCountForSize(size_t file_size) {
const size_t count = file_size / CONTACT_RECORD_SIZE;
const size_t capacity = CONTACT_PAGE_COUNT * CONTACTS_PER_PAGE;
return (uint16_t)(count < capacity ? count : capacity);
}
inline uint8_t legacyMigrationPage(uint16_t legacy_contact_count) {
return legacy_contact_count == 0 ? CONTACT_PAGE_COUNT
: (uint8_t)((legacy_contact_count - 1) / CONTACTS_PER_PAGE);
}
inline uint16_t legacyCountAfterMigratingPage(uint8_t page) {
return page < CONTACT_PAGE_COUNT
? (uint16_t)page * CONTACTS_PER_PAGE : 0;
}
// During tail-first migration, the still-present prefix in /contacts3 wins.
// Page slots at or beyond its complete-record count are already committed and
// can be loaded. This also makes a reset between page commit and truncation
// harmless: the duplicate page records remain hidden until truncation commits.
inline bool loadSlotFromMigratedPage(uint16_t slot,
uint16_t legacy_contact_count) {
return slot >= legacy_contact_count;
}
inline uint16_t readLE16(const uint8_t* src) {
return (uint16_t)src[0] | ((uint16_t)src[1] << 8);
}
inline uint32_t readLE32(const uint8_t* src) {
return (uint32_t)src[0] | ((uint32_t)src[1] << 8)
| ((uint32_t)src[2] << 16) | ((uint32_t)src[3] << 24);
}
inline void writeLE16(uint8_t* dest, uint16_t value) {
dest[0] = (uint8_t)value;
dest[1] = (uint8_t)(value >> 8);
}
inline void writeLE32(uint8_t* dest, uint32_t value) {
dest[0] = (uint8_t)value;
dest[1] = (uint8_t)(value >> 8);
dest[2] = (uint8_t)(value >> 16);
dest[3] = (uint8_t)(value >> 24);
}
// Same CRC convention as AtomicFileWriter: reflected CRC-32, initial value
// 0xFFFFFFFF, with no final xor. Keeping this incremental makes it usable on
// small targets without buffering an entire file.
inline uint32_t updateCRC32(uint32_t crc, const uint8_t* data, size_t len) {
while (len-- > 0) {
crc ^= *data++;
for (uint8_t bit = 0; bit < 8; bit++) {
crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320UL : 0);
}
}
return crc;
}
inline void encodeContactPageHeader(uint8_t dest[CONTACT_PAGE_HEADER_SIZE],
const ContactPageHeader& header) {
memcpy(dest, CONTACT_PAGE_MAGIC, sizeof(CONTACT_PAGE_MAGIC));
dest[4] = CONTACT_PAGE_VERSION;
dest[5] = header.page_index;
writeLE16(&dest[6], CONTACT_RECORD_SIZE);
writeLE32(&dest[8], header.occupied);
writeLE32(&dest[12], header.generation);
writeLE32(&dest[16], header.payload_crc);
}
inline bool decodeContactPageHeader(const uint8_t src[CONTACT_PAGE_HEADER_SIZE],
uint8_t expected_page,
ContactPageHeader& header) {
const uint32_t valid_slots = (1UL << CONTACTS_PER_PAGE) - 1UL;
if (memcmp(src, CONTACT_PAGE_MAGIC, sizeof(CONTACT_PAGE_MAGIC)) != 0
|| src[4] != CONTACT_PAGE_VERSION || src[5] != expected_page
|| readLE16(&src[6]) != CONTACT_RECORD_SIZE) {
return false;
}
header.page_index = src[5];
header.occupied = readLE32(&src[8]);
header.generation = readLE32(&src[12]);
header.payload_crc = readLE32(&src[16]);
return (header.occupied & ~valid_slots) == 0;
}
class DirtyPageSet {
uint32_t _bits;
public:
DirtyPageSet() : _bits(0) {}
void clearAll() { _bits = 0; }
bool empty() const { return _bits == 0; }
uint32_t bits() const { return _bits; }
bool mark(uint8_t page) {
if (page >= CONTACT_PAGE_COUNT) return false;
_bits |= (1UL << page);
return true;
}
int first() const {
for (uint8_t page = 0; page < CONTACT_PAGE_COUNT; page++) {
if ((_bits & (1UL << page)) != 0) return page;
}
return -1;
}
void clear(uint8_t page) {
if (page < CONTACT_PAGE_COUNT) _bits &= ~(1UL << page);
}
};
class ContactSlotMap {
uint32_t _used[CONTACT_PAGE_COUNT];
public:
ContactSlotMap() { clear(); }
void clear() { memset(_used, 0, sizeof(_used)); }
uint32_t pageMask(uint8_t page) const {
return page < CONTACT_PAGE_COUNT ? _used[page] : 0;
}
bool isUsed(uint16_t slot) const {
if (slot >= CONTACT_PAGE_COUNT * CONTACTS_PER_PAGE) return false;
return (_used[slot / CONTACTS_PER_PAGE]
& (1UL << (slot % CONTACTS_PER_PAGE))) != 0;
}
bool reserve(uint16_t slot) {
if (slot >= CONTACT_PAGE_COUNT * CONTACTS_PER_PAGE) return false;
const uint8_t page = slot / CONTACTS_PER_PAGE;
const uint32_t bit = 1UL << (slot % CONTACTS_PER_PAGE);
if ((_used[page] & bit) != 0) return false;
_used[page] |= bit;
return true;
}
uint16_t allocate() {
const uint32_t valid_slots = (1UL << CONTACTS_PER_PAGE) - 1UL;
for (uint8_t page = 0; page < CONTACT_PAGE_COUNT; page++) {
if ((_used[page] & valid_slots) == valid_slots) continue;
for (uint8_t index = 0; index < CONTACTS_PER_PAGE; index++) {
const uint32_t bit = 1UL << index;
if ((_used[page] & bit) == 0) {
_used[page] |= bit;
return (uint16_t)page * CONTACTS_PER_PAGE + index;
}
}
}
return CONTACT_SLOT_NONE;
}
bool release(uint16_t slot) {
if (!isUsed(slot)) return false;
_used[slot / CONTACTS_PER_PAGE] &= ~(1UL << (slot % CONTACTS_PER_PAGE));
return true;
}
};
} // namespace storage
} // namespace mesh