From 07f8f33e52e678e5b9a5537c2abdc92aa38bd2cc Mon Sep 17 00:00:00 2001 From: bonesyjr Date: Tue, 14 Jul 2026 19:53:32 -0400 Subject: [PATCH] Convert a PMTiles metadata decompression failure into an error response (#4399) Co-authored-by: Bart Louwers --- platform/android/CHANGELOG.md | 6 ++ .../src/mbgl/storage/pmtiles_file_source.cpp | 9 +- platform/ios/CHANGELOG.md | 4 + .../pmtiles/corrupt-gzip-metadata.pmtiles | Bin 0 -> 166 bytes .../storage/pmtiles/corrupt-gzip-metadata.py | 83 ++++++++++++++++++ test/storage/pmtiles_file_source.test.cpp | 21 +++++ 6 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/storage/pmtiles/corrupt-gzip-metadata.pmtiles create mode 100644 test/fixtures/storage/pmtiles/corrupt-gzip-metadata.py diff --git a/platform/android/CHANGELOG.md b/platform/android/CHANGELOG.md index 34bd0aa01..6bf9fe496 100644 --- a/platform/android/CHANGELOG.md +++ b/platform/android/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog MapLibre Native for Android +## main + +### 🐞 Bug fixes + +- Convert a PMTiles metadata decompression failure into an error response instead of an uncaught exception ([#4399](https://github.com/maplibre/maplibre-native/pull/4399)). + ## 13.3.1 ### ✨ Features and improvements diff --git a/platform/default/src/mbgl/storage/pmtiles_file_source.cpp b/platform/default/src/mbgl/storage/pmtiles_file_source.cpp index 6991e9c1a..e82da09f0 100644 --- a/platform/default/src/mbgl/storage/pmtiles_file_source.cpp +++ b/platform/default/src/mbgl/storage/pmtiles_file_source.cpp @@ -450,7 +450,14 @@ private: std::string data = *responseMetadata.data; if (header.internal_compression == pmtiles::COMPRESSION_GZIP) { - data = util::decompress(data); + try { + data = util::decompress(data); + } catch (const std::exception& e) { + callback(std::make_unique( + Response::Error::Reason::Other, + std::string("Error decompressing PMTiles metadata: ") + e.what())); + return; + } } parse_callback(data); diff --git a/platform/ios/CHANGELOG.md b/platform/ios/CHANGELOG.md index ad485564a..f3fa00206 100644 --- a/platform/ios/CHANGELOG.md +++ b/platform/ios/CHANGELOG.md @@ -2,6 +2,10 @@ MapLibre welcomes participation and contributions from everyone. Please read [`MapLibre iOS Developer Guide`](https://maplibre.org/maplibre-native/docs/book/ios/index.html) to get started. +## main + +- Convert a PMTiles metadata decompression failure into an error response instead of an uncaught exception ([#4399](https://github.com/maplibre/maplibre-native/pull/4399)). + ## 6.27.0 - Implement ambient cache for PMTiles sources ([#4290](https://github.com/maplibre/maplibre-native/pull/4290)). diff --git a/test/fixtures/storage/pmtiles/corrupt-gzip-metadata.pmtiles b/test/fixtures/storage/pmtiles/corrupt-gzip-metadata.pmtiles new file mode 100644 index 0000000000000000000000000000000000000000..53e7cef9aa6ac42ab155f1e9a1708e47c182c55d GIT binary patch literal 166 zcmWIW4av+&EoQD~fB;b_Jq1b&LFr{s8b+b>Vd5Ax6C)E)OmEv12Jz@q43|8!8D1_i gg{zY9<^Z#q{wFgq^uA}h#0ccdci&sP?>#aG06!5O8UO$Q literal 0 HcmV?d00001 diff --git a/test/fixtures/storage/pmtiles/corrupt-gzip-metadata.py b/test/fixtures/storage/pmtiles/corrupt-gzip-metadata.py new file mode 100644 index 000000000..a1ee5899d --- /dev/null +++ b/test/fixtures/storage/pmtiles/corrupt-gzip-metadata.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Generate corrupt-gzip-metadata.pmtiles fixture. + +This fixture is a minimal PMTiles v3 archive with: + - internal_compression = COMPRESSION_GZIP (0x2) + - json_metadata_bytes > 0, starting with gzip magic bytes (0x1f 0x8b) but otherwise corrupt + +Used by test PMTilesFileSource.CorruptGzipMetadata to verify that a decompression +failure on the JSON metadata block is converted to an error response instead of +propagating as an exception out of the PMTiles file source thread. +""" + +import gzip +import os +import struct + + +def write_varint(value): + result = bytearray() + while value >= 0x80: + result.append((value & 0x7f) | 0x80) + value >>= 7 + result.append(value) + return bytes(result) + + +# Root directory: zero entries, gzip-compressed because internal_compression = GZIP. +# It is valid, so the failure under test is isolated to the metadata block. +DIR_DATA = gzip.compress(write_varint(0)) + +# Metadata: starts with gzip magic bytes (0x1f 0x8b) but the rest is garbage, so +# util::decompress() throws. +METADATA = b'\x1f\x8b' + b'\xde\xad\xbe\xef' * 4 # 18 bytes of corrupt "gzip" + +root_dir_offset = 127 +root_dir_bytes = len(DIR_DATA) +json_meta_offset = root_dir_offset + root_dir_bytes +json_meta_bytes = len(METADATA) +leaf_dirs_offset = json_meta_offset + json_meta_bytes +leaf_dirs_bytes = 0 +tile_data_offset = leaf_dirs_offset +tile_data_bytes = 0 + +hdr = b'PMTiles' +hdr += struct.pack(' 0 +hdr += struct.pack(' req = pmtiles.request( + {Resource::Unknown, toAbsoluteURL("corrupt-gzip-metadata.pmtiles")}, [&](Response res) { + req.reset(); + ASSERT_NE(nullptr, res.error); + EXPECT_EQ(Response::Error::Reason::Other, res.error->reason); + EXPECT_NE(res.error->message.find("Error decompressing PMTiles metadata:"), std::string::npos); + loop.stop(); + }); + + loop.run(); +} + // Regression test for https://github.com/maplibre/maplibre-native/issues/3690 — the loading // method for PMTiles sub-requests changed from `Network` to `All` so that http(s)-hosted // archives flow through the ambient cache. The file:// path bypasses the cache by design,