Skip to content

Removed LSBLK dependency, fixed progress bar. (Issue #178, partially #171)#3

Merged
R-Larocque merged 23 commits into
feature-win-tweaksfrom
less-deps-feature-win-tweaks
May 7, 2026
Merged

Removed LSBLK dependency, fixed progress bar. (Issue #178, partially #171)#3
R-Larocque merged 23 commits into
feature-win-tweaksfrom
less-deps-feature-win-tweaks

Conversation

@R-Larocque

@R-Larocque R-Larocque commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

Replace external system utilities with Python and /proc-/sysfs-based logic, improve flashing progress reporting, and modernize USB/ISO handling while dropping the Windows tweaks GUI workflow.

New Features:

  • Support detecting and prefilling the user’s downloads directory via XDG config without external helpers.
  • Add /proc-based helpers to detect and kill processes holding a target device and to restore terminal state without shelling out.
  • Support non-.iso disk image extensions when auto-detecting ISO type and improve Linux ISO detection heuristics.

Bug Fixes:

  • Fix dd-based flash progress reporting so the progress bar updates reliably from stderr output and handles non-progress lines without spurious warnings.
  • Ensure LC_ALL is set for dd without mutating the global environment, stabilizing progress parsing across locales.
  • Resolve get_usb_info and find_usb robustness issues by handling pyudev/stat failures gracefully and avoiding lsblk/TimeoutExpired errors.
  • Adjust GUI tooltips, translations trimming, and default dialog paths to improve usability and avoid stale or incorrectly formatted text.

Enhancements:

  • Remove dependencies on lsblk, blockdev, lsof, fuser, and other external tools in favor of psutil, pyudev, and /proc-/sysfs-based implementations for device info, sector size, and process inspection.
  • Refine Windows flashing to use a shared run_cmd helper, sysfs-based disk sizing, and sudo-backed filesystem operations instead of direct os and shutil calls.
  • Simplify the settings/state model by removing the Windows tweaks dialog and related state flags, and streamline About dialog content.
  • Improve CI and packaging by adding pyudev/requests dependencies, updating workflow installs, and bumping the project version to 1.0.1b1.

Build:

  • Re-enable installation of system libraries in the AppImage build script based on requirements-system.txt.

CI:

  • Update GitHub Actions workflows to install pyudev as part of the test environment.

Tests:

  • Expand and refactor tests around flash progress parsing, USB detection, pyudev integration, and Linux ISO detection to cover new behaviours and regressions.

R-Larocque added 3 commits May 6, 2026 22:50
 Please enter the commit message for your changes. Lines starting
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   src/lufus/drives/find_usb.py
	modified:   src/lufus/drives/get_usb_info.py
	modified:   tests/test_find_usb.py
	modified:   tests/test_flash_usb_and_find_usb_fixes.py
	modified:   tests/test_get_usb_info.py
 Please enter the commit message for your changes. Lines starting
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is ahead of 'origin/less-deps-feature-win-tweaks' by 1 commit.
   (use "git push" to publish your local commits)

 Changes to be committed:
	modified:   tests/test_get_usb_info_and_detect_windows_fixes.py
 Please enter the commit message for your changes. Lines starting
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   src/lufus/writing/flash_usb.py
@sourcery-ai

sourcery-ai Bot commented May 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors device and USB handling to remove external CLI dependencies in favor of /proc, sysfs, pyudev, and psutil; improves dd-based flash progress reporting and GUI UX (downloads dir autodetection, tooltips, dialogs); tightens Windows flashing behavior and tweaks handling; and updates tests, build, CI, versioning, and docs accordingly.

Sequence diagram for dd-based USB flashing progress reporting

sequenceDiagram
    actor User
    participant MainWindow
    participant FlashWorker
    participant flash_usb
    participant subprocess_dd as dd_process

    User->>MainWindow: click btn_start
    MainWindow->>MainWindow: validate options
    MainWindow->>FlashWorker: create with options
    MainWindow->>FlashWorker: start()
    activate FlashWorker

    FlashWorker->>flash_usb: flash_usb(device_node, iso_path, scheme, progress_cb, status_cb)

    activate flash_usb
    flash_usb->>flash_usb: build dd_args
    flash_usb->>subprocess_dd: Popen(dd_args, stderr=PIPE, env={LC_ALL="C"})
    flash_usb->>MainWindow: status_cb("Flash starting...")
    flash_usb->>MainWindow: progress_cb(0)

    loop while dd running
        subprocess_dd-->>flash_usb: stderr chunks with CR-separated progress
        flash_usb->>flash_usb: buffer += chunk
        flash_usb->>flash_usb: split on CR/LF
        alt percent line ("NN%" or derived from bytes)
            flash_usb->>MainWindow: status_cb("bytes / total bytes (NN% completed)")
            flash_usb->>MainWindow: progress_cb(NN)
        else dd bookkeeping or other stderr
            flash_usb->>flash_usb: log.info("dd stderr: line")
        end
    end

    subprocess_dd-->>flash_usb: exit code
    flash_usb->>MainWindow: status_cb("dd exited with code X")
    deactivate flash_usb

    FlashWorker-->>MainWindow: flash_done(success)
    deactivate FlashWorker

    MainWindow->>MainWindow: on_flash_finished(success)
    MainWindow->>User: show success/failure dialog, update progress bar
Loading

Sequence diagram for cancelling a flash and killing device-using processes

sequenceDiagram
    actor User
    participant MainWindow
    participant FlashWorker
    participant ProcFD as _procs_using_device_fd
    participant ProcMnt as _procs_with_device_mounted
    participant OS as os_kernel

    User->>MainWindow: click btn_cancel
    activate MainWindow
    MainWindow->>MainWindow: device_node = state.device_node

    MainWindow->>ProcFD: _procs_using_device_fd(device_node)
    ProcFD->>ProcFD: stat(device_node).st_rdev
    ProcFD->>OS: iterate /proc/*/fd
    OS-->>ProcFD: open fds with matching st_rdev
    ProcFD-->>MainWindow: list[(pid, comm)]
    MainWindow->>MainWindow: log processes using device

    alt FlashWorker is running
        MainWindow->>FlashWorker: request_interruption()
        MainWindow->>FlashWorker: wait(2000)
    end

    MainWindow->>ProcMnt: _procs_with_device_mounted(device_node)
    ProcMnt->>ProcMnt: stat(device_node) -> major:minor
    ProcMnt->>OS: read /proc/*/mountinfo
    OS-->>ProcMnt: lines with matching major:minor
    ProcMnt-->>MainWindow: list[(pid, comm)]

    loop for each pid
        MainWindow->>OS: os.kill(pid, SIGKILL)
    end
    MainWindow->>MainWindow: log killed pids or no processes

    alt is_terminal
        MainWindow->>MainWindow: termios.tcgetattr(stdin_fd)
        MainWindow->>MainWindow: restore ICANON,ECHO,ISIG,ICRNL,OPOST
        MainWindow->>MainWindow: termios.tcsetattr(...)
    end

    MainWindow->>MainWindow: reset progress bar, status text, buttons
    deactivate MainWindow
Loading

Updated class diagram for GUI workers, dialogs, and Windows flashing types

classDiagram
    class AppState {
        +str version
        +int filesystem_index
        +int cluster_size
        +int quick_format
        +int check_bad
        +str new_label
        +int flash_mode
        +str iso_path
        +str device_node
        +bool verify_hash
        +str expected_hash
        +int win_hardware_bypass
        +int win_microsoft_acc
        +str win_local_acc
        +int win_privacy
    }

    class FlashWorker {
        +dict options
        +dict _T
        +run()
        +progress:int
        +status:str
        +flash_done:bool
    }

    class LogWindow {
        +__init__(parent)
    }

    class AboutWindow {
        +__init__(parent)
    }

    class SettingsDialog {
        +__init__(parent)
        +accepted()
        +rejected()
    }

    class PartitionInfo {
        <<TypedDict>>
        +str role
        +str path
    }

    class WindowsFlashModule {
        <<module writing.windows.flash>>
        +mount_iso(iso_path) str|None
        +create_partitions(drive, scheme) list~PartitionInfo~
        +_copy_efi_boot_files(iso_mount, mount_efi, _status)
        +_fix_efi_bootloader(efi_mount)
        +_get_disk_size_sectors(drive) int
    }

    class UtilsModule {
        <<module lufus.utils>>
        +run_cmd(cmd list~str~)
        +elevate_privileges()
    }

    class RemovedWinTweaks {
        <<removed QDialog>>
        +log_message(msg)
        +update_winhardware()
        +update_winmicrosoftacc()
        +update_winlocalaccchk()
        +sync_username(new_username)
        +update_winprivacy()
        +applywintweaks()
    }

    %% Relationships
    FlashWorker --> AppState : reads
    FlashWorker ..> WindowsFlashModule : calls
    FlashWorker ..> flash_usb : calls
    LogWindow --> AppState : reads version
    AboutWindow --> AppState : reads version
    SettingsDialog --> AppState : updates theme and preferences
    WindowsFlashModule ..> PartitionInfo : returns list
    WindowsFlashModule ..> UtilsModule : uses run_cmd
    RemovedWinTweaks --> AppState : modified Windows tweak flags
Loading

File-Level Changes

Change Details Files
Replace lsblk/blockdev/lsof/fuser/pgrep/lsblk-based flows with /proc, sysfs, pyudev, psutil, and shared run_cmd utilities.
  • Add /proc-scanning helpers in the GUI to list and kill processes using or mounting a device, replacing lsof/fuser usage.
  • Refactor remount and drive geometry logic to use psutil for mount discovery and sysfs for block sizes instead of blockdev.
  • Switch USB discovery (find_usb) and get_usb_info to pyudev-based label/size lookup with graceful fallbacks and logging.
  • Introduce lufus.utils.run_cmd as a logging subprocess wrapper and use it across Windows flashing/tweaks and sync operations.
src/lufus/gui/gui.py
src/lufus/drives/formatting.py
src/lufus/drives/find_usb.py
src/lufus/drives/get_usb_info.py
src/lufus/utils.py
src/lufus/writing/windows/flash.py
src/lufus/writing/windows/tweaks.py
src/lufus/writing/install_ventoy.py
Improve flashing progress reporting by parsing dd stderr in real time, enforcing LC_ALL, and updating tests.
  • Change flash_usb to spawn dd with LC_ALL=C in a copied environment and parse stderr via read1() for carriage-return-delimited progress.
  • Emit initial 0% milestone, scaled percentage updates from dd byte counts or percent lines, and treat non-progress stderr as info logs.
  • Update and extend tests to cover the new progress parsing behavior, LC_ALL handling, and stderr filtering semantics.
src/lufus/writing/flash_usb.py
tests/test_flash_usb_and_find_usb_fixes.py
Improve GUI UX around downloads detection, ISO/image handling, verification UI, tooltips, and logging while removing WinTweaks dialog integration.
  • Add helpers to resolve the real user home (pkexec/sudo aware) and XDG download directory without xdg-user-dir, and use them when auto-selecting the latest ISO and as the default file dialog directory.
  • Adjust ISO detection trigger to only run for known image extensions, simplify filesystem option definitions, and streamline hash verification UI by removing the separate label.
  • Simplify About dialog contents and toolbar tooltip text, and log USB scan errors and other status messages in a more compact style.
  • Remove WinTweaks dialog usage and related signal wiring from the GUI workflow so flashing no longer prompts for Windows tweaks at start/finish.
src/lufus/gui/gui.py
src/lufus/gui/dialogs.py
Tighten Windows flashing behavior and partitioning logic around schemes, EFI copying, and disk size, while constraining Windows mode handling.
  • Change Windows flash worker path to treat only ISO mode as valid for Windows images and return failure for unsupported Windows DD modes.
  • Update Windows EFI/boot file copying and BOOTX64.EFI fixups to use sudo-run commands via run_cmd (mkdir, cp, sync) instead of direct shutil/os calls.
  • Replace blockdev --getsz in Windows partitioning with a sysfs-based disk size reader that returns sector counts.
src/lufus/gui/workers.py
src/lufus/writing/windows/flash.py
Harden environment handling, privilege elevation, and terminal restoration for CLI/GUI flows.
  • In elevate_privileges, simplify and tighten LUFUS_THEME validation to accept only simple, non-traversing basenames and preserve a controlled set of environment variables.
  • Replace stty-based terminal reset with a termios-based restoration of canonical/echo/signal flags when cancelling from a terminal session.
src/lufus/utils.py
src/lufus/gui/gui.py
Update tests, CI, build scripts, ISO detection heuristics, translations, and versions to match the new behavior and dependencies.
  • Adjust Linux ISO detection markers and heuristics to require multiple Linux markers and extend the marker set, updating the Linux ISO tests accordingly.
  • Refactor tests for find_usb/get_usb_info and GUI get_usb_info to mock pyudev and os.stat instead of lsblk, and add new tests for pyudev error paths.
  • Re-enable system dependency installation in the AppImage build script, add pyudev to CI workflow dependencies, and bump project/Briefcase versions plus add requests as a runtime dependency.
  • Trim whitespace from translation CSV values and slightly tweak the README beta disclaimer wording.
src/lufus/writing/windows/detect.py
tests/test_find_usb.py
tests/test_get_usb_info.py
tests/test_get_usb_info_and_detect_windows_fixes.py
tests/test_linux_iso_detection.py
scripts/build-appimage.sh
.github/workflows/ci.yml
.github/workflows/python-compatibility.yml
pyproject.toml
src/lufus/state.py
src/lufus/gui/i18n.py
README.md
src/lufus/gui/languages/*.csv

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

R-Larocque added 4 commits May 7, 2026 00:34
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   src/lufus/drives/get_usb_info.py
	new file:   tests/conftest.py
	modified:   tests/test_find_usb.py
	modified:   tests/test_flash_usb_and_find_usb_fixes.py
	modified:   tests/test_get_usb_info.py
	modified:   tests/test_get_usb_info_and_detect_windows_fixes.py
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   .github/workflows/python-compatibility.yml
…ting"

This reverts commit dc34beb.

 Please enter the commit message for your changes. Lines starting
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   src/lufus/drives/get_usb_info.py
	deleted:    tests/conftest.py
	modified:   tests/test_find_usb.py
	modified:   tests/test_flash_usb_and_find_usb_fixes.py
	modified:   tests/test_get_usb_info.py
	modified:   tests/test_get_usb_info_and_detect_windows_fixes.py
This reverts commit f77fd1c.

 Please enter the commit message for your changes. Lines starting
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   src/lufus/writing/flash_usb.py
@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   README.md
@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

1 similar comment
@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   .github/workflows/ci.yml
	modified:   src/lufus/drives/get_usb_info.py
	modified:   tests/test_find_usb.py
	modified:   tests/test_flash_usb_and_find_usb_fixes.py
	modified:   tests/test_get_usb_info.py
	modified:   tests/test_get_usb_info_and_detect_windows_fixes.py

 Untracked files:
	TODO
@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 6 issues, and left some high level feedback:

  • In get_usb_info, device.attributes.get("size") from pyudev is likely to be a bytes-like object rather than a plain string; consider normalizing/decoding the attribute (and handling non‑numeric values) before passing it to int() to avoid runtime errors on real systems.
  • There is quite a bit of duplicated mocking setup for os.stat and pyudev across multiple tests; extracting a shared helper or fixture for the udev/stat mocking would make the tests easier to maintain and less error-prone.
  • The updated docstring in TestLabelIsWindowsDeadBranch includes informal language ("Idk why I don't like that, but eh, it works..."); consider reverting to a neutral description to keep tests and diagnostics professional and focused on behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_usb_info`, `device.attributes.get("size")` from pyudev is likely to be a bytes-like object rather than a plain string; consider normalizing/decoding the attribute (and handling non‑numeric values) before passing it to `int()` to avoid runtime errors on real systems.
- There is quite a bit of duplicated mocking setup for `os.stat` and `pyudev` across multiple tests; extracting a shared helper or fixture for the udev/stat mocking would make the tests easier to maintain and less error-prone.
- The updated docstring in `TestLabelIsWindowsDeadBranch` includes informal language (`"Idk why I don't like that, but eh, it works..."`); consider reverting to a neutral description to keep tests and diagnostics professional and focused on behavior.

## Individual Comments

### Comment 1
<location path="src/lufus/drives/get_usb_info.py" line_range="33-37" />
<code_context>
-        if not size_output.isdigit():
-            log.warning("Could not parse device size: %r", size_output)
+        # Size in bytes: udev attributes 'size' is in 512-byte sectors
+        size_attr = device.attributes.get("size")
+        usb_size = int(size_attr) * 512 if size_attr else 0
+
+        label = device.get("ID_FS_LABEL")
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider handling and logging invalid or missing `size` attributes more explicitly.

The previous implementation logged a warning when `lsblk` returned a non-numeric size before defaulting to 0. In this new path, missing or non-numeric `size_attr` quietly results in 0, which can obscure size-detection issues. Please add a warning when `size_attr` is `None` or when `int(size_attr)` fails, so operators can distinguish parsing failures from genuine 0-sized devices.

```suggestion
        # Size in bytes: udev attributes 'size' is in 512-byte sectors
        size_attr = device.attributes.get("size")
        if size_attr is None:
            log.warning(
                "Could not get 'size' attribute from udev for device %s; defaulting to 0 bytes",
                device_node,
            )
            usb_size = 0
        else:
            try:
                usb_size = int(size_attr) * 512
            except (TypeError, ValueError):
                log.warning(
                    "Could not parse udev 'size' attribute %r for device %s; defaulting to 0 bytes",
                    size_attr,
                    device_node,
                )
                usb_size = 0

        label = device.get("ID_FS_LABEL")
```
</issue_to_address>

### Comment 2
<location path="src/lufus/drives/find_usb.py" line_range="56-61" />
<code_context>

         label = None
         try:
-            label = subprocess.check_output(
-                ["lsblk", "-d", "-n", "-o", "LABEL", device_node],
-                text=True,
-                timeout=5,
-            ).strip()
-        except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
+            # Using os.stat to get device number as per requirements
+            st = os.stat(device_node)
+            device = pyudev.Devices.from_device_number(context, "block", st.st_rdev)
+            label = device.get("ID_FS_LABEL")
+        except Exception:
             pass

</code_context>
<issue_to_address>
**issue (bug_risk):** The blanket `except Exception: pass` may hide real issues when querying pyudev.

Catching all exceptions here can mask persistent pyudev failures (e.g. permissions/env issues) and silently reduce behavior to always using `os.path.basename`. Please narrow the exception to the specific pyudev errors you expect, or at least log at debug level so failures in this block remain visible.
</issue_to_address>

### Comment 3
<location path=".github/workflows/python-compatibility.yml" line_range="17" />
<code_context>
     strategy:
       fail-fast: false
       matrix:
-        python-version: ["3.10", "3.11", "3.12", "3.13", "3.14-dev"]
+        python-version: ["3.10", "3.13"]

     steps:
</code_context>
<issue_to_address>
**question (testing):** Reducing the Python matrix may drop coverage for supported runtimes.

This change speeds up CI but drops CI coverage for 3.11 and 3.12. If we still claim support for those versions, consider keeping at least one (e.g. 3.11 or 3.12) in the matrix to help catch compatibility issues across 3.10–3.13.
</issue_to_address>

### Comment 4
<location path="tests/test_get_usb_info.py" line_range="97-100" />
<code_context>


-def test_get_usb_info_returns_empty_when_lsblk_fails(monkeypatch) -> None:
+def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
     mount_path = "/media/testuser/USB"
     device_node = "/dev/sdb1"
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test where pyudev fails independently of os.stat to fully exercise the new error-handling path.

The existing `test_get_usb_info_returns_empty_when_pyudev_fails` only covers failures via `os.stat` raising. Since the code can also fail when `pyudev.Devices.from_device_number` or attribute access on the returned `device` raises, please add a test where `os.stat` succeeds but the pyudev lookup fails (e.g., `Devices.from_device_number` raises or returns a device without `attributes['size']`) to verify these cases also return `None`.

Suggested implementation:

```python
def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:

```

Please add a new test function to cover the case where `os.stat` succeeds but the pyudev lookup fails independently (e.g. `Devices.from_device_number` raises). Place it near the other `get_usb_info` tests, for example directly after `test_get_usb_info_returns_empty_when_pyudev_fails`:

```python
def test_get_usb_info_returns_empty_when_pyudev_device_lookup_raises(monkeypatch) -> None:
    mount_path = "/media/testuser/USB"
    device_node = "/dev/sdb1"

    # Partition for the mount path is found
    monkeypatch.setattr(
        get_usb_info_module.psutil,
        "disk_partitions",
        lambda *args, **kwargs: [
            SimpleNamespace(mountpoint=mount_path, device=device_node)
        ],
    )

    # os.stat succeeds and yields a valid st_dev
    monkeypatch.setattr(
        get_usb_info_module.os,
        "stat",
        lambda p: SimpleNamespace(st_dev=1234),
    )

    # pyudev context can be constructed, but the device lookup itself fails
    mock_context = object()
    monkeypatch.setattr(
        get_usb_info_module.pyudev,
        "Context",
        lambda: mock_context,
    )

    def failing_from_device_number(*args, **kwargs):
        raise RuntimeError("pyudev failure")

    monkeypatch.setattr(
        get_usb_info_module.pyudev.Devices,
        "from_device_number",
        failing_from_device_number,
    )

    result = get_usb_info_module.get_usb_info(mount_path)

    # When pyudev fails independently of os.stat, we should still return an empty result
    assert result is None
```

If your implementation also has a separate failure path when the device is found but `device.attributes["size"]` is missing or raises, you can add a sister test to explicitly cover that:

```python
def test_get_usb_info_returns_empty_when_pyudev_missing_size_attribute(monkeypatch) -> None:
    mount_path = "/media/testuser/USB"
    device_node = "/dev/sdb1"

    monkeypatch.setattr(
        get_usb_info_module.psutil,
        "disk_partitions",
        lambda *args, **kwargs: [
            SimpleNamespace(mountpoint=mount_path, device=device_node)
        ],
    )

    monkeypatch.setattr(
        get_usb_info_module.os,
        "stat",
        lambda p: SimpleNamespace(st_dev=1234),
    )

    mock_context = object()
    monkeypatch.setattr(
        get_usb_info_module.pyudev,
        "Context",
        lambda: mock_context,
    )

    mock_device = SimpleNamespace()
    # Simulate missing / inaccessible size attribute
    mock_device.attributes = {}

    monkeypatch.setattr(
        get_usb_info_module.pyudev.Devices,
        "from_device_number",
        lambda *args, **kwargs: mock_device,
    )

    result = get_usb_info_module.get_usb_info(mount_path)
    assert result is None
```

Adjust the final assertions (`assert result is None` vs `assert result == {}` or similar) to match the actual "empty" value returned by `get_usb_info` in your codebase, keeping it consistent with the existing `test_get_usb_info_returns_empty_when_pyudev_fails` expectations.
</issue_to_address>

### Comment 5
<location path="tests/test_get_usb_info.py" line_range="49-53" />
<code_context>
+
+    # Mock pyudev
+    mock_context = MagicMock()
+    mock_device = MagicMock()
+    mock_device.get.return_value = "lufus_USB"
+    monkeypatch.setattr(find_usb_module.pyudev, "Context", lambda: mock_context)
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for malformed or non-integer pyudev size attributes.

Right now the tests only cover cases where `device.attributes['size']` is a valid integer string. Since the code does `int(size_attr) * 512 if size_attr else 0` and catches all `Exception`, please add a test where `size` is non-numeric (e.g. `'abc'`) and assert the resulting behavior (likely `None`). This will lock in the expected handling of malformed udev metadata and avoid regressions around `ValueError` handling.
</issue_to_address>

### Comment 6
<location path="tests/test_find_usb.py" line_range="66-62" />
<code_context>


-def test_find_usb_falls_back_to_dir_name_when_lsblk_fails(monkeypatch) -> None:
+def test_find_usb_falls_back_to_dir_name_when_pyudev_fails(monkeypatch) -> None:
     user = "testuser"
     mount_path = f"/media/{user}/NO_LABEL"
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test covering the fallback when pyudev returns an empty label rather than raising.

The current test only covers the exception path. Since the code also falls back to the directory name when `label` is falsy, please add a test where `Devices.from_device_number` succeeds but `device.get("ID_FS_LABEL")` returns an empty string, to explicitly cover that branch.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lufus/drives/get_usb_info.py Outdated
Comment thread src/lufus/drives/find_usb.py Outdated
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14-dev"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (testing): Reducing the Python matrix may drop coverage for supported runtimes.

This change speeds up CI but drops CI coverage for 3.11 and 3.12. If we still claim support for those versions, consider keeping at least one (e.g. 3.11 or 3.12) in the matrix to help catch compatibility issues across 3.10–3.13.

Comment on lines +97 to 100
def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
mount_path = "/media/testuser/USB"
device_node = "/dev/sdb1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test where pyudev fails independently of os.stat to fully exercise the new error-handling path.

The existing test_get_usb_info_returns_empty_when_pyudev_fails only covers failures via os.stat raising. Since the code can also fail when pyudev.Devices.from_device_number or attribute access on the returned device raises, please add a test where os.stat succeeds but the pyudev lookup fails (e.g., Devices.from_device_number raises or returns a device without attributes['size']) to verify these cases also return None.

Suggested implementation:

def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:

Please add a new test function to cover the case where os.stat succeeds but the pyudev lookup fails independently (e.g. Devices.from_device_number raises). Place it near the other get_usb_info tests, for example directly after test_get_usb_info_returns_empty_when_pyudev_fails:

def test_get_usb_info_returns_empty_when_pyudev_device_lookup_raises(monkeypatch) -> None:
    mount_path = "/media/testuser/USB"
    device_node = "/dev/sdb1"

    # Partition for the mount path is found
    monkeypatch.setattr(
        get_usb_info_module.psutil,
        "disk_partitions",
        lambda *args, **kwargs: [
            SimpleNamespace(mountpoint=mount_path, device=device_node)
        ],
    )

    # os.stat succeeds and yields a valid st_dev
    monkeypatch.setattr(
        get_usb_info_module.os,
        "stat",
        lambda p: SimpleNamespace(st_dev=1234),
    )

    # pyudev context can be constructed, but the device lookup itself fails
    mock_context = object()
    monkeypatch.setattr(
        get_usb_info_module.pyudev,
        "Context",
        lambda: mock_context,
    )

    def failing_from_device_number(*args, **kwargs):
        raise RuntimeError("pyudev failure")

    monkeypatch.setattr(
        get_usb_info_module.pyudev.Devices,
        "from_device_number",
        failing_from_device_number,
    )

    result = get_usb_info_module.get_usb_info(mount_path)

    # When pyudev fails independently of os.stat, we should still return an empty result
    assert result is None

If your implementation also has a separate failure path when the device is found but device.attributes["size"] is missing or raises, you can add a sister test to explicitly cover that:

def test_get_usb_info_returns_empty_when_pyudev_missing_size_attribute(monkeypatch) -> None:
    mount_path = "/media/testuser/USB"
    device_node = "/dev/sdb1"

    monkeypatch.setattr(
        get_usb_info_module.psutil,
        "disk_partitions",
        lambda *args, **kwargs: [
            SimpleNamespace(mountpoint=mount_path, device=device_node)
        ],
    )

    monkeypatch.setattr(
        get_usb_info_module.os,
        "stat",
        lambda p: SimpleNamespace(st_dev=1234),
    )

    mock_context = object()
    monkeypatch.setattr(
        get_usb_info_module.pyudev,
        "Context",
        lambda: mock_context,
    )

    mock_device = SimpleNamespace()
    # Simulate missing / inaccessible size attribute
    mock_device.attributes = {}

    monkeypatch.setattr(
        get_usb_info_module.pyudev.Devices,
        "from_device_number",
        lambda *args, **kwargs: mock_device,
    )

    result = get_usb_info_module.get_usb_info(mount_path)
    assert result is None

Adjust the final assertions (assert result is None vs assert result == {} or similar) to match the actual "empty" value returned by get_usb_info in your codebase, keeping it consistent with the existing test_get_usb_info_returns_empty_when_pyudev_fails expectations.

Comment thread tests/test_get_usb_info.py Outdated
Comment on lines +49 to +53
mock_device = MagicMock()
mock_device.attributes = {"size": str((16 * 1024**3) // 512)}
mock_device.get.return_value = "MYUSB"
monkeypatch.setattr(get_usb_info_module.pyudev, "Context", lambda: mock_context)
monkeypatch.setattr(get_usb_info_module.pyudev.Devices, "from_device_number", lambda ctx, type, num: mock_device)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding a test for malformed or non-integer pyudev size attributes.

Right now the tests only cover cases where device.attributes['size'] is a valid integer string. Since the code does int(size_attr) * 512 if size_attr else 0 and catches all Exception, please add a test where size is non-numeric (e.g. 'abc') and assert the resulting behavior (likely None). This will lock in the expected handling of malformed udev metadata and avoid regressions around ValueError handling.

Comment thread tests/test_find_usb.py
monkeypatch.setattr(find_usb_module.pyudev, "Context", lambda: mock_context)
monkeypatch.setattr(find_usb_module.pyudev.Devices, "from_device_number", lambda ctx, type, num: mock_device)

result = find_usb_module.find_usb()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test covering the fallback when pyudev returns an empty label rather than raising.

The current test only covers the exception path. Since the code also falls back to the directory name when label is falsy, please add a test where Devices.from_device_number succeeds but device.get("ID_FS_LABEL") returns an empty string, to explicitly cover that branch.

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 issues, and left some high level feedback:

  • The pyudev-based lookups in both get_usb_info and find_usb are wrapped in broad Exception handlers; consider narrowing these to catch only expected pyudev/os errors so that unexpected bugs don’t get silently swallowed.
  • There is quite a bit of duplicated mocking logic for os.stat and pyudev across multiple tests; extracting a shared helper (similar to _mock_os_stat_and_pyudev) into a common test utility would make the tests easier to maintain.
  • The updated test docstring for TestLabelIsWindowsDeadBranch now includes informal wording (“Idk why…”); consider reverting to a neutral explanation to keep comments focused and professional.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The pyudev-based lookups in both get_usb_info and find_usb are wrapped in broad Exception handlers; consider narrowing these to catch only expected pyudev/os errors so that unexpected bugs don’t get silently swallowed.
- There is quite a bit of duplicated mocking logic for os.stat and pyudev across multiple tests; extracting a shared helper (similar to _mock_os_stat_and_pyudev) into a common test utility would make the tests easier to maintain.
- The updated test docstring for TestLabelIsWindowsDeadBranch now includes informal wording (“Idk why…”); consider reverting to a neutral explanation to keep comments focused and professional.

## Individual Comments

### Comment 1
<location path="src/lufus/drives/get_usb_info.py" line_range="33-37" />
<code_context>
-        if not size_output.isdigit():
-            log.warning("Could not parse device size: %r", size_output)
+        # Size in bytes: udev attributes 'size' is in 512-byte sectors
+        size_attr = device.attributes.get("size")
+        usb_size = int(size_attr) * 512 if size_attr else 0
+
+        label = device.get("ID_FS_LABEL")
</code_context>
<issue_to_address>
**suggestion:** Consider handling missing or non-numeric `size` attributes more explicitly.

With this change, missing `size` now silently maps to `usb_size = 0`, and non-numeric `size` will raise and hit the generic `Exception` path, returning `None`. Consider explicitly handling both cases: log a clear warning when `size` is missing or non-numeric, and, where reasonable, still return a `USBDeviceInfo` with `usb_size = 0`. This preserves the previous, more predictable behavior and keeps diagnosing udev issues easier.

```suggestion
        # Size in bytes: udev attribute 'size' is in 512-byte sectors
        size_attr = device.attributes.get("size")
        usb_size = 0
        if size_attr is None:
            log.warning(
                "Missing 'size' attribute for device %s; assuming size 0 bytes.",
                device_node,
            )
        else:
            try:
                usb_size = int(size_attr) * 512
            except (TypeError, ValueError):
                log.warning(
                    "Could not parse numeric 'size' attribute %r for device %s; "
                    "assuming size 0 bytes.",
                    size_attr,
                    device_node,
                )

        label = device.get("ID_FS_LABEL")
```
</issue_to_address>

### Comment 2
<location path="src/lufus/drives/find_usb.py" line_range="61" />
<code_context>
+            st = os.stat(device_node)
+            device = pyudev.Devices.from_device_number(context, "block", st.st_rdev)
+            label = device.get("ID_FS_LABEL")
+        except Exception:
             pass

</code_context>
<issue_to_address>
**issue (bug_risk):** Catching all exceptions here may hide unexpected failures when inspecting devices.

Previously we only suppressed `CalledProcessError`/`TimeoutExpired`, which matched the expected `lsblk` failures. With `pyudev`, `except Exception:` will now swallow any error from `os.stat` or `pyudev.Devices.from_device_number`, including programming or environment issues. Please narrow this to the specific exceptions you expect (or at least log unexpected ones) so real problems with device enumeration are visible.
</issue_to_address>

### Comment 3
<location path="tests/test_get_usb_info.py" line_range="93-97" />
<code_context>


-def test_get_usb_info_returns_empty_when_lsblk_fails(monkeypatch) -> None:
+def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
     mount_path = "/media/testuser/USB"
     device_node = "/dev/sdb1"
</code_context>
<issue_to_address>
**suggestion (testing):** Add a dedicated test for PermissionError in get_usb_info to ensure that branch is covered.

Right now only the generic exception path from `os.stat` is tested. Please add a test that explicitly makes `get_usb_info_module.os.stat` raise `PermissionError` and verifies that `get_usb_info` returns `None` without re-raising, so the dedicated handler is covered and guarded against regressions.

```suggestion
    result = get_usb_info_module.get_usb_info(mount_path)
    assert result["label"] == "NO_LABEL"


def test_get_usb_info_returns_none_on_permission_error(monkeypatch) -> None:
    mount_path = "/media/testuser/USB"
    device_node = "/dev/sdb1"

    monkeypatch.setattr(
        get_usb_info_module.psutil,
        "disk_partitions",
        lambda *args, **kwargs: [
            SimpleNamespace(mountpoint=mount_path, device=device_node)
        ],
    )

    def mock_os_stat(path: str) -> None:
        raise PermissionError("Permission denied")

    monkeypatch.setattr(get_usb_info_module.os, "stat", mock_os_stat)

    result = get_usb_info_module.get_usb_info(mount_path)

    assert result is None


def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lufus/drives/get_usb_info.py Outdated
Comment thread src/lufus/drives/find_usb.py Outdated
Comment on lines 93 to +97
result = get_usb_info_module.get_usb_info(mount_path)
assert result["label"] == "NO_LABEL"


def test_get_usb_info_returns_empty_when_lsblk_fails(monkeypatch) -> None:
def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a dedicated test for PermissionError in get_usb_info to ensure that branch is covered.

Right now only the generic exception path from os.stat is tested. Please add a test that explicitly makes get_usb_info_module.os.stat raise PermissionError and verifies that get_usb_info returns None without re-raising, so the dedicated handler is covered and guarded against regressions.

Suggested change
result = get_usb_info_module.get_usb_info(mount_path)
assert result["label"] == "NO_LABEL"
def test_get_usb_info_returns_empty_when_lsblk_fails(monkeypatch) -> None:
def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
result = get_usb_info_module.get_usb_info(mount_path)
assert result["label"] == "NO_LABEL"
def test_get_usb_info_returns_none_on_permission_error(monkeypatch) -> None:
mount_path = "/media/testuser/USB"
device_node = "/dev/sdb1"
monkeypatch.setattr(
get_usb_info_module.psutil,
"disk_partitions",
lambda *args, **kwargs: [
SimpleNamespace(mountpoint=mount_path, device=device_node)
],
)
def mock_os_stat(path: str) -> None:
raise PermissionError("Permission denied")
monkeypatch.setattr(get_usb_info_module.os, "stat", mock_os_stat)
result = get_usb_info_module.get_usb_info(mount_path)
assert result is None
def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:

@SourceryAI SourceryAI 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.

Hey - I've found 4 issues, and left some high level feedback:

  • The pyudev/os.stat mocking logic in the tests (e.g. for get_usb_info and find_usb) is duplicated and slightly varied; consider extracting a shared helper or pytest fixture to centralize this setup and reduce risk of inconsistencies between tests.
  • In get_usb_info, parsing the udev size attribute assumes it is always a valid integer; adding a small try/except around the int(size_attr) conversion with a targeted warning would make the behaviour clearer and avoid the generic catch-all handling unexpected ValueError cases.
  • The new pyudev integration in find_usb wraps all errors in a broad except Exception with no logging; adding at least a debug-level log there would make it easier to diagnose situations where label lookup repeatedly fails without surfacing why.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The pyudev/os.stat mocking logic in the tests (e.g. for get_usb_info and find_usb) is duplicated and slightly varied; consider extracting a shared helper or pytest fixture to centralize this setup and reduce risk of inconsistencies between tests.
- In get_usb_info, parsing the udev `size` attribute assumes it is always a valid integer; adding a small try/except around the `int(size_attr)` conversion with a targeted warning would make the behaviour clearer and avoid the generic catch-all handling unexpected ValueError cases.
- The new pyudev integration in find_usb wraps all errors in a broad `except Exception` with no logging; adding at least a debug-level log there would make it easier to diagnose situations where label lookup repeatedly fails without surfacing why.

## Individual Comments

### Comment 1
<location path="src/lufus/drives/get_usb_info.py" line_range="33-37" />
<code_context>
-        if not size_output.isdigit():
-            log.warning("Could not parse device size: %r", size_output)
+        # Size in bytes: udev attributes 'size' is in 512-byte sectors
+        size_attr = device.attributes.get("size")
+        usb_size = int(size_attr) * 512 if size_attr else 0
+
+        label = device.get("ID_FS_LABEL")
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Handle non-numeric or malformed `size` attributes more defensively.

If `size_attr` is present but not a clean integer, `int(size_attr)` will raise `ValueError` and we’ll hit the generic `Exception` handler, causing the whole function to return `None` instead of just treating the size as unknown. Wrap the cast in a narrow `try/except` for `TypeError`/`ValueError`, log a warning, and fall back to `usb_size = 0` so other USB info can still be returned.

```suggestion
        # Size in bytes: udev attributes 'size' is in 512-byte sectors
        size_attr = device.attributes.get("size")
        usb_size = 0
        if size_attr:
            try:
                usb_size = int(size_attr) * 512
            except (TypeError, ValueError):
                log.warning(
                    "Could not parse device size attribute %r; treating size as unknown",
                    size_attr,
                )

        label = device.get("ID_FS_LABEL")
```
</issue_to_address>

### Comment 2
<location path="tests/test_get_usb_info.py" line_range="97-100" />
<code_context>


-def test_get_usb_info_returns_empty_when_lsblk_fails(monkeypatch) -> None:
+def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
     mount_path = "/media/testuser/USB"
     device_node = "/dev/sdb1"
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for PermissionError and malformed pyudev size to fully cover get_usb_info error handling.

This currently only exercises the generic `Exception` path via `os.stat` failure. To fully cover `get_usb_info`’s error handling, please also add tests that:

1. Simulate `PermissionError` from `os.stat` or `pyudev.Devices.from_device_number` and assert `get_usb_info` returns `None`.
2. Simulate a non-numeric `device.attributes.get("size")` that causes `int(size_attr)` to raise `ValueError`, and assert it also returns `None`.

These will ensure the dedicated `PermissionError` branch and malformed size handling remain covered and regressions are caught.
</issue_to_address>

### Comment 3
<location path="tests/test_find_usb.py" line_range="66-62" />
<code_context>


-def test_find_usb_falls_back_to_dir_name_when_lsblk_fails(monkeypatch) -> None:
+def test_find_usb_falls_back_to_dir_name_when_pyudev_fails(monkeypatch) -> None:
     user = "testuser"
     mount_path = f"/media/{user}/NO_LABEL"
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for pyudev returning an empty/None label (non-exceptional) to exercise the fallback path.

The new test covers the exception path from `pyudev.Devices.from_device_number`. Please also add a test where `from_device_number` succeeds but `device.get("ID_FS_LABEL")` returns `None` or `""`, so we verify the fallback to the directory basename for non-exceptional pyudev failures as well.
</issue_to_address>

### Comment 4
<location path="tests/test_get_usb_info_and_detect_windows_fixes.py" line_range="24-33" />
<code_context>
-        if "SIZE" in cmd:
-            return size + "\n"
-        return label + "\n"
+def _mock_os_stat_and_pyudev(monkeypatch, size="1000000000", label="MY_USB"):
+    # Mock os.stat safely
+    os_stat_orig = gui_module.os.stat

-    return impl
+    def mock_os_stat(p):
+        if str(p).startswith("/dev/"):
+            m = MagicMock()
+            m.st_rdev = 1234
+            return m
+        return os_stat_orig(p)
+
+    monkeypatch.setattr(gui_module.os, "stat", mock_os_stat)
+
+    # Mock pyudev
+    mock_context = MagicMock()
+    mock_device = MagicMock()
+    # size in 512-byte sectors
+    mock_device.attributes = {"size": str(int(size) // 512)}
+    mock_device.get.return_value = label
+    monkeypatch.setattr(gui_module.pyudev, "Context", lambda: mock_context)
+    monkeypatch.setattr(gui_module.pyudev.Devices, "from_device_number", lambda ctx, type, num: mock_device)


</code_context>
<issue_to_address>
**suggestion:** Consider reusing _mock_os_stat_and_pyudev in other tests to reduce duplication and keep behaviour aligned.

Similar pyudev mocking appears in `tests/test_get_usb_info.py` and `tests/test_find_usb.py`. Reusing this helper there (or moving it to a shared test helper/fixture) would keep all pyudev-dependent tests aligned on how `os.stat.st_rdev` and `device.attributes['size']` are interpreted, and make future changes to the pyudev integration easier to apply consistently. This is a non-blocking suggestion for test maintainability.

Suggested implementation:

```python
def _fake_partitions(mount, device):
    return lambda all=False: [SimpleNamespace(mountpoint=mount, device=device)]


def _mock_os_stat_and_pyudev(monkeypatch, size="1000000000", label="MY_USB"):
    # Mock os.stat safely
    os_stat_orig = gui_module.os.stat

    def mock_os_stat(p):
        if str(p).startswith("/dev/"):
            m = MagicMock()
            m.st_rdev = 1234
            return m
        return os_stat_orig(p)

    monkeypatch.setattr(gui_module.os, "stat", mock_os_stat)

    # Mock pyudev
    mock_context = MagicMock()
    mock_device = MagicMock()
    # size in 512-byte sectors
    mock_device.attributes = {"size": str(int(size) // 512)}
    mock_device.get.return_value = label
    monkeypatch.setattr(gui_module.pyudev, "Context", lambda: mock_context)
    monkeypatch.setattr(gui_module.pyudev.Devices, "from_device_number", lambda ctx, type, num: mock_device)

```

To fully implement the reuse suggestion across the test suite, you should also:
1. Ensure `gui_module` and `MagicMock` are imported at the top of `tests/test_get_usb_info_and_detect_windows_fixes.py` (if not already present):
   - `from unittest.mock import MagicMock`
   - `import gui_module` (or whatever module contains the GUI code under test).
2. In `tests/test_get_usb_info.py` and `tests/test_find_usb.py`, remove any duplicated pyudev/os.stat mocking logic and call `_mock_os_stat_and_pyudev(monkeypatch, ...)` from this module instead, e.g.:
   - `from tests.test_get_usb_info_and_detect_windows_fixes import _mock_os_stat_and_pyudev`
   - Then use `_mock_os_stat_and_pyudev(monkeypatch, size="...", label="...")` in those tests.
3. If you prefer not to import from a test module, you can instead move `_mock_os_stat_and_pyudev` into a shared helper (e.g. `tests/usb_test_helpers.py` or `tests/conftest.py`) and update:
   - This file to import and use that shared helper.
   - `tests/test_get_usb_info.py` and `tests/test_find_usb.py` to import and use the shared helper as well.
</issue_to_address>

Hi @Radiump123! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lufus/drives/get_usb_info.py Outdated
Comment on lines +97 to 100
def test_get_usb_info_returns_empty_when_pyudev_fails(monkeypatch) -> None:
mount_path = "/media/testuser/USB"
device_node = "/dev/sdb1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for PermissionError and malformed pyudev size to fully cover get_usb_info error handling.

This currently only exercises the generic Exception path via os.stat failure. To fully cover get_usb_info’s error handling, please also add tests that:

  1. Simulate PermissionError from os.stat or pyudev.Devices.from_device_number and assert get_usb_info returns None.
  2. Simulate a non-numeric device.attributes.get("size") that causes int(size_attr) to raise ValueError, and assert it also returns None.

These will ensure the dedicated PermissionError branch and malformed size handling remain covered and regressions are caught.

Comment thread tests/test_find_usb.py
monkeypatch.setattr(find_usb_module.pyudev, "Context", lambda: mock_context)
monkeypatch.setattr(find_usb_module.pyudev.Devices, "from_device_number", lambda ctx, type, num: mock_device)

result = find_usb_module.find_usb()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for pyudev returning an empty/None label (non-exceptional) to exercise the fallback path.

The new test covers the exception path from pyudev.Devices.from_device_number. Please also add a test where from_device_number succeeds but device.get("ID_FS_LABEL") returns None or "", so we verify the fallback to the directory basename for non-exceptional pyudev failures as well.

Comment on lines +24 to +33
def _mock_os_stat_and_pyudev(monkeypatch, size="1000000000", label="MY_USB"):
# Mock os.stat safely
os_stat_orig = gui_module.os.stat

return impl
def mock_os_stat(p):
if str(p).startswith("/dev/"):
m = MagicMock()
m.st_rdev = 1234
return m
return os_stat_orig(p)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider reusing _mock_os_stat_and_pyudev in other tests to reduce duplication and keep behaviour aligned.

Similar pyudev mocking appears in tests/test_get_usb_info.py and tests/test_find_usb.py. Reusing this helper there (or moving it to a shared test helper/fixture) would keep all pyudev-dependent tests aligned on how os.stat.st_rdev and device.attributes['size'] are interpreted, and make future changes to the pyudev integration easier to apply consistently. This is a non-blocking suggestion for test maintainability.

Suggested implementation:

def _fake_partitions(mount, device):
    return lambda all=False: [SimpleNamespace(mountpoint=mount, device=device)]


def _mock_os_stat_and_pyudev(monkeypatch, size="1000000000", label="MY_USB"):
    # Mock os.stat safely
    os_stat_orig = gui_module.os.stat

    def mock_os_stat(p):
        if str(p).startswith("/dev/"):
            m = MagicMock()
            m.st_rdev = 1234
            return m
        return os_stat_orig(p)

    monkeypatch.setattr(gui_module.os, "stat", mock_os_stat)

    # Mock pyudev
    mock_context = MagicMock()
    mock_device = MagicMock()
    # size in 512-byte sectors
    mock_device.attributes = {"size": str(int(size) // 512)}
    mock_device.get.return_value = label
    monkeypatch.setattr(gui_module.pyudev, "Context", lambda: mock_context)
    monkeypatch.setattr(gui_module.pyudev.Devices, "from_device_number", lambda ctx, type, num: mock_device)

To fully implement the reuse suggestion across the test suite, you should also:

  1. Ensure gui_module and MagicMock are imported at the top of tests/test_get_usb_info_and_detect_windows_fixes.py (if not already present):
    • from unittest.mock import MagicMock
    • import gui_module (or whatever module contains the GUI code under test).
  2. In tests/test_get_usb_info.py and tests/test_find_usb.py, remove any duplicated pyudev/os.stat mocking logic and call _mock_os_stat_and_pyudev(monkeypatch, ...) from this module instead, e.g.:
    • from tests.test_get_usb_info_and_detect_windows_fixes import _mock_os_stat_and_pyudev
    • Then use _mock_os_stat_and_pyudev(monkeypatch, size="...", label="...") in those tests.
  3. If you prefer not to import from a test module, you can instead move _mock_os_stat_and_pyudev into a shared helper (e.g. tests/usb_test_helpers.py or tests/conftest.py) and update:
    • This file to import and use that shared helper.
    • tests/test_get_usb_info.py and tests/test_find_usb.py to import and use the shared helper as well.

@SourceryAI SourceryAI 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.

Hey - I've found 1 issue, and left some high level feedback:

  • The os.stat/pyudev mocking pattern is repeated across several tests; consider extracting a small helper or fixture (similar to _mock_os_stat_and_pyudev) to avoid duplication and make the tests easier to maintain.
  • In get_usb_info and find_usb, the blanket except Exception around the pyudev logic makes it harder to reason about failures; narrowing this to expected error types (e.g. OSError/pyudev errors) would keep logging and behaviour more predictable while still handling failure cases.
  • When reading the size attribute from the udev device, it may be worth defensively validating that the value is a non-negative integer before int() conversion to avoid unexpected ValueError or weird values from unusual udev setups.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The os.stat/pyudev mocking pattern is repeated across several tests; consider extracting a small helper or fixture (similar to `_mock_os_stat_and_pyudev`) to avoid duplication and make the tests easier to maintain.
- In `get_usb_info` and `find_usb`, the blanket `except Exception` around the pyudev logic makes it harder to reason about failures; narrowing this to expected error types (e.g. `OSError`/pyudev errors) would keep logging and behaviour more predictable while still handling failure cases.
- When reading the `size` attribute from the udev device, it may be worth defensively validating that the value is a non-negative integer before `int()` conversion to avoid unexpected `ValueError` or weird values from unusual udev setups.

## Individual Comments

### Comment 1
<location path=".github/workflows/python-compatibility.yml" line_range="11-12" />
<code_context>

 jobs:
   test:
-    name: "Test — Python ${{ matrix.python-version }}"
+    name: "Run Tests (Python 3.10+)"
     runs-on: ubuntu-latest
     strategy:
</code_context>
<issue_to_address>
**suggestion:** Job name no longer differentiates between Python versions in the matrix

The static name makes all matrix jobs look identical in the Actions UI, which makes it harder to see which Python version failed. Please reintroduce the version in the job name (e.g., `Run Tests (Python ${{ matrix.python-version }})`) so each matrix entry is distinguishable.

```suggestion
  test:
    name: "Run Tests (Python ${{ matrix.python-version }})"
```
</issue_to_address>

Hi @Radiump123! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 11 to -12
test:
name: "Test — Python ${{ matrix.python-version }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Job name no longer differentiates between Python versions in the matrix

The static name makes all matrix jobs look identical in the Actions UI, which makes it harder to see which Python version failed. Please reintroduce the version in the job name (e.g., Run Tests (Python ${{ matrix.python-version }})) so each matrix entry is distinguishable.

Suggested change
test:
name: "Test — Python ${{ matrix.python-version }}"
test:
name: "Run Tests (Python ${{ matrix.python-version }})"

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 1 issue, and left some high level feedback:

  • In get_usb_info, parsing device.attributes.get("size") via int(size_attr) assumes a non-empty numeric string; consider mirroring the previous lsblk path by validating the value (e.g. isdigit() or catching ValueError) and logging a warning before defaulting to 0 to avoid unexpected exceptions on malformed udev attributes.
  • In find_usb, the broad except Exception around the pyudev lookup silently swallows all failures; narrowing this to expected error types or at least logging at debug level would make diagnosing USB detection issues easier without changing behavior for callers.
  • The test suite now repeats similar os.stat and pyudev mocking patterns in multiple files; consider extracting a small helper or fixture (e.g. _mock_os_stat_and_pyudev) to reduce duplication and keep the mocking behavior consistent, and clean up stray/unneeded imports or informal comments in tests (like the Idk why I don't like that remark).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_usb_info`, parsing `device.attributes.get("size")` via `int(size_attr)` assumes a non-empty numeric string; consider mirroring the previous `lsblk` path by validating the value (e.g. `isdigit()` or catching `ValueError`) and logging a warning before defaulting to 0 to avoid unexpected exceptions on malformed udev attributes.
- In `find_usb`, the broad `except Exception` around the pyudev lookup silently swallows all failures; narrowing this to expected error types or at least logging at debug level would make diagnosing USB detection issues easier without changing behavior for callers.
- The test suite now repeats similar `os.stat` and pyudev mocking patterns in multiple files; consider extracting a small helper or fixture (e.g. `_mock_os_stat_and_pyudev`) to reduce duplication and keep the mocking behavior consistent, and clean up stray/unneeded imports or informal comments in tests (like the `Idk why I don't like that` remark).

## Individual Comments

### Comment 1
<location path="src/lufus/drives/get_usb_info.py" line_range="33-37" />
<code_context>
-        if not size_output.isdigit():
-            log.warning("Could not parse device size: %r", size_output)
+        # Size in bytes: udev attributes 'size' is in 512-byte sectors
+        size_attr = device.attributes.get("size")
+        usb_size = int(size_attr) * 512 if size_attr else 0
+
+        label = device.get("ID_FS_LABEL")
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Handle non-numeric or malformed `size` attributes more defensively.

With this change, a non-numeric or malformed `size` attribute will cause `int(size_attr)` to raise `ValueError`, be caught by the generic `Exception` handler, and make the function return `None`, whereas previously we just logged a warning and used size 0. To preserve the prior resilience, consider checking `size_attr.isdigit()` (after decoding if needed) or catching `ValueError` around `int(size_attr)`, logging a warning, and defaulting the size to 0 instead of failing the whole call.

```suggestion
        # Size in bytes: udev attributes 'size' is in 512-byte sectors
        size_attr = device.attributes.get("size")
        usb_size = 0
        if size_attr:
            try:
                usb_size = int(size_attr) * 512
            except (ValueError, TypeError):
                log.warning("Could not parse device size attribute: %r", size_attr)

        label = device.get("ID_FS_LABEL")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lufus/drives/get_usb_info.py Outdated

@sourcery-ai sourcery-ai Bot 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.

Hey - I've left some high level feedback:

  • In get_usb_info and find_usb, the new pyudev-based paths now rely on a broad except Exception where previously TimeoutExpired/CalledProcessError were handled explicitly; consider restoring more targeted exception handling (and logging) around the pyudev lookup so that timeouts or permission issues are not silently swallowed.
  • The pyudev/os.stat mocking logic is duplicated across multiple tests (e.g., in test_get_usb_info, test_get_usb_info_and_detect_windows_fixes, and test_find_usb); you could extract a shared helper to keep the tests shorter and reduce the risk of inconsistencies between them.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_usb_info` and `find_usb`, the new pyudev-based paths now rely on a broad `except Exception` where previously `TimeoutExpired`/`CalledProcessError` were handled explicitly; consider restoring more targeted exception handling (and logging) around the pyudev lookup so that timeouts or permission issues are not silently swallowed.
- The pyudev/os.stat mocking logic is duplicated across multiple tests (e.g., in `test_get_usb_info`, `test_get_usb_info_and_detect_windows_fixes`, and `test_find_usb`); you could extract a shared helper to keep the tests shorter and reduce the risk of inconsistencies between them.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

R-Larocque added 4 commits May 7, 2026 12:39
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is up to date with 'origin/less-deps-feature-win-tweaks'.

 Changes to be committed:
	modified:   .github/workflows/ci.yml
	modified:   .github/workflows/python-compatibility.yml
	modified:   pyproject.toml
	modified:   scripts/build-appimage.sh
	modified:   src/lufus/drives/find_usb.py
	modified:   src/lufus/drives/formatting.py
	modified:   src/lufus/drives/get_usb_info.py
	modified:   src/lufus/gui/dialogs.py
	modified:   src/lufus/gui/gui.py
	modified:   src/lufus/gui/i18n.py
	modified:   src/lufus/gui/languages/Deutsch.csv
	modified:   src/lufus/gui/languages/English.csv
	modified:   "src/lufus/gui/languages/Espa\303\261ol.csv"
	modified:   "src/lufus/gui/languages/Fran\303\247ais.csv"
	modified:   "src/lufus/gui/languages/Portugue\314\202s Brasileiro.csv"
	modified:   src/lufus/gui/languages/Svenska.csv
	modified:   "src/lufus/gui/languages/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.csv"
	modified:   "src/lufus/gui/languages/\321\203\320\272\321\200\320\260\321\227\320\275\321\201\321\214\320\272\320\260.csv"
	modified:   "src/lufus/gui/languages/\330\271\330\261\330\250\331\212.csv"
	modified:   "src/lufus/gui/languages/\340\246\254\340\246\276\340\246\202\340\246\262\340\246\276.csv"
	modified:   src/lufus/gui/workers.py
	modified:   src/lufus/state.py
	modified:   src/lufus/utils.py
	modified:   src/lufus/writing/flash_usb.py
	modified:   src/lufus/writing/install_ventoy.py
	modified:   src/lufus/writing/windows/flash.py
	modified:   src/lufus/writing/windows/tweaks.py
	modified:   tests/test_find_usb.py
	modified:   tests/test_flash_usb_and_find_usb_fixes.py
	modified:   tests/test_get_usb_info.py
	modified:   tests/test_get_usb_info_and_detect_windows_fixes.py

 Untracked files:
	Archive.tar.gz
	TODO
	root-log.txt
	src/lufus/gui/languages/U0420U0443U0441U0441U043aU0438U0439.csv
	src/lufus/gui/languages/U0443U043aU0440U0430U0457U043dU0441U044cU043aU0430.csv
	src/lufus/gui/languages/U0639U0631U0628U064a.csv
	src/lufus/gui/languages/U09acU09beU0982U09b2U09be.csv
	src/lufus/gui/languages/EspaU00f1ol.csv
	src/lufus/gui/languages/FranU00e7ais.csv
	src/lufus/gui/languages/PortugueU0302s Brasileiro.csv
	user-log.txt
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is ahead of 'origin/less-deps-feature-win-tweaks' by 1 commit.
   (use "git push" to publish your local commits)

 Changes to be committed:
	modified:   src/lufus/gui/gui.py
	new file:   src/lufus/gui/languages/U0420U0443U0441U0441U043aU0438U0439.csv
	new file:   src/lufus/gui/languages/U0443U043aU0440U0430U0457U043dU0441U044cU043aU0430.csv
	new file:   src/lufus/gui/languages/U0639U0631U0628U064a.csv
	new file:   src/lufus/gui/languages/U09acU09beU0982U09b2U09be.csv
	new file:   src/lufus/gui/languages/EspaU00f1ol.csv
	new file:   src/lufus/gui/languages/FranU00e7ais.csv
	new file:   src/lufus/gui/languages/PortugueU0302s Brasileiro.csv
	modified:   src/lufus/writing/flash_usb.py
 Please enter the commit message for your changes. Lines starting
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is ahead of 'origin/less-deps-feature-win-tweaks' by 2 commits.
   (use "git push" to publish your local commits)

 Changes to be committed:
	deleted:    src/lufus/gui/languages/U0420U0443U0441U0441U043aU0438U0439.csv
	deleted:    src/lufus/gui/languages/U0443U043aU0440U0430U0457U043dU0441U044cU043aU0430.csv
	deleted:    src/lufus/gui/languages/U0639U0631U0628U064a.csv
	deleted:    src/lufus/gui/languages/U09acU09beU0982U09b2U09be.csv
	deleted:    src/lufus/gui/languages/EspaU00f1ol.csv
	deleted:    src/lufus/gui/languages/FranU00e7ais.csv
	deleted:    src/lufus/gui/languages/PortugueU0302s Brasileiro.csv
 with '' will be ignored, and an empty message aborts the commit.

 On branch less-deps-feature-win-tweaks
 Your branch is ahead of 'origin/less-deps-feature-win-tweaks' by 3 commits.
   (use "git push" to publish your local commits)

 Changes to be committed:
	modified:   src/lufus/gui/gui.py

@sourcery-ai sourcery-ai Bot 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.

New security issues found

Comment thread src/lufus/drives/formatting.py
Comment thread src/lufus/drives/formatting.py
# injection risk here.
try:
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL)
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 security issues, 5 other issues, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • In drives/formatting.remount, when a drive argument is passed the mount variable is never set anymore (the lookup was removed), so the subsequent if not drive or not mount will always hit the mount branch and prevent remounting; consider restoring the mount-point lookup for the supplied drive.
  • The change in utils.elevate_privileges to drop validation of state.theme before exporting LUFUS_THEME reopens the possibility of injecting unexpected values into the elevated environment; it would be safer to keep the filename/path validation or similar sanitisation.
  • The new load_translations implementation in gui/i18n.py always uses utf-8 instead of utf-8-sig and assumes key/value columns are present, which means CSVs with a BOM or malformed/missing columns will now raise instead of being skipped; consider reintroducing BOM-tolerant reading and defensive checks around row keys.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `drives/formatting.remount`, when a `drive` argument is passed the `mount` variable is never set anymore (the lookup was removed), so the subsequent `if not drive or not mount` will always hit the `mount` branch and prevent remounting; consider restoring the mount-point lookup for the supplied drive.
- The change in `utils.elevate_privileges` to drop validation of `state.theme` before exporting `LUFUS_THEME` reopens the possibility of injecting unexpected values into the elevated environment; it would be safer to keep the filename/path validation or similar sanitisation.
- The new `load_translations` implementation in `gui/i18n.py` always uses `utf-8` instead of `utf-8-sig` and assumes `key`/`value` columns are present, which means CSVs with a BOM or malformed/missing columns will now raise instead of being skipped; consider reintroducing BOM-tolerant reading and defensive checks around row keys.

## Individual Comments

### Comment 1
<location path="src/lufus/gui/i18n.py" line_range="16-15" />
<code_context>
     if lang_file.exists():
-        # utf-8-sig strips BOM that some editors insert before the first byte.
-        # Graceful row handling: skip malformed rows (no key column, empty key).
-        with open(lang_file, encoding="utf-8-sig", newline="") as f:
+        # read translations from csv :3
+        with open(lang_file, encoding="utf-8", newline="") as f:
             for row in csv.DictReader(f):
-                key = row.get("key", "").strip()
</code_context>
<issue_to_address>
**issue:** CSV parsing no longer tolerates BOMs or malformed rows and can now raise KeyError

The previous version used `utf-8-sig` and `row.get()` to tolerate BOMs and malformed rows; this change removes both safeguards. With plain `utf-8` and direct `row["key"]`/`row["value"]` indexing:

1. CSVs with a UTF-8 BOM will expose `"\ufeffkey"` instead of `"key"`, causing `KeyError` and failing translation loading.
2. Rows missing `key`/`value` now raise `KeyError` instead of being skipped.

If this robustness is still required, please either restore `utf-8-sig` and `get()`-based access, or add explicit checks around `row.get("key")`/`row.get("value")` before use.
</issue_to_address>

### Comment 2
<location path="src/lufus/utils.py" line_range="15" />
<code_context>
     # if the app was able to find them before elevation.
     env = os.environ.copy()
     if state.theme:
-        # Validate theme is a safe filename/path: no path separators, no shell metacharacters,
-        # and must resolve inside the config directory to prevent path traversal.
</code_context>
<issue_to_address>
**🚨 issue (security):** Removing validation of LUFUS_THEME reintroduces potential path / injection issues in the elevated environment

`state.theme` used to be constrained to a safe filename (no separators/`..`/restricted charset) before being exported as `LUFUS_THEME`, protecting the elevated process from hostile or malformed values. Now it’s exported as-is, so any user- or config-controlled value could be interpreted as a path, enabling traversal or pointing the elevated process at arbitrary locations. Even if you’re simplifying, please reintroduce a minimal check (e.g., `os.path.basename(theme) == theme` and reject `..`) to keep the elevated environment safe.
</issue_to_address>

### Comment 3
<location path="src/lufus/writing/flash_usb.py" line_range="123-132" />
<code_context>
+        buf = b""
</code_context>
<issue_to_address>
**suggestion:** dd progress parsing may spin on EOF and can be noisy for non-progress stderr lines

Two small behaviors to consider tightening:

1. If `dd` emits a final stderr fragment without a trailing newline/carriage return, it will remain in `buf` and never be logged. It would be safer to flush any remaining `buf` after the read loop.
2. All non-progress stderr is logged at `warning` level. Some `dd` variants print benign informational lines, which could now spam logs and alarm users. Consider logging those at `info` or filtering obviously harmless messages.

These aren’t critical, but they would make progress reporting and logging more predictable.

Suggested implementation:

```python
        buf = b""
        while True:
            chunk = process.stderr.read1(65536)
            if not chunk:
                # Flush any remaining partial stderr fragment so it is not lost
                if buf:
                    # Decode conservatively and avoid logging a purely empty line
                    decoded = buf.decode(errors="replace").rstrip("\r\n")
                    if decoded:
                        # Log at info to avoid alarming users with benign dd output
                        logger.info("dd stderr (final fragment): %s", decoded)
                break
            buf += chunk

```

1. Ensure there is a `logger` available in this module:
   - If the file already defines or imports a logger (for example, `logger = logging.getLogger(__name__)`), reuse it instead of introducing a new one.
   - If not, add at the top of the file:
     ```python
     import logging
     logger = logging.getLogger(__name__)
     ```
2. In the part of this file where non-progress `dd` stderr lines are currently logged (likely something like `logger.warning("dd stderr: %s", line)`), adjust the behavior:
   - Either:
     - Change the level to `info`: `logger.info("dd stderr: %s", line)`, **or**
     - Add simple filtering, e.g.:
       ```python
       harmless_phrases = ("records in", "records out", "bytes copied")
       if any(p in line for p in harmless_phrases):
           logger.info("dd stderr: %s", line)
       else:
           logger.warning("dd stderr: %s", line)
       ```
   - This will keep benign informational `dd` output from spamming warnings while preserving higher severity for unexpected stderr content.
</issue_to_address>

### Comment 4
<location path="scripts/build-appimage.sh" line_range="7-12" />
<code_context>
-#     echo "requirements-system.txt not found!"
-#     exit 1
-# fi
+echo "------------ Installing system libraries ------------"
+apt-get update && apt-get upgrade -y
+INSTALLER="apt-get install -y"
+if [[ -f requirements-system.txt ]]; then
+    $INSTALLER $(cat requirements-system.txt) >> appimage-setup.log
+    echo "System libraries installed."
+else
+    echo "requirements-system.txt not found!"
</code_context>
<issue_to_address>
**suggestion (performance):** Running `apt-get update && apt-get upgrade -y` in the AppImage build script can have unintended side effects in CI/build environments

Running `apt-get update && apt-get upgrade -y` in the build script makes the build environment mutable and less predictable:

- Increases build time and network usage.
- May update unrelated system packages on CI runners, leading to nondeterministic builds.
- Can conflict with CI provider recommendations, where full upgrades are discouraged or blocked.

Prefer installing only the required packages via `apt-get install -y` (assuming the base image is maintained) or managing system packages in the CI/YAML configuration instead of inside the build script.

Suggested implementation:

```
echo "------------ Installing system libraries ------------"
INSTALLER="apt-get install -y"
if [[ -f requirements-system.txt ]]; then
    $INSTALLER $(cat requirements-system.txt) >> appimage-setup.log
    echo "System libraries installed."
else
    echo "requirements-system.txt not found!"
    exit 1
fi

```

Depending on your CI setup, you may want to:
1. Move `apt-get update` (and any controlled upgrade policy) into the CI/YAML configuration so the base image is explicitly managed there.
2. Ensure the base image used in CI is regularly updated so `apt-get install` can resolve the packages listed in `requirements-system.txt` without needing an inline `apt-get update` here.
</issue_to_address>

### Comment 5
<location path="tests/test_flash_usb_and_find_usb_fixes.py" line_range="384-393" />
<code_context>
+    def test_progress_cb_receives_milestones_and_scaled_dd_progress(self, monkeypatch):
</code_context>
<issue_to_address>
**issue (testing):** Progress callback tests still assume a two-argument progress_cb and conflate progress and status, but flash_usb now calls progress_cb(progress) and uses status_cb for messages.

In the current tests, `progress_cb(progress, status)` will raise a `TypeError` because `flash_usb.flash_usb` now calls `progress_cb(progress)` and sends messages via a separate `status_cb`. As a result, `status_messages` is never populated.

Please update the tests to:
- Use a single-argument `progress_cb(progress)` and capture messages with a separate `status_cb(message)`.
- Pass both callbacks to `flash_usb_module.flash_usb(..., progress_cb=progress_cb, status_cb=status_cb)`.
- Update assertions so they no longer expect status text from `progress_cb`.

This will align the tests with the new callback API and avoid spurious failures from the wrong signature.
</issue_to_address>

### Comment 6
<location path="src/lufus/drives/formatting.py" line_range="358-361" />
<code_context>
            subprocess.run(
                [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"],
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 7
<location path="src/lufus/drives/formatting.py" line_range="365-368" />
<code_context>
            subprocess.run(
                [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"],
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 8
<location path="src/lufus/writing/flash_usb.py" line_range="105" />
<code_context>
            process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lufus/gui/i18n.py
Comment thread src/lufus/utils.py
Comment thread src/lufus/writing/flash_usb.py
Comment thread scripts/build-appimage.sh
Comment thread tests/test_flash_usb_and_find_usb_fixes.py
Comment thread src/lufus/drives/formatting.py
Comment thread src/lufus/drives/formatting.py
# injection risk here.
try:
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL)
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

… lignes

 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         .github/workflows/ci.yml
@R-Larocque

Copy link
Copy Markdown
Owner Author

Gotta fix these

… lignes

 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         scripts/build-appimage.sh
	modifié :         src/lufus/drives/formatting.py
	modifié :         src/lufus/gui/i18n.py
	modifié :         src/lufus/utils.py
	modifié :         src/lufus/writing/flash_usb.py
	modifié :         tests/test_flash_usb_and_find_usb_fixes.py
R-Larocque added 6 commits May 7, 2026 16:50
 Veuillez saisir le message de validation pour vos modifications. Les lignes
 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est en avance sur 'origin/less-deps-feature-win-tweaks' de 1 commit.
   (utilisez "git push" pour publier vos commits locaux)

 Modifications qui seront validées :
	modifié :         src/lufus/gui/gui.py
	modifié :         src/lufus/writing/windows/detect.py
… lignes

 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         src/lufus/drives/formatting.py
	modifié :         tests/test_flash_usb_and_find_usb_fixes.py
… lignes

 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         tests/test_flash_usb_and_find_usb_fixes.py
… lignes

 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         tests/test_linux_iso_detection.py
@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 security issues, 5 other issues, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • In writing/windows/flash.py, replacing shutil.which with subprocess.run(['which', ...]) introduces a runtime dependency on the external which binary and repeated process spawning; consider reverting to shutil.which to keep tool detection lightweight and more portable.
  • In tests/test_find_usb.py, _ORIG_STAT is assigned from _real_os.stat, but _real_os is never defined or imported, so the helper will fail before tests run; capture os.stat directly (e.g. _ORIG_STAT = os.stat) or import the intended alias.
  • The Windows tweaks helpers in writing/windows/tweaks.py now call bare mkdir/rm via subprocess.run without sudo or the existing run_cmd abstraction, which may fail depending on privileges and diverges from the new centralised privileged-command handling; consider routing these through the same sudo/run_cmd path for consistency.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `writing/windows/flash.py`, replacing `shutil.which` with `subprocess.run(['which', ...])` introduces a runtime dependency on the external `which` binary and repeated process spawning; consider reverting to `shutil.which` to keep tool detection lightweight and more portable.
- In `tests/test_find_usb.py`, `_ORIG_STAT` is assigned from `_real_os.stat`, but `_real_os` is never defined or imported, so the helper will fail before tests run; capture `os.stat` directly (e.g. `_ORIG_STAT = os.stat`) or import the intended alias.
- The Windows tweaks helpers in `writing/windows/tweaks.py` now call bare `mkdir`/`rm` via `subprocess.run` without sudo or the existing `run_cmd` abstraction, which may fail depending on privileges and diverges from the new centralised privileged-command handling; consider routing these through the same sudo/run_cmd path for consistency.

## Individual Comments

### Comment 1
<location path="src/lufus/drives/formatting.py" line_range="104-111" />
<code_context>

 # mountain
 def remount(drive: str = None) -> bool:
-    mount = None
     if not drive:
         mount, drive, _ = _get_mount_and_drive()
     else:
-        # drive was supplied by caller; resolve mount point from current state
-        _, _, mount_dict = _get_mount_and_drive()
-        # find the mount point whose device node matches the given drive
-        mount = next((mp for mp, _label in mount_dict.items()), None)
+        # Look up mount point for the specified drive
+        from lufus.drives.find_usb import find_usb
+
+        mount_dict = find_usb()
+        mount = mount_dict.get(drive)
     if not drive:
</code_context>
<issue_to_address>
**issue (bug_risk):** The remount logic misuses `find_usb()` and will not find the mount point for a given device node.

`_get_mount_and_drive()` used to give you both the selected `(mount, drive)` and a mapping keyed appropriately. Now `find_usb()` returns `{mount_path: label}`, but you’re calling `mount = mount_dict.get(drive)` where `drive` is `'/dev/sdX...'`, so this lookup will almost always be `None`. As a result, `remount(drive='/dev/...')` will incorrectly hit the "No mount point found" path even when the device is mounted. You should either invert the mapping from `find_usb()` so you can look up by device node, or restore a helper that returns `(mount, drive, mount_dict)` with keys that match `drive` values.
</issue_to_address>

### Comment 2
<location path="src/lufus/gui/gui.py" line_range="1378" />
<code_context>
+                    self.log_message(f"Failed to restore terminal: {e}")

             # reset ui state :D
-            self.progress_bar.setRange(0, 100)  # exit indeterminate mode
             self.progress_bar.setValue(0)
             self.progress_bar.setFormat("")
</code_context>
<issue_to_address>
**issue (bug_risk):** Removing the progress-bar range reset can leave the bar stuck in indeterminate mode after cancel.

Since `cancel_process` no longer calls `self.progress_bar.setRange(0, 100)`, a bar that was set to an indeterminate range `(0, 0)` will stay visually “busy” even after cancel, because updating only `value` does not exit indeterminate mode. Please reintroduce the range reset or add a conditional reset when leaving indeterminate mode.
</issue_to_address>

### Comment 3
<location path="src/lufus/writing/windows/flash.py" line_range="274" />
<code_context>
     if efi_src:
         efi_items = os.listdir(efi_src)
         _status(f"Found EFI/ with {len(efi_items)} items: {efi_items}")
-        for item in efi_items:
-            src_path = os.path.join(efi_src, item)
-            dst_path = os.path.join(mount_efi, "EFI", item)
-            if os.path.isdir(src_path):
-                shutil.copytree(src_path, dst_path, dirs_exist_ok=True)
-            else:
-                os.makedirs(os.path.dirname(dst_path), exist_ok=True)
-                shutil.copy2(src_path, dst_path)
+        run_cmd(["sudo", "cp", "-r"] + [os.path.join(efi_src, i) for i in efi_items] + [mount_efi])
         _status("Copied EFI/ tree to EFI partition")
     else:
</code_context>
<issue_to_address>
**issue (bug_risk):** Copying EFI and boot trees directly into `mount_efi` changes the on-disk layout compared to the previous `mount_efi/EFI` and `mount_efi/boot` locations.

The previous logic explicitly copied into `mount_efi/EFI` and `mount_efi/boot`, matching firmware’s expected layout. The new `cp -r <EFI/*> mount_efi` and `cp -r <boot/*> mount_efi` calls instead flatten the directory structure so entries like `EFI/Microsoft/...` become `<mount_efi>/Microsoft/...`. This can break boot and violate `_fix_efi_bootloader`’s assumption that `mount_efi/EFI` exists. Please update the copy commands to preserve the original `EFI/` and `boot/` subdirectories (e.g., by using `mount_efi/EFI` and `mount_efi/boot` as destinations).
</issue_to_address>

### Comment 4
<location path="src/lufus/writing/windows/tweaks.py" line_range="59-62" />
<code_context>
     cmd_string = "\n".join(commands) + "\n"
     log.info("win_hardware_bypass: injecting registry keys into boot.wim at %s...", mount)
     try:
-        os.makedirs("/media/tempwinmnt", exist_ok=True)
+        subprocess.run(["mkdir", "/media/tempwinmnt"], check=True)
         subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True)
</code_context>
<issue_to_address>
**issue (bug_risk):** Switching to `mkdir`/`rm -rf` subprocess calls changes robustness and error handling for temporary mount directories.

Replacing `os.makedirs(..., exist_ok=True)` / `shutil.rmtree(..., ignore_errors=True)` with `subprocess.run([
</issue_to_address>

### Comment 5
<location path="src/lufus/writing/windows/flash.py" line_range="157-158" />
<code_context>
 def _find_ntfs_tool(status_cb=None) -> str | None:
     """Find mkfs.ntfs/mkntfs, installing ntfs-3g if needed. Returns command name or None."""
     for candidate in ["mkfs.ntfs", "mkntfs"]:
-        if shutil.which(candidate):
+        if subprocess.run(["which", candidate], capture_output=True).returncode == 0:
             return candidate

</code_context>
<issue_to_address>
**suggestion (bug_risk):** Replacing `shutil.which` with an external `which` command introduces a new dependency and potential portability issues.

Both `_find_ntfs_tool` and `_ensure_wimlib` now rely on an external `which` via `subprocess.run([...])` instead of `shutil.which`, assuming:
- A `which` binary exists on `PATH` (not guaranteed everywhere).
- Its behavior matches expectations, whereas `shutil.which` is already cross‑platform and handles PATH resolution.

Unless there is a strong reason to avoid `shutil.which`, please revert to it, or at least use it as a fallback when the external `which` call fails to avoid portability/regression issues.
</issue_to_address>

### Comment 6
<location path="src/lufus/drives/formatting.py" line_range="364-367" />
<code_context>
            subprocess.run(
                [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"],
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 7
<location path="src/lufus/drives/formatting.py" line_range="371-374" />
<code_context>
            subprocess.run(
                [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"],
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 8
<location path="src/lufus/writing/flash_usb.py" line_range="105" />
<code_context>
            process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/lufus/drives/formatting.py Outdated
Comment thread src/lufus/gui/gui.py
Comment thread src/lufus/writing/windows/flash.py
Comment on lines -59 to 62
os.makedirs("/media/tempwinmnt", exist_ok=True)
subprocess.run(["mkdir", "/media/tempwinmnt"], check=True)
subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True)
subprocess.run(
["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SYSTEM"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Switching to mkdir/rm -rf subprocess calls changes robustness and error handling for temporary mount directories.

Replacing os.makedirs(..., exist_ok=True) / shutil.rmtree(..., ignore_errors=True) with `subprocess.run([

Comment thread src/lufus/writing/windows/flash.py
Comment thread src/lufus/drives/formatting.py
Comment thread src/lufus/drives/formatting.py
# injection risk here.
try:
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL)
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

R-Larocque added 2 commits May 7, 2026 18:15
… lignes

 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         src/lufus/drives/formatting.py
	modifié :         src/lufus/gui/gui.py
	modifié :         src/lufus/utils.py
	modifié :         src/lufus/writing/windows/flash.py
	modifié :         src/lufus/writing/windows/tweaks.py
 Veuillez saisir le message de validation pour vos modifications. Les lignes
 commençant par '' seront ignorées, et un message vide abandonne la validation.

 Sur la branche less-deps-feature-win-tweaks
 Votre branche est à jour avec 'origin/less-deps-feature-win-tweaks'.

 Modifications qui seront validées :
	modifié :         src/lufus/writing/windows/flash.py
@R-Larocque

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 3 security issues, 1 other issue, and left some high level feedback:

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
  • Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)

General comments:

  • In cancel_process, _procs_with_device_mounted will unconditionally send SIGKILL to any PID with the device mounted; it might be safer to send SIGTERM first, optionally skip your own process, and make it clear in the log that other user processes may be force-killed.
  • The Windows flashing helpers now use run_cmd with hard-coded sudo for operations like mkdir, cp, and sync; if Lufus is already running under pkexec or another elevation mechanism this can fail—consider centralising privilege handling so these calls run with the correct effective user instead of assuming sudo.
  • In the Windows path of FlashWorker.run, any non-ISO flash_mode is now treated as success = False without a clear user-facing explanation; it may be clearer to either disable unsupported Windows/DD combinations in the UI or emit a specific status/error message about unsupported mode selection.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `cancel_process`, `_procs_with_device_mounted` will unconditionally send `SIGKILL` to any PID with the device mounted; it might be safer to send `SIGTERM` first, optionally skip your own process, and make it clear in the log that other user processes may be force-killed.
- The Windows flashing helpers now use `run_cmd` with hard-coded `sudo` for operations like `mkdir`, `cp`, and `sync`; if Lufus is already running under `pkexec` or another elevation mechanism this can fail—consider centralising privilege handling so these calls run with the correct effective user instead of assuming `sudo`.
- In the Windows path of `FlashWorker.run`, any non-ISO `flash_mode` is now treated as `success = False` without a clear user-facing explanation; it may be clearer to either disable unsupported Windows/DD combinations in the UI or emit a specific status/error message about unsupported mode selection.

## Individual Comments

### Comment 1
<location path="scripts/build-appimage.sh" line_range="7-15" />
<code_context>
-#     echo "requirements-system.txt not found!"
-#     exit 1
-# fi
+echo "------------ Installing system libraries ------------"
+INSTALLER="apt-get install -y"
+if [[ -f requirements-system.txt ]]; then
+    $INSTALLER $(cat requirements-system.txt) >> appimage-setup.log
+    echo "System libraries installed."
+else
+    echo "requirements-system.txt not found!"
+    exit 1
+fi
</code_context>
<issue_to_address>
**suggestion (bug_risk):** System dependency install runs `apt-get install` without a preceding `apt-get update`, which can fail on fresh runners.

On fresh CI runners or minimal images, `apt-get install` can fail with “package not found” or stale metadata if `apt-get update` hasn’t been run. Please ensure we run `apt-get update` (and optionally set `DEBIAN_FRONTEND=noninteractive`) before invoking `$INSTALLER` so the AppImage build remains reliable.

```suggestion
echo "------------ Installing system libraries ------------"
export DEBIAN_FRONTEND=noninteractive
INSTALLER="apt-get install -y"

if [[ -f requirements-system.txt ]]; then
    echo "Updating apt package index..."
    apt-get update >> appimage-setup.log

    echo "Installing system libraries from requirements-system.txt..."
    $INSTALLER $(cat requirements-system.txt) >> appimage-setup.log
    echo "System libraries installed."
else
    echo "requirements-system.txt not found!"
    exit 1
fi
```
</issue_to_address>

### Comment 2
<location path="src/lufus/drives/formatting.py" line_range="368-371" />
<code_context>
            subprocess.run(
                [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"],
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 3
<location path="src/lufus/drives/formatting.py" line_range="375-378" />
<code_context>
            subprocess.run(
                [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"],
                check=True,
            )
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="src/lufus/writing/flash_usb.py" line_range="105" />
<code_context>
            process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/build-appimage.sh
Comment thread src/lufus/drives/formatting.py
Comment thread src/lufus/drives/formatting.py
# injection risk here.
try:
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL)
process = subprocess.Popen(dd_args, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, env=env)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'Popen' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@R-Larocque R-Larocque force-pushed the less-deps-feature-win-tweaks branch from 426985f to 9f94364 Compare May 7, 2026 23:05
@R-Larocque

Copy link
Copy Markdown
Owner Author

If it ain't broke, don't fix it

@R-Larocque R-Larocque merged commit 97b6a4d into feature-win-tweaks May 7, 2026
32 of 33 checks passed
@R-Larocque R-Larocque deleted the less-deps-feature-win-tweaks branch May 8, 2026 13:03
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