#pragma once #include #include #include 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