#include "adapter_configuration.h" #include "configuration_storage.h" #include "configuration_transaction.h" #include #include #include namespace { constexpr size_t kSectorSize = 4096; constexpr size_t kPageSize = 256; struct FakeFlash { uint8_t bytes[CONFIGURATION_STORAGE_COPY_COUNT][kSectorSize]; int successful_programs = 0; int fail_after_programs = -1; FakeFlash() { memset(bytes, 0xff, sizeof(bytes)); } }; void require(bool condition, const char* message) { if (!condition) { std::cerr << message << '\n'; std::exit(1); } } bool fake_read(void* context, uint8_t copy, size_t offset, uint8_t* output, size_t size) { auto* flash = static_cast(context); if (copy >= CONFIGURATION_STORAGE_COPY_COUNT || offset + size > kSectorSize) { return false; } memcpy(output, &flash->bytes[copy][offset], size); return true; } bool fake_erase(void* context, uint8_t copy) { auto* flash = static_cast(context); if (copy >= CONFIGURATION_STORAGE_COPY_COUNT) { return false; } memset(flash->bytes[copy], 0xff, kSectorSize); return true; } bool fake_program(void* context, uint8_t copy, size_t offset, const uint8_t* data, size_t size) { auto* flash = static_cast(context); if (copy >= CONFIGURATION_STORAGE_COPY_COUNT || size != kPageSize || offset + size > kSectorSize) { return false; } if (flash->fail_after_programs >= 0 && flash->successful_programs >= flash->fail_after_programs) { return false; } for (size_t index = 0; index < size; ++index) { flash->bytes[copy][offset + index] &= data[index]; } ++flash->successful_programs; return true; } ConfigurationStorageIo fake_io(FakeFlash* flash) { return { flash, kSectorSize, kPageSize, fake_read, fake_erase, fake_program, }; } void test_schema_encoding() { AdapterConfiguration configuration{}; configuration.pairing_window_seconds = 90; uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{}; require(adapter_configuration_encode(configuration, payload, sizeof(payload)), "valid configuration did not encode"); AdapterConfiguration decoded{}; require(adapter_configuration_decode(payload, sizeof(payload), &decoded) && decoded.pairing_window_seconds == 90, "configuration did not round trip"); payload[2] = 1; require(!adapter_configuration_decode(payload, sizeof(payload), &decoded), "nonzero reserved configuration byte was accepted"); configuration.pairing_window_seconds = 9; require(!adapter_configuration_encode(configuration, payload, sizeof(payload)), "out-of-range pairing window was accepted"); } void test_two_copy_recovery() { FakeFlash flash; ConfigurationStorage store; require(store.initialize(fake_io(&flash)), "storage did not initialize"); require(!store.snapshot().valid, "erased storage appeared valid"); const uint8_t first[] = {1, 2, 3, 4}; require(store.commit(1, first, sizeof(first)) == ConfigurationStorageResult::kOk && store.snapshot().generation == 1, "first generation did not commit"); const int programs_after_first = flash.successful_programs; require(store.commit(1, first, sizeof(first)) == ConfigurationStorageResult::kUnchanged && flash.successful_programs == programs_after_first, "identical configuration consumed a flash write"); const uint8_t second[] = {5, 6, 7, 8}; require(store.commit(1, second, sizeof(second)) == ConfigurationStorageResult::kOk && store.snapshot().generation == 2, "second generation did not commit"); ConfigurationStorage after_power_cycle; require(after_power_cycle.initialize(fake_io(&flash)) && after_power_cycle.snapshot().generation == 2 && memcmp(after_power_cycle.snapshot().payload, second, sizeof(second)) == 0, "latest generation did not survive reinitialization"); flash.bytes[1][CONFIGURATION_STORAGE_RECORD_HEADER_SIZE] ^= 0x01; ConfigurationStorage after_corruption; require(after_corruption.initialize(fake_io(&flash)) && after_corruption.snapshot().generation == 1 && memcmp(after_corruption.snapshot().payload, first, sizeof(first)) == 0, "corrupt newest generation did not roll back"); } void test_interrupted_write_retains_previous_generation() { FakeFlash flash; ConfigurationStorage store; require(store.initialize(fake_io(&flash)), "storage did not initialize for interruption test"); const uint8_t first[] = {9, 8, 7, 6}; require(store.commit(1, first, sizeof(first)) == ConfigurationStorageResult::kOk, "baseline generation did not commit"); uint8_t large[300]; memset(large, 0x5a, sizeof(large)); flash.fail_after_programs = flash.successful_programs + 1; require(store.commit(1, large, sizeof(large)) == ConfigurationStorageResult::kIoError, "interrupted multi-page write reported success"); flash.fail_after_programs = -1; ConfigurationStorage recovered; require(recovered.initialize(fake_io(&flash)) && recovered.snapshot().generation == 1 && recovered.snapshot().payload_size == sizeof(first) && memcmp(recovered.snapshot().payload, first, sizeof(first)) == 0, "interrupted write replaced the previous generation"); } void test_transaction_validation() { AdapterConfiguration configuration{}; configuration.pairing_window_seconds = 120; uint8_t payload[ADAPTER_CONFIGURATION_ENCODED_SIZE]{}; require(adapter_configuration_encode(configuration, payload, sizeof(payload)), "transaction payload did not encode"); const uint32_t crc = configuration_crc32(payload, sizeof(payload)); ConfigurationTransaction transaction; require(transaction.begin(7, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload), crc) == ConfigurationTransactionStatus::kReceiving, "valid transaction did not begin"); require(transaction.append(7, 1, payload, 1) == ConfigurationTransactionStatus::kOutOfOrder, "out-of-order chunk was accepted"); require(transaction.begin(8, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload), crc ^ 1u) == ConfigurationTransactionStatus::kReceiving, "replacement transaction did not begin"); require(transaction.append(8, 0, payload, 2) == ConfigurationTransactionStatus::kReceiving && transaction.append(8, 2, &payload[2], 2) == ConfigurationTransactionStatus::kReceiving, "ordered chunks were rejected"); require(transaction.finish(8) == ConfigurationTransactionStatus::kBadCrc, "bad transaction CRC was accepted"); require(transaction.begin(9, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload), crc) == ConfigurationTransactionStatus::kReceiving && transaction.append(9, 0, payload, sizeof(payload)) == ConfigurationTransactionStatus::kReceiving && transaction.finish(9) == ConfigurationTransactionStatus::kPending, "valid transaction did not reach pending commit"); require(transaction.begin(10, ADAPTER_CONFIGURATION_SCHEMA_VERSION, sizeof(payload), crc) == ConfigurationTransactionStatus::kBusy, "pending transaction was replaced"); transaction.set_result(ConfigurationTransactionStatus::kCommitted, 4, crc); require(transaction.begin(11, 99, sizeof(payload), crc) == ConfigurationTransactionStatus::kUnsupportedSchema, "unsupported schema was accepted"); require(transaction.begin( 12, ADAPTER_CONFIGURATION_SCHEMA_VERSION, CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE + 1, crc) == ConfigurationTransactionStatus::kTooLarge, "oversized transaction was accepted"); } } // namespace int main() { test_schema_encoding(); test_two_copy_recovery(); test_interrupted_write_retains_previous_generation(); test_transaction_validation(); return 0; }