feat(firmware): Confirm local firmware files before flashing#6083
feat(firmware): Confirm local firmware files before flashing#6083jeremiah-k wants to merge 3 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (11)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (10)
📝 WalkthroughWalkthroughThis PR changes local firmware handling to a select-then-confirm flow. It adds filename resolution, validation, pending-selection state, confirmation UI, updated strings, and expanded tests for bundle extraction, cleanup, and cancellation. ChangesLocal firmware confirmation flow
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant FirmwareUpdateScreen
participant FirmwareUpdateViewModel
participant FirmwareFileHandler
participant FirmwareUpdateManager
User->>FirmwareUpdateScreen: pick local firmware file
FirmwareUpdateScreen->>FirmwareUpdateViewModel: prepareLocalFirmwareFile(uri)
FirmwareUpdateViewModel->>FirmwareFileHandler: getDisplayName(uri)
FirmwareFileHandler-->>FirmwareUpdateViewModel: filename
FirmwareUpdateViewModel->>FirmwareFileHandler: validateLocalFirmwareFileName(...)
FirmwareFileHandler-->>FirmwareUpdateViewModel: validation result
FirmwareUpdateViewModel->>FirmwareUpdateViewModel: set pendingLocalFirmwareFile
FirmwareUpdateScreen->>User: show confirmation dialog
User->>FirmwareUpdateScreen: confirm or dismiss
FirmwareUpdateScreen->>FirmwareUpdateViewModel: confirmLocalFirmwareFile() / dismissLocalFirmwareFile()
FirmwareUpdateViewModel->>FirmwareFileHandler: validatePendingLocalFirmwareFile(...)
FirmwareFileHandler-->>FirmwareUpdateViewModel: valid or invalid reason
FirmwareUpdateViewModel->>FirmwareUpdateManager: startUpdate(uri, pendingArtifact)
FirmwareUpdateManager-->>FirmwareUpdateViewModel: update state
</details>
<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->
<details>
<summary>🚥 Pre-merge checks | ✅ 5</summary>
<details>
<summary>✅ Passed checks (5 passed)</summary>
| Check name | Status | Explanation |
| :------------------------: | :------- | :-------------------------------------------------------------------------------------------------------------------- |
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | The title clearly matches the main change: adding an explicit confirmation step before flashing local firmware files. |
| Docstring Coverage | ✅ Passed | No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
</details>
</details>
<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->
<details>
<summary>✨ Finishing Touches</summary>
<details>
<summary>🧪 Generate unit tests (beta)</summary>
- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests
</details>
</details>
<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->
---
<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>
<!-- tips_end -->
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt (1)
599-623: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRename this test or add coverage for the non-Ready cleanup path
cancelUpdate()clearspendingLocalFirmwareFile, soconfirmLocalFirmwareFile()returns at the null guard here and never reachescleanupPendingLocalFirmwareArtifact(...). This test only covers the no-op-after-cancel case; add a separate case with a still-set pending selection and non-Ready state, or rename it to match the asserted behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt` around lines 599 - 623, The test currently named confirmLocalFirmwareFile cleans up artifact when state is not Ready only verifies the no-op path after cancelUpdate clears pendingLocalFirmwareFile, so it never exercises cleanupPendingLocalFirmwareArtifact(...) in confirmLocalFirmwareFile. Either rename this test to reflect the null-guard/no-op behavior, or add a separate case in FirmwareUpdateViewModelFileTest that keeps pendingLocalFirmwareFile set while moving the ViewModel state away from Ready so the cleanup path is actually covered.feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt (1)
817-819: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate target-fallback logic.
effectiveTarget()(platformioTarget.ifEmpty { hwModelSlug }) duplicates the identical fallback already used inJvmFirmwareFileHandler.extractFromZipFile(hardware.platformioTarget.ifEmpty { hardware.hwModelSlug }, feature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.kt:217), and is likely re-implemented again inside the validation logic inFirmwareFileHandler.ktthat this ViewModel calls into. Worth consolidating into a singleDeviceHardwareextension in the common model to avoid future divergence between the value shown to users and the value actually used for matching.♻️ Suggested consolidation
- /** pioEnv-aware target: falls back to the hwModel slug when [DeviceHardware.platformioTarget] is unset. */ - private fun DeviceHardware.effectiveTarget(): String = platformioTarget.ifEmpty { hwModelSlug } + // Replace with a shared `DeviceHardware.effectiveTarget` extension defined once in the common model + // module, and reuse it from JvmFirmwareFileHandler / FirmwareFileHandler as well.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt` around lines 817 - 819, Consolidate the target fallback logic so it is defined once on DeviceHardware and reused everywhere instead of being duplicated in FirmwareUpdateViewModel.effectiveTarget, JvmFirmwareFileHandler.extractFromZipFile, and the validation path in FirmwareFileHandler. Move the platformioTarget.ifEmpty { hwModelSlug } behavior into a shared common extension or helper on DeviceHardware, then update the ViewModel and file handlers to call that shared symbol so the displayed target and matching logic always stay in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.kt`:
- Around line 142-164: The validation order in validatePendingLocalFirmwareFile
should prioritize stale selection detection before rechecking the filename
against the new Ready state. Move the currentTarget/ConfirmationContextChanged
checks ahead of validateLocalFirmwareFileName so that when updateMethod or
address has changed since selection, the function returns
LocalFirmwareFileValidation.Invalid(LocalFirmwareFileValidationReason.ConfirmationContextChanged)
instead of a misleading method-specific filename error. Keep the existing
TargetMismatch handling and only run validateLocalFirmwareFileName after
confirming the context still matches.
---
Nitpick comments:
In
`@feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt`:
- Around line 817-819: Consolidate the target fallback logic so it is defined
once on DeviceHardware and reused everywhere instead of being duplicated in
FirmwareUpdateViewModel.effectiveTarget,
JvmFirmwareFileHandler.extractFromZipFile, and the validation path in
FirmwareFileHandler. Move the platformioTarget.ifEmpty { hwModelSlug } behavior
into a shared common extension or helper on DeviceHardware, then update the
ViewModel and file handlers to call that shared symbol so the displayed target
and matching logic always stay in sync.
In
`@feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt`:
- Around line 599-623: The test currently named confirmLocalFirmwareFile cleans
up artifact when state is not Ready only verifies the no-op path after
cancelUpdate clears pendingLocalFirmwareFile, so it never exercises
cleanupPendingLocalFirmwareArtifact(...) in confirmLocalFirmwareFile. Either
rename this test to reflect the null-guard/no-op behavior, or add a separate
case in FirmwareUpdateViewModelFileTest that keeps pendingLocalFirmwareFile set
while moving the ViewModel state away from Ready so the cleanup path is actually
covered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: da917a8c-b6ab-47ca-ae1b-73dd4146114a
📒 Files selected for processing (11)
.skills/compose-ui/strings-index.txtcore/resources/src/commonMain/composeResources/values/strings.xmlfeature/firmware/src/androidMain/kotlin/org/meshtastic/feature/firmware/AndroidFirmwareFileHandler.ktfeature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareFileHandler.ktfeature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateActions.ktfeature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateScreen.ktfeature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.ktfeature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonFirmwareRetrieverTest.ktfeature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/IsValidFirmwareFileTest.ktfeature/firmware/src/jvmMain/kotlin/org/meshtastic/feature/firmware/JvmFirmwareFileHandler.ktfeature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
Add filename validation and a confirmation dialog before flashing local firmware files, preventing users from accidentally flashing wrong-target firmware. Supports both specific firmware files and release .zip bundles. Validation flow: - User picks a file via the system file picker (MIME */*). - prepareLocalFirmwareFile resolves the file: tries direct filename validation first; if the file is a .zip bundle, extracts the matching per-target entry via extractFirmware with a per-platform extension (.zip for nRF52 BLE DFU, .bin for ESP32, .uf2 for USB). - The confirmation dialog shows the resolved filename (extracted entry name for bundles) so the user verifies the correct target. - confirmLocalFirmwareFile re-validates against current device state before starting the update, catching hardware/connection changes. State management: - A stale-state guard makes prepareLocalFirmwareFile a complete no-op if the Ready state changes during the suspend call (cancelUpdate, checkForUpdates, device disconnect). - A prepareJob is tracked and cancelled on dismiss/cancel to prevent double-prepare temp-file leaks. - Extracted temp artifacts are cleaned on dismiss, cancel, onCleared, and on every error path. Per-platform extension mapping (localFirmwarePayloadExtension) fixes a pre-existing bug where ESP32-over-BLE bundle extraction searched for .zip entries instead of .bin.
Add localized strings for the confirmation dialog, validation error messages, and archive-missing-target error. Update strings-index.txt.
Add unit tests for filename validation (all platforms), bundle extraction (nRF52/ESP32/USB), stale-state no-op, filename-unavailable error, temp artifact cleanup on dismiss, pending re-validation on confirm, and checkForUpdates clearing pending selection.
efa29bd to
0fdba9a
Compare
jamesarich
left a comment
There was a problem hiding this comment.
Re-reviewed after the force-push. The confirmation-context ordering fix landed (context change now surfaces "reselect" instead of an extension error), and most temp-file orphan paths are cleaned up — thanks. But two confirmed holes defeat the feature's core purpose (flashing the right firmware), so I'm requesting changes. Details inline. Minor: one new string (firmware_update_invalid_local_file_detail) has unescaped "%1$s" quotes while its sibling escapes them — sort-strings.py/lint may flag it.
| LocalFirmwareResolution.Invalid(reason = fallbackReason, fileName = fileName) | ||
| } else { | ||
| val extractedArtifact = | ||
| safeCatching { fileHandler.extractFirmware(uri, state.deviceHardware, payloadExtension) } |
There was a problem hiding this comment.
This extracts with payloadExtension (".bin") and no preferredFilename, so AndroidFirmwareFileHandler falls back to minByOrNull { it.first.name.length } and picks the shorter plain firmware-<target>-<ver>.bin. On pre-2.7.17 bundles that's the merged bootloader+partition+app image FirmwareRetriever's own comment says esp_ota_end rejects — it deliberately prefers the manifest then -update.bin. Reuse that preference chain (pass preferredFilename) so a side-loaded official bundle doesn't extract an un-flashable image that then passes validation and gets confirmed.
| } | ||
|
|
||
| private fun validateTargetMatch(fileName: String, target: String, fileExtension: String): LocalFirmwareFileValidation = | ||
| if (isValidFirmwareFile(fileName, target, fileExtension)) { |
There was a problem hiding this comment.
The target match here delegates to isValidFirmwareFile, whose regex .*[-_]<target>[-_.].* accepts delimiter-bounded superstrings: a tbeam device validates firmware-tbeam-s3-core-*.bin as Valid (real colliding pairs in device_hardware.json: tbeam/tbeam-s3-core, t-echo/t-echo-lite, t-deck/t-deck-pro, nano-g1/nano-g1-explorer). Since this feature exists to block wrong-board files, the match needs to require the target be immediately followed by a version/extension delimiter (i.e. anchor the tail), not just appear delimiter-bounded.
| } | ||
| } | ||
|
|
||
| private suspend fun resolveLocalFirmwareBundle( |
There was a problem hiding this comment.
Bundle resolution here runs getDisplayName + a full multi-MB zip extraction with no user-visible progress — the old path set Processing(firmware_update_extracting) but it was removed and the string isn't even imported. The screen sits at Ready during extraction; a user seeing nothing happen may re-tap and spawn a second prepare. Set an extracting/Processing state around the resolve.
| // The user selected the file under a different connection (method or address) than the | ||
| // one active now — the file may still match the target, but the confirmation context is stale. | ||
| currentState.updateMethod != pendingFile.updateMethod || currentState.address != pendingFile.address -> | ||
| LocalFirmwareFileValidation.Invalid(LocalFirmwareFileValidationReason.ConfirmationContextChanged) |
There was a problem hiding this comment.
The context-changed fix only covers same-target/different-transport. TargetMismatch is still checked before this branch, so switching to a different device mid-dialog (new address and target) shows the scary "file does not match " error instead of the intended "connection changed, reselect". Check ConfirmationContextChanged (address/method delta) before the target match.
Overview
This PR makes local firmware file selection safer.
Previously, selecting a file from the Android picker immediately started the update. On devices where long filenames are truncated, it was easy to select the wrong release bundle or firmware artifact without seeing the full name first.
This change resolves and validates the selected file, shows the full filename, and requires explicit confirmation before flashing begins.
Key Changes
Testing
git diff --checkpasses.Migration Notes
Summary by CodeRabbit
New Features
Bug Fixes
Tests