Reject zero-denominator RATIONAL/SRATIONAL tags (#2313)#2317
Merged
Conversation
`_read_value` used to coerce a malformed (denominator=0) RATIONAL or SRATIONAL to 0.0, which let corrupted `XResolution` / `YResolution` metadata round-trip through the reader as if the file were valid. Raise `ValueError` instead, name the offending tag and the denominator, and update the two existing tests that pinned the old silent behaviour.
brendancol
commented
May 22, 2026
Contributor
Author
brendancol
left a comment
There was a problem hiding this comment.
PR Review: Reject zero-denominator RATIONAL/SRATIONAL tags (#2313)
Blockers
None.
Suggestions
test_header.py:261andtest_header.py:272: the two new tests dofrom xrspatial.geotiff._header import TAG_X_RESOLUTION(and Y) inside the method body. The module is already imported at the top of the file (_read_value, parse_all_ifds, parse_header); folding the tag constants into that same import keeps the block consistent and avoids the local import.
Nits
_header.py:88-96:_TAG_NAMEScovers only X/YResolution and ResolutionUnit, which is the right scope for this PR. If a follow-up threadstag=through other error sites, growing this map will help. Flagging it so it doesn't get forgotten.test_rational_zero_denominator_2313.py:147-208:test_yresolution_zero_denominator_named_in_errorreimplements most of_build_tiff_with_rational_xres. Adding awhich: int = 282parameter to the helper (default XResolution, 283 for YResolution) would let the YResolution test reuse the helper instead of duplicating ~60 lines.
What looks good
- Error message follows the "refusing to parse possibly malformed TIFF" pattern already used by
parse_ifd(lines 666/672 in the original). - All three
_read_valuecall sites inparse_ifdgettag=tag, including the pre-scan path that swallows ValueError. That swallow is safe because_DIMENSION_TAGSdoesn't include any RATIONAL tag. - The regression test exercises both
parse_all_ifdsandopen_geotiff, so the failure is verified at the public entry point, not just the internal helper. - The two existing tests that pinned the old silent behaviour are flipped to assert the new failure, not deleted. A regression that reintroduces the silent path would fail loudly.
- BigTIFF coverage is implicit:
parse_ifduses the same_read_valuefor both classic and BigTIFF entries. - Error message includes the element index, so a malformed entry inside a multi-element RATIONAL array gets pinpointed.
Checklist
- Algorithm matches spec (TIFF treats denom=0 as malformed)
- NaN handling unchanged (rationals never produced NaN before either)
- Edge cases covered (count>1 in
test_rational_denominator_zero_in_array_raises) - No premature materialization or unnecessary copies
- Benchmark coverage: not needed (error path)
- README feature matrix: not applicable (pure bug fix)
- Docstrings present and accurate (
_read_valuedocstring documents the new behaviour)
- Pull `TAG_X_RESOLUTION` / `TAG_Y_RESOLUTION` up into the top-level import in `test_header.py` instead of importing inside the test bodies. - Add a `which` parameter to `_build_tiff_with_malformed_resolution` in `test_rational_zero_denominator_2313.py` so the YResolution test reuses the helper instead of duplicating ~60 lines.
brendancol
commented
May 22, 2026
Contributor
Author
brendancol
left a comment
There was a problem hiding this comment.
Follow-up review after ca86979
Re-checked the two items that needed action and confirmed the new commit addresses them.
Disposition
- Suggestion (hoist local imports in
test_header.py): fixed in ca86979.TAG_X_RESOLUTIONandTAG_Y_RESOLUTIONare now in the top-level import block, and the two local imports inside the test methods are gone. - Nit (share helper between XResolution and YResolution tests): fixed in ca86979.
_build_tiff_with_malformed_resolutionnow takes awhichparameter, andtest_yresolution_zero_denominator_named_in_erroris a one-liner that reuses the helper. Net-41lines in the test file. - Nit (
_TAG_NAMESmap could grow): deferred. The map only exists to label tags in error messages produced by_read_value, and the only error sites that take atag=kwarg today are the RATIONAL / SRATIONAL branches. Adding entries for tags that no error site references would be dead code. If a future PR threadstag=through other error sites, growing the map should be part of that PR.
Tests: pytest xrspatial/geotiff/tests/test_header.py xrspatial/geotiff/tests/test_rational_zero_denominator_2313.py -> 44 passed.
No new findings on this pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2313.
Summary
_read_valueinxrspatial/geotiff/_header.pyused to coerce a malformed RATIONAL or SRATIONAL value (denominator=0) to0.0. It now raisesValueErrorwith the offending tag and denominator in the message, matching the actionable failure style from PR Pin actionable failure modes for unsupported COG writer inputs (#2286 prod-ready wave B) #2301.parse_ifd, so the message can nameXResolution/YResolution/ResolutionUnitdirectly when it's one of those.test_rational_zero_denominator_2313.py) builds a TIFF whoseXResolutionrational has a zero denominator and confirms both the header parser andopen_geotifffail loudly.Backend coverage
Header decoding is shared across all four backends (numpy, cupy, dask+numpy, dask+cupy), so the change applies uniformly. No backend-specific code is touched.
Test plan
pytest xrspatial/geotiff/tests/test_header.pypytest xrspatial/geotiff/tests/test_rational_zero_denominator_2313.pypytest xrspatial/geotiff/tests/ -k "not gpu and not cuda"(4282 passed, 51 skipped)