diff --git a/xrspatial/geotiff/_pam.py b/xrspatial/geotiff/_pam.py index aa7255bca..6b52b9894 100644 --- a/xrspatial/geotiff/_pam.py +++ b/xrspatial/geotiff/_pam.py @@ -15,6 +15,7 @@ from __future__ import annotations import os +from xml.etree.ElementTree import ParseError from xml.sax.saxutils import escape from ._safe_xml import safe_fromstring @@ -144,9 +145,17 @@ def read_pam_sidecar(path): if colors is not None: out['category_colors'] = colors return out - except (OSError, ValueError, TypeError): + except (OSError, ValueError, TypeError, IndexError, ParseError): # A missing, malformed, or foreign sidecar is non-fatal auxiliary # metadata, not a read error -- never let it break open_geotiff. + # IndexError covers a thematic RAT whose carries fewer + # cells than its highest column index (e.g. a Name column at index + # 1 paired with a single-cell row), which would otherwise escape + # _parse_rat and crash the open_geotiff call that reads the + # adjacent sidecar. ParseError covers a truncated or otherwise + # non-well-formed sidecar: safe_fromstring raises it (a SyntaxError + # subclass, so not covered by the types above) and it would + # likewise escape and crash the read. return {} diff --git a/xrspatial/tests/test_rasterize_categorical_3482.py b/xrspatial/tests/test_rasterize_categorical_3482.py index 2aa1e26bf..4cce25899 100644 --- a/xrspatial/tests/test_rasterize_categorical_3482.py +++ b/xrspatial/tests/test_rasterize_categorical_3482.py @@ -257,6 +257,46 @@ def test_malformed_sidecar_returns_empty(self, tmp_path): # Must never raise; worst case returns {}. assert isinstance(read_pam_sidecar(path), dict) + def test_short_row_thematic_rat_returns_empty(self, tmp_path): + """A thematic RAT row with fewer cells than the Name column + index must not crash read_pam_sidecar. + + The Name column sits at index 1 (after a Value column at index 0), + but the row carries a single cell, so indexing the name cell raises + IndexError inside _parse_rat. read_pam_sidecar must swallow it and + fall back to {} like every other malformed-sidecar case, since + open_geotiff reads this sidecar for any local string source. + """ + from xrspatial.geotiff._pam import read_pam_sidecar + path = str(tmp_path / 'short_row.tif') + with open(path + '.aux.xml', 'w') as fh: + fh.write('' + '' + 'Value0' + '5' + 'Class2' + '2' + '0' + '' + '') + # Must never raise; worst case returns {}. + assert read_pam_sidecar(path) == {} + + def test_non_well_formed_xml_sidecar_returns_empty(self, tmp_path): + """A truncated / non-well-formed .aux.xml must not crash the read. + + safe_fromstring raises xml.etree.ElementTree.ParseError, which is a + SyntaxError subclass (not an OSError/ValueError/TypeError/IndexError), + so it would otherwise escape read_pam_sidecar and break the + open_geotiff call that reads the sidecar for any local string source. + """ + from xrspatial.geotiff._pam import read_pam_sidecar + path = str(tmp_path / 'truncated.tif') + with open(path + '.aux.xml', 'w') as fh: + fh.write('oops') + # Must never raise; worst case returns {}. + assert read_pam_sidecar(path) == {} + # --------------------------------------------------------------------------- # GPU backend parity