mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 13:32:35 +00:00
4587 lines
198 KiB
C++
4587 lines
198 KiB
C++
#include <gtest/gtest.h>
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <vector>
|
|
#include <cstring>
|
|
#include <type_traits>
|
|
|
|
#include "helpers/ota/MotaContainer.h"
|
|
#include "helpers/ota/MerkleTree.h"
|
|
#include "helpers/ota/BlockBitmap.h"
|
|
#include "helpers/ota/Multihash.h"
|
|
#include "helpers/ota/FirmwareInfo.h"
|
|
#include "helpers/ota/MotaSeederProto.h"
|
|
#include "helpers/ota/MotaSourceSerial.h"
|
|
#include "helpers/ota/FolderMotaStore.h"
|
|
#include "helpers/ota/SignerAllowlist.h"
|
|
#include "helpers/ota/OtaStore.h"
|
|
#include "helpers/ota/OtaProtocol.h"
|
|
#include "helpers/ota/OtaManager.h"
|
|
#include "helpers/ota/OtaDeflate.h"
|
|
#include "helpers/ota/OtaApply.h"
|
|
#include "helpers/ota/OtaBlInfo.h"
|
|
#include "helpers/ota/OtaFlashLayout_nrf52.h"
|
|
#include "helpers/ota/OtaSdBootToken.h"
|
|
#include "helpers/ota/OtaSdAuthRecord.h"
|
|
#include "mota_vectors.h" // auto-generated by tools/mota/gen_vectors.py
|
|
|
|
extern "C" {
|
|
#include "helpers/ota/detools/detools.h" // vendored detools 0.53.0 embeddable decoder
|
|
}
|
|
|
|
using namespace mesh::ota;
|
|
|
|
static std::vector<uint8_t> boot_manifest_bytes();
|
|
|
|
TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x90), 0x90);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x9F), 0x9F);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xB0), 0xB0);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xB8), 0xB8);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBC), 0xBC);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0xBD); // SD authorization failure
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xC0), 0xC0);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xC8), 0xC8);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xCF), 0xCF);
|
|
EXPECT_TRUE(ota_nrf52_boot_update_result(0xC8));
|
|
EXPECT_FALSE(ota_nrf52_boot_update_result(0xB8));
|
|
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x00), 0x00);
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x4C), 0x00); // shutdown reason
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x51), 0x00); // QSPI handoff
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0x53), 0x00); // SD handoff
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xD4), 0x00); // legacy handoff
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xED), 0x00); // expanded handoff
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xE0), 0x00); // retired handoff remains non-result
|
|
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBE), 0x00);
|
|
}
|
|
|
|
TEST(OtaPolicy, TrustedAutoInstallIsStrictlyForwardOnly) {
|
|
EXPECT_FALSE(ota_trusted_auto_version_allows(0, 0x01170104u));
|
|
EXPECT_FALSE(ota_trusted_auto_version_allows(0x01170103u, 0));
|
|
EXPECT_FALSE(ota_trusted_auto_version_allows(0x01170103u, 0x01170102u));
|
|
EXPECT_FALSE(ota_trusted_auto_version_allows(0x01170103u, 0x01170103u));
|
|
EXPECT_TRUE(ota_trusted_auto_version_allows(0x01170103u, 0x01170104u));
|
|
}
|
|
|
|
TEST(OtaCapability, LiveMaximumTracksTheApplicationReassemblyLimit) {
|
|
EXPECT_EQ(ota_max_block_capability(), 2048u);
|
|
EXPECT_EQ(ota_max_block_capability(), OTA_MAX_BLOCK);
|
|
EXPECT_LE(ota_max_block_capability(), OTA_DATA_V2_MAX_ENCODED);
|
|
}
|
|
|
|
TEST(OtaBootPackage, StorageProfilesAndSdGeometryAreExact) {
|
|
EXPECT_EQ(OTA_BL_PROFILE_SD_BOOT_UPDATE, 0x09u);
|
|
EXPECT_EQ(OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE, 0x0Au);
|
|
EXPECT_EQ(OTA_BL_PROFILE_QSPI_BOOT_UPDATE, 0x0Eu);
|
|
EXPECT_EQ(OTA_BL_REQUIRED_APP_CODEC_MASK, 0x0005u);
|
|
EXPECT_EQ(GPREGRET2_OTA_STAGE_SD, 0x53u);
|
|
EXPECT_TRUE(ota_bootloader_image_geometry_valid(
|
|
OTA_BOOT_IMAGE_SIZE, OTA_BOOT_IMAGE_SIZE));
|
|
EXPECT_FALSE(ota_bootloader_image_geometry_valid(
|
|
OTA_BOOT_IMAGE_SIZE - 1u, OTA_BOOT_IMAGE_SIZE));
|
|
EXPECT_FALSE(ota_bootloader_image_geometry_valid(
|
|
OTA_BOOT_IMAGE_SIZE, OTA_BOOT_IMAGE_SIZE - 1u));
|
|
EXPECT_TRUE(ota_bootloader_scratch_headroom_valid(
|
|
true, MOTA_NRF52_APP_BASE_S140_V6,
|
|
OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6,
|
|
OTA_BOOT_SCRATCH_START));
|
|
EXPECT_FALSE(ota_bootloader_scratch_headroom_valid(
|
|
false, MOTA_NRF52_APP_BASE_S140_V6, 1u, OTA_BOOT_SCRATCH_START));
|
|
EXPECT_FALSE(ota_bootloader_scratch_headroom_valid(
|
|
true, MOTA_NRF52_APP_BASE_S140_V6,
|
|
OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6 + 1u,
|
|
OTA_BOOT_SCRATCH_START));
|
|
|
|
const uint32_t safe_span = OTA_BOOT_SCRATCH_START - MOTA_NRF52_APP_BASE_S140_V6;
|
|
const uint32_t live_size = safe_span - 0x1000u;
|
|
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
|
|
OTA_BOOT_BANK_ERASED_FALLBACK, 0xFFFFu, UINT32_MAX, live_size, safe_span));
|
|
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
|
|
OTA_BOOT_BANK_VALID_APP, 0u, UINT32_MAX, live_size, safe_span));
|
|
EXPECT_TRUE(ota_bootloader_scratch_bank_geometry_valid(
|
|
OTA_BOOT_BANK_VALID_APP, 1u, live_size, live_size, safe_span));
|
|
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
|
|
OTA_BOOT_BANK_VALID_APP, 1u, live_size - 1u, live_size, safe_span));
|
|
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
|
|
OTA_BOOT_BANK_VALID_APP, 1u, safe_span + 1u, live_size, safe_span));
|
|
EXPECT_FALSE(ota_bootloader_scratch_bank_geometry_valid(
|
|
0x00FFu, 0u, 0u, live_size, safe_span));
|
|
}
|
|
|
|
TEST(OtaSdAuth, RetainedRecordBindsPurposeGeometryAndContainerDigest) {
|
|
uint8_t digest[32];
|
|
for (uint8_t i = 0; i < sizeof(digest); ++i) digest[i] = (uint8_t)(0x40u + i);
|
|
uint8_t record[MOTA_SD_AUTH_LEN];
|
|
const uint32_t total = 41330u;
|
|
const uint32_t sectors = (total + 511u) / 512u;
|
|
ASSERT_TRUE(mota_sd_auth_encode(record, MOTA_SD_AUTH_PURPOSE_BOOTLOADER,
|
|
MOTA_BOOT_FORMAT_VER, 2048u, sectors,
|
|
total, 8000000u, digest));
|
|
EXPECT_EQ(0, memcmp(record, "MOTASDA2", 8));
|
|
EXPECT_EQ(mota_sd_auth_rd16(record + 8), 2u);
|
|
EXPECT_EQ(mota_sd_auth_rd16(record + 10), 72u);
|
|
EXPECT_EQ(record[12], MOTA_SD_AUTH_PURPOSE_BOOTLOADER);
|
|
EXPECT_EQ(record[13], MOTA_BOOT_FORMAT_VER);
|
|
EXPECT_EQ(mota_sd_auth_rd32(record + 16), 2048u);
|
|
EXPECT_EQ(mota_sd_auth_rd32(record + 20), sectors);
|
|
EXPECT_EQ(mota_sd_auth_rd32(record + 24), total);
|
|
EXPECT_EQ(mota_sd_auth_rd32(record + 28), 8000000u);
|
|
EXPECT_EQ(0, memcmp(record + 32, digest, sizeof(digest)));
|
|
EXPECT_TRUE(mota_sd_auth_valid(record));
|
|
|
|
record[32] ^= 1u;
|
|
EXPECT_FALSE(mota_sd_auth_valid(record));
|
|
record[32] ^= 1u;
|
|
EXPECT_FALSE(mota_sd_auth_encode(record, MOTA_SD_AUTH_PURPOSE_APP,
|
|
MOTA_BOOT_FORMAT_VER, 2048u, sectors,
|
|
total, 8000000u, digest));
|
|
EXPECT_FALSE(mota_sd_auth_encode(record, MOTA_SD_AUTH_PURPOSE_APP,
|
|
MOTA_APP_FORMAT_VER, 2048u, sectors + 1u,
|
|
total, 8000000u, digest));
|
|
}
|
|
|
|
TEST(OtaBootPackage, ContinuityExtensionIsBackwardCompatibleAndStrictlyMonotonic) {
|
|
uint8_t envelope[OTA_BOOT_ENVELOPE_SIZE] = {0};
|
|
wr_u32le(envelope, OTA_BOOT_MANIFEST_MAGIC0);
|
|
wr_u32le(envelope + 4, OTA_BOOT_MANIFEST_MAGIC1);
|
|
envelope[8] = (uint8_t)OTA_BOOT_MANIFEST_VERSION;
|
|
envelope[10] = (uint8_t)OTA_BOOT_MANIFEST_SIZE;
|
|
wr_u32le(envelope + 12, OTA_BOOT_IMAGE_START);
|
|
wr_u32le(envelope + 16, OTA_BOOT_IMAGE_SIZE);
|
|
wr_u32le(envelope + 20, 0x239A0071u);
|
|
memcpy(envelope + 24, "TOWER_V2_OTA", 12);
|
|
wr_u32le(envelope + 44, OTA_BOOT_CONTINUITY_MAGIC0);
|
|
wr_u32le(envelope + 48, OTA_BOOT_CONTINUITY_MAGIC1);
|
|
envelope[52] = (uint8_t)OTA_BOOT_CONTINUITY_VERSION;
|
|
envelope[54] = (uint8_t)OTA_BOOT_CONTINUITY_SIZE;
|
|
const uint32_t candidate_version = 0x0117010Du;
|
|
wr_u32le(envelope + 56, candidate_version);
|
|
envelope[60] = (uint8_t)OTA_BOOT_CONTINUITY_FAMILY_S140;
|
|
envelope[62] = 0xB6;
|
|
wr_u32le(envelope + 64, MOTA_NRF52_APP_BASE_S140_V6);
|
|
envelope[68] = (uint8_t)OTA_BOOT_CONTINUITY_LAYOUT_ABI;
|
|
|
|
OtaBootloaderIdentity candidate;
|
|
ASSERT_TRUE(ota_bootloader_manifest_parse(envelope, 0x100u, candidate,
|
|
sizeof(envelope)));
|
|
ASSERT_TRUE(candidate.continuity_present);
|
|
EXPECT_EQ(candidate.boot_version, candidate_version);
|
|
EXPECT_EQ(candidate.softdevice_family, 140u);
|
|
EXPECT_EQ(candidate.softdevice_fwid, 0x00B6u);
|
|
EXPECT_EQ(candidate.app_base, MOTA_NRF52_APP_BASE_S140_V6);
|
|
|
|
OtaBootloaderIdentity legacy = candidate;
|
|
legacy.continuity_present = false;
|
|
legacy.crc_ok = true;
|
|
EXPECT_FALSE(ota_bootloader_sd_retained_auth_ready(
|
|
legacy, 140u, 0x00B6u, MOTA_NRF52_APP_BASE_S140_V6, 1u));
|
|
candidate.crc_ok = true;
|
|
EXPECT_TRUE(ota_bootloader_sd_retained_auth_ready(
|
|
candidate, 140u, 0x00B6u, MOTA_NRF52_APP_BASE_S140_V6, 1u));
|
|
EXPECT_EQ(ota_bootloader_continuity_gate(
|
|
legacy, candidate, candidate_version, 140u, 0x00B6u,
|
|
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
|
OTA_BOOT_CONTINUITY_OK); // one-time v1 bootstrap
|
|
EXPECT_EQ(ota_bootloader_continuity_gate(
|
|
legacy, candidate, candidate_version + 1u, 140u, 0x00B6u,
|
|
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
|
OTA_BOOT_CONTINUITY_OUTER_VERSION);
|
|
|
|
OtaBootloaderIdentity installed = candidate;
|
|
installed.boot_version = candidate_version - 1u;
|
|
EXPECT_EQ(ota_bootloader_continuity_gate(
|
|
installed, candidate, candidate_version, 140u, 0x00B6u,
|
|
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
|
OTA_BOOT_CONTINUITY_OK);
|
|
installed.boot_version = candidate_version;
|
|
EXPECT_EQ(ota_bootloader_continuity_gate(
|
|
installed, candidate, candidate_version, 140u, 0x00B6u,
|
|
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
|
OTA_BOOT_CONTINUITY_NOT_NEWER);
|
|
EXPECT_EQ(ota_bootloader_continuity_gate(
|
|
legacy, candidate, candidate_version, 140u, 0x00B7u,
|
|
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
|
OTA_BOOT_CONTINUITY_PLATFORM);
|
|
|
|
EXPECT_FALSE(ota_bootloader_version_valid(0x02040100u));
|
|
EXPECT_FALSE(ota_bootloader_version_valid(UINT32_MAX));
|
|
EXPECT_TRUE(ota_bootloader_version_valid(0x020401FFu));
|
|
candidate.boot_version = UINT32_MAX;
|
|
EXPECT_EQ(ota_bootloader_continuity_gate(
|
|
legacy, candidate, UINT32_MAX, 140u, 0x00B6u,
|
|
MOTA_NRF52_APP_BASE_S140_V6, 1u),
|
|
OTA_BOOT_CONTINUITY_OUTER_VERSION);
|
|
candidate.boot_version = candidate_version;
|
|
|
|
envelope[48] ^= 1u;
|
|
EXPECT_FALSE(ota_bootloader_manifest_parse(envelope, 0x100u, candidate,
|
|
sizeof(envelope)));
|
|
}
|
|
|
|
TEST(OtaBootPackage, CandidateContinuityEnvelopeHasOneCanonicalFinalOffset) {
|
|
auto make_image = [](uint32_t manifest_offset) {
|
|
std::vector<uint8_t> image(OTA_BOOT_IMAGE_SIZE, 0xFF);
|
|
wr_u32le(image.data(), OTA_NRF52840_RAM_END);
|
|
wr_u32le(image.data() + 4, OTA_BOOT_IMAGE_START + 0x101u);
|
|
uint8_t* caps = image.data() + 0x400u;
|
|
memset(caps, 0, 16u);
|
|
memcpy(caps, "MOTABLDR", 8u);
|
|
caps[8] = MOTA_BOOT_FORMAT_VER;
|
|
caps[10] = (uint8_t)OTA_BL_REQUIRED_APP_CODEC_MASK;
|
|
caps[12] = OTA_BL_PROFILE_SD_BOOT_UPDATE;
|
|
uint8_t* envelope = image.data() + manifest_offset;
|
|
memset(envelope, 0, OTA_BOOT_ENVELOPE_SIZE);
|
|
wr_u32le(envelope, OTA_BOOT_MANIFEST_MAGIC0);
|
|
wr_u32le(envelope + 4, OTA_BOOT_MANIFEST_MAGIC1);
|
|
envelope[8] = (uint8_t)OTA_BOOT_MANIFEST_VERSION;
|
|
envelope[10] = (uint8_t)OTA_BOOT_MANIFEST_SIZE;
|
|
wr_u32le(envelope + 12, OTA_BOOT_IMAGE_START);
|
|
wr_u32le(envelope + 16, OTA_BOOT_IMAGE_SIZE);
|
|
wr_u32le(envelope + 20, OTA_XIAO_BOARD_ID_BASE);
|
|
memcpy(envelope + 24, OTA_XIAO_BOOT_DEVICE_NAME,
|
|
OTA_BOOT_DEVICE_NAME_SIZE);
|
|
wr_u32le(envelope + 44, OTA_BOOT_CONTINUITY_MAGIC0);
|
|
wr_u32le(envelope + 48, OTA_BOOT_CONTINUITY_MAGIC1);
|
|
envelope[52] = (uint8_t)OTA_BOOT_CONTINUITY_VERSION;
|
|
envelope[54] = (uint8_t)OTA_BOOT_CONTINUITY_SIZE;
|
|
wr_u32le(envelope + 56, 0x0117010Du);
|
|
envelope[60] = (uint8_t)OTA_BOOT_CONTINUITY_FAMILY_S140;
|
|
envelope[62] = 0x23u;
|
|
envelope[63] = 0x01u;
|
|
wr_u32le(envelope + 64, MOTA_NRF52_APP_BASE_S140_V7);
|
|
envelope[68] = (uint8_t)OTA_BOOT_CONTINUITY_LAYOUT_ABI;
|
|
wr_u32le(envelope + 40,
|
|
ota_boot_image_crc32(image.data(), image.size(), manifest_offset + 40));
|
|
return image;
|
|
};
|
|
|
|
OtaBootloaderIdentity identity;
|
|
auto canonical = make_image(OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
|
|
ASSERT_TRUE(ota_bootloader_candidate_identity_from_image(
|
|
canonical.data(), canonical.size(), identity));
|
|
EXPECT_TRUE(ota_bootloader_candidate_identity_canonical(identity));
|
|
EXPECT_EQ(identity.manifest_offset, 0x9FB4u);
|
|
|
|
auto relocated = make_image(0x8000u);
|
|
ASSERT_TRUE(ota_bootloader_identity_from_image(
|
|
relocated.data(), relocated.size(), identity)); // installed/legacy scanner remains generic
|
|
EXPECT_FALSE(ota_bootloader_candidate_identity_canonical(identity));
|
|
EXPECT_FALSE(ota_bootloader_candidate_identity_from_image(
|
|
relocated.data(), relocated.size(), identity));
|
|
|
|
OtaStoreRam<OTA_BOOT_IMAGE_SIZE> store;
|
|
OtaBootloaderCapsMarker caps;
|
|
ASSERT_TRUE(store.begin((uint32_t)canonical.size()));
|
|
ASSERT_TRUE(store.write(0, canonical.data(), (uint32_t)canonical.size()));
|
|
EXPECT_TRUE(ota_bootloader_external_image_metadata(
|
|
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
|
EXPECT_EQ(identity.manifest_offset, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET);
|
|
|
|
// A second CRC-valid base identity still counts when its adjacent BLM2
|
|
// extension is only half present. These coupled values are the fixed point
|
|
// for this deterministic image. Counting continuity first would wrongly
|
|
// discard the decoy and disagree with a deployed legacy updater.
|
|
auto ambiguous = canonical;
|
|
const uint32_t corrupt_decoy = 0x200u;
|
|
memcpy(ambiguous.data() + corrupt_decoy,
|
|
ambiguous.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET,
|
|
OTA_BOOT_MANIFEST_SIZE);
|
|
wr_u32le(ambiguous.data() + corrupt_decoy + 44, OTA_BOOT_CONTINUITY_MAGIC0);
|
|
wr_u32le(ambiguous.data() + corrupt_decoy + 48, 0x21444142u); // "BAD!"
|
|
wr_u32le(ambiguous.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40,
|
|
0x614E59E1u);
|
|
wr_u32le(ambiguous.data() + corrupt_decoy + 40, 0x4A03F7A2u);
|
|
ASSERT_EQ(ota_boot_image_crc32(
|
|
ambiguous.data(), ambiguous.size(),
|
|
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40), 0x614E59E1u);
|
|
ASSERT_EQ(ota_boot_image_crc32(
|
|
ambiguous.data(), ambiguous.size(), corrupt_decoy + 40),
|
|
0x4A03F7A2u);
|
|
EXPECT_FALSE(ota_bootloader_candidate_identity_from_image(
|
|
ambiguous.data(), ambiguous.size(), identity));
|
|
ASSERT_TRUE(store.begin((uint32_t)ambiguous.size()));
|
|
ASSERT_TRUE(store.write(0, ambiguous.data(), (uint32_t)ambiguous.size()));
|
|
EXPECT_FALSE(ota_bootloader_external_image_metadata(
|
|
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
|
|
|
// The same malformed extension is harmless when its base CRC is invalid;
|
|
// only the canonical CRC-valid identity is selected and interpreted.
|
|
auto bad_crc_decoy = canonical;
|
|
memcpy(bad_crc_decoy.data() + corrupt_decoy,
|
|
bad_crc_decoy.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET,
|
|
OTA_BOOT_MANIFEST_SIZE);
|
|
wr_u32le(bad_crc_decoy.data() + corrupt_decoy + 44,
|
|
OTA_BOOT_CONTINUITY_MAGIC0);
|
|
wr_u32le(bad_crc_decoy.data() + corrupt_decoy + 48, 0x21444142u);
|
|
wr_u32le(bad_crc_decoy.data() + corrupt_decoy + 40, 0xA5A5A5A5u);
|
|
wr_u32le(bad_crc_decoy.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40, 0u);
|
|
wr_u32le(bad_crc_decoy.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40,
|
|
ota_boot_image_crc32(
|
|
bad_crc_decoy.data(), bad_crc_decoy.size(),
|
|
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40));
|
|
ASSERT_NE(ota_boot_image_crc32(
|
|
bad_crc_decoy.data(), bad_crc_decoy.size(), corrupt_decoy + 40),
|
|
0xA5A5A5A5u);
|
|
EXPECT_TRUE(ota_bootloader_candidate_identity_from_image(
|
|
bad_crc_decoy.data(), bad_crc_decoy.size(), identity));
|
|
ASSERT_TRUE(store.begin((uint32_t)bad_crc_decoy.size()));
|
|
ASSERT_TRUE(store.write(0, bad_crc_decoy.data(),
|
|
(uint32_t)bad_crc_decoy.size()));
|
|
EXPECT_TRUE(ota_bootloader_external_image_metadata(
|
|
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
|
|
|
// A sole CRC-valid identity that claims a corrupt extension is not a legacy
|
|
// image; installed diagnostics and candidate validation both fail closed.
|
|
auto sole_half = canonical;
|
|
wr_u32le(sole_half.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 48,
|
|
0x21444142u);
|
|
wr_u32le(sole_half.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40, 0u);
|
|
wr_u32le(sole_half.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40,
|
|
ota_boot_image_crc32(
|
|
sole_half.data(), sole_half.size(),
|
|
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 40));
|
|
EXPECT_FALSE(ota_bootloader_identity_from_image(
|
|
sole_half.data(), sole_half.size(), identity));
|
|
ASSERT_TRUE(store.begin((uint32_t)sole_half.size()));
|
|
ASSERT_TRUE(store.write(0, sole_half.data(), (uint32_t)sole_half.size()));
|
|
EXPECT_FALSE(ota_bootloader_external_image_metadata(
|
|
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
|
|
|
ASSERT_TRUE(store.begin((uint32_t)relocated.size()));
|
|
ASSERT_TRUE(store.write(0, relocated.data(), (uint32_t)relocated.size()));
|
|
EXPECT_FALSE(ota_bootloader_external_image_metadata(
|
|
store, 0, OTA_BL_PROFILE_SD_BOOT_UPDATE, identity, caps));
|
|
}
|
|
|
|
TEST(OtaBootPackage, SdApprovalTokenBindsTheAuthenticatedImageHash) {
|
|
uint8_t hash[32];
|
|
for (uint8_t i = 0; i < sizeof(hash); i++) hash[i] = i;
|
|
uint8_t token[MOTA_SD_BOOT_TOKEN_LEN];
|
|
mota_sd_boot_token_encode(token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash);
|
|
EXPECT_EQ(0, memcmp(token, "MOTASDBL", 8));
|
|
EXPECT_EQ(mota_sd_boot_token_rd32(token + 8), 1u);
|
|
EXPECT_EQ(mota_sd_boot_token_rd32(token + 12), 64u);
|
|
EXPECT_EQ(mota_sd_boot_token_rd32(token + 16), MOTA_NRF52_BOOT_CONTAINER_SIZE);
|
|
EXPECT_EQ(mota_sd_boot_token_rd32(token + 20), ~MOTA_NRF52_BOOT_CONTAINER_SIZE);
|
|
EXPECT_EQ(0, memcmp(token + MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET, hash, sizeof(hash)));
|
|
EXPECT_TRUE(mota_sd_boot_token_valid(
|
|
token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash));
|
|
|
|
token[MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET] ^= 1u;
|
|
EXPECT_FALSE(mota_sd_boot_token_valid(
|
|
token, MOTA_NRF52_BOOT_CONTAINER_SIZE, hash));
|
|
token[MOTA_SD_BOOT_TOKEN_IMAGE_HASH_OFFSET] ^= 1u;
|
|
EXPECT_FALSE(mota_sd_boot_token_valid(
|
|
token, MOTA_NRF52_BOOT_CONTAINER_SIZE + 1u, hash));
|
|
}
|
|
|
|
TEST(OtaBootPackage, GenericIdentityUsesBoardAndNameForCollisionSafeTarget) {
|
|
const uint32_t shared_board_id = 0x239A0029u;
|
|
const uint8_t rak3401_name[16] = {'3','4','0','1','_','D','F','U',0,0,0,0,0,0,0,0};
|
|
const uint8_t rak4631_name[16] = {'4','6','3','1','_','D','F','U',0,0,0,0,0,0,0,0};
|
|
uint8_t hw3401[32], hw4631[32];
|
|
ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, rak3401_name, hw3401));
|
|
ASSERT_TRUE(ota_bootloader_hw_id(shared_board_id, rak4631_name, hw4631));
|
|
EXPECT_EQ(0, memcmp(hw3401, "NRF_BL_239A0029_3401_DFU", 24));
|
|
EXPECT_EQ(0, memcmp(hw4631, "NRF_BL_239A0029_4631_DFU", 24));
|
|
EXPECT_NE(0, memcmp(hw3401, hw4631, sizeof(hw3401)));
|
|
EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak3401_name), 0x23818A80u);
|
|
EXPECT_EQ(ota_bootloader_target_id(shared_board_id, rak4631_name), 0x2D0DF000u);
|
|
|
|
uint8_t invalid[16] = {'B','A','D',0,'T','A','I','L',0,0,0,0,0,0,0,0};
|
|
EXPECT_FALSE(ota_bootloader_device_name_valid(shared_board_id, invalid));
|
|
memset(invalid, 'A', sizeof(invalid));
|
|
EXPECT_FALSE(ota_bootloader_device_name_valid(shared_board_id, invalid));
|
|
EXPECT_FALSE(ota_bootloader_board_id_valid(0));
|
|
EXPECT_FALSE(ota_bootloader_board_id_valid(UINT32_MAX));
|
|
|
|
auto raw = boot_manifest_bytes();
|
|
wr_u32le(raw.data() + 3, ota_bootloader_target_id(shared_board_id, rak3401_name));
|
|
memcpy(raw.data() + 57, hw3401, sizeof(hw3401));
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse_manifest(raw.data(), raw.size(), manifest));
|
|
OtaBootloaderIdentity installed;
|
|
installed.present = installed.crc_ok = true;
|
|
installed.image_start = OTA_BOOT_IMAGE_START;
|
|
installed.image_size = OTA_BOOT_IMAGE_SIZE;
|
|
installed.board_id = shared_board_id;
|
|
memcpy(installed.device_name, rak3401_name, sizeof(rak3401_name));
|
|
uint8_t mid[4] = {0x11,0x22,0x33,0x44};
|
|
uint8_t hash8[8]; memcpy(hash8, manifest.image_hash, sizeof(hash8));
|
|
EXPECT_EQ(ota_bootloader_confirmation_gate(manifest, installed, mid, mid, hash8),
|
|
OTA_BOOT_CONFIRM_OK);
|
|
wr_u32le(raw.data() + 3, shared_board_id); // raw USB ID is not the generic wire target
|
|
ASSERT_TRUE(mota_parse_manifest(raw.data(), raw.size(), manifest));
|
|
EXPECT_EQ(ota_bootloader_confirmation_gate(manifest, installed, mid, mid, hash8),
|
|
OTA_BOOT_CONFIRM_TARGET);
|
|
|
|
struct KnownIdentity { uint32_t board_id; const char* name; uint32_t target_id; };
|
|
const KnownIdentity known[] = {
|
|
{0x239A0071u, "TOWER_V2_OTA", 0x1150F50Eu},
|
|
{0x239A0071u, "T096_DFU", 0x42354C85u},
|
|
{0x239A0071u, "T1_DFU", 0xFC556FFCu},
|
|
{0x239A0071u, "T114_DFU", 0x0C3F2902u},
|
|
{0x239A0071u, "MESH_POCKET_OTA",0x059277F4u},
|
|
{0x239A00B3u, "KeepteenLT1_OTA",0xDB2E7B51u},
|
|
{0x239A0029u, "MX25_DFU", 0x026AA982u},
|
|
{0x239A00B3u, "PROM_DFU", 0xAF79E8CCu},
|
|
{0x28860057u, "T1KE_DFU", 0xE6F5F03Fu},
|
|
{0x239A00DAu, "TNM3_DFU", 0x0CA41DB2u},
|
|
{0x239A0029u, "3401_DFU", 0x23818A80u},
|
|
{0x239A0029u, "4631_DFU", 0x2D0DF000u},
|
|
{0x239A0029u, "RTAG_DFU", 0xC72E9C9Cu},
|
|
};
|
|
uint32_t targets[sizeof(known) / sizeof(known[0])] = {0};
|
|
for (size_t i = 0; i < sizeof(known) / sizeof(known[0]); i++) {
|
|
uint8_t name[16] = {0};
|
|
ASSERT_LT(strlen(known[i].name), sizeof(name));
|
|
memcpy(name, known[i].name, strlen(known[i].name));
|
|
targets[i] = ota_bootloader_target_id(known[i].board_id, name);
|
|
EXPECT_EQ(targets[i], known[i].target_id);
|
|
for (size_t j = 0; j < i; j++) EXPECT_NE(targets[i], targets[j]);
|
|
}
|
|
}
|
|
|
|
static std::vector<uint8_t> boot_manifest_bytes() {
|
|
std::vector<uint8_t> m(MOTA_MFL, 0);
|
|
m[0] = MOTA_BOOT_FORMAT_VER;
|
|
m[1] = MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER;
|
|
m[2] = HASH_ALGO_SHA256;
|
|
wr_u32le(m.data() + 3, OTA_XIAO_BOARD_ID_BASE);
|
|
wr_u32le(m.data() + 7, 1);
|
|
wr_u32le(m.data() + 11, OTA_BOOT_IMAGE_SIZE);
|
|
wr_u32le(m.data() + 15, OTA_BOOT_IMAGE_SIZE);
|
|
m[19] = 10;
|
|
m[20] = 0x11; m[21] = 0x22; m[22] = 0x33; m[23] = 0x44;
|
|
for (uint8_t i = 0; i < 32; i++) m[24 + i] = (uint8_t)(0x80 + i);
|
|
m[56] = CODEC_FULL;
|
|
uint8_t hw[32]; EXPECT_TRUE(ota_xiao_bootloader_hw_id(OTA_XIAO_BOARD_ID_BASE, hw));
|
|
memcpy(m.data() + 57, hw, sizeof(hw));
|
|
memset(m.data() + MOTA_OFF_APPROVAL, 0xFF, 4);
|
|
return m;
|
|
}
|
|
|
|
TEST(OtaBootPackage, ParserSeparatesV2ApplicationsFromStrictV3Bootloader) {
|
|
MotaManifest parsed;
|
|
auto m = boot_manifest_bytes();
|
|
ASSERT_TRUE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
EXPECT_TRUE(parsed.is_bootloader());
|
|
EXPECT_EQ(parsed.block_size(), 1024u);
|
|
EXPECT_EQ(parsed.block_count, 40u);
|
|
|
|
m[0] = MOTA_APP_FORMAT_VER;
|
|
EXPECT_FALSE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
m = boot_manifest_bytes(); m[1] &= ~MFLAG_BOOTLOADER;
|
|
EXPECT_FALSE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
m = boot_manifest_bytes(); m[19] = 9;
|
|
EXPECT_FALSE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
m = boot_manifest_bytes(); m[19] = 11;
|
|
EXPECT_FALSE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
m = boot_manifest_bytes(); memset(m.data() + 7, 0, 4);
|
|
EXPECT_FALSE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
m = boot_manifest_bytes(); m[MOTA_OFF_BASE_HASH] = 1;
|
|
EXPECT_FALSE(mota_parse_manifest(m.data(), m.size(), parsed));
|
|
}
|
|
|
|
TEST(OtaBootPackage, EmbeddedIdentityVectorCapsAndExplicitConfirmationGate) {
|
|
std::vector<uint8_t> image(OTA_BOOT_IMAGE_SIZE, 0xFF);
|
|
wr_u32le(image.data(), OTA_NRF52840_RAM_END);
|
|
wr_u32le(image.data() + 4, OTA_BOOT_IMAGE_START + 0x101u);
|
|
ASSERT_TRUE(ota_bootloader_vector_sane(image.data()));
|
|
image[4] &= 0xFE;
|
|
EXPECT_FALSE(ota_bootloader_vector_sane(image.data()));
|
|
wr_u32le(image.data() + 4, OTA_BOOT_IMAGE_START + 0x101u);
|
|
|
|
const uint32_t moff = 0x100;
|
|
memset(image.data() + moff, 0, OTA_BOOT_MANIFEST_SIZE);
|
|
wr_u32le(image.data() + moff, OTA_BOOT_MANIFEST_MAGIC0);
|
|
wr_u32le(image.data() + moff + 4, OTA_BOOT_MANIFEST_MAGIC1);
|
|
image[moff + 8] = OTA_BOOT_MANIFEST_VERSION;
|
|
image[moff + 10] = OTA_BOOT_MANIFEST_SIZE;
|
|
wr_u32le(image.data() + moff + 12, OTA_BOOT_IMAGE_START);
|
|
wr_u32le(image.data() + moff + 16, OTA_BOOT_IMAGE_SIZE);
|
|
wr_u32le(image.data() + moff + 20, OTA_XIAO_BOARD_ID_BASE);
|
|
memcpy(image.data() + moff + 24, OTA_XIAO_BOOT_DEVICE_NAME,
|
|
OTA_BOOT_DEVICE_NAME_SIZE);
|
|
wr_u32le(image.data() + moff + 40,
|
|
ota_boot_image_crc32(image.data(), image.size(), moff + 40));
|
|
const std::vector<uint8_t> canonical_image = image;
|
|
|
|
OtaBootloaderIdentity identity;
|
|
ASSERT_TRUE(ota_bootloader_identity_from_image(image.data(), image.size(), identity));
|
|
EXPECT_TRUE(identity.crc_ok);
|
|
EXPECT_EQ(identity.board_id, OTA_XIAO_BOARD_ID_BASE);
|
|
|
|
// A structurally complete bad-CRC decoy before the real manifest must not
|
|
// shadow the later valid identity.
|
|
const uint32_t decoy = 0x20;
|
|
memcpy(image.data() + decoy, image.data() + moff, OTA_BOOT_MANIFEST_SIZE);
|
|
wr_u32le(image.data() + decoy + 40, 0xA5A5A5A5u);
|
|
wr_u32le(image.data() + moff + 40, 0);
|
|
wr_u32le(image.data() + moff + 40,
|
|
ota_boot_image_crc32(image.data(), image.size(), moff + 40));
|
|
ASSERT_NE(ota_boot_image_crc32(image.data(), image.size(), decoy + 40), 0xA5A5A5A5u);
|
|
ASSERT_TRUE(ota_bootloader_identity_from_image(image.data(), image.size(), identity));
|
|
EXPECT_EQ(identity.manifest_offset, moff);
|
|
|
|
// These coupled CRC values make both otherwise identical manifests valid
|
|
// in this deterministic fixture. Two privileged identities are ambiguous
|
|
// and must fail closed in the helper shared with the staged-QSPI scanner.
|
|
image = canonical_image;
|
|
const uint32_t second = 0x200;
|
|
memcpy(image.data() + second, image.data() + moff, OTA_BOOT_MANIFEST_SIZE);
|
|
wr_u32le(image.data() + moff + 40, 0x07033138u);
|
|
wr_u32le(image.data() + second + 40, 0x0998508Cu);
|
|
ASSERT_EQ(ota_boot_image_crc32(image.data(), image.size(), moff + 40), 0x07033138u);
|
|
ASSERT_EQ(ota_boot_image_crc32(image.data(), image.size(), second + 40), 0x0998508Cu);
|
|
EXPECT_FALSE(ota_bootloader_identity_from_image(image.data(), image.size(), identity));
|
|
|
|
image = canonical_image;
|
|
image[0x300] ^= 1;
|
|
EXPECT_FALSE(ota_bootloader_identity_from_image(image.data(), image.size(), identity));
|
|
|
|
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
|
(uint8_t)(OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
|
|
OtaBootloaderCapsMarker caps;
|
|
EXPECT_TRUE(ota_bootloader_caps_marker_parse(marker, caps));
|
|
marker[15] = 1;
|
|
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
|
marker[15] = 0;
|
|
marker[8] = 0; marker[9] = 1; // ABI 0x0100: valid little-endian u16
|
|
ASSERT_TRUE(ota_bootloader_caps_marker_parse(marker, caps));
|
|
EXPECT_EQ(caps.apply_abi, 0x0100u);
|
|
marker[8] = 0; marker[9] = 0;
|
|
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
|
marker[8] = 0xFF; marker[9] = 0xFF;
|
|
EXPECT_FALSE(ota_bootloader_caps_marker_parse(marker, caps));
|
|
|
|
std::vector<uint8_t> internal_caps_image(64, 0xFF);
|
|
const uint8_t internal_marker[16] = {
|
|
'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
|
(uint8_t)(OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE), 0,0,0};
|
|
memcpy(internal_caps_image.data() + 4, internal_marker, sizeof(internal_marker));
|
|
EXPECT_TRUE(ota_bootloader_caps_from_image(
|
|
internal_caps_image.data(), internal_caps_image.size(),
|
|
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
|
caps));
|
|
EXPECT_FALSE(ota_bootloader_caps_from_image(
|
|
internal_caps_image.data(), internal_caps_image.size(),
|
|
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE,
|
|
caps));
|
|
memcpy(internal_caps_image.data() + 24, internal_marker, sizeof(internal_marker));
|
|
internal_caps_image[24 + 12] =
|
|
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE;
|
|
EXPECT_FALSE(ota_bootloader_caps_from_image(
|
|
internal_caps_image.data(), internal_caps_image.size(),
|
|
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
|
caps));
|
|
|
|
std::vector<uint8_t> full_only_caps(32, 0xFF);
|
|
memcpy(full_only_caps.data() + 4, internal_marker, sizeof(internal_marker));
|
|
full_only_caps[4 + 10] = 1;
|
|
EXPECT_FALSE(ota_bootloader_caps_from_image(
|
|
full_only_caps.data(), full_only_caps.size(),
|
|
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_BOOT_UPDATE,
|
|
caps));
|
|
|
|
OtaBlCaps selected;
|
|
selected.present = true;
|
|
selected.apply_abi = 3;
|
|
selected.storage_flags = OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE;
|
|
EXPECT_FALSE(ota_bl_caps_prefer(selected, 99, OTA_BL_STORAGE_QSPI, true));
|
|
EXPECT_TRUE(ota_bl_caps_prefer(selected, 4,
|
|
OTA_BL_STORAGE_QSPI | OTA_BL_STORAGE_BOOT_UPDATE, true));
|
|
|
|
auto mf = boot_manifest_bytes();
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse_manifest(mf.data(), mf.size(), m));
|
|
OtaBootloaderIdentity installed;
|
|
installed.present = installed.crc_ok = true;
|
|
installed.image_start = OTA_BOOT_IMAGE_START;
|
|
installed.image_size = OTA_BOOT_IMAGE_SIZE;
|
|
installed.board_id = OTA_XIAO_BOARD_ID_BASE;
|
|
memcpy(installed.device_name, OTA_XIAO_BOOT_DEVICE_NAME,
|
|
OTA_BOOT_DEVICE_NAME_SIZE);
|
|
uint8_t mid[4] = {0x11,0x22,0x33,0x44};
|
|
uint8_t hash8[8]; memcpy(hash8, m.image_hash, sizeof(hash8));
|
|
EXPECT_EQ(ota_bootloader_confirmation_gate(m, installed, mid, mid, hash8), OTA_BOOT_CONFIRM_OK);
|
|
uint8_t bad_mid[4] = {0};
|
|
EXPECT_EQ(ota_bootloader_confirmation_gate(m, installed, mid, bad_mid, hash8),
|
|
OTA_BOOT_CONFIRM_MID);
|
|
hash8[0] ^= 1;
|
|
EXPECT_EQ(ota_bootloader_confirmation_gate(m, installed, mid, mid, hash8),
|
|
OTA_BOOT_CONFIRM_IMAGE_HASH);
|
|
}
|
|
|
|
TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
|
|
const uint8_t qspi_profile =
|
|
OTA_BL_STORAGE_STAGE_CEILING | OTA_BL_STORAGE_QSPI |
|
|
OTA_BL_STORAGE_BOOT_UPDATE;
|
|
uint8_t marker[16] = {'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
|
qspi_profile, 0,0,0};
|
|
uint8_t image[64];
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 1, marker, sizeof(marker));
|
|
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
|
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 4, marker, sizeof(marker));
|
|
const OtaBlCaps caps = ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile);
|
|
ASSERT_TRUE(caps.present);
|
|
EXPECT_EQ(caps.apply_abi, 3u);
|
|
EXPECT_EQ(caps.storage_flags, qspi_profile);
|
|
|
|
marker[8] = 0; marker[9] = 1;
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 4, marker, sizeof(marker));
|
|
const OtaBlCaps high_abi =
|
|
ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile);
|
|
ASSERT_TRUE(high_abi.present);
|
|
EXPECT_EQ(high_abi.apply_abi, 0x0100u);
|
|
marker[8] = 3; marker[9] = 0;
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 4, marker, sizeof(marker));
|
|
|
|
// A malformed aligned magic decoy is ignored, but a second fully valid
|
|
// privileged marker makes the installed capability identity ambiguous.
|
|
memcpy(image + 24, marker, sizeof(marker));
|
|
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
|
memset(image + 24, 0xFF, sizeof(marker));
|
|
memcpy(image + 24, marker, sizeof(marker));
|
|
image[24 + 13] = 1;
|
|
EXPECT_TRUE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
|
|
|
// Any second structurally valid privileged marker is ambiguous, including
|
|
// another known storage profile or the bare BOOT_UPDATE capability.
|
|
memcpy(image + 24, marker, sizeof(marker));
|
|
image[24 + 12] = OTA_BL_STORAGE_STAGE_CEILING |
|
|
OTA_BL_STORAGE_BOOT_UPDATE;
|
|
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
|
image[24 + 12] = OTA_BL_STORAGE_BOOT_UPDATE;
|
|
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
|
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 4, marker, sizeof(marker));
|
|
image[4 + 12] = OTA_BL_STORAGE_BOOT_UPDATE;
|
|
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
|
|
}
|
|
|
|
TEST(OtaBootPackage, LegacyAndCurrentBootloadersHaveSeparateCapabilityViews) {
|
|
const uint8_t internal_profile = OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE;
|
|
const uint8_t legacy_marker[16] = {
|
|
'M','O','T','A','B','L','D','R', 2,0, 4,0, 0,0,0,0};
|
|
const uint8_t current_marker[16] = {
|
|
'M','O','T','A','B','L','D','R', 3,0, 5,0,
|
|
internal_profile, 0,0,0};
|
|
uint8_t image[64];
|
|
|
|
// The released OTAFIX Preview 5 marker is naturally halfword-aligned at
|
|
// address 2 mod 4. It can still install ABI-2, codec-2 application deltas,
|
|
// but cannot replace itself and must retain the legacy staging ceiling.
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 2, legacy_marker, sizeof(legacy_marker));
|
|
OtaBlCaps app = ota_bl_app_caps_scan(image, sizeof(image));
|
|
OtaBlCaps update = ota_bl_update_caps_scan_aligned(
|
|
image, sizeof(image), internal_profile);
|
|
ASSERT_TRUE(app.present);
|
|
EXPECT_EQ(app.apply_abi, 2u);
|
|
EXPECT_EQ(app.codec_mask, 1u << 2);
|
|
EXPECT_FALSE(update.present);
|
|
EXPECT_FALSE(ota_bootloader_supports_expanded_stage(app));
|
|
|
|
// Do not turn the compatibility exception into a bytewise magic scan.
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 1, legacy_marker, sizeof(legacy_marker));
|
|
EXPECT_FALSE(ota_bl_app_caps_scan(image, sizeof(image)).present);
|
|
|
|
// The current marker enables both views and advertises the expanded-stage
|
|
// handoff independently of the privileged self-update decision.
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 4, current_marker, sizeof(current_marker));
|
|
app = ota_bl_app_caps_scan(image, sizeof(image));
|
|
update = ota_bl_update_caps_scan_aligned(image, sizeof(image), internal_profile);
|
|
ASSERT_TRUE(app.present);
|
|
ASSERT_TRUE(update.present);
|
|
EXPECT_EQ(update.storage_flags, internal_profile);
|
|
EXPECT_TRUE(ota_bootloader_supports_expanded_stage(app));
|
|
|
|
// Ambiguous privileged markers fail closed only for self-update. Ordinary
|
|
// application OTA remains available through the unprivileged view.
|
|
memcpy(image + 24, current_marker, sizeof(current_marker));
|
|
app = ota_bl_app_caps_scan(image, sizeof(image));
|
|
update = ota_bl_update_caps_scan_aligned(image, sizeof(image), internal_profile);
|
|
EXPECT_TRUE(app.present);
|
|
EXPECT_FALSE(update.present);
|
|
|
|
// A valid marker for another storage profile is visible diagnostically but
|
|
// cannot authorize an internal-flash bootloader replacement.
|
|
memset(image, 0xFF, sizeof(image));
|
|
memcpy(image + 4, current_marker, sizeof(current_marker));
|
|
image[4 + 12] = OTA_BL_PROFILE_QSPI_BOOT_UPDATE;
|
|
app = ota_bl_app_caps_scan(image, sizeof(image));
|
|
update = ota_bl_update_caps_scan_aligned(image, sizeof(image), internal_profile);
|
|
EXPECT_TRUE(app.present);
|
|
EXPECT_FALSE(update.present);
|
|
}
|
|
|
|
class FakeMotaSeederStream : public Stream {
|
|
public:
|
|
using Stream::write;
|
|
|
|
size_t write(const uint8_t* request, size_t len) override {
|
|
request_valid = len == 11 && request[0] == MOTA_SEEDER_REQ_MAGIC0
|
|
&& request[1] == MOTA_SEEDER_REQ_MAGIC1 && request[2] == MS_OP_READ;
|
|
if (!request_valid) return len;
|
|
|
|
uint8_t checksum = request[2];
|
|
for (size_t i = 3; i + 1 < len; i++) checksum ^= request[i];
|
|
request_valid = checksum == request[len - 1];
|
|
const uint32_t offset = rd_u32le(request + 4);
|
|
const uint16_t read_len = rd_u16le(request + 8);
|
|
offsets.push_back(offset);
|
|
lengths.push_back(read_len);
|
|
|
|
response.clear();
|
|
response_pos = 0;
|
|
response.push_back(MOTA_SEEDER_RSP_MAGIC0);
|
|
response.push_back(MOTA_SEEDER_RSP_MAGIC1);
|
|
response.push_back(MS_OP_READ);
|
|
response.push_back(MS_STATUS_OK);
|
|
uint8_t response_checksum = MOTA_SEEDER_RSP_MAGIC0 ^ MOTA_SEEDER_RSP_MAGIC1
|
|
^ MS_OP_READ ^ MS_STATUS_OK;
|
|
for (uint16_t i = 0; i < read_len; i++) {
|
|
const uint8_t value = (uint8_t)(offset + i);
|
|
response.push_back(value);
|
|
response_checksum ^= value;
|
|
}
|
|
response.push_back(response_checksum);
|
|
return len;
|
|
}
|
|
|
|
int read() override {
|
|
if (response_pos < response.size()) return response[response_pos++];
|
|
++g_mock_millis;
|
|
return -1;
|
|
}
|
|
|
|
void flush() override {
|
|
++flush_calls;
|
|
if (flush_discards_receive) {
|
|
response.clear();
|
|
response_pos = 0;
|
|
}
|
|
}
|
|
|
|
std::vector<uint32_t> offsets;
|
|
std::vector<uint16_t> lengths;
|
|
bool request_valid = true;
|
|
bool flush_discards_receive = false;
|
|
size_t flush_calls = 0;
|
|
|
|
private:
|
|
std::vector<uint8_t> response;
|
|
size_t response_pos = 0;
|
|
};
|
|
|
|
class FakeDeflateMotaSeederStream : public Stream {
|
|
public:
|
|
using Stream::write;
|
|
|
|
FakeDeflateMotaSeederStream() {
|
|
encoded.resize(421);
|
|
for (size_t i = 0; i < encoded.size(); i++) encoded[i] = (uint8_t)(i * 17u + 3u);
|
|
}
|
|
|
|
size_t write(const uint8_t* request, size_t len) override {
|
|
request_valid = len == 11 && request[0] == MOTA_SEEDER_REQ_MAGIC0
|
|
&& request[1] == MOTA_SEEDER_REQ_MAGIC1 && request[2] == MS_OP_DEFLATE_BLOCK;
|
|
uint8_t checksum = request_valid ? request[2] : 0;
|
|
for (size_t i = 3; request_valid && i + 1 < len; i++) checksum ^= request[i];
|
|
request_valid = request_valid && checksum == request[len - 1];
|
|
if (!request_valid) return len;
|
|
|
|
const uint16_t block = rd_u16le(request + 4);
|
|
const uint16_t offset = rd_u16le(request + 6);
|
|
const uint16_t read_len = rd_u16le(request + 8);
|
|
blocks.push_back(block);
|
|
offsets.push_back(offset);
|
|
lengths.push_back(read_len);
|
|
|
|
response.clear();
|
|
response_pos = 0;
|
|
response.push_back(MOTA_SEEDER_RSP_MAGIC0);
|
|
response.push_back(MOTA_SEEDER_RSP_MAGIC1);
|
|
response.push_back(MS_OP_DEFLATE_BLOCK);
|
|
const bool valid_range = offset <= encoded.size() && read_len <= encoded.size() - offset;
|
|
const uint8_t status = reject || !valid_range ? MS_STATUS_ERR : MS_STATUS_OK;
|
|
response.push_back(status);
|
|
if (status == MS_STATUS_OK) {
|
|
response.push_back((uint8_t)(encoded.size() & 0xFF));
|
|
response.push_back((uint8_t)(encoded.size() >> 8));
|
|
response.insert(response.end(), encoded.begin() + offset,
|
|
encoded.begin() + offset + read_len);
|
|
}
|
|
uint8_t response_checksum = 0;
|
|
for (uint8_t byte : response) response_checksum ^= byte;
|
|
response.push_back(response_checksum);
|
|
return len;
|
|
}
|
|
|
|
int read() override {
|
|
if (response_pos < response.size()) return response[response_pos++];
|
|
++g_mock_millis;
|
|
return -1;
|
|
}
|
|
|
|
void flush() override { ++flush_calls; }
|
|
|
|
std::vector<uint8_t> encoded;
|
|
std::vector<uint16_t> blocks;
|
|
std::vector<uint16_t> offsets;
|
|
std::vector<uint16_t> lengths;
|
|
bool request_valid = true;
|
|
bool reject = false;
|
|
size_t flush_calls = 0;
|
|
|
|
private:
|
|
std::vector<uint8_t> response;
|
|
size_t response_pos = 0;
|
|
};
|
|
|
|
static_assert(!std::is_constructible<SerialMotaSource, Stream&>::value,
|
|
"mOTA source callers must choose a stream write policy");
|
|
static_assert(!std::is_constructible<SerialMotaSource, Stream&, uint32_t>::value,
|
|
"a timeout must not be mistaken for a stream write policy");
|
|
static_assert(!std::is_constructible<FolderMotaStore, Stream&>::value,
|
|
"folder store callers must choose a stream write policy");
|
|
static_assert(!std::is_constructible<FolderMotaStore, Stream&, uint32_t>::value,
|
|
"a timeout must not be mistaken for a stream write policy");
|
|
|
|
TEST(MotaSourceSerial, SplitsOneKilobyteReadsBelowCdcReceiveRing) {
|
|
resetArduinoMock();
|
|
FakeMotaSeederStream stream;
|
|
SerialMotaSource source(stream, MotaStreamWritePolicy::FlushTransmit);
|
|
std::array<uint8_t, 1024> data{};
|
|
|
|
ASSERT_TRUE(source.read(0, 0x1000, data.data(), data.size()));
|
|
EXPECT_TRUE(stream.request_valid);
|
|
EXPECT_EQ(stream.offsets,
|
|
(std::vector<uint32_t>{0x1000, 0x10C0, 0x1180, 0x1240, 0x1300, 0x13C0}));
|
|
EXPECT_EQ(stream.lengths,
|
|
(std::vector<uint16_t>{192, 192, 192, 192, 192, 64}));
|
|
EXPECT_EQ(stream.flush_calls, 6U);
|
|
for (size_t i = 0; i < data.size(); i++) {
|
|
EXPECT_EQ(data[i], (uint8_t)(0x1000 + i));
|
|
}
|
|
}
|
|
|
|
TEST(MotaSourceSerial, AcceptsEmptyReadAndRejectsInvalidRange) {
|
|
resetArduinoMock();
|
|
FakeMotaSeederStream stream;
|
|
SerialMotaSource source(stream, MotaStreamWritePolicy::FlushTransmit);
|
|
uint8_t byte = 0;
|
|
|
|
EXPECT_TRUE(source.read(0, 123, nullptr, 0));
|
|
EXPECT_FALSE(source.read(0, 123, nullptr, 1));
|
|
EXPECT_FALSE(source.read(0, UINT32_MAX, &byte, 2));
|
|
EXPECT_TRUE(stream.offsets.empty());
|
|
}
|
|
|
|
TEST(MotaSourceSerial, NetworkPolicyPreservesReplyWhenFlushWouldDiscardRx) {
|
|
resetArduinoMock();
|
|
FakeMotaSeederStream stream;
|
|
stream.flush_discards_receive = true;
|
|
SerialMotaSource source(stream, MotaStreamWritePolicy::NoFlush, 20);
|
|
std::array<uint8_t, 16> data{};
|
|
|
|
ASSERT_TRUE(source.read(0, 0x2000, data.data(), data.size()));
|
|
EXPECT_EQ(stream.flush_calls, 0U);
|
|
for (size_t i = 0; i < data.size(); i++) {
|
|
EXPECT_EQ(data[i], (uint8_t)(0x2000 + i));
|
|
}
|
|
}
|
|
|
|
TEST(MotaSourceSerial, WrongFlushPolicyDemonstratesNetworkReplyLoss) {
|
|
resetArduinoMock();
|
|
FakeMotaSeederStream stream;
|
|
stream.flush_discards_receive = true;
|
|
SerialMotaSource source(stream, MotaStreamWritePolicy::FlushTransmit, 20);
|
|
std::array<uint8_t, 4> data{};
|
|
|
|
EXPECT_FALSE(source.read(0, 0x3000, data.data(), data.size()));
|
|
EXPECT_EQ(stream.flush_calls, 1U);
|
|
}
|
|
|
|
TEST(MotaSourceSerial, ReadsIndependentDeflateBlockInBoundedChunks) {
|
|
resetArduinoMock();
|
|
FakeDeflateMotaSeederStream stream;
|
|
SerialMotaSource source(stream, MotaStreamWritePolicy::FlushTransmit);
|
|
std::array<uint8_t, 1024> data{};
|
|
uint16_t encoded_len = 0;
|
|
|
|
ASSERT_TRUE(source.read_deflated_block(2, 37, data.data(), data.size(), &encoded_len));
|
|
EXPECT_TRUE(stream.request_valid);
|
|
EXPECT_EQ(encoded_len, stream.encoded.size());
|
|
EXPECT_EQ(stream.blocks, (std::vector<uint16_t>{37, 37, 37, 37}));
|
|
EXPECT_EQ(stream.offsets, (std::vector<uint16_t>{0, 0, 190, 380}));
|
|
EXPECT_EQ(stream.lengths, (std::vector<uint16_t>{0, 190, 190, 41}));
|
|
EXPECT_EQ(stream.flush_calls, 4u);
|
|
EXPECT_EQ(0, memcmp(data.data(), stream.encoded.data(), encoded_len));
|
|
}
|
|
|
|
TEST(MotaSourceSerial, DeflateExtensionFailureLeavesRawFallbackAvailable) {
|
|
resetArduinoMock();
|
|
FakeDeflateMotaSeederStream stream;
|
|
stream.reject = true; // deployed host does not implement operation 0x09
|
|
SerialMotaSource source(stream, MotaStreamWritePolicy::FlushTransmit);
|
|
std::array<uint8_t, 1024> data{};
|
|
uint16_t encoded_len = 999;
|
|
|
|
EXPECT_FALSE(source.read_deflated_block(0, 0, data.data(), data.size(), &encoded_len));
|
|
EXPECT_EQ(encoded_len, 0u);
|
|
EXPECT_EQ(stream.offsets, (std::vector<uint16_t>{0}));
|
|
EXPECT_EQ(stream.lengths, (std::vector<uint16_t>{0}));
|
|
}
|
|
|
|
class FakeFolderMotaSeederStream : public Stream {
|
|
public:
|
|
using Stream::write;
|
|
|
|
size_t write(const uint8_t* request, size_t len) override {
|
|
request_valid = len >= 4
|
|
&& request[0] == MOTA_SEEDER_REQ_MAGIC0
|
|
&& request[1] == MOTA_SEEDER_REQ_MAGIC1;
|
|
if (!request_valid) return len;
|
|
|
|
uint8_t checksum = request[2];
|
|
for (size_t i = 3; i + 1 < len; i++) checksum ^= request[i];
|
|
request_valid = checksum == request[len - 1];
|
|
if (!request_valid) return len;
|
|
|
|
operations.push_back(request[2]);
|
|
response = {
|
|
MOTA_SEEDER_RSP_MAGIC0,
|
|
MOTA_SEEDER_RSP_MAGIC1,
|
|
request[2],
|
|
MS_STATUS_OK,
|
|
(uint8_t)(MOTA_SEEDER_RSP_MAGIC0 ^ MOTA_SEEDER_RSP_MAGIC1
|
|
^ request[2] ^ MS_STATUS_OK),
|
|
};
|
|
response_pos = 0;
|
|
return len;
|
|
}
|
|
|
|
int read() override {
|
|
if (response_pos < response.size()) return response[response_pos++];
|
|
++g_mock_millis;
|
|
return -1;
|
|
}
|
|
|
|
void flush() override {
|
|
++flush_calls;
|
|
response.clear();
|
|
response_pos = 0;
|
|
}
|
|
|
|
std::vector<uint8_t> operations;
|
|
bool request_valid = true;
|
|
size_t flush_calls = 0;
|
|
|
|
private:
|
|
std::vector<uint8_t> response;
|
|
size_t response_pos = 0;
|
|
};
|
|
|
|
TEST(FolderMotaStore, NetworkPolicyPreservesFastBeginAndWriteReplies) {
|
|
resetArduinoMock();
|
|
FakeFolderMotaSeederStream stream;
|
|
FolderMotaStore store(stream, MotaStreamWritePolicy::NoFlush, 20);
|
|
const uint8_t mid[4] = {0x10, 0x20, 0x30, 0x40};
|
|
const uint8_t data[4] = {1, 2, 3, 4};
|
|
store.set_mid(mid);
|
|
|
|
ASSERT_TRUE(store.begin(64));
|
|
ASSERT_TRUE(store.write(0, data, sizeof(data)));
|
|
EXPECT_TRUE(stream.request_valid);
|
|
EXPECT_EQ(stream.operations,
|
|
(std::vector<uint8_t>{MS_OP_BEGIN, MS_OP_WRITE}));
|
|
EXPECT_EQ(stream.flush_calls, 0U);
|
|
}
|
|
|
|
// Build a flashed-image layout (body || fixed 56-byte EndF) the way the host packager / build hook do:
|
|
// marker(4) body_len(4) body_hash8(8) fw_version(4) target_id(4) hw_id(32). Identity is always present
|
|
// (zero/"" = unknown).
|
|
static std::vector<uint8_t> make_image_id(const std::vector<uint8_t>& body, uint32_t fw_version,
|
|
uint32_t target_id, const char* hw_id) {
|
|
std::vector<uint8_t> img = body;
|
|
img.insert(img.end(), ENDF_MAGIC, ENDF_MAGIC + 4);
|
|
uint32_t n = (uint32_t)body.size();
|
|
for (int i = 0; i < 4; i++) img.push_back((uint8_t)(n >> (8 * i)));
|
|
uint8_t h[8]; mh8(h, body.data(), body.size());
|
|
img.insert(img.end(), h, h + 8);
|
|
for (int i = 0; i < 4; i++) img.push_back((uint8_t)(fw_version >> (8 * i)));
|
|
for (int i = 0; i < 4; i++) img.push_back((uint8_t)(target_id >> (8 * i)));
|
|
uint8_t hw[32] = {0}; size_t k = hw_id ? strlen(hw_id) : 0; if (k > 32) k = 32; if (k) memcpy(hw, hw_id, k);
|
|
img.insert(img.end(), hw, hw + 32); // -> fixed 56-byte trailer
|
|
return img;
|
|
}
|
|
static std::vector<uint8_t> make_image(const std::vector<uint8_t>& body) {
|
|
return make_image_id(body, 0, 0, ""); // zero identity (still a full 56-byte trailer)
|
|
}
|
|
|
|
// --- cross-check the C++ parser/merkle against the Python reference vectors ----------------
|
|
|
|
TEST(OtaParse, ParsesReferenceContainer) {
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m));
|
|
EXPECT_EQ(m.format_ver, MOTA_FORMAT_VER);
|
|
EXPECT_TRUE(m.is_full());
|
|
EXPECT_FALSE(m.is_signed());
|
|
EXPECT_EQ(m.target_id, EXP_TARGET_ID);
|
|
EXPECT_EQ(m.fw_version, EXP_FW_VERSION);
|
|
EXPECT_EQ(m.image_size, EXP_IMAGE_SIZE);
|
|
EXPECT_EQ(m.payload_size, EXP_PAYLOAD_SIZE);
|
|
EXPECT_EQ(m.block_count, EXP_BLOCK_COUNT);
|
|
EXPECT_EQ(m.block_size_log2, EXP_BLOCK_SIZE_LOG2);
|
|
EXPECT_EQ(m.codec_id, EXP_CODEC_ID);
|
|
EXPECT_EQ(0, memcmp(m.merkle_root, EXP_MERKLE_ROOT, 4));
|
|
EXPECT_EQ(0, memcmp(m.image_hash, EXP_IMAGE_HASH, 32));
|
|
ASSERT_NE(m.hw_id, nullptr);
|
|
EXPECT_EQ(0, memcmp(m.hw_id, EXP_HW_ID, 32)); // v2 hardware tag ("TESTHW" NUL-padded)
|
|
EXPECT_EQ(0, memcmp(m.approval, APPROVAL_NOT, 4)); // distributed = not approved
|
|
EXPECT_FALSE(m.is_approved());
|
|
}
|
|
|
|
TEST(OtaRescue, RequiresInvalidSelfExactBaseAndExactTarget) {
|
|
const uint8_t package_base[8] = {0x63, 0xd8, 0xdf, 0x63, 1, 2, 3, 4};
|
|
const uint8_t operator_base[8] = {0x63, 0xd8, 0xdf, 0x63, 1, 2, 3, 4};
|
|
const uint8_t wrong_base[8] = {0x63, 0xd8, 0xdf, 0x63, 1, 2, 3, 5};
|
|
const uint32_t target_id = 0x2FA509C1u;
|
|
|
|
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, operator_base,
|
|
target_id, target_id), NRF52_RESCUE_OK);
|
|
EXPECT_EQ(ota_nrf52_rescue_gate(true, package_base, operator_base,
|
|
target_id, target_id), NRF52_RESCUE_SELF_VALID);
|
|
EXPECT_EQ(ota_nrf52_rescue_gate(false, nullptr, operator_base,
|
|
target_id, target_id), NRF52_RESCUE_BASE_MISSING);
|
|
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, wrong_base,
|
|
target_id, target_id), NRF52_RESCUE_BASE_MISMATCH);
|
|
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, operator_base,
|
|
target_id, target_id + 1), NRF52_RESCUE_TARGET_MISMATCH);
|
|
EXPECT_EQ(ota_nrf52_rescue_gate(false, package_base, operator_base,
|
|
target_id, 0), NRF52_RESCUE_TARGET_MISMATCH);
|
|
}
|
|
|
|
TEST(OtaParse, RejectsTampering) {
|
|
MotaManifest m;
|
|
// bad magic
|
|
std::vector<uint8_t> b(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN);
|
|
b[0] ^= 0xFF;
|
|
EXPECT_FALSE(mota_parse(b.data(), b.size(), m));
|
|
// bad trailer
|
|
b.assign(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN);
|
|
b[b.size() - 1] ^= 0xFF;
|
|
EXPECT_FALSE(mota_parse(b.data(), b.size(), m));
|
|
// wrong total-size field
|
|
b.assign(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN);
|
|
b[4] ^= 0x01;
|
|
EXPECT_FALSE(mota_parse(b.data(), b.size(), m));
|
|
}
|
|
|
|
// block_idx is a uint16 on the wire, so a manifest needing > 65535 blocks can't be addressed and must be
|
|
// rejected at parse (this also keeps block_count*4 from overflowing the leaves-length computation).
|
|
TEST(OtaParse, RejectsTooManyBlocks) {
|
|
auto manifest = [](uint32_t payload_size, uint8_t bsl) { // fixed-layout unsigned-full manifest
|
|
std::vector<uint8_t> m(MOTA_MFL, 0);
|
|
m[0] = MOTA_FORMAT_VER; m[1] = MFLAG_FULL; m[2] = 0x12;
|
|
m[15] = payload_size; m[16] = payload_size >> 8; m[17] = payload_size >> 16; m[18] = payload_size >> 24;
|
|
m[19] = bsl;
|
|
return m;
|
|
};
|
|
MotaManifest mm;
|
|
auto over = manifest(65536u * 1024u, 10); // 65536 blocks -> rejected
|
|
EXPECT_FALSE(mota_parse_manifest(over.data(), over.size(), mm));
|
|
auto ok = manifest(65535u * 1024u, 10); // 65535 blocks -> allowed
|
|
EXPECT_TRUE(mota_parse_manifest(ok.data(), ok.size(), mm));
|
|
EXPECT_EQ(mm.block_count, 65535u);
|
|
}
|
|
|
|
TEST(OtaParse, BlockCountCeilingDoesNotOverflowAtUint32Maximum) {
|
|
std::vector<uint8_t> manifest(MOTA_MFL, 0);
|
|
manifest[0] = MOTA_FORMAT_VER;
|
|
manifest[1] = MFLAG_FULL;
|
|
manifest[2] = HASH_ALGO_SHA256;
|
|
manifest[15] = 0xFF;
|
|
manifest[16] = 0xFF;
|
|
manifest[17] = 0xFF;
|
|
manifest[18] = 0xFF;
|
|
manifest[19] = 24; // largest parser-valid block: ceil(UINT32_MAX / 2^24) = 256
|
|
|
|
MotaManifest parsed;
|
|
ASSERT_TRUE(mota_parse_manifest(manifest.data(), manifest.size(), parsed));
|
|
EXPECT_EQ(parsed.block_count, 256u);
|
|
}
|
|
|
|
TEST(OtaMerkle, RootMatchesVectorAndLeaves) {
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m));
|
|
// root recomputed from stored leaves[] == merkle_root field == Python's EXP_MERKLE_ROOT
|
|
uint8_t root[4];
|
|
merkle_root(root, m.leaves, m.block_count);
|
|
EXPECT_EQ(0, memcmp(root, EXP_MERKLE_ROOT, 4));
|
|
EXPECT_TRUE(mota_check_root(m));
|
|
// recompute each leaf from the payload block and compare to the stored leaf
|
|
uint32_t bs = m.block_size();
|
|
for (uint32_t i = 0; i < m.block_count; i++) {
|
|
uint32_t off = i * bs;
|
|
uint32_t len = (off + bs <= m.payload_size) ? bs : (m.payload_size - off);
|
|
uint8_t leaf[4];
|
|
merkle_leaf(leaf, m.payload + off, len);
|
|
EXPECT_EQ(0, memcmp(leaf, m.leaves + i * 4, 4)) << "leaf " << i;
|
|
}
|
|
EXPECT_TRUE(mota_check_payload(m));
|
|
}
|
|
|
|
TEST(OtaMerkle, PayloadCheckDetectsCorruptionNotVisibleInStoredLeaves) {
|
|
std::vector<uint8_t> b(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN);
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(b.data(), b.size(), m));
|
|
ASSERT_TRUE(mota_check_root(m));
|
|
ASSERT_TRUE(mota_check_payload(m));
|
|
|
|
size_t payload_off = (size_t)(m.payload - b.data());
|
|
b[payload_off + m.payload_size / 2] ^= 0x01;
|
|
ASSERT_TRUE(mota_parse(b.data(), b.size(), m));
|
|
EXPECT_TRUE(mota_check_root(m)); // leaves[] and its root are still self-consistent
|
|
EXPECT_FALSE(mota_check_payload(m)); // actual staged payload no longer matches those leaves
|
|
}
|
|
|
|
TEST(OtaMerkle, FullImageHashMatches) {
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m));
|
|
EXPECT_TRUE(mota_check_image_hash_full(m));
|
|
}
|
|
|
|
TEST(OtaMerkle, ProofFromReferenceVerifies) {
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m));
|
|
uint32_t bs = m.block_size();
|
|
uint32_t off = PROOF_INDEX * bs;
|
|
uint32_t len = (off + bs <= m.payload_size) ? bs : (m.payload_size - off);
|
|
|
|
EXPECT_TRUE(merkle_verify(m.payload + off, len, PROOF_INDEX,
|
|
PROOF_SIBLINGS, PROOF_NSIB, EXP_MERKLE_ROOT, m.block_count));
|
|
|
|
// tampered block -> fails
|
|
std::vector<uint8_t> blk(m.payload + off, m.payload + off + len);
|
|
blk[0] ^= 0xFF;
|
|
EXPECT_FALSE(merkle_verify(blk.data(), len, PROOF_INDEX,
|
|
PROOF_SIBLINGS, PROOF_NSIB, EXP_MERKLE_ROOT, m.block_count));
|
|
|
|
// wrong index with the same proof -> fails
|
|
EXPECT_FALSE(merkle_verify(m.payload + off, len, PROOF_INDEX + 1,
|
|
PROOF_SIBLINGS, PROOF_NSIB, EXP_MERKLE_ROOT, m.block_count));
|
|
}
|
|
|
|
// --- validate the O(log n) binary-counter root vs a plain level-by-level reference ----------
|
|
|
|
static void ref_root(uint8_t out[4], std::vector<std::array<uint8_t, 4>> level) {
|
|
while (level.size() > 1) {
|
|
std::vector<std::array<uint8_t, 4>> nxt;
|
|
for (size_t i = 0; i < level.size(); i += 2) {
|
|
if (i + 1 < level.size()) {
|
|
std::array<uint8_t, 4> p;
|
|
merkle_combine(p.data(), level[i].data(), level[i + 1].data());
|
|
nxt.push_back(p);
|
|
} else {
|
|
nxt.push_back(level[i]); // promote lone last node
|
|
}
|
|
}
|
|
level.swap(nxt);
|
|
}
|
|
std::memcpy(out, level[0].data(), 4);
|
|
}
|
|
|
|
TEST(OtaMerkle, BinaryCounterMatchesLevelByLevel) {
|
|
uint32_t state = 0x12345678;
|
|
auto rnd = [&]() { state = state * 1103515245u + 12345u; return (uint8_t)(state >> 16); };
|
|
// O(log n) root must equal the plain level-by-level root for every count
|
|
for (uint32_t count = 1; count <= 600; count++) {
|
|
std::vector<uint8_t> leaves(count * 4);
|
|
std::vector<std::array<uint8_t, 4>> ref(count);
|
|
for (uint32_t i = 0; i < count; i++)
|
|
for (int j = 0; j < 4; j++) { uint8_t v = rnd(); leaves[i * 4 + j] = v; ref[i][j] = v; }
|
|
uint8_t a[4], b[4];
|
|
merkle_root(a, leaves.data(), count);
|
|
ref_root(b, ref);
|
|
ASSERT_EQ(0, std::memcmp(a, b, 4)) << "root mismatch count=" << count;
|
|
}
|
|
}
|
|
|
|
TEST(OtaMerkle, StreamingAccumulatorMatchesContiguousRoot) {
|
|
uint8_t leaves[257 * 4];
|
|
for (size_t i = 0; i < sizeof(leaves); ++i) {
|
|
leaves[i] = (uint8_t)(i * 29U + 7U);
|
|
}
|
|
MerkleAccumulator accumulator;
|
|
for (uint32_t i = 0; i < 257; ++i) {
|
|
ASSERT_TRUE(accumulator.add(leaves + i * 4));
|
|
}
|
|
uint8_t streamed[4], contiguous[4];
|
|
ASSERT_TRUE(accumulator.finish(streamed));
|
|
merkle_root(contiguous, leaves, 257);
|
|
EXPECT_EQ(0, std::memcmp(streamed, contiguous, 4));
|
|
EXPECT_EQ(257U, accumulator.count());
|
|
|
|
accumulator.reset();
|
|
EXPECT_FALSE(accumulator.finish(streamed));
|
|
EXPECT_EQ(0U, accumulator.count());
|
|
}
|
|
|
|
// Verify every block's proof for several tricky counts, using proofs generated by the Python
|
|
// reference (the oracle) - covers deep promotion chains (100, 255, 256, ...).
|
|
TEST(OtaMerkle, ReferenceProofsAllIndices) {
|
|
for (int c = 0; c < N_PROOF_CASES; c++) {
|
|
const ProofCase& pc = PROOF_CASES[c];
|
|
uint8_t root[4];
|
|
merkle_root(root, pc.leaves, pc.count);
|
|
EXPECT_EQ(0, std::memcmp(root, pc.root, 4)) << "root mismatch count=" << pc.count;
|
|
for (uint32_t i = 0; i < pc.count; i++) {
|
|
EXPECT_TRUE(merkle_verify_from_leaf(pc.leaves + i * 4, i,
|
|
pc.pblob + pc.poff[i], pc.pnsib[i], pc.root, pc.count))
|
|
<< "count=" << pc.count << " idx=" << i;
|
|
}
|
|
// a wrong sibling for index 0 must fail
|
|
if (pc.pnsib[0] > 0) {
|
|
std::vector<uint8_t> bad(pc.pblob + pc.poff[0], pc.pblob + pc.poff[0] + pc.pnsib[0] * 4);
|
|
bad[0] ^= 0xFF;
|
|
EXPECT_FALSE(merkle_verify_from_leaf(pc.leaves, 0, bad.data(), pc.pnsib[0], pc.root, pc.count));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- availability bitmap (derived from leaves[]) -------------------------------------------
|
|
|
|
TEST(OtaBitmap, AllPresentForCompleteContainer) {
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m));
|
|
EXPECT_TRUE(all_present(m.leaves, m.block_count));
|
|
EXPECT_EQ(count_present(m.leaves, m.block_count), m.block_count);
|
|
|
|
// a leaf slot of all-FF (erased) means "missing"; bitmap round-trips
|
|
std::vector<uint8_t> leaves(m.leaves, m.leaves + m.block_count * 4);
|
|
std::memset(&leaves[4], 0xFF, 4); // mark block 1 missing
|
|
EXPECT_FALSE(leaf_present(leaves.data(), 1));
|
|
EXPECT_FALSE(all_present(leaves.data(), m.block_count));
|
|
EXPECT_EQ(count_present(leaves.data(), m.block_count), m.block_count - 1);
|
|
|
|
std::vector<uint8_t> bm(bitmap_bytes(m.block_count));
|
|
leaves_to_bitmap(leaves.data(), m.block_count, bm.data());
|
|
EXPECT_FALSE(bitmap_get(bm.data(), 1));
|
|
EXPECT_TRUE(bitmap_get(bm.data(), 0));
|
|
}
|
|
|
|
// --- EndF self-firmware scan (P2) -----------------------------------------------------------
|
|
|
|
TEST(OtaFirmwareInfo, FindsEndFInImage) {
|
|
std::vector<uint8_t> body(4321);
|
|
for (size_t i = 0; i < body.size(); i++) body[i] = (uint8_t)(i * 37 + 11);
|
|
std::vector<uint8_t> img = make_image(body);
|
|
|
|
// simulate a flash region: image, then erased 0xFF up to the partition end
|
|
std::vector<uint8_t> region = img;
|
|
region.resize(img.size() + 4096, 0xFF);
|
|
|
|
SelfFwInfo fi;
|
|
ASSERT_TRUE(find_self_firmware(region.data(), (uint32_t)region.size(), fi, /*verify_body=*/true));
|
|
EXPECT_EQ(fi.body_len, body.size());
|
|
EXPECT_EQ(fi.image_len, img.size());
|
|
EXPECT_EQ(fi.endf_offset, body.size());
|
|
uint8_t h[8]; mh8(h, body.data(), body.size());
|
|
EXPECT_EQ(0, std::memcmp(fi.body_hash, h, 8));
|
|
}
|
|
|
|
// The self-describing identity lives at fixed offsets in the 56-byte EndF and is always parsed; a
|
|
// zero-identity trailer reports zero/"" (unknown), still at the fixed 56-byte size.
|
|
TEST(OtaFirmwareInfo, ParsesIdentity) {
|
|
std::vector<uint8_t> body(2000);
|
|
for (size_t i = 0; i < body.size(); i++) body[i] = (uint8_t)(i * 13 + 5);
|
|
|
|
auto img = make_image_id(body, 0x01100000u, 0x04d413fdu, "RAK4631");
|
|
std::vector<uint8_t> region = img; region.resize(img.size() + 4096, 0xFF);
|
|
SelfFwInfo fi;
|
|
ASSERT_TRUE(find_self_firmware(region.data(), (uint32_t)region.size(), fi, /*verify_body=*/true));
|
|
EXPECT_EQ(fi.body_len, body.size());
|
|
EXPECT_EQ(fi.image_len, body.size() + 56); // fixed trailer length
|
|
EXPECT_EQ(fi.fw_version, 0x01100000u);
|
|
EXPECT_EQ(fi.target_id, 0x04d413fdu);
|
|
EXPECT_STREQ(fi.hw_id, "RAK4631");
|
|
|
|
auto img1 = make_image(body); // zero-identity trailer (still 56 bytes)
|
|
std::vector<uint8_t> r1 = img1; r1.resize(img1.size() + 64, 0xFF);
|
|
SelfFwInfo fi1;
|
|
ASSERT_TRUE(find_self_firmware(r1.data(), (uint32_t)r1.size(), fi1, true));
|
|
EXPECT_EQ(fi1.fw_version, 0u);
|
|
EXPECT_EQ(fi1.target_id, 0u);
|
|
EXPECT_STREQ(fi1.hw_id, "");
|
|
EXPECT_EQ(fi1.image_len, body.size() + 56);
|
|
}
|
|
|
|
TEST(OtaFirmwareInfo, IgnoresStagedMotaHigherInRegion) {
|
|
// The firmware's own EndF must win even when a staged .mota (which embeds its own EndF) sits
|
|
// above it in the same region - the body_len == offset check disambiguates.
|
|
std::vector<uint8_t> body(2000);
|
|
for (size_t i = 0; i < body.size(); i++) body[i] = (uint8_t)(i ^ 0x5A);
|
|
std::vector<uint8_t> img = make_image(body);
|
|
|
|
std::vector<uint8_t> region = img;
|
|
region.resize(8192, 0xFF); // gap
|
|
// drop the reference .mota (which contains an embedded EndF in its payload) higher up
|
|
region.insert(region.end(), MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN);
|
|
|
|
SelfFwInfo fi;
|
|
ASSERT_TRUE(find_self_firmware(region.data(), (uint32_t)region.size(), fi, true));
|
|
EXPECT_EQ(fi.endf_offset, body.size()); // found OUR firmware, not the .mota's
|
|
EXPECT_EQ(fi.body_len, body.size());
|
|
}
|
|
|
|
TEST(OtaFirmwareInfo, NoMarkerReturnsFalse) {
|
|
std::vector<uint8_t> region(1000, 0xAB);
|
|
SelfFwInfo fi;
|
|
EXPECT_FALSE(find_self_firmware(region.data(), (uint32_t)region.size(), fi));
|
|
}
|
|
|
|
// --- signer allowlist (P3) ------------------------------------------------------------------
|
|
|
|
TEST(OtaAllowlist, AddContainsRemoveSerialize) {
|
|
SignerAllowlist a;
|
|
uint8_t k1[32], k2[32], k3[32];
|
|
memset(k1, 0x11, 32); memset(k2, 0x22, 32); memset(k3, 0x33, 32);
|
|
EXPECT_FALSE(a.contains(k1));
|
|
EXPECT_TRUE(a.add(k1));
|
|
EXPECT_TRUE(a.add(k2));
|
|
EXPECT_TRUE(a.add(k1)); // idempotent
|
|
EXPECT_EQ(a.count(), 2);
|
|
EXPECT_TRUE(a.contains(k1));
|
|
EXPECT_FALSE(a.contains(k3));
|
|
|
|
uint8_t buf[1 + MAX_OTA_SIGNERS * 32];
|
|
uint32_t n = a.serialize(buf, sizeof(buf));
|
|
EXPECT_EQ(n, 1u + 2 * 32);
|
|
SignerAllowlist b;
|
|
EXPECT_TRUE(b.deserialize(buf, n));
|
|
EXPECT_EQ(b.count(), 2);
|
|
EXPECT_TRUE(b.contains(k1) && b.contains(k2));
|
|
|
|
EXPECT_TRUE(a.remove(k1));
|
|
EXPECT_EQ(a.count(), 1);
|
|
EXPECT_FALSE(a.contains(k1));
|
|
EXPECT_TRUE(a.contains(k2));
|
|
}
|
|
|
|
// --- RAM store: out-of-order writes + availability via leaves[] --------------------------------
|
|
|
|
TEST(OtaStoreRamTest, RandomAccessAndErasedSentinel) {
|
|
OtaStoreRam<4096> s;
|
|
ASSERT_TRUE(s.begin(1000));
|
|
EXPECT_EQ(s.staged_size(), 1000u);
|
|
uint8_t blk[8] = {1,2,3,4,5,6,7,8};
|
|
EXPECT_TRUE(s.write(500, blk, 8)); // out-of-order offset
|
|
EXPECT_TRUE(s.write(0, blk, 8));
|
|
EXPECT_FALSE(s.write(998, blk, 8)); // out of range
|
|
uint8_t rd[8];
|
|
EXPECT_TRUE(s.read(500, rd, 8));
|
|
EXPECT_EQ(0, memcmp(rd, blk, 8));
|
|
// untouched region reads as erased 0xFF (so an unfilled leaf slot is "missing")
|
|
EXPECT_TRUE(s.read(100, rd, 8));
|
|
for (int i = 0; i < 8; i++) EXPECT_EQ(rd[i], 0xFF);
|
|
}
|
|
|
|
TEST(OtaStoreRamTest, ClearKeepsResumeFixtureButDiscardConsumesHeader) {
|
|
OtaStoreRam<4096> s;
|
|
constexpr uint32_t total = 8u + MOTA_MFL + 5u;
|
|
uint8_t header[8] = {'m', 'O', 'T', 'A',
|
|
static_cast<uint8_t>(total),
|
|
static_cast<uint8_t>(total >> 8),
|
|
static_cast<uint8_t>(total >> 16),
|
|
static_cast<uint8_t>(total >> 24)};
|
|
ASSERT_TRUE(s.begin(total));
|
|
ASSERT_TRUE(s.write(0, header, sizeof(header)));
|
|
|
|
// clear() remains the cheap RAM-session reset used by reboot/resume tests.
|
|
s.clear();
|
|
EXPECT_TRUE(s.reopen());
|
|
EXPECT_EQ(s.staged_size(), total);
|
|
|
|
// User-facing cancellation uses discard(), which must prevent a fresh
|
|
// reopen even for the native in-process persistence model.
|
|
EXPECT_TRUE(s.discard());
|
|
EXPECT_EQ(s.staged_size(), 0u);
|
|
EXPECT_FALSE(s.reopen());
|
|
}
|
|
|
|
TEST(OtaStoreRamTest, SeederSizedStoreDiscardsWithoutAssumingAHeaderFits) {
|
|
// OTA_SEEDER_ONLY intentionally uses OtaStoreRam<1>: it needs a valid
|
|
// manager destination object but never stages firmware locally.
|
|
OtaStoreRam<1> s;
|
|
EXPECT_TRUE(s.discard());
|
|
EXPECT_EQ(s.staged_size(), 0u);
|
|
EXPECT_FALSE(s.reopen());
|
|
}
|
|
|
|
namespace {
|
|
|
|
class SessionOnlyOtaStore : public OtaStore {
|
|
public:
|
|
bool begin(uint32_t) override { return false; }
|
|
bool write(uint32_t, const uint8_t*, uint32_t) override { return false; }
|
|
bool read(uint32_t, uint8_t*, uint32_t) const override { return false; }
|
|
uint32_t capacity() const override { return 0; }
|
|
uint32_t staged_size() const override { return 0; }
|
|
void clear() override { cleared = true; }
|
|
|
|
bool cleared = false;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST(OtaStoreDiscardTest, ConservativeDefaultCannotClaimDurableInvalidation) {
|
|
SessionOnlyOtaStore store;
|
|
EXPECT_FALSE(store.discard());
|
|
EXPECT_TRUE(store.cleared);
|
|
}
|
|
|
|
TEST(OtaDeflate, AcceptsStoredFixedDynamicAndMultiBlockRawStreams) {
|
|
static const uint8_t stored[] = {
|
|
0x01,0x40,0x00,0xBF,0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,
|
|
0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,
|
|
0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,
|
|
0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31,0x32,0x33,0x34,0x35,0x36,
|
|
0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,
|
|
};
|
|
static const uint8_t fixed[] = {
|
|
0xF3,0x4D,0x2D,0xCE,0x70,0xCE,0x2F,0x4A,0x55,0x28,0x29,0x4A,0xCC,0x2B,0x2E,0xC8,
|
|
0x2F,0x2A,0x51,0x48,0xCB,0xAC,0x48,0x4D,0x51,0xF0,0x28,0x4D,0x4B,0xCB,0x4D,0xCC,
|
|
0x53,0x28,0x49,0x2D,0x2E,0xD1,0x53,0xF0,0x1D,0x55,0x46,0x1B,0x65,0x00,
|
|
};
|
|
static const uint8_t dynamic[] = {
|
|
0xED,0xCF,0xD9,0x0D,0x83,0x30,0x14,0x44,0xD1,0x0B,0xC1,0x2C,0xC1,0x2C,0xC1,0x2C,
|
|
0xB1,0x81,0xF4,0x5F,0x66,0x9E,0x26,0x05,0xA4,0x01,0x7F,0x5F,0x1D,0x69,0x86,0xA2,
|
|
0x7C,0x54,0xAE,0xA6,0xED,0x9E,0xBD,0x1F,0x98,0xE6,0xD7,0x12,0x56,0xF6,0xE3,0x1D,
|
|
0xD3,0xC9,0xFD,0xC1,0x32,0xAE,0x6E,0x2C,0xE3,0x87,0xD1,0x32,0x61,0xDD,0x2C,0x93,
|
|
0xCE,0xCB,0x32,0xD2,0x0D,0xD2,0x23,0xD2,0x1B,0xD2,0x17,0xD2,0x15,0xD2,0x3D,0xD2,
|
|
0x0B,0xD2,0x11,0xE9,0x02,0xE9,0x16,0xE9,0x09,0xE9,0x1D,0xE9,0x1B,0x69,0x87,0xB4,
|
|
0x47,0x3A,0x20,0x9D,0x90,0x2E,0x91,0xEE,0x90,0x9E,0x91,0x3E,0x88,0xBF,0x69,0xF9,
|
|
0x58,0x3E,0x96,0x8F,0xFD,0x3B,0xF6,0x05,
|
|
};
|
|
static const uint8_t multi_block[] = {
|
|
0x4A,0xCB,0x2C,0x2A,0x2E,0xD1,0x4D,0xCA,0xC9,0x4F,0xCE,0xD6,0x4D,0x1B,0x66,0x6C,
|
|
0x00,0x00,0x00,0x00,0xFF,0xFF,0x2B,0x4E,0x4D,0xCE,0xCF,0x4B,0x81,0x0A,0x8C,0x72,
|
|
0xF0,0x72,0x00,
|
|
};
|
|
|
|
EXPECT_EQ((stored[0] >> 1) & 3u, 0u);
|
|
EXPECT_EQ((fixed[0] >> 1) & 3u, 1u);
|
|
EXPECT_EQ((dynamic[0] >> 1) & 3u, 2u);
|
|
|
|
std::array<uint8_t, 1024> output{};
|
|
uint16_t output_len = 0;
|
|
ASSERT_TRUE(ota_transport_inflate(nullptr, stored, sizeof(stored), output.data(), 64, &output_len));
|
|
ASSERT_EQ(output_len, 64u);
|
|
for (uint16_t i = 0; i < output_len; i++) EXPECT_EQ(output[i], (uint8_t)i);
|
|
|
|
static const char fixed_phrase[] = "MeshCore transport fixed Huffman test. ";
|
|
ASSERT_TRUE(ota_transport_inflate(nullptr, fixed, sizeof(fixed), output.data(), 384, &output_len));
|
|
ASSERT_EQ(output_len, 384u);
|
|
for (uint16_t i = 0; i < output_len; i++)
|
|
EXPECT_EQ(output[i], (uint8_t)fixed_phrase[i % (sizeof(fixed_phrase) - 1)]);
|
|
|
|
ASSERT_TRUE(ota_transport_inflate(nullptr, dynamic, sizeof(dynamic), output.data(), 1024, &output_len));
|
|
ASSERT_EQ(output_len, 1024u);
|
|
for (uint16_t i = 0; i < output_len; i++)
|
|
EXPECT_EQ(output[i], (uint8_t)((i % 7u) ? i % 31u : 0u));
|
|
|
|
ASSERT_TRUE(ota_transport_inflate(nullptr, multi_block, sizeof(multi_block), output.data(), 520,
|
|
&output_len));
|
|
ASSERT_EQ(output_len, 520u);
|
|
static const char first_phrase[] = "first-block-";
|
|
static const char second_phrase[] = "second-block-";
|
|
for (uint16_t i = 0; i < 220; i++)
|
|
EXPECT_EQ(output[i], (uint8_t)first_phrase[i % (sizeof(first_phrase) - 1)]);
|
|
for (uint16_t i = 220; i < output_len; i++)
|
|
EXPECT_EQ(output[i], (uint8_t)second_phrase[(i - 220) % (sizeof(second_phrase) - 1)]);
|
|
}
|
|
|
|
TEST(OtaDeflate, RejectsTruncationBoundsWrongLengthTrailingBytesAndMalformedTrees) {
|
|
static const uint8_t dynamic[] = {
|
|
0xED,0xCF,0xD9,0x0D,0x83,0x30,0x14,0x44,0xD1,0x0B,0xC1,0x2C,0xC1,0x2C,0xC1,0x2C,
|
|
0xB1,0x81,0xF4,0x5F,0x66,0x9E,0x26,0x05,0xA4,0x01,0x7F,0x5F,0x1D,0x69,0x86,0xA2,
|
|
0x7C,0x54,0xAE,0xA6,0xED,0x9E,0xBD,0x1F,0x98,0xE6,0xD7,0x12,0x56,0xF6,0xE3,0x1D,
|
|
0xD3,0xC9,0xFD,0xC1,0x32,0xAE,0x6E,0x2C,0xE3,0x87,0xD1,0x32,0x61,0xDD,0x2C,0x93,
|
|
0xCE,0xCB,0x32,0xD2,0x0D,0xD2,0x23,0xD2,0x1B,0xD2,0x17,0xD2,0x15,0xD2,0x3D,0xD2,
|
|
0x0B,0xD2,0x11,0xE9,0x02,0xE9,0x16,0xE9,0x09,0xE9,0x1D,0xE9,0x1B,0x69,0x87,0xB4,
|
|
0x47,0x3A,0x20,0x9D,0x90,0x2E,0x91,0xEE,0x90,0x9E,0x91,0x3E,0x88,0xBF,0x69,0xF9,
|
|
0x58,0x3E,0x96,0x8F,0xFD,0x3B,0xF6,0x05,
|
|
};
|
|
static const uint8_t invalid_distance[] = {0x63,0x00,0x3E,0x00};
|
|
static const uint8_t invalid_tree[] = {
|
|
0x0D,0xCE,0x81,0x00,0x00,0x00,0x00,0x80,0xA0,0xFD,0xA9,0xBB,0x1F,0xA0,0x01,
|
|
};
|
|
std::array<uint8_t, 1025> output{};
|
|
uint16_t output_len = 999;
|
|
|
|
EXPECT_FALSE(ota_transport_inflate(nullptr, dynamic, sizeof(dynamic) - 1,
|
|
output.data(), 1024, &output_len));
|
|
EXPECT_EQ(output_len, 0u);
|
|
EXPECT_FALSE(ota_transport_inflate(nullptr, dynamic, sizeof(dynamic),
|
|
output.data(), 1023, &output_len));
|
|
EXPECT_FALSE(ota_transport_inflate(nullptr, dynamic, sizeof(dynamic),
|
|
output.data(), 1025, &output_len));
|
|
|
|
std::array<uint8_t, sizeof(dynamic) + 1> trailing{};
|
|
memcpy(trailing.data(), dynamic, sizeof(dynamic));
|
|
trailing.back() = 0x00;
|
|
EXPECT_FALSE(ota_transport_inflate(nullptr, trailing.data(), trailing.size(),
|
|
output.data(), 1024, &output_len));
|
|
EXPECT_FALSE(ota_transport_inflate(nullptr, invalid_distance, sizeof(invalid_distance),
|
|
output.data(), 4, &output_len));
|
|
EXPECT_FALSE(ota_transport_inflate(nullptr, invalid_tree, sizeof(invalid_tree),
|
|
output.data(), 4, &output_len));
|
|
}
|
|
|
|
// --- merkle proof GENERATION (server side) matches the Python oracle ---------------------------
|
|
|
|
TEST(OtaMerkle, GenProofMatchesPythonAndVerifies) {
|
|
for (int c = 0; c < N_PROOF_CASES; c++) {
|
|
const ProofCase& pc = PROOF_CASES[c];
|
|
std::vector<uint8_t> scratch(pc.count * 4);
|
|
uint8_t out[32 * 4];
|
|
for (uint32_t i = 0; i < pc.count; i++) {
|
|
uint8_t n = merkle_gen_proof(pc.leaves, pc.count, i, scratch.data(), out);
|
|
ASSERT_EQ(n, pc.pnsib[i]) << "count=" << pc.count << " idx=" << i;
|
|
EXPECT_EQ(0, std::memcmp(out, pc.pblob + pc.poff[i], (size_t)n * 4))
|
|
<< "gen_proof != python count=" << pc.count << " idx=" << i;
|
|
EXPECT_TRUE(merkle_verify_from_leaf(pc.leaves + i * 4, i, out, n, pc.root, pc.count));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- protocol codec round-trips ---------------------------------------------------------------
|
|
|
|
TEST(OtaProtocol, ClassifiesOnlyActiveTransferMessagesAsPrimary) {
|
|
EXPECT_FALSE(ota_is_transfer_message(OTA_ADV));
|
|
EXPECT_FALSE(ota_is_transfer_message(OTA_QUERY));
|
|
EXPECT_FALSE(ota_is_transfer_message(OTA_HAVE));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_GET_MANIFEST));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_MANIFEST));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_REQ));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_DATA));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_REQ_PROOF));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_PROOF));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_GET_LEAVES));
|
|
EXPECT_TRUE(ota_is_transfer_message(OTA_LEAVES));
|
|
EXPECT_FALSE(ota_is_transfer_message(0xFF));
|
|
}
|
|
|
|
TEST(OtaProtocol, CodecRoundTrips) {
|
|
uint8_t buf[200];
|
|
|
|
// OTA_ADV is now a tiny per-node beacon: seeder_id + n_motas + set_digest
|
|
AdvMsg adv{{0x29,0x17,0xe4,0xf7}, 7, {0xde,0xad,0xbe,0xef}};
|
|
uint16_t n = encode_adv(buf, sizeof(buf), adv);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_ADV); EXPECT_EQ(n, 10);
|
|
AdvMsg a2; ASSERT_TRUE(decode_adv(buf, n, a2));
|
|
EXPECT_EQ(0, memcmp(a2.seeder_id, adv.seeder_id, 4));
|
|
EXPECT_EQ(a2.n_motas, 7);
|
|
EXPECT_EQ(0, memcmp(a2.set_digest, adv.set_digest, 4));
|
|
|
|
// OTA_QUERY: ask a source (by seeder_id) for the offering set_digest, optionally filtered to a target
|
|
QueryMsg qy{{0x29,0x17,0xe4,0xf7}, {0xd1,0xd2,0xd3,0xd4}, 0x11223344, 0xA5A50002};
|
|
n = encode_query(buf, sizeof(buf), qy);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_QUERY);
|
|
QueryMsg q2; ASSERT_TRUE(decode_query(buf, n, q2));
|
|
EXPECT_EQ(0, memcmp(q2.seeder_id, qy.seeder_id, 4));
|
|
EXPECT_EQ(0, memcmp(q2.set_digest, qy.set_digest, 4));
|
|
EXPECT_EQ(q2.filter_target, 0x11223344u);
|
|
EXPECT_EQ(q2.want_fragments, 0xA5A50002u);
|
|
QueryMsg qlegacy;
|
|
ASSERT_TRUE(decode_query(buf, n - 4, qlegacy)); // original QUERY ended after filter_target
|
|
EXPECT_EQ(qlegacy.filter_target, 0x11223344u);
|
|
EXPECT_EQ(qlegacy.want_fragments, 0u); // old sender means "all fragments"
|
|
|
|
// OTA_HAVE: a 2-row catalog (mid, target, fwver, codec, flags per row) tagged with the offering digest
|
|
uint8_t rows[2 * OTA_HAVE_ROW_BYTES];
|
|
for (int i = 0; i < 2 * OTA_HAVE_ROW_BYTES; i++) rows[i] = (uint8_t)(i + 1);
|
|
HaveMsg hv{{0x29,0x17,0xe4,0xf7}, {0xd1,0xd2,0xd3,0xd4}, 0, 1, 2, rows};
|
|
n = encode_have(buf, sizeof(buf), hv);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_HAVE);
|
|
HaveMsg h2; ASSERT_TRUE(decode_have(buf, n, h2));
|
|
EXPECT_EQ(0, memcmp(h2.seeder_id, hv.seeder_id, 4));
|
|
EXPECT_EQ(0, memcmp(h2.set_digest, hv.set_digest, 4));
|
|
EXPECT_EQ(h2.frag_total, 1); EXPECT_EQ(h2.n_rows, 2);
|
|
EXPECT_EQ(0, memcmp(h2.rows, rows, 2 * OTA_HAVE_ROW_BYTES));
|
|
|
|
GetManifestMsg gm{{1,2,3,4}, 0x0002}; // want only manifest fragment 1 (recovery)
|
|
n = encode_get_manifest(buf, sizeof(buf), gm);
|
|
GetManifestMsg g2; ASSERT_TRUE(decode_get_manifest(buf, n, g2));
|
|
EXPECT_EQ(0, memcmp(g2.manifest_id, gm.manifest_id, 4));
|
|
EXPECT_EQ(g2.want_mask, 0x0002);
|
|
|
|
uint8_t mbytes[40]; for (int i = 0; i < 40; i++) mbytes[i] = (uint8_t)(i + 1);
|
|
ManifestMsg mm{{9,8,7,6}, 0, 1, mbytes, 40};
|
|
n = encode_manifest(buf, sizeof(buf), mm);
|
|
ManifestMsg m2; ASSERT_TRUE(decode_manifest(buf, n, m2));
|
|
EXPECT_EQ(m2.frag_idx, 0); EXPECT_EQ(m2.frag_total, 1); EXPECT_EQ(m2.len, 40);
|
|
EXPECT_EQ(0, memcmp(m2.bytes, mbytes, 40));
|
|
|
|
ReqMsg rq{{4,3,2,1}, 7, 0x005f}; // block 7, want fragments {0,1,2,3,4,6} (a recovery mask)
|
|
n = encode_req(buf, sizeof(buf), rq);
|
|
ReqMsg r2; ASSERT_TRUE(decode_req(buf, n, r2));
|
|
EXPECT_EQ(r2.block_idx, 7); EXPECT_EQ(r2.want_mask, 0x005f);
|
|
|
|
// A request window is an append-only extension: its first row is byte-for-byte a legacy ReqMsg, while
|
|
// new decoders see every requested block. This is the rolling-upgrade fallback for adaptive flights.
|
|
ReqWindowMsg rw{};
|
|
memcpy(rw.manifest_id, rq.manifest_id, 4);
|
|
rw.n_items = 3;
|
|
rw.items[0] = {7, 0x005f}; rw.items[1] = {8, 0x007f}; rw.items[2] = {9, 0x0003};
|
|
n = encode_req_window(buf, sizeof(buf), rw);
|
|
ASSERT_EQ(n, 17u);
|
|
ReqMsg legacy;
|
|
ASSERT_TRUE(decode_req(buf, n, legacy)); // an old source serves row zero
|
|
EXPECT_EQ(legacy.block_idx, 7); EXPECT_EQ(legacy.want_mask, 0x005f);
|
|
ReqWindowMsg rw2{};
|
|
ASSERT_TRUE(decode_req_window(buf, n, rw2));
|
|
ASSERT_EQ(rw2.n_items, 3u);
|
|
EXPECT_EQ(rw2.items[1].block_idx, 8); EXPECT_EQ(rw2.items[2].want_mask, 0x0003);
|
|
EXPECT_FALSE(decode_req_window(buf, n - 1, rw2)); // reject a truncated final row
|
|
|
|
// GET_LEAVES: bulk-fetch the target leaves[] with a fragment want_mask (motatool warm-start)
|
|
GetLeavesMsg gl{{5,6,7,8}, 0x0007}; // want leaves fragments {0,1,2}
|
|
n = encode_get_leaves(buf, sizeof(buf), gl);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_GET_LEAVES);
|
|
GetLeavesMsg gl2; ASSERT_TRUE(decode_get_leaves(buf, n, gl2));
|
|
EXPECT_EQ(0, memcmp(gl2.manifest_id, gl.manifest_id, 4)); EXPECT_EQ(gl2.want_mask, 0x0007);
|
|
|
|
// LEAVES: one fragment of the leaves[] array
|
|
uint8_t lbytes[80]; for (int i = 0; i < 80; i++) lbytes[i] = (uint8_t)(200 - i);
|
|
LeavesMsg lm{{5,6,7,8}, 1, 4, lbytes, 80};
|
|
n = encode_leaves(buf, sizeof(buf), lm);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_LEAVES);
|
|
LeavesMsg lm2; ASSERT_TRUE(decode_leaves(buf, n, lm2));
|
|
EXPECT_EQ(lm2.frag_idx, 1); EXPECT_EQ(lm2.frag_total, 4); EXPECT_EQ(lm2.len, 80);
|
|
EXPECT_EQ(0, memcmp(lm2.bytes, lbytes, 80));
|
|
|
|
// DATA is one self-describing fragment of a block (frag_off places it; proof is fetched separately)
|
|
uint8_t data[100]; for (int i = 0; i < 100; i++) data[i] = (uint8_t)(i * 3);
|
|
DataMsg dm{{0,1,2,3}, 42, 0, data, 100}; // block 42, fragment at offset 0
|
|
n = encode_data(buf, sizeof(buf), dm);
|
|
DataMsg d2; ASSERT_TRUE(decode_data(buf, n, d2));
|
|
EXPECT_EQ(d2.block_idx, 42); EXPECT_EQ(d2.frag_off, 0);
|
|
EXPECT_EQ(d2.data_len, 100); EXPECT_EQ(0, memcmp(d2.data, data, 100));
|
|
|
|
// a later slice of the same block (non-zero frag_off)
|
|
DataMsg dm2{{0,1,2,3}, 42, 160, data, 50};
|
|
n = encode_data(buf, sizeof(buf), dm2);
|
|
DataMsg d3; ASSERT_TRUE(decode_data(buf, n, d3));
|
|
EXPECT_EQ(d3.block_idx, 42); EXPECT_EQ(d3.frag_off, 160); EXPECT_EQ(d3.data_len, 50);
|
|
|
|
// REQ_PROOF: request the merkle proof for one (reassembled) block
|
|
ReqProofMsg rp{{7,7,8,8}, 13};
|
|
n = encode_req_proof(buf, sizeof(buf), rp);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_REQ_PROOF);
|
|
ReqProofMsg rp2; ASSERT_TRUE(decode_req_proof(buf, n, rp2));
|
|
EXPECT_EQ(0, memcmp(rp2.manifest_id, rp.manifest_id, 4)); EXPECT_EQ(rp2.block_idx, 13);
|
|
|
|
// PROOF: ordered sibling digests for one block
|
|
uint8_t proof[12]; for (int i = 0; i < 12; i++) proof[i] = (uint8_t)(0xA0 + i);
|
|
ProofMsg pm{{7,7,8,8}, 13, 3, proof};
|
|
n = encode_proof(buf, sizeof(buf), pm);
|
|
ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_PROOF);
|
|
ProofMsg pm2; ASSERT_TRUE(decode_proof(buf, n, pm2));
|
|
EXPECT_EQ(0, memcmp(pm2.manifest_id, pm.manifest_id, 4));
|
|
EXPECT_EQ(pm2.block_idx, 13); EXPECT_EQ(pm2.n_proof, 3);
|
|
EXPECT_EQ(0, memcmp(pm2.proof, proof, 12));
|
|
}
|
|
|
|
TEST(OtaProtocol, V2ProfilesPackLegacyAndExtendedLengthBoundaries) {
|
|
const uint16_t request = ota_req_make_v2(0x007Fu, true);
|
|
EXPECT_TRUE(ota_req_is_v2(request));
|
|
EXPECT_EQ(ota_req_v2_fragments(request), 0x007Fu);
|
|
EXPECT_NE(request & OTA_REQ_V2_ALLOW_DEFLATE, 0u);
|
|
EXPECT_FALSE(ota_req_v2_extended_length(request));
|
|
|
|
// Extended v2 needs at most twelve 171-byte fragments. Keeping fragment bit 12 clear leaves the
|
|
// deployed legacy "send all" value 0xFFFF unambiguous.
|
|
const uint16_t extended_request = ota_req_make_v2(0x0FFFu, true, true);
|
|
EXPECT_TRUE(ota_req_is_v2(extended_request));
|
|
EXPECT_TRUE(ota_req_v2_extended_length(extended_request));
|
|
EXPECT_EQ(ota_req_v2_fragments(extended_request), 0x0FFFu);
|
|
EXPECT_EQ(extended_request, 0xEFFFu);
|
|
EXPECT_FALSE(ota_req_is_v2(0xFFFFu)); // deployed legacy full-block request
|
|
EXPECT_FALSE(ota_req_is_v2(0x007Fu));
|
|
|
|
uint16_t descriptor = 0;
|
|
ASSERT_TRUE(ota_data_v2_pack(5, 1024, true, descriptor));
|
|
uint8_t fragment = 0;
|
|
uint16_t encoded_len = 0;
|
|
bool deflated = false;
|
|
ASSERT_TRUE(ota_data_v2_unpack(descriptor, fragment, encoded_len, deflated));
|
|
EXPECT_EQ(fragment, 5u);
|
|
EXPECT_EQ(encoded_len, 1024u);
|
|
EXPECT_TRUE(deflated);
|
|
EXPECT_FALSE(ota_data_v2_unpack((uint16_t)(descriptor & ~OTA_DATA_V2_MARK),
|
|
fragment, encoded_len, deflated));
|
|
EXPECT_FALSE(ota_data_v2_pack(16, 1024, false, descriptor));
|
|
EXPECT_FALSE(ota_data_v2_pack(0, 0, false, descriptor));
|
|
EXPECT_FALSE(ota_data_v2_pack(0, 1025, false, descriptor));
|
|
|
|
ASSERT_TRUE(ota_data_v2_pack_extended(15, 2048, descriptor));
|
|
EXPECT_EQ(descriptor, 0xFFFFu);
|
|
ASSERT_TRUE(ota_data_v2_unpack_extended(descriptor, 2048,
|
|
fragment, encoded_len, deflated));
|
|
EXPECT_EQ(fragment, 15u);
|
|
EXPECT_EQ(encoded_len, 2048u);
|
|
EXPECT_FALSE(deflated);
|
|
EXPECT_FALSE(ota_data_v2_unpack_extended(descriptor, 2047,
|
|
fragment, encoded_len, deflated));
|
|
|
|
ASSERT_TRUE(ota_data_v2_pack_extended(0, 1024, descriptor));
|
|
ASSERT_TRUE(ota_data_v2_unpack_extended(descriptor, 2048,
|
|
fragment, encoded_len, deflated));
|
|
EXPECT_EQ(fragment, 0u);
|
|
EXPECT_EQ(encoded_len, 1024u);
|
|
EXPECT_TRUE(deflated);
|
|
|
|
ASSERT_TRUE(ota_data_v2_pack_extended(0, 193, descriptor));
|
|
ASSERT_TRUE(ota_data_v2_unpack_extended(descriptor, 193,
|
|
fragment, encoded_len, deflated));
|
|
EXPECT_EQ(encoded_len, 193u);
|
|
EXPECT_FALSE(deflated); // raw short tail in a 2 KiB manifest
|
|
EXPECT_FALSE(ota_data_v2_pack_extended(16, 2048, descriptor));
|
|
EXPECT_FALSE(ota_data_v2_pack_extended(0, 0, descriptor));
|
|
EXPECT_FALSE(ota_data_v2_pack_extended(0, 2049, descriptor));
|
|
EXPECT_FALSE(ota_data_v2_unpack_extended(0, 2048,
|
|
fragment, encoded_len, deflated));
|
|
EXPECT_FALSE(ota_data_v2_unpack_extended(OTA_DATA_V2_MARK, 0,
|
|
fragment, encoded_len, deflated));
|
|
EXPECT_FALSE(ota_data_v2_unpack_extended(OTA_DATA_V2_MARK, 2049,
|
|
fragment, encoded_len, deflated));
|
|
|
|
std::array<uint8_t, OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2> payload{};
|
|
ASSERT_TRUE(ota_data_v2_pack(0, 1024, false, descriptor));
|
|
DataMsg message{{1, 2, 3, 4}, 9, descriptor, payload.data(),
|
|
(uint16_t)payload.size()};
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
const uint16_t wire_len = encode_data(wire, sizeof(wire), message);
|
|
EXPECT_EQ(wire_len, MAX_PACKET_PAYLOAD);
|
|
DataMsg decoded{};
|
|
ASSERT_TRUE(decode_data(wire, wire_len, decoded));
|
|
EXPECT_EQ(decoded.data_len,
|
|
(uint16_t)(OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2));
|
|
}
|
|
|
|
// --- full transfer simulation between two OtaManagers (P4b) ------------------------------------
|
|
|
|
namespace {
|
|
struct SimMsg { OtaManager* dest; std::vector<uint8_t> bytes; };
|
|
static std::vector<SimMsg> g_q;
|
|
struct SendTo { OtaManager* dest; };
|
|
static bool sim_send(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) {
|
|
g_q.push_back({((SendTo*)ctx)->dest, std::vector<uint8_t>(msg, msg + len)});
|
|
return true;
|
|
}
|
|
struct ExtendedV2Trace {
|
|
const MotaManifest* manifest = nullptr;
|
|
uint32_t extended_requests = 0;
|
|
uint32_t legacy_requests = 0;
|
|
bool full_raw = false;
|
|
bool tail_raw = false;
|
|
bool full_deflated = false;
|
|
bool tail_deflated = false;
|
|
};
|
|
struct TracedSendTo { OtaManager* dest; ExtendedV2Trace* trace; };
|
|
static bool traced_sim_send(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) {
|
|
TracedSendTo* route = (TracedSendTo*)ctx;
|
|
ExtendedV2Trace* trace = route->trace;
|
|
if (trace && trace->manifest && ota_msg_type(msg, len) == OTA_REQ) {
|
|
ReqWindowMsg request{};
|
|
if (decode_req_window(msg, len, request)) {
|
|
for (uint8_t i = 0; i < request.n_items; i++) {
|
|
if (ota_req_is_v2(request.items[i].want_mask) &&
|
|
ota_req_v2_extended_length(request.items[i].want_mask)) {
|
|
trace->extended_requests++;
|
|
} else if (!ota_req_is_v2(request.items[i].want_mask)) {
|
|
trace->legacy_requests++;
|
|
}
|
|
}
|
|
}
|
|
} else if (trace && trace->manifest && ota_msg_type(msg, len) == OTA_DATA) {
|
|
DataMsg data{};
|
|
if (decode_data(msg, len, data) && (data.frag_off & OTA_DATA_V2_MARK) != 0 &&
|
|
data.block_idx < trace->manifest->block_count) {
|
|
const uint32_t block_offset = (uint32_t)data.block_idx * trace->manifest->block_size();
|
|
const uint16_t raw_len = (uint16_t)std::min<uint32_t>(
|
|
trace->manifest->block_size(), trace->manifest->payload_size - block_offset);
|
|
uint8_t fragment = 0;
|
|
uint16_t encoded_len = 0;
|
|
bool deflated = false;
|
|
if (ota_data_v2_unpack_extended(data.frag_off, raw_len,
|
|
fragment, encoded_len, deflated)) {
|
|
const bool full = raw_len == trace->manifest->block_size();
|
|
if (deflated) {
|
|
if (full) trace->full_deflated = true;
|
|
else trace->tail_deflated = true;
|
|
} else {
|
|
if (full) trace->full_raw = true;
|
|
else trace->tail_raw = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
g_q.push_back({route->dest, std::vector<uint8_t>(msg, msg + len)});
|
|
return true;
|
|
}
|
|
struct CapturedMessages { std::vector<std::vector<uint8_t>> items; };
|
|
static bool capture_send(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) {
|
|
((CapturedMessages*)ctx)->items.emplace_back(msg, msg + len);
|
|
return true;
|
|
}
|
|
struct GatedCapture {
|
|
bool accept = false;
|
|
std::vector<std::vector<uint8_t>> items;
|
|
};
|
|
static bool gated_capture_send(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) {
|
|
GatedCapture* capture = (GatedCapture*)ctx;
|
|
if (!capture->accept) return false;
|
|
capture->items.emplace_back(msg, msg + len);
|
|
return true;
|
|
}
|
|
|
|
// Tiny reversible stand-in for the separately tested RFC1951 implementation. These callbacks isolate the
|
|
// OtaManager negotiation/reassembly path: each payload block is represented by its authenticated block index,
|
|
// then expanded from the immutable test manifest before the normal Merkle proof is checked.
|
|
struct TestWireCodec {
|
|
const MotaManifest* manifest = nullptr;
|
|
uint32_t calls = 0;
|
|
};
|
|
|
|
static bool test_wire_encode(void* context, const uint8_t* source, uint16_t source_len,
|
|
uint8_t* destination, uint16_t capacity, uint16_t* encoded_len) {
|
|
TestWireCodec* codec = (TestWireCodec*)context;
|
|
if (!codec || !codec->manifest || !source || !destination || !encoded_len || capacity < 2) return false;
|
|
const MotaManifest& manifest = *codec->manifest;
|
|
const uint32_t block_size = manifest.block_size();
|
|
for (uint32_t block = 0; block < manifest.block_count && block <= 0xFFu; block++) {
|
|
const uint32_t offset = block * block_size;
|
|
const uint32_t length = std::min<uint32_t>(block_size, manifest.payload_size - offset);
|
|
if (length == source_len && memcmp(source, manifest.payload + offset, length) == 0) {
|
|
destination[0] = 0xD1;
|
|
destination[1] = (uint8_t)block;
|
|
*encoded_len = 2;
|
|
codec->calls++;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool test_wire_decode(void* context, const uint8_t* source, uint16_t source_len,
|
|
uint8_t* destination, uint16_t capacity, uint16_t* decoded_len) {
|
|
TestWireCodec* codec = (TestWireCodec*)context;
|
|
if (!codec || !codec->manifest || !source || !destination || !decoded_len || source_len != 2 ||
|
|
source[0] != 0xD1 || source[1] >= codec->manifest->block_count) return false;
|
|
const MotaManifest& manifest = *codec->manifest;
|
|
const uint32_t offset = (uint32_t)source[1] * manifest.block_size();
|
|
const uint32_t length = std::min<uint32_t>(manifest.block_size(), manifest.payload_size - offset);
|
|
if (length != capacity) return false;
|
|
memcpy(destination, manifest.payload + offset, length);
|
|
*decoded_len = (uint16_t)length;
|
|
codec->calls++;
|
|
return true;
|
|
}
|
|
|
|
struct VectorDeflateCodec {
|
|
const MotaManifest* manifest = nullptr;
|
|
uint32_t calls = 0;
|
|
};
|
|
|
|
static bool vector_2k_wire_encode(void* context, const uint8_t* source, uint16_t source_len,
|
|
uint8_t* destination, uint16_t capacity,
|
|
uint16_t* encoded_len) {
|
|
VectorDeflateCodec* codec = (VectorDeflateCodec*)context;
|
|
if (!codec || !codec->manifest || !source || !destination || !encoded_len ||
|
|
codec->manifest->block_count != SIM_MOTA_2K_BLOCKS) return false;
|
|
for (uint32_t block = 0; block < codec->manifest->block_count; block++) {
|
|
const uint32_t offset = block * codec->manifest->block_size();
|
|
const uint16_t length = (uint16_t)std::min<uint32_t>(
|
|
codec->manifest->block_size(), codec->manifest->payload_size - offset);
|
|
if (length != source_len || memcmp(source, codec->manifest->payload + offset, length) != 0) continue;
|
|
const uint16_t representation_len = SIM_MOTA_2K_DEFLATED_LENGTHS[block];
|
|
if (representation_len > capacity) return false;
|
|
memcpy(destination,
|
|
SIM_MOTA_2K_DEFLATED + SIM_MOTA_2K_DEFLATED_OFFSETS[block],
|
|
representation_len);
|
|
*encoded_len = representation_len;
|
|
codec->calls++;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct RejectWireDecode {
|
|
uint32_t calls = 0;
|
|
};
|
|
|
|
static bool reject_wire_decode(void* context, const uint8_t*, uint16_t,
|
|
uint8_t*, uint16_t, uint16_t* decoded_len) {
|
|
RejectWireDecode* decoder = (RejectWireDecode*)context;
|
|
if (decoder) decoder->calls++;
|
|
if (decoded_len) *decoded_len = 0;
|
|
return false;
|
|
}
|
|
|
|
struct SyntheticRepresentationDecode {
|
|
const uint8_t* representation = nullptr;
|
|
uint16_t representation_len = 0;
|
|
const uint8_t* decoded = nullptr;
|
|
uint16_t decoded_len = 0;
|
|
uint32_t calls = 0;
|
|
};
|
|
|
|
static bool synthetic_representation_decode(void* context, const uint8_t* source,
|
|
uint16_t source_len, uint8_t* destination,
|
|
uint16_t capacity, uint16_t* decoded_len) {
|
|
SyntheticRepresentationDecode* decoder = (SyntheticRepresentationDecode*)context;
|
|
if (!decoder || !source || !destination || !decoded_len ||
|
|
source_len != decoder->representation_len || capacity != decoder->decoded_len ||
|
|
memcmp(source, decoder->representation, source_len) != 0) return false;
|
|
memcpy(destination, decoder->decoded, capacity);
|
|
*decoded_len = capacity;
|
|
decoder->calls++;
|
|
return true;
|
|
}
|
|
|
|
static bool test_representation_decode(void* context, const uint8_t* source, uint16_t source_len,
|
|
uint8_t* destination, uint16_t capacity,
|
|
uint16_t* decoded_len) {
|
|
const MotaManifest* manifest = (const MotaManifest*)context;
|
|
if (!manifest || !source || !destination || !decoded_len || source_len != 300 ||
|
|
(source[0] != 0xA1 && source[0] != 0xB2) || capacity != manifest->block_size()) return false;
|
|
memcpy(destination, manifest->payload, capacity);
|
|
*decoded_len = capacity;
|
|
return true;
|
|
}
|
|
|
|
TEST(OtaMetrics, CountsOnlyPacketsAcceptedByTheRadioAdapter) {
|
|
OtaManager manager;
|
|
GatedCapture sent;
|
|
manager.begin(0, gated_capture_send, &sent);
|
|
|
|
EXPECT_EQ(manager.packetsSent(), 0u);
|
|
manager.announce();
|
|
EXPECT_EQ(manager.packetsSent(), 0u);
|
|
EXPECT_TRUE(sent.items.empty());
|
|
|
|
sent.accept = true;
|
|
manager.announce();
|
|
manager.announce();
|
|
EXPECT_EQ(manager.packetsSent(), 2u);
|
|
EXPECT_EQ(sent.items.size(), 2u);
|
|
}
|
|
|
|
// Drive the bus to quiescence: deliver queued messages; when idle, advance the client's clock (monotonic
|
|
// across calls, so a jittered query scheduled in a prior pump still comes due) and call loop() (fires the
|
|
// scheduled catalog query / block re-requests). Two idle ticks in a row = quiescent.
|
|
static uint32_t g_clk = 0;
|
|
static void pump(OtaManager& client, OtaManager* server = nullptr, int guard_max = 200000) {
|
|
int idle = 0, guard = 0;
|
|
while (guard++ < guard_max) {
|
|
if (server) server->serviceEgress();
|
|
client.serviceEgress();
|
|
if (!g_q.empty()) {
|
|
SimMsg m = std::move(g_q.front()); g_q.erase(g_q.begin());
|
|
m.dest->on_message(m.bytes.data(), (uint16_t)m.bytes.size());
|
|
idle = 0;
|
|
} else {
|
|
g_clk += 5000;
|
|
client.set_clock(g_clk);
|
|
client.loop();
|
|
if (server) {
|
|
server->set_clock(g_clk);
|
|
server->loop();
|
|
server->serviceEgress();
|
|
}
|
|
client.serviceEgress();
|
|
if (!g_q.empty()) { idle = 0; continue; }
|
|
if (++idle >= 2) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void finish_staged_verification(OtaManager& manager) {
|
|
int guard = 10000;
|
|
while (manager.fetchState() == OtaManager::VERIFYING_STAGED
|
|
&& guard-- > 0) {
|
|
manager.loop();
|
|
}
|
|
ASSERT_GT(guard, 0);
|
|
}
|
|
|
|
class FaultingResumeStore : public OtaStoreRam<4096> {
|
|
public:
|
|
void failReadAt(uint32_t offset) { _fail_offset = offset; }
|
|
|
|
bool read(uint32_t offset, uint8_t* buffer,
|
|
uint32_t length) const override {
|
|
if (offset == _fail_offset) return false;
|
|
return OtaStoreRam<4096>::read(offset, buffer, length);
|
|
}
|
|
|
|
private:
|
|
uint32_t _fail_offset = UINT32_MAX;
|
|
};
|
|
|
|
// A test MotaSource backing an external "folder" with one or more complete `.mota` images held in RAM -
|
|
// the simplest concrete transport (a real device uses serial/BLE/WiFi/FS, same interface). describe()
|
|
// parses each container for the catalog + region offsets; read() is a bounds-checked memcpy.
|
|
class RamMotaSource : public mesh::ota::MotaSource {
|
|
public:
|
|
void add(const uint8_t* buf, uint32_t len) { if (_n < 8) { _buf[_n] = buf; _len[_n] = len; _n++; } }
|
|
void setSourceCaps(uint8_t caps) { _source_caps = caps; }
|
|
uint32_t deflateCalls() const { return _deflate_calls; }
|
|
uint8_t count() override { return _n; }
|
|
bool describe(uint8_t idx, mesh::ota::MotaDesc& d) override {
|
|
if (idx >= _n) return false;
|
|
MotaManifest m;
|
|
if (!mota_parse(_buf[idx], _len[idx], m)) return false;
|
|
std::memcpy(d.mid, m.merkle_root, 4);
|
|
d.target_id = m.target_id; d.fw_version = m.fw_version;
|
|
d.codec_id = m.codec_id; d.flags = m.flags;
|
|
d.block_size_log2 = m.block_size_log2;
|
|
d.source_caps = _source_caps;
|
|
d.total_size = _len[idx];
|
|
d.leaves_off = (uint32_t)(m.leaves - _buf[idx]);
|
|
d.block_count = m.block_count;
|
|
d.payload_off = (uint32_t)(m.payload - _buf[idx]);
|
|
d.payload_size = m.payload_size;
|
|
return true;
|
|
}
|
|
bool read(uint8_t idx, uint32_t off, uint8_t* out, uint32_t len) override {
|
|
if (idx >= _n || (uint64_t)off + len > _len[idx]) return false;
|
|
std::memcpy(out, _buf[idx] + off, len);
|
|
return true;
|
|
}
|
|
bool read_deflated_block(uint8_t, uint16_t, uint8_t*, uint16_t, uint16_t* len) override {
|
|
_deflate_calls++;
|
|
if (len) *len = 0;
|
|
return false;
|
|
}
|
|
private:
|
|
const uint8_t* _buf[8] = {nullptr}; uint32_t _len[8] = {0}; uint8_t _n = 0;
|
|
uint8_t _source_caps = 0;
|
|
uint32_t _deflate_calls = 0;
|
|
};
|
|
|
|
class SyntheticCatalogSource : public mesh::ota::MotaSource {
|
|
public:
|
|
explicit SyntheticCatalogSource(uint8_t count, uint8_t block_log2 = 10)
|
|
: _count(count), _block_log2(block_log2) {}
|
|
uint8_t count() override { return _count; }
|
|
bool describe(uint8_t idx, mesh::ota::MotaDesc& d) override {
|
|
if (idx >= _count) return false;
|
|
d = mesh::ota::MotaDesc{};
|
|
d.mid[0] = (uint8_t)(idx + 1); d.mid[1] = 0xA5; d.mid[2] = 0x5A; d.mid[3] = 0xC3;
|
|
d.target_id = SIM_TARGET_ID; d.fw_version = 0x01000000u + idx;
|
|
d.codec_id = CODEC_FULL; d.flags = MFLAG_FULL; d.block_size_log2 = _block_log2;
|
|
d.leaves_off = 8 + MOTA_MFL; d.block_count = 1;
|
|
d.payload_off = d.leaves_off + 4; d.payload_size = 1; d.total_size = d.payload_off + 1 + 5;
|
|
return true;
|
|
}
|
|
bool read(uint8_t, uint32_t, uint8_t*, uint32_t) override { return false; }
|
|
private:
|
|
uint8_t _count;
|
|
uint8_t _block_log2;
|
|
};
|
|
}
|
|
|
|
TEST(OtaServe, ClearPrimaryInvalidatesCallerOwnedView) {
|
|
OtaManager manager;
|
|
manager.begin(0, nullptr, nullptr);
|
|
|
|
ASSERT_TRUE(manager.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
RamMotaSource folder;
|
|
folder.add(SIM_MOTA_1K, SIM_MOTA_1K_LEN);
|
|
ASSERT_TRUE(manager.add_source(&folder));
|
|
ASSERT_EQ(manager.servedCount(), 2);
|
|
|
|
manager.clear_primary();
|
|
ASSERT_EQ(manager.servedCount(), 1);
|
|
EXPECT_FALSE(manager.servedEntry(0)->is_self);
|
|
|
|
// A caller may release the container and later install a fresh primary view
|
|
// without dropping or overwriting attached folder sources.
|
|
EXPECT_TRUE(manager.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
ASSERT_EQ(manager.servedCount(), 2);
|
|
EXPECT_TRUE(manager.servedEntry(0)->is_self);
|
|
EXPECT_FALSE(manager.servedEntry(1)->is_self);
|
|
|
|
// Detaching one source must preserve the primary view (and, on device, any other source such as SD).
|
|
EXPECT_TRUE(manager.remove_source(&folder));
|
|
ASSERT_EQ(manager.servedCount(), 1);
|
|
EXPECT_TRUE(manager.servedEntry(0)->is_self);
|
|
EXPECT_FALSE(manager.remove_source(&folder));
|
|
}
|
|
|
|
TEST(OtaServe, FetchSessionResetPreservesIndependentPrimaryView) {
|
|
OtaManager manager;
|
|
manager.begin(0, nullptr, nullptr);
|
|
ASSERT_TRUE(manager.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
ASSERT_EQ(manager.servedCount(), 1);
|
|
ASSERT_TRUE(manager.servedEntry(0)->is_self);
|
|
|
|
// `ota cancel` resets only the receive side. A served image (and its
|
|
// caller-owned backing buffer) must remain registered until the serving
|
|
// side explicitly calls clear_primary().
|
|
manager.reset_session();
|
|
ASSERT_EQ(manager.servedCount(), 1);
|
|
EXPECT_TRUE(manager.servedEntry(0)->is_self);
|
|
}
|
|
|
|
TEST(OtaTransfer, TwoManagersFullTransfer) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
|
|
server.begin(/*server's own target irrelevant for serving*/ 0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY); // tests exercise fetch-on-advert; policy default is OFF
|
|
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
server.announce(); // -> client hears the beacon, queries, catalogs, then fetches
|
|
|
|
pump(client, &server); // beacon -> query -> have -> startFetch -> full transfer
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksHave(), client.blocksTotal());
|
|
EXPECT_GT(client.blocksTotal(), 1u);
|
|
|
|
// the client's reassembled container must be byte-identical to the original .mota...
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_LEN);
|
|
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA, SIM_MOTA_LEN));
|
|
|
|
// ...and independently re-verify it parses with a matching root + image_hash
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), m));
|
|
EXPECT_TRUE(mota_check_root(m));
|
|
EXPECT_TRUE(mota_check_image_hash_full(m));
|
|
}
|
|
|
|
TEST(OtaTransfer, LegacyOneKilobyteV2CodecStillStagesOriginalContainer) {
|
|
g_q.clear();
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 1024u);
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
TestWireCodec encoder;
|
|
TestWireCodec decoder;
|
|
encoder.manifest = &manifest;
|
|
decoder.manifest = &manifest;
|
|
|
|
server.begin(0, sim_send, &to_client);
|
|
server.set_transport_deflate_encoder(test_wire_encode, &encoder);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_transport_deflate_decoder(test_wire_decode, &decoder);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
|
|
server.announce();
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_GT(encoder.calls, 0u);
|
|
EXPECT_EQ(decoder.calls, manifest.block_count);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN);
|
|
EXPECT_EQ(0, memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
}
|
|
|
|
TEST(OtaTransfer, TwoKilobyteExtendedV2TransfersFullAndShortRawBlocks) {
|
|
g_q.clear();
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_2K, SIM_MOTA_2K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 2048u);
|
|
ASSERT_EQ(manifest.block_count, SIM_MOTA_2K_BLOCKS);
|
|
ASSERT_EQ(manifest.block_count, 2u);
|
|
ASSERT_GT(manifest.payload_size, manifest.block_size());
|
|
ASSERT_LT(manifest.payload_size - manifest.block_size(), manifest.block_size());
|
|
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
ExtendedV2Trace trace{};
|
|
trace.manifest = &manifest;
|
|
TracedSendTo to_client{&client, &trace}, to_server{&server, &trace};
|
|
server.begin(0, traced_sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, traced_sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
|
|
server.announce();
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_GT(trace.extended_requests, 0u);
|
|
EXPECT_EQ(trace.legacy_requests, 0u);
|
|
EXPECT_TRUE(trace.full_raw);
|
|
EXPECT_TRUE(trace.tail_raw);
|
|
EXPECT_FALSE(trace.full_deflated);
|
|
EXPECT_FALSE(trace.tail_deflated);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_2K_LEN);
|
|
EXPECT_EQ(0, memcmp(store.data(), SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
}
|
|
|
|
TEST(OtaTransfer, TwoKilobyteExtendedV2InflatesFullAndShortBlocksBeforeProof) {
|
|
g_q.clear();
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_2K, SIM_MOTA_2K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 2048u);
|
|
ASSERT_EQ(manifest.block_count, SIM_MOTA_2K_BLOCKS);
|
|
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
ExtendedV2Trace trace{};
|
|
trace.manifest = &manifest;
|
|
TracedSendTo to_client{&client, &trace}, to_server{&server, &trace};
|
|
VectorDeflateCodec encoder{};
|
|
encoder.manifest = &manifest;
|
|
server.begin(0, traced_sim_send, &to_client);
|
|
server.set_transport_deflate_encoder(vector_2k_wire_encode, &encoder);
|
|
client.begin(SIM_TARGET_ID, traced_sim_send, &to_server);
|
|
client.set_transport_deflate_decoder(ota_transport_inflate);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
|
|
server.announce();
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(encoder.calls, manifest.block_count);
|
|
EXPECT_GT(trace.extended_requests, 0u);
|
|
EXPECT_EQ(trace.legacy_requests, 0u);
|
|
EXPECT_TRUE(trace.full_deflated);
|
|
EXPECT_TRUE(trace.tail_deflated);
|
|
EXPECT_FALSE(trace.full_raw);
|
|
EXPECT_FALSE(trace.tail_raw);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_2K_LEN);
|
|
EXPECT_EQ(0, memcmp(store.data(), SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
}
|
|
|
|
static void deliver_manifest_fragment(OtaManager& client, const uint8_t mid[4], uint8_t frag_idx,
|
|
const uint8_t* bytes, uint16_t len) {
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
ManifestMsg msg;
|
|
memcpy(msg.manifest_id, mid, 4);
|
|
msg.frag_idx = frag_idx;
|
|
msg.frag_total = (uint8_t)((MOTA_MFL + OTA_MF_FRAG - 1) / OTA_MF_FRAG);
|
|
msg.bytes = bytes;
|
|
msg.len = len;
|
|
uint16_t wire_len = encode_manifest(wire, sizeof(wire), msg);
|
|
ASSERT_GT(wire_len, 0);
|
|
client.on_message(wire, wire_len);
|
|
}
|
|
|
|
static void deliver_verified_block(OtaManager& client, const MotaManifest& manifest,
|
|
uint32_t block, bool reverse_fragments = false) {
|
|
ASSERT_LT(block, manifest.block_count);
|
|
const uint32_t block_size = manifest.block_size();
|
|
const uint32_t block_off = block * block_size;
|
|
const uint32_t block_len = block_off + block_size <= manifest.payload_size
|
|
? block_size : manifest.payload_size - block_off;
|
|
const uint32_t fragments = (block_len + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
for (uint32_t position = 0; position < fragments; position++) {
|
|
const uint32_t fragment = reverse_fragments ? fragments - position - 1 : position;
|
|
const uint32_t offset = fragment * OTA_FRAG_DATA;
|
|
uint32_t length = block_len - offset;
|
|
if (length > OTA_FRAG_DATA) length = OTA_FRAG_DATA;
|
|
DataMsg data;
|
|
memcpy(data.manifest_id, manifest.merkle_root, 4);
|
|
data.block_idx = (uint16_t)block;
|
|
data.frag_off = (uint16_t)offset;
|
|
data.data = manifest.payload + block_off + offset;
|
|
data.data_len = (uint16_t)length;
|
|
uint16_t wire_len = encode_data(wire, sizeof(wire), data);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
}
|
|
std::vector<uint8_t> scratch(manifest.block_count * 4);
|
|
uint8_t siblings[32 * 4];
|
|
uint8_t sibling_count = merkle_gen_proof(
|
|
manifest.leaves, manifest.block_count, block, scratch.data(), siblings);
|
|
ProofMsg proof;
|
|
memcpy(proof.manifest_id, manifest.merkle_root, 4);
|
|
proof.block_idx = (uint16_t)block;
|
|
proof.n_proof = sibling_count;
|
|
proof.proof = siblings;
|
|
uint16_t wire_len = encode_proof(wire, sizeof(wire), proof);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
}
|
|
|
|
static void deliver_verified_v2_raw_block(OtaManager& client, const MotaManifest& manifest,
|
|
uint32_t block) {
|
|
ASSERT_LT(block, manifest.block_count);
|
|
const uint32_t block_size = manifest.block_size();
|
|
const uint32_t block_off = block * block_size;
|
|
const uint16_t block_len = (uint16_t)(block_off + block_size <= manifest.payload_size
|
|
? block_size : manifest.payload_size - block_off);
|
|
const uint32_t fragments = (block_len + OTA_FRAG_DATA_V2 - 1) / OTA_FRAG_DATA_V2;
|
|
uint8_t stream_id[OTA_DATA_V2_STREAM_ID_BYTES];
|
|
mh4(stream_id, manifest.payload + block_off, block_len);
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
for (uint32_t fragment = 0; fragment < fragments; fragment++) {
|
|
const uint32_t offset = fragment * OTA_FRAG_DATA_V2;
|
|
uint16_t length = (uint16_t)(block_len - offset);
|
|
if (length > OTA_FRAG_DATA_V2) length = OTA_FRAG_DATA_V2;
|
|
uint8_t body[OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2];
|
|
memcpy(body, stream_id, sizeof(stream_id));
|
|
memcpy(body + sizeof(stream_id), manifest.payload + block_off + offset, length);
|
|
DataMsg data{};
|
|
memcpy(data.manifest_id, manifest.merkle_root, 4);
|
|
data.block_idx = (uint16_t)block;
|
|
if (block_size > OTA_DATA_V2_LEGACY_MAX_ENCODED) {
|
|
ASSERT_TRUE(ota_data_v2_pack_extended((uint8_t)fragment, block_len, data.frag_off));
|
|
} else {
|
|
ASSERT_TRUE(ota_data_v2_pack((uint8_t)fragment, block_len, false, data.frag_off));
|
|
}
|
|
data.data = body;
|
|
data.data_len = (uint16_t)(sizeof(stream_id) + length);
|
|
const uint16_t wire_len = encode_data(wire, sizeof(wire), data);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
}
|
|
std::vector<uint8_t> scratch(manifest.block_count * 4);
|
|
uint8_t siblings[32 * 4];
|
|
const uint8_t sibling_count = merkle_gen_proof(
|
|
manifest.leaves, manifest.block_count, block, scratch.data(), siblings);
|
|
ProofMsg proof{};
|
|
memcpy(proof.manifest_id, manifest.merkle_root, 4);
|
|
proof.block_idx = (uint16_t)block;
|
|
proof.n_proof = sibling_count;
|
|
proof.proof = siblings;
|
|
const uint16_t wire_len = encode_proof(wire, sizeof(wire), proof);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
}
|
|
|
|
static void deliver_v2_representation(OtaManager& client, const MotaManifest& manifest,
|
|
uint32_t block, const uint8_t* representation,
|
|
uint16_t representation_len) {
|
|
ASSERT_LT(block, manifest.block_count);
|
|
ASSERT_NE(representation, nullptr);
|
|
const uint32_t block_offset = block * manifest.block_size();
|
|
const uint16_t raw_len = (uint16_t)std::min<uint32_t>(
|
|
manifest.block_size(), manifest.payload_size - block_offset);
|
|
ASSERT_GT(representation_len, 0u);
|
|
ASSERT_LT(representation_len, raw_len);
|
|
|
|
uint8_t stream_id[OTA_DATA_V2_STREAM_ID_BYTES];
|
|
mh4(stream_id, representation, representation_len);
|
|
const uint32_t fragments =
|
|
(representation_len + OTA_FRAG_DATA_V2 - 1) / OTA_FRAG_DATA_V2;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
for (uint32_t fragment = 0; fragment < fragments; fragment++) {
|
|
const uint32_t offset = fragment * OTA_FRAG_DATA_V2;
|
|
const uint16_t length = (uint16_t)std::min<uint32_t>(
|
|
OTA_FRAG_DATA_V2, representation_len - offset);
|
|
uint8_t body[OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2];
|
|
memcpy(body, stream_id, sizeof(stream_id));
|
|
memcpy(body + sizeof(stream_id), representation + offset, length);
|
|
DataMsg data{};
|
|
memcpy(data.manifest_id, manifest.merkle_root, 4);
|
|
data.block_idx = (uint16_t)block;
|
|
if (manifest.block_size() > OTA_DATA_V2_LEGACY_MAX_ENCODED) {
|
|
ASSERT_TRUE(ota_data_v2_pack_extended(
|
|
(uint8_t)fragment, representation_len, data.frag_off));
|
|
} else {
|
|
ASSERT_TRUE(ota_data_v2_pack(
|
|
(uint8_t)fragment, representation_len, true, data.frag_off));
|
|
}
|
|
data.data = body;
|
|
data.data_len = (uint16_t)(sizeof(stream_id) + length);
|
|
const uint16_t wire_len = encode_data(wire, sizeof(wire), data);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
}
|
|
}
|
|
|
|
static void deliver_block_proof(OtaManager& client, const MotaManifest& manifest,
|
|
uint32_t block) {
|
|
ASSERT_LT(block, manifest.block_count);
|
|
std::vector<uint8_t> scratch(manifest.block_count * 4);
|
|
uint8_t siblings[32 * 4];
|
|
const uint8_t sibling_count = merkle_gen_proof(
|
|
manifest.leaves, manifest.block_count, block, scratch.data(), siblings);
|
|
ProofMsg proof{};
|
|
memcpy(proof.manifest_id, manifest.merkle_root, 4);
|
|
proof.block_idx = (uint16_t)block;
|
|
proof.n_proof = sibling_count;
|
|
proof.proof = siblings;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
const uint16_t wire_len = encode_proof(wire, sizeof(wire), proof);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
}
|
|
|
|
TEST(OtaTransfer, ServerPacesOneKilobyteBlockAndProactiveProofWithBackpressure) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 1024u);
|
|
|
|
OtaManager server;
|
|
GatedCapture sent;
|
|
server.begin(0, gated_capture_send, &sent);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
|
|
ReqMsg request;
|
|
memcpy(request.manifest_id, manifest.merkle_root, 4);
|
|
request.block_idx = 0;
|
|
request.want_mask = 0xFFFF;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t wire_len = encode_req(wire, sizeof(wire), request);
|
|
ASSERT_GT(wire_len, 0);
|
|
OtaManager relay;
|
|
relay.begin(0, capture_send, &sent);
|
|
EXPECT_FALSE(relay.on_message(wire, wire_len)); // an intermediate still forwards this request
|
|
EXPECT_TRUE(server.on_message(wire, wire_len));
|
|
EXPECT_TRUE(server.on_message(wire, wire_len)); // identical in-flight retry merges
|
|
EXPECT_EQ(server.pendingServeJobs(), 1u);
|
|
EXPECT_TRUE(sent.items.empty()); // receive handler never allocates a packet burst
|
|
|
|
server.serviceEgress(); // callback applies packet-queue backpressure
|
|
EXPECT_TRUE(sent.items.empty());
|
|
EXPECT_EQ(server.pendingServeJobs(), 1u);
|
|
|
|
sent.accept = true;
|
|
const uint32_t fragment_count = (1024 + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA;
|
|
for (uint32_t i = 0; i < fragment_count; i++) {
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), i + 1);
|
|
DataMsg data;
|
|
ASSERT_TRUE(decode_data(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), data));
|
|
EXPECT_EQ(data.block_idx, 0);
|
|
EXPECT_EQ(data.frag_off, i * OTA_FRAG_DATA);
|
|
}
|
|
EXPECT_EQ(server.pendingServeJobs(), 1u); // proof is retained behind DATA
|
|
server.serviceEgress(); // reserve RX turnaround for legacy REQ_PROOF
|
|
ASSERT_EQ(sent.items.size(), fragment_count);
|
|
server.set_clock(OTA_MANIFEST_EGRESS_MIN_GAP_MS);
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), fragment_count + 1);
|
|
ProofMsg proof;
|
|
ASSERT_TRUE(decode_proof(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), proof));
|
|
EXPECT_EQ(proof.block_idx, 0);
|
|
EXPECT_EQ(server.pendingServeJobs(), 0u);
|
|
}
|
|
|
|
TEST(OtaTransfer, LiteralLegacyFullMaskServesTwoKilobyteBlockInThirteenFragments) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_2K, SIM_MOTA_2K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 2048u);
|
|
|
|
OtaManager server;
|
|
CapturedMessages sent;
|
|
server.begin(0, capture_send, &sent);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
|
|
ReqMsg request{};
|
|
memcpy(request.manifest_id, manifest.merkle_root, 4);
|
|
request.block_idx = 0;
|
|
request.want_mask = 0xFFFFu; // deployed legacy "all fragments"
|
|
EXPECT_FALSE(ota_req_is_v2(request.want_mask));
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
const uint16_t request_len = encode_req(wire, sizeof(wire), request);
|
|
ASSERT_GT(request_len, 0);
|
|
ASSERT_TRUE(server.on_message(wire, request_len));
|
|
|
|
const uint32_t fragment_count =
|
|
(manifest.block_size() + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA;
|
|
ASSERT_EQ(fragment_count, 13u);
|
|
for (uint32_t fragment = 0; fragment < fragment_count; fragment++) {
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), fragment + 1);
|
|
DataMsg data{};
|
|
ASSERT_TRUE(decode_data(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), data));
|
|
EXPECT_EQ(data.block_idx, 0u);
|
|
EXPECT_EQ(data.frag_off, fragment * OTA_FRAG_DATA);
|
|
const uint16_t expected_len = (uint16_t)std::min<uint32_t>(
|
|
OTA_FRAG_DATA, manifest.block_size() - fragment * OTA_FRAG_DATA);
|
|
EXPECT_EQ(data.data_len, expected_len);
|
|
EXPECT_EQ(0, memcmp(data.data,
|
|
manifest.payload + fragment * OTA_FRAG_DATA,
|
|
expected_len));
|
|
}
|
|
EXPECT_EQ(sent.items.size(), 13u); // no v2 12-fragment interpretation
|
|
EXPECT_EQ(server.pendingServeJobs(), 1u);
|
|
|
|
server.serviceEgress(); // establish proof turnaround deadline
|
|
ASSERT_EQ(sent.items.size(), 13u);
|
|
server.set_clock(OTA_MANIFEST_EGRESS_MIN_GAP_MS);
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), 14u);
|
|
ProofMsg proof{};
|
|
ASSERT_TRUE(decode_proof(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), proof));
|
|
EXPECT_EQ(proof.block_idx, 0u);
|
|
EXPECT_EQ(server.pendingServeJobs(), 0u);
|
|
}
|
|
|
|
TEST(OtaTransfer, NegotiatedV2Carries171RawBytesAndRepeatsRepresentationId) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 1024u);
|
|
|
|
OtaManager server;
|
|
CapturedMessages sent;
|
|
server.begin(0, capture_send, &sent);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
|
|
ReqMsg request;
|
|
memcpy(request.manifest_id, manifest.merkle_root, 4);
|
|
request.block_idx = 0;
|
|
request.want_mask = ota_req_make_v2(0x007Fu, true); // no encoder installed: source must fall back to raw
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
const uint16_t request_len = encode_req(wire, sizeof(wire), request);
|
|
ASSERT_GT(request_len, 0);
|
|
ASSERT_TRUE(server.on_message(wire, request_len));
|
|
|
|
const uint32_t fragment_count =
|
|
(manifest.block_size() + OTA_FRAG_DATA_V2 - 1) / OTA_FRAG_DATA_V2;
|
|
ASSERT_EQ(fragment_count, 6u);
|
|
std::array<uint8_t, 1024> reassembled{};
|
|
uint8_t expected_stream_id[4];
|
|
mh4(expected_stream_id, manifest.payload, manifest.block_size());
|
|
|
|
for (uint32_t i = 0; i < fragment_count; i++) {
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), i + 1);
|
|
DataMsg data{};
|
|
ASSERT_TRUE(decode_data(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), data));
|
|
uint8_t fragment = 0;
|
|
uint16_t encoded_len = 0;
|
|
bool deflated = true;
|
|
ASSERT_TRUE(ota_data_v2_unpack(data.frag_off, fragment, encoded_len, deflated));
|
|
EXPECT_EQ(fragment, i);
|
|
EXPECT_EQ(encoded_len, manifest.block_size());
|
|
EXPECT_FALSE(deflated);
|
|
ASSERT_GE(data.data_len, OTA_DATA_V2_STREAM_ID_BYTES);
|
|
EXPECT_EQ(0, memcmp(data.data, expected_stream_id, sizeof(expected_stream_id)));
|
|
const uint16_t slice_len =
|
|
(uint16_t)(data.data_len - OTA_DATA_V2_STREAM_ID_BYTES);
|
|
const uint32_t offset = i * OTA_FRAG_DATA_V2;
|
|
EXPECT_EQ(slice_len, (uint16_t)std::min<uint32_t>(OTA_FRAG_DATA_V2,
|
|
manifest.block_size() - offset));
|
|
if (i + 1 < fragment_count) EXPECT_EQ(sent.items.back().size(), MAX_PACKET_PAYLOAD);
|
|
memcpy(reassembled.data() + offset, data.data + OTA_DATA_V2_STREAM_ID_BYTES, slice_len);
|
|
}
|
|
EXPECT_EQ(0, memcmp(reassembled.data(), manifest.payload, manifest.block_size()));
|
|
}
|
|
|
|
TEST(OtaTransfer, LegacyProofRequestBypassesProactiveProofTurnaround) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
OtaManager server;
|
|
CapturedMessages sent;
|
|
server.begin(0, capture_send, &sent);
|
|
server.set_link_timing(80, 2000); // 3 admitted packet intervals = 480 ms
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
|
|
ReqMsg request;
|
|
memcpy(request.manifest_id, manifest.merkle_root, 4);
|
|
request.block_idx = 0;
|
|
request.want_mask = 0xFFFF;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t wire_len = encode_req(wire, sizeof(wire), request);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(server.on_message(wire, wire_len));
|
|
|
|
const uint32_t fragment_count =
|
|
(manifest.block_size() + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA;
|
|
for (uint32_t i = 0; i < fragment_count; i++) server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), fragment_count);
|
|
|
|
server.set_clock(479);
|
|
server.serviceEgress(); // no unsolicited proof before turnaround
|
|
ASSERT_EQ(sent.items.size(), fragment_count);
|
|
|
|
ReqProofMsg proof_request;
|
|
memcpy(proof_request.manifest_id, manifest.merkle_root, 4);
|
|
proof_request.block_idx = 0;
|
|
wire_len = encode_req_proof(wire, sizeof(wire), proof_request);
|
|
ASSERT_GT(wire_len, 0);
|
|
ASSERT_TRUE(server.on_message(wire, wire_len));
|
|
server.serviceEgress(); // explicit legacy request is served immediately
|
|
ASSERT_EQ(sent.items.size(), fragment_count + 1);
|
|
ProofMsg proof;
|
|
ASSERT_TRUE(decode_proof(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), proof));
|
|
EXPECT_EQ(proof.block_idx, 0);
|
|
}
|
|
|
|
TEST(OtaTransfer, ServerPacesManifestFragmentsAndConsumesResolvedRequest) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
|
|
OtaManager server;
|
|
CapturedMessages sent;
|
|
server.begin(0, capture_send, &sent);
|
|
server.set_link_timing(80, 2000); // 160 ms at the active airtime/duty spacing
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
|
|
GetManifestMsg request;
|
|
memcpy(request.manifest_id, manifest.merkle_root, 4);
|
|
request.want_mask = 0xFFFF;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t wire_len = encode_get_manifest(wire, sizeof(wire), request);
|
|
ASSERT_GT(wire_len, 0);
|
|
|
|
EXPECT_TRUE(server.on_message(wire, wire_len));
|
|
EXPECT_TRUE(sent.items.empty());
|
|
EXPECT_EQ(server.pendingManifestJobs(), 1u);
|
|
|
|
const uint32_t manifest_gap = 160;
|
|
server.set_clock(manifest_gap - 1);
|
|
server.serviceEgress();
|
|
EXPECT_TRUE(sent.items.empty());
|
|
|
|
server.set_clock(manifest_gap);
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ManifestMsg fragment;
|
|
ASSERT_TRUE(decode_manifest(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), fragment));
|
|
EXPECT_EQ(fragment.frag_idx, 0u);
|
|
EXPECT_EQ(fragment.frag_total, 2u);
|
|
|
|
server.set_clock(2 * manifest_gap - 1);
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
|
|
server.set_clock(2 * manifest_gap);
|
|
server.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ASSERT_TRUE(decode_manifest(sent.items[1].data(),
|
|
(uint16_t)sent.items[1].size(), fragment));
|
|
EXPECT_EQ(fragment.frag_idx, 1u);
|
|
EXPECT_EQ(server.pendingManifestJobs(), 0u);
|
|
|
|
sent.items.clear();
|
|
request.manifest_id[0] ^= 0xFF;
|
|
wire_len = encode_get_manifest(wire, sizeof(wire), request);
|
|
ASSERT_GT(wire_len, 0);
|
|
EXPECT_FALSE(server.on_message(wire, wire_len));
|
|
EXPECT_TRUE(sent.items.empty());
|
|
}
|
|
|
|
TEST(OtaTransfer, ServerQueuesEveryBlockInOneRequestFlight) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_count, 3u);
|
|
|
|
OtaManager server;
|
|
CapturedMessages sent;
|
|
server.begin(0, capture_send, &sent);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
|
|
ReqWindowMsg request{};
|
|
memcpy(request.manifest_id, manifest.merkle_root, 4);
|
|
request.n_items = 3;
|
|
for (uint8_t i = 0; i < request.n_items; i++) {
|
|
request.items[i].block_idx = i;
|
|
request.items[i].want_mask = 0xFFFF;
|
|
}
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t wire_len = encode_req_window(wire, sizeof(wire), request);
|
|
ASSERT_GT(wire_len, 0);
|
|
server.on_message(wire, wire_len);
|
|
EXPECT_EQ(server.pendingServeJobs(), 3u);
|
|
|
|
// The bounded sender remains paced and drains the jobs in request order, including each proactive proof
|
|
// after the legacy-receiver turnaround gap.
|
|
const uint32_t data_packets_per_full_block =
|
|
(manifest.block_size() + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA;
|
|
for (uint32_t i = 0; i < data_packets_per_full_block; i++) server.serviceEgress();
|
|
server.set_clock(OTA_MANIFEST_EGRESS_MIN_GAP_MS);
|
|
server.serviceEgress();
|
|
const uint32_t packets_per_full_block = data_packets_per_full_block + 1;
|
|
ASSERT_EQ(sent.items.size(), packets_per_full_block);
|
|
ProofMsg proof;
|
|
ASSERT_TRUE(decode_proof(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), proof));
|
|
EXPECT_EQ(proof.block_idx, 0);
|
|
EXPECT_EQ(server.pendingServeJobs(), 2u);
|
|
}
|
|
|
|
TEST(OtaTransfer, MissingProactiveProofFallsBackAfterGrace) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
client.set_clock(100);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(sent.items.size(), 1u); // first one-block flight opens immediately
|
|
|
|
const uint32_t block_len = manifest.block_size();
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
for (uint32_t offset = 0; offset < block_len; offset += OTA_FRAG_DATA) {
|
|
uint32_t length = block_len - offset;
|
|
if (length > OTA_FRAG_DATA) length = OTA_FRAG_DATA;
|
|
DataMsg data;
|
|
memcpy(data.manifest_id, manifest.merkle_root, 4);
|
|
data.block_idx = 0;
|
|
data.frag_off = (uint16_t)offset;
|
|
data.data = manifest.payload + offset;
|
|
data.data_len = (uint16_t)length;
|
|
uint16_t length_on_wire = encode_data(wire, sizeof(wire), data);
|
|
ASSERT_GT(length_on_wire, 0);
|
|
client.on_message(wire, length_on_wire);
|
|
}
|
|
EXPECT_EQ(sent.items.size(), 1u);
|
|
|
|
client.set_clock(100 + OTA_PROOF_GRACE_MS - 1);
|
|
client.serviceEgress();
|
|
EXPECT_EQ(sent.items.size(), 1u);
|
|
client.set_clock(100 + OTA_PROOF_GRACE_MS);
|
|
client.serviceEgress();
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ReqProofMsg fallback;
|
|
ASSERT_TRUE(decode_req_proof(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), fallback));
|
|
EXPECT_EQ(fallback.block_idx, 0);
|
|
}
|
|
|
|
TEST(OtaTransfer, ClientUsesQuietBatchedFlightsAndAcceptsOutOfOrderBlocks) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_count, 3u);
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear(); // discard the initial GET_MANIFEST
|
|
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(client.fetchState(), OtaManager::FETCHING);
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqWindowMsg first{};
|
|
ASSERT_TRUE(decode_req_window(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), first));
|
|
ASSERT_EQ(first.n_items, 1u);
|
|
EXPECT_EQ(first.items[0].block_idx, 0);
|
|
EXPECT_EQ(client.fetchPipelineWidth(), 1u);
|
|
|
|
// A clean one-block probe grows the next flight to two blocks, both named in ONE request packet.
|
|
deliver_verified_block(client, manifest, 0);
|
|
ASSERT_EQ(client.blocksHave(), 1u);
|
|
ASSERT_EQ(client.fetchPipelineWidth(), 2u);
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ReqWindowMsg second{};
|
|
ASSERT_TRUE(decode_req_window(sent.items[1].data(),
|
|
(uint16_t)sent.items[1].size(), second));
|
|
ASSERT_EQ(second.n_items, 2u);
|
|
EXPECT_EQ(second.items[0].block_idx, 1);
|
|
EXPECT_EQ(second.items[1].block_idx, 2);
|
|
|
|
// Complete block 2 first, with reversed fragments. Its slot verifies independently, but the client does
|
|
// not refill or transmit anything while block 1's response is still expected from the same flight.
|
|
deliver_verified_block(client, manifest, 2, true);
|
|
EXPECT_EQ(client.blocksHave(), 2u);
|
|
EXPECT_EQ(sent.items.size(), 2u);
|
|
|
|
deliver_verified_block(client, manifest, 1);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksHave(), 3u);
|
|
EXPECT_EQ(sent.items.size(), 2u); // no continuous-refill request was emitted
|
|
}
|
|
|
|
TEST(OtaTransfer, NewClientRequestsV2ThenRetriesLegacyWhenNoV2DataArrives) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg first{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(), (uint16_t)sent.items[0].size(), first));
|
|
EXPECT_TRUE(ota_req_is_v2(first.want_mask));
|
|
EXPECT_EQ(ota_req_v2_fragments(first.want_mask), 0x007Fu); // remains a complete old-source request
|
|
EXPECT_EQ(first.want_mask & OTA_REQ_V2_ALLOW_DEFLATE, 0u); // no decoder was linked/configured
|
|
|
|
const uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
client.set_clock(100 + timeout);
|
|
client.loop();
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ReqMsg fallback{};
|
|
ASSERT_TRUE(decode_req(sent.items[1].data(), (uint16_t)sent.items[1].size(), fallback));
|
|
EXPECT_FALSE(ota_req_is_v2(fallback.want_mask));
|
|
EXPECT_EQ(fallback.want_mask, 0x007Fu);
|
|
}
|
|
|
|
TEST(OtaTransfer, TwoKilobyteClientAcceptsCanonicalLegacyFallbackThroughBitTwelve) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_2K, SIM_MOTA_2K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 2048u);
|
|
ASSERT_EQ(manifest.block_count, 2u);
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg extended{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), extended));
|
|
EXPECT_TRUE(ota_req_is_v2(extended.want_mask));
|
|
EXPECT_TRUE(ota_req_v2_extended_length(extended.want_mask));
|
|
EXPECT_EQ(ota_req_v2_fragments(extended.want_mask), 0x0FFFu);
|
|
|
|
// The first canonical legacy slice selects the fallback geometry. Completing the 2 KiB block then
|
|
// necessarily receives fragment bit 12 (offset 1920) before its proof can authenticate and commit it.
|
|
ASSERT_EQ((manifest.block_size() + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA, 13u);
|
|
deliver_verified_block(client, manifest, 0);
|
|
ASSERT_EQ(client.blocksHave(), 1u);
|
|
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ReqMsg next{};
|
|
ASSERT_TRUE(decode_req(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), next));
|
|
EXPECT_EQ(next.block_idx, 1u);
|
|
EXPECT_FALSE(ota_req_is_v2(next.want_mask));
|
|
EXPECT_EQ(next.want_mask, 0x0003u); // short tail remains legacy after downgrade
|
|
|
|
deliver_verified_block(client, manifest, 1);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksHave(), client.blocksTotal());
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_2K_LEN);
|
|
EXPECT_EQ(0, memcmp(store.data(), SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
}
|
|
|
|
TEST(OtaTransfer, MalformedLegacyDataCannotForceV2SessionDowngrade) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
DataMsg malformed;
|
|
memcpy(malformed.manifest_id, manifest.merkle_root, 4);
|
|
malformed.block_idx = 0;
|
|
malformed.frag_off = 1; // not canonical 160-byte legacy alignment
|
|
malformed.data = manifest.payload;
|
|
malformed.data_len = OTA_FRAG_DATA;
|
|
uint16_t wire_len = encode_data(wire, sizeof(wire), malformed);
|
|
ASSERT_GT(wire_len, 0);
|
|
EXPECT_FALSE(client.on_message(wire, wire_len));
|
|
|
|
uint8_t body[OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2];
|
|
mh4(body, manifest.payload, manifest.block_size());
|
|
memcpy(body + OTA_DATA_V2_STREAM_ID_BYTES, manifest.payload, OTA_FRAG_DATA_V2);
|
|
DataMsg valid;
|
|
memcpy(valid.manifest_id, manifest.merkle_root, 4);
|
|
valid.block_idx = 0;
|
|
ASSERT_TRUE(ota_data_v2_pack(0, (uint16_t)manifest.block_size(), false, valid.frag_off));
|
|
valid.data = body;
|
|
valid.data_len = sizeof(body);
|
|
wire_len = encode_data(wire, sizeof(wire), valid);
|
|
ASSERT_EQ(wire_len, MAX_PACKET_PAYLOAD);
|
|
EXPECT_TRUE(client.on_message(wire, wire_len)); // malformed legacy packet left v2 negotiation intact
|
|
}
|
|
|
|
TEST(OtaTransfer, PartialV2SeederCanDisappearAndLegacySeederTakesOver) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_transport_deflate_decoder(ota_transport_inflate);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
|
|
uint8_t body[OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2];
|
|
mh4(body, manifest.payload, manifest.block_size());
|
|
memcpy(body + OTA_DATA_V2_STREAM_ID_BYTES, manifest.payload, OTA_FRAG_DATA_V2);
|
|
DataMsg partial{};
|
|
memcpy(partial.manifest_id, manifest.merkle_root, 4);
|
|
partial.block_idx = 0;
|
|
ASSERT_TRUE(ota_data_v2_pack(0, (uint16_t)manifest.block_size(), false, partial.frag_off));
|
|
partial.data = body;
|
|
partial.data_len = sizeof(body);
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t wire_len = encode_data(wire, sizeof(wire), partial);
|
|
ASSERT_EQ(wire_len, MAX_PACKET_PAYLOAD);
|
|
ASSERT_TRUE(client.on_message(wire, wire_len));
|
|
|
|
sent.items.clear();
|
|
uint32_t now = 100;
|
|
uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
now += timeout;
|
|
client.set_clock(now);
|
|
client.loop(); // one sparse v2 retry
|
|
ASSERT_FALSE(sent.items.empty());
|
|
ReqMsg retry{};
|
|
ASSERT_TRUE(decode_req(sent.items.back().data(), (uint16_t)sent.items.back().size(), retry));
|
|
EXPECT_TRUE(ota_req_is_v2(retry.want_mask));
|
|
|
|
sent.items.clear();
|
|
timeout = client.fetchRetryTimeoutMs();
|
|
now += timeout;
|
|
client.set_clock(now);
|
|
client.loop(); // bounded whole-session legacy fallback
|
|
ASSERT_FALSE(sent.items.empty());
|
|
ReqMsg fallback{};
|
|
ASSERT_TRUE(decode_req(sent.items.back().data(), (uint16_t)sent.items.back().size(), fallback));
|
|
EXPECT_FALSE(ota_req_is_v2(fallback.want_mask));
|
|
EXPECT_EQ(fallback.want_mask, 0x007Fu);
|
|
|
|
deliver_verified_block(client, manifest, 0); // an old seeder can now make forward progress
|
|
EXPECT_EQ(client.blocksHave(), 1u);
|
|
}
|
|
|
|
TEST(OtaTransfer, FailedTwoKilobyteInflateRetriesRawWithoutLeavingExtendedV2) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_2K, SIM_MOTA_2K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 2048u);
|
|
ASSERT_EQ(manifest.block_count, 2u);
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
RejectWireDecode decoder{};
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_transport_deflate_decoder(reject_wire_decode, &decoder);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg first{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), first));
|
|
EXPECT_TRUE(ota_req_is_v2(first.want_mask));
|
|
EXPECT_TRUE(ota_req_v2_extended_length(first.want_mask));
|
|
EXPECT_NE(first.want_mask & OTA_REQ_V2_ALLOW_DEFLATE, 0u);
|
|
|
|
sent.items.clear();
|
|
deliver_v2_representation(
|
|
client, manifest, 0,
|
|
SIM_MOTA_2K_DEFLATED + SIM_MOTA_2K_DEFLATED_OFFSETS[0],
|
|
SIM_MOTA_2K_DEFLATED_LENGTHS[0]);
|
|
EXPECT_EQ(decoder.calls, 1u);
|
|
EXPECT_EQ(client.blocksHave(), 0u);
|
|
EXPECT_TRUE(sent.items.empty()); // retry is paced by the normal deadline
|
|
|
|
const uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
client.set_clock(100 + timeout);
|
|
client.loop();
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg retry{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), retry));
|
|
EXPECT_EQ(retry.block_idx, 0u);
|
|
EXPECT_TRUE(ota_req_is_v2(retry.want_mask));
|
|
EXPECT_TRUE(ota_req_v2_extended_length(retry.want_mask));
|
|
EXPECT_EQ(retry.want_mask & OTA_REQ_V2_ALLOW_DEFLATE, 0u);
|
|
EXPECT_EQ(ota_req_v2_fragments(retry.want_mask), 0x0FFFu);
|
|
|
|
sent.items.clear();
|
|
deliver_verified_v2_raw_block(client, manifest, 0);
|
|
ASSERT_EQ(client.blocksHave(), 1u);
|
|
EXPECT_EQ(decoder.calls, 1u); // raw v2 bypasses the failed decoder
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg next{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), next));
|
|
EXPECT_EQ(next.block_idx, 1u);
|
|
EXPECT_TRUE(ota_req_is_v2(next.want_mask));
|
|
EXPECT_TRUE(ota_req_v2_extended_length(next.want_mask));
|
|
EXPECT_EQ(next.want_mask & OTA_REQ_V2_ALLOW_DEFLATE, 0u);
|
|
|
|
deliver_verified_v2_raw_block(client, manifest, 1);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksHave(), client.blocksTotal());
|
|
EXPECT_EQ(decoder.calls, 1u);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_2K_LEN);
|
|
EXPECT_EQ(0, memcmp(store.data(), SIM_MOTA_2K, SIM_MOTA_2K_LEN));
|
|
}
|
|
|
|
TEST(OtaTransfer, ConfirmedV2SeederCanDisappearBetweenBlocksAndLegacySeederTakesOver) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_GE(manifest.block_count, 2u);
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
|
|
deliver_verified_v2_raw_block(client, manifest, 0);
|
|
ASSERT_EQ(client.blocksHave(), 1u);
|
|
sent.items.clear(); // v2 source vanishes before the next flight answers
|
|
|
|
uint32_t now = 100;
|
|
uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
now += timeout;
|
|
client.set_clock(now);
|
|
client.loop(); // one sparse v2 retry
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqWindowMsg retry{};
|
|
ASSERT_TRUE(decode_req_window(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), retry));
|
|
ASSERT_EQ(retry.n_items, 1u);
|
|
EXPECT_TRUE(ota_req_is_v2(retry.items[0].want_mask));
|
|
|
|
sent.items.clear();
|
|
timeout = client.fetchRetryTimeoutMs();
|
|
now += timeout;
|
|
client.set_clock(now);
|
|
client.loop(); // whole empty flight falls back to legacy
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqWindowMsg fallback{};
|
|
ASSERT_TRUE(decode_req_window(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), fallback));
|
|
ASSERT_GE(fallback.n_items, 1u);
|
|
for (uint8_t i = 0; i < fallback.n_items; i++) {
|
|
EXPECT_FALSE(ota_req_is_v2(fallback.items[i].want_mask));
|
|
}
|
|
|
|
deliver_verified_block(client, manifest, 1); // deployed source resumes the same session
|
|
EXPECT_EQ(client.blocksHave(), 2u);
|
|
}
|
|
|
|
TEST(OtaTransfer, V2NeverMixesDifferentCompressedRepresentationsAcrossSeeders) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_transport_deflate_decoder(test_representation_decode, &manifest);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg request{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(), (uint16_t)sent.items[0].size(), request));
|
|
EXPECT_NE(request.want_mask & OTA_REQ_V2_ALLOW_DEFLATE, 0u);
|
|
|
|
std::array<uint8_t, 300> representation_a{};
|
|
std::array<uint8_t, 300> representation_b{};
|
|
representation_a.fill(0xA1);
|
|
representation_b.fill(0xB2);
|
|
uint8_t id_a[4], id_b[4];
|
|
mh4(id_a, representation_a.data(), representation_a.size());
|
|
mh4(id_b, representation_b.data(), representation_b.size());
|
|
ASSERT_NE(0, memcmp(id_a, id_b, sizeof(id_a)));
|
|
|
|
auto deliver_fragment = [&](const std::array<uint8_t, 300>& representation,
|
|
const uint8_t id[4], uint8_t fragment) {
|
|
const uint32_t offset = (uint32_t)fragment * OTA_FRAG_DATA_V2;
|
|
const uint16_t length = (uint16_t)std::min<uint32_t>(OTA_FRAG_DATA_V2,
|
|
representation.size() - offset);
|
|
std::array<uint8_t, OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2> body{};
|
|
memcpy(body.data(), id, OTA_DATA_V2_STREAM_ID_BYTES);
|
|
memcpy(body.data() + OTA_DATA_V2_STREAM_ID_BYTES, representation.data() + offset, length);
|
|
uint16_t descriptor = 0;
|
|
if (!ota_data_v2_pack(fragment, (uint16_t)representation.size(), true, descriptor)) return false;
|
|
DataMsg data;
|
|
memcpy(data.manifest_id, manifest.merkle_root, 4);
|
|
data.block_idx = 0;
|
|
data.frag_off = descriptor;
|
|
data.data = body.data();
|
|
data.data_len = (uint16_t)(OTA_DATA_V2_STREAM_ID_BYTES + length);
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
const uint16_t wire_len = encode_data(wire, sizeof(wire), data);
|
|
return wire_len != 0 && client.on_message(wire, wire_len);
|
|
};
|
|
|
|
ASSERT_TRUE(deliver_fragment(representation_a, id_a, 0));
|
|
EXPECT_FALSE(deliver_fragment(representation_b, id_b, 1)); // valid stream, wrong representation id
|
|
ASSERT_TRUE(deliver_fragment(representation_a, id_a, 1));
|
|
|
|
std::vector<uint8_t> scratch(manifest.block_count * 4);
|
|
uint8_t siblings[32 * 4];
|
|
const uint8_t sibling_count = merkle_gen_proof(
|
|
manifest.leaves, manifest.block_count, 0, scratch.data(), siblings);
|
|
ProofMsg proof;
|
|
memcpy(proof.manifest_id, manifest.merkle_root, 4);
|
|
proof.block_idx = 0;
|
|
proof.n_proof = sibling_count;
|
|
proof.proof = siblings;
|
|
uint8_t proof_wire[MAX_PACKET_PAYLOAD];
|
|
const uint16_t proof_len = encode_proof(proof_wire, sizeof(proof_wire), proof);
|
|
ASSERT_GT(proof_len, 0);
|
|
ASSERT_TRUE(client.on_message(proof_wire, proof_len));
|
|
EXPECT_EQ(client.blocksHave(), 1u);
|
|
const uint32_t payload_offset = (uint32_t)(manifest.payload - SIM_MOTA_1K);
|
|
EXPECT_EQ(0, memcmp(store.data() + payload_offset, manifest.payload, manifest.block_size()));
|
|
}
|
|
|
|
TEST(OtaTransfer, ExtendedCompressedRepresentationReassemblesThroughFragmentEight) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_2K, SIM_MOTA_2K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_size(), 2048u);
|
|
|
|
std::vector<uint8_t> representation(1500);
|
|
for (uint32_t i = 0; i < representation.size(); i++) {
|
|
representation[i] = (uint8_t)(i * 29u + 7u);
|
|
}
|
|
ASSERT_GT(representation.size(), 1024u);
|
|
ASSERT_LT(representation.size(), manifest.block_size());
|
|
ASSERT_EQ((representation.size() + OTA_FRAG_DATA_V2 - 1) / OTA_FRAG_DATA_V2, 9u);
|
|
|
|
SyntheticRepresentationDecode decoder{};
|
|
decoder.representation = representation.data();
|
|
decoder.representation_len = (uint16_t)representation.size();
|
|
decoder.decoded = manifest.payload;
|
|
decoder.decoded_len = (uint16_t)manifest.block_size();
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_transport_deflate_decoder(synthetic_representation_decode, &decoder);
|
|
client.set_fetch_store(&store);
|
|
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
ReqMsg request{};
|
|
ASSERT_TRUE(decode_req(sent.items[0].data(),
|
|
(uint16_t)sent.items[0].size(), request));
|
|
EXPECT_TRUE(ota_req_v2_extended_length(request.want_mask));
|
|
EXPECT_NE(request.want_mask & OTA_REQ_V2_ALLOW_DEFLATE, 0u);
|
|
|
|
sent.items.clear();
|
|
deliver_v2_representation(client, manifest, 0,
|
|
representation.data(), (uint16_t)representation.size());
|
|
EXPECT_EQ(decoder.calls, 1u); // fragment 8 completed reassembly + decode
|
|
EXPECT_EQ(client.blocksHave(), 0u); // proof still gates the decoded bytes
|
|
EXPECT_TRUE(sent.items.empty());
|
|
|
|
deliver_block_proof(client, manifest, 0);
|
|
EXPECT_EQ(client.blocksHave(), 1u);
|
|
const uint32_t payload_offset = (uint32_t)(manifest.payload - SIM_MOTA_2K);
|
|
EXPECT_EQ(0, memcmp(store.data() + payload_offset,
|
|
manifest.payload, manifest.block_size()));
|
|
}
|
|
|
|
TEST(OtaTransfer, NewClientFallsBackWhenLegacySourceServesOnlyFirstWindowRow) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
ASSERT_EQ(manifest.block_count, 3u);
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
deliver_verified_block(client, manifest, 0);
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
|
|
// Legacy decode sees and serves only row zero (block 1) from the new two-row request.
|
|
ReqMsg legacy{};
|
|
ASSERT_TRUE(decode_req(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), legacy));
|
|
ASSERT_EQ(legacy.block_idx, 1);
|
|
deliver_verified_block(client, manifest, legacy.block_idx);
|
|
ASSERT_EQ(client.blocksHave(), 2u);
|
|
ASSERT_EQ(sent.items.size(), 2u); // block 2 remains reserved; no immediate refill
|
|
|
|
const uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
client.set_clock(100 + timeout);
|
|
client.loop(); // tail row was not served: recover it conventionally
|
|
ASSERT_EQ(sent.items.size(), 3u);
|
|
EXPECT_EQ(sent.items.back().size(), 9u); // exact legacy single-row OTA_REQ
|
|
ReqMsg fallback{};
|
|
ASSERT_TRUE(decode_req(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), fallback));
|
|
EXPECT_EQ(fallback.block_idx, 2);
|
|
|
|
deliver_verified_block(client, manifest, 2);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksHave(), 3u);
|
|
}
|
|
|
|
#if OTA_FETCH_PIPELINE >= 3
|
|
TEST(OtaTransfer, RequestFlightGrowsOnCleanFlightsAndHalvesAfterRecovery) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
ASSERT_GE(manifest.block_count, 7u);
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
ASSERT_EQ(client.fetchPipelineCapacity(), 4u);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(client.fetchPipelineWidth(), 1u);
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
|
|
deliver_verified_block(client, manifest, 0); // clean width-1 flight -> width 2
|
|
ASSERT_EQ(client.fetchPipelineWidth(), 2u);
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ReqWindowMsg width2{};
|
|
ASSERT_TRUE(decode_req_window(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), width2));
|
|
ASSERT_EQ(width2.n_items, 2u);
|
|
|
|
deliver_verified_block(client, manifest, 1);
|
|
EXPECT_EQ(client.fetchPipelineWidth(), 2u); // still waiting for the same flight's block 2
|
|
EXPECT_EQ(sent.items.size(), 2u); // receiver remains silent
|
|
deliver_verified_block(client, manifest, 2); // clean width-2 flight -> width 3
|
|
EXPECT_EQ(client.fetchPipelineWidth(), 3u);
|
|
ASSERT_EQ(sent.items.size(), 3u);
|
|
ReqWindowMsg width3{};
|
|
ASSERT_TRUE(decode_req_window(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), width3));
|
|
ASSERT_EQ(width3.n_items, 3u);
|
|
EXPECT_EQ(width3.items[0].block_idx, 3);
|
|
|
|
// Ordinary one-second maintenance ticks do nothing until the flight's calculated service time expires.
|
|
const uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
client.set_clock(100 + timeout - 1);
|
|
client.loop();
|
|
EXPECT_EQ(sent.items.size(), 3u);
|
|
EXPECT_EQ(client.fetchPipelineWidth(), 3u);
|
|
|
|
client.set_clock(100 + timeout);
|
|
client.loop(); // recover one slot with a legacy single-row REQ
|
|
ASSERT_EQ(sent.items.size(), 4u);
|
|
ReqMsg recovery{};
|
|
ASSERT_TRUE(decode_req(sent.items.back().data(),
|
|
(uint16_t)sent.items.back().size(), recovery));
|
|
EXPECT_EQ(recovery.block_idx, 3);
|
|
EXPECT_EQ(client.fetchPipelineWidth(), 3u); // resize only after this flight drains
|
|
|
|
deliver_verified_block(client, manifest, 3);
|
|
deliver_verified_block(client, manifest, 4);
|
|
deliver_verified_block(client, manifest, 5);
|
|
EXPECT_EQ(client.fetchPipelineWidth(), 2u);
|
|
}
|
|
#endif
|
|
|
|
TEST(OtaTransfer, FlightRetriesOnlyMissingFragmentsAfterItsDeadline) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
ASSERT_EQ(sent.items.size(), 1u); // conservative one-block probe flight
|
|
|
|
DataMsg first_fragment;
|
|
memcpy(first_fragment.manifest_id, manifest.merkle_root, 4);
|
|
first_fragment.block_idx = 0;
|
|
first_fragment.frag_off = 0;
|
|
first_fragment.data = manifest.payload;
|
|
first_fragment.data_len = OTA_FRAG_DATA;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t wire_len = encode_data(wire, sizeof(wire), first_fragment);
|
|
ASSERT_GT(wire_len, 0);
|
|
client.on_message(wire, wire_len);
|
|
|
|
const uint32_t timeout = client.fetchRetryTimeoutMs();
|
|
client.set_clock(100 + timeout - 1);
|
|
client.loop(); // no premature fixed-tick retry
|
|
EXPECT_EQ(sent.items.size(), 1u);
|
|
client.set_clock(100 + timeout);
|
|
client.loop(); // deadline: retry only block 0's holes
|
|
ASSERT_EQ(sent.items.size(), 2u);
|
|
ReqMsg retry0;
|
|
ASSERT_TRUE(decode_req(sent.items[1].data(), (uint16_t)sent.items[1].size(), retry0));
|
|
EXPECT_EQ(retry0.block_idx, 0);
|
|
EXPECT_EQ(retry0.want_mask, (uint16_t)(0x007F & ~0x0001));
|
|
|
|
client.set_clock(100 + timeout * 2 - 1);
|
|
client.loop(); // the recovery request restarted the deadline
|
|
EXPECT_EQ(sent.items.size(), 2u);
|
|
}
|
|
|
|
TEST(OtaTransfer, RetryTimingAdaptsToRadioPathAndAirtimeBudget) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_clock(100);
|
|
client.set_max_hops(0);
|
|
client.set_link_timing(100, 2000); // direct response fits the conservative quiet floor
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
sent.items.clear();
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
const uint32_t fast_direct = client.fetchRetryTimeoutMs();
|
|
EXPECT_GE(fast_direct, (uint32_t)OTA_FETCH_RETRY_MIN_MS);
|
|
|
|
client.set_max_hops(2); // no observed reply path: budget source + 2 relays
|
|
const uint32_t fast_relayed = client.fetchRetryTimeoutMs();
|
|
EXPECT_GT(fast_relayed, fast_direct);
|
|
|
|
client.set_link_timing(200, 2000); // half the bandwidth doubles packet airtime
|
|
const uint32_t slower_radio = client.fetchRetryTimeoutMs();
|
|
EXPECT_GT(slower_radio, fast_relayed);
|
|
|
|
client.set_link_timing(200, 3000); // a 1/3-duty node needs more service time
|
|
EXPECT_GT(client.fetchRetryTimeoutMs(), slower_radio);
|
|
EXPECT_GT(client.proofGraceMs(), (uint32_t)OTA_PROOF_GRACE_MS);
|
|
}
|
|
|
|
TEST(OtaTransfer, RejectsShortNonFinalManifestFragment) {
|
|
g_q.clear();
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_none{&client};
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_none);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
g_q.clear();
|
|
|
|
const uint8_t* bytes = manifest.manifest_start;
|
|
const uint16_t final_len = (uint16_t)(MOTA_MFL - OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes + OTA_MF_FRAG, final_len);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes, OTA_MF_FRAG - 1);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
|
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes, OTA_MF_FRAG);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FETCHING);
|
|
}
|
|
|
|
TEST(OtaTransfer, RejectsManifestBlockExponentBeforeShift) {
|
|
g_q.clear();
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
std::array<uint8_t, MOTA_MFL> bytes;
|
|
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
|
|
bytes[19] = 32;
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_none{&client};
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_none);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
g_q.clear();
|
|
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
}
|
|
|
|
TEST(OtaTransfer, RejectsUnsupportedManifestHashAlgorithm) {
|
|
g_q.clear();
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
std::array<uint8_t, MOTA_MFL> bytes;
|
|
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
|
|
bytes[2] = HASH_ALGO_SHA256 + 1;
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_none{&client};
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_none);
|
|
client.set_fetch_store(&store);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
g_q.clear();
|
|
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
}
|
|
|
|
TEST(OtaTransfer, RejectsEnvelopeMidThatDoesNotMatchParsedManifestRootBeforeStaging) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
std::array<uint8_t, MOTA_MFL> bytes;
|
|
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
|
|
bytes[20] ^= 0x80; // parsed merkle_root; the peer keeps the requested MID in the wire envelope
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST);
|
|
EXPECT_EQ(store.staged_size(), 0u);
|
|
}
|
|
|
|
TEST(OtaTransfer, RejectsParsedManifestTargetThatDoesNotMatchRequestedCatalogTargetBeforeStaging) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
std::array<uint8_t, MOTA_MFL> bytes;
|
|
memcpy(bytes.data(), manifest.manifest_start, bytes.size());
|
|
wr_u32le(bytes.data() + 3, manifest.target_id ^ 0x01000000u);
|
|
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0, bytes.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1, bytes.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST);
|
|
EXPECT_EQ(store.staged_size(), 0u);
|
|
}
|
|
|
|
// Same end-to-end transfer, but with 1 KB logical blocks: each block is delivered as several
|
|
// self-describing DATA fragments (frag_off), reassembled by the client, then its merkle PROOF is
|
|
// requested + verified separately before the block is committed. Exercises the multi-fragment path.
|
|
TEST(OtaTransfer, MultiFragmentBlocks) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
server.announce();
|
|
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksTotal(), SIM_MOTA_1K_BLOCKS); // 1 KB blocks => fewer, larger blocks
|
|
EXPECT_EQ(client.blocksHave(), client.blocksTotal());
|
|
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN);
|
|
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), m));
|
|
EXPECT_TRUE(mota_check_root(m));
|
|
EXPECT_TRUE(mota_check_image_hash_full(m));
|
|
}
|
|
|
|
// Multi-mota folder serve: a node serves its OWN fw (view0) PLUS an external folder (RamMotaSource) of
|
|
// other `.mota`. Peers discover BOTH via the tiny beacon -> query -> broadcast HAVE catalog, then fetch an
|
|
// external mota end-to-end. The relaying node never holds the folder image in RAM - it streams the
|
|
// manifest/leaves/blocks from the source on demand (loadSource + srcReadTramp + proof-gen from read
|
|
// leaves). The fetched bytes must equal the original `.mota` (proves the trustless relay is byte-exact).
|
|
TEST(OtaFolder, ServesSelfPlusFolderAndFetchesExternal) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
|
|
server.begin(/*own target irrelevant for serving*/ 0, sim_send, &to_client);
|
|
uint8_t srv_id[4] = {0xAB, 0xCD, 0xEF, 0x01}; server.set_seeder_id(srv_id);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
|
|
MotaManifest mSelf, mExt;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, mSelf)); // served as our own fw (view0)
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, mExt)); // served from the external folder
|
|
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN)); // entry 0 = self
|
|
static RamMotaSource folder;
|
|
folder.add(SIM_MOTA_1K, SIM_MOTA_1K_LEN); // an external image (different mid)
|
|
folder.add(SIM_MOTA, SIM_MOTA_LEN); // same as self -> must be DEDUPED
|
|
ASSERT_TRUE(server.add_source(&folder));
|
|
EXPECT_EQ(server.servedCount(), 2); // self + 1 distinct folder mota (dedup)
|
|
|
|
// discovery: beacon -> the client catalogs the source, queries it, and the broadcast HAVE fills the
|
|
// catalog with BOTH served mids.
|
|
server.announce();
|
|
pump(client, &server);
|
|
client.queryAll();
|
|
pump(client, &server);
|
|
EXPECT_EQ(client.catalogCount(), 2);
|
|
|
|
// fetch the EXTERNAL (folder) mota by mid -> served via the source, relayed block-by-block.
|
|
client.pull(mExt.merkle_root, mExt.target_id);
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.blocksHave(), client.blocksTotal());
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN);
|
|
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN)); // byte-exact relay
|
|
MotaManifest got;
|
|
ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), got));
|
|
EXPECT_TRUE(mota_check_root(got));
|
|
EXPECT_TRUE(mota_check_image_hash_full(got));
|
|
}
|
|
|
|
TEST(OtaFolder, OldHostCapabilityZeroSkipsUnsupportedDeflateOperation) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_transport_deflate_decoder(ota_transport_inflate);
|
|
client.set_fetch_store(&store);
|
|
|
|
RamMotaSource old_host;
|
|
old_host.add(SIM_MOTA_1K, SIM_MOTA_1K_LEN);
|
|
old_host.setSourceCaps(0); // deployed descriptor's reserved byte
|
|
ASSERT_TRUE(server.add_source(&old_host));
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, manifest));
|
|
|
|
server.announce();
|
|
pump(client, &server);
|
|
client.pull(manifest.merkle_root, manifest.target_id);
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(old_host.deflateCalls(), 0u); // no per-block timeout on unknown op 0x09
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN);
|
|
EXPECT_EQ(0, memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
}
|
|
|
|
TEST(OtaCatalog, QueryCanRequestOnlyMissingHaveFragment) {
|
|
OtaManager server;
|
|
CapturedMessages sent;
|
|
uint8_t sid[4] = {0x10, 0x20, 0x30, 0x40};
|
|
server.begin(0, capture_send, &sent);
|
|
server.set_seeder_id(sid);
|
|
SyntheticCatalogSource source(12); // 10 rows/fragment => two HAVE fragments
|
|
ASSERT_TRUE(server.add_source(&source));
|
|
ASSERT_EQ(server.servedCount(), 12);
|
|
|
|
QueryMsg query{};
|
|
memcpy(query.seeder_id, sid, 4);
|
|
query.want_fragments = 1UL << 1; // recover only fragment 1
|
|
uint8_t wire[32];
|
|
uint16_t n = encode_query(wire, sizeof(wire), query);
|
|
ASSERT_GT(n, 0);
|
|
server.on_message(wire, n);
|
|
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
HaveMsg have;
|
|
ASSERT_TRUE(decode_have(sent.items[0].data(), (uint16_t)sent.items[0].size(), have));
|
|
EXPECT_EQ(have.frag_idx, 1);
|
|
EXPECT_EQ(have.frag_total, 2);
|
|
EXPECT_EQ(have.n_rows, 2);
|
|
}
|
|
|
|
TEST(OtaCatalog, IncompleteCatalogRetriesOnlyMissingFragment) {
|
|
OtaManager client;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_archive_interest(true);
|
|
uint8_t sid[4] = {0x44, 0x33, 0x22, 0x11};
|
|
uint8_t digest[4] = {0xDE, 0xAD, 0xBE, 0xEF};
|
|
AdvMsg adv{}; memcpy(adv.seeder_id, sid, 4); memcpy(adv.set_digest, digest, 4); adv.n_motas = 3;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t n = encode_adv(wire, sizeof(wire), adv);
|
|
client.set_clock(100);
|
|
client.on_message(wire, n);
|
|
client.set_clock(5000); // past every possible discovery jitter
|
|
client.loop();
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
QueryMsg first;
|
|
ASSERT_TRUE(decode_query(sent.items[0].data(), (uint16_t)sent.items[0].size(), first));
|
|
EXPECT_EQ(first.want_fragments, 0u);
|
|
sent.items.clear();
|
|
|
|
auto deliver_have = [&](uint8_t frag) {
|
|
uint8_t row[OTA_HAVE_ROW_BYTES] = {0};
|
|
row[0] = (uint8_t)(frag + 1); row[1] = 0x77;
|
|
wr_u32le(row + 4, SIM_TARGET_ID); wr_u32le(row + 8, 0x01000000u + frag);
|
|
row[12] = CODEC_FULL; row[13] = MFLAG_FULL; row[14] = 1;
|
|
HaveMsg have{};
|
|
memcpy(have.seeder_id, sid, 4); memcpy(have.set_digest, digest, 4);
|
|
have.frag_idx = frag; have.frag_total = 3; have.n_rows = 1; have.rows = row;
|
|
uint16_t have_len = encode_have(wire, sizeof(wire), have);
|
|
ASSERT_GT(have_len, 0);
|
|
client.on_message(wire, have_len);
|
|
};
|
|
|
|
deliver_have(0);
|
|
deliver_have(2); // fragment 1 is lost
|
|
client.set_clock(5000 + OTA_CATALOG_RETRY_MS + 1);
|
|
client.loop();
|
|
ASSERT_EQ(sent.items.size(), 1u);
|
|
QueryMsg retry;
|
|
ASSERT_TRUE(decode_query(sent.items[0].data(), (uint16_t)sent.items[0].size(), retry));
|
|
EXPECT_EQ(retry.want_fragments, 1UL << 1);
|
|
sent.items.clear();
|
|
|
|
deliver_have(1);
|
|
EXPECT_EQ(client.catalogCount(), 3);
|
|
client.set_clock(5000 + OTA_CATALOG_RETRY_MS * 2 + 2);
|
|
client.loop();
|
|
EXPECT_TRUE(sent.items.empty()); // complete means no further catalog retries
|
|
}
|
|
|
|
TEST(OtaCatalog, RetainsProtocolMaximumRows) {
|
|
OtaManager client;
|
|
client.begin(SIM_TARGET_ID, nullptr, nullptr);
|
|
const uint8_t per = (uint8_t)((MAX_PACKET_PAYLOAD - 12) / OTA_HAVE_ROW_BYTES);
|
|
const uint8_t total = (uint8_t)((255 + per - 1) / per);
|
|
uint8_t sid[4] = {1, 2, 3, 4}, digest[4] = {5, 6, 7, 8};
|
|
uint16_t index = 0;
|
|
for (uint8_t frag = 0; frag < total; frag++) {
|
|
uint8_t rows[MAX_PACKET_PAYLOAD] = {0};
|
|
uint8_t count = 0;
|
|
while (count < per && index < 255) {
|
|
uint8_t* row = rows + (uint16_t)count * OTA_HAVE_ROW_BYTES;
|
|
row[0] = (uint8_t)index; row[1] = 0xC1; row[2] = 0xD2; row[3] = 0xE3;
|
|
wr_u32le(row + 4, SIM_TARGET_ID); wr_u32le(row + 8, 0x01000000u + index);
|
|
row[12] = CODEC_FULL; row[13] = MFLAG_FULL; row[14] = 1;
|
|
count++; index++;
|
|
}
|
|
HaveMsg have{};
|
|
memcpy(have.seeder_id, sid, 4); memcpy(have.set_digest, digest, 4);
|
|
have.frag_idx = frag; have.frag_total = total; have.n_rows = count; have.rows = rows;
|
|
uint8_t wire[MAX_PACKET_PAYLOAD];
|
|
uint16_t n = encode_have(wire, sizeof(wire), have);
|
|
ASSERT_GT(n, 0);
|
|
client.on_message(wire, n);
|
|
}
|
|
ASSERT_EQ(client.catalogCount(), 255);
|
|
ASSERT_NE(client.catalogRow(254), nullptr);
|
|
EXPECT_EQ(client.catalogRow(254)->mid[0], 254);
|
|
}
|
|
|
|
TEST(OtaCatalog, RejectsAdvertisedSourceWithOversizedBlocks) {
|
|
OtaManager server;
|
|
server.begin(0, nullptr, nullptr);
|
|
SyntheticCatalogSource source(1, 12); // 4096-byte blocks exceed OTA_MAX_BLOCK
|
|
ASSERT_TRUE(server.add_source(&source));
|
|
EXPECT_EQ(server.servedCount(), 0);
|
|
}
|
|
|
|
TEST(OtaFolder, ReportsEntriesOmittedByServeRegistryCapacity) {
|
|
OtaManager server;
|
|
server.begin(0, nullptr, nullptr);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
SyntheticCatalogSource source((uint8_t)(OTA_MAX_SERVE + 3));
|
|
ASSERT_TRUE(server.add_source(&source));
|
|
|
|
uint16_t offered = 0, advertised = 0;
|
|
ASSERT_TRUE(server.sourceStats(&source, offered, advertised));
|
|
EXPECT_EQ(offered, (uint16_t)(OTA_MAX_SERVE + 3));
|
|
EXPECT_EQ(advertised, (uint16_t)(OTA_MAX_SERVE - 1)); // primary image occupies slot zero
|
|
EXPECT_EQ(server.servedCount(), OTA_MAX_SERVE);
|
|
}
|
|
|
|
// Fetch-resume across a reboot: a client commits some blocks, "reboots" (a fresh OtaManager on the SAME
|
|
// persisted store), and resumeStaged() re-adopts the partial container and finishes the remaining blocks -
|
|
// without re-fetching the manifest or the blocks already present.
|
|
TEST(OtaTransfer, ResumeAfterReboot) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
|
|
ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN));
|
|
server.announce();
|
|
|
|
// drive only until the first block commits, then "crash"
|
|
int idle = 0, guard = 0;
|
|
while (guard++ < 100000) {
|
|
server.serviceEgress();
|
|
client.serviceEgress();
|
|
if (!g_q.empty()) {
|
|
SimMsg msg = std::move(g_q.front()); g_q.erase(g_q.begin());
|
|
msg.dest->on_message(msg.bytes.data(), (uint16_t)msg.bytes.size());
|
|
idle = 0;
|
|
} else {
|
|
g_clk += 5000;
|
|
client.set_clock(g_clk); client.loop(); client.serviceEgress();
|
|
server.set_clock(g_clk); server.loop(); server.serviceEgress();
|
|
if (!g_q.empty()) { idle = 0; } else if (++idle >= 2) break;
|
|
}
|
|
if (client.blocksHave() >= 1) break;
|
|
}
|
|
ASSERT_GE(client.blocksHave(), 1u);
|
|
ASSERT_LT(client.blocksHave(), client.blocksTotal()); // genuinely partial
|
|
uint32_t had = client.blocksHave();
|
|
g_q.clear(); // in-flight packets are lost in the "reboot"
|
|
|
|
// "reboot": a brand-new manager on the SAME store (its bytes survived) resumes the partial
|
|
OtaManager client2;
|
|
to_client.dest = &client2; // server now replies to the rebooted client
|
|
SendTo to_server2{&server};
|
|
client2.begin(SIM_TARGET_ID, sim_send, &to_server2);
|
|
client2.set_fetch_store(&store);
|
|
client2.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(client2.resumeStaged(nullptr)); // adopt whatever is staged
|
|
EXPECT_EQ(client2.fetchState(), OtaManager::VERIFYING_STAGED);
|
|
finish_staged_verification(client2);
|
|
EXPECT_EQ(client2.blocksHave(), had); // resumed exactly where we left off
|
|
EXPECT_EQ(client2.fetchState(), OtaManager::FETCHING);
|
|
EXPECT_EQ(client2.blocksTotal(), SIM_MOTA_1K_BLOCKS);
|
|
|
|
pump(client2, &server);
|
|
EXPECT_EQ(client2.fetchState(), OtaManager::COMPLETE);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN);
|
|
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN)); // byte-identical to the original
|
|
}
|
|
|
|
TEST(OtaTransfer, BootResumeHonorsCurrentAutofetchTargetAndVersionPolicy) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
ASSERT_TRUE(manifest.is_signed());
|
|
auto stage = [](OtaStoreRam<4096>& store, const uint8_t* bytes,
|
|
uint32_t length) {
|
|
return store.begin(length) && store.write(0, bytes, length);
|
|
};
|
|
|
|
OtaStoreRam<4096> off_store;
|
|
ASSERT_TRUE(stage(off_store, SIM_MOTA, SIM_MOTA_LEN));
|
|
OtaManager off;
|
|
off.begin(manifest.target_id, nullptr, nullptr);
|
|
off.set_fetch_store(&off_store);
|
|
EXPECT_FALSE(off.resumeStaged(nullptr));
|
|
|
|
std::vector<uint8_t> unsigned_mota(SIM_MOTA, SIM_MOTA + SIM_MOTA_LEN);
|
|
unsigned_mota[8u + 1u] &= (uint8_t)~MFLAG_SIGNED;
|
|
memset(unsigned_mota.data() + 8u + 97u, 0, 32u + 64u);
|
|
MotaManifest unsigned_manifest;
|
|
ASSERT_TRUE(mota_parse(unsigned_mota.data(),
|
|
(uint32_t)unsigned_mota.size(), unsigned_manifest));
|
|
ASSERT_FALSE(unsigned_manifest.is_signed());
|
|
OtaStoreRam<4096> unsigned_store;
|
|
ASSERT_TRUE(stage(unsigned_store, unsigned_mota.data(),
|
|
(uint32_t)unsigned_mota.size()));
|
|
OtaManager signed_only;
|
|
signed_only.begin(manifest.target_id, nullptr, nullptr);
|
|
signed_only.set_fetch_store(&unsigned_store);
|
|
signed_only.set_autofetch(OtaManager::AUTOFETCH_SIGNED);
|
|
signed_only.set_auto_version_floor(manifest.fw_version - 1u, true);
|
|
EXPECT_FALSE(signed_only.resumeStaged(nullptr));
|
|
|
|
OtaStoreRam<4096> replay_store;
|
|
ASSERT_TRUE(stage(replay_store, SIM_MOTA, SIM_MOTA_LEN));
|
|
OtaManager replay;
|
|
replay.begin(manifest.target_id, nullptr, nullptr);
|
|
replay.set_fetch_store(&replay_store);
|
|
replay.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
replay.set_auto_version_floor(manifest.fw_version, true);
|
|
EXPECT_FALSE(replay.resumeStaged(nullptr));
|
|
|
|
OtaStoreRam<4096> wrong_target_store;
|
|
ASSERT_TRUE(stage(wrong_target_store, SIM_MOTA, SIM_MOTA_LEN));
|
|
OtaManager wrong_target;
|
|
wrong_target.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
|
wrong_target.set_fetch_store(&wrong_target_store);
|
|
wrong_target.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
wrong_target.set_auto_version_floor(manifest.fw_version - 1u, true);
|
|
EXPECT_FALSE(wrong_target.resumeStaged(nullptr));
|
|
|
|
OtaStoreRam<4096> newer_store;
|
|
ASSERT_TRUE(stage(newer_store, SIM_MOTA, SIM_MOTA_LEN));
|
|
OtaManager newer;
|
|
newer.begin(manifest.target_id, nullptr, nullptr);
|
|
newer.set_fetch_store(&newer_store);
|
|
newer.set_autofetch(OtaManager::AUTOFETCH_SIGNED);
|
|
newer.set_auto_version_floor(manifest.fw_version - 1u, true);
|
|
EXPECT_TRUE(newer.resumeStaged(nullptr));
|
|
EXPECT_EQ(newer.fetchState(), OtaManager::VERIFYING_STAGED);
|
|
}
|
|
|
|
TEST(OtaTransfer, ExplicitResumePreservesTargetZeroWildcard) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
auto stage = [](OtaStoreRam<4096>& store) {
|
|
return store.begin(SIM_MOTA_LEN) &&
|
|
store.write(0, SIM_MOTA, SIM_MOTA_LEN);
|
|
};
|
|
|
|
OtaStoreRam<4096> manual_store;
|
|
ASSERT_TRUE(stage(manual_store));
|
|
OtaManager manual;
|
|
manual.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
|
manual.set_fetch_store(&manual_store);
|
|
EXPECT_EQ(manual.pull(manifest.merkle_root, 0), OtaManager::PULL_RESUMED);
|
|
|
|
OtaStoreRam<4096> archive_store;
|
|
ASSERT_TRUE(stage(archive_store));
|
|
OtaManager archive;
|
|
archive.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
|
archive.set_fetch_store(&archive_store);
|
|
EXPECT_EQ(archive.pull_archive(manifest.merkle_root, 0),
|
|
OtaManager::PULL_RESUMED);
|
|
|
|
OtaStoreRam<4096> mismatch_store;
|
|
ASSERT_TRUE(stage(mismatch_store));
|
|
OtaManager mismatch;
|
|
mismatch.begin(manifest.target_id, nullptr, nullptr);
|
|
mismatch.set_fetch_store(&mismatch_store);
|
|
EXPECT_EQ(mismatch.pull(manifest.merkle_root, manifest.target_id ^ 1u),
|
|
OtaManager::PULL_STARTED);
|
|
EXPECT_EQ(mismatch.fetchState(), OtaManager::WANT_MANIFEST);
|
|
}
|
|
|
|
TEST(OtaTransfer, ExplicitDebugResumeUsesManualIntentNotAutofetchPolicy) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
OtaStoreRam<4096> store;
|
|
ASSERT_TRUE(store.begin(SIM_MOTA_LEN));
|
|
ASSERT_TRUE(store.write(0, SIM_MOTA, SIM_MOTA_LEN));
|
|
|
|
// A deliberate debug/operator resume remains possible with autofetch disabled, an equal running
|
|
// version, and a cross-target wildcard. Those same properties are rejected by resumeStaged(nullptr).
|
|
OtaManager explicit_resume;
|
|
explicit_resume.begin(manifest.target_id ^ 1u, nullptr, nullptr);
|
|
explicit_resume.set_fetch_store(&store);
|
|
explicit_resume.set_autofetch(OtaManager::AUTOFETCH_OFF);
|
|
explicit_resume.set_auto_version_floor(manifest.fw_version, true);
|
|
ASSERT_TRUE(explicit_resume.resumeStagedExplicit(manifest.merkle_root, 0));
|
|
EXPECT_EQ(explicit_resume.fetchState(), OtaManager::VERIFYING_STAGED);
|
|
|
|
OtaStoreRam<4096> wrong_mid_store;
|
|
ASSERT_TRUE(wrong_mid_store.begin(SIM_MOTA_LEN));
|
|
ASSERT_TRUE(wrong_mid_store.write(0, SIM_MOTA, SIM_MOTA_LEN));
|
|
OtaManager wrong_mid;
|
|
wrong_mid.begin(manifest.target_id, nullptr, nullptr);
|
|
wrong_mid.set_fetch_store(&wrong_mid_store);
|
|
uint8_t other_mid[4];
|
|
memcpy(other_mid, manifest.merkle_root, sizeof(other_mid));
|
|
other_mid[0] ^= 1u;
|
|
EXPECT_FALSE(wrong_mid.resumeStagedExplicit(other_mid, 0));
|
|
EXPECT_EQ(wrong_mid.fetchState(), OtaManager::IDLE);
|
|
}
|
|
|
|
TEST(OtaTransfer, ResumeRehashesPayloadBeforeTrustingPresentLeaf) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
server.announce();
|
|
pump(client, &server);
|
|
ASSERT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
|
|
MotaManifest staged;
|
|
ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), staged));
|
|
const uint32_t payload_offset =
|
|
(uint32_t)(staged.payload - store.data());
|
|
const uint32_t leaves_offset =
|
|
(uint32_t)(staged.leaves - store.data());
|
|
uint8_t damaged = (uint8_t)(staged.payload[0] ^ 0x5A);
|
|
ASSERT_TRUE(store.write(payload_offset, &damaged, 1));
|
|
|
|
OtaManager resumed;
|
|
resumed.begin(SIM_TARGET_ID, nullptr, nullptr);
|
|
resumed.set_fetch_store(&store);
|
|
resumed.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(resumed.resumeStaged(nullptr));
|
|
finish_staged_verification(resumed);
|
|
EXPECT_EQ(resumed.fetchState(), OtaManager::FETCHING);
|
|
EXPECT_EQ(resumed.blocksHave() + 1, resumed.blocksTotal());
|
|
uint8_t marker[4] = {};
|
|
ASSERT_TRUE(store.read(leaves_offset, marker, sizeof(marker)));
|
|
const uint8_t missing[4] = {0xFF, 0xFF, 0xFF, 0xFF};
|
|
EXPECT_EQ(0, std::memcmp(marker, missing, sizeof(marker)));
|
|
}
|
|
|
|
TEST(OtaTransfer, ResumeReadFailureCanNeverBecomeComplete) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
FaultingResumeStore store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
server.announce();
|
|
pump(client, &server);
|
|
ASSERT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
|
|
MotaManifest staged;
|
|
ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), staged));
|
|
store.failReadAt((uint32_t)(staged.leaves - store.data()));
|
|
|
|
OtaManager resumed;
|
|
resumed.begin(SIM_TARGET_ID, nullptr, nullptr);
|
|
resumed.set_fetch_store(&store);
|
|
resumed.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(resumed.resumeStaged(nullptr));
|
|
finish_staged_verification(resumed);
|
|
EXPECT_EQ(resumed.fetchState(), OtaManager::FAILED);
|
|
}
|
|
|
|
TEST(OtaTransfer, ClientRejectsWrongTarget) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID ^ 0x1u, sim_send, &to_server); // different target -> not interested
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY); // tests exercise fetch-on-advert; policy default is OFF
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
server.announce();
|
|
pump(client, &server); // catalogs the row but wantRow rejects it (wrong target) -> never fetches
|
|
EXPECT_EQ(client.fetchState(), OtaManager::IDLE); // never started
|
|
}
|
|
|
|
TEST(OtaTransfer, ManualCrossTargetFetch) {
|
|
// A node whose own target differs from the served firmware normally won't fetch (role-switch case:
|
|
// e.g. companion wanting repeater firmware). An explicit want() override lets it fetch deliberately.
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID ^ 0xABCDu, sim_send, &to_server); // DIFFERENT own target
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY); // tests exercise fetch-on-advert; policy default is OFF
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
|
|
// without the override: catalogs the row but won't fetch (wrong target)
|
|
server.announce();
|
|
pump(client, &server);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::IDLE);
|
|
|
|
// with want(): deliberately fetch the different-target firmware to completion
|
|
client.want(SIM_TARGET_ID);
|
|
server.announce();
|
|
pump(client, &server);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_LEN);
|
|
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA, SIM_MOTA_LEN));
|
|
}
|
|
|
|
// Encode a 1-row OTA_HAVE catalog (the discovery reply a peer acts on).
|
|
static uint16_t make_have1(uint8_t* buf, uint16_t cap, const uint8_t mid[4],
|
|
uint32_t target, uint32_t fwver, uint8_t codec, uint8_t flags) {
|
|
uint8_t row[OTA_HAVE_ROW_BYTES];
|
|
memcpy(row, mid, 4);
|
|
row[4]=target; row[5]=target>>8; row[6]=target>>16; row[7]=target>>24;
|
|
row[8]=fwver; row[9]=fwver>>8; row[10]=fwver>>16; row[11]=fwver>>24;
|
|
row[12]=codec; row[13]=flags;
|
|
row[14]=0; row[15]=0; // have_count (unused in this 1-row discovery test)
|
|
HaveMsg hv{{0xAA,0xBB,0xCC,0xDD}, {0,0,0,0}, 0, 1, 1, row};
|
|
return encode_have(buf, cap, hv);
|
|
}
|
|
|
|
TEST(OtaTransfer, BootloaderPackageRequiresCapableExplicitMidPull) {
|
|
const auto manifest = boot_manifest_bytes();
|
|
const uint8_t mid[4] = {0x11, 0x22, 0x33, 0x44};
|
|
const uint8_t boot_flags = MFLAG_FULL | MFLAG_SIGNED | MFLAG_BOOTLOADER;
|
|
uint8_t wire[64];
|
|
|
|
// Even a capable XIAO must never autofetch a bootloader package. The target is intentionally
|
|
// its exact installed-bootloader board ID, so this proves the manual-MID gate rather than a
|
|
// coincidental target mismatch.
|
|
OtaManager capable;
|
|
OtaStoreRam<45000> capable_store;
|
|
capable.begin(OTA_XIAO_BOARD_ID_BASE, nullptr, nullptr);
|
|
capable.set_fetch_store(&capable_store);
|
|
capable.set_accept_full(false); // internal nRF52 app FULL remains forbidden
|
|
capable.set_accept_bootloader(true);
|
|
capable.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
capable.on_message(
|
|
wire, make_have1(wire, sizeof(wire), mid, OTA_XIAO_BOARD_ID_BASE, 1,
|
|
CODEC_FULL, boot_flags));
|
|
EXPECT_EQ(capable.fetchState(), OtaManager::IDLE);
|
|
EXPECT_EQ(capable_store.staged_size(), 0u);
|
|
|
|
// Enabling the privileged bootloader path must not broaden the ordinary
|
|
// single-slot application codec policy or turn autofetch on for FULL apps.
|
|
const uint8_t app_mid[4] = {0x21, 0x22, 0x23, 0x24};
|
|
capable.on_message(
|
|
wire, make_have1(wire, sizeof(wire), app_mid, OTA_XIAO_BOARD_ID_BASE, 2,
|
|
CODEC_FULL, MFLAG_FULL));
|
|
EXPECT_EQ(capable.fetchState(), OtaManager::IDLE);
|
|
EXPECT_EQ(capable_store.staged_size(), 0u);
|
|
|
|
// An exact explicit pull admits the strict v3 manifest and opens the store for the 40 KiB payload.
|
|
EXPECT_EQ(capable.pull(mid, OTA_XIAO_BOARD_ID_BASE), OtaManager::PULL_STARTED);
|
|
deliver_manifest_fragment(capable, mid, 0, manifest.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(capable, mid, 1, manifest.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(capable.fetchState(), OtaManager::FETCHING);
|
|
EXPECT_TRUE(capable.fetched_is_bootloader());
|
|
EXPECT_GT(capable_store.staged_size(), OTA_BOOT_IMAGE_SIZE);
|
|
|
|
// The same explicit pull is rejected before storage begin when this firmware lacks the privileged
|
|
// capability. This is the manager-layer guard; every apply backend has an independent rejection too.
|
|
OtaManager incapable;
|
|
OtaStoreRam<45000> incapable_store;
|
|
incapable.begin(OTA_XIAO_BOARD_ID_BASE, nullptr, nullptr);
|
|
incapable.set_fetch_store(&incapable_store);
|
|
incapable.set_accept_full(true);
|
|
EXPECT_EQ(incapable.pull(mid, OTA_XIAO_BOARD_ID_BASE), OtaManager::PULL_STARTED);
|
|
deliver_manifest_fragment(incapable, mid, 0, manifest.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(incapable, mid, 1, manifest.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(incapable.fetchState(), OtaManager::FAILED);
|
|
EXPECT_EQ(incapable.fetchError(), OtaManager::FETCH_ERROR_CODEC);
|
|
EXPECT_EQ(incapable_store.staged_size(), 0u);
|
|
}
|
|
|
|
// A node must not fetch firmware it can't apply: a catalog row whose codec the platform can't decode is
|
|
// not fetched. Full-image acceptance is platform-selectable (nRF52 single-slot disables it).
|
|
TEST(OtaTransfer, RejectsIncompatibleCodec) {
|
|
g_q.clear();
|
|
OtaManager client; OtaStoreRam<4096> store;
|
|
SendTo to_server{&client}; // dest unused (we only check client state)
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
client.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
|
client.set_accept_full(false); // nRF52-style: in-place delta only
|
|
uint8_t b[64];
|
|
|
|
// a SEQUENTIAL delta for our target -> incompatible -> not fetched (stays IDLE)
|
|
uint8_t midA[4] = {1,2,3,4};
|
|
client.on_message(b, make_have1(b, sizeof(b), midA, SIM_TARGET_ID, 0x01000000, CODEC_DETOOLS_SEQUENTIAL, 0));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::IDLE);
|
|
|
|
// a FULL image cannot be installed in an nRF52 single slot -> do not spend hours fetching it
|
|
uint8_t midFull[4] = {2,3,4,5};
|
|
client.on_message(b, make_have1(b, sizeof(b), midFull, SIM_TARGET_ID, 0x01000000, CODEC_FULL, MFLAG_FULL));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::IDLE);
|
|
|
|
// an IN-PLACE delta for our target -> compatible -> begins fetching (requests the manifest)
|
|
uint8_t midB[4] = {5,6,7,8};
|
|
client.on_message(b, make_have1(b, sizeof(b), midB, SIM_TARGET_ID, 0x01000000, CODEC_DETOOLS_INPLACE, 0));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
|
g_q.clear();
|
|
}
|
|
|
|
TEST(OtaTransfer, ManualPullReportsIncompatibleManifestInsteadOfGoingIdle) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
|
client.set_accept_full(false);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
|
|
EXPECT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_CODEC);
|
|
EXPECT_EQ(client.wanted(), 0u); // terminal rejection no longer leaves discovery armed
|
|
EXPECT_EQ(store.staged_size(), 0u); // compatibility is rejected before the store is begun
|
|
}
|
|
|
|
TEST(OtaTransfer, AutomaticAdmissionRejectsReplayAndRechecksLyingHaveVersion) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
client.set_auto_version_floor(manifest.fw_version, true);
|
|
|
|
uint8_t wire[64];
|
|
client.on_message(wire, make_have1(wire, sizeof(wire), manifest.merkle_root,
|
|
manifest.target_id, manifest.fw_version,
|
|
manifest.codec_id, manifest.flags));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::IDLE); // equal-version replay rejected at catalog admission
|
|
|
|
client.on_message(wire, make_have1(wire, sizeof(wire), manifest.merkle_root,
|
|
manifest.target_id, manifest.fw_version + 1u,
|
|
manifest.codec_id, manifest.flags));
|
|
ASSERT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED); // parsed version defeats the lying HAVE row
|
|
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_VERSION);
|
|
EXPECT_EQ(store.staged_size(), 0u);
|
|
}
|
|
|
|
TEST(OtaTransfer, SignedOnlyAutofetchRechecksTheParsedManifestFlag) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
ASSERT_TRUE(manifest.is_signed());
|
|
std::array<uint8_t, MOTA_MFL> unsigned_bytes;
|
|
memcpy(unsigned_bytes.data(), manifest.manifest_start, unsigned_bytes.size());
|
|
unsigned_bytes[1] &= (uint8_t)~MFLAG_SIGNED;
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_SIGNED);
|
|
client.set_auto_version_floor(manifest.fw_version - 1u, true);
|
|
|
|
uint8_t wire[64];
|
|
client.on_message(wire, make_have1(wire, sizeof(wire), manifest.merkle_root,
|
|
manifest.target_id, manifest.fw_version,
|
|
manifest.codec_id,
|
|
(uint8_t)(manifest.flags | MFLAG_SIGNED)));
|
|
ASSERT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
unsigned_bytes.data(), OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
unsigned_bytes.data() + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST);
|
|
EXPECT_EQ(store.staged_size(), 0u);
|
|
}
|
|
|
|
TEST(OtaTransfer, ExplicitManualPullMayOverrideAutomaticVersionFloor) {
|
|
MotaManifest manifest;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, manifest));
|
|
OtaManager client;
|
|
OtaStoreRam<4096> store;
|
|
CapturedMessages sent;
|
|
client.begin(SIM_TARGET_ID, capture_send, &sent);
|
|
client.set_fetch_store(&store);
|
|
client.set_auto_version_floor(manifest.fw_version, true);
|
|
|
|
ASSERT_EQ(client.pull(manifest.merkle_root, manifest.target_id), OtaManager::PULL_STARTED);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 0,
|
|
manifest.manifest_start, OTA_MF_FRAG);
|
|
deliver_manifest_fragment(client, manifest.merkle_root, 1,
|
|
manifest.manifest_start + OTA_MF_FRAG,
|
|
(uint16_t)(MOTA_MFL - OTA_MF_FRAG));
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FETCHING);
|
|
EXPECT_GT(store.staged_size(), 0u);
|
|
}
|
|
|
|
TEST(OtaTransfer, PullAdmissionReportsNoStoreAndBusyWithoutReplacingIntent) {
|
|
g_q.clear();
|
|
OtaManager client;
|
|
SendTo to_none{&client};
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_none);
|
|
uint8_t first[4] = {1, 2, 3, 4}, second[4] = {5, 6, 7, 8};
|
|
EXPECT_EQ(client.pull(first, SIM_TARGET_ID), OtaManager::PULL_NO_STORE);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::IDLE);
|
|
EXPECT_EQ(client.wanted(), 0u);
|
|
|
|
OtaStoreRam<4096> store;
|
|
client.set_fetch_store(&store);
|
|
EXPECT_EQ(client.pull(first, SIM_TARGET_ID), OtaManager::PULL_STARTED);
|
|
EXPECT_EQ(client.pull(second, SIM_TARGET_ID ^ 0x55AAu), OtaManager::PULL_BUSY);
|
|
EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
|
EXPECT_EQ(client.wanted(), SIM_TARGET_ID);
|
|
EXPECT_EQ(0, memcmp(client.fetchManifestId(), first, sizeof(first)));
|
|
g_q.clear();
|
|
}
|
|
|
|
// An archive capture is not an install. It must retain cross-target and otherwise unsupported containers
|
|
// byte-for-byte so this node can relay them to hardware that does understand their codec.
|
|
TEST(OtaTransfer, ArchivePullAcceptsCrossTargetUnsupportedCodec) {
|
|
g_q.clear();
|
|
OtaManager server, client;
|
|
OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID ^ 0x55AAu, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_apply_codec(CODEC_DETOOLS_INPLACE);
|
|
client.set_accept_full(false); // install path cannot accept SIM_MOTA (full image)
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
|
|
MotaManifest m;
|
|
ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, m));
|
|
client.pull_archive(m.merkle_root, m.target_id); // capture ignores local target + install codec
|
|
pump(client, &server);
|
|
|
|
EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
ASSERT_EQ(store.staged_size(), SIM_MOTA_LEN);
|
|
EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA, SIM_MOTA_LEN));
|
|
}
|
|
|
|
// Encode a 1-row OTA_HAVE from a specific seeder, carrying have_count (Phase-2 awareness).
|
|
static uint16_t make_have_row(uint8_t* buf, uint16_t cap, const uint8_t mid[4], uint32_t target,
|
|
uint32_t fwver, uint8_t codec, uint8_t flags,
|
|
const uint8_t seeder[4], uint16_t have_count,
|
|
const uint8_t digest[4] = nullptr) {
|
|
uint8_t row[OTA_HAVE_ROW_BYTES];
|
|
memcpy(row, mid, 4);
|
|
row[4]=target; row[5]=target>>8; row[6]=target>>16; row[7]=target>>24;
|
|
row[8]=fwver; row[9]=fwver>>8; row[10]=fwver>>16; row[11]=fwver>>24;
|
|
row[12]=codec; row[13]=flags;
|
|
row[14]=(uint8_t)(have_count & 0xFF); row[15]=(uint8_t)(have_count >> 8);
|
|
HaveMsg hv; memcpy(hv.seeder_id, seeder, 4);
|
|
if (digest) memcpy(hv.set_digest, digest, 4); else memset(hv.set_digest, 0, 4);
|
|
hv.frag_idx=0; hv.frag_total=1; hv.n_rows=1; hv.rows=row;
|
|
return encode_have(buf, cap, hv);
|
|
}
|
|
|
|
// Catalog accounting: "N nodes have it" must count DISTINCT seeders (a repeated HAVE from one node must
|
|
// not inflate it), and have_max tracks the best progress any source reported.
|
|
TEST(OtaCatalog, DistinctSeederCountAndHaveCount) {
|
|
OtaManager m; SendTo none{&m}; m.begin(SIM_TARGET_ID, sim_send, &none);
|
|
uint8_t b[64]; uint8_t mid[4]={9,9,9,9};
|
|
uint8_t s1[4]={1,0,0,0}, s2[4]={2,0,0,0};
|
|
m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s1, 5));
|
|
m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s1, 7)); // same seeder
|
|
ASSERT_EQ(m.catalogCount(), 1);
|
|
EXPECT_EQ(m.catalogRow(0)->n_seeders, 1); // counted once despite two HAVEs
|
|
EXPECT_EQ(m.catalogRow(0)->have_max, 7u); // max progress seen
|
|
m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s2, 3)); // new seeder
|
|
EXPECT_EQ(m.catalogRow(0)->n_seeders, 2);
|
|
EXPECT_EQ(m.catalogRow(0)->have_max, 7u); // still the max, not overwritten by the lower one
|
|
g_q.clear();
|
|
}
|
|
|
|
TEST(OtaCatalog, DigestChangePurgesOnlyThatSeedersRowsAndProgress) {
|
|
OtaManager m; SendTo none{&m}; m.begin(SIM_TARGET_ID, sim_send, &none);
|
|
m.set_archive_interest(true);
|
|
uint8_t wire[64], mid[4] = {9, 8, 7, 6};
|
|
uint8_t s1[4] = {1, 0, 0, 0}, s2[4] = {2, 0, 0, 0};
|
|
uint8_t d1[4] = {0x11, 0, 0, 0}, d2[4] = {0x22, 0, 0, 0}, changed[4] = {0x33, 0, 0, 0};
|
|
|
|
auto advertise = [&](const uint8_t sid[4], const uint8_t digest[4], uint8_t count = 1) {
|
|
AdvMsg adv{}; memcpy(adv.seeder_id, sid, 4); memcpy(adv.set_digest, digest, 4); adv.n_motas = count;
|
|
uint16_t n = encode_adv(wire, sizeof(wire), adv);
|
|
ASSERT_GT(n, 0);
|
|
m.on_message(wire, n);
|
|
};
|
|
advertise(s1, d1);
|
|
advertise(s2, d2);
|
|
m.set_clock(100);
|
|
m.on_message(wire, make_have_row(wire, sizeof(wire), mid, SIM_TARGET_ID, 0x01020300,
|
|
CODEC_FULL, MFLAG_FULL, s1, 7, d1));
|
|
m.set_clock(200);
|
|
m.on_message(wire, make_have_row(wire, sizeof(wire), mid, SIM_TARGET_ID, 0x01020300,
|
|
CODEC_FULL, MFLAG_FULL, s2, 3, d2));
|
|
ASSERT_EQ(m.catalogCount(), 1);
|
|
ASSERT_EQ(m.catalogRow(0)->n_seeders, 2);
|
|
ASSERT_EQ(m.catalogRow(0)->have_max, 7u);
|
|
|
|
advertise(s1, changed);
|
|
ASSERT_EQ(m.catalogCount(), 1);
|
|
EXPECT_EQ(m.catalogRow(0)->n_seeders, 1);
|
|
EXPECT_EQ(m.catalogRow(0)->have_max, 3u);
|
|
EXPECT_EQ(m.catalogRow(0)->last_ms, 200u);
|
|
|
|
m.set_clock(300);
|
|
m.on_message(wire, make_have_row(wire, sizeof(wire), mid, SIM_TARGET_ID, 0x01020300,
|
|
CODEC_FULL, MFLAG_FULL, s1, 9, d1)); // delayed row from the old digest
|
|
ASSERT_EQ(m.catalogCount(), 1);
|
|
EXPECT_EQ(m.catalogRow(0)->n_seeders, 1);
|
|
EXPECT_EQ(m.catalogRow(0)->have_max, 3u);
|
|
|
|
advertise(s2, changed, 0); // an explicit empty advert withdraws the source
|
|
EXPECT_EQ(m.catalogCount(), 0); // nobody still advertises the old set
|
|
g_q.clear();
|
|
}
|
|
|
|
// An unanswered GET_MANIFEST must not pin the fetch slot forever: after OTA_MANIFEST_MAX_RETRY ticks with
|
|
// no manifest, the session gives up (FAILED) so a new pull can take the slot. (Bounded primary operation.)
|
|
TEST(OtaTransfer, ManifestGiveUpAfterRetries) {
|
|
g_q.clear();
|
|
OtaManager client; OtaStoreRam<4096> store; SendTo to_none{&client};
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_none);
|
|
client.set_fetch_store(&store);
|
|
uint8_t mid[4]={7,7,7,7};
|
|
client.pull(mid, SIM_TARGET_ID); // no server -> stuck WANT_MANIFEST
|
|
EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST);
|
|
for (int i = 0; i < OTA_MANIFEST_MAX_RETRY + 2; i++) { g_clk += 5000; client.set_clock(g_clk); client.loop(); g_q.clear(); }
|
|
EXPECT_EQ(client.fetchState(), OtaManager::FAILED);
|
|
EXPECT_EQ(client.fetchError(), OtaManager::FETCH_ERROR_MANIFEST_TIMEOUT);
|
|
EXPECT_EQ(client.wanted(), 0u);
|
|
}
|
|
|
|
// A receiver never becomes a source, either while fetching or after completion.
|
|
TEST(OtaTransfer, ReceiverDoesNotReSeed) {
|
|
g_q.clear();
|
|
OtaManager server, client; OtaStoreRam<4096> store;
|
|
SendTo to_client{&client}, to_server{&server};
|
|
server.begin(0, sim_send, &to_client);
|
|
client.begin(SIM_TARGET_ID, sim_send, &to_server);
|
|
client.set_fetch_store(&store);
|
|
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
|
|
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
|
|
server.announce();
|
|
pump(client, &server);
|
|
ASSERT_EQ(client.fetchState(), OtaManager::COMPLETE);
|
|
EXPECT_EQ(client.servedCount(), 0);
|
|
client.reset_session();
|
|
EXPECT_EQ(client.servedCount(), 0);
|
|
}
|
|
|
|
// --- detools delta decode (vendored detools C decoder, CRLE-only build) ----------------------
|
|
// Mirrors the device apply path (src/helpers/ota/OtaApply.cpp): base read via from_read/from_seek,
|
|
// patch streamed via patch_read, output written via to_write. Proves the on-device delta apply uses
|
|
// detools 0.53.0's own decoder and reproduces the exact target the host packager targeted.
|
|
namespace {
|
|
struct DTMem {
|
|
const uint8_t* base; long base_len; long base_pos;
|
|
const uint8_t* patch; long patch_len; long patch_pos;
|
|
std::vector<uint8_t> out;
|
|
};
|
|
int dt_from_read(void* a, uint8_t* b, size_t n) {
|
|
DTMem* c = (DTMem*)a;
|
|
if (c->base_pos < 0 || c->base_pos + (long)n > c->base_len) return -DETOOLS_IO_FAILED;
|
|
std::memcpy(b, c->base + c->base_pos, n); c->base_pos += (long)n; return DETOOLS_OK;
|
|
}
|
|
int dt_from_seek(void* a, int off) {
|
|
DTMem* c = (DTMem*)a; c->base_pos += off;
|
|
if (c->base_pos < 0 || c->base_pos > c->base_len) return -DETOOLS_IO_FAILED;
|
|
return DETOOLS_OK;
|
|
}
|
|
int dt_patch_read(void* a, uint8_t* b, size_t n) {
|
|
DTMem* c = (DTMem*)a;
|
|
if (c->patch_pos + (long)n > c->patch_len) return -DETOOLS_IO_FAILED;
|
|
std::memcpy(b, c->patch + c->patch_pos, n); c->patch_pos += (long)n; return DETOOLS_OK;
|
|
}
|
|
int dt_to_write(void* a, const uint8_t* b, size_t n) {
|
|
DTMem* c = (DTMem*)a; c->out.insert(c->out.end(), b, b + n); return DETOOLS_OK;
|
|
}
|
|
|
|
// In-place apply over a flat memory region (models the nRF52 app workspace / the bootloader's flash).
|
|
struct DTInPlace {
|
|
std::vector<uint8_t> mem; // [0,memory_size): base in, target out
|
|
const uint8_t* patch; long plen, ppos; int step;
|
|
};
|
|
int ip_mem_read(void* a, void* dst, uintptr_t src, size_t n) {
|
|
DTInPlace* c = (DTInPlace*)a; if (src + n > c->mem.size()) return -DETOOLS_IO_FAILED;
|
|
std::memcpy(dst, c->mem.data() + src, n); return DETOOLS_OK;
|
|
}
|
|
int ip_mem_write(void* a, uintptr_t dst, void* src, size_t n) {
|
|
DTInPlace* c = (DTInPlace*)a; if (dst + n > c->mem.size()) return -DETOOLS_IO_FAILED;
|
|
std::memcpy(c->mem.data() + dst, src, n); return DETOOLS_OK;
|
|
}
|
|
int ip_mem_erase(void* a, uintptr_t addr, size_t n) {
|
|
DTInPlace* c = (DTInPlace*)a; if (addr + n > c->mem.size()) return -DETOOLS_IO_FAILED;
|
|
std::memset(c->mem.data() + addr, 0xFF, n); return DETOOLS_OK;
|
|
}
|
|
int ip_step_set(void* a, int s) { ((DTInPlace*)a)->step = s; return DETOOLS_OK; }
|
|
int ip_step_get(void* a, int* s) { *s = ((DTInPlace*)a)->step; return DETOOLS_OK; }
|
|
int ip_patch_read(void* a, uint8_t* b, size_t n) {
|
|
DTInPlace* c = (DTInPlace*)a; if (c->ppos + (long)n > c->plen) return -DETOOLS_IO_FAILED;
|
|
std::memcpy(b, c->patch + c->ppos, n); c->ppos += (long)n; return DETOOLS_OK;
|
|
}
|
|
} // namespace
|
|
|
|
TEST(Detools, SequentialCrlePatchReproducesTarget) {
|
|
DTMem c{DT_BASE, (long)DT_BASE_LEN, 0, DT_PATCH, (long)DT_PATCH_LEN, 0, {}};
|
|
int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
|
|
(size_t)DT_PATCH_LEN, dt_to_write, &c);
|
|
ASSERT_EQ(r, (int)DT_TARGET_LEN); // returns to-size on success
|
|
ASSERT_EQ(c.out.size(), (size_t)DT_TARGET_LEN);
|
|
EXPECT_EQ(0, std::memcmp(c.out.data(), DT_TARGET, DT_TARGET_LEN));
|
|
}
|
|
|
|
TEST(Detools, WrongBaseDoesNotReproduceTarget) {
|
|
// a base that differs from the one the patch was built against must NOT yield the target
|
|
std::vector<uint8_t> bad(DT_BASE, DT_BASE + DT_BASE_LEN);
|
|
for (size_t i = 0; i < bad.size(); i += 7) bad[i] ^= 0xFF;
|
|
DTMem c{bad.data(), (long)bad.size(), 0, DT_PATCH, (long)DT_PATCH_LEN, 0, {}};
|
|
int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
|
|
(size_t)DT_PATCH_LEN, dt_to_write, &c);
|
|
bool reproduced = (r == (int)DT_TARGET_LEN && c.out.size() == (size_t)DT_TARGET_LEN &&
|
|
std::memcmp(c.out.data(), DT_TARGET, DT_TARGET_LEN) == 0);
|
|
EXPECT_FALSE(reproduced); // wrong base -> wrong/short output (the device then fails image_hash)
|
|
}
|
|
|
|
TEST(Detools, TruncatedPatchFails) {
|
|
DTMem c{DT_BASE, (long)DT_BASE_LEN, 0, DT_PATCH, (long)(DT_PATCH_LEN / 2), 0, {}};
|
|
int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read,
|
|
(size_t)(DT_PATCH_LEN / 2), dt_to_write, &c);
|
|
EXPECT_TRUE(r < 0 || c.out.size() != (size_t)DT_TARGET_LEN);
|
|
}
|
|
|
|
// nRF52 path: the bootloader applies an in-place patch over the single app slot. Model the app
|
|
// region as a DT_IP_MEM buffer holding the base; after apply, region[0:to_size] must equal the target.
|
|
TEST(Detools, InPlaceCrlePatchReproducesTarget) {
|
|
DTInPlace c; c.mem.assign(DT_IP_MEM, 0xFF);
|
|
std::memcpy(c.mem.data(), DT_IP_BASE, DT_IP_BASE_LEN); // base loaded at offset 0
|
|
c.patch = DT_IP_PATCH; c.plen = DT_IP_PATCH_LEN; c.ppos = 0; c.step = 0;
|
|
int r = detools_apply_patch_in_place_callbacks(ip_mem_read, ip_mem_write, ip_mem_erase,
|
|
ip_step_set, ip_step_get, ip_patch_read,
|
|
(size_t)DT_IP_PATCH_LEN, &c);
|
|
ASSERT_EQ(r, (int)DT_IP_TARGET_LEN); // returns to-size on success
|
|
EXPECT_EQ(0, std::memcmp(c.mem.data(), DT_IP_TARGET, DT_IP_TARGET_LEN));
|
|
}
|
|
|
|
// --- leaf-diff warm-start core (motatool folder-capture): the device fetches the target leaves[], recomputes
|
|
// the root to authenticate them, then keeps every seed block whose leaf matches and refetches only the rest.
|
|
// This exercises that logic (leaf authentication + per-block diff) with no store/fetch machinery. ----------
|
|
TEST(OtaWarmStart, LeafDiffAuthenticatesAndFindsDifferingBlocks) {
|
|
const uint32_t BS = 16, BC = 5;
|
|
std::vector<uint8_t> target(BS * BC), seed(BS * BC);
|
|
for (uint32_t i = 0; i < target.size(); i++) target[i] = seed[i] = (uint8_t)(i * 7 + 3);
|
|
seed[1 * BS + 5] ^= 0xFF; // blocks 1 and 3 differ in the seed (a non-deterministic-rebuild style diff)
|
|
seed[3 * BS + 0] ^= 0x01;
|
|
|
|
// target leaves + root (what the device receives over OTA_LEAVES + the manifest merkle_root)
|
|
uint8_t tleaves[BC * 4], troot[4];
|
|
for (uint32_t i = 0; i < BC; i++) merkle_leaf(tleaves + i * 4, target.data() + i * BS, BS);
|
|
merkle_root(troot, tleaves, BC);
|
|
|
|
// authenticate the fetched leaves: recomputing the root from them must equal the manifest root
|
|
uint8_t chk[4]; merkle_root(chk, tleaves, BC);
|
|
EXPECT_EQ(0, memcmp(chk, troot, 4));
|
|
|
|
// diff: a seed block is kept iff its leaf equals the (authenticated) target leaf
|
|
int nmiss = 0; bool miss[BC] = {false};
|
|
for (uint32_t i = 0; i < BC; i++) {
|
|
uint8_t sl[4]; merkle_leaf(sl, seed.data() + i * BS, BS);
|
|
if (memcmp(sl, tleaves + i * 4, 4) != 0) { miss[i] = true; nmiss++; }
|
|
}
|
|
EXPECT_EQ(nmiss, 2);
|
|
EXPECT_TRUE(miss[1]); EXPECT_TRUE(miss[3]);
|
|
EXPECT_FALSE(miss[0]); EXPECT_FALSE(miss[2]); EXPECT_FALSE(miss[4]);
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|