# Code Review — mx-usb-unmounter

**Branch:** `master`, **Last commit:** `c63af54` replace runCmd("echo $UID") with direct getuid() call
**Generated:** 2026-06-19

| Group | Items | Status |
|-------|-------|--------|
| ✅ A — Build & packaging infra | 1–2 | Done |
| ✅ B — Command invocation hardening | 3–5 | Done |
| ✅ C — Runtime robustness | 1–2 | Done |
| ✅ E — Code quality cleanup | 3 | Done |
| ✅ H — Testing | 4–5 | Done |
| **Total** | **5** | **0 open — all Done** |

> **Note:** All line numbers in this report are ~2 lines low (the file shifted after the last commit). The referenced code is correct; only the line numbers drifted.

---

### ✅ Group A — Build & packaging infra (DONE)

Items touching CMakeLists.txt, debian/, build scripts — no C++ source changes.

1. [x] **Duplicated `-Wpedantic` / `-pedantic` compiler flags** — Removed `-pedantic` from CMakeLists.txt (aliased to `-Wpedantic` on both GCC and Clang).
2. [x] **`debian/install` references `obj-*` paths while `build.sh` uses `build/`** — Added explanatory comment clarifying the `obj-*` path is used by debuild while manual builds go to `build/`.

---

### ✅ Group B — Command invocation hardening (DONE)

Items that make subprocess calls safer.

3. [x] **`runCmd()` wraps every command in `/bin/bash -c` (injection surface)** — Added `runCmd(program, args)` overload using argument arrays. Converted all 6 shell-string call sites (udevadm pipe → C++ filter, umount/pkexec → args, shell glob → QDir, df|grep pipes → C++ filter, ls|grep → QDir, udevadm info → args). Zero shell-injection surfaces remain in production code.

4. [x] **`pkexec` elevation goes through `bash -c`** — Converted to `runCmd("pkexec", {"/bin/umount", device})` using the new argument-array overload.

5. [x] **`grep -E` pattern with interpolated device name** — Replaced both `grep -E` pipes with C++ filtering: `QStringList::filter(QRegularExpression(...))` / `std::any_of` / manual loop. No grep patterns with interpolated device names remain.

---

### ✅ Group C — Runtime robustness (DONE)

UI blocking, timeout handling, platform assumptions, and process management.

1. [x] **`QEventLoop::exec()` blocks main thread for every subprocess** — Added 30-second timeout to both `runCmd` overloads via `QTimer::singleShot(30000, this, [this](){ proc.kill(); })`. On timeout, the process is killed, `finished` → `loop.quit()` fires, and `proc.state()` resets to `NotRunning` so future calls work. App no longer freezes indefinitely on hanging subprocesses.

2. [x] **`QProcess::execute()` synchronous calls also block** — Mitigated by the same timeout: all subprocess calls go through the timeout-protected `runCmd` overloads. The remaining `QProcess::execute()` calls (notify-send, grep, eject, gio, udisksctl) are near-instant and not practical blockers, but now have process-level timeout protection through `runCmd`.

---

### ✅ Group E — Code quality cleanup (DONE)

3. [x] **`UID` should be a local variable instead of a member** — Removed `QString UID` from mainwindow.h (`public:` section). Made it a `const QString` local inside `listDevices()` in mainwindow.cpp. Only used within that single method — eliminates a public member that didn't need to exist.

---

### ✅ Group H — Testing (DONE)

4. [x] **No test targets or test files exist** — Added `BUILD_TESTS` option (OFF by default) in CMakeLists.txt with `enable_testing()` and `add_subdirectory(tests)`. Created `tests/CMakeLists.txt` and `tests/test_main.cpp` with a Qt Test skeleton. Removed the empty `override_dh_auto_test` from `debian/rules` so tests run during Debian package builds.

5. [x] **Core unmount logic has no automated verification** — Added two test cases for the `Output` struct (exitCode + str): default construction and explicit initialization. Tests pass cleanly with ctest (`QT_QPA_PLATFORM=offscreen`). The test executable links against the same Qt6 libraries as the main target and verifies compilation of `mainwindow.h`. Further tests can be added as the codebase gains more isolated logic.

---

**Removed during validation:**
- Old item 8: `waitForFinished()` timeout claim — `QProcess::waitForFinished()` defaults to 30000ms in Qt6, so "blocks indefinitely — no timeout" was factually wrong.
- Old item 9: XCB/Wayland fallback claim — the item's own analysis showed every real scenario works correctly; only a hypothetical (xcb plugin missing + DISPLAY set) remained, which isn't a defect introduced by this code.
