#include #include #include #include #include #include #include #include using namespace mln; namespace { constexpr size_t Header = 112; constexpr size_t Descriptor = 32; void u16(std::vector& b, size_t o, uint16_t v) { b[o] = uint8_t(v); b[o + 1] = uint8_t(v >> 8); } void u32(std::vector& b, size_t o, uint32_t v) { for (size_t i = 0; i < 4; ++i) b[o + i] = uint8_t(v >> (8 * i)); } uint32_t r32(const std::vector& b, size_t o) { uint32_t v = 0; for (size_t i = 0; i < 4; ++i) v |= uint32_t(b[o + i]) << (8 * i); return v; } void u64(std::vector& b, size_t o, uint64_t v) { for (size_t i = 0; i < 8; ++i) b[o + i] = uint8_t(v >> (8 * i)); } void i64(std::vector& b, size_t o, int64_t v) { u64(b, o, static_cast(v)); } void f32(std::vector& b, size_t o, float v) { uint32_t bits = 0; std::memcpy(&bits, &v, sizeof(bits)); u32(b, o, bits); } void f64(std::vector& b, size_t o, double v) { uint64_t bits = 0; std::memcpy(&bits, &v, sizeof(bits)); u64(b, o, bits); } size_t a16(size_t x) { return (x + 15u) & ~size_t(15u); } uint32_t crc(const std::vector& b) { uint32_t c = ~0u; for (size_t i = 0; i < b.size(); ++i) { const uint8_t x = i >= 76 && i < 80 ? 0 : b[i]; c ^= x; for (int j = 0; j < 8; ++j) c = (c >> 1) ^ ((c & 1u) ? 0xedb88320u : 0); } return ~c; } void recalc(std::vector& b) { u32(b, 76, crc(b)); } bool validAt(const std::vector& v, uint64_t i) { return ((v[i >> 3] >> (i & 7u)) & 1u) != 0; } int64_t normDir(int64_t x, uint32_t n) { if (x < 0) x += n; else if (x >= int64_t(n)) x -= n; return x; } uint64_t zigzag(int64_t r) { return r >= 0 ? uint64_t(2 * r) : uint64_t(-2 * r - 1); } struct Bits { std::vector bytes; size_t pos = 0; void bit(uint8_t value) { if ((pos & 7u) == 0) bytes.push_back(0); if (value) bytes.back() |= uint8_t(1u << (pos & 7u)); ++pos; } void bits(uint32_t value, uint8_t count) { for (uint8_t i = 0; i < count; ++i) bit((value >> i) & 1u); } }; struct PlaneSpec { uint16_t semantic = 1; uint8_t dtype = 1; uint8_t flags = 1; float scale = 1.0f; float offset = 0.0f; uint16_t unit = 0; uint16_t group = 0; uint8_t valueClass = 0; uint8_t component = 0; std::vector values; }; int64_t predict(const PlaneSpec& p, const std::vector& validity, uint32_t width, uint32_t row, uint32_t col, uint32_t n) { const uint64_t i = uint64_t(row) * width + col; const bool left = col > 0 && validAt(validity, i - 1); const bool up = row > 0 && validAt(validity, i - width); const bool upperLeft = row > 0 && col > 0 && validAt(validity, i - width - 1); int64_t predicted = 0; if (left && up && upperLeft) { predicted = p.values[i - 1] + p.values[i - width] - p.values[i - width - 1]; } else if (left) { predicted = p.values[i - 1]; } else if (up) { predicted = p.values[i - width]; } return n ? normDir(predicted, n) : predicted; } std::vector encodePlane(const PlaneSpec& p, const std::vector& validity, uint32_t width, uint32_t height, std::optional forcedK = std::nullopt) { const uint32_t n = p.valueClass == 2 ? uint32_t(std::floor(360.0 / double(p.scale) + 0.5)) : 0; std::vector out; for (uint32_t rb = 0; rb < height; rb += 16) { for (uint32_t cb = 0; cb < width; cb += 16) { std::vector symbols; for (uint32_t row = rb; row < std::min(rb + 16u, height); ++row) { for (uint32_t col = cb; col < std::min(cb + 16u, width); ++col) { const uint64_t i = uint64_t(row) * width + col; if (!validAt(validity, i)) continue; const int64_t predicted = predict(p, validity, width, row, col, n); int64_t residual = p.values[i] - predicted; if (n) { int64_t m = residual; if (m < 0) m += n; residual = 2 * m > int64_t(n) ? m - n : 2 * m < int64_t(n) ? m : -int64_t(n / 2); } symbols.push_back(zigzag(residual)); } } if (symbols.empty()) continue; uint8_t bestK = forcedK.value_or(0); if (!forcedK) { uint64_t bestBits = ~uint64_t(0); for (uint8_t k = 0; k <= 31; ++k) { uint64_t bits = 0; for (const auto z : symbols) bits += (z >> k) + 1u + k; if (bits < bestBits) { bestBits = bits; bestK = k; } } } out.push_back(bestK); Bits writer; for (const auto z : symbols) { const uint64_t q = z >> bestK; for (uint64_t j = 0; j < q; ++j) writer.bit(0); writer.bit(1); if (bestK) writer.bits(uint32_t(z & ((uint64_t(1) << bestK) - 1u)), bestK); } out.insert(out.end(), writer.bytes.begin(), writer.bytes.end()); } } return out; } std::vector rawBytes(const PlaneSpec& p, const std::vector& validity) { const size_t stride = p.dtype == 1 || p.dtype == 2 ? 2u : 1u; std::vector out(p.values.size() * stride, 0); for (size_t i = 0; i < p.values.size(); ++i) { if (!validAt(validity, i)) continue; if (stride == 2) { const uint16_t v = static_cast(p.values[i]); out[i * 2] = uint8_t(v); out[i * 2 + 1] = uint8_t(v >> 8); } else { out[i] = static_cast(p.values[i]); } } return out; } std::vector fixture(uint32_t width, uint32_t height, std::vector validity, std::vector planes, std::optional forcedK = std::nullopt, uint16_t fieldType = 999) { const uint64_t cells = uint64_t(width) * height; const uint16_t count = uint16_t(planes.size() + 1); const size_t payloadOffset = Header + size_t(count) * Descriptor; struct Encoded { PlaneSpec p; std::vector bytes; }; std::vector encoded; for (auto& p : planes) encoded.push_back({p, encodePlane(p, validity, width, height, forcedK)}); size_t payloadLength = 0; for (const auto& e : encoded) payloadLength = a16(payloadLength + e.bytes.size()); payloadLength = a16(payloadLength + validity.size()); std::vector b(payloadOffset + payloadLength, 0); std::memcpy(b.data(), "QBV3", 4); b[4] = 3; b[5] = 1; u16(b, 6, fieldType); b[8] = b[9] = b[10] = 1; u16(b, 12, count); u32(b, 16, width); u32(b, 20, height); i64(b, 24, 100); i64(b, 32, 90); f64(b, 40, -10); f64(b, 48, -10); f64(b, 56, 10); f64(b, 64, 10); u32(b, 72, payloadLength); u16(b, 80, 0xffff); size_t off = 0; for (size_t i = 0; i < encoded.size(); ++i) { const auto& e = encoded[i]; const size_t d = Header + i * Descriptor; u16(b, d, e.p.semantic); b[d + 2] = e.p.dtype; b[d + 3] = e.p.flags; f32(b, d + 4, e.p.scale); f32(b, d + 8, e.p.offset); u32(b, d + 12, cells); u32(b, d + 16, off); u32(b, d + 20, e.bytes.size()); u16(b, d + 24, e.p.unit); u16(b, d + 26, e.p.group); b[d + 28] = e.p.valueClass; b[d + 29] = e.p.component; std::copy(e.bytes.begin(), e.bytes.end(), b.begin() + payloadOffset + off); off = a16(off + e.bytes.size()); } const size_t d = Header + encoded.size() * Descriptor; u16(b, d, 3); b[d + 2] = 6; b[d + 3] = 1; f32(b, d + 4, 1.0f); f32(b, d + 8, 0.0f); u32(b, d + 12, cells); u32(b, d + 16, off); u32(b, d + 20, validity.size()); b[d + 28] = 3; std::copy(validity.begin(), validity.end(), b.begin() + payloadOffset + off); recalc(b); return b; } std::vector fixtureV2(uint32_t width, uint32_t height, const std::vector& validity, const std::vector& planes, uint16_t fieldType = 999) { const uint64_t cells = uint64_t(width) * height; const uint16_t count = uint16_t(planes.size() + 1); const size_t payloadOffset = Header + size_t(count) * Descriptor; struct RawPlane { PlaneSpec p; std::vector bytes; }; std::vector raws; for (const auto& p : planes) raws.push_back({p, rawBytes(p, validity)}); size_t payloadLength = 0; for (const auto& r : raws) payloadLength = a16(payloadLength + r.bytes.size()); payloadLength = a16(payloadLength + validity.size()); std::vector b(payloadOffset + payloadLength, 0); std::memcpy(b.data(), "QBV2", 4); b[4] = 2; b[5] = 1; u16(b, 6, fieldType); b[8] = b[9] = b[10] = 1; u16(b, 12, count); u32(b, 16, width); u32(b, 20, height); i64(b, 24, 100); i64(b, 32, 90); f64(b, 40, -10); f64(b, 48, -10); f64(b, 56, 10); f64(b, 64, 10); u32(b, 72, payloadLength); u16(b, 80, 0xffff); size_t off = 0; for (size_t i = 0; i < raws.size(); ++i) { const auto& r = raws[i]; const size_t d = Header + i * Descriptor; u16(b, d, r.p.semantic); b[d + 2] = r.p.dtype; b[d + 3] = r.p.flags; f32(b, d + 4, r.p.scale); f32(b, d + 8, r.p.offset); u32(b, d + 12, cells); u32(b, d + 16, off); u32(b, d + 20, r.bytes.size()); u16(b, d + 24, r.p.unit); u16(b, d + 26, r.p.group); b[d + 28] = r.p.valueClass; b[d + 29] = r.p.component; std::copy(r.bytes.begin(), r.bytes.end(), b.begin() + payloadOffset + off); off = a16(off + r.bytes.size()); } const size_t d = Header + raws.size() * Descriptor; u16(b, d, 3); b[d + 2] = 6; b[d + 3] = 1; f32(b, d + 4, 1.0f); f32(b, d + 8, 0.0f); u32(b, d + 12, cells); u32(b, d + 16, off); u32(b, d + 20, validity.size()); b[d + 28] = 3; std::copy(validity.begin(), validity.end(), b.begin() + payloadOffset + off); recalc(b); return b; } std::vector validityBits(uint32_t cells, bool allValid = true) { std::vector v((cells + 7u) / 8u, allValid ? 0xffu : 0u); if (allValid && (cells & 7u)) v.back() = uint8_t((1u << (cells & 7u)) - 1u); return v; } particle::ingress::EncodedFieldRuntimeMetadata meta() { return {"qbv3-test", 1, 1}; } using Code = particle::ingress::QbinDecodeErrorCode; } // namespace TEST(ParticleQbinV3Decoder, DecodesAcrossBlockBoundaryAndDirectionWrap) { constexpr uint32_t width = 17, height = 17, cells = width * height; auto validity = validityBits(cells); validity[3] &= uint8_t(~(1u << 1)); // one invalid cell std::vector scalar(cells), direction(cells); for (uint32_t r = 0; r < height; ++r) for (uint32_t c = 0; c < width; ++c) { const size_t i = r * width + c; scalar[i] = int64_t(r) * 40 - int64_t(c) * 7; direction[i] = (350 + int64_t(c) * 3 + int64_t(r) * 11) % 360; } PlaneSpec a{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, scalar}; PlaneSpec d{2, 2, 1, 1.0f, 0.0f, 4, 0, 2, 0, direction}; auto bytes = fixture(width, height, validity, {a, d}); auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes); ASSERT_TRUE(result.accepted()) << result.detail; ASSERT_EQ(result.frame->planes.size(), 2u); EXPECT_EQ(*result.frame->planes[0].storage, rawBytes(a, validity)); EXPECT_EQ(*result.frame->planes[1].storage, rawBytes(d, validity)); EXPECT_EQ(*result.frame->validityBits, validity); } TEST(ParticleQbinV3Decoder, AcceptsAllInvalidWithSharedZeroLengthOffsets) { constexpr uint32_t width = 2, height = 2, cells = 4; auto validity = validityBits(cells, false); PlaneSpec a{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1,2,3,4}}; PlaneSpec d{2, 2, 1, 1.0f, 0.0f, 4, 0, 2, 0, {0,90,180,270}}; auto bytes = fixture(width, height, validity, {a, d}); const size_t d0 = Header, d1 = Header + Descriptor, dv = Header + 2 * Descriptor; EXPECT_EQ(r32(bytes, d0 + 16), 0u); EXPECT_EQ(r32(bytes, d1 + 16), 0u); EXPECT_EQ(r32(bytes, dv + 16), 0u); auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes); ASSERT_TRUE(result.accepted()) << result.detail; EXPECT_EQ(*result.frame->planes[0].storage, std::vector(cells * 2, 0)); EXPECT_EQ(*result.frame->planes[1].storage, std::vector(cells * 2, 0)); } TEST(ParticleQbinV3Decoder, AcceptsNonOptimalRiceK) { auto validity = validityBits(4); PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0,0,0,0}}; auto bytes = fixture(2, 2, validity, {p}, uint8_t(5)); auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes); EXPECT_TRUE(result.accepted()) << result.detail; } TEST(ParticleQbinV3Decoder, RejectsCrcDescriptorAndValidityCorruption) { auto validity = validityBits(4); PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1,2,3,4}}; auto bytes = fixture(2, 2, validity, {p}); bytes[6] ^= 1; EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::CrcMismatch); bytes = fixture(2, 2, validity, {p}); bytes[Header + 2] = 5; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidDescriptor); bytes = fixture(2, 2, validity, {p}); const size_t validityDescriptor = Header + Descriptor; bytes[validityDescriptor + 2] = 3; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidValidity); } TEST(ParticleQbinV3Decoder, RejectsMalformedRiceAndNonzeroBlockPadding) { auto validity = validityBits(4); PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0,0,0,0}}; auto bytes = fixture(2, 2, validity, {p}); const size_t payload = Header + 2 * Descriptor; bytes[payload] = 32; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice); auto oneValid = validityBits(4, false); oneValid[0] = 0x01; bytes = fixture(2, 2, oneValid, {p}, uint8_t(0)); bytes[payload + 1] |= 0x80; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice); } TEST(ParticleQbinV3Decoder, RejectsResidualOutsideLogicalRange) { auto validity = validityBits(4); PlaneSpec p{1, 3, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0,0,0,0}}; auto bytes = fixture(2, 2, validity, {p}, uint8_t(31)); const size_t payload = Header + 2 * Descriptor; ASSERT_GT(bytes.size(), payload + 4); bytes[payload + 4] |= 0x80; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::ResidualOutOfRange); } TEST(ParticleQbinV3Decoder, PreservesNodeRegisteredGridGeometry) { auto validity = validityBits(9); PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, std::vector(9, 0)}; auto bytes = fixture(3, 3, validity, {p}); auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes); ASSERT_TRUE(result.accepted()) << result.detail; const auto& g = result.frame->grid; EXPECT_EQ(g.width, 3u); EXPECT_EQ(g.height, 3u); EXPECT_DOUBLE_EQ(g.west, -10.0); EXPECT_DOUBLE_EQ(g.south, -10.0); EXPECT_DOUBLE_EQ(g.east, 10.0); EXPECT_DOUBLE_EQ(g.north, 10.0); EXPECT_DOUBLE_EQ(g.west + (g.east - g.west) * 1.0 / (g.width - 1), 0.0); EXPECT_DOUBLE_EQ(g.south + (g.north - g.south) * 1.0 / (g.height - 1), 0.0); } TEST(ParticleEncodedFieldIngress, DispatchesQbinV3AndRejectsUnknownMagic) { auto validity = validityBits(4); PlaneSpec u{1, 1, 1, 1.0f, 0.0f, 1, 1, 1, 1, {1,2,3,4}}; PlaneSpec v{2, 1, 1, 1.0f, 0.0f, 1, 1, 1, 2, {5,6,7,8}}; auto bytes = fixture(2, 2, validity, {u, v}); particle::ResolvedFieldProfileContract contract; contract.profileID = "qbv3.wind.test"; contract.profileVersion = 1; contract.requiredPlanes = { {1, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::UEast, true}, {2, particle::FieldValueClass::VelocityComponent, 1, 1, particle::FieldComponent::VNorth, true}, {3, particle::FieldValueClass::Validity, 0, 0, particle::FieldComponent::None, true}, }; auto accepted = particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(), &contract, bytes); EXPECT_TRUE(accepted.accepted()) << accepted.detail; bytes[0] = 'X'; recalc(bytes); auto rejected = particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(), &contract, bytes); EXPECT_EQ(rejected.code, particle::ingress::EncodedFieldIngressCode::DecodeRejected); EXPECT_EQ(rejected.decodeCode, Code::BadMagic); } TEST(ParticleQbinV3Decoder, CoversAllCompressedIntegerDtypes) { auto validity = validityBits(4); PlaneSpec u8{1, 3, 1, 0.5f, 1.0f, 77, 0, 0, 0, {0, 255, 128, 1}}; PlaneSpec i8{2, 4, 1, 2.0f, -5.0f, 78, 0, 0, 0, {-128, 127, -1, 0}}; auto bytes = fixture(2, 2, validity, {u8, i8}); auto result = particle::ingress::QbinV3Decoder::decode(meta(), bytes); ASSERT_TRUE(result.accepted()) << result.detail; ASSERT_EQ(result.frame->planes.size(), 2u); EXPECT_EQ(*result.frame->planes[0].storage, rawBytes(u8, validity)); EXPECT_EQ(*result.frame->planes[1].storage, rawBytes(i8, validity)); } TEST(ParticleQbinV3Decoder, AcceptsOpaqueFieldTypeZeroAndUnknown) { auto validity = validityBits(4); PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1, 2, 3, 4}}; EXPECT_TRUE(particle::ingress::QbinV3Decoder::decode(meta(), fixture(2, 2, validity, {p}, std::nullopt, 0)).accepted()); EXPECT_TRUE(particle::ingress::QbinV3Decoder::decode(meta(), fixture(2, 2, validity, {p}, std::nullopt, 65535)).accepted()); } TEST(ParticleQbinV3Decoder, RejectsHeaderVersionEndianReservedBoundsAndNegativeZero) { auto validity = validityBits(4); PlaneSpec p{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {1, 2, 3, 4}}; auto bytes = fixture(2, 2, validity, {p}); bytes[0] = 'X'; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::BadMagic); bytes = fixture(2, 2, validity, {p}); bytes[4] = 2; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::UnsupportedVersion); bytes = fixture(2, 2, validity, {p}); bytes[11] = 1; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::ReservedNonZero); bytes = fixture(2, 2, validity, {p}); bytes[16] = 0; bytes[17] = 0; bytes[18] = 0; bytes[19] = 2; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::LimitExceeded); bytes = fixture(2, 2, validity, {p}); f64(bytes, 40, 11.0); recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidBounds); bytes = fixture(2, 2, validity, {p}); u64(bytes, 40, 0x8000000000000000ull); recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidBounds); } TEST(ParticleQbinV3Decoder, RejectsDescriptorOrderDuplicateAndPacking) { auto validity = validityBits(4); PlaneSpec u{1, 1, 1, 1.0f, 0.0f, 1, 1, 1, 1, {1, 2, 3, 4}}; PlaneSpec v{2, 1, 1, 1.0f, 0.0f, 1, 1, 1, 2, {5, 6, 7, 8}}; auto bytes = fixture(2, 2, validity, {u, v}); u16(bytes, Header, 2); u16(bytes, Header + Descriptor, 1); recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::NonCanonicalDescriptorOrder); bytes = fixture(2, 2, validity, {u, v}); u16(bytes, Header + Descriptor, 1); recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::DuplicatePlaneKey); bytes = fixture(2, 2, validity, {u, v}); u32(bytes, Header + Descriptor + 16, 0); recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::NonCanonicalPacking); } TEST(ParticleQbinV3Decoder, RejectsUnterminatedUnaryExtraEncodedByteAndRawOverflow) { auto validity = validityBits(4); PlaneSpec zero{1, 1, 1, 1.0f, 0.0f, 77, 0, 0, 0, {0, 0, 0, 0}}; const size_t payload = Header + 2 * Descriptor; auto bytes = fixture(2, 2, validity, {zero}, uint8_t(0)); bytes[payload + 1] = 0; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice); bytes = fixture(2, 2, validity, {zero}, uint8_t(0)); const uint32_t length = r32(bytes, Header + 20); ASSERT_LT(payload + length, bytes.size()); u32(bytes, Header + 20, length + 1); bytes[payload + length] = 0; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::MalformedRice); PlaneSpec overflow{1, 3, 1, 1.0f, 0.0f, 77, 0, 0, 0, {300, 0, 0, 0}}; bytes = fixture(2, 2, validity, {overflow}); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::InvalidCellPayload); } TEST(ParticleQbinV3Decoder, EnforcesDirectionHalfCircleNegativeTie) { auto validity = validityBits(4); PlaneSpec direction{1, 2, 1, 90.0f, 0.0f, 4, 0, 2, 0, {0, 2, 0, 2}}; auto bytes = fixture(2, 2, validity, {direction}, uint8_t(0)); auto canonical = particle::ingress::QbinV3Decoder::decode(meta(), bytes); ASSERT_TRUE(canonical.accepted()) << canonical.detail; const size_t payload = Header + 2 * Descriptor; ASSERT_EQ(r32(bytes, Header + 20), 2u); // canonical bits are 0x71: 0, -2, 0, 0. Replace the -2 symbol // with the forbidden +2 half-circle representation (z=4), yielding 0xe1. bytes[payload + 1] = 0xe1; recalc(bytes); EXPECT_EQ(particle::ingress::QbinV3Decoder::decode(meta(), bytes).code, Code::ResidualOutOfRange); } TEST(ParticleQbinV3Decoder, ContentIdentityMatchesQbinV2CanonicalFrame) { auto validity = validityBits(4); PlaneSpec u{1, 1, 1, 0.25f, 0.0f, 1, 1, 1, 1, {1, -2, 3, -4}}; PlaneSpec v{2, 1, 1, 0.25f, 0.0f, 1, 1, 1, 2, {5, 6, -7, 8}}; auto v2 = particle::ingress::QbinV2Decoder::decode(meta(), fixtureV2(2, 2, validity, {u, v})); auto v3 = particle::ingress::QbinV3Decoder::decode(meta(), fixture(2, 2, validity, {u, v})); ASSERT_TRUE(v2.accepted()) << v2.detail; ASSERT_TRUE(v3.accepted()) << v3.detail; ASSERT_EQ(v2.frame->planes.size(), v3.frame->planes.size()); for (size_t i = 0; i < v2.frame->planes.size(); ++i) { EXPECT_EQ(*v2.frame->planes[i].storage, *v3.frame->planes[i].storage); EXPECT_EQ(v2.frame->planes[i].contentIdentity, v3.frame->planes[i].contentIdentity); } EXPECT_EQ(*v2.frame->validityBits, *v3.frame->validityBits); EXPECT_EQ(v2.frame->validityContentIdentity, v3.frame->validityContentIdentity); }