Files
MapLibreParticleLab/test/particle/qbin_v2_decoder.test.cpp

103 lines
16 KiB
C++

#include <mbgl/particle/ingress/qbin_v2_decoder.hpp>
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include <vector>
using namespace mbgl;
namespace {
constexpr size_t Header = 112, Descriptor = 32;
void u16(std::vector<uint8_t>& b, size_t o, uint16_t v) { b[o] = uint8_t(v); b[o + 1] = uint8_t(v >> 8); }
void u32(std::vector<uint8_t>& b, size_t o, uint32_t v) { for (size_t i = 0; i < 4; ++i) b[o + i] = uint8_t(v >> (8 * i)); }
void u64(std::vector<uint8_t>& 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<uint8_t>& b, size_t o, int64_t v) { u64(b, o, static_cast<uint64_t>(v)); }
void f32(std::vector<uint8_t>& b, size_t o, float v) { uint32_t bits; std::memcpy(&bits, &v, sizeof(bits)); u32(b, o, bits); }
void f64(std::vector<uint8_t>& b, size_t o, double v) { uint64_t bits; std::memcpy(&bits, &v, sizeof(bits)); u64(b, o, bits); }
size_t a16(size_t x) { return (x + 15) & ~size_t(15); }
uint32_t crc(const std::vector<uint8_t>& b) { uint32_t c = ~0u; for (size_t i = 0; i < b.size(); ++i) { uint8_t x = i >= 76 && i < 80 ? 0 : b[i]; c ^= x; for (int j = 0; j < 8; ++j) c = (c >> 1) ^ ((c & 1) ? 0xedb88320u : 0); } return ~c; }
void recalc(std::vector<uint8_t>& b) { u32(b, 76, crc(b)); }
struct P { uint16_t semantic = 1, unit = 1, group = 1; uint8_t type = 1, flags = 1, vc = 1, component = 1; float scale = 1, offset = 0; std::vector<uint8_t> raw; };
std::vector<P> basePlanes() { return {{1,1,1,1,1,1,1,1,0,{1,0,2,0,3,0,4,0}}, {2,1,1,1,1,1,2,1,0,{5,0,6,0,7,0,8,0}}, {3,0,0,6,1,3,0,1,0,{15}}}; }
size_t rawLength(uint8_t type, uint64_t cells) { return type == 6 ? (cells + 7) / 8 : (type == 1 || type == 2 ? cells * 2 : type == 5 ? cells * 4 : cells); }
std::vector<uint8_t> fixture(uint32_t w = 2, uint32_t h = 2, std::vector<P> ps = {}) {
if (ps.empty()) ps = basePlanes(); const uint64_t cells = uint64_t(w) * h;
for (auto& p : ps) if (p.raw.empty() || cells != 4) { p.raw.assign(rawLength(p.type, cells), 0); if (p.type == 6) { std::fill(p.raw.begin(), p.raw.end(), 0xff); if (cells % 8) p.raw.back() = uint8_t((1u << (cells % 8)) - 1); } }
size_t payload = 0; for (const auto& p : ps) payload = a16(payload + p.raw.size()); const size_t po = Header + ps.size() * Descriptor; std::vector<uint8_t> b(po + payload);
std::memcpy(b.data(), "QBV2", 4); b[4] = 2; b[5] = 1; u16(b, 6, 999); b[8] = b[9] = b[10] = 1; u16(b, 12, ps.size()); u32(b, 16, w); u32(b, 20, h); 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, payload); u16(b, 80, 0xffff);
size_t off = 0; for (size_t i = 0; i < ps.size(); ++i) { const auto& p = ps[i]; const size_t d = Header + i * Descriptor; u16(b,d,p.semantic); b[d+2]=p.type; b[d+3]=p.flags; f32(b,d+4,p.scale); f32(b,d+8,p.offset); u32(b,d+12,cells); u32(b,d+16,off); u32(b,d+20,p.raw.size()); u16(b,d+24,p.unit); u16(b,d+26,p.group); b[d+28]=p.vc; b[d+29]=p.component; std::copy(p.raw.begin(),p.raw.end(),b.begin()+po+off); off=a16(off+p.raw.size()); }
recalc(b); return b;
}
particle::ingress::EncodedFieldRuntimeMetadata meta() { return {"field", 1, 7}; }
particle::ResolvedFieldProfileContract contract(std::vector<particle::FieldPlaneRequirement> required) { particle::ResolvedFieldProfileContract c; c.profileID="generic.test"; c.profileVersion=1; c.requiredPlanes=std::move(required); return c; }
particle::ResolvedFieldProfileContract wind() { return contract({{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}}); }
using Code = particle::ingress::QbinV2DecodeErrorCode;
Code decodeCode(const std::vector<uint8_t>& b) { return particle::ingress::QbinV2Decoder::decode(meta(), b).code; }
}
TEST(ParticleQbinV2Decoder, DecodesGenericFrameAndSharesValidity) { auto r=particle::ingress::QbinV2Decoder::decode(meta(),fixture()); ASSERT_TRUE(r.accepted())<<r.detail; EXPECT_EQ(r.frame->referenceTimeMs,100000); EXPECT_EQ(r.frame->frameContentIdentity.validTimeMs,90000); EXPECT_EQ(r.frame->planes.size(),2); EXPECT_EQ(r.frame->validityPlane.storage,r.frame->validityBits); EXPECT_TRUE(r.frame->isStructurallyValid()); }
TEST(ParticleQbinV2Decoder, AllowsUnknownScalarOpaqueUnitAndPastValidTime) { auto ps=basePlanes(); ps.push_back({99,65535,0,3,1,0,0,1,0,{1,2,3,4}}); auto r=particle::ingress::QbinV2Decoder::decode(meta(),fixture(2,2,ps)); EXPECT_TRUE(r.accepted())<<r.detail; }
TEST(ParticleQbinV2Decoder, RejectsTooSmallInputBeforeMagic) { for (const size_t size : {size_t(0), size_t(1), Header - 1}) { std::vector<uint8_t> b(size); EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(), b).code, Code::InputTooSmall) << size; } }
TEST(ParticleQbinV2Decoder, RejectsBadMagicAfterCompleteHeader) { std::vector<uint8_t> b(Header, 0); EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(meta(), b).code, Code::BadMagic); }
TEST(ParticleQbinV2Decoder, RejectsInvalidRuntimeMetadataButAllowsZeroDataVersion) { auto b=fixture(); auto empty=meta(); empty.fieldID.clear(); EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(empty, b).code, Code::InvalidRuntimeMetadata); auto zeroGeneration=meta(); zeroGeneration.generation=0; EXPECT_EQ(particle::ingress::QbinV2Decoder::decode(zeroGeneration, b).code, Code::InvalidRuntimeMetadata); auto zeroDataVersion=meta(); zeroDataVersion.dataVersion=0; EXPECT_TRUE(particle::ingress::QbinV2Decoder::decode(zeroDataVersion, b).accepted()); }
TEST(ParticleQbinV2Decoder, RejectsBigEndianHeaderWidthWithValidCrc) { auto b=fixture(); b[16]=0; b[17]=0; b[18]=0; b[19]=2; recalc(b); EXPECT_EQ(decodeCode(b), Code::LimitExceeded); }
TEST(ParticleQbinV2Decoder, RejectsMixedEndianDescriptorCountWithValidCrc) { auto b=fixture(); b[124]=0; b[125]=0; b[126]=0; b[127]=4; recalc(b); EXPECT_EQ(decodeCode(b), Code::InvalidDescriptor); }
TEST(ParticleQbinV2Decoder, RejectsHeaderVariants) { auto b=fixture(); b[4]=3; recalc(b); EXPECT_EQ(decodeCode(b),Code::UnsupportedVersion); b=fixture();b[5]=2;recalc(b);EXPECT_EQ(decodeCode(b),Code::UnsupportedLayout); b=fixture();b[8]=2;recalc(b);EXPECT_EQ(decodeCode(b),Code::UnsupportedProjection); b=fixture();b[9]=2;recalc(b);EXPECT_EQ(decodeCode(b),Code::UnsupportedSampleOrder); b=fixture();b[10]=2;recalc(b);EXPECT_EQ(decodeCode(b),Code::UnsupportedGridRegistration); }
TEST(ParticleQbinV2Decoder, RejectsFrameAndReservedHeader) { auto b=fixture();u16(b,80,0);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidHeader); b=fixture();b[11]=1;recalc(b);EXPECT_EQ(decodeCode(b),Code::ReservedNonZero); b=fixture();u32(b,92,1);recalc(b);EXPECT_EQ(decodeCode(b),Code::ReservedNonZero); }
TEST(ParticleQbinV2Decoder, RejectsGridTimeAndBounds) { auto b=fixture();u32(b,16,1);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidHeader); b=fixture();u32(b,16,2049);u32(b,20,1024);recalc(b);EXPECT_EQ(decodeCode(b),Code::LimitExceeded); b=fixture();i64(b,24,0);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidTime); b=fixture();i64(b,32,0);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidTime); b=fixture();i64(b,24,std::numeric_limits<int64_t>::max());recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidTime); b=fixture();f64(b,40,std::numeric_limits<double>::quiet_NaN());recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidBounds); b=fixture();f64(b,40,11);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidBounds); b=fixture();f64(b,48,86);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidBounds); }
TEST(ParticleQbinV2Decoder, RejectsLengthAndFileLimits) { auto b=fixture();b.pop_back();EXPECT_EQ(decodeCode(b),Code::LengthMismatch); b=fixture();b.push_back(0);EXPECT_EQ(decodeCode(b),Code::LengthMismatch); std::vector<uint8_t> huge(64u*1024u*1024u+1);EXPECT_EQ(decodeCode(huge),Code::LimitExceeded); }
TEST(ParticleQbinV2Decoder, LimitsDecodedBytesWithoutChargingCanonicalPadding) { std::vector<P> ps={{3,0,0,6,1,3,0,1,0,{}}}; for(uint16_t semantic=4;semantic<13;++semantic) ps.push_back({semantic,77,0,5,0,0,0,1,0,{}}); EXPECT_EQ(decodeCode(fixture(1024,1024,ps)),Code::LimitExceeded); }
TEST(ParticleQbinV2Decoder, AcceptsRawLimitWithCanonicalPaddingExcluded) {
constexpr uint64_t limit = 32ull * 1024ull * 1024ull;
const uint32_t width = 1101, height = 1890;
const uint64_t cells = uint64_t(width) * height;
ASSERT_LE(cells, 2'097'152u);
std::vector<P> ps = {{3, 0, 0, 6, 1, 3, 0, 1, 0, {}}};
for (uint16_t semantic = 4; semantic < 20; ++semantic) {
ps.push_back({semantic, 77, 0, 3, 0, 0, 0, 1, 0, {}});
}
uint64_t rawSum = 0;
uint64_t payloadLength = 0;
for (const auto& p : ps) {
const uint64_t bytes = rawLength(p.type, cells);
rawSum += bytes;
payloadLength = a16(payloadLength + bytes);
}
auto b = fixture(width, height, ps);
const uint64_t totalQbinBytes = b.size();
EXPECT_LE(rawSum, limit);
EXPECT_GT(payloadLength, limit);
EXPECT_GT(payloadLength, rawSum);
EXPECT_LE(totalQbinBytes, 64ull * 1024ull * 1024ull);
ASSERT_EQ(totalQbinBytes, Header + ps.size() * Descriptor + payloadLength);
auto r = particle::ingress::QbinV2Decoder::decode(meta(), b);
EXPECT_TRUE(r.accepted()) << r.detail;
}
TEST(ParticleQbinV2Decoder, RejectsPlaneCountAndDescriptorEnums) { auto b=fixture();u16(b,12,0);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidHeader); b=fixture();u16(b,12,33);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidHeader); b=fixture();b[114]=0;recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();b[140]=4;recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();b[141]=4;recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); }
TEST(ParticleQbinV2Decoder, RejectsDescriptorMetadataAndNumericParameters) { auto b=fixture();u16(b,142,1);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();b[115]=2;recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();u32(b,124,3);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();f32(b,116,0);recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();f32(b,116,std::numeric_limits<float>::infinity());recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); b=fixture();f32(b,120,std::numeric_limits<float>::quiet_NaN());recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidDescriptor); }
TEST(ParticleQbinV2Decoder, EnforcesBitsetAndValidityPairing) { auto ps=basePlanes();ps[0].type=6;ps[0].raw={15};auto b=fixture(2,2,ps);EXPECT_EQ(decodeCode(b),Code::InvalidValidity); ps=basePlanes();ps[2].type=3;ps[2].raw={0,0,0,0};b=fixture(2,2,ps);EXPECT_EQ(decodeCode(b),Code::InvalidValidity); }
TEST(ParticleQbinV2Decoder, RejectsCanonicalOrderDuplicateAndPacking) { auto ps=basePlanes();std::swap(ps[0],ps[1]);auto b=fixture(2,2,ps);EXPECT_EQ(decodeCode(b),Code::NonCanonicalDescriptorOrder); ps=basePlanes();ps[1].semantic=1;b=fixture(2,2,ps);EXPECT_EQ(decodeCode(b),Code::DuplicatePlaneKey); b=fixture();u32(b,128,16);recalc(b);EXPECT_EQ(decodeCode(b),Code::NonCanonicalPacking); b=fixture();u32(b,160,32);recalc(b);EXPECT_EQ(decodeCode(b),Code::NonCanonicalPacking); b=fixture();u32(b,72,64);b.resize(Header+3*Descriptor+64);recalc(b);EXPECT_EQ(decodeCode(b),Code::NonCanonicalPacking); }
TEST(ParticleQbinV2Decoder, RejectsPaddingAndCrcCorruption) { auto b=fixture();b[216]=1;recalc(b);EXPECT_EQ(decodeCode(b),Code::NonZeroPadding); b=fixture();b.back()=1;recalc(b);EXPECT_EQ(decodeCode(b),Code::NonZeroPadding); b=fixture();b[6]^=1;EXPECT_EQ(decodeCode(b),Code::CrcMismatch); b=fixture();b[136]^=1;EXPECT_EQ(decodeCode(b),Code::CrcMismatch); b=fixture();b[208]^=1;EXPECT_EQ(decodeCode(b),Code::CrcMismatch); }
TEST(ParticleQbinV2Decoder, RejectsValidityIntegrityAndInvalidCells) { auto b=fixture();b[240]=0xf0;recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidValidity); b=fixture();b[240]=0x0e;recalc(b);EXPECT_EQ(decodeCode(b),Code::InvalidCellPayload); auto ps=basePlanes();ps.push_back({4,0,0,6,1,3,0,1,0,{15}});b=fixture(2,2,ps);EXPECT_EQ(decodeCode(b),Code::InvalidValidity); }
TEST(ParticleQbinV2Decoder, RejectsInvalidCellPayloadForEveryNumericDType) { for (uint8_t type : {uint8_t(1),uint8_t(2),uint8_t(3),uint8_t(4),uint8_t(5)}) { auto ps=basePlanes();ps[0].type=type;ps[0].raw.assign(rawLength(type,4),0);ps[0].raw[0]=1;ps[2].raw={0x0e};EXPECT_EQ(decodeCode(fixture(2,2,ps)),Code::InvalidCellPayload)<<int(type); } }
TEST(ParticleQbinV2Decoder, PlaneIdentityIsIndependentAndCanonical) { auto ps=basePlanes();ps.push_back({50,777,0,3,0,0,0,1,0,{1,2,3,4}});auto a=fixture(2,2,ps);ps[3].raw[0]=9;auto b=fixture(2,2,ps);auto ra=particle::ingress::QbinV2Decoder::decode(meta(),a),rb=particle::ingress::QbinV2Decoder::decode(meta(),b);ASSERT_TRUE(ra.accepted());ASSERT_TRUE(rb.accepted());EXPECT_NE(crc(a),crc(b));EXPECT_EQ(ra.frame->planes[0].contentIdentity,rb.frame->planes[0].contentIdentity);EXPECT_EQ(ra.frame->planes[1].contentIdentity,rb.frame->planes[1].contentIdentity);EXPECT_NE(ra.frame->planes[2].contentIdentity,rb.frame->planes[2].contentIdentity);EXPECT_EQ(ra.frame->validityContentIdentity,rb.frame->validityContentIdentity); EXPECT_EQ(ra.frame->planes[0].contentIdentity,"75930fed16c969a7"); }
TEST(ParticleQbinV2Decoder, DecodesLargeFixtureByteForByte) { auto b=fixture(513,769);auto r=particle::ingress::QbinV2Decoder::decode(meta(),b);ASSERT_TRUE(r.accepted())<<r.detail;const uint64_t cells=513u*769u;const size_t uv=cells*2, validity=(cells+7)/8;EXPECT_EQ(b.size()-208,a16(a16(uv)+uv)+a16(validity));EXPECT_EQ(r.frame->planes[0].storage->size(),uv);EXPECT_EQ(r.frame->planes[1].storage->size(),uv);EXPECT_EQ(r.frame->validityBits->size(),validity);EXPECT_EQ(r.frame->validityPlane.storage,r.frame->validityBits);EXPECT_EQ(*r.frame->planes[0].storage,std::vector<uint8_t>(b.begin()+208,b.begin()+208+uv));EXPECT_EQ(*r.frame->planes[1].storage,std::vector<uint8_t>(b.begin()+208+a16(uv),b.begin()+208+a16(uv)+uv));EXPECT_EQ(*r.frame->validityBits,std::vector<uint8_t>(b.begin()+208+a16(a16(uv)+uv),b.begin()+208+a16(a16(uv)+uv)+validity)); }
TEST(ParticleEncodedFieldIngress, AcceptsAndRejectsContracts) { auto good=wind();EXPECT_TRUE(particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&good,fixture()).accepted());EXPECT_EQ(particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),nullptr,fixture()).code,particle::ingress::EncodedFieldIngressCode::ContractMissing);auto invalid=good;invalid.contractSchemaVersion=2;EXPECT_EQ(particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&invalid,fixture()).code,particle::ingress::EncodedFieldIngressCode::ContractRejected);auto missing=good;missing.requiredPlanes.pop_back();EXPECT_EQ(particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&missing,fixture()).code,particle::ingress::EncodedFieldIngressCode::ContractRejected); }
TEST(ParticleEncodedFieldIngress, RejectsKnownCoreConflictAndAcceptsUnknownOpaqueScalar) { auto ps=basePlanes();ps[0]={1,65535,0,3,1,0,0,1,0,{1,2,3,4}};auto c=contract({{1,particle::FieldValueClass::Scalar,65535,0,particle::FieldComponent::None,true},{2,particle::FieldValueClass::VelocityComponent,1,1,particle::FieldComponent::VNorth,true},{3,particle::FieldValueClass::Validity,0,0,particle::FieldComponent::None,true}});EXPECT_EQ(particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&c,fixture(2,2,ps)).code,particle::ingress::EncodedFieldIngressCode::ContractRejected);ps={{3,0,0,6,1,3,0,1,0,{15}},{99,65535,0,3,1,0,0,1,0,{1,2,3,4}}};c=contract({{3,particle::FieldValueClass::Validity,0,0,particle::FieldComponent::None,true},{99,particle::FieldValueClass::Scalar,65535,0,particle::FieldComponent::None,true}});EXPECT_TRUE(particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(),&c,fixture(2,2,ps)).accepted()); }
TEST(ParticleEncodedFieldIngress, RetainsResolvedProfileIdentityForRuntimeWindowing) {
auto resolved = wind();
resolved.profileID = "wind.test.v2";
resolved.profileVersion = 2;
const auto ingress = particle::ingress::EncodedFieldIngress::decodeAndValidate(meta(), &resolved, fixture());
ASSERT_TRUE(ingress.accepted()) << ingress.detail;
EXPECT_EQ(ingress.frame->resolvedProfileID, "wind.test.v2");
EXPECT_EQ(ingress.frame->resolvedProfileVersion, 2u);
}