Convert a PMTiles metadata decompression failure into an error response (#4399)
Co-authored-by: Bart Louwers <bart@emeel.net>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
Response::Error::Reason::Other,
|
||||
std::string("Error decompressing PMTiles metadata: ") + e.what()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
parse_callback(data);
|
||||
|
||||
@@ -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)).
|
||||
|
||||
BIN
test/fixtures/storage/pmtiles/corrupt-gzip-metadata.pmtiles
vendored
Normal file
BIN
test/fixtures/storage/pmtiles/corrupt-gzip-metadata.pmtiles
vendored
Normal file
Binary file not shown.
83
test/fixtures/storage/pmtiles/corrupt-gzip-metadata.py
vendored
Normal file
83
test/fixtures/storage/pmtiles/corrupt-gzip-metadata.py
vendored
Normal file
@@ -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('<B', 3)
|
||||
hdr += struct.pack('<Q', root_dir_offset)
|
||||
hdr += struct.pack('<Q', root_dir_bytes)
|
||||
hdr += struct.pack('<Q', json_meta_offset) # json_metadata_offset
|
||||
hdr += struct.pack('<Q', json_meta_bytes) # json_metadata_bytes > 0
|
||||
hdr += struct.pack('<Q', leaf_dirs_offset) # leaf_dirs_offset
|
||||
hdr += struct.pack('<Q', leaf_dirs_bytes) # leaf_dirs_bytes = 0
|
||||
hdr += struct.pack('<Q', tile_data_offset)
|
||||
hdr += struct.pack('<Q', tile_data_bytes)
|
||||
hdr += struct.pack('<Q', 0) # addressed_tiles_count
|
||||
hdr += struct.pack('<Q', 0) # tile_entries_count
|
||||
hdr += struct.pack('<Q', 0) # tile_contents_count
|
||||
hdr += struct.pack('<B', 0) # clustered = false
|
||||
hdr += struct.pack('<B', 2) # internal_compression = GZIP
|
||||
hdr += struct.pack('<B', 1) # tile_compression = NONE
|
||||
hdr += struct.pack('<B', 2) # tile_type = PNG
|
||||
hdr += struct.pack('<B', 0) # min_zoom
|
||||
hdr += struct.pack('<B', 0) # max_zoom
|
||||
hdr += struct.pack('<i', -1800000000)
|
||||
hdr += struct.pack('<i', -900000000)
|
||||
hdr += struct.pack('<i', 1800000000)
|
||||
hdr += struct.pack('<i', 900000000)
|
||||
hdr += struct.pack('<B', 0)
|
||||
hdr += struct.pack('<i', 0)
|
||||
hdr += struct.pack('<i', 0)
|
||||
|
||||
assert len(hdr) == 127, f"Header is {len(hdr)} bytes"
|
||||
|
||||
out = hdr + DIR_DATA + METADATA
|
||||
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
path = os.path.join(script_dir, 'corrupt-gzip-metadata.pmtiles')
|
||||
|
||||
with open(path, 'wb') as f:
|
||||
f.write(out)
|
||||
|
||||
print(f"Written {len(out)} bytes to {path}")
|
||||
print(" internal_compression = GZIP (0x02) — metadata starts with gzip magic but is corrupt")
|
||||
@@ -124,6 +124,27 @@ TEST(PMTilesFileSource, CorruptGzipTile) {
|
||||
loop.run();
|
||||
}
|
||||
|
||||
// Metadata whose bytes start with gzip magic but are otherwise corrupt must yield an error
|
||||
// response — the decompression failure must not propagate as an exception. Unlike the tile path,
|
||||
// this one runs on the PMTiles file source thread with no handler above it, so an escaping
|
||||
// exception calls std::terminate and aborts the process.
|
||||
TEST(PMTilesFileSource, CorruptGzipMetadata) {
|
||||
util::RunLoop loop;
|
||||
|
||||
PMTilesFileSource pmtiles(ResourceOptions::Default(), ClientOptions());
|
||||
|
||||
std::unique_ptr<AsyncRequest> 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,
|
||||
|
||||
Reference in New Issue
Block a user