Files
MeshCore/test/fixtures/ota_sd_cache/test.cpp
T

289 lines
14 KiB
C++

#include <SdFat.h>
#include <helpers/ota/OtaCacheSdNrf52.h>
#include <helpers/ota/OtaStoreSdNrf52.h>
#include <helpers/ota/MotaContainer.h>
#include <helpers/ota/OtaManager.h>
#include <helpers/ota/MerkleTree.h>
#include <helpers/ota/Multihash.h>
#include <helpers/ota/OtaByteIO.h>
#include "../../test_ota/mota_vectors.h"
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <deque>
using namespace mesh::ota;
// The existing SD store owns the shared mounted volume. Replace only that
// hardware boundary: no boot authorization, flash writes, or apply call exists
// in this executable. The real archive implementation is compiled separately.
namespace mesh { namespace ota {
OtaStoreSdNrf52::OtaStoreSdNrf52() : _sd(new OtaSdFs), _file(new OtaSdFile) {}
OtaStoreSdNrf52::~OtaStoreSdNrf52() { delete _file; delete _sd; }
bool OtaStoreSdNrf52::mount() {
if (_mounted) return true;
_mounted = _sd && _sd->begin();
if (!_mounted) fail("SD mount failed");
return _mounted;
}
bool OtaStoreSdNrf52::busAccessAllowed() const { return _mounted && sd_test::medium.connected; }
void OtaStoreSdNrf52::fail(const char* text) { snprintf(_error, sizeof(_error), "%s", text); }
bool OtaStoreSdNrf52::begin(uint32_t) { assert(false); return false; }
bool OtaStoreSdNrf52::write(uint32_t, const uint8_t*, uint32_t) { assert(false); return false; }
bool OtaStoreSdNrf52::read(uint32_t, uint8_t*, uint32_t) const { assert(false); return false; }
uint32_t OtaStoreSdNrf52::capacity() const { return 0; }
void OtaStoreSdNrf52::clear() { assert(false); }
bool OtaStoreSdNrf52::discard() { assert(false); return false; }
bool OtaStoreSdNrf52::set_meta_size(uint32_t) { assert(false); return false; }
bool OtaStoreSdNrf52::finalize() { assert(false); return false; }
void OtaStoreSdNrf52::checkpoint() { assert(false); }
bool OtaStoreSdNrf52::reopen() { assert(false); return false; }
bool OtaStoreSdNrf52::plan_layout(bool, uint32_t, uint32_t, uint32_t, bool) {
assert(false); return false;
}
} }
static const uint8_t* const bytes = SIM_MOTA_2K;
static constexpr uint32_t length = SIM_MOTA_2K_LEN;
static const uint8_t* const mid = SIM_MOTA_2K + 8 + 20;
static std::string path(const char* ext) {
char out[40];
snprintf(out, sizeof(out), "/mota/%02x%02x%02x%02x.%s", mid[0], mid[1], mid[2], mid[3], ext);
return out;
}
static void attach(OtaCacheSdNrf52& cache, OtaStoreSdNrf52& owner) {
cache.attach(owner); cache.set_mid(mid);
}
static void capture(OtaCacheSdNrf52& cache) {
assert(cache.begin(length));
assert(cache.set_meta_size(8 + MOTA_MFL + SIM_MOTA_2K_BLOCKS * 4));
assert(cache.write(0, bytes, length));
}
static void require_valid_fixture() {
MotaManifest parsed;
assert(mota_parse(bytes, length, parsed) && parsed.is_signed());
assert(mota_check_root(parsed) && mota_check_payload(parsed) && mota_check_image_hash_full(parsed));
}
static void published(OtaCacheSdNrf52& cache) {
assert(cache.count() == 1 && cache.contains(mid));
MotaDesc d;
assert(cache.describe(0, d) && !cache.describe(1, d));
assert(d.target_id == SIM_TARGET_ID && d.total_size == length && d.block_count == 2);
assert(d.block_size_log2 == 11 && d.flags == (MFLAG_FULL | MFLAG_SIGNED));
MotaManifest manifest;
assert(mota_parse_manifest(bytes + 8, MOTA_MFL, manifest));
assert(!memcmp(d.mid, manifest.merkle_root, 4));
assert(d.fw_version == manifest.fw_version && d.codec_id == manifest.codec_id);
assert(d.source_caps == 0 && d.leaves_off == 8 + MOTA_MFL);
assert(d.payload_off == d.leaves_off + manifest.block_count * 4);
assert(d.payload_size == manifest.payload_size);
std::vector<uint8_t> loaded(length);
assert(cache.read(0, 0, loaded.data(), length));
assert(!memcmp(loaded.data(), bytes, length));
assert(!cache.read(0, length - 1, loaded.data(), 2));
assert(!cache.read(0, UINT32_MAX, loaded.data(), 2));
assert(!cache.read(1, 0, loaded.data(), 1));
assert(sd_test::medium.nodes.count(path("mota")) == 1);
assert(sd_test::medium.nodes.count(path("part")) == 0);
}
struct Link {
OtaManager* peer = nullptr;
std::deque<std::vector<uint8_t>> packets;
};
static bool send(void* context, const uint8_t* data, uint16_t count, bool) {
static_cast<Link*>(context)->packets.emplace_back(data, data + count);
return true;
}
static void deliver(Link& link) {
while (!link.packets.empty()) {
auto data = std::move(link.packets.front()); link.packets.pop_front();
link.peer->on_message(data.data(), static_cast<uint16_t>(data.size()));
}
}
static std::vector<uint8_t> distinct_image(uint8_t value) {
std::vector<uint8_t> image(bytes, bytes + length);
image[9] = MFLAG_FULL; // distinct unsigned fixtures, never a forged signature
memset(image.data() + 8 + MOTA_OFF_SIGNER, 0, 32 + 64);
MotaManifest manifest;
assert(mota_parse(image.data(), image.size(), manifest));
auto* payload = const_cast<uint8_t*>(manifest.payload);
payload[0] = value;
for (uint32_t block = 0; block < manifest.block_count; ++block) {
const uint32_t offset = block * manifest.block_size();
const uint32_t count = std::min(manifest.block_size(), manifest.payload_size - offset);
merkle_leaf(const_cast<uint8_t*>(manifest.leaves) + block * 4, payload + offset, count);
}
merkle_root(const_cast<uint8_t*>(manifest.merkle_root), manifest.leaves, manifest.block_count);
mh32(const_cast<uint8_t*>(manifest.image_hash), payload, manifest.payload_size);
assert(mota_check_root(manifest) && mota_check_payload(manifest) && mota_check_image_hash_full(manifest));
return image;
}
int main(int argc, char** argv) {
assert(argc == 2);
const int scenario = atoi(argv[1]);
sd_test::medium.reset(); require_valid_fixture();
if (scenario == 0) {
{
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
capture(cache); assert(cache.count() == 0); assert(cache.finalize()); published(cache);
assert(!cache.begin(length)); // duplicate must not truncate the good archive
}
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 rebooted; attach(rebooted, owner);
assert(rebooted.initialize()); published(rebooted);
} else if (scenario == 1) {
{
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
assert(cache.begin(length)); assert(cache.write(0, bytes, 8 + MOTA_MFL));
cache.checkpoint(); assert(cache.count() == 0);
}
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 resumed; attach(resumed, owner);
assert(resumed.initialize() && resumed.count() == 0 && resumed.reopen());
assert(resumed.staged_size() == length);
uint8_t header[8]; assert(resumed.read(0, header, sizeof(header)));
assert(!memcmp(header, bytes, sizeof(header)));
assert(resumed.write(0, bytes, length) && resumed.finalize()); published(resumed);
} else if (scenario == 2) {
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner); capture(cache);
sd_test::medium.rename_ok = false;
assert(!cache.finalize() && cache.count() == 0);
assert(sd_test::medium.nodes.count(path("part")) == 1);
sd_test::medium.rename_ok = true;
assert(cache.reopen() && cache.finalize()); published(cache);
} else if (scenario == 3) {
for (int kind = 0; kind < 3; ++kind) {
sd_test::medium.reset();
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner); capture(cache);
if (kind == 0) sd_test::medium.file_sync_ok = false;
else sd_test::medium.fail_card_sync_at = kind;
assert(!cache.finalize());
if (kind == 2) {
// The rename occurred, but its subsequent device sync failed. Valid
// published data remains discoverable; do not pretend the call succeeded.
assert(cache.count() == 1); published(cache);
} else {
assert(cache.count() == 0 && sd_test::medium.nodes.count(path("part")) == 1);
sd_test::medium.file_sync_ok = true; sd_test::medium.fail_card_sync_at = 0;
assert(cache.finalize()); published(cache);
}
}
} else if (scenario == 4) {
for (int kind = 0; kind < 5; ++kind) {
sd_test::medium.reset();
auto& m = sd_test::medium;
if (kind == 0) m.connected = false;
if (kind == 1) m.mount_ok = false;
if (kind == 2) m.free_clusters = 1;
if (kind == 3) m.free_clusters = m.clusters + 1;
if (kind == 4) m.cluster_bytes = 0;
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
assert(!cache.begin(length) && cache.staged_size() == 0 && cache.count() == 0);
assert(!m.nodes.count(path("part")) && cache.last_error()[0]);
}
} else if (scenario == 5) {
for (bool writes : {true, false}) {
sd_test::medium.reset(); sd_test::medium.preallocate_ok = false;
sd_test::medium.write_ok = writes;
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
if (writes) { capture(cache); assert(cache.finalize()); published(cache); }
else {
assert(!cache.begin(length) && cache.staged_size() == 0);
assert(!sd_test::medium.nodes.count(path("part")));
}
}
} else if (scenario == 6) {
for (int kind = 0; kind < 5; ++kind) {
sd_test::medium.reset();
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner); capture(cache);
auto& data = sd_test::medium.nodes[path("part")].bytes;
if (kind == 0) data[0] ^= 1; // bad magic
if (kind == 1) data.back() ^= 1; // bad trailer
if (kind == 2) data[8 + 20] ^= 1; // MID not selected
if (kind == 3) data[4] ^= 1; // declared length mismatch
if (kind == 4) data[8 + 2] = 0; // unknown hash algorithm
assert(!cache.finalize() && cache.count() == 0);
assert(sd_test::medium.nodes.count(path("part")) == 1);
// Malformed complete files must not be listed even after reboot/rescan.
auto saved = std::move(sd_test::medium.nodes[path("part")]);
sd_test::medium.nodes.erase(path("part"));
sd_test::medium.nodes[path("mota")] = std::move(saved);
assert(cache.rescan() && cache.count() == 0 && !cache.contains(mid));
}
} else if (scenario == 7) {
{
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
capture(cache); assert(cache.finalize()); assert(cache.setAutoCaptureEnabled(false));
assert(!cache.autoCaptureEnabled()); published(cache);
}
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
assert(cache.initialize() && !cache.autoCaptureEnabled()); published(cache);
assert(cache.setAutoCaptureEnabled(true) && cache.autoCaptureEnabled());
assert(!sd_test::medium.nodes.count("/mota/cache.off"));
} else if (scenario == 8) {
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
assert(cache.initialize());
// A malformed existing final cannot suppress clean replacement. Keep the
// bad bytes under quarantine instead of silently erasing operator evidence.
sd_test::medium.nodes[path("mota")] = {false, {1, 2, 3}};
capture(cache);
assert(sd_test::medium.nodes.count(path("bad")) == 1);
assert(cache.finalize()); published(cache);
} else if (scenario == 9) {
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
capture(cache); assert(cache.finalize());
OtaManager seeder, receiver;
OtaStoreRam<8192> destination;
Link outbound{&receiver, {}}, inbound{&seeder, {}};
seeder.begin(0, send, &outbound);
receiver.begin(SIM_TARGET_ID, send, &inbound);
receiver.set_fetch_store(&destination);
assert(seeder.add_source(&cache) && seeder.servedCount() == 1);
assert(receiver.pull(mid, SIM_TARGET_ID) == OtaManager::PULL_STARTED);
for (uint32_t now = 100; now <= 300000 && receiver.fetchState() != OtaManager::COMPLETE;
now += 100) {
seeder.set_clock(now); receiver.set_clock(now);
deliver(inbound); deliver(outbound);
seeder.serviceEgress(); receiver.serviceEgress();
seeder.loop(); receiver.loop();
assert(receiver.fetchState() != OtaManager::FAILED);
}
assert(receiver.fetchState() == OtaManager::COMPLETE);
assert(destination.staged_size() == length && !memcmp(destination.data(), bytes, length));
MotaManifest fetched;
assert(mota_parse(destination.data(), destination.staged_size(), fetched));
assert(mota_check_root(fetched) && mota_check_payload(fetched));
} else if (scenario == 10) {
static_assert(OTA_SD_CACHE_MAX == 16, "fixture exercises the exact optional profile cap");
sd_test::medium.nodes["/mota"] = {true, {}};
std::string first;
for (unsigned i = 0; i < OTA_SD_CACHE_MAX; ++i) {
auto image = distinct_image(static_cast<uint8_t>(i));
const uint8_t* image_mid = image.data() + 8 + 20;
char name[40]; snprintf(name, sizeof(name), "/mota/%02x%02x%02x%02x.mota",
image_mid[0], image_mid[1], image_mid[2], image_mid[3]);
if (i == 0) first = name;
sd_test::medium.nodes[name] = {false, std::move(image)};
}
// A previously interrupted seventeenth capture must remain untouched when
// no advertised slot is available, rather than being deleted by begin().
sd_test::medium.nodes[path("part")] = {false, std::vector<uint8_t>(bytes, bytes + length)};
OtaStoreSdNrf52 owner; OtaCacheSdNrf52 cache; attach(cache, owner);
assert(cache.initialize() && cache.count() == OTA_SD_CACHE_MAX);
const auto original = sd_test::medium.nodes[path("part")].bytes;
const auto count = sd_test::medium.nodes.size();
assert(!cache.begin(length));
assert(strstr(cache.last_error(), "entry limit"));
assert(sd_test::medium.nodes.size() == count);
assert(sd_test::medium.nodes[path("part")].bytes == original);
// Once an operator makes room, the existing partial can be resumed and
// published without re-creating or truncating it.
sd_test::medium.nodes.erase(first);
assert(cache.rescan() && cache.count() == OTA_SD_CACHE_MAX - 1);
assert(cache.reopen() && cache.finalize() && cache.count() == OTA_SD_CACHE_MAX);
assert(cache.contains(mid) && !sd_test::medium.nodes.count(path("part")));
} else return 2;
puts("PASS: production SD archive boundary");
}