Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion xrspatial/geotiff/_pam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <Row> carries fewer <F>
# 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 {}


Expand Down
40 changes: 40 additions & 0 deletions xrspatial/tests/test_rasterize_categorical_3482.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <F> 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('<PAMDataset><PAMRasterBand band="1">'
'<GDALRasterAttributeTable tableType="thematic">'
'<FieldDefn index="0"><Name>Value</Name><Type>0</Type>'
'<Usage>5</Usage></FieldDefn>'
'<FieldDefn index="1"><Name>Class</Name><Type>2</Type>'
'<Usage>2</Usage></FieldDefn>'
'<Row index="0"><F>0</F></Row>'
'</GDALRasterAttributeTable>'
'</PAMRasterBand></PAMDataset>')
# 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('<PAMDataset><PAMRasterBand band="1"><Category>oops')
# Must never raise; worst case returns {}.
assert read_pam_sidecar(path) == {}


# ---------------------------------------------------------------------------
# GPU backend parity
Expand Down
Loading