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
|
# 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
|
## 13.3.1
|
||||||
|
|
||||||
### ✨ Features and improvements
|
### ✨ Features and improvements
|
||||||
|
|||||||
@@ -450,7 +450,14 @@ private:
|
|||||||
std::string data = *responseMetadata.data;
|
std::string data = *responseMetadata.data;
|
||||||
|
|
||||||
if (header.internal_compression == pmtiles::COMPRESSION_GZIP) {
|
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);
|
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.
|
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
|
## 6.27.0
|
||||||
|
|
||||||
- Implement ambient cache for PMTiles sources ([#4290](https://github.com/maplibre/maplibre-native/pull/4290)).
|
- 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();
|
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
|
// 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
|
// 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,
|
// archives flow through the ambient cache. The file:// path bypasses the cache by design,
|
||||||
|
|||||||
Reference in New Issue
Block a user