Support 2 KiB DEFLATE OTA transport

This commit is contained in:
mikecarper committed 2026-09-03 01:07:06 -07:00
1 parent 066f1abca6
commit e2da2701da
32 files changed
+3877 -357

No files matched your search

+11
View File
@@ -99,6 +99,17 @@ static const uint8_t SIM_MOTA_1K[2278] = {109,79,84,65,230,8,0,0,2,3,18,190,186,
static const uint32_t SIM_MOTA_1K_LEN = 2278;
static const uint32_t SIM_MOTA_1K_BLOCKS = 3;
// 2 KiB-block signed .mota: one full block plus a short tail
static const uint8_t SIM_MOTA_2K[2459] = {109,79,84,65,155,9,0,0,2,3,18,190,186,254,202,0,0,1,3,193,8,0,0,193,8,0,0,11,215,231,3,172,108,182,235,118,117,144,154,116,108,17,3,95,148,199,59,101,154,141,10,217,94,113,26,75,186,162,4,12,251,124,40,158,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,161,7,191,243,206,16,190,29,112,221,24,231,75,192,153,103,228,214,48,155,165,13,95,29,220,134,100,18,85,49,184,112,233,110,114,111,38,182,17,125,35,32,237,155,232,221,22,219,162,105,92,72,126,98,190,122,199,153,55,34,3,96,147,196,73,213,151,142,144,239,68,90,135,125,89,141,89,54,238,22,82,134,79,80,161,194,141,199,109,134,124,188,202,221,7,255,255,255,255,126,165,133,214,82,63,130,220,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111,114,46,32,77,101,115,104,67,111,114,101,32,50,32,75,105,66,32,116,114,97,110,115,112,111,114,116,32,68,69,70,76,65,84,69,32,118,101,99,116,111Line truncated
static const uint32_t SIM_MOTA_2K_LEN = 2459;
static const uint32_t SIM_MOTA_2K_BLOCKS = 2;
static const uint8_t SIM_MOTA_2K_DEFLATED[127] = {243,77,45,206,112,206,47,74,85,48,82,240,206,116,82,40,41,74,204,43,46,200,47,42,81,112,113,117,243,113,12,113,85,40,75,77,46,201,47,210,83,240,29,85,56,170,112,84,225,168,194,81,133,163,10,71,21,14,23,133,0,211,83,240,77,45,206,112,206,47,74,85,48,82,240,206,116,82,40,41,74,204,43,46,200,47,42,81,112,113,117,243,113,12,113,85,40,75,77,46,201,47,210,163,177,66,215,188,20,183,78,14,6,134,31,155,53,95,239,44,136,125,205,64,36,0,0};
static const uint16_t SIM_MOTA_2K_DEFLATED_OFFSETS[2] = {0,61};
static const uint16_t SIM_MOTA_2K_DEFLATED_LENGTHS[2] = {61,66};
// detools sequential+crle delta: apply DT_PATCH to DT_BASE -> DT_TARGET
static const uint8_t DT_BASE[3056] = {120,46,186,148,77,51,227,185,104,193,183,194,67,136,62,162,208,188,127,90,106,134,186,157,246,55,79,139,180,84,132,19,187,198,255,221,52,176,192,186,119,236,181,212,223,167,37,136,54,222,105,250,14,197,89,160,106,119,31,185,190,35,195,83,99,84,88,203,51,83,109,106,81,145,54,231,222,104,58,52,10,191,57,195,4,248,221,66,216,129,81,197,245,145,205,180,107,157,28,84,217,167,155,199,59,60,254,118,93,34,51,94,126,152,214,160,36,67,99,159,86,85,240,181,255,182,119,220,43,175,178,196,220,33,84,236,52,148,175,16,25,240,215,44,1,230,38,112,180,60,89,58,20,50,205,72,61,177,118,154,67,123,134,225,111,169,248,106,51,215,18,77,77,71,34,144,169,187,64,134,25,126,55,228,50,200,99,45,131,217,57,81,90,192,179,228,204,11,148,230,238,173,139,96,239,189,184,243,162,18,30,58,14,132,32,241,212,53,232,162,157,236,22,242,129,44,60,124,149,204,187,42,41,22,32,158,26,207,241,152,143,207,254,154,161,7,152,27,143,46,139,178,80,0,31,71,7,46,15,26,162,219,159,172,158,187,53,148,53,165,48,118,47,121,80,69,187,116,162,112,213,183,206,210,55,102,150,221,114,221,107,152,179,34,225,53,41,79,101,50,192,45,91,116,175,3,30,85,172,0,197,57,192,129,107,168,249,9,54,155,118,141,127,140,206,12,110,85,2,130,87,141,249,230,240,224,65,170,187,40,57,154,248,27,211,203,216,110,17,245,225,34,44,6,220,86,78,242,234,64,126,41,92,213,119,237,111,241,125,157,83,40,9,197,31,30,93,108,162,244,46,129,180,24,13,168,111,228,6,207,233,227,240,69,59,30,81,139,222,145,35,54,145,149,27,110,26,241,153,180,178,68,111,143,40,195,59,240,0,131,31,50,96,137,146,104,114,146,201,44,212,165,236,63,141,235,195,74,230,208,70,151,91,115,163,28,103,101,194,72,81,128,133,58,60,210,199,206,91,58,107,201,119,146,79,73,245,172,175,236,49,119,165,138,13,64,97,211,166,53,67,105,132,34,167,80,72,176,137,206,241,34,195,23,129,56,118,155,71,75,63,165,132,99,189,72,244,47,246,228,233,247,122,206,93,247,7,152,165,96,177,16,193,185,231,34,25,108,156,82,48,255,196,244,244,19,194,148,65,8,206,163,198,66,171,217,133,48,241,218,204,111,42,49,182,120,211,68,17,118,31,25,151,68,33,191,98,200,250,150,212,165,25,57,193,149,60,42,75,166,229,35,209,177,238,206,98,175,27,249,33,86,105,84,161,179,85,140,210,192,60,5,154,71,97,35,145,68,43,129,204,230,65,55,225,230,140,33,179,109,189,138,48,32,208,33,172,177,59,64,6,163,157,173,28,251,108,135,107,8,121,39,70,182,92,118,88,74,123,227,173,148,114,232,141,165,8,174,233,72,47,98,166,229,126,163,92,128,124,93,240,40,16,129,188,248,249,141,68,50,46,112,119,83,159,1,36,188,29,108,12,72,193,168,191,20,181,224,21,171,122,118,241,53,134,128,172,191,216,59,171,163,169,80,167,131,162,77,124,75,148,14,173,54,172,186,138,121,182,62,79,166,255,80,64,84,10,35,159,137,202,138,66,88,88,38,13,89,3,158,80,192,80,50,47,239,72,206,116,135,12,251,40,25,201,123,163,67,100,13,105,51,174,34,34,93,3,115,92,192,106,231,63,31,87,218,156,115,83,1,188,172,152,212,206,133,105,183,160,198,113,51,32,213,29,130,254,173,239,245,104,126,255,101,127,115,186,56,236,11,217,178,248,114,215,168,163,134,209,19,244,68,229,206,194,29,94,160,221,137,217,92,17,83,227,202,123,155,151,121,220,191,199,202,241,205,234,70,164,104,185,211,96,128,163,65,230,28,4,63,219,134,230,237,183,181,229,109,41,192,38,48,3,205,3,69,15,27,85,41,249,58,60,229,132,70,199,19,39,247,213,215,10,64,234,230,209,250,91,21,134,94,143,43,27,92,64,200,3,206,207,147,130,194,137,110,114,158,192,62,200,9,173,197,36,195,222,90,68,139,0,117,232,231,62,93,112,14,37,53,86,17,150,199,31,117,20,200,185,21,58,0,193,20,229,210,41,161,119,53,225,179,50,59,120,212,104,243,159,148,20,197,167,106,194,135,192,98,133,68,210,210,227,227,161,183,180,36,62,92,189,50,39,122,5,177,246,255,185,32,70,105,173,233,213,61,174,111,102,212,174,40,98,96,113,237,146,163,30,186,69,139,67,157,37,83,30,161,170,92,20,88,126,31,148,49,32,133,35,206,117,26,155,154,102,119,184,196,144,146,104,188,112,247,229,124,82,237,146,81,215,157,134,60,215,44,249,56,157,202,243,236,105,6,130,117,190,131,93,11,78,190,251,41,176,233,229,147,70,59,56,145,165,174,147,111,242,160,241,201,203,118,88,132,232,178,22,117,168,133,225,76,86,240,4,246,158,51,191,121,245,52,164,72,112,109,131,246,21,37,123,166,143,107,76,69,128,184,87,200,23,39,48,21,138,248,161,140,8,151,174,127,172,55,217,205,164,191,41,40,21,65,102,38,231,245,193,137,82,79,200,219,185,83,168,181,87,125,247,47,112,61,195,99,115,20,131,186,193,11,9,106,72,43,175,52,216,11,212,247,27,151,35,162,53,237,20,208,201,47,218,30,26,86,4,185,26,142,255,250,59,219,212,222,152,155,131,117,49,122,51,2,78,187,225,215,90,99,196,255,71,7,19,218,205,190,132,213,178,193,114,94,81,104,244,138,51,254,116,38,23,73,190,214,169,31,220,41,172,172,205,57,159,93,237,199,135,219,63,101,42,222,90,192,55,33,98,171,182,18,47,63,214,206,6,76,251,28,178,150,174,154,63,127,87,170,172,107,230,55,12,253,165,15,70,55,191,80,114,158,203,222,55,160,212,2,147,59,21,136,61,126,64,193,123,132,221,255,44,162,133,94,13,143,204,9,115,165,105,81,111,100,16,20,44,80,204,209,171,181,144,67,195,225,253,33,209,171,193,155,183,25,142,1Line truncated
+548 -5
View File
@@ -64,6 +64,12 @@ TEST(OtaPolicy, TrustedAutoInstallIsStrictlyForwardOnly) {
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);
@@ -461,6 +467,7 @@ TEST(OtaBootPackage, ParserSeparatesV2ApplicationsFromStrictV3Bootloader) {
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;
@@ -469,6 +476,8 @@ TEST(OtaBootPackage, ParserSeparatesV2ApplicationsFromStrictV3Bootloader) {
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;
@@ -1098,6 +1107,22 @@ TEST(OtaParse, RejectsTooManyBlocks) {
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));
@@ -1680,12 +1705,21 @@ TEST(OtaProtocol, CodecRoundTrips) {
EXPECT_EQ(0, memcmp(pm2.proof, proof, 12));
}
TEST(OtaProtocol, V2ProfilePacks171ByteFragmentsInsideExistingMessages) {
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_is_v2((uint16_t)(request | OTA_REQ_V2_RESERVED)));
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;
@@ -1697,10 +1731,44 @@ TEST(OtaProtocol, V2ProfilePacks171ByteFragmentsInsideExistingMessages) {
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(),
@@ -1724,6 +1792,57 @@ 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);
@@ -1783,6 +1902,67 @@ static bool test_wire_decode(void* context, const uint8_t* source, uint16_t sour
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) {
@@ -2000,10 +2180,11 @@ TEST(OtaTransfer, TwoManagersFullTransfer) {
EXPECT_TRUE(mota_check_image_hash_full(m));
}
TEST(OtaTransfer, TransportCodecInflatesBeforeProofAndStagesOriginalContainer) {
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};
@@ -2030,6 +2211,78 @@ TEST(OtaTransfer, TransportCodecInflatesBeforeProofAndStagesOriginalContainer) {
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];
@@ -2103,7 +2356,11 @@ static void deliver_verified_v2_raw_block(OtaManager& client, const MotaManifest
DataMsg data{};
memcpy(data.manifest_id, manifest.merkle_root, 4);
data.block_idx = (uint16_t)block;
ASSERT_TRUE(ota_data_v2_pack((uint8_t)fragment, block_len, false, data.frag_off));
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);
@@ -2124,6 +2381,65 @@ static void deliver_verified_v2_raw_block(OtaManager& client, const MotaManifest
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));
@@ -2177,6 +2493,59 @@ TEST(OtaTransfer, ServerPacesOneKilobyteBlockAndProactiveProofWithBackpressure)
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));
@@ -2498,6 +2867,55 @@ TEST(OtaTransfer, NewClientRequestsV2ThenRetriesLegacyWhenNoV2DataArrives) {
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));
@@ -2597,6 +3015,78 @@ TEST(OtaTransfer, PartialV2SeederCanDisappearAndLegacySeederTakesOver) {
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));
@@ -2723,6 +3213,59 @@ TEST(OtaTransfer, V2NeverMixesDifferentCompressedRepresentationsAcrossSeeders) {
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));
@@ -3244,7 +3787,7 @@ TEST(OtaCatalog, RetainsProtocolMaximumRows) {
TEST(OtaCatalog, RejectsAdvertisedSourceWithOversizedBlocks) {
OtaManager server;
server.begin(0, nullptr, nullptr);
SyntheticCatalogSource source(1, 11); // 2048-byte blocks exceed OTA_MAX_BLOCK
SyntheticCatalogSource source(1, 12); // 4096-byte blocks exceed OTA_MAX_BLOCK
ASSERT_TRUE(server.add_source(&source));
EXPECT_EQ(server.servedCount(), 0);
}