// Identifier-width wildcard matrix. // // SEMI E5 documents identifier fields (DATAID, RPTID, VID, CEID, ALID, // EXID, OBJID, …) as "U1 | U2 | U4 | U8" — peers may pick the smallest // width that fits the value. Our parsers route through // `any_unsigned_first` (messages_helpers.hpp) which accepts any // unsigned format and range-checks the cast. // // This test enumerates every (target_type, source_width, value) combo // and asserts the cross-width parsing is symmetric. Without this, a // future code-gen change that hard-codes a specific width on a given // field would silently regress wire compatibility (this regression is // the original bug behind the U1-encoded ALID interop incident with // secsgem-py 0.3.0, captured in [[secsgem_interop_findings]]). #include #include #include #include "secsgem/gem/messages_helpers.hpp" #include "secsgem/secs2/codec.hpp" #include "secsgem/secs2/item.hpp" using namespace secsgem::gem; namespace s2 = secsgem::secs2; // Build an Item at every unsigned width that can hold `value`. static std::vector all_widths(uint64_t value) { std::vector out; if (value <= 0xFF) out.push_back(s2::Item::u1(static_cast(value))); if (value <= 0xFFFF) out.push_back(s2::Item::u2(static_cast(value))); if (value <= 0xFFFFFFFFu) out.push_back(s2::Item::u4(static_cast(value))); out.push_back(s2::Item::u8(value)); return out; } TEST_CASE("identifier wildcard: as_u4_scalar accepts U1/U2/U4 encodings of small values") { // The U1 case is the historic interop bug. for (uint64_t value : {uint64_t{0}, uint64_t{1}, uint64_t{42}, uint64_t{255}}) { for (const auto& item : all_widths(value)) { auto got = as_u4_scalar(item); REQUIRE(got.has_value()); CHECK(*got == value); } } } TEST_CASE("identifier wildcard: as_u8_scalar accepts every width") { for (uint64_t value : {uint64_t{0}, uint64_t{1}, uint64_t{255}, uint64_t{65535}, uint64_t{4294967295u}, uint64_t{1ULL << 40}}) { for (const auto& item : all_widths(value)) { auto got = as_u8_scalar(item); REQUIRE(got.has_value()); CHECK(*got == value); } } } TEST_CASE("identifier wildcard: as_u1_scalar accepts wider widths if value fits") { // U2/U4/U8 encodings of small values should still parse as U1 via // range-checked downcast. This is the symmetric case to the U4 bug. for (uint64_t value : {uint64_t{0}, uint64_t{1}, uint64_t{200}, uint64_t{255}}) { for (const auto& item : all_widths(value)) { auto got = as_u1_scalar(item); REQUIRE(got.has_value()); CHECK(*got == static_cast(value)); } } } TEST_CASE("identifier wildcard: as_u1_scalar rejects values that exceed its range") { // 256 fits U2 but not U1 — the downcast must signal failure rather // than truncate. auto u2_val = s2::Item::u2(uint16_t{256}); CHECK_FALSE(as_u1_scalar(u2_val).has_value()); auto u4_val = s2::Item::u4(uint32_t{0x1FFFFu}); CHECK_FALSE(as_u1_scalar(u4_val).has_value()); CHECK_FALSE(as_u2_scalar(u4_val).has_value()); } TEST_CASE("identifier wildcard: round-trip through codec preserves value across widths") { // The byte-level concern: an encoded U1 must decode back to a U1 // Item; the scalar accessor must still extract the value. This // proves the bug isn't reintroduced when frames travel through the // full codec rather than via in-memory Items. for (uint64_t value : {uint64_t{0}, uint64_t{1}, uint64_t{42}, uint64_t{12345}}) { for (const auto& item : all_widths(value)) { auto bytes = s2::encode(item); auto decoded = s2::decode(bytes); CHECK(decoded.format() == item.format()); auto got = as_u8_scalar(decoded); REQUIRE(got.has_value()); CHECK(*got == value); } } } TEST_CASE("identifier wildcard: I1/I2/I4/I8 same lenient pattern") { // The signed variants follow the same wildcard rule per E5. for (int64_t value : {int64_t{0}, int64_t{1}, int64_t{-1}, int64_t{42}, int64_t{-128}, int64_t{127}}) { s2::Item item = s2::Item::i1(static_cast(value)); auto got = as_i4_scalar(item); REQUIRE(got.has_value()); CHECK(*got == value); } for (int64_t value : {int64_t{-32768}, int64_t{0}, int64_t{32767}}) { s2::Item item = s2::Item::i2(static_cast(value)); auto got = as_i4_scalar(item); REQUIRE(got.has_value()); CHECK(*got == value); } }