Skip to content

Commit 6a999af

Browse files
committed
Address PR #1602 review: validate band upstream in _vrt.read_vrt
- Move the ``band`` range/type check into the internal ``_vrt.read_vrt`` reader where ``len(vrt.bands)`` is already in scope. Reject negative indices, non-int values, and out-of-range positive values with a clear ``ValueError`` that names the available band count. Previously these would either silently return a different band (Python negative indexing), raise an opaque ``IndexError`` deep in the read path, or raise ``TypeError`` on the list index. - Drop the now-redundant post-parse guard in the public ``read_vrt`` and update the comment to reflect that ``band`` is validated upstream. - Add three regression tests covering the negative, out-of-range, and non-int rejection paths. The original bug from #1598 (band=N reads pairing with band 0's nodata sentinel) was masked by these silent acceptances on the negative-band edge: ``read_vrt(path, band=-1)`` previously returned the last band's data with ``attrs['nodata']`` unset entirely.
1 parent 008fcb1 commit 6a999af

3 files changed

Lines changed: 56 additions & 7 deletions

File tree

xrspatial/geotiff/__init__.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2738,16 +2738,14 @@ def read_vrt(source: str, *, dtype=None, window=None,
27382738
# advertise band 0's sentinel, the integer-promotion block below
27392739
# would mask against band 0's sentinel, and band N's actual nodata
27402740
# pixels would survive as literal integers. See issue #1598.
2741+
# ``band`` has already been validated by ``_vrt.read_vrt`` as
2742+
# 0 <= band < len(vrt.bands), so a simple lookup is safe here.
27412743
nodata = None
27422744
if vrt.bands:
27432745
band_idx_for_nodata = band if band is not None else 0
2744-
# ``_vrt.read_vrt`` already validates ``band`` is in range; the
2745-
# extra guard keeps a clearer message if a future refactor
2746-
# widens the public range without updating the internal reader.
2747-
if 0 <= band_idx_for_nodata < len(vrt.bands):
2748-
nodata = vrt.bands[band_idx_for_nodata].nodata
2749-
if nodata is not None:
2750-
attrs['nodata'] = nodata
2746+
nodata = vrt.bands[band_idx_for_nodata].nodata
2747+
if nodata is not None:
2748+
attrs['nodata'] = nodata
27512749

27522750
# Mirror the integer-with-nodata promotion that open_geotiff /
27532751
# read_geotiff_dask / read_geotiff_gpu apply post-decode. The VRT

xrspatial/geotiff/_vrt.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,23 @@ def read_vrt(vrt_path: str, *, window=None,
242242
vrt_dir = os.path.dirname(os.path.abspath(vrt_path))
243243
vrt = parse_vrt(xml_str, vrt_dir)
244244

245+
# Validate ``band`` against the parsed band count. Python list
246+
# indexing would silently accept negative values (``vrt.bands[-1]``
247+
# returns the last band) and raise an opaque ``IndexError`` for
248+
# out-of-range positive values, neither of which is the contract the
249+
# public API documents (``band`` is a 0-based positive index). Reject
250+
# both up front with a clear ``ValueError`` so callers do not get
251+
# band-N data paired with band-0's nodata sentinel or a downstream
252+
# IndexError from deep in the read path.
253+
if band is not None:
254+
if not isinstance(band, (int, np.integer)) or isinstance(band, bool):
255+
raise ValueError(
256+
f"band must be a non-negative int, got {band!r}")
257+
if band < 0 or band >= len(vrt.bands):
258+
raise ValueError(
259+
f"band index {band} out of range for VRT with "
260+
f"{len(vrt.bands)} band(s)")
261+
245262
if window is not None:
246263
r0, c0, r1, c1 = window
247264
r0 = max(0, r0)

xrspatial/geotiff/tests/test_vrt_band_nodata_1598.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from __future__ import annotations
1616

1717
import numpy as np
18+
import pytest
1819

1920
from xrspatial.geotiff import read_vrt
2021
from xrspatial.geotiff._writer import write
@@ -105,3 +106,36 @@ def test_read_vrt_no_band_keeps_band0_nodata_attr(tmp_path):
105106
vrt_path = _write_two_band_per_band_nodata_vrt(tmp_path)
106107
r = read_vrt(vrt_path)
107108
assert r.attrs.get('nodata') == 65535.0
109+
110+
111+
def test_read_vrt_negative_band_raises(tmp_path):
112+
"""Negative band indices used to be silently accepted via Python
113+
list indexing (``vrt.bands[-1]`` returned the last band) while the
114+
public reader's nodata lookup rejected them, producing band-N data
115+
with no nodata sentinel. They are now a clear ValueError up front.
116+
"""
117+
vrt_path = _write_two_band_per_band_nodata_vrt(tmp_path)
118+
with pytest.raises(ValueError, match="band"):
119+
read_vrt(vrt_path, band=-1)
120+
121+
122+
def test_read_vrt_out_of_range_band_raises(tmp_path):
123+
"""Out-of-range band indices used to raise IndexError from deep in
124+
the read path. They are now a ValueError that names the available
125+
band count.
126+
"""
127+
vrt_path = _write_two_band_per_band_nodata_vrt(tmp_path)
128+
with pytest.raises(ValueError, match="out of range"):
129+
read_vrt(vrt_path, band=5)
130+
131+
132+
def test_read_vrt_non_integer_band_raises(tmp_path):
133+
"""A non-int ``band`` would previously have raised TypeError on the
134+
list index. ValueError here matches the rest of the input
135+
validation surface.
136+
"""
137+
vrt_path = _write_two_band_per_band_nodata_vrt(tmp_path)
138+
with pytest.raises(ValueError, match="band"):
139+
read_vrt(vrt_path, band="1")
140+
with pytest.raises(ValueError, match="band"):
141+
read_vrt(vrt_path, band=True)

0 commit comments

Comments
 (0)