From 3422392aebc992a0bf896bc8e1b76fc4825bc413 Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:23:49 +0000 Subject: [PATCH] feat: encode persistent location state --- .../Telemetry/LocationStateRecord.cpp | 350 ++++++++++++++++++ lib/tdeck_ui/Telemetry/LocationStateRecord.h | 57 +++ tests/native/test_location_state_record.cpp | 296 +++++++++++++++ tests/native/test_location_state_record.py | 23 ++ 4 files changed, 726 insertions(+) create mode 100644 lib/tdeck_ui/Telemetry/LocationStateRecord.cpp create mode 100644 lib/tdeck_ui/Telemetry/LocationStateRecord.h create mode 100644 tests/native/test_location_state_record.cpp create mode 100644 tests/native/test_location_state_record.py diff --git a/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp b/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp new file mode 100644 index 00000000..cad74944 --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationStateRecord.cpp @@ -0,0 +1,350 @@ +#include "LocationStateRecord.h" + +#include +#include +#include +#include + +namespace Telemetry { +namespace { + +constexpr uint8_t MAGIC[4] = {'P', 'Y', 'L', 'S'}; +constexpr uint8_t SESSION_HAS_EXPIRY = 0x01; +constexpr uint8_t SESSION_CEASE_PENDING = 0x02; +constexpr uint8_t LOCATION_HAS_EXPIRY = 0x01; + +uint16_t readU16(const uint8_t* data) { + return static_cast( + (static_cast(data[0]) << 8U) | + static_cast(data[1])); +} + +uint32_t readU32(const uint8_t* data) { + return (static_cast(data[0]) << 24U) | + (static_cast(data[1]) << 16U) | + (static_cast(data[2]) << 8U) | + static_cast(data[3]); +} + +uint64_t readU64(const uint8_t* data) { + uint64_t value = 0; + for (uint8_t index = 0; index < 8; ++index) { + value = (value << 8U) | data[index]; + } + return value; +} + +int32_t readI32(const uint8_t* data) { + const uint32_t raw = readU32(data); + if (raw <= static_cast(std::numeric_limits::max())) { + return static_cast(raw); + } + return -1 - static_cast( + std::numeric_limits::max() - raw); +} + +void writeU16(uint8_t* output, uint16_t value) { + output[0] = static_cast(value >> 8U); + output[1] = static_cast(value); +} + +void writeU32(uint8_t* output, uint32_t value) { + output[0] = static_cast(value >> 24U); + output[1] = static_cast(value >> 16U); + output[2] = static_cast(value >> 8U); + output[3] = static_cast(value); +} + +void writeI32(uint8_t* output, int32_t value) { + writeU32(output, static_cast(value)); +} + +void writeU64(uint8_t* output, uint64_t value) { + for (int index = 7; index >= 0; --index) { + output[index] = static_cast(value); + value >>= 8U; + } +} + +uint32_t crc32(const uint8_t* data, std::size_t size) { + uint32_t crc = 0xffffffffU; + for (std::size_t index = 0; index < size; ++index) { + crc ^= data[index]; + for (uint8_t bit = 0; bit < 8; ++bit) { + crc = (crc & 1U) != 0 + ? (crc >> 1U) ^ 0xedb88320U + : crc >> 1U; + } + } + return crc ^ 0xffffffffU; +} + +bool peerEquals(const PeerId& left, const PeerId& right) { + return std::memcmp(left.bytes, right.bytes, PEER_ID_SIZE) == 0; +} + +bool validSession(const ShareRestoreEntry& entry) { + const ShareRestoreRecord& record = entry.record; + return record.cadence_millis >= MIN_SHARE_CADENCE_MILLIS && + record.cadence_millis <= MAX_SHARE_CADENCE_MILLIS && + record.approx_radius_meters >= 0 && + (record.has_expiry || record.expires_at_millis == 0) && + record.expires_at_millis <= + static_cast(std::numeric_limits::max()); +} + +bool validLocation(const PeerLocationRecord& record) { + return record.location.latitude_e6 >= -90000000 && + record.location.latitude_e6 <= 90000000 && + record.location.longitude_e6 >= -180000000 && + record.location.longitude_e6 <= 180000000 && + (record.has_expiry || record.expires_at_millis == 0) && + record.expires_at_millis <= + static_cast(std::numeric_limits::max()) && + record.approx_radius_meters <= + static_cast(std::numeric_limits::max()); +} + +bool duplicateSession( + const LocationStateSnapshot& state, + std::size_t index) { + for (std::size_t prior = 0; prior < index; ++prior) { + if (peerEquals(state.sessions[prior].peer, state.sessions[index].peer)) { + return true; + } + } + return false; +} + +bool duplicateLocation( + const LocationStateSnapshot& state, + std::size_t index) { + for (std::size_t prior = 0; prior < index; ++prior) { + if (peerEquals(state.locations[prior].peer, state.locations[index].peer)) { + return true; + } + } + return false; +} + +void encodeSession(const ShareRestoreEntry& entry, uint8_t* output) { + std::memcpy(output, entry.peer.bytes, PEER_ID_SIZE); + output[16] = static_cast( + (entry.record.has_expiry ? SESSION_HAS_EXPIRY : 0U) | + (entry.record.cease_pending ? SESSION_CEASE_PENDING : 0U)); + output[17] = 0; + writeU32(output + 18, entry.record.cadence_millis); + writeI32(output + 22, entry.record.approx_radius_meters); + writeU64(output + 26, entry.record.expires_at_millis); +} + +ShareRestoreEntry decodeSession(const uint8_t* data) { + ShareRestoreEntry entry{}; + std::memcpy(entry.peer.bytes, data, PEER_ID_SIZE); + entry.record.has_expiry = (data[16] & SESSION_HAS_EXPIRY) != 0; + entry.record.cease_pending = (data[16] & SESSION_CEASE_PENDING) != 0; + entry.record.cadence_millis = readU32(data + 18); + entry.record.approx_radius_meters = readI32(data + 22); + entry.record.expires_at_millis = readU64(data + 26); + return entry; +} + +void encodeLocation(const PeerLocationRecord& record, uint8_t* output) { + std::memcpy(output, record.peer.bytes, PEER_ID_SIZE); + output[16] = record.has_expiry ? LOCATION_HAS_EXPIRY : 0; + output[17] = 0; + writeI32(output + 18, record.location.latitude_e6); + writeI32(output + 22, record.location.longitude_e6); + writeI32(output + 26, record.location.altitude_cm); + writeU32(output + 30, record.location.speed_centi_kmh); + writeI32(output + 34, record.location.bearing_cdeg); + writeU16(output + 38, record.location.accuracy_cm); + writeU16(output + 40, 0); + writeU64(output + 42, record.location.timestamp_seconds); + writeU64(output + 50, record.location.sensor_timestamp_seconds); + writeU64(output + 58, record.source_timestamp_millis); + writeU64(output + 66, record.received_at_millis); + writeU64(output + 74, record.expires_at_millis); + writeU32(output + 82, record.approx_radius_meters); +} + +PeerLocationRecord decodeLocation(const uint8_t* data) { + PeerLocationRecord record{}; + std::memcpy(record.peer.bytes, data, PEER_ID_SIZE); + record.has_expiry = (data[16] & LOCATION_HAS_EXPIRY) != 0; + record.location.latitude_e6 = readI32(data + 18); + record.location.longitude_e6 = readI32(data + 22); + record.location.altitude_cm = readI32(data + 26); + record.location.speed_centi_kmh = readU32(data + 30); + record.location.bearing_cdeg = readI32(data + 34); + record.location.accuracy_cm = readU16(data + 38); + record.location.timestamp_seconds = readU64(data + 42); + record.location.sensor_timestamp_seconds = readU64(data + 50); + record.source_timestamp_millis = readU64(data + 58); + record.received_at_millis = readU64(data + 66); + record.expires_at_millis = readU64(data + 74); + record.approx_radius_meters = readU32(data + 82); + return record; +} + +LocationStateRecordResult validateEncodedRecords( + const uint8_t* payload, + std::size_t session_count, + std::size_t location_count) { + for (std::size_t index = 0; index < session_count; ++index) { + const uint8_t* current = payload + index * LOCATION_STATE_SESSION_BYTES; + if ((current[16] & ~0x03U) != 0 || current[17] != 0) { + return LocationStateRecordResult::MALFORMED; + } + const ShareRestoreEntry entry = decodeSession(current); + if (!validSession(entry)) return LocationStateRecordResult::OUT_OF_RANGE; + for (std::size_t prior = 0; prior < index; ++prior) { + if (std::memcmp(payload + prior * LOCATION_STATE_SESSION_BYTES, + current, PEER_ID_SIZE) == 0) { + return LocationStateRecordResult::DUPLICATE_PEER; + } + } + } + + const uint8_t* locations = + payload + session_count * LOCATION_STATE_SESSION_BYTES; + for (std::size_t index = 0; index < location_count; ++index) { + const uint8_t* current = locations + index * LOCATION_STATE_LOCATION_BYTES; + if ((current[16] & ~0x01U) != 0 || current[17] != 0 || + readU16(current + 40) != 0) { + return LocationStateRecordResult::MALFORMED; + } + const PeerLocationRecord record = decodeLocation(current); + if (!validLocation(record)) return LocationStateRecordResult::OUT_OF_RANGE; + for (std::size_t prior = 0; prior < index; ++prior) { + if (std::memcmp(locations + prior * LOCATION_STATE_LOCATION_BYTES, + current, PEER_ID_SIZE) == 0) { + return LocationStateRecordResult::DUPLICATE_PEER; + } + } + } + return LocationStateRecordResult::OK; +} + +} // namespace + +LocationStateRecordResult encodeLocationStateRecord( + const LocationStateSnapshot& input, + uint8_t* output, + std::size_t capacity, + std::size_t& written_or_required) { + written_or_required = 0; + if (input.session_count > MAX_SHARE_SESSIONS || + input.location_count > MAX_PEER_LOCATIONS) { + return LocationStateRecordResult::OUT_OF_RANGE; + } + for (std::size_t index = 0; index < input.session_count; ++index) { + if (!validSession(input.sessions[index])) { + return LocationStateRecordResult::OUT_OF_RANGE; + } + if (duplicateSession(input, index)) { + return LocationStateRecordResult::DUPLICATE_PEER; + } + } + for (std::size_t index = 0; index < input.location_count; ++index) { + if (!validLocation(input.locations[index])) { + return LocationStateRecordResult::OUT_OF_RANGE; + } + if (duplicateLocation(input, index)) { + return LocationStateRecordResult::DUPLICATE_PEER; + } + } + + const std::size_t payload_size = + input.session_count * LOCATION_STATE_SESSION_BYTES + + input.location_count * LOCATION_STATE_LOCATION_BYTES; + const std::size_t required = + LOCATION_STATE_HEADER_BYTES + payload_size + LOCATION_STATE_CRC_BYTES; + written_or_required = required; + if (capacity < required) return LocationStateRecordResult::BUFFER_TOO_SMALL; + if (output == nullptr) return LocationStateRecordResult::INVALID_ARGUMENT; + + std::memcpy(output, MAGIC, sizeof(MAGIC)); + writeU16(output + 4, LOCATION_STATE_SCHEMA_VERSION); + writeU16(output + 6, 0); + writeU32(output + 8, static_cast(payload_size)); + writeU16(output + 12, static_cast(input.session_count)); + writeU16(output + 14, static_cast(input.location_count)); + uint8_t* cursor = output + LOCATION_STATE_HEADER_BYTES; + for (std::size_t index = 0; index < input.session_count; ++index) { + encodeSession(input.sessions[index], cursor); + cursor += LOCATION_STATE_SESSION_BYTES; + } + for (std::size_t index = 0; index < input.location_count; ++index) { + encodeLocation(input.locations[index], cursor); + cursor += LOCATION_STATE_LOCATION_BYTES; + } + writeU32(cursor, crc32(output, required - LOCATION_STATE_CRC_BYTES)); + return LocationStateRecordResult::OK; +} + +LocationStateRecordResult decodeLocationStateRecord( + const uint8_t* data, + std::size_t size, + LocationStateSnapshot& output) { + if (data == nullptr) return LocationStateRecordResult::INVALID_ARGUMENT; + if (size < LOCATION_STATE_HEADER_BYTES + LOCATION_STATE_CRC_BYTES) { + return LocationStateRecordResult::MALFORMED; + } + if (std::memcmp(data, MAGIC, sizeof(MAGIC)) != 0) { + return LocationStateRecordResult::BAD_MAGIC; + } + if (readU16(data + 4) != LOCATION_STATE_SCHEMA_VERSION) { + return LocationStateRecordResult::UNSUPPORTED_VERSION; + } + if (readU16(data + 6) != 0) return LocationStateRecordResult::MALFORMED; + + const std::size_t payload_size = readU32(data + 8); + const std::size_t session_count = readU16(data + 12); + const std::size_t location_count = readU16(data + 14); + if (session_count > MAX_SHARE_SESSIONS || + location_count > MAX_PEER_LOCATIONS) { + return LocationStateRecordResult::OUT_OF_RANGE; + } + const std::size_t expected_payload = + session_count * LOCATION_STATE_SESSION_BYTES + + location_count * LOCATION_STATE_LOCATION_BYTES; + if (payload_size != expected_payload) { + return LocationStateRecordResult::MALFORMED; + } + const std::size_t expected_size = + LOCATION_STATE_HEADER_BYTES + expected_payload + LOCATION_STATE_CRC_BYTES; + if (size != expected_size) return LocationStateRecordResult::MALFORMED; + if (readU32(data + size - LOCATION_STATE_CRC_BYTES) != + crc32(data, size - LOCATION_STATE_CRC_BYTES)) { + return LocationStateRecordResult::CRC_MISMATCH; + } + + const uint8_t* payload = data + LOCATION_STATE_HEADER_BYTES; + const LocationStateRecordResult validation = + validateEncodedRecords(payload, session_count, location_count); + if (validation != LocationStateRecordResult::OK) return validation; + + output.session_count = 0; + output.location_count = 0; + for (std::size_t index = 0; index < MAX_SHARE_SESSIONS; ++index) { + output.sessions[index] = ShareRestoreEntry{}; + } + for (std::size_t index = 0; index < MAX_PEER_LOCATIONS; ++index) { + output.locations[index] = PeerLocationRecord{}; + } + output.session_count = session_count; + output.location_count = location_count; + const uint8_t* cursor = payload; + for (std::size_t index = 0; index < session_count; ++index) { + output.sessions[index] = decodeSession(cursor); + cursor += LOCATION_STATE_SESSION_BYTES; + } + for (std::size_t index = 0; index < location_count; ++index) { + output.locations[index] = decodeLocation(cursor); + cursor += LOCATION_STATE_LOCATION_BYTES; + } + return LocationStateRecordResult::OK; +} + +} // namespace Telemetry diff --git a/lib/tdeck_ui/Telemetry/LocationStateRecord.h b/lib/tdeck_ui/Telemetry/LocationStateRecord.h new file mode 100644 index 00000000..5bfe2a6f --- /dev/null +++ b/lib/tdeck_ui/Telemetry/LocationStateRecord.h @@ -0,0 +1,57 @@ +#ifndef PYXIS_TELEMETRY_LOCATION_STATE_RECORD_H +#define PYXIS_TELEMETRY_LOCATION_STATE_RECORD_H + +#include +#include + +#include "LocationShareScheduler.h" +#include "LocationShareState.h" + +namespace Telemetry { + +constexpr uint16_t LOCATION_STATE_SCHEMA_VERSION = 1; +constexpr std::size_t LOCATION_STATE_HEADER_BYTES = 16; +constexpr std::size_t LOCATION_STATE_SESSION_BYTES = 34; +constexpr std::size_t LOCATION_STATE_LOCATION_BYTES = 86; +constexpr std::size_t LOCATION_STATE_CRC_BYTES = 4; +constexpr std::size_t MAX_LOCATION_STATE_RECORD_BYTES = + LOCATION_STATE_HEADER_BYTES + + MAX_SHARE_SESSIONS * LOCATION_STATE_SESSION_BYTES + + MAX_PEER_LOCATIONS * LOCATION_STATE_LOCATION_BYTES + + LOCATION_STATE_CRC_BYTES; +static_assert(MAX_LOCATION_STATE_RECORD_BYTES <= 4096, + "persistent location record must remain bounded to 4 KiB"); + +struct LocationStateSnapshot { + std::size_t session_count = 0; + ShareRestoreEntry sessions[MAX_SHARE_SESSIONS]{}; + std::size_t location_count = 0; + PeerLocationRecord locations[MAX_PEER_LOCATIONS]{}; +}; + +enum class LocationStateRecordResult : uint8_t { + OK, + INVALID_ARGUMENT, + BUFFER_TOO_SMALL, + BAD_MAGIC, + UNSUPPORTED_VERSION, + MALFORMED, + CRC_MISMATCH, + OUT_OF_RANGE, + DUPLICATE_PEER, +}; + +LocationStateRecordResult encodeLocationStateRecord( + const LocationStateSnapshot& input, + uint8_t* output, + std::size_t capacity, + std::size_t& written_or_required); + +LocationStateRecordResult decodeLocationStateRecord( + const uint8_t* data, + std::size_t size, + LocationStateSnapshot& output); + +} // namespace Telemetry + +#endif // PYXIS_TELEMETRY_LOCATION_STATE_RECORD_H diff --git a/tests/native/test_location_state_record.cpp b/tests/native/test_location_state_record.cpp new file mode 100644 index 00000000..16dffb1d --- /dev/null +++ b/tests/native/test_location_state_record.cpp @@ -0,0 +1,296 @@ +#include +#include +#include +#include + +#include "Telemetry/LocationStateRecord.h" + +namespace { + +int passed = 0; +int failures = 0; + +#define CHECK(expr) \ + do { \ + if (expr) { \ + ++passed; \ + } else { \ + ++failures; \ + std::cerr << "FAIL line " << __LINE__ << ": " #expr << '\n'; \ + } \ + } while (false) + +Telemetry::PeerId peer(uint8_t seed) { + Telemetry::PeerId id{}; + for (std::size_t i = 0; i < Telemetry::PEER_ID_SIZE; ++i) { + id.bytes[i] = static_cast(seed + i); + } + return id; +} + +Telemetry::LocationStateSnapshot sample() { + Telemetry::LocationStateSnapshot state{}; + state.session_count = 1; + state.sessions[0].peer = peer(0); + auto& session = state.sessions[0].record; + session.cadence_millis = 60000; + session.approx_radius_meters = 250; + session.has_expiry = true; + session.expires_at_millis = 1700000900000ULL; + session.cease_pending = true; + + state.location_count = 1; + auto& record = state.locations[0]; + record.peer = peer(16); + record.location.latitude_e6 = 37774900; + record.location.longitude_e6 = -122419400; + record.location.altitude_cm = 12345; + record.location.speed_centi_kmh = 1234; + record.location.bearing_cdeg = 27000; + record.location.accuracy_cm = 500; + record.location.timestamp_seconds = 1700000000ULL; + record.location.sensor_timestamp_seconds = 1699999999ULL; + record.source_timestamp_millis = 1700000000123ULL; + record.received_at_millis = 1700000001000ULL; + record.has_expiry = true; + record.expires_at_millis = 1700000900000ULL; + record.approx_radius_meters = 250; + return state; +} + +uint32_t crc32(const uint8_t* data, std::size_t size) { + uint32_t crc = 0xffffffffU; + for (std::size_t i = 0; i < size; ++i) { + crc ^= data[i]; + for (uint8_t bit = 0; bit < 8; ++bit) { + crc = (crc & 1U) != 0 + ? (crc >> 1U) ^ 0xedb88320U + : crc >> 1U; + } + } + return crc ^ 0xffffffffU; +} + +void writeU16(uint8_t* output, uint16_t value) { + output[0] = static_cast(value >> 8U); + output[1] = static_cast(value); +} + +void writeU32(uint8_t* output, uint32_t value) { + output[0] = static_cast(value >> 24U); + output[1] = static_cast(value >> 16U); + output[2] = static_cast(value >> 8U); + output[3] = static_cast(value); +} + +void repairCrc(uint8_t* bytes, std::size_t size) { + writeU32(bytes + size - 4, crc32(bytes, size - 4)); +} + +bool sameState( + const Telemetry::LocationStateSnapshot& left, + const Telemetry::LocationStateSnapshot& right) { + return std::memcmp(&left, &right, sizeof(left)) == 0; +} + +void matchesIndependentGoldenBytes() { + static const uint8_t expected[] = { + 0x50,0x59,0x4c,0x53,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x78,0x00,0x01,0x00,0x01, + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, + 0x03,0x00,0x00,0x00,0xea,0x60,0x00,0x00,0x00,0xfa,0x00,0x00,0x01,0x8b,0xcf,0xf3, + 0x23,0xa0,0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d, + 0x1e,0x1f,0x01,0x00,0x02,0x40,0x66,0x34,0xf8,0xb4,0x07,0x38,0x00,0x00,0x30,0x39, + 0x00,0x00,0x04,0xd2,0x00,0x00,0x69,0x78,0x01,0xf4,0x00,0x00,0x00,0x00,0x00,0x00, + 0x65,0x53,0xf1,0x00,0x00,0x00,0x00,0x00,0x65,0x53,0xf0,0xff,0x00,0x00,0x01,0x8b, + 0xcf,0xe5,0x68,0x7b,0x00,0x00,0x01,0x8b,0xcf,0xe5,0x6b,0xe8,0x00,0x00,0x01,0x8b, + 0xcf,0xf3,0x23,0xa0,0x00,0x00,0x00,0xfa,0xa4,0x54,0x8e,0x49, + }; + uint8_t encoded[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]{}; + std::size_t written = 0; + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + CHECK(written == sizeof(expected)); + CHECK(std::memcmp(encoded, expected, sizeof(expected)) == 0); + + Telemetry::LocationStateSnapshot decoded{}; + CHECK(Telemetry::decodeLocationStateRecord( + expected, sizeof(expected), decoded) == + Telemetry::LocationStateRecordResult::OK); + CHECK(sameState(decoded, sample())); +} + +void rejectsEveryTruncationTransactionally() { + uint8_t encoded[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]{}; + std::size_t written = 0; + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + for (std::size_t size = 0; size < written; ++size) { + Telemetry::LocationStateSnapshot output = sample(); + output.sessions[0].record.cadence_millis = 1234; + const auto before = output; + CHECK(Telemetry::decodeLocationStateRecord(encoded, size, output) != + Telemetry::LocationStateRecordResult::OK); + CHECK(sameState(output, before)); + } +} + +void rejectsCorruptionAndUnsupportedHeadersTransactionally() { + uint8_t encoded[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]{}; + std::size_t written = 0; + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + Telemetry::LocationStateSnapshot sentinel = sample(); + sentinel.location_count = 0; + const auto before = sentinel; + + encoded[70] ^= 1U; + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, sentinel) == + Telemetry::LocationStateRecordResult::CRC_MISMATCH); + CHECK(sameState(sentinel, before)); + encoded[70] ^= 1U; + + encoded[0] = 'X'; + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, sentinel) == + Telemetry::LocationStateRecordResult::BAD_MAGIC); + CHECK(sameState(sentinel, before)); + encoded[0] = 'P'; + + writeU16(encoded + 4, 2); + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, sentinel) == + Telemetry::LocationStateRecordResult::UNSUPPORTED_VERSION); + CHECK(sameState(sentinel, before)); +} + +void rejectsCountOverflowDuplicatesAndInvalidCoordinates() { + uint8_t encoded[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]{}; + std::size_t written = 0; + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + Telemetry::LocationStateSnapshot output{}; + + writeU16(encoded + 12, 33); + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, output) == + Telemetry::LocationStateRecordResult::OUT_OF_RANGE); + + auto two = sample(); + two.session_count = 2; + two.sessions[1] = two.sessions[0]; + two.sessions[1].peer = peer(32); + CHECK(Telemetry::encodeLocationStateRecord( + two, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + std::memcpy(encoded + Telemetry::LOCATION_STATE_HEADER_BYTES + + Telemetry::LOCATION_STATE_SESSION_BYTES, + encoded + Telemetry::LOCATION_STATE_HEADER_BYTES, + Telemetry::PEER_ID_SIZE); + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, output) == + Telemetry::LocationStateRecordResult::DUPLICATE_PEER); + + two = sample(); + two.location_count = 2; + two.locations[1] = two.locations[0]; + two.locations[1].peer = peer(48); + CHECK(Telemetry::encodeLocationStateRecord( + two, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + const std::size_t first_location = + Telemetry::LOCATION_STATE_HEADER_BYTES + + Telemetry::LOCATION_STATE_SESSION_BYTES; + std::memcpy(encoded + first_location + + Telemetry::LOCATION_STATE_LOCATION_BYTES, + encoded + first_location, Telemetry::PEER_ID_SIZE); + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, output) == + Telemetry::LocationStateRecordResult::DUPLICATE_PEER); + + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + writeU32(encoded + first_location + 18, 90000001U); + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, output) == + Telemetry::LocationStateRecordResult::OUT_OF_RANGE); + + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OK); + encoded[Telemetry::LOCATION_STATE_HEADER_BYTES + 16] |= 0x80U; + repairCrc(encoded, written); + CHECK(Telemetry::decodeLocationStateRecord(encoded, written, output) == + Telemetry::LocationStateRecordResult::MALFORMED); + + auto duplicate = sample(); + duplicate.session_count = 2; + duplicate.sessions[1] = duplicate.sessions[0]; + CHECK(Telemetry::encodeLocationStateRecord( + duplicate, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::DUPLICATE_PEER); + + duplicate = sample(); + duplicate.location_count = 2; + duplicate.locations[1] = duplicate.locations[0]; + CHECK(Telemetry::encodeLocationStateRecord( + duplicate, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::DUPLICATE_PEER); + + auto invalid = sample(); + invalid.locations[0].location.latitude_e6 = 90000001; + CHECK(Telemetry::encodeLocationStateRecord( + invalid, encoded, sizeof(encoded), written) == + Telemetry::LocationStateRecordResult::OUT_OF_RANGE); +} + +void encodingIsAtomicAndBounded() { + uint8_t encoded[Telemetry::MAX_LOCATION_STATE_RECORD_BYTES]; + std::memset(encoded, 0xa5, sizeof(encoded)); + std::size_t required = 0; + for (std::size_t capacity = 0; capacity < 140; ++capacity) { + std::memset(encoded, 0xa5, sizeof(encoded)); + CHECK(Telemetry::encodeLocationStateRecord( + sample(), encoded, capacity, required) == + Telemetry::LocationStateRecordResult::BUFFER_TOO_SMALL); + CHECK(required == 140); + CHECK(encoded[0] == 0xa5); + } + + auto maximum = sample(); + maximum.session_count = Telemetry::MAX_SHARE_SESSIONS; + maximum.location_count = Telemetry::MAX_PEER_LOCATIONS; + for (std::size_t i = 0; i < maximum.session_count; ++i) { + maximum.sessions[i] = maximum.sessions[0]; + maximum.sessions[i].peer = peer(static_cast(i)); + } + for (std::size_t i = 0; i < maximum.location_count; ++i) { + maximum.locations[i] = maximum.locations[0]; + maximum.locations[i].peer = peer(static_cast(64 + i)); + } + CHECK(Telemetry::encodeLocationStateRecord( + maximum, encoded, sizeof(encoded), required) == + Telemetry::LocationStateRecordResult::OK); + CHECK(required <= Telemetry::MAX_LOCATION_STATE_RECORD_BYTES); + Telemetry::LocationStateSnapshot decoded{}; + CHECK(Telemetry::decodeLocationStateRecord(encoded, required, decoded) == + Telemetry::LocationStateRecordResult::OK); + CHECK(sameState(decoded, maximum)); +} + +} // namespace + +int main() { + matchesIndependentGoldenBytes(); + rejectsEveryTruncationTransactionally(); + rejectsCorruptionAndUnsupportedHeadersTransactionally(); + rejectsCountOverflowDuplicatesAndInvalidCoordinates(); + encodingIsAtomicAndBounded(); + std::cout << "location state record: " << passed << " passed, " + << failures << " failed\n"; + return failures == 0 ? 0 : 1; +} diff --git a/tests/native/test_location_state_record.py b/tests/native/test_location_state_record.py new file mode 100644 index 00000000..c9d1fc63 --- /dev/null +++ b/tests/native/test_location_state_record.py @@ -0,0 +1,23 @@ +"""Compile and execute versioned location-state record tests.""" + +from pathlib import Path + +from native_test import compile_and_run + +HERE = Path(__file__).resolve().parent +PYXIS_ROOT = HERE.parents[1] + + +def test_location_state_record(tmp_path): + ran = compile_and_run( + tmp_path, + name="test_location_state_record", + sources=[ + HERE / "test_location_state_record.cpp", + PYXIS_ROOT / "lib" / "tdeck_ui" / "Telemetry" / "LocationStateRecord.cpp", + ], + include_dirs=[PYXIS_ROOT / "lib" / "tdeck_ui"], + sanitize=True, + timeout=60, + ) + assert "0 failed" in ran.stdout