Skip to content

geotiff: enforce tile_size positivity in array-level writers (#2997)#3000

Merged
brendancol merged 2 commits into
mainfrom
issue-2997
Jun 6, 2026
Merged

geotiff: enforce tile_size positivity in array-level writers (#2997)#3000
brendancol merged 2 commits into
mainfrom
issue-2997

Conversation

@brendancol
Copy link
Copy Markdown
Contributor

Closes #2997

What

The public to_geotiff() validates tile_size (positive int + multiple of 16) whenever tiled or cog is set, but the array-level private writers didn't all do the same:

  • _write_streaming() skipped tile_size validation, so tile_size=0 reached math.ceil(width / tw) and raised a bare ZeroDivisionError.
  • _write() only checked positivity under cog=True; the tiled=True, cog=False path passed the value straight through.

Both now run the positivity/type check on any path that consumes tile_size, reusing the existing _validate_tile_size_arg helper.

A note on the multiple-of-16 rule

I did not push the multiple-of-16 TIFF spec rule down to the array-level writers. That rule is enforced at the public to_geotiff boundary and stays there. The array-level writer is the lower-level tool, and the in-repo reader plus the existing COG and round-trip tests deliberately use small spec-noncompliant tiles (4, 8) to keep test rasters small. Forcing multiple-of-16 at this layer broke about a dozen existing tests. So _validate_tile_size gained a require_multiple_of_16 flag (default True) and the private writers opt out of just that rule while keeping the crash-inducing positivity/type checks.

Backend coverage

Validation runs before any backend dispatch, so it applies to numpy, cupy, dask+numpy, and dask+cupy alike. The GPU writer keeps full multiple-of-16 enforcement (it is reached through the public-style boundary).

Test plan

  • _write / _write_streaming reject tile_size=0, negative, and non-int on the tiled path
  • cog=True still rejects non-positive tile_size
  • array-level writers still accept small non-multiple-of-16 tiles (no regression for the COG/round-trip tests)
  • strip layout (tiled=False) ignores tile_size
  • public to_geotiff still enforces multiple-of-16
  • full write / streaming / cog / input-validation suites pass (660 passed, 1 skipped)

_write and _write_streaming used to under-validate tile_size. _write_streaming
skipped it entirely, so tile_size=0 hit a bare ZeroDivisionError in the
math.ceil(width / tw) layout math; _write only checked positivity under
cog=True. Both now run the positivity/type check on any path that consumes
tile_size, reusing _validate_tile_size_arg.

The multiple-of-16 TIFF spec rule stays at the public to_geotiff boundary:
the array-level writers are the lower-level tool, and the in-repo reader plus
the existing COG/round-trip tests rely on small spec-noncompliant tiles. Added
a require_multiple_of_16 flag (default True) to _validate_tile_size so the
public path keeps full enforcement while the private writers opt out of just
that rule.
@github-actions github-actions Bot added the performance PR touches performance-sensitive code label Jun 6, 2026
Copy link
Copy Markdown
Contributor Author

@brendancol brendancol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: geotiff: enforce tile_size positivity in array-level writers (#2997)

Blockers (must fix before merge)

None.

Suggestions (should fix, not blocking)

  • The inner COG positivity guard at xrspatial/geotiff/_writer.py:560-565 is now dead code. The new top-of-function check (if tiled or cog: _validate_tile_size_arg(...)) rejects a non-positive tile_size before the auto-overview block ever runs, and that block only runs when cog=True. So the old guard's narrower message ("for COG overview generation") can't fire anymore. It's pre-existing defense-in-depth and harmless to leave, but the next person to read that block might think COG validation lives there. Either drop it or leave a one-line comment pointing at the top-level gate.

Nits (optional improvements)

  • The new comment blocks run long, but they match the comment density of the rest of the file, so no real objection.

What looks good

  • require_multiple_of_16 defaults to True, so the public to_geotiff and the GPU writer keep the exact enforcement they had before. Checked the GPU call site at _writers/gpu.py:365 and it uses the default.
  • Keeping the multiple-of-16 rule at the public boundary instead of shoving it down is the right call, and the PR explains why: the array-level writers and the in-repo reader use small spec-noncompliant tiles, and pushing the rule down broke about a dozen existing COG/round-trip tests.
  • Tests cover the cases that matter: 0, negative, and non-int rejected on both _write and _write_streaming; small non-multiple-of-16 tiles still write; strip layout ignores tile_size; and a guard test confirms the public path still rejects 17.
  • The _write_streaming gate keys on tiled alone, which is correct since there's no cog parameter on that function.

Checklist

  • Algorithm matches reference/paper: N/A (validation fix)
  • Backends consistent: validation runs before dispatch, so it's backend-independent
  • NaN handling: N/A
  • Edge cases covered: yes (0, negative, non-int, strip path, valid sizes)
  • Dask chunk boundaries: N/A
  • No premature materialization: N/A
  • Benchmark: not needed
  • README matrix: N/A (no new public API)
  • Docstrings: yes, the helper docstring documents the new flag

Review flagged the auto-overview tile_size guard as now-unreachable dead code.
It is deliberate loop-side defense pinned by test_inner_overview_loop_guard_message_is_pinned
(#2311); removing it breaks that contract. Keep the guard and note in the comment
that the top-level gate now catches the input first, so the redundancy is by
design rather than an oversight.
Copy link
Copy Markdown
Contributor Author

@brendancol brendancol left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review (after 1c549a2)

Re-reviewed after the follow-up commit.

Disposition of the prior suggestion

  • The inner COG overview guard at xrspatial/geotiff/_writer.py is kept, not removed. It turns out an existing test, test_inner_overview_loop_guard_message_is_pinned (added in #2311), deliberately pins that guard's message as loop-side defense, so removing it breaks that contract. The follow-up addresses the "a future reader might be misled" concern the other way: the comment now says outright that the top-level if tiled or cog gate catches the bad input first and the inner guard is redundant by design. That resolves the readability worry without dropping the pinned defense.

Remaining items

  • None. No blockers, no open suggestions.

Verification

  • xrspatial/geotiff/tests/write/ plus tests/unit/test_input_validation.py: 1223 passed, 1 skipped.

@brendancol brendancol merged commit 57f7f31 into main Jun 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance PR touches performance-sensitive code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lower-level GeoTIFF writers (_write, _write_streaming) skip tile_size validation

1 participant