339 lines
14 KiB
C++
339 lines
14 KiB
C++
#include <helpers/ota/FolderMotaStore.h>
|
|
#include <helpers/ota/OtaByteIO.h>
|
|
#include <helpers/ota/OtaManager.h>
|
|
#include <helpers/ota/MotaSeederProto.h>
|
|
#include <Utils.h>
|
|
#include "test_ota/mota_vectors.h"
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
|
|
using namespace mesh::ota;
|
|
|
|
namespace mesh {
|
|
namespace ota {
|
|
struct OtaContext {
|
|
OtaManager manager;
|
|
OtaStoreRam<32768> fetch_store;
|
|
FolderMotaStore* folder_dest = nullptr;
|
|
const char* folder_dest_info = "test host";
|
|
bool apply_pending = false, fetch_to_folder = false;
|
|
};
|
|
|
|
// @CLI_HELPERS@
|
|
|
|
static bool pull_command(const char* a, char* reply, OtaContext& c) {
|
|
const char* rest = nullptr;
|
|
if (false) {
|
|
// @CLI_PULL@
|
|
} else {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
} // namespace ota
|
|
} // namespace mesh
|
|
|
|
struct HostStream : Stream {
|
|
enum Failure { None, Timeout, Truncated, BadChecksum, Error, ShortWrite, WrongOp };
|
|
std::vector<uint8_t> bytes{SIM_MOTA_1K, SIM_MOTA_1K + SIM_MOTA_1K_LEN};
|
|
std::vector<uint8_t> response;
|
|
std::vector<uint8_t> operations;
|
|
size_t position = 0;
|
|
bool present = true;
|
|
uint8_t fail_op = MS_OP_SREAD;
|
|
uint32_t fail_offset = 0;
|
|
Failure failure = None;
|
|
unsigned failures_left = 0, begins = 0, writes = 0;
|
|
|
|
int available() override { return response.size() - position; }
|
|
int read() override { return position < response.size() ? response[position++] : -1; }
|
|
int peek() override { return -1; }
|
|
void flush() override { assert(false); }
|
|
size_t write(uint8_t) override { assert(false); return 0; }
|
|
size_t write(const uint8_t* request, size_t length) override {
|
|
assert(length >= 4 && request[0] == MOTA_SEEDER_REQ_MAGIC0
|
|
&& request[1] == MOTA_SEEDER_REQ_MAGIC1);
|
|
uint8_t checksum = 0;
|
|
for (size_t i = 2; i < length - 1; ++i) checksum ^= request[i];
|
|
assert(checksum == request[length - 1]);
|
|
const uint8_t op = request[2];
|
|
operations.push_back(op);
|
|
const bool fail = failures_left && op == fail_op
|
|
&& (op != MS_OP_SREAD || rd_u32le(request + 7) == fail_offset);
|
|
if (fail) --failures_left;
|
|
response.clear();
|
|
position = 0;
|
|
if (fail && failure == ShortWrite) return length - 1;
|
|
if (fail && failure == Timeout) return length;
|
|
response = {MOTA_SEEDER_RSP_MAGIC0, MOTA_SEEDER_RSP_MAGIC1,
|
|
uint8_t(fail && failure == WrongOp ? op + 1 : op),
|
|
uint8_t(fail && failure == Error ? MS_STATUS_ERR : MS_STATUS_OK)};
|
|
if (!(fail && failure == Error)) {
|
|
if (op == MS_OP_STAT) {
|
|
response.push_back(present ? 1 : 0);
|
|
uint8_t size[4]; wr_u32le(size, bytes.size());
|
|
response.insert(response.end(), size, size + sizeof(size));
|
|
} else if (op == MS_OP_SREAD) {
|
|
const uint32_t offset = rd_u32le(request + 7);
|
|
const uint16_t count = request[11] | (uint16_t(request[12]) << 8);
|
|
assert(present && offset <= bytes.size() && count <= bytes.size() - offset);
|
|
response.insert(response.end(), bytes.begin() + offset, bytes.begin() + offset + count);
|
|
} else if (op == MS_OP_BEGIN) {
|
|
++begins;
|
|
present = true;
|
|
bytes.assign(rd_u32le(request + 7), 0xFF);
|
|
} else if (op == MS_OP_WRITE) {
|
|
++writes;
|
|
const uint32_t offset = rd_u32le(request + 7);
|
|
const uint16_t count = request[11] | (uint16_t(request[12]) << 8);
|
|
assert(present && offset <= bytes.size() && count <= bytes.size() - offset);
|
|
std::copy(request + 13, request + 13 + count, bytes.begin() + offset);
|
|
} else {
|
|
assert(op == MS_OP_FIN);
|
|
}
|
|
}
|
|
checksum = 0;
|
|
for (uint8_t value : response) checksum ^= value;
|
|
response.push_back(fail && failure == BadChecksum ? uint8_t(checksum ^ 1) : checksum);
|
|
if (fail && failure == Truncated) response.resize(4);
|
|
return length;
|
|
}
|
|
};
|
|
|
|
struct Flight {
|
|
unsigned manifests = 0;
|
|
std::vector<uint32_t> requested_blocks;
|
|
static bool send(void* context, const uint8_t* bytes, uint16_t length, bool) {
|
|
auto& flight = *static_cast<Flight*>(context);
|
|
GetManifestMsg manifest{};
|
|
if (decode_get_manifest(bytes, length, manifest)) ++flight.manifests;
|
|
ReqWindowMsg request{};
|
|
if (decode_req_window(bytes, length, request)) {
|
|
for (uint8_t i = 0; i < request.n_items; ++i)
|
|
flight.requested_blocks.push_back(request.items[i].block_idx);
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct Fixture {
|
|
HostStream host;
|
|
FolderMotaStore store{host, MotaStreamWritePolicy::NoFlush, 4};
|
|
OtaContext context;
|
|
Flight flight;
|
|
MotaManifest manifest;
|
|
|
|
Fixture() {
|
|
assert(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
context.folder_dest = &store;
|
|
context.manager.begin(manifest.target_id, Flight::send, &flight);
|
|
context.manager.set_wire_v2_enabled(false);
|
|
uint8_t row[OTA_HAVE_ROW_BYTES] = {};
|
|
memcpy(row, manifest.merkle_root, 4);
|
|
wr_u32le(row + 4, manifest.target_id);
|
|
wr_u32le(row + 8, manifest.fw_version);
|
|
row[12] = manifest.codec_id;
|
|
row[13] = manifest.flags;
|
|
HaveMsg have{};
|
|
have.seeder_id[0] = 1;
|
|
have.frag_total = have.n_rows = 1;
|
|
have.rows = row;
|
|
uint8_t wire[256];
|
|
context.manager.on_message(wire, encode_have(wire, sizeof(wire), have));
|
|
assert(context.manager.catalogCount() == 1);
|
|
}
|
|
|
|
void pull(bool validate = false) {
|
|
char reply[160];
|
|
assert(pull_command(validate ? "pull 1 folder validate" : "pull 1 folder", reply, context));
|
|
assert(strncmp(reply, "OK ", 3) == 0);
|
|
if (context.manager.fetchState() == OtaManager::PAUSED) {
|
|
assert(strstr(reply, "OK paused (folder I/O error; reconnect to resume)") == reply);
|
|
assert(context.fetch_to_folder);
|
|
}
|
|
}
|
|
|
|
void deliver_manifest() {
|
|
for (uint32_t offset = 0; offset < MOTA_MFL; offset += OTA_MF_FRAG) {
|
|
ManifestMsg message{};
|
|
memcpy(message.manifest_id, manifest.merkle_root, 4);
|
|
message.frag_idx = offset / OTA_MF_FRAG;
|
|
message.frag_total = (MOTA_MFL + OTA_MF_FRAG - 1) / OTA_MF_FRAG;
|
|
message.bytes = manifest.manifest_start + offset;
|
|
message.len = std::min(uint32_t(OTA_MF_FRAG), MOTA_MFL - offset);
|
|
uint8_t wire[256];
|
|
context.manager.on_message(wire, encode_manifest(wire, sizeof(wire), message));
|
|
}
|
|
}
|
|
|
|
void finish_verification() {
|
|
for (unsigned guard = 0; guard < 100 && context.manager.fetchState() == OtaManager::VERIFYING_STAGED; ++guard)
|
|
context.manager.loop();
|
|
assert(context.manager.fetchState() != OtaManager::VERIFYING_STAGED);
|
|
}
|
|
|
|
void deliver_block(uint32_t block) {
|
|
const uint32_t block_size = manifest.block_size();
|
|
const uint32_t length = std::min(block_size, manifest.payload_size - block * block_size);
|
|
uint8_t wire[256];
|
|
for (uint32_t offset = 0; offset < length; offset += OTA_FRAG_DATA) {
|
|
DataMsg message{};
|
|
memcpy(message.manifest_id, manifest.merkle_root, 4);
|
|
message.block_idx = block;
|
|
message.frag_off = offset;
|
|
message.data = manifest.payload + block * block_size + offset;
|
|
message.data_len = std::min(uint32_t(OTA_FRAG_DATA), length - offset);
|
|
assert(context.manager.on_message(wire, encode_data(wire, sizeof(wire), message)));
|
|
}
|
|
std::vector<uint8_t> scratch(manifest.block_count * 4);
|
|
uint8_t siblings[128];
|
|
ProofMsg proof{};
|
|
memcpy(proof.manifest_id, manifest.merkle_root, 4);
|
|
proof.block_idx = block;
|
|
proof.n_proof = merkle_gen_proof(manifest.leaves, manifest.block_count, block,
|
|
scratch.data(), siblings);
|
|
proof.proof = siblings;
|
|
assert(context.manager.on_message(wire, encode_proof(wire, sizeof(wire), proof)));
|
|
}
|
|
};
|
|
|
|
static void transient_resume_failure_preserves_bytes(HostStream::Failure failure,
|
|
uint8_t op, uint32_t offset,
|
|
bool retry_by_pull) {
|
|
Fixture f;
|
|
f.host.failure = failure;
|
|
f.host.fail_op = op;
|
|
f.host.fail_offset = offset;
|
|
f.host.failures_left = 2;
|
|
const auto saved = f.host.bytes;
|
|
f.pull();
|
|
f.deliver_manifest(); // Even a late peer response cannot initialize a paused slot.
|
|
if (f.host.bytes != saved)
|
|
fputs("FAIL: resume I/O fallback issued BEGIN and erased staging\n", stderr);
|
|
assert(f.host.bytes == saved && f.host.begins == 0 && f.host.writes == 0);
|
|
assert(f.context.manager.fetchState() == OtaManager::PAUSED);
|
|
assert(f.context.manager.fetchError() == OtaManager::FETCH_ERROR_STORAGE);
|
|
assert(memcmp(f.context.manager.fetchManifestId(), f.manifest.merkle_root, 4) == 0);
|
|
assert(f.flight.manifests == 0 && f.host.begins == 0 && f.host.writes == 0);
|
|
f.context.manager.set_clock(1000000);
|
|
f.context.manager.loop();
|
|
assert(f.host.bytes == saved && f.flight.manifests == 0);
|
|
char reply[160];
|
|
assert(pull_command("pull 1 folder", reply, f.context));
|
|
assert(strncmp(reply, "ERR OTA slot is paused", 22) == 0);
|
|
assert(!f.context.manager.resumeFetchAfterReconnect());
|
|
assert(f.context.manager.fetchState() == OtaManager::PAUSED);
|
|
assert(f.host.bytes == saved && f.host.begins == 0 && f.flight.manifests == 0);
|
|
if (retry_by_pull) {
|
|
f.context.manager.reset_session(); // The manager action performed by explicit cancellation.
|
|
f.pull();
|
|
} else {
|
|
assert(f.context.manager.resumeFetchAfterReconnect());
|
|
}
|
|
assert(f.context.manager.fetchState() == OtaManager::VERIFYING_STAGED);
|
|
f.finish_verification();
|
|
assert(f.context.manager.fetchState() == OtaManager::COMPLETE);
|
|
assert(f.context.manager.blocksHave() == f.manifest.block_count);
|
|
assert(f.context.manager.fetchError() == OtaManager::FETCH_ERROR_NONE);
|
|
assert(f.host.bytes == saved && f.host.begins == 0 && f.host.writes == 0);
|
|
}
|
|
|
|
static void known_absent_or_invalid_staging_starts_fresh(unsigned corruption,
|
|
bool after_io_failure) {
|
|
Fixture f;
|
|
if (after_io_failure) {
|
|
f.host.failure = HostStream::Timeout;
|
|
f.host.fail_op = MS_OP_STAT;
|
|
f.host.failures_left = 1;
|
|
f.pull();
|
|
assert(f.context.manager.fetchState() == OtaManager::PAUSED);
|
|
}
|
|
if (corruption == 0) f.host.present = false;
|
|
else if (corruption == 1) f.host.bytes[0] ^= 1;
|
|
else if (corruption == 2) wr_u32le(f.host.bytes.data() + 4, f.host.bytes.size() + 1);
|
|
else if (corruption == 3) f.host.bytes[8] = 0xFF; // Invalid manifest format version.
|
|
else if (corruption == 4) f.host.bytes.back() ^= 1;
|
|
else if (corruption == 5) f.host.bytes.resize(12);
|
|
else if (corruption == 6) f.host.bytes[8 + 20] ^= 1; // Another MID, read successfully.
|
|
else wr_u32le(f.host.bytes.data() + 8 + 3, f.manifest.target_id ^ 1u);
|
|
if (after_io_failure) assert(f.context.manager.resumeFetchAfterReconnect());
|
|
else f.pull();
|
|
assert(f.context.manager.fetchState() == OtaManager::WANT_MANIFEST);
|
|
assert(f.context.manager.fetchError() == OtaManager::FETCH_ERROR_NONE);
|
|
assert(!f.store.reopenHadIoError());
|
|
assert(f.flight.manifests == 1 && f.host.begins == 0);
|
|
f.deliver_manifest();
|
|
assert(f.context.manager.fetchState() == OtaManager::FETCHING);
|
|
assert(f.host.begins == 1);
|
|
}
|
|
|
|
static void explicit_validate_requests_fresh_initialization() {
|
|
Fixture f;
|
|
f.host.failure = HostStream::Timeout;
|
|
f.host.fail_op = MS_OP_STAT;
|
|
f.host.failures_left = 1;
|
|
f.pull(true);
|
|
assert(f.context.manager.fetchState() == OtaManager::WANT_MANIFEST);
|
|
assert(f.host.operations.empty() && f.flight.manifests == 1);
|
|
f.deliver_manifest();
|
|
assert(f.host.begins == 1 && f.host.failures_left == 1);
|
|
}
|
|
|
|
static void transient_probe_failure_resumes_only_missing_blocks() {
|
|
Fixture f;
|
|
const auto complete = f.host.bytes;
|
|
const uint32_t missing = 1;
|
|
const uint32_t leaf = f.manifest.leaves - SIM_MOTA_1K + missing * 4;
|
|
const uint32_t payload = f.manifest.payload - SIM_MOTA_1K + missing * f.manifest.block_size();
|
|
std::fill(f.host.bytes.begin() + leaf, f.host.bytes.begin() + leaf + 4, 0xFF);
|
|
std::fill(f.host.bytes.begin() + payload,
|
|
f.host.bytes.begin() + payload + f.manifest.block_size(), 0xFF);
|
|
const auto partial = f.host.bytes;
|
|
f.host.failure = HostStream::Timeout;
|
|
f.host.fail_offset = 8;
|
|
f.host.failures_left = 1;
|
|
f.pull();
|
|
assert(f.context.manager.fetchState() == OtaManager::PAUSED);
|
|
assert(f.context.manager.resumeFetchAfterReconnect());
|
|
f.finish_verification();
|
|
assert(f.context.manager.fetchState() == OtaManager::FETCHING);
|
|
assert(f.context.manager.blocksHave() == f.manifest.block_count - 1);
|
|
assert(f.host.bytes == partial && f.host.begins == 0 && f.host.writes == 0);
|
|
assert(!f.flight.requested_blocks.empty());
|
|
for (uint32_t block : f.flight.requested_blocks) assert(block == missing);
|
|
f.deliver_block(missing);
|
|
assert(f.context.manager.fetchState() == OtaManager::COMPLETE);
|
|
assert(f.host.bytes == complete && f.host.begins == 0);
|
|
}
|
|
|
|
static void established_capture_still_refuses_replacement_file() {
|
|
Fixture f;
|
|
f.pull();
|
|
assert(f.context.manager.fetchState() == OtaManager::VERIFYING_STAGED);
|
|
assert(f.context.manager.pauseFetchForDisconnect());
|
|
f.host.bytes[0] ^= 1;
|
|
assert(!f.context.manager.resumeFetchAfterReconnect());
|
|
assert(f.context.manager.fetchState() == OtaManager::PAUSED);
|
|
assert(f.host.begins == 0 && f.flight.manifests == 0);
|
|
}
|
|
|
|
int main() {
|
|
for (auto failure : {HostStream::Timeout, HostStream::Truncated, HostStream::BadChecksum,
|
|
HostStream::Error, HostStream::ShortWrite, HostStream::WrongOp}) {
|
|
for (bool retry_by_pull : {false, true}) {
|
|
transient_resume_failure_preserves_bytes(failure, MS_OP_STAT, 0, retry_by_pull);
|
|
for (uint32_t offset : {0u, 8u, SIM_MOTA_1K_LEN - 5u})
|
|
transient_resume_failure_preserves_bytes(failure, MS_OP_SREAD, offset, retry_by_pull);
|
|
}
|
|
}
|
|
for (unsigned corruption = 0; corruption < 8; ++corruption)
|
|
for (bool after_io_failure : {false, true})
|
|
known_absent_or_invalid_staging_starts_fresh(corruption, after_io_failure);
|
|
explicit_validate_requests_fresh_initialization();
|
|
transient_probe_failure_resumes_only_missing_blocks();
|
|
established_capture_still_refuses_replacement_file();
|
|
puts("PASS: folder resume I/O failures preserve staging and recover safely");
|
|
}
|