From 5f1f90067f10715bf70f821baa001ff42b1c3f9e Mon Sep 17 00:00:00 2001 From: QIU Date: Fri, 14 Aug 2026 15:12:30 +0000 Subject: [PATCH] fix(aprs): clamp altitude and wrap course to keep fixed-width fields Both extensions are fixed-width decimal fields, but neither value was range checked before formatting: formatAltitude(-50.0) -> /A=-00164 ('-' eats a digit slot) formatCourseSpeed(_, 360f) -> /360/... (course must be 000..359) formatCourseSpeed(_, -1f) -> /-01/... (widens the field) A negative altitude is reachable from a below-sea-level position or a poor GPS fix, and the malformed extension corrupts everything after it in the comment field. Altitude now clamps to 0..999999, course wraps modulo 360, and speed clamps to three digits. Found by the same locale probe that produced the previous commit. :core:domain:test BUILD SUCCESSFUL. --- .../look4sat/core/domain/aprs/AprsPacket.kt | 21 ++++++++++++++----- .../core/domain/aprs/AprsPacketLocaleTest.kt | 16 ++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt index 37e84237..3b1d4c0f 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacket.kt @@ -39,17 +39,28 @@ object AprsPacket { return String.format(Locale.ROOT, "r/%.3f/%.3f/%d", latitude, longitude, distKm) } - /** Altitude extension /A=00000 (feet) */ + /** + * Altitude extension /A=000000 (feet). The field is a fixed six-digit + * decimal, so a negative altitude (below sea level, or a bad GPS fix) must + * be clamped: "%06d" of -164 yields "/A=-00164", which is not a valid + * extension and corrupts the rest of the comment field. + */ fun formatAltitude(altitudeMeters: Double?): String { if (altitudeMeters == null) return "" - return String.format(Locale.ROOT, "/A=%06d", (altitudeMeters * 3.2808399).toInt()) + val feet = (altitudeMeters * 3.2808399).toInt().coerceIn(0, 999999) + return String.format(Locale.ROOT, "/A=%06d", feet) } - /** Speed/course extension (knots/degrees) */ + /** + * Speed/course extension /CCC/SSS (degrees/knots). Course wraps into + * 0..359 and speed is clamped to three digits, because "%03d" of an + * out-of-range value widens the field and breaks the fixed-width format. + */ fun formatCourseSpeed(speedMps: Double?, bearing: Float?): String { if (speedMps == null || bearing == null) return "" - val knots = (speedMps * 1.94384449).toInt() - return String.format(Locale.ROOT, "/%03d/%03d", bearing.toInt(), knots) + val knots = (speedMps * 1.94384449).toInt().coerceIn(0, 999) + val course = ((bearing.toInt() % 360) + 360) % 360 + return String.format(Locale.ROOT, "/%03d/%03d", course, knots) } } diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacketLocaleTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacketLocaleTest.kt index 6ed412f4..c1843575 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacketLocaleTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/aprs/AprsPacketLocaleTest.kt @@ -62,6 +62,22 @@ class AprsPacketLocaleTest { assertEquals("r/39.904/116.407/100", filter) } + @Test + fun altitude_clampsNegativeToKeepSixDigitField() { + // "%06d" of a negative value yields "/A=-00164": the '-' takes a digit + // slot, so the extension is no longer a valid fixed-width field. + assertEquals("/A=000000", AprsPacket.formatAltitude(-50.0)) + assertEquals("/A=000000", AprsPacket.formatAltitude(-1.0)) + assertEquals("/A=000328", AprsPacket.formatAltitude(100.0)) + } + + @Test + fun courseSpeed_wrapsCourseIntoValidRange() { + assertEquals("/000/019", AprsPacket.formatCourseSpeed(10.0, 360f)) + assertEquals("/359/019", AprsPacket.formatCourseSpeed(10.0, -1f)) + assertEquals("/090/019", AprsPacket.formatCourseSpeed(10.0, 90f)) + } + @Test fun ambiguousPosition_staysAsciiUnderArabicLocale() { Locale.setDefault(Locale.forLanguageTag("ar-EG"))