// Copyright (c) 2026 Pyxis contributors // SPDX-License-Identifier: MIT #include "UI/LXMF/MapPackManifest.h" #include #include namespace Pyxis { namespace { const std::size_t HEADER_SIZE = 16U; const std::size_t CRC_SIZE = 4U; const std::size_t EXTENT_SIZE = 26U; const std::size_t ROW_SPAN_SIZE = 13U; const std::uint8_t MAGIC[4] = {'P', 'M', 'P', 'K'}; void putU16(std::uint8_t* output, std::uint16_t value) { output[0] = static_cast(value); output[1] = static_cast(value >> 8U); } void putU32(std::uint8_t* output, std::uint32_t value) { output[0] = static_cast(value); output[1] = static_cast(value >> 8U); output[2] = static_cast(value >> 16U); output[3] = static_cast(value >> 24U); } std::uint16_t getU16(const std::uint8_t* input) { return static_cast(input[0]) | static_cast(static_cast(input[1]) << 8U); } std::uint32_t getU32(const std::uint8_t* input) { return static_cast(input[0]) | (static_cast(input[1]) << 8U) | (static_cast(input[2]) << 16U) | (static_cast(input[3]) << 24U); } std::uint32_t crc32(const std::uint8_t* input, std::size_t length) { std::uint32_t crc = UINT32_C(0xffffffff); for (std::size_t index = 0U; index < length; ++index) { crc ^= input[index]; for (std::uint8_t bit = 0U; bit < 8U; ++bit) { crc = (crc >> 1U) ^ ((crc & 1U) != 0U ? UINT32_C(0xedb88320) : 0U); } } return ~crc; } ManifestResult checkedStringLength(const char* text, std::size_t capacity, bool pack_id, std::size_t& length) { length = 0U; while (length < capacity && text[length] != '\0') { const unsigned char character = static_cast(text[length]); if (character < 0x20U || character > 0x7eU) return ManifestResult::INVALID_STRING; if (pack_id && !((character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '_' || character == '-')) { return ManifestResult::INVALID_STRING; } ++length; } if (length == 0U || length == capacity) return ManifestResult::INVALID_STRING; return ManifestResult::OK; } ManifestResult validate(const MapPackManifest& manifest) { std::size_t ignored = 0U; if (checkedStringLength(manifest.pack_id, sizeof(manifest.pack_id), true, ignored) != ManifestResult::OK || checkedStringLength(manifest.name, sizeof(manifest.name), false, ignored) != ManifestResult::OK || checkedStringLength(manifest.attribution, sizeof(manifest.attribution), false, ignored) != ManifestResult::OK || checkedStringLength(manifest.source, sizeof(manifest.source), false, ignored) != ManifestResult::OK || checkedStringLength(manifest.license, sizeof(manifest.license), false, ignored) != ManifestResult::OK) { return ManifestResult::INVALID_STRING; } if (manifest.min_zoom > manifest.max_zoom || manifest.max_zoom > MapPackManifest::MAX_ZOOM) { return ManifestResult::INVALID_ZOOM; } const std::uint8_t expected_count = static_cast(manifest.max_zoom - manifest.min_zoom + 1U); if (manifest.extent_count != expected_count || manifest.extent_count > MapPackManifest::MAX_ZOOM_LEVELS) { return ManifestResult::INVALID_EXTENT; } std::uint64_t total = 0U; for (std::size_t index = 0U; index < manifest.extent_count; ++index) { const ZoomExtent& extent = manifest.extents[index]; const std::uint8_t expected_zoom = static_cast(manifest.min_zoom + index); if (extent.zoom != expected_zoom || extent.interval_count < 1U || extent.interval_count > 2U) { return ManifestResult::INVALID_EXTENT; } const std::uint32_t world_size = UINT32_C(1) << extent.zoom; const std::uint32_t world_maximum = world_size - 1U; if (extent.y_minimum > extent.y_maximum || extent.y_maximum > world_maximum) { return ManifestResult::INVALID_EXTENT; } std::uint64_t columns = 0U; for (std::size_t interval_index = 0U; interval_index < extent.interval_count; ++interval_index) { const XInterval& interval = extent.x[interval_index]; if (interval.minimum > interval.maximum || interval.maximum > world_maximum) { return ManifestResult::INVALID_EXTENT; } columns += static_cast(interval.maximum) - interval.minimum + 1U; } if (extent.interval_count == 1U) { if (extent.x[1].minimum != 0U || extent.x[1].maximum != 0U) { return ManifestResult::INVALID_EXTENT; } } else { if (extent.x[0].minimum != 0U || extent.x[1].maximum != world_maximum || extent.x[0].maximum >= extent.x[1].minimum || extent.x[0].maximum + 1U == extent.x[1].minimum) { return ManifestResult::INVALID_EXTENT; } } const std::uint64_t rows = static_cast(extent.y_maximum) - extent.y_minimum + 1U; const std::uint64_t level_tiles = columns * rows; if (level_tiles > std::numeric_limits::max() || total > std::numeric_limits::max() - level_tiles) { return ManifestResult::INVALID_TILE_COUNT; } total += level_tiles; } if (manifest.tile_count == 0U || total != manifest.tile_count) { return ManifestResult::INVALID_TILE_COUNT; } return ManifestResult::OK; } RowSpan readRowSpan(const std::uint8_t* input) { RowSpan span = {}; span.zoom = input[0]; span.y = getU32(input + 1U); span.x_minimum = getU32(input + 5U); span.x_maximum = getU32(input + 9U); return span; } void writeRowSpan(std::uint8_t* output, const RowSpan& span) { output[0] = span.zoom; putU32(output + 1U, span.y); putU32(output + 5U, span.x_minimum); putU32(output + 9U, span.x_maximum); } ManifestResult validateSparse(const MapPackManifest& manifest, const RowSpan* spans, std::size_t span_count) { std::size_t ignored = 0U; if (checkedStringLength(manifest.pack_id, sizeof(manifest.pack_id), true, ignored) != ManifestResult::OK || checkedStringLength(manifest.name, sizeof(manifest.name), false, ignored) != ManifestResult::OK || checkedStringLength(manifest.attribution, sizeof(manifest.attribution), false, ignored) != ManifestResult::OK || checkedStringLength(manifest.source, sizeof(manifest.source), false, ignored) != ManifestResult::OK || checkedStringLength(manifest.license, sizeof(manifest.license), false, ignored) != ManifestResult::OK) { return ManifestResult::INVALID_STRING; } if (manifest.min_zoom > manifest.max_zoom || manifest.max_zoom > MapPackManifest::MAX_ZOOM) { return ManifestResult::INVALID_ZOOM; } if (spans == 0 || span_count == 0U || span_count > MapPackManifest::MAX_ROW_SPANS) { return ManifestResult::INVALID_EXTENT; } std::uint32_t zoom_mask = 0U; std::uint64_t total = 0U; RowSpan previous = {}; for (std::size_t index = 0U; index < span_count; ++index) { const RowSpan& span = spans[index]; if (span.zoom < manifest.min_zoom || span.zoom > manifest.max_zoom) { return ManifestResult::INVALID_EXTENT; } const std::uint32_t world_maximum = (UINT32_C(1) << span.zoom) - 1U; if (span.y > world_maximum || span.x_minimum > span.x_maximum || span.x_maximum > world_maximum) { return ManifestResult::INVALID_EXTENT; } if (index != 0U && (span.zoom < previous.zoom || (span.zoom == previous.zoom && span.y < previous.y) || (span.zoom == previous.zoom && span.y == previous.y && span.x_minimum <= previous.x_maximum + 1U))) { return ManifestResult::INVALID_EXTENT; } zoom_mask |= UINT32_C(1) << span.zoom; const std::uint64_t count = static_cast(span.x_maximum) - span.x_minimum + 1U; if (total > std::numeric_limits::max() - count) { return ManifestResult::INVALID_TILE_COUNT; } total += count; previous = span; } std::uint32_t expected_mask = 0U; for (std::uint8_t zoom = manifest.min_zoom; zoom <= manifest.max_zoom; ++zoom) { expected_mask |= UINT32_C(1) << zoom; } if (zoom_mask != expected_mask) return ManifestResult::INVALID_ZOOM; if (manifest.tile_count == 0U || total != manifest.tile_count) { return ManifestResult::INVALID_TILE_COUNT; } return ManifestResult::OK; } void writeString(std::uint8_t* output, std::size_t& position, const char* text, std::size_t length) { output[position++] = static_cast(length); std::memcpy(output + position, text, length); position += length; } ManifestResult readString(const std::uint8_t* input, std::size_t payload_end, std::size_t& position, char* output, std::size_t capacity, bool pack_id) { if (position >= payload_end) return ManifestResult::BAD_LENGTH; const std::size_t length = input[position++]; if (length == 0U || length >= capacity || length > payload_end - position) { return length >= capacity ? ManifestResult::INVALID_STRING : ManifestResult::BAD_LENGTH; } for (std::size_t index = 0U; index < length; ++index) { const unsigned char character = input[position + index]; if (character < 0x20U || character > 0x7eU || (pack_id && !((character >= 'a' && character <= 'z') || (character >= '0' && character <= '9') || character == '_' || character == '-'))) { return ManifestResult::INVALID_STRING; } } std::memcpy(output, input + position, length); output[length] = '\0'; position += length; return ManifestResult::OK; } } // namespace ManifestResult MapPackManifest::serialize(const MapPackManifest& manifest, std::uint8_t* output, std::size_t capacity, std::size_t& written) { const ManifestResult validity = validate(manifest); if (validity != ManifestResult::OK) return validity; std::size_t lengths[5] = {0U, 0U, 0U, 0U, 0U}; (void)checkedStringLength(manifest.pack_id, sizeof(manifest.pack_id), true, lengths[0]); (void)checkedStringLength(manifest.name, sizeof(manifest.name), false, lengths[1]); (void)checkedStringLength(manifest.attribution, sizeof(manifest.attribution), false, lengths[2]); (void)checkedStringLength(manifest.source, sizeof(manifest.source), false, lengths[3]); (void)checkedStringLength(manifest.license, sizeof(manifest.license), false, lengths[4]); std::size_t required = HEADER_SIZE + CRC_SIZE + 7U + EXTENT_SIZE * manifest.extent_count; for (std::size_t index = 0U; index < 5U; ++index) required += 1U + lengths[index]; if (output == 0 || capacity < required) return ManifestResult::INSUFFICIENT_CAPACITY; std::uint8_t temporary[MAX_SERIALIZED_SIZE] = {}; std::memcpy(temporary, MAGIC, sizeof(MAGIC)); temporary[4] = LEGACY_FORMAT_VERSION; temporary[5] = 0U; putU16(temporary + 6U, static_cast(HEADER_SIZE)); putU32(temporary + 8U, static_cast(required)); putU32(temporary + 12U, 0U); std::size_t position = HEADER_SIZE; writeString(temporary, position, manifest.pack_id, lengths[0]); writeString(temporary, position, manifest.name, lengths[1]); writeString(temporary, position, manifest.attribution, lengths[2]); writeString(temporary, position, manifest.source, lengths[3]); writeString(temporary, position, manifest.license, lengths[4]); temporary[position++] = manifest.min_zoom; temporary[position++] = manifest.max_zoom; temporary[position++] = manifest.extent_count; putU32(temporary + position, manifest.tile_count); position += 4U; for (std::size_t index = 0U; index < manifest.extent_count; ++index) { const ZoomExtent& extent = manifest.extents[index]; temporary[position++] = extent.zoom; temporary[position++] = extent.interval_count; putU32(temporary + position, extent.y_minimum); position += 4U; putU32(temporary + position, extent.y_maximum); position += 4U; for (std::size_t interval = 0U; interval < 2U; ++interval) { putU32(temporary + position, extent.x[interval].minimum); position += 4U; putU32(temporary + position, extent.x[interval].maximum); position += 4U; } } putU32(temporary + position, crc32(temporary, position)); position += CRC_SIZE; if (position != required) return ManifestResult::BAD_LENGTH; std::memcpy(output, temporary, required); written = required; return ManifestResult::OK; } ManifestResult MapPackManifest::serializeSparse(const MapPackManifest& manifest, const RowSpan* spans, std::size_t span_count, std::uint8_t* output, std::size_t capacity, std::size_t& written) { const ManifestResult validity = validateSparse(manifest, spans, span_count); if (validity != ManifestResult::OK) return validity; std::size_t lengths[5] = {0U, 0U, 0U, 0U, 0U}; (void)checkedStringLength(manifest.pack_id, sizeof(manifest.pack_id), true, lengths[0]); (void)checkedStringLength(manifest.name, sizeof(manifest.name), false, lengths[1]); (void)checkedStringLength(manifest.attribution, sizeof(manifest.attribution), false, lengths[2]); (void)checkedStringLength(manifest.source, sizeof(manifest.source), false, lengths[3]); (void)checkedStringLength(manifest.license, sizeof(manifest.license), false, lengths[4]); std::size_t required = HEADER_SIZE + CRC_SIZE + 8U + ROW_SPAN_SIZE * span_count; for (std::size_t index = 0U; index < 5U; ++index) required += 1U + lengths[index]; if (output == 0 || capacity < required) return ManifestResult::INSUFFICIENT_CAPACITY; std::uint8_t temporary[MAX_SERIALIZED_SIZE] = {}; std::memcpy(temporary, MAGIC, sizeof(MAGIC)); temporary[4] = FORMAT_VERSION; temporary[5] = 0U; putU16(temporary + 6U, static_cast(HEADER_SIZE)); putU32(temporary + 8U, static_cast(required)); putU32(temporary + 12U, 0U); std::size_t position = HEADER_SIZE; writeString(temporary, position, manifest.pack_id, lengths[0]); writeString(temporary, position, manifest.name, lengths[1]); writeString(temporary, position, manifest.attribution, lengths[2]); writeString(temporary, position, manifest.source, lengths[3]); writeString(temporary, position, manifest.license, lengths[4]); temporary[position++] = manifest.min_zoom; temporary[position++] = manifest.max_zoom; putU16(temporary + position, static_cast(span_count)); position += 2U; putU32(temporary + position, manifest.tile_count); position += 4U; for (std::size_t index = 0U; index < span_count; ++index) { writeRowSpan(temporary + position, spans[index]); position += ROW_SPAN_SIZE; } putU32(temporary + position, crc32(temporary, position)); position += CRC_SIZE; if (position != required) return ManifestResult::BAD_LENGTH; std::memcpy(output, temporary, required); written = required; return ManifestResult::OK; } ManifestResult MapPackManifest::parse(const std::uint8_t* input, std::size_t length, MapPackManifest& output) { if (input == 0 || length < HEADER_SIZE + CRC_SIZE) return ManifestResult::BAD_LENGTH; if (std::memcmp(input, MAGIC, sizeof(MAGIC)) != 0) return ManifestResult::BAD_MAGIC; const std::uint8_t version = input[4]; if (version != LEGACY_FORMAT_VERSION && version != FORMAT_VERSION) { return ManifestResult::UNSUPPORTED_VERSION; } if (input[5] != 0U || getU16(input + 6U) != HEADER_SIZE || getU32(input + 12U) != 0U) { return ManifestResult::BAD_HEADER; } if (getU32(input + 8U) != length || length > MAX_SERIALIZED_SIZE) return ManifestResult::BAD_LENGTH; if (getU32(input + length - CRC_SIZE) != crc32(input, length - CRC_SIZE)) return ManifestResult::BAD_CRC; const std::size_t payload_end = length - CRC_SIZE; std::size_t position = HEADER_SIZE; MapPackManifest candidate = {}; ManifestResult result = readString(input, payload_end, position, candidate.pack_id, sizeof(candidate.pack_id), true); if (result != ManifestResult::OK) return result; result = readString(input, payload_end, position, candidate.name, sizeof(candidate.name), false); if (result != ManifestResult::OK) return result; result = readString(input, payload_end, position, candidate.attribution, sizeof(candidate.attribution), false); if (result != ManifestResult::OK) return result; result = readString(input, payload_end, position, candidate.source, sizeof(candidate.source), false); if (result != ManifestResult::OK) return result; result = readString(input, payload_end, position, candidate.license, sizeof(candidate.license), false); if (result != ManifestResult::OK) return result; candidate.format_version = version; if (version == LEGACY_FORMAT_VERSION) { if (payload_end - position < 7U) return ManifestResult::BAD_LENGTH; candidate.min_zoom = input[position++]; candidate.max_zoom = input[position++]; candidate.extent_count = input[position++]; candidate.tile_count = getU32(input + position); position += 4U; if (candidate.extent_count > MAX_ZOOM_LEVELS || static_cast(candidate.extent_count) > (payload_end - position) / EXTENT_SIZE) { return ManifestResult::INVALID_EXTENT; } for (std::size_t index = 0U; index < candidate.extent_count; ++index) { ZoomExtent& extent = candidate.extents[index]; extent.zoom = input[position++]; extent.interval_count = input[position++]; extent.y_minimum = getU32(input + position); position += 4U; extent.y_maximum = getU32(input + position); position += 4U; for (std::size_t interval = 0U; interval < 2U; ++interval) { extent.x[interval].minimum = getU32(input + position); position += 4U; extent.x[interval].maximum = getU32(input + position); position += 4U; } } if (position != payload_end) return ManifestResult::BAD_LENGTH; result = validate(candidate); } else { if (payload_end - position < 8U) return ManifestResult::BAD_LENGTH; candidate.min_zoom = input[position++]; candidate.max_zoom = input[position++]; candidate.row_span_count = getU16(input + position); position += 2U; candidate.tile_count = getU32(input + position); position += 4U; if (candidate.row_span_count == 0U || candidate.row_span_count > MAX_ROW_SPANS || static_cast(candidate.row_span_count) > (payload_end - position) / ROW_SPAN_SIZE) { return ManifestResult::INVALID_EXTENT; } candidate.row_span_bytes = input + position; if (candidate.min_zoom > candidate.max_zoom || candidate.max_zoom > MAX_ZOOM) { return ManifestResult::INVALID_ZOOM; } std::uint32_t zoom_mask = 0U; std::uint64_t total = 0U; RowSpan previous = {}; for (std::size_t index = 0U; index < candidate.row_span_count; ++index) { const RowSpan span = readRowSpan(input + position); const std::uint32_t world_maximum = span.zoom <= MAX_ZOOM ? (UINT32_C(1) << span.zoom) - 1U : 0U; if (span.zoom < candidate.min_zoom || span.zoom > candidate.max_zoom || span.y > world_maximum || span.x_minimum > span.x_maximum || span.x_maximum > world_maximum || (index != 0U && (span.zoom < previous.zoom || (span.zoom == previous.zoom && span.y < previous.y) || (span.zoom == previous.zoom && span.y == previous.y && span.x_minimum <= previous.x_maximum + 1U)))) { return ManifestResult::INVALID_EXTENT; } const std::uint64_t count = static_cast(span.x_maximum) - span.x_minimum + 1U; if (total > std::numeric_limits::max() - count) { return ManifestResult::INVALID_TILE_COUNT; } total += count; zoom_mask |= UINT32_C(1) << span.zoom; previous = span; position += ROW_SPAN_SIZE; } if (position != payload_end) return ManifestResult::BAD_LENGTH; std::uint32_t expected_mask = 0U; for (std::uint8_t zoom = candidate.min_zoom; zoom <= candidate.max_zoom; ++zoom) { expected_mask |= UINT32_C(1) << zoom; } result = zoom_mask != expected_mask ? ManifestResult::INVALID_ZOOM : (candidate.tile_count == 0U || total != candidate.tile_count ? ManifestResult::INVALID_TILE_COUNT : ManifestResult::OK); } if (result != ManifestResult::OK) return result; output = candidate; return ManifestResult::OK; } bool MapPackManifest::covers(const Hardware::TDeck::TileKey& key) const { if (key.zoom > MAX_ZOOM) return false; const std::uint32_t world_size = UINT32_C(1) << key.zoom; if (key.x >= world_size || key.y >= world_size || key.zoom < min_zoom || key.zoom > max_zoom) return false; if (format_version == FORMAT_VERSION) { if (row_span_bytes == 0 || row_span_count == 0U || row_span_count > MAX_ROW_SPANS) return false; for (std::size_t index = 0U; index < row_span_count; ++index) { const RowSpan span = readRowSpan(row_span_bytes + index * ROW_SPAN_SIZE); if (span.zoom > key.zoom || (span.zoom == key.zoom && span.y > key.y)) return false; if (span.zoom == key.zoom && span.y == key.y && key.x >= span.x_minimum && key.x <= span.x_maximum) return true; } return false; } const std::size_t index = static_cast(key.zoom - min_zoom); if (index >= extent_count) return false; const ZoomExtent& extent = extents[index]; if (extent.zoom != key.zoom || key.y < extent.y_minimum || key.y > extent.y_maximum || extent.interval_count < 1U || extent.interval_count > 2U) return false; for (std::size_t interval = 0U; interval < extent.interval_count; ++interval) { if (key.x >= extent.x[interval].minimum && key.x <= extent.x[interval].maximum) return true; } return false; } } // namespace Pyxis