# Code Review

Review scope: application, privileged helper, schedule generation, build and
packaging test integration.  The findings below are ordered roughly by impact.

1. [x] **Critical — prevent shell injection through scheduled “all logs” cleanup.** ~~`generateScheduleScript()` writes `find ... -exec sh -c "echo > '{}'"` ([helperlib.cpp:290](helperlib.cpp#L290)). A log filename containing a single quote or shell syntax is inserted into that shell command and can execute as root when the schedule runs. Generate the command without a shell, e.g. use `-exec truncate -s 0 {} +`, as the direct helper path already does.~~ **Fixed:** now uses `-exec truncate -s 0 {} +`, matching the direct helper path.

2. [x] **High — make schedule replacement atomic and do not delete another user’s schedule.** ~~`cmdWriteSchedule()` deletes the unsuffixed base file even when writing a suffixed per-user file, then deletes the target before attempting the replacement ([helper.cpp:373](helper.cpp#L373)). This can remove a different user’s/legacy schedule and contradicts the intended atomic-write behavior: a failed replacement loses the old target. For reboot schedules, a failure after writing the cron entry can additionally leave cron pointing to a missing script. Write new target files first with `writeFileAsRoot()`, then remove only superseded files whose ownership/selection has been explicitly established.~~ **Fixed (round 2):**
   - `cmdWriteSchedule()` no longer pre-deletes the base/target files; `writeFileAsRoot()`'s temp-file-then-rename already replaces the target atomically.
   - For `@reboot`, the helper now writes the script file *before* the cron entry that references it, so a failed script write can no longer leave cron pointing at a missing script.
   - `pushApply_clicked()` (`mainwindow.cpp`) no longer removes the selected period's schedule files before calling `saveSchedule()`. It now writes the new schedule first and only removes stale entries for the *other* periods (and the leftover script, when leaving `@reboot`) once that write succeeds — `saveSchedule()` now returns success/failure instead of being void. Disabling scheduling (`radioNone`) still removes all schedule files outright, since that is the requested action.
   - **Round 3:** the round-2 fix left a migration regression — a pre-per-user legacy unsuffixed schedule (e.g. `/etc/cron.daily/mx-cleanup`) for the same period was never removed once the new per-user suffixed file was written, so both would run. Fixed by recording, *before* the write, whether the legacy base file is currently the selected (current) user's active schedule for that period (and, for `@reboot`, whether the legacy script file is active); only after the suffixed write succeeds is that recorded legacy file retired. This preserves the atomic-failure guarantee (nothing is removed if the write fails) while eliminating the duplicate-job window.

3. [x] **High — stop treating failed schedule/config writes as successful completion.** ~~`pushApply_clicked()` ignores failures while removing schedules ([mainwindow.cpp:1122](mainwindow.cpp#L1122)), `saveSettings()` ignores the result of `write-settings` ([mainwindow.cpp:771](mainwindow.cpp#L771)), and the final dialog can still report “Done.” Propagate these failures into the existing `failures` list (or abort before destructive schedule removal), retain the UI state on failure, and present one unambiguous result.~~ **Fixed:**
   - `saveSchedule()` and `saveSettings()` now return success/failure (`[[nodiscard]] bool`) instead of silently swallowing `helperProc()`'s result or showing their own separate dialog.
   - Every `remove-schedule` call in `pushApply_clicked()` (stale periods, the leftover script, and the legacy-file retirement added in item 2's round 3) now checks its result and sets a `scheduleCleanupFailed` flag, which is folded into the `failures` list as a single "Remove previous cleanup schedule" entry.
   - A failed `saveSchedule()`/`saveSettings()` call now adds a labeled entry to `failures` instead of popping an independent dialog.
   - The final dialog logic now checks `!failures.isEmpty()` *first*: the `@reboot` "Cleanup script will run at reboot" success message is only shown when nothing failed, so a failed schedule/settings write can no longer be reported as "Done."
   - **Round 2:** the UI-level checks above were undermined by two helper-side gaps: `cmdWriteSchedule()`'s `cmdRemoveSchedule()` in `helper.cpp` always returned success after `QFile::remove()` regardless of outcome, so the new UI checks had nothing real to detect. Fixed via a `removeScheduleFileIfPresent()` helper that treats an already-absent file as success but propagates a genuine `::remove()` failure. Separately, `saveSettings()`'s elevated (root) path ignored `tempSettings.sync()`'s status and a failed reopen of the temp file, either of which could send blank/stale content to the privileged `write-settings` writer; both are now checked and abort with `return false` before any content is sent.

4. [x] **Medium — do not mask package-build test failures.** ~~Debian packaging runs `xvfb-run dh_auto_test || true` ([debian/rules:19](debian/rules#L19)), so every automated-test failure is accepted. Remove `|| true`; if display setup is genuinely optional, explicitly skip only when `xvfb-run` is unavailable. This matters now: plain `ctest` fails without a display, while `QT_QPA_PLATFORM=offscreen ctest` passes, so make the CI/test environment headless-aware rather than hiding failures.~~ **Fixed:** replaced `xvfb-run dh_auto_test || true` with `QT_QPA_PLATFORM=offscreen dh_auto_test`. This drops the `xvfb-run` dependency entirely (the offscreen platform plugin needs no real display) and, since `|| true` is gone, a genuine test failure now fails the package build. Verified with a full `dpkg-buildpackage -us -uc -b`: the test suite runs and passes for real inside the packaging build, not just the plain CMake build.

5. [x] **Medium — make privileged settings writes crash-safe.** ~~`cmdWriteSettings()` opens the live file with `O_TRUNC` and writes it in place ([helper.cpp:250](helper.cpp#L250)). A crash, full filesystem, or interrupted write can leave a user’s configuration empty or partial. Reuse the helper’s temp-file, `fsync`, and atomic-rename pattern (with directory-fd-safe operations) used for schedules.~~ **Fixed:** `cmdWriteSettings()` now writes to a temp filename (`.mx-cleanup.<pid>.tmp`) via `openat(dirFd, ...)`, `fchmod`/`fchown`/`fsync`s it, then `renameat(dirFd, tmp, dirFd, "mx-cleanup.conf")` and best-effort `fsync`s the directory — all relative to the already directory-fd-safe `openSettingsDirFd()` handle (rather than reusing `writeFileAsRoot()`'s path-string-based mkstemp/rename, which would reintroduce a symlink/TOCTOU race in this user-controlled home-directory path). Verified manually: a fresh write and an overwrite of an existing settings file both succeed with no leftover temp file.

6. [x] **Medium — avoid duplicate system-wide scheduled work when schedules are saved per user.** ~~Each saved per-user schedule may include global operations such as apt/pacman cache cleanup, residual-package purge, and `/var/log` cleanup ([helperlib.cpp:286](helperlib.cpp#L286), [helperlib.cpp:301](helperlib.cpp#L301)). Multiple user schedules can therefore run the same package-manager operation concurrently and contend for locks. Separate global cleanup into one system schedule, or omit global options from per-user schedules and make the UI communicate that scope.~~ **Fixed (chosen approach: shared script called by each user's own schedule):**
   - The UI is back to plain per-user selection (no scope control at all — several earlier attempts at a UI-level "which schedule am I editing" toggle, first inferred from period/checkbox state and then an explicit checkbox, were tried and reverted as unnecessarily complex/fiddly in favor of this simpler design).
   - `write-schedule` still accepts the full, single `ScheduleOptions` set exactly as before this item (cache/thumbs/apt/purge/logs/trash/flatpak together, `--user <selectedUser>`), and `pushApply_clicked()` builds one combined `scheduleOpts` list exactly as it always did. The split into two files happens entirely inside `cmdWriteSchedule()` (`helper.cpp`): `generateSystemScript(opts)` (`helperlib.cpp`) writes apt/purge/logs/trash-all to a single shared, unsuffixed file (`systemScriptPath()`, `/usr/bin/mx-cleanup-system-script`) with **no cron entry of its own**; `generateUserScript(opts)` writes cache/thumbs/trash-user/flatpak to this user's own cron-triggered script, ending with a line that invokes the shared script if it's executable. No new helper verb, no scope UI, no separate "is this scope active" bookkeeping.
   - This sidesteps the whole scope-ambiguity problem the earlier UI attempts were fighting: different users can schedule their own cron entry at different periods (Alice daily, Bob monthly), each independently invoking the same shared script at their own cadence — there is no single "the system schedule's period" to move or clear, so one user's period choice can never clobber another's. The shared script's *content* still follows a simple last-write-wins rule (whoever last applies with apt/purge/logs/trash-all checked determines it), matching how a single shared setting naturally behaves and requiring no extra bookkeeping to avoid — an explicit, accepted tradeoff for the simplicity gained.
   - `loadOptions()` reads the selected user's own file for cache/thumbs/trash-user/flatpak (and period detection, via `loadSchedule()`, is back to that file alone) and separately reads the shared script for apt/purge/logs/trash-all, unconditionally, regardless of period/user — so the UI always shows the true current shared state without any scope-dependent branching.
   - Tests: `testGenerateScheduleScript_*` split into `testGenerateUserScript_*` / `testGenerateSystemScript_*`, plus a new `testGenerateUserScript_CallsSystemScript` asserting the per-user script both invokes the shared script and never embeds apt/purge/logs commands directly.
   - **Round 2:** two further gaps surfaced after the redesign above:
     - `loadOptions()` fell back to `loadSettings()` (this user's own saved apt/log/trash preference) whenever they had no schedule of their own yet — the exact `settings`-then-shared-script fallback ordering that risks a fresh user's stale/default preference silently overwriting the real shared script on their first Apply. Fixed by extracting `loadSystemScriptOptions()`, which always reads the actual shared script (regardless of period or whether this user has a schedule) and is now called from both `loadOptions()` and `loadSettings()`; `loadSettings()` no longer reads `keyAptCleanup`/`keyAptSelection`/`keyAptPurge`/`keyLogsCleanup`/`keyLogsSelection`/`keyLogsOlderThan` at all, and only falls back to this user's saved trash preference when the shared script isn't already scheduling "all users" trash.
     - `cmdWriteSchedule()` wrote the shared system script *before* this user's own script/cron entry, so a failure writing the user's own files could leave the shared script already changed — silently affecting every other user's schedule despite this Apply reporting failure. Reordered so this user's own script (and, for `@reboot`, cron entry) writes first; the shared script writes last, so a failure there leaves the previous shared script fully intact (`writeFileAsRoot()`'s atomic rename), and a failure in this user's own step never touches the shared script at all.
   - **Round 3:** two remaining gaps:
     - **Legacy upgrade migration:** on upgrade from a pre-split release, apt/purge/logs/trash-all commands live inline in the existing user script and no shared script exists — the UI read only the (missing) shared script, displayed those options unchecked, and the next Apply would rewrite the user script without them plus create an empty shared script, silently stopping scheduled global cleanup. `loadSystemScriptOptions()` now takes the user script's content as a legacy fallback: when the shared script doesn't exist yet, the global options are parsed from the legacy user script instead, so they display checked and the next Apply naturally migrates them into the shared script. (The trash-all detection matches the literal `/home/*/` glob, so a legacy per-user trash line can't false-positive.) When the shared script *does* exist, the fallback is ignored — an intentionally-emptied shared script stays authoritative.
     - **Transactional multi-file write:** the round-2 reordering still left a partial-apply window — if the shared-script write failed after the user script was committed, the user's schedule had already changed. `writeFileAsRoot()` is now split into `stageFileAsRoot()`/`commitStagedFile()`/`discardStagedFile()` (`helperlib.{h,cpp}`), and `cmdWriteSchedule()` stages all files (user script, `@reboot` cron entry, shared script) before committing any: every write/space failure aborts with all targets untouched. Only a failure of a commit rename itself (exotic — e.g. target directory removed mid-operation) can still end partially applied, and that residue is documented at the call site. New tests: `testStagedFile_NothingVisibleUntilCommit`, `testStagedFile_DiscardLeavesTargetAndDirUntouched`.

7. [x] **Low — add regression coverage for the orchestration paths.** ~~Existing unit tests cover helper parsing and file primitives, but not generated-script safety, schedule replacement/failure behavior, or the UI’s handling of helper failures. Add tests for hostile filenames in all-log scripts, preserving an existing schedule if a write fails, per-user/base schedule isolation, and a failing `write-settings` helper result.~~ **Fixed** — four new tests in `tests/test_utils.cpp`, one per requested area (in addition to the tests already added alongside items 1–6: `testGenerateUserScript_CallsSystemScript`, `testGenerateSystemScript_AllLogsUsesTruncate`, `testStagedFile_*`):
   - `testGenerateSystemScript_HostileLogFilenamesAreSafe` — end-to-end: generates the "all logs" cleanup script, retargets it at a temp directory containing files named with quote/`$( )`/backtick injection attempts, actually runs it through `/bin/sh`, and asserts every file was truncated while no injected command executed.
   - `testWriteFileAsRoot_FailedWritePreservesExistingTarget` — forces a write failure (read-only directory) and asserts the previously working file is byte-for-byte untouched; together with the `testStagedFile_*` tests this covers the preserve-on-failure behavior `cmdWriteSchedule()` is built from.
   - `testGenerateScripts_ScopeIsolation` — the current equivalent of "per-user/base schedule isolation": the per-user script must never contain `/home/*`, `/var/log`, or package-manager commands, and the shared system script must never reference the submitting user.
   - `testHelperBinary_ReportsFailureExitCodes` — runs the built `helper` binary as a subprocess (skips if absent) and asserts non-zero exit for a missing verb, an unknown verb, `write-settings` for a nonexistent user, an invalid period, per-user options without `--user`, and `remove-schedule` for a nonexistent user. This pins the exit-code contract the UI's failure handling (item 3) depends on. Known boundary: the UI side itself (`saveSettings()` folding a failed helper call into the `failures` list) is not directly unit-tested — instantiating `MainWindow` in the harness would spawn external processes (`logname`, `lslogins`) and read real schedule/settings files, which is deliberately avoided.

