feat(memory): sanitize NULs, add field dropdowns, and smoother editing#3657
Conversation
Overhaul the Memory Channels dialog for usability and data integrity.
Data integrity
- Add MemoryFields::sanitizeText() to strip C0 control bytes (incl. the
NUL characters seen corrupting Mode) and the reserved 0x7f DEL byte.
- Apply it at all four boundaries: radio status in (handleMemoryStatus),
command out (encodeMemoryText + mode/offset/tone wire conversion),
CSV import (parseRecord), and CSV export (recordToFields). Corrupt
values can no longer reach the UI, the radio, or other SmartSDR tools.
One-click dropdowns (MemoryFieldDelegate)
- Mode / Offset Dir / Tone Mode are strict combos sourced from the
FlexLib value sets (Slice.cs / Memory.cs); Tone Value, Step and Group
are type-or-pick combos. The list pops open on edit for a one-click
pick, and display<->wire conversion is centralized so the table,
radio I/O and CSV always agree (modes upper-case, offset/tone lower).
Smoother editing & navigation
- Retire the modal Edit-mode toggle: cells are always editable (click a
selected cell, F2, or type). Tab/Shift+Tab move between fields.
Double-click stays reserved for Tune.
Column sizing (macOS)
- resizeColumnsToContents ran a few px tight, eliding short values like
"CW" to one character, and crowded the headers. Pad every column and
floor each dropdown column to its widest value measured in the table's
own (themed) font, so DIGU / CTCSS_TX / SIMPLEX always fit. Shorten the
longest header ("FM TX Offset Dir" -> "Offset Dir").
Tests
- New memory_field_values_test covers sanitization (NUL/TAB/CR/LF/DEL),
mode validation, and offset/tone display<->wire round-trips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Nice, well-structured PR — thanks @jensenpat. I read the full diff (the inline preview truncates) plus the resulting MemoryDialog.cpp/RadioModel.cpp/MemoryCsvCompat.cpp on the branch. All six CI checks are green (build, macOS, Windows, CodeQL, Qt a11y), includes for QTimer/<functional> are present, and the removed onEdit/setInlineEditMode/m_inlineEditMode/m_editBtn plumbing is cleanly excised on both sides.
What I like:
- Sanitization at every boundary is the right design — centralizing it in
MemoryFields::sanitizeText()and wiring it into radio-in (handleMemoryStatus), radio-out (encodeMemoryText+*ToWire), CSV-in (parseRecord) and CSV-out (recordToFields) means corruption genuinely can't enter or escape. ThedecodeTextordering (0x7f→space then sanitize) is correct. - Single source of truth for the value lists / display↔wire conversion, with focused unit coverage in
memory_field_values_test. Bonus that you registered the previously-unregisteredmemory_recall_policy_testwithadd_testwhile you were in there. - Delegate ownership/RAII is clean (delegates parented to the dialog, validators to the combo), and the
setEditorData"preserve an out-of-list value" path is a thoughtful touch for legacy modes.
A couple of minor, non-blocking observations:
-
toneModeToDisplaycollapses unknown tone modes toOFF. FlexLib'sFMToneModereservesctcss_txrx(your header notes it's "not exposed"). If a memory ever arrives from the radio with that value,populateTablewill render it asOFF— a faithful-display gap rather than a crash. If you're confident the radio never emits it for memories, this is fine to leave; otherwise consider preserving the raw token the way the Mode delegate'ssetEditorDatadoes. Worth a one-line confirmation. -
offsetDirToWire/toneModeToWirereturn""for unrecognized input. Not reachable today (cols 6/8 use strict, non-editable combos), but if a preserved out-of-list value were committed unchanged it'd sendrepeater=/tone_mode=with an empty value. Low severity given the strict delegates — just flagging the latent edge for future callers. -
Trivial: the
if (combo)guard inside theQTimer::singleShotlambda is always true (combois captured by value and non-null); thecombocontext object already cancels the timer if the editor is destroyed. Harmless, ignore at will.
None of these block — the core fix and UX work look solid and well-tested. 👍
🤖 aethersdr-agent · cost: $6.8243 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Approving — a genuine data-corruption fix plus well-architected ergonomics.
Verified:
sanitizeText()correctly drops C0 controls (< 0x20, incl. NUL) + 0x7f DEL, and is wired at all four boundaries. The radio-in path orders it right: decode 0x7f→space first, then sanitize, so legit spaces survive while stray control bytes are stripped. App→radio, CSV import, and CSV export all sanitize too — corruption can't enter the model, reach the radio, or leak to a .csv.- Display↔wire conversion is centralized + authoritative (FlexLib Slice.cs/Memory.cs), normalizing case from one place so table/commands/CSV can't drift.
isKnownMode()logs an unrecognized CSV mode and passes it through to the radio rather than rejecting — radio stays authoritative (Principle I).- New hardware-independent
memory_field_values_test; CI green across all six. macOS column-sizing fixes solve a platform-specific problem.
The editing-model change (modal Edit → always-editable + F2/Tab) is a UX refinement the RFC gate technically names — landing it as a maintainer-driven dialog refinement. Nice work @jensenpat.
Memory Channels dialog: tougher data, friendlier editing ⚡
This is a focused glow-up of the Memory Channels dialog — it fixes a real data-corruption bug, makes the constrained fields impossible to fat-finger, and turns the spreadsheet from "fight it" into "fly through it."
🐛 The bug that started it: NUL bytes in
ModeMemory entries were turning up with embedded NUL (
\0) characters in the Mode field — invisible in the UI, but enough to confuse the radio and any other SmartSDR-compatible software that later reads the channel. Nothing in the pipeline was scrubbing control characters, on the way in or out.Fixed at every boundary. New
MemoryFields::sanitizeText()strips all C0 control bytes (0x00–0x1F, including those NULs) plus the reserved0x7fDEL byte, and it's wired into all four crossing points:RadioModel::handleMemoryStatus(decode0x7f→space, then sanitize)encodeMemoryText+ mode/offset/tone wire conversionMemoryCsvCompat::parseRecord(+ logs unrecognized modes)recordToFields— so corruption can never leak to other toolsGarbage can no longer enter the model, reach the radio, or escape into a
.csv.🎯 One-click dropdowns for the constrained fields
No more typing
CTCSS_TXby hand and hoping. A newMemoryFieldDelegategives the constrained columns proper combo-box editors that pop open on a single click:USB, LSB, CW, AM, SAM, DSB, FM, NFM, DFM, DIGU, DIGL, RTTY, FDV, DSTR, AMESIMPLEX, UP, DOWNOFF, CTCSS_TXEvery value list is sourced from the authoritative FlexLib definitions (
Slice.cs/Memory.cs), and display↔wire conversion is centralized in one place so the table, the radio commands, and CSV always agree (modes upper-case on the wire, offset/tone lower-case).⌨️ Editing that gets out of your way
📏 macOS column sizing fixes
Two papercuts squashed:
resizeColumnsToContents()runs a couple pixels tight on macOS, eliding short values —CWshowed as justCafter selection.DIGUandCTCSS_TXgot chopped because the floor widths were measured in the dialog's font while the table renders in the (larger) themed font.Now every column gets breathing room, and each dropdown column is floored to its widest possible value, measured in the table's own font + slack for cell margins and the combo arrow — so the value always shows in full. Also shortened the most crowded header (
FM TX Offset Dir→Offset Dir).✅ Tests
New
memory_field_values_testcovering sanitization (NUL / TAB / CR / LF / DEL), mode validation, and offset/tone display↔wire round-trips. Passes alongside the existingmemory_recall_policy_test.🔍 How to verify
DIGU/CTCSS_TX) shows in full.💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat