318 lines
13 KiB
C++
318 lines
13 KiB
C++
// Compile the actual RegionMap declarations and selected implementation bodies
|
|
// with the real platform transaction helper, not a second serializer.
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <helpers/RegionMap.h>
|
|
#include <helpers/RegionNameUtils.h>
|
|
#include <helpers/TxtDataHelpers.h>
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
|
|
#include <helpers/AtomicFileWriter.h>
|
|
#else
|
|
#include <contact_transaction_under_test.h>
|
|
#if defined(ESP32_PLATFORM)
|
|
#include <helpers/FilePresence.h>
|
|
#endif
|
|
#endif
|
|
|
|
#include "region_under_test.inc"
|
|
|
|
static void require(bool condition, const char* message) {
|
|
if (!condition) {
|
|
std::fprintf(stderr, "FAIL: %s\n", message);
|
|
std::exit(1);
|
|
}
|
|
}
|
|
|
|
static void select(MemoryFS& fs) { metadata_filesystem = &fs; }
|
|
|
|
static bool has_policy(RegionMap& regions, bool candidate) {
|
|
if (regions.getCount() != (candidate ? 4 : 3)) return false;
|
|
const auto* root = regions.findById(11);
|
|
const auto* private_region = regions.findById(27);
|
|
const auto* local = regions.findById(35);
|
|
const auto* added = regions.findById(50);
|
|
if (!root || !private_region || !local) return false;
|
|
if (std::strcmp(root->name, "#Mountain") || root->parent != 0
|
|
|| root->flags != (candidate ? 0 : REGION_DENY_FLOOD | REGION_DENY_DIRECT)) return false;
|
|
if (std::strcmp(private_region->name, "$Private") || private_region->parent != 11
|
|
|| private_region->flags != REGION_DENY_DIRECT) return false;
|
|
if (std::strcmp(local->name, "Local") || local->parent != 0
|
|
|| local->flags != REGION_DENY_FLOOD) return false;
|
|
if (candidate && (!added || std::strcmp(added->name, "Candidate")
|
|
|| added->parent != 27 || added->flags != 0)) return false;
|
|
if (!candidate && added) return false;
|
|
if (!regions.getDefaultRegion() || !regions.getHomeRegion()) return false;
|
|
return regions.getWildcard().flags == (candidate ? 0 : REGION_DENY_FLOOD | REGION_DENY_DIRECT)
|
|
&& regions.getDefaultRegion()->id == (candidate ? 35 : 11)
|
|
&& regions.getHomeRegion()->id == (candidate ? 11 : 27);
|
|
}
|
|
|
|
struct Scenario {
|
|
TransportKeyStore keys;
|
|
MemoryFS fs;
|
|
RegionMap original{keys};
|
|
RegionMap candidate{keys};
|
|
std::vector<uint8_t> baseline;
|
|
std::vector<uint8_t> replacement;
|
|
std::string path;
|
|
|
|
explicit Scenario(const char* target = "/regions2") : path(target) {
|
|
select(fs);
|
|
RegionEntry* root = original.putRegion("#Mountain", 0, 11);
|
|
RegionEntry* private_region = original.putRegion("$Private", 11, 27);
|
|
RegionEntry* local = original.putRegion("Local", 0, 35);
|
|
require(root && private_region && local, "create baseline region hierarchy");
|
|
root->flags = REGION_DENY_FLOOD | REGION_DENY_DIRECT;
|
|
private_region->flags = REGION_DENY_DIRECT;
|
|
local->flags = REGION_DENY_FLOOD;
|
|
original.getWildcard().flags = REGION_DENY_FLOOD | REGION_DENY_DIRECT;
|
|
original.setDefaultRegion(root);
|
|
original.setHomeRegion(private_region);
|
|
require(original.save(&fs, path.c_str()), "write baseline regions");
|
|
baseline = fs.files.at(path);
|
|
require(baseline.size() == 10 + 3 * 164, "legacy region image size unchanged");
|
|
require(candidate.load(&fs, path.c_str()), "load candidate from durable baseline");
|
|
candidate.findById(11)->flags = 0;
|
|
candidate.getWildcard().flags = 0;
|
|
candidate.setDefaultRegion(candidate.findById(35));
|
|
candidate.setHomeRegion(candidate.findById(11));
|
|
RegionEntry* added = candidate.putRegion("Candidate", 27, 50);
|
|
require(added != nullptr, "create candidate region");
|
|
added->flags = 0;
|
|
MemoryFS healthy;
|
|
select(healthy);
|
|
require(candidate.save(&healthy, path.c_str()), "write candidate reference");
|
|
replacement = healthy.files.at(path);
|
|
select(fs);
|
|
}
|
|
|
|
void reboot(MemoryFS& storage, bool expected_candidate = false) {
|
|
select(storage);
|
|
RegionMap restarted(keys);
|
|
const bool loaded = restarted.load(&storage, path.c_str());
|
|
require(loaded && has_policy(restarted, expected_candidate),
|
|
expected_candidate ? "durable candidate region/default/home/wildcard policy survived"
|
|
: "durable old region/default/home/wildcard policy survived");
|
|
// The next auto-assigned ID is also durable state, not merely recomputed
|
|
// policy. Explicit IDs above leave 36 or 51 as the next allocation.
|
|
const RegionEntry* next = restarted.putRegion("AfterReboot", 0);
|
|
require(next && next->id == (expected_candidate ? 51 : 36),
|
|
"durable next region ID survived");
|
|
require(storage.files.at(path) == (expected_candidate ? replacement : baseline),
|
|
"exact durable public image retained");
|
|
}
|
|
};
|
|
|
|
static void healthy_save_and_reboot() {
|
|
Scenario test;
|
|
test.reboot(test.fs);
|
|
require(test.original.save(&test.fs), "repeated healthy region save");
|
|
test.reboot(test.fs);
|
|
require(test.candidate.save(&test.fs), "publish changed region policy");
|
|
test.reboot(test.fs, true);
|
|
require(!test.fs.exists("/regions2.tmp") && !test.fs.exists("/regions2.bak"),
|
|
"successful save cleaned transaction siblings");
|
|
}
|
|
|
|
static void invalid_inputs_do_not_mutate() {
|
|
Scenario test;
|
|
const auto before = test.fs.files;
|
|
const unsigned opens = test.fs.open_writes;
|
|
const unsigned removes = test.fs.remove_calls;
|
|
const unsigned renames = test.fs.rename_calls;
|
|
require(!test.candidate.save(nullptr), "null filesystem save refused");
|
|
require(!test.candidate.load(nullptr), "null filesystem load refused");
|
|
require(!test.candidate.save(&test.fs, ""), "empty target save refused");
|
|
require(!test.candidate.load(&test.fs, ""), "empty target load refused");
|
|
require(test.fs.files == before && test.fs.open_writes == opens
|
|
&& test.fs.remove_calls == removes && test.fs.rename_calls == renames,
|
|
"invalid input left namespace untouched");
|
|
require(has_policy(test.candidate, true), "invalid input retained caller's RAM policy");
|
|
test.reboot(test.fs);
|
|
}
|
|
|
|
static void refusal_and_short_writes() {
|
|
Scenario test;
|
|
{
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.fail_write_open = true;
|
|
require(!test.candidate.save(&storage), "replacement open refusal reported");
|
|
storage.fail_write_open = false;
|
|
test.reboot(storage);
|
|
}
|
|
// Exhaust every byte offset. This includes every reserved/header field,
|
|
// every region field/padding boundary, and clean record-boundary prefixes.
|
|
for (size_t boundary = 0; boundary < test.replacement.size(); ++boundary) {
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.fail_write_after = static_cast<int>(boundary);
|
|
require(!test.candidate.save(&storage), "ordinary short write reported");
|
|
storage.fail_write_after = -1;
|
|
test.reboot(storage);
|
|
require(!storage.exists("/regions2.tmp"), "short write cleaned staging file");
|
|
require(has_policy(test.candidate, true), "save failure retained caller's RAM policy");
|
|
}
|
|
// A full filesystem is independent of the File::write short-write hook:
|
|
// committed bytes consume capacity before each staging write is attempted.
|
|
for (size_t available = 0; available < test.replacement.size(); ++available) {
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.capacity = storage.used() + available;
|
|
require(!test.candidate.save(&storage), "full-storage write failure reported");
|
|
storage.capacity = std::numeric_limits<size_t>::max();
|
|
test.reboot(storage);
|
|
}
|
|
std::printf("PASS: %zu short-write offsets and %zu full-storage capacities\n",
|
|
test.replacement.size(), test.replacement.size());
|
|
}
|
|
|
|
static void staged_readback_and_publication_failure() {
|
|
Scenario test;
|
|
{
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.fail_read_open = true;
|
|
require(!test.candidate.save(&storage), "staged readback refusal reported");
|
|
storage.fail_read_open = false;
|
|
test.reboot(storage);
|
|
}
|
|
{
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.failed_rename_sources.insert("/regions2.tmp");
|
|
require(!test.candidate.save(&storage), "known unchanged publication reported");
|
|
storage.failed_rename_sources.clear();
|
|
test.reboot(storage);
|
|
}
|
|
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
|
{
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.failed_rename_sources.insert("/regions2");
|
|
require(!test.candidate.save(&storage), "backup move refusal reported");
|
|
storage.failed_rename_sources.clear();
|
|
test.reboot(storage);
|
|
}
|
|
{
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.failed_rename_sources.insert("/regions2.tmp");
|
|
storage.failed_rename_sources.insert("/regions2.bak");
|
|
require(!test.candidate.save(&storage), "failed publication and rollback reported");
|
|
require(!storage.exists("/regions2") && storage.files.at("/regions2.bak") == test.baseline,
|
|
"rollback failure retained old recovery image");
|
|
storage.failed_rename_sources.clear();
|
|
test.reboot(storage);
|
|
}
|
|
{
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
require(storage.rename("/regions2", "/regions2.bak"), "simulate reset in backup gap");
|
|
storage.files["/regions2.tmp"] = test.replacement;
|
|
test.reboot(storage);
|
|
require(!storage.exists("/regions2.bak"), "fresh load recovered backup gap");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void custom_path_isolation() {
|
|
Scenario test("/regions-custom");
|
|
test.fs.files["/regions2"] = {0x12, 0x34};
|
|
test.fs.files["/regions2.tmp"] = {0x56};
|
|
test.fs.files["/regions2.bak"] = {0x78};
|
|
const auto unrelated = test.fs.files;
|
|
test.fs.fail_write_after = 9;
|
|
require(!test.candidate.save(&test.fs, test.path.c_str()), "custom path short write reported");
|
|
test.fs.fail_write_after = -1;
|
|
test.reboot(test.fs);
|
|
for (const char* name : {"/regions2", "/regions2.tmp", "/regions2.bak"}) {
|
|
require(test.fs.files.at(name) == unrelated.at(name), "custom path left default artifacts alone");
|
|
}
|
|
#if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
|
|
require(test.fs.rename(test.path.c_str(), (test.path + ".bak").c_str()),
|
|
"simulate custom backup gap");
|
|
test.fs.files[test.path + ".tmp"] = test.replacement;
|
|
test.reboot(test.fs);
|
|
for (const char* name : {"/regions2", "/regions2.tmp", "/regions2.bak"}) {
|
|
require(test.fs.files.at(name) == unrelated.at(name), "custom recovery left default artifacts alone");
|
|
}
|
|
// ContactFileTransaction has 48 bytes for each sibling pathname, including
|
|
// its suffix and terminator. Reject overlong paths before any recovery or
|
|
// staging can address a truncated name belonging to another file.
|
|
const std::string limit_path = "/" + std::string(42, 'x');
|
|
Scenario limit(limit_path.c_str());
|
|
require(limit.candidate.save(&limit.fs, limit.path.c_str()), "43-byte custom target fits");
|
|
limit.reboot(limit.fs, true);
|
|
for (size_t length : {size_t(44), size_t(80), size_t(128)}) {
|
|
const std::string path = "/" + std::string(length - 1, 'y');
|
|
MemoryFS storage;
|
|
select(storage);
|
|
storage.files[path] = test.baseline;
|
|
storage.files[path + ".tmp"] = test.replacement;
|
|
storage.files[path + ".bak"] = test.baseline;
|
|
storage.files[(path + ".tmp").substr(0, 47)] = {0x66};
|
|
storage.files[(path + ".bak").substr(0, 47)] = {0x77};
|
|
const auto before = storage.files;
|
|
require(!test.candidate.save(&storage, path.c_str()), "overlong custom save refused");
|
|
RegionMap restarted(test.keys);
|
|
require(!restarted.load(&storage, path.c_str()), "overlong custom load refused");
|
|
require(storage.files == before && storage.open_writes == 0
|
|
&& storage.remove_calls == 0 && storage.rename_calls == 0,
|
|
"overlong custom path left all complete and truncated artifacts untouched");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#if defined(ESP32_PLATFORM)
|
|
static void esp_metadata_errors_fail_closed() {
|
|
Scenario test;
|
|
for (const char* failed : {"/regions2", "/regions2.bak", "/regions2.tmp"}) {
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
storage.files["/regions2.tmp"] = {0x61};
|
|
const auto before = storage.files;
|
|
storage.metadata_errors.insert(failed);
|
|
require(!test.candidate.save(&storage), "ESP metadata error refused save");
|
|
require(storage.files == before && storage.open_writes == test.fs.open_writes
|
|
&& storage.remove_calls == test.fs.remove_calls
|
|
&& storage.rename_calls == test.fs.rename_calls,
|
|
"ESP metadata error caused no namespace mutation");
|
|
storage.metadata_errors.clear();
|
|
test.reboot(storage);
|
|
}
|
|
for (const char* failed : {"/regions2", "/regions2.bak"}) {
|
|
MemoryFS storage = test.fs;
|
|
select(storage);
|
|
require(storage.rename("/regions2", "/regions2.bak"), "create metadata recovery gap");
|
|
storage.files["/regions2.tmp"] = test.replacement;
|
|
storage.metadata_errors.insert(failed);
|
|
const auto before = storage.files;
|
|
RegionMap restarted(test.keys);
|
|
require(!restarted.load(&storage), "ESP metadata error refused recovery load");
|
|
require(storage.files == before, "ESP metadata error retained backup and candidate bytes");
|
|
storage.metadata_errors.clear();
|
|
test.reboot(storage);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc == 2 && std::strcmp(argv[1], "short-write") == 0) {
|
|
refusal_and_short_writes();
|
|
return 0;
|
|
}
|
|
healthy_save_and_reboot();
|
|
invalid_inputs_do_not_mutate();
|
|
refusal_and_short_writes();
|
|
staged_readback_and_publication_failure();
|
|
custom_path_isolation();
|
|
#if defined(ESP32_PLATFORM)
|
|
esp_metadata_errors_fail_closed();
|
|
#endif
|
|
std::puts("PASS: actual RegionMap durable transaction and reboot policy");
|
|
}
|