Skip to content

Bound the flat-ring map draw walk so an odd-length ring can't overrun#582

Open
patrickrb wants to merge 1 commit into
devfrom
optio/task-5f6647f6-2e86-4fef-a157-dafe9d4ee970
Open

Bound the flat-ring map draw walk so an odd-length ring can't overrun#582
patrickrb wants to merge 1 commit into
devfrom
optio/task-5f6647f6-2e86-4fef-a157-dafe9d4ee970

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Summary

Four live Compose land renderers walked a flattened [lon, lat, lon, lat, ...]
polygon ring with while (i < ring.size) { ...ring[i + 1]...; i += 2 }, which
reads one element past the end of an odd-length ring
ArrayIndexOutOfBoundsException on the Compose draw thread (uncaught → whole-app
crash). This PR routes all four through one bounded, unit-tested helper, matching
the two siblings that already do it safely.

Affected loops (all live, hosted in ComposeMainActivity):

  • ui/map/MapScreen.ktdrawWorldLand (equirectangular basemap)
  • ui/map/MapScreen.ktdrawAzimuthalLand (azimuthal basemap)
  • ui/pota/PotaActivationMap.ktbuildRingPath (POTA activation map)
  • ui/decode/QsoSheet.ktbuildRingPath (QSO-path map)

Root cause

The pair walk trusted ring.size to be even. Today every ring is produced by
WorldOutlines.ringToFlat, which allocates FloatArray(n * 2) (always even), so
the overrun is latent with the current bundled Natural Earth basemap. But the
four live draw paths carry no guard against a malformed/regenerated ring — unlike
their bounded siblings WorldOutlines.pointInRing and
CarMapSurfaceRenderer.buildPaths, which both iterate 0 until ring.size / 2.
This is the same "one view has a guard its siblings lack" asymmetry fixed before
for the waterfall/spectrum views.

Fix

Extract a single forEachRingVertex helper in WorldOutlines.kt (next to the
flat-ring format docs and pointInRing) that walks exactly ring.size / 2
complete vertices, ignores a trailing unpaired coordinate, and skips rings with
fewer than 3 vertices (equivalent to the old if (ring.size < 6) continue). All
four draw loops become thin callers. inline keeps the per-vertex callback
allocation-free on the draw hot path (runs for every land ring, every frame).

Byte-identical output for every even-length ring — the only behavioral change
is that a hypothetical odd-length ring no longer crashes (its stray trailing
coordinate is dropped, as CarMapSurfaceRenderer already does).

Testing

  • New RingVertexWalkTest (pure JVM, JUnit4 + Truth): even / odd / larger-odd /
    short / five-float / empty rings, the first-vertex flag, and a reproduction of
    the pre-fix unbounded walk asserting it overruns an odd ring while the bounded
    walk does not.
  • :app:testDebugUnitTest — green (full suite).
  • :app:assembleDebug — green (all 4 ABIs, native + hamlib).

Risk

Low. Localized to map/POTA/QSO land rendering; no protocol, DSP, decoder, or
timing behavior changes; happy path (all real rings) is byte-identical.

🤖 Generated with Claude Code

The equirectangular land renderer (`MapScreen.drawWorldLand`), the azimuthal
land renderer (`MapScreen.drawAzimuthalLand`), and the POTA / QSO-path map
renderers (`PotaActivationMap.buildRingPath`, `QsoSheet.buildRingPath`) all
walked a flattened `[lon, lat, lon, lat, ...]` polygon ring with
`while (i < ring.size) { ...ring[i + 1]...; i += 2 }`. That reads one element
past the end of an odd-length ring, throwing ArrayIndexOutOfBoundsException on
the Compose draw thread (uncaught → whole-app crash). Two siblings in the same
codebase already avoid this by bounding to `ring.size / 2`
(`WorldOutlines.pointInRing`, `CarMapSurfaceRenderer.buildPaths`); the four draw
loops were the outliers.

Root cause: the pair walk trusted `ring.size` to be even. Today every ring comes
from `WorldOutlines.ringToFlat`, which allocates `FloatArray(n * 2)` (always
even), so the overrun is latent — but the four live draw paths have no guard
against a malformed/regenerated basemap ring, unlike their bounded siblings.

Fix: extract a single `forEachRingVertex` helper (next to the flat-ring format
docs and `pointInRing` in WorldOutlines) that walks exactly `ring.size / 2`
complete vertices, dropping a trailing unpaired coordinate, and skips rings with
fewer than 3 vertices (equivalent to the old `if (ring.size < 6) continue`). All
four draw loops now call it. `inline` keeps the per-vertex callback
allocation-free on the draw hot path. Byte-identical output for every
even-length ring.

Test: RingVertexWalkTest (pure JVM, JUnit4 + Truth) covers even/odd/short/empty
rings, the first-vertex flag, and a reproduction of the pre-fix unbounded walk
that asserts it overruns an odd ring while the bounded walk does not.

Verified: :app:testDebugUnitTest and :app:assembleDebug (4 ABIs) both green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.42%. Comparing base (647b12e) to head (b55ce9e).

Files with missing lines Patch % Lines
...main/kotlin/radio/ks3ckc/ft8af/ui/map/MapScreen.kt 0.00% 7 Missing ⚠️
.../kotlin/radio/ks3ckc/ft8af/ui/map/WorldOutlines.kt 0.00% 7 Missing ⚠️
...in/kotlin/radio/ks3ckc/ft8af/ui/decode/QsoSheet.kt 0.00% 5 Missing ⚠️
...in/radio/ks3ckc/ft8af/ui/pota/PotaActivationMap.kt 0.00% 5 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #582      +/-   ##
============================================
+ Coverage     29.36%   29.42%   +0.05%     
  Complexity      197      197              
============================================
  Files           179      179              
  Lines         24068    24087      +19     
  Branches       3263     3270       +7     
============================================
+ Hits           7067     7087      +20     
+ Misses        16780    16777       -3     
- Partials        221      223       +2     
Flag Coverage Δ
android 14.63% <0.00%> (+0.11%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...in/kotlin/radio/ks3ckc/ft8af/ui/decode/QsoSheet.kt 1.97% <0.00%> (+<0.01%) ⬆️
...in/radio/ks3ckc/ft8af/ui/pota/PotaActivationMap.kt 0.00% <0.00%> (ø)
...main/kotlin/radio/ks3ckc/ft8af/ui/map/MapScreen.kt 8.20% <0.00%> (+0.11%) ⬆️
.../kotlin/radio/ks3ckc/ft8af/ui/map/WorldOutlines.kt 64.63% <0.00%> (-2.89%) ⬇️

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR prevents Compose land-rendering code paths from crashing on malformed odd-length flattened polygon rings by routing all ring-walks through a single bounded helper in WorldOutlines.kt, and adds unit tests to lock in the new behavior.

Changes:

  • Added forEachRingVertex(...) helper to iterate ring.size / 2 complete (lon, lat) pairs and ignore any trailing unpaired coordinate.
  • Refactored four live renderers (MapScreen equirectangular + azimuthal, POTA map, QSO-path map) to use the bounded helper and only close() when a ring was actually plotted.
  • Added RingVertexWalkTest to cover even/odd/short rings and a regression reproduction of the prior overrun.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/map/WorldOutlines.kt Introduces the shared bounded ring-vertex walker used by multiple renderers.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/map/MapScreen.kt Refactors both land renderers to use the bounded helper (prevents draw-thread OOB).
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaActivationMap.kt Updates POTA ring path building to use the bounded helper and conditional close.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/decode/QsoSheet.kt Updates QSO-path ring path building to use the bounded helper and conditional close.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/map/RingVertexWalkTest.kt Adds JVM unit tests asserting bounded behavior and guarding against regressions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +70 to +72
val vertices = ring.size / 2
if (vertices < minVertices) return false
for (v in 0 until vertices) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants