Skip to content

Fix app crash on malformed / 32-bit Xiegu CAT handle frame#488

Merged
patrickrb merged 2 commits into
devfrom
optio/task-2ec8a1c2-da03-4b26-bb72-3cd33619d4a7
Jul 9, 2026
Merged

Fix app crash on malformed / 32-bit Xiegu CAT handle frame#488
patrickrb merged 2 commits into
devfrom
optio/task-2ec8a1c2-da03-4b26-bb72-3cd33619d4a7

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Summary

Fixes a deterministic full-app crash on the Xiegu (X6100/G90-family) network CAT path. X6100Radio.doReceiveLineEvent parsed the radio-assigned client handle with an unguarded parse:

case HANDLE:
    this.handle = Integer.parseInt(response.head.substring(1), 16);

Every other parse in this class (seq_number, resultCode, play_volume) and the identical handle parse in the sibling flex/FlexRadio wrap the same call in a try/catch. The X6100 variant moved this parse out of the XieguResponse constructor and lost the guard.

Root cause

Two inputs make Integer.parseInt(head.substring(1), 16) throw NumberFormatException:

  1. A garbled/truncated frame whose tail is not hex — including a lone "H", whose tail is the empty string (parseInt("", 16) throws).
  2. A legitimate high-bit 32-bit handle such as "HFFFFFFFF". The HANDLE enum documents the field as a 32-bit hex representation; 0xFFFFFFFF (and anything ≥ 0x80000000) overflows a signed int, so Integer.parseInt rejects roughly half of all valid handles.

The parse runs on the TCP CAT read thread (flex/RadioTcpClient.SocketThread.run), whose read loop catches only SocketException/IOException:

} catch (SocketException e){ ... }
catch (IOException e) { ... return; }

A NumberFormatException from the listener therefore propagates out of run(), kills the read thread, and crashes the whole app via the default uncaught-exception handler.

Fix

Extract the parse into a package-private static parseXieguHandle(head, currentHandle) that:

  • length-guards the input (null / too-short → keep current handle),
  • parses via Long.parseLong(..., 16) so the full unsigned 32-bit range round-trips into the int handle field (same bit pattern), and
  • contains any remaining malformed input by keeping the current handle (matching FlexRadio).

Long.parseLong is used rather than Integer.parseUnsignedInt because the latter is API 26+ and this module's minSdk is 23 (no core-library desugaring).

The handle field is write-only within this class (never read), so preserving the prior value on a bad frame is behaviourally safe; the only observable effect of the old code was the crash.

Testing

  • New X6100RadioHandleParseTest (pure JUnit): low handles, full 8-digit handles, the 0xFFFFFFFF/0x80000000 high-bit cases that previously overflowed and crashed, plus lone-"H" / non-hex / empty / null frames. Against the old code the high-bit, lone-"H", and non-hex cases throw NumberFormatException, so they pin the regression.
  • ./gradlew :app:testDebugUnitTest — full suite green, no regressions.
  • ./gradlew :app:assembleDebug — builds (native + packaging).

Risk

Low. One localized method extraction on the Xiegu CAT receive path; no protocol/DSP/encoder behaviour changes; valid handles ≤ 0x7FFFFFFF parse identically to before.

🤖 Generated with Claude Code

The HANDLE branch of X6100Radio.doReceiveLineEvent parsed the Xiegu
client handle with an unguarded Integer.parseInt(head.substring(1), 16).
Every sibling parse in this class (seq_number, resultCode, play_volume)
and the identical handle parse in the Flex sibling (FlexRadio) wrap the
same call in a try/catch; the X6100 variant moved the parse out of the
XieguResponse constructor and lost the guard.

Two inputs make it throw NumberFormatException:

  * a garbled or truncated frame whose tail is not hex — even a lone
    "H", whose tail is the empty string; and
  * a legitimate high-bit 32-bit handle such as "HFFFFFFFF": the Xiegu
    protocol handle is a full 32-bit value (see the HANDLE enum doc) and
    0xFFFFFFFF overflows a signed int, so Integer.parseInt rejects it.

That parse runs on the TCP CAT read thread (RadioTcpClient.SocketThread),
whose read loop catches only SocketException/IOException. A
NumberFormatException escaping the listener therefore propagates out of
run(), killing the read thread and crashing the whole app via the
default uncaught-exception handler.

Extract the parse into a package-private static parseXieguHandle(head,
currentHandle): it length-guards the input, parses via Long.parseLong so
the full unsigned 32-bit range round-trips into the int handle field,
and contains any remaining malformed input by keeping the current handle
(matching FlexRadio). The handle field is write-only in this class, so
preserving the prior value on a bad frame is behaviourally safe.

Add X6100RadioHandleParseTest covering low handles, full 8-digit and
high-bit handles (the 0xFFFFFFFF / 0x80000000 overflow cases that used
to crash), and the lone-"H"/non-hex/empty/null frames.

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

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 23.35%. Comparing base (d71f162) to head (cecc56f).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##                dev     #488   +/-   ##
=========================================
  Coverage     23.34%   23.35%           
- Complexity      149      162   +13     
=========================================
  Files           165      167    +2     
  Lines         21401    21450   +49     
  Branches       3149     3154    +5     
=========================================
+ Hits           4997     5009   +12     
- Misses        16215    16248   +33     
- Partials        189      193    +4     
Flag Coverage Δ
android 13.26% <ø> (+0.03%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 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 pull request fixes a deterministic crash in the Xiegu (X6100/G90-family) network CAT receive path by guarding the parsing of the radio-assigned client handle so malformed frames and unsigned 32-bit handle values do not throw and escape the TCP read thread.

Changes:

  • Replace the unguarded Integer.parseInt(..., 16) in the HANDLE response branch with a guarded helper method.
  • Add parseXieguHandle(head, currentHandle) to tolerate malformed/truncated input and accept the full unsigned 32-bit handle range via Long.parseLong.
  • Add a pure JVM unit test suite covering valid, malformed, and high-bit handle inputs (including 0xFFFFFFFF / 0x80000000).

Reviewed changes

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

File Description
ft8af/app/src/main/java/com/k1af/ft8af/x6100/X6100Radio.java Introduces guarded Xiegu handle parsing and routes HANDLE responses through it to prevent crashes.
ft8af/app/src/test/java/com/k1af/ft8af/x6100/X6100RadioHandleParseTest.java Adds unit tests to pin behavior for malformed frames and unsigned 32-bit handles.

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

Comment thread ft8af/app/src/main/java/com/k1af/ft8af/x6100/X6100Radio.java
Copilot review on PR #488: parsing via Long.parseLong alone was too
permissive for an unsigned handle — it accepted a leading sign
("H-1" -> -1 instead of keeping the current handle) and accepted more
than 8 hex digits and then silently truncated on the narrowing int cast
("H100000000" -> 0). The error log also copied FlexRadio's misleading
"parseInt"/"XieguResponse" wording.

Validate the tail as 1-8 unsigned hex digits (isUnsignedHex) before
parsing; reject anything else and keep the last good handle. After
validation Long.parseLong can't throw, and 0x80000000..0xFFFFFFFF still
round-trip into the int field with the same 32-bit pattern. Log an
accurate message naming the offending tail.

Add tests for lowercase hex, a signed tail, an overlong (9-digit) tail,
and a whitespace tail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@patrickrb patrickrb merged commit 3ec1324 into dev Jul 9, 2026
17 checks passed
@patrickrb patrickrb deleted the optio/task-2ec8a1c2-da03-4b26-bb72-3cd33619d4a7 branch July 9, 2026 02:31
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