Skip to content

Fix ADIF import silently dropping every QSO from a headerless log - #672

Open
patrickrb wants to merge 1 commit into
devfrom
optio/task-d0c5704a-d32b-4e11-b621-9a11cceda998
Open

Fix ADIF import silently dropping every QSO from a headerless log#672
patrickrb wants to merge 1 commit into
devfrom
optio/task-d0c5704a-d32b-4e11-b621-9a11cceda998

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Summary

Importing a headerless ADIF log (a file that begins directly with a <field...> record, with no header) resulted in zero QSOs imported and no error shown to the user — silent data loss on import.

Per the ADI file format the Header is optional: it is present only when the file does not begin with <. A file that begins with < is headerless and consists entirely of data records.

Both ADIF importers derived the record body by splitting the file on <eoh> and returning "" whenever no marker was found:

  • LogFileImport.getLogBody() — the built-in web-logger HTTP upload path (LogHttpServernew LogFileImport)
  • ImportSharedLogs.getLogBody() — the Android share / VIEW-intent path (another app hands a .adi to FT8AF)

For a valid headerless file the split found no <eoh>, returned "", and getLogRecords() produced an empty list — while getErrorCount() stayed 0, so the import UI reported success. Every QSO was dropped.

This shape is common: many loggers and hand exports omit the header, and a WSJT-X wsjtx_log.adi whose header was written with the known v2.2.2 <eh> bug also has no parseable <eoh>.

Root cause & fix

Added a single spec-correct, unit-tested helper AdifFormat.stripHeader(String) and pointed both getLogBody() methods at it (same root cause → one PR). It returns:

  • the whole content when the file is headerless (first non-blank char, after an optional UTF-8 BOM, is <);
  • the text after the first <eoh> when a header is present and terminated;
  • "" only when a header is present but unterminated (no <eoh>), or the input is null — genuinely no record body.

No protocol/DSP behavior changes; this is purely the ADIF reader's header/record split. Headed files (including the existing WSJT-X test fixtures) are handled exactly as before.

Testing performed

./gradlew :app:testDebugUnitTest for the three affected classes — all pass (33 + 30 + 16 tests, 0 failures):

  • AdifFormatTest: new stripHeader cases — headed body-after-<eoh>, headerless whole-content, leading BOM/whitespace, case-insensitive <eoh>, first-<eoh>-only, header-present-but-unterminated → empty, null/empty.
  • LogFileImportTest: new end-to-end headerlessAdif_importsRecords (drives the real constructor → getLogRecords, now 2 records) and headerPresentButUnterminated_yieldsNoRecords. These replace the stale getLogBody_returnsEmptyWhenNoEoh test, which pinned the old drop-everything behavior.

The end-to-end test was confirmed to fail against the pre-fix code (0 records) before the fix was applied.

Risk assessment

Low. The change only affects files that previously imported as zero records; headed files are unchanged (verified by the untouched, passing WSJT-X fixture tests). Nothing downstream keys off an empty body to signal an invalid file — doImportADI simply iterates the record list.

The two ADIF importers (LogFileImport, used by the web-logger HTTP upload,
and ImportSharedLogs, used by the Android share/VIEW intent) both derived
the record body by splitting the file on <eoh> and returning "" when no
marker was found. Per the ADI file format a Header is optional: it is
present only when the file does NOT begin with '<'. A file that begins with
'<' is headerless and consists entirely of data records.

So a valid headerless ADIF log — a common export shape, and what a WSJT-X
wsjtx_log.adi whose header was written with the v2.2.2 <eh> bug looks like —
imported as zero QSOs, with no error surfaced to the user (getErrorCount()
== 0). Silent data loss on import.

Root cause fix: a shared, spec-correct AdifFormat.stripHeader() returns the
whole content for a headerless file (first non-blank char, after an optional
UTF-8 BOM, is '<'), the text after the first <eoh> for a headed file, and
"" only when a header is present but unterminated or the input is null.
Both getLogBody() methods now delegate to it.

Tests: new AdifFormat.stripHeader cases (headed/headerless/BOM/whitespace/
case-insensitive <eoh>/first-<eoh>-only/unterminated/null) and an end-to-end
LogFileImport headerless-import test (replacing the stale test that pinned
the old drop-everything behaviour). All pass.

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

codecov Bot commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 36.80%. Comparing base (e662cd7) to head (a598e61).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #672      +/-   ##
============================================
+ Coverage     36.73%   36.80%   +0.06%     
- Complexity      197      211      +14     
============================================
  Files           216      219       +3     
  Lines         26882    27054     +172     
  Branches       3284     3321      +37     
============================================
+ Hits           9876     9957      +81     
- Misses        16781    16863      +82     
- Partials        225      234       +9     
Flag Coverage Δ
android 15.38% <ø> (+0.35%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 13 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 fixes a silent ADIF import failure where headerless .adi logs (files that begin with <field...>, with no <eoh> header terminator) previously imported zero QSOs without surfacing an error. It centralizes the header/body split in AdifFormat.stripHeader(String) and updates both import paths to use it.

Changes:

  • Added AdifFormat.stripHeader(String) to correctly handle optional ADIF headers (headerless vs headed-with-<eoh>).
  • Updated both LogFileImport.getLogBody() (web upload path) and ImportSharedLogs.getLogBody() (share/VIEW intent path) to use stripHeader.
  • Added/updated unit tests covering headerless logs and unterminated headers.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ft8af/app/src/main/java/com/k1af/ft8af/log/AdifFormat.java Adds stripHeader helper to correctly derive record body for headed and headerless ADIF files.
ft8af/app/src/main/java/com/k1af/ft8af/log/LogFileImport.java Switches record-body extraction to AdifFormat.stripHeader for the web upload import path.
ft8af/app/src/main/java/com/k1af/ft8af/log/ImportSharedLogs.java Switches record-body extraction to AdifFormat.stripHeader for the share/VIEW-intent import path.
ft8af/app/src/test/java/com/k1af/ft8af/log/AdifFormatTest.java Adds focused unit tests for stripHeader behavior (headed/headerless/case/BOM/unterminated).
ft8af/app/src/test/java/com/k1af/ft8af/log/LogFileImportTest.java Updates end-to-end import tests to cover headerless ADIF logs and malformed unterminated headers.
Comments suppressed due to low confidence (1)

ft8af/app/src/test/java/com/k1af/ft8af/log/AdifFormatTest.java:247

  • Test method name says "headless" but this test is about "headerless" ADIF logs (no header). Renaming makes the suite easier to scan and avoids conflating with unrelated "headless" terminology.
    public void stripHeader_headlessFile_skipsLeadingWhitespaceAndBom() {

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

}

@Test
public void stripHeader_headlessFile_returnsWholeContent() {
}

@Test
public void stripHeader_headPresentButNoEoh_returnsEmpty() {
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