From e79c36e65b09e1078ea4f9fa01bac8cb7a96f578 Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 19:31:47 +0000 Subject: [PATCH] [verified] feat: read selected offline map packs --- lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp | 262 ++++++++++++++++ lib/tdeck_ui/Hardware/TDeck/MapTilePack.h | 103 +++++++ tests/native/test_map_tile_pack.cpp | 321 ++++++++++++++++++++ tests/native/test_map_tile_pack.py | 35 +++ 4 files changed, 721 insertions(+) create mode 100644 lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp create mode 100644 lib/tdeck_ui/Hardware/TDeck/MapTilePack.h create mode 100644 tests/native/test_map_tile_pack.cpp create mode 100644 tests/native/test_map_tile_pack.py diff --git a/lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp b/lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp new file mode 100644 index 00000000..b131ed37 --- /dev/null +++ b/lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp @@ -0,0 +1,262 @@ +// Copyright (c) 2026 Pyxis contributors +// SPDX-License-Identifier: MIT + +#include "Hardware/TDeck/MapTilePack.h" + +#include +#include + +namespace Hardware { +namespace TDeck { + +const char MapTilePack::ACTIVE_PACK_PATH[] = "/pyxis-map/active-pack"; + +MapTilePack::MapTilePack(MapTileStorage& storage) + : storage_(storage), manifest_(), status_(MapTilePackStatus::UNINITIALIZED), + stream_open_(false), stream_remaining_(0U), manifest_buffer_() {} + +MapTilePack::~MapTilePack() { + endGet(); +} + +bool MapTilePack::isValidPackId(const char* pack_id) { + if (pack_id == NULL) return false; + std::size_t length = 0U; + while (length < Pyxis::MapPackManifest::PACK_ID_CAPACITY && pack_id[length] != '\0') { + const char character = pack_id[length]; + if (!((character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || character == '_' || character == '-')) { + return false; + } + ++length; + } + return length > 0U && length < Pyxis::MapPackManifest::PACK_ID_CAPACITY; +} + +bool MapTilePack::isValidKey(const TileKey& key) { + if (key.zoom > Pyxis::MapPackManifest::MAX_ZOOM) return false; + const std::uint32_t edge = UINT32_C(1) << key.zoom; + return key.x < edge && key.y < edge; +} + +MapTilePackResult MapTilePack::makePath(const char* pack_id, const TileKey* key, + bool manifest, char* output, std::size_t capacity) { + if (!isValidPackId(pack_id)) return MapTilePackResult::INVALID_PACK_ID; + if (output == NULL || capacity == 0U) return MapTilePackResult::INVALID_ARGUMENT; + if (!manifest && (key == NULL || !isValidKey(*key))) return MapTilePackResult::INVALID_KEY; + + char temporary[PATH_CAPACITY]; + int count = 0; + if (manifest) { + count = std::snprintf(temporary, sizeof(temporary), "/pyxis-map/packs/%s/manifest.pmp", pack_id); + } else { + count = std::snprintf(temporary, sizeof(temporary), + "/pyxis-map/packs/%s/tiles/%u/%lu/%lu.png", pack_id, + static_cast(key->zoom), + static_cast(key->x), + static_cast(key->y)); + } + if (count < 0 || static_cast(count) >= sizeof(temporary) || + static_cast(count) + 1U > capacity) { + return MapTilePackResult::PATH_TOO_LONG; + } + std::memcpy(output, temporary, static_cast(count) + 1U); + return MapTilePackResult::OK; +} + +MapTilePackResult MapTilePack::manifestPath(const char* pack_id, char* output, + std::size_t capacity) { + return makePath(pack_id, NULL, true, output, capacity); +} + +MapTilePackResult MapTilePack::tilePath(const char* pack_id, const TileKey& key, + char* output, std::size_t capacity) { + return makePath(pack_id, &key, false, output, capacity); +} + +MapTilePackResult MapTilePack::loadFile(const char* path, std::uint8_t* output, + std::size_t capacity, std::size_t& length, + MapTilePackResult missing_result, + MapTilePackResult oversized_result) { + std::uint32_t declared_size = 0U; + const TileStoreResult begin = storage_.beginRead(path, declared_size); + if (begin == TileStoreResult::MISS) return missing_result; + if (begin == TileStoreResult::STORAGE_UNAVAILABLE) return MapTilePackResult::STORAGE_UNAVAILABLE; + if (begin != TileStoreResult::OK) return MapTilePackResult::IO_ERROR; + + if (declared_size > capacity) { + storage_.endRead(); + return oversized_result; + } + + std::size_t total = 0U; + while (total < declared_size) { + std::size_t count = 0U; + const TileStoreResult read = storage_.readChunk(output + total, + static_cast(declared_size) - total, count); + if (read != TileStoreResult::OK || count == 0U || + count > static_cast(declared_size) - total) { + storage_.endRead(); + return MapTilePackResult::IO_ERROR; + } + total += count; + } + std::uint8_t trailing = 0U; + std::size_t trailing_count = 0U; + const TileStoreResult eof = storage_.readChunk(&trailing, 1U, trailing_count); + storage_.endRead(); + if (eof != TileStoreResult::OK || trailing_count != 0U) { + return MapTilePackResult::IO_ERROR; + } + length = total; + return MapTilePackResult::OK; +} + +MapTilePackResult MapTilePack::failInitialize(MapTilePackResult result, + MapTilePackStatus initial_status, + bool had_selection) { + if (!had_selection) status_ = initial_status; + return result; +} + +MapTilePackResult MapTilePack::initialize() { + endGet(); + const bool had_selection = hasSelection(); + if (!storage_.isAvailable()) { + return failInitialize(MapTilePackResult::STORAGE_UNAVAILABLE, + MapTilePackStatus::STORAGE_UNAVAILABLE, had_selection); + } + + std::uint8_t marker[Pyxis::MapPackManifest::PACK_ID_CAPACITY]; + std::size_t marker_length = 0U; + MapTilePackResult result = loadFile(ACTIVE_PACK_PATH, marker, sizeof(marker) - 1U, + marker_length, MapTilePackResult::NO_SELECTION, + MapTilePackResult::INVALID_PACK_ID); + if (result == MapTilePackResult::NO_SELECTION) { + manifest_ = Pyxis::MapPackManifest(); + status_ = MapTilePackStatus::NO_SELECTION; + return result; + } + if (result != MapTilePackResult::OK) { + const MapTilePackStatus state = result == MapTilePackResult::STORAGE_UNAVAILABLE + ? MapTilePackStatus::STORAGE_UNAVAILABLE : MapTilePackStatus::INVALID_SELECTION; + return failInitialize(result, state, had_selection); + } + if (marker_length == 0U) { + manifest_ = Pyxis::MapPackManifest(); + status_ = MapTilePackStatus::NO_SELECTION; + return MapTilePackResult::NO_SELECTION; + } + marker[marker_length] = 0U; + const char* pack_id = reinterpret_cast(marker); + if (std::memchr(marker, 0, marker_length) != NULL || !isValidPackId(pack_id)) { + return failInitialize(MapTilePackResult::INVALID_PACK_ID, + MapTilePackStatus::INVALID_SELECTION, had_selection); + } + + char manifest_path[PATH_CAPACITY]; + result = manifestPath(pack_id, manifest_path, sizeof(manifest_path)); + if (result != MapTilePackResult::OK) { + return failInitialize(result, MapTilePackStatus::INVALID_SELECTION, had_selection); + } + std::size_t manifest_length = 0U; + result = loadFile(manifest_path, manifest_buffer_, sizeof(manifest_buffer_), manifest_length, + MapTilePackResult::MANIFEST_MISSING, + MapTilePackResult::MANIFEST_TOO_LARGE); + if (result != MapTilePackResult::OK) { + const MapTilePackStatus state = result == MapTilePackResult::STORAGE_UNAVAILABLE + ? MapTilePackStatus::STORAGE_UNAVAILABLE : MapTilePackStatus::INVALID_SELECTION; + return failInitialize(result, state, had_selection); + } + + Pyxis::MapPackManifest candidate = {}; + if (Pyxis::MapPackManifest::parse(manifest_buffer_, manifest_length, candidate) != + Pyxis::ManifestResult::OK) { + return failInitialize(MapTilePackResult::INVALID_MANIFEST, + MapTilePackStatus::INVALID_SELECTION, had_selection); + } + if (std::strcmp(pack_id, candidate.pack_id) != 0) { + return failInitialize(MapTilePackResult::PACK_ID_MISMATCH, + MapTilePackStatus::INVALID_SELECTION, had_selection); + } + + manifest_ = candidate; + status_ = MapTilePackStatus::READY; + return MapTilePackResult::OK; +} + +MapTilePackResult MapTilePack::beginGet(const TileKey& key, std::uint32_t& size) { + if (stream_open_) return MapTilePackResult::BUSY; + if (!hasSelection()) return MapTilePackResult::NOT_INITIALIZED; + if (!isValidKey(key)) return MapTilePackResult::INVALID_KEY; + if (!manifest_.covers(key)) return MapTilePackResult::UNCOVERED; + if (!storage_.isAvailable()) return MapTilePackResult::STORAGE_UNAVAILABLE; + + char path[PATH_CAPACITY]; + MapTilePackResult result = tilePath(manifest_.pack_id, key, path, sizeof(path)); + if (result != MapTilePackResult::OK) return result; + std::uint32_t candidate_size = 0U; + const TileStoreResult begin = storage_.beginRead(path, candidate_size); + if (begin == TileStoreResult::MISS) return MapTilePackResult::TILE_MISSING; + if (begin == TileStoreResult::STORAGE_UNAVAILABLE) return MapTilePackResult::STORAGE_UNAVAILABLE; + if (begin == TileStoreResult::BUSY) return MapTilePackResult::BUSY; + if (begin != TileStoreResult::OK) return MapTilePackResult::IO_ERROR; + stream_open_ = true; + stream_remaining_ = candidate_size; + size = candidate_size; + return MapTilePackResult::OK; +} + +MapTilePackResult MapTilePack::readGetChunk(std::uint8_t* output, std::size_t capacity, + std::size_t& count) { + count = 0U; + if (output == NULL || capacity == 0U) { + endGet(); + return MapTilePackResult::INVALID_ARGUMENT; + } + if (!stream_open_) return MapTilePackResult::NOT_STREAMING; + + if (stream_remaining_ == 0U) { + std::uint8_t trailing = 0U; + std::size_t trailing_count = 0U; + const TileStoreResult eof = storage_.readChunk(&trailing, 1U, trailing_count); + endGet(); + return (eof == TileStoreResult::OK && trailing_count == 0U) + ? MapTilePackResult::OK : MapTilePackResult::IO_ERROR; + } + + std::size_t read_count = 0U; + const std::size_t allowed = capacity < stream_remaining_ ? capacity : stream_remaining_; + const TileStoreResult read = storage_.readChunk(output, allowed, read_count); + if (read != TileStoreResult::OK || read_count > allowed || + (read_count == 0U && stream_remaining_ != 0U)) { + endGet(); + return MapTilePackResult::IO_ERROR; + } + if (read_count == 0U) { + endGet(); + return MapTilePackResult::OK; + } + stream_remaining_ -= static_cast(read_count); + if (stream_remaining_ == 0U) { + std::uint8_t trailing = 0U; + std::size_t trailing_count = 0U; + const TileStoreResult eof = storage_.readChunk(&trailing, 1U, trailing_count); + if (eof != TileStoreResult::OK || trailing_count != 0U) { + endGet(); + return MapTilePackResult::IO_ERROR; + } + endGet(); + } + count = read_count; + return MapTilePackResult::OK; +} + +void MapTilePack::endGet() { + if (stream_open_) storage_.endRead(); + stream_open_ = false; + stream_remaining_ = 0U; +} + +} // namespace TDeck +} // namespace Hardware diff --git a/lib/tdeck_ui/Hardware/TDeck/MapTilePack.h b/lib/tdeck_ui/Hardware/TDeck/MapTilePack.h new file mode 100644 index 00000000..3a59889c --- /dev/null +++ b/lib/tdeck_ui/Hardware/TDeck/MapTilePack.h @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Pyxis contributors +// SPDX-License-Identifier: MIT + +#ifndef HARDWARE_TDECK_MAP_TILE_PACK_H +#define HARDWARE_TDECK_MAP_TILE_PACK_H + +#include +#include + +#include "Hardware/TDeck/MapTileStore.h" +#include "UI/LXMF/MapPackManifest.h" + +namespace Hardware { +namespace TDeck { + +enum class MapTilePackResult : std::uint8_t { + OK = 0, + NO_SELECTION, + INVALID_PACK_ID, + STORAGE_UNAVAILABLE, + MANIFEST_MISSING, + MANIFEST_TOO_LARGE, + INVALID_MANIFEST, + PACK_ID_MISMATCH, + NOT_INITIALIZED, + INVALID_KEY, + UNCOVERED, + TILE_MISSING, + PATH_TOO_LONG, + INVALID_ARGUMENT, + IO_ERROR, + BUSY, + NOT_STREAMING +}; + +enum class MapTilePackStatus : std::uint8_t { + UNINITIALIZED = 0, + NO_SELECTION, + READY, + INVALID_SELECTION, + STORAGE_UNAVAILABLE +}; + +/** + * Allocation-free, read-only access to one SD-selected map pack. + * + * initialize() reads the bounded active marker and manifest transactionally: + * an already usable selection remains active if a replacement is malformed or + * unreadable. Only one tile stream may be open. Every read failure, explicit + * end, reinitialize, and destruction closes that stream. + */ +class MapTilePack { +public: + static const char ACTIVE_PACK_PATH[]; + static const std::size_t PATH_CAPACITY = 80U; + static const std::size_t MANIFEST_BUFFER_CAPACITY = Pyxis::MapPackManifest::MAX_SERIALIZED_SIZE; + + explicit MapTilePack(MapTileStorage& storage); + ~MapTilePack(); + + MapTilePackResult initialize(); + + MapTilePackStatus status() const { return status_; } + bool hasSelection() const { return status_ == MapTilePackStatus::READY; } + const Pyxis::MapPackManifest& metadata() const { return manifest_; } + + static bool isValidPackId(const char* pack_id); + static MapTilePackResult manifestPath(const char* pack_id, char* output, std::size_t capacity); + static MapTilePackResult tilePath(const char* pack_id, const TileKey& key, + char* output, std::size_t capacity); + + MapTilePackResult beginGet(const TileKey& key, std::uint32_t& size); + MapTilePackResult readGetChunk(std::uint8_t* output, std::size_t capacity, + std::size_t& count); + void endGet(); + + static std::size_t ramBytes() { return sizeof(MapTilePack); } + +private: + MapTileStorage& storage_; + Pyxis::MapPackManifest manifest_; + MapTilePackStatus status_; + bool stream_open_; + std::uint32_t stream_remaining_; + std::uint8_t manifest_buffer_[MANIFEST_BUFFER_CAPACITY]; + + MapTilePack(const MapTilePack&); + MapTilePack& operator=(const MapTilePack&); + + MapTilePackResult failInitialize(MapTilePackResult result, MapTilePackStatus initial_status, + bool had_selection); + MapTilePackResult loadFile(const char* path, std::uint8_t* output, std::size_t capacity, + std::size_t& length, MapTilePackResult missing_result, + MapTilePackResult oversized_result); + static bool isValidKey(const TileKey& key); + static MapTilePackResult makePath(const char* pack_id, const TileKey* key, + bool manifest, char* output, std::size_t capacity); +}; + +} // namespace TDeck +} // namespace Hardware + +#endif diff --git a/tests/native/test_map_tile_pack.cpp b/tests/native/test_map_tile_pack.cpp new file mode 100644 index 00000000..6aa86e4e --- /dev/null +++ b/tests/native/test_map_tile_pack.cpp @@ -0,0 +1,321 @@ +#include "Hardware/TDeck/MapTilePack.h" + +#include +#include +#include +#include +#include +#include + +using Hardware::TDeck::MapTilePack; +using Hardware::TDeck::MapTilePackResult; +using Hardware::TDeck::MapTilePackStatus; +using Hardware::TDeck::MapTileStorage; +using Hardware::TDeck::TileKey; +using Hardware::TDeck::TileStoreResult; +using Pyxis::MapPackManifest; + +namespace { +std::size_t allocations = 0U; +std::size_t tests_run = 0U; +void fail(const char* expression, int line) { + std::cerr << "line " << line << ": " << expression << '\n'; + std::exit(1); +} +#define CHECK(expression) do { if (!(expression)) fail(#expression, __LINE__); } while (false) +void beginTest() { ++tests_run; } +} + +void* operator new(std::size_t size) { + ++allocations; + void* memory = std::malloc(size); + if (memory == NULL) throw std::bad_alloc(); + return memory; +} +void* operator new[](std::size_t size) { + ++allocations; + void* memory = std::malloc(size); + if (memory == NULL) throw std::bad_alloc(); + return memory; +} +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete[](void* memory) noexcept { std::free(memory); } + +namespace { +struct File { + char path[MapTilePack::PATH_CAPACITY]; + std::uint8_t bytes[MapPackManifest::MAX_SERIALIZED_SIZE]; + std::size_t size; + std::uint32_t declared_size; +}; + +class FakeStorage : public MapTileStorage { +public: + FakeStorage() + : available(true), file_count(0U), open_file(NULL), position(0U), + read_calls(0U), end_calls(0U), fail_read_call(0U), zero_read_call(0U) {} + + void clear() { file_count = 0U; open_file = NULL; position = 0U; } + void add(const char* path, const std::uint8_t* bytes, std::size_t size) { + CHECK(file_count < 8U); + CHECK(std::strlen(path) < sizeof(files[0].path)); + CHECK(size <= sizeof(files[0].bytes)); + File& file = files[file_count++]; + std::strcpy(file.path, path); + if (size != 0U) std::memcpy(file.bytes, bytes, size); + file.size = size; + file.declared_size = static_cast(size); + } + void addText(const char* path, const char* text) { + add(path, reinterpret_cast(text), std::strlen(text)); + } + void setDeclaredSize(const char* path, std::uint32_t size) { + File* file = find(path); CHECK(file != NULL); file->declared_size = size; + } + File* find(const char* path) { + for (std::size_t index = 0U; index < file_count; ++index) { + if (std::strcmp(files[index].path, path) == 0) return &files[index]; + } + return NULL; + } + + virtual bool isAvailable() const { return available; } + virtual TileStoreResult beginRead(const char* path, std::uint32_t& size) { + if (!available) return TileStoreResult::STORAGE_UNAVAILABLE; + if (open_file != NULL) return TileStoreResult::BUSY; + File* file = find(path); + if (file == NULL) return TileStoreResult::MISS; + open_file = file; position = 0U; read_calls = 0U; size = file->declared_size; + return TileStoreResult::OK; + } + virtual TileStoreResult readChunk(std::uint8_t* output, std::size_t capacity, std::size_t& count) { + count = 0U; + if (open_file == NULL) return TileStoreResult::NOT_INITIALIZED; + ++read_calls; + if (fail_read_call != 0U && read_calls == fail_read_call) return TileStoreResult::IO_ERROR; + if (zero_read_call != 0U && read_calls == zero_read_call) return TileStoreResult::OK; + const std::size_t remaining = open_file->size - position; + const std::size_t amount = remaining < capacity ? remaining : capacity; + if (amount != 0U) std::memcpy(output, open_file->bytes + position, amount); + position += amount; count = amount; + return TileStoreResult::OK; + } + virtual void endRead() { if (open_file != NULL) { open_file = NULL; ++end_calls; } } + virtual TileStoreResult beginWrite(const char*) { return TileStoreResult::IO_ERROR; } + virtual TileStoreResult writeChunk(const std::uint8_t*, std::size_t, std::size_t&) { return TileStoreResult::IO_ERROR; } + virtual TileStoreResult commitWrite() { return TileStoreResult::IO_ERROR; } + virtual void abortWrite() {} + virtual TileStoreResult remove(const char*) { return TileStoreResult::IO_ERROR; } + virtual TileStoreResult rename(const char*, const char*) { return TileStoreResult::IO_ERROR; } + virtual TileStoreResult stat(const char*, std::uint32_t&) { return TileStoreResult::IO_ERROR; } + virtual TileStoreResult beginList() { return TileStoreResult::IO_ERROR; } + virtual TileStoreResult nextList(char*, std::size_t, bool&) { return TileStoreResult::IO_ERROR; } + virtual void endList() {} + + bool available; + File files[8]; + std::size_t file_count; + File* open_file; + std::size_t position; + std::size_t read_calls; + std::size_t end_calls; + std::size_t fail_read_call; + std::size_t zero_read_call; +}; + +MapPackManifest manifestFor(const char* id) { + MapPackManifest manifest = {}; + std::strcpy(manifest.pack_id, id); + std::strcpy(manifest.name, "Test Pack"); + std::strcpy(manifest.attribution, "Test attribution"); + std::strcpy(manifest.source, "local-test"); + std::strcpy(manifest.license, "CC0"); + manifest.min_zoom = 2U; manifest.max_zoom = 2U; manifest.extent_count = 1U; + manifest.tile_count = 4U; + manifest.extents[0].zoom = 2U; manifest.extents[0].interval_count = 1U; + manifest.extents[0].x[0].minimum = 1U; manifest.extents[0].x[0].maximum = 2U; + manifest.extents[0].y_minimum = 1U; manifest.extents[0].y_maximum = 2U; + return manifest; +} + +void addSelection(FakeStorage& storage, const char* marker_id, const MapPackManifest& manifest) { + storage.addText(MapTilePack::ACTIVE_PACK_PATH, marker_id); + std::uint8_t bytes[MapPackManifest::MAX_SERIALIZED_SIZE]; + std::size_t written = 0U; + CHECK(MapPackManifest::serialize(manifest, bytes, sizeof(bytes), written) == Pyxis::ManifestResult::OK); + char path[MapTilePack::PATH_CAPACITY]; + CHECK(MapTilePack::manifestPath(marker_id, path, sizeof(path)) == MapTilePackResult::OK); + storage.add(path, bytes, written); +} + +void testNoSelection() { + beginTest(); FakeStorage storage; MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::NO_SELECTION); + CHECK(pack.status() == MapTilePackStatus::NO_SELECTION); CHECK(!pack.hasSelection()); + storage.add(MapTilePack::ACTIVE_PACK_PATH, NULL, 0U); + CHECK(pack.initialize() == MapTilePackResult::NO_SELECTION); +} +void testInvalidAndTraversalIds() { + beginTest(); const char* bad[] = {"../bad", "bad/id", "UPPER", "a.b", "abcdefghijklmnopqrstuvwxyzabcdef"}; + for (std::size_t index = 0U; index < sizeof(bad) / sizeof(bad[0]); ++index) { + FakeStorage storage; storage.addText(MapTilePack::ACTIVE_PACK_PATH, bad[index]); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::INVALID_PACK_ID); + CHECK(pack.status() == MapTilePackStatus::INVALID_SELECTION); + } +} +void testMissingManifest() { + beginTest(); FakeStorage storage; storage.addText(MapTilePack::ACTIVE_PACK_PATH, "one"); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::MANIFEST_MISSING); + CHECK(pack.status() == MapTilePackStatus::INVALID_SELECTION); +} +void testCorruptAndOversizedManifest() { + beginTest(); + FakeStorage corrupt; corrupt.addText(MapTilePack::ACTIVE_PACK_PATH, "one"); corrupt.addText("/pyxis-map/packs/one/manifest.pmp", "bad"); + MapTilePack first(corrupt); CHECK(first.initialize() == MapTilePackResult::INVALID_MANIFEST); CHECK(corrupt.end_calls == 2U); + FakeStorage large; large.addText(MapTilePack::ACTIVE_PACK_PATH, "one"); large.addText("/pyxis-map/packs/one/manifest.pmp", "x"); + large.setDeclaredSize("/pyxis-map/packs/one/manifest.pmp", static_cast(MapPackManifest::MAX_SERIALIZED_SIZE + 1U)); + MapTilePack second(large); CHECK(second.initialize() == MapTilePackResult::MANIFEST_TOO_LARGE); CHECK(large.end_calls == 2U); +} +void testMarkerManifestMismatch() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("two")); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::PACK_ID_MISMATCH); CHECK(!pack.hasSelection()); +} +void testUncoveredKey() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::OK); std::uint32_t size = 99U; + CHECK(pack.beginGet(TileKey{2U, 0U, 0U}, size) == MapTilePackResult::UNCOVERED); CHECK(size == 99U); +} +void testCoveredMissingFile() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::OK); std::uint32_t size = 0U; + CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::TILE_MISSING); +} +void testCanonicalPathsAndCapacity() { + beginTest(); char path[MapTilePack::PATH_CAPACITY]; const char* id = "abcdefghijklmnopqrstuvwxyz12345"; + CHECK(MapTilePack::manifestPath(id, path, sizeof(path)) == MapTilePackResult::OK); + CHECK(std::strcmp(path, "/pyxis-map/packs/abcdefghijklmnopqrstuvwxyz12345/manifest.pmp") == 0); + CHECK(MapTilePack::tilePath(id, TileKey{22U, 4194303U, 4194303U}, path, sizeof(path)) == MapTilePackResult::OK); + CHECK(std::strcmp(path, "/pyxis-map/packs/abcdefghijklmnopqrstuvwxyz12345/tiles/22/4194303/4194303.png") == 0); + char short_path[77]; std::memset(short_path, 'q', sizeof(short_path)); + CHECK(MapTilePack::tilePath(id, TileKey{22U, 4194303U, 4194303U}, short_path, sizeof(short_path)) == MapTilePackResult::PATH_TOO_LONG); + CHECK(short_path[0] == 'q'); + CHECK(MapTilePack::tilePath("../x", TileKey{2U, 1U, 1U}, path, sizeof(path)) == MapTilePackResult::INVALID_PACK_ID); +} +void testStorageUnavailable() { + beginTest(); FakeStorage storage; storage.available = false; MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::STORAGE_UNAVAILABLE); CHECK(pack.status() == MapTilePackStatus::STORAGE_UNAVAILABLE); +} +void testChunkReadAndAutomaticEnd() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); + const std::uint8_t tile[] = {1U, 2U, 3U}; storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", tile, sizeof(tile)); + MapTilePack pack(storage); CHECK(pack.initialize() == MapTilePackResult::OK); const std::size_t before = storage.end_calls; + std::uint32_t size = 0U; CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); CHECK(size == 3U); + std::uint8_t output[2] = {}; std::size_t count = 0U; + CHECK(pack.readGetChunk(output, sizeof(output), count) == MapTilePackResult::OK); CHECK(count == 2U && output[0] == 1U && output[1] == 2U); + CHECK(pack.readGetChunk(output, sizeof(output), count) == MapTilePackResult::OK); CHECK(count == 1U && output[0] == 3U); + CHECK(storage.end_calls == before + 1U); + CHECK(pack.readGetChunk(output, sizeof(output), count) == MapTilePackResult::NOT_STREAMING); CHECK(count == 0U); +} +void testReadErrorClosesAndArgumentsAreBounded() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); const std::uint8_t tile[] = {1U}; + storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", tile, sizeof(tile)); MapTilePack pack(storage); CHECK(pack.initialize() == MapTilePackResult::OK); + std::uint32_t size = 0U; CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); storage.fail_read_call = 1U; + std::uint8_t output = 0U; std::size_t count = 55U; const std::size_t before = storage.end_calls; + CHECK(pack.readGetChunk(&output, 1U, count) == MapTilePackResult::IO_ERROR); CHECK(count == 0U); CHECK(storage.end_calls == before + 1U); + CHECK(pack.readGetChunk(&output, 1U, count) == MapTilePackResult::NOT_STREAMING); + storage.fail_read_call = 0U; + CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); + const std::size_t argument_before = storage.end_calls; + CHECK(pack.readGetChunk(NULL, 1U, count) == MapTilePackResult::INVALID_ARGUMENT); + CHECK(storage.end_calls == argument_before + 1U); + CHECK(pack.readGetChunk(&output, 1U, count) == MapTilePackResult::NOT_STREAMING); +} +void testPrematureEndCloses() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); const std::uint8_t tile[] = {1U, 2U}; + storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", tile, sizeof(tile)); MapTilePack pack(storage); CHECK(pack.initialize() == MapTilePackResult::OK); + std::uint32_t size = 0U; CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); storage.zero_read_call = 1U; + std::uint8_t output[2]; std::size_t count = 0U; const std::size_t before = storage.end_calls; + CHECK(pack.readGetChunk(output, sizeof(output), count) == MapTilePackResult::IO_ERROR); CHECK(storage.end_calls == before + 1U); +} +void testReinitializeSelectionChange() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::OK); CHECK(std::strcmp(pack.metadata().pack_id, "one") == 0); + storage.clear(); addSelection(storage, "two", manifestFor("two")); + CHECK(pack.initialize() == MapTilePackResult::OK); CHECK(std::strcmp(pack.metadata().pack_id, "two") == 0); +} +void testFailedReinitializeIsTransactional() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); MapTilePack pack(storage); + CHECK(pack.initialize() == MapTilePackResult::OK); storage.clear(); storage.addText(MapTilePack::ACTIVE_PACK_PATH, "bad/../id"); + CHECK(pack.initialize() == MapTilePackResult::INVALID_PACK_ID); CHECK(pack.status() == MapTilePackStatus::READY); + CHECK(pack.hasSelection()); CHECK(std::strcmp(pack.metadata().pack_id, "one") == 0); +} +void testReinitializeAndDestructorCloseStreams() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); const std::uint8_t tile[] = {1U}; + storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", tile, sizeof(tile)); + { + MapTilePack pack(storage); CHECK(pack.initialize() == MapTilePackResult::OK); std::uint32_t size = 0U; + CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); const std::size_t before = storage.end_calls; + CHECK(pack.initialize() == MapTilePackResult::OK); CHECK(storage.end_calls > before); + CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); + } + CHECK(storage.open_file == NULL); +} +void testDeclaredLengthsAndEmbeddedNulFailClosed() { + beginTest(); + FakeStorage marker_storage; + const std::uint8_t bad_marker[] = {'o', 'n', 'e', 0U, 'x'}; + marker_storage.add(MapTilePack::ACTIVE_PACK_PATH, bad_marker, sizeof(bad_marker)); + MapTilePack marker_pack(marker_storage); + CHECK(marker_pack.initialize() == MapTilePackResult::INVALID_PACK_ID); + + FakeStorage short_marker; + addSelection(short_marker, "one", manifestFor("one")); + short_marker.setDeclaredSize(MapTilePack::ACTIVE_PACK_PATH, 2U); + MapTilePack short_marker_pack(short_marker); + CHECK(short_marker_pack.initialize() == MapTilePackResult::IO_ERROR); + + FakeStorage trailing_manifest; + addSelection(trailing_manifest, "one", manifestFor("one")); + File* manifest = trailing_manifest.find("/pyxis-map/packs/one/manifest.pmp"); CHECK(manifest != NULL); + CHECK(manifest->size + 1U <= sizeof(manifest->bytes)); + manifest->bytes[manifest->size++] = 0xa5U; + MapTilePack trailing_manifest_pack(trailing_manifest); + CHECK(trailing_manifest_pack.initialize() == MapTilePackResult::IO_ERROR); +} +void testTileDeclaredLengthCapsWritesAndProbesEof() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); + const std::uint8_t tile[] = {1U, 2U}; + storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", tile, sizeof(tile)); + storage.setDeclaredSize("/pyxis-map/packs/one/tiles/2/1/1.png", 1U); + MapTilePack pack(storage); CHECK(pack.initialize() == MapTilePackResult::OK); + std::uint32_t size = 0U; CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); CHECK(size == 1U); + std::uint8_t output[2] = {0xa5U, 0xa5U}; std::size_t count = 99U; + CHECK(pack.readGetChunk(output, sizeof(output), count) == MapTilePackResult::IO_ERROR); + CHECK(count == 0U); CHECK(output[1] == 0xa5U); CHECK(storage.open_file == NULL); + + FakeStorage empty_storage; addSelection(empty_storage, "one", manifestFor("one")); + empty_storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", NULL, 0U); + MapTilePack empty_pack(empty_storage); CHECK(empty_pack.initialize() == MapTilePackResult::OK); + size = 99U; CHECK(empty_pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); CHECK(size == 0U); + count = 99U; CHECK(empty_pack.readGetChunk(output, sizeof(output), count) == MapTilePackResult::OK); + CHECK(count == 0U); CHECK(empty_storage.open_file == NULL); +} +void testCoreDoesNotAllocate() { + beginTest(); FakeStorage storage; addSelection(storage, "one", manifestFor("one")); const std::uint8_t tile[] = {7U}; + storage.add("/pyxis-map/packs/one/tiles/2/1/1.png", tile, sizeof(tile)); const std::size_t before = allocations; + MapTilePack pack(storage); CHECK(pack.initialize() == MapTilePackResult::OK); std::uint32_t size = 0U; + CHECK(pack.beginGet(TileKey{2U, 1U, 1U}, size) == MapTilePackResult::OK); std::uint8_t output = 0U; std::size_t count = 0U; + CHECK(pack.readGetChunk(&output, 1U, count) == MapTilePackResult::OK); pack.endGet(); CHECK(allocations == before); +} +} // namespace + +int main() { + testNoSelection(); testInvalidAndTraversalIds(); testMissingManifest(); testCorruptAndOversizedManifest(); + testMarkerManifestMismatch(); testUncoveredKey(); testCoveredMissingFile(); testCanonicalPathsAndCapacity(); + testStorageUnavailable(); testChunkReadAndAutomaticEnd(); testReadErrorClosesAndArgumentsAreBounded(); + testPrematureEndCloses(); testReinitializeSelectionChange(); testFailedReinitializeIsTransactional(); + testReinitializeAndDestructorCloseStreams(); testDeclaredLengthsAndEmbeddedNulFailClosed(); + testTileDeclaredLengthCapsWritesAndProbesEof(); testCoreDoesNotAllocate(); + std::cout << "map tile pack: " << tests_run << " tests passed\n"; + return 0; +} diff --git a/tests/native/test_map_tile_pack.py b/tests/native/test_map_tile_pack.py new file mode 100644 index 00000000..c060cc43 --- /dev/null +++ b/tests/native/test_map_tile_pack.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from pathlib import Path +import os +import subprocess + +import pytest + +from native_test import find_cxx + +ROOT = Path(__file__).resolve().parents[2] +TEST_SOURCE = ROOT / "tests/native/test_map_tile_pack.cpp" +PACK_SOURCE = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTilePack.cpp" +MANIFEST_SOURCE = ROOT / "lib/tdeck_ui/UI/LXMF/MapPackManifest.cpp" + + +@pytest.mark.parametrize("sanitize", [False, True], ids=["strict-cxx11", "asan-ubsan"]) +def test_map_tile_pack(tmp_path: Path, sanitize: bool) -> None: + binary = tmp_path / "test_map_tile_pack" + command = [ + find_cxx(), "-std=c++11", "-Wall", "-Wextra", "-Werror", "-pedantic", + "-Wconversion", "-Wsign-conversion", f"-I{ROOT / 'lib/tdeck_ui'}", + str(TEST_SOURCE), str(PACK_SOURCE), str(MANIFEST_SOURCE), "-o", str(binary), + ] + if sanitize: + command[1:1] = ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"] + compiled = subprocess.run(command, capture_output=True, text=True, timeout=60) + assert compiled.returncode == 0, compiled.stdout + compiled.stderr + env = os.environ.copy() + if sanitize: + env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=1" + env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1" + ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env) + assert ran.returncode == 0, ran.stdout + ran.stderr + assert ran.stdout == "map tile pack: 18 tests passed\n"