From 88e7b2fa15ba6f2dbaf5f090d59908f633b1563e Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Wed, 13 May 2026 07:21:40 -0700 Subject: [PATCH 1/2] Honor max_pixels in VRT source reads (#1796) --- xrspatial/geotiff/_vrt.py | 5 +++ .../tests/test_vrt_source_max_pixels_1796.py | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py diff --git a/xrspatial/geotiff/_vrt.py b/xrspatial/geotiff/_vrt.py index 5a792ffa..b8e83661 100644 --- a/xrspatial/geotiff/_vrt.py +++ b/xrspatial/geotiff/_vrt.py @@ -871,10 +871,15 @@ def read_vrt(vrt_path: str, *, window=None, src.filename, window=(read_r0, read_c0, read_r1, read_c1), band=src.band - 1, # convert 1-based to 0-based + max_pixels=max_pixels, ) except ( OSError, ValueError, struct.error, ) + _CODEC_DECODE_EXCEPTIONS as e: + if (isinstance(e, ValueError) + and 'exceed' in str(e) + and 'safety limit' in str(e)): + raise # Under XRSPATIAL_GEOTIFF_STRICT=1, surface the read failure # so partial mosaics are caught in CI. Default mode warns # once per missing source then continues, preserving the diff --git a/xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py b/xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py new file mode 100644 index 00000000..7ce72288 --- /dev/null +++ b/xrspatial/geotiff/tests/test_vrt_source_max_pixels_1796.py @@ -0,0 +1,35 @@ +"""VRT source reads must honor the caller's max_pixels budget (#1796).""" +from __future__ import annotations + +import os + +import numpy as np +import pytest + +from xrspatial.geotiff import to_geotiff, read_vrt + + +def test_vrt_source_read_forwards_max_pixels(tmp_path): + """A tiny VRT output cannot force an oversized source-window decode.""" + src = tmp_path / "tmp_1796_source.tif" + data = np.arange(16, dtype=np.uint8).reshape(4, 4) + to_geotiff(data, str(src), compression='none') + + vrt = tmp_path / "tmp_1796_source_cap.vrt" + vrt.write_text( + '\n' + ' \n' + ' \n' + f' {os.path.basename(src)}' + '\n' + ' 1\n' + ' \n' + ' \n' + ' \n' + ' \n' + '\n' + ) + + with pytest.raises(ValueError, match="exceed"): + read_vrt(str(vrt), max_pixels=1) + From 362430a5b306cf8663423a368f0d084f2b4da11b Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Wed, 13 May 2026 08:19:26 -0700 Subject: [PATCH 2/2] Use structured pixel safety errors --- xrspatial/geotiff/_reader.py | 8 ++++++-- xrspatial/geotiff/_vrt.py | 6 ++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/xrspatial/geotiff/_reader.py b/xrspatial/geotiff/_reader.py index 19c2105f..ca776e87 100644 --- a/xrspatial/geotiff/_reader.py +++ b/xrspatial/geotiff/_reader.py @@ -47,11 +47,15 @@ MAX_PIXELS_DEFAULT = 1_000_000_000 +class PixelSafetyLimitError(ValueError): + """Raised when a requested TIFF allocation exceeds max_pixels.""" + + def _check_dimensions(width, height, samples, max_pixels): - """Raise ValueError if the requested allocation exceeds *max_pixels*.""" + """Raise PixelSafetyLimitError if the request exceeds *max_pixels*.""" total = width * height * samples if total > max_pixels: - raise ValueError( + raise PixelSafetyLimitError( f"TIFF image dimensions ({width} x {height} x {samples} = " f"{total:,} pixels) exceed the safety limit of " f"{max_pixels:,} pixels. Pass a larger max_pixels value to " diff --git a/xrspatial/geotiff/_vrt.py b/xrspatial/geotiff/_vrt.py index b8e83661..a30013b4 100644 --- a/xrspatial/geotiff/_vrt.py +++ b/xrspatial/geotiff/_vrt.py @@ -626,7 +626,7 @@ def read_vrt(vrt_path: str, *, window=None, ------- (np.ndarray, VRTDataset) tuple """ - from ._reader import read_to_array + from ._reader import PixelSafetyLimitError, read_to_array with open(vrt_path, 'r') as f: xml_str = f.read() @@ -876,9 +876,7 @@ def read_vrt(vrt_path: str, *, window=None, except ( OSError, ValueError, struct.error, ) + _CODEC_DECODE_EXCEPTIONS as e: - if (isinstance(e, ValueError) - and 'exceed' in str(e) - and 'safety limit' in str(e)): + if isinstance(e, PixelSafetyLimitError): raise # Under XRSPATIAL_GEOTIFF_STRICT=1, surface the read failure # so partial mosaics are caught in CI. Default mode warns