diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7188a94..12a8d30e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install psutil pytest + pip install psutil pytest pyudev - name: Install package (no-deps to avoid GUI reqs) run: pip install -e . --no-deps diff --git a/.github/workflows/python-compatibility.yml b/.github/workflows/python-compatibility.yml index 193b9dbc..d55ff430 100644 --- a/.github/workflows/python-compatibility.yml +++ b/.github/workflows/python-compatibility.yml @@ -35,7 +35,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install psutil pytest packaging platformdirs + pip install psutil pytest packaging platformdirs pyudev - name: Install package (no-deps to avoid GUI reqs) run: pip install -e . --no-deps diff --git a/README.md b/README.md index ebdca691..24829373 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## Beta Release Disclaimer -**Lufus** is currently in **Beta**. +**Lufus** is currently in **Beta**! Lufus is a physical drive imaging and formatting utility written in Python, inspired by **Rufus** on Windows, with the goal of delivering a greater experience for Linux users. While core functionality has been implemented, the project is still under active development. Users should expect bugs, incomplete features, and ongoing structural changes. diff --git a/deps.txt b/deps.txt new file mode 100644 index 00000000..59ac1da2 --- /dev/null +++ b/deps.txt @@ -0,0 +1,83 @@ +# Lufus External Dependencies (Non-Python) + +## System Privileges +- pkexec (polkit) +- sudo +- runuser (util-linux) + +## Flashing & Imaging +- dd (coreutils) +- sync (coreutils) +- cp (coreutils) +- rm (coreutils) +- mkdir (coreutils) + +## Partitioning & Disk Info +- lsblk (util-linux) +- parted +- sfdisk (util-linux) +- partprobe (parted) +- udevadm (systemd/udev) +- wipefs (util-linux) +- blockdev (util-linux) +- badblocks (e2fsprogs) + +## Filesystem Tools +- mkfs.vfat, fatlabel (dosfstools) +- mkfs.exfat (exfatprogs or exfat-utils) +- mkfs.ntfs / mkntfs, ntfslabel (ntfs-3g) +- mkfs.ext4, e2label (e2fsprogs) +- mkudffs, udflabel (udftools) + +## Mounting & ISO Handling +- mount (util-linux) +- umount (util-linux) +- 7z (p7zip-full) - Used for Windows ISO detection +- isoinfo (genisoimage) - Fallback for Windows ISO detection + +## Process & System Management +- lsof +- fuser (psmisc) +- pgrep (procps) +- which (debianutils or similar) +- stty (coreutils) +- xdg-user-dir (xdg-user-dirs) +- xdg-open (xdg-utils) + +## Windows Image Operations (WIM) +- wimlib-imagex (wimlib / wimtools) +- wimmountrw (wimlib) +- wimunmount (wimlib) + +## Bootloader Installation +- grub-install (grub2 / grub-common) + +## GUI & System Libraries (PyQt6 Requirements) +- libgl1 +- libx11-6 +- libxcb1 +- libxrender1 +- fontconfig +- libfreetype6 +- libxext6 +- libxrandr2 +- libxcursor1 +- libxi6 +- libxfixes3 +- libxcomposite1 +- libxdamage1 + +## Package Managers (Used for auto-installing missing tools) +- apt-get (Debian/Ubuntu) +- dnf (Fedora/RHEL) +- pacman (Arch) +- zypper (openSUSE) + +## Python Environment +- python3 +- python3-pip +- python3-venv + +## Other (from requirements-system.txt) +- wget +- file diff --git a/pyproject.toml b/pyproject.toml index c943990a..daa2cb15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "lufus" -version = "1.0.0" # keep synced with [tool.briefcase] version +version = "1.0.1b1" # keep synced with [tool.briefcase] version description = "A rufus clone written in Python and designed to work with Linux." readme = "README.md" license = "MIT" @@ -29,7 +29,7 @@ lufus = "lufus.__main__:main" [tool.briefcase] project_name = "Lufus" bundle = "com.github.hog185" -version = "1.0.0" # keep synced with [project] version +version = "1.0.1b1" # keep synced with [project] version url = "https://github.com/Hog185/Lufus" license.file = "LICENSE" author = "Hog185" @@ -50,6 +50,7 @@ requires = [ "psutil>=7.2", "pyqt6>=6.8.0", "pyudev>=0.24.4", + "requests", "packaging", "platformdirs>=4.2" ] diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 8286b087..fc5a8c2d 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -4,17 +4,15 @@ set -euo pipefail ls -la # Install system dependencies -## COMMENTED BECAUSE ALREADY HANDELED IN YAML -# 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!" -# 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 if command -v python3 &>/dev/null; then PYTHON=python3 diff --git a/src/lufus/drives/find_usb.py b/src/lufus/drives/find_usb.py index f7486561..e45e61fe 100644 --- a/src/lufus/drives/find_usb.py +++ b/src/lufus/drives/find_usb.py @@ -1,6 +1,6 @@ +import pyudev import psutil import os -import subprocess import getpass from lufus import state from lufus.lufus_logging import get_logger @@ -42,30 +42,30 @@ def find_usb() -> dict[str, str]: all_directories = _media_directories() dir_set = set(all_directories) + context = pyudev.Context() + # Check each partition to see if it matches our potential mount points for part in psutil.disk_partitions(all=True): if part.mountpoint not in dir_set: continue mount_path = part.mountpoint device_node = part.device - if not device_node: - continue - - label = None - try: - label = subprocess.check_output( - ["lsblk", "-d", "-n", "-o", "LABEL", device_node], - text=True, - timeout=5, - ).strip() - except (subprocess.CalledProcessError, subprocess.TimeoutExpired): - pass - - if not label: - label = os.path.basename(mount_path) - - usbdict[mount_path] = label - log.info("Found USB: %s -> %s", mount_path, label) + if device_node: + label = os.path.basename(mount_path) # fallback + try: + st = os.stat(device_node) + udev_device = pyudev.Devices.from_device_number(context, "block", st.st_rdev) + udev_label = udev_device.get("ID_FS_LABEL") + if udev_label: + label = udev_label + except Exception: + log.debug( + "Failed to get label for %s via udev; using mountpoint basename", + device_node, + exc_info=True, + ) + usbdict[mount_path] = label + log.info("Found USB: %s -> %s", mount_path, label) return usbdict diff --git a/src/lufus/drives/formatting.py b/src/lufus/drives/formatting.py index 14b80092..645fc3da 100644 --- a/src/lufus/drives/formatting.py +++ b/src/lufus/drives/formatting.py @@ -26,6 +26,26 @@ def _find_tool(name: str) -> str: return name +def _get_logical_block_size(drive: str) -> int: + """Return the logical sector size of *drive* by reading sysfs. + + Uses ``/sys/class/block//queue/logical_block_size`` so no external + tool is required. Falls back to 512 bytes if the value cannot be read. + """ + dev_name = os.path.basename(drive) + sysfs_path = f"/sys/class/block/{dev_name}/queue/logical_block_size" + try: + with open(sysfs_path) as fh: + return int(fh.read().strip()) + except Exception as exc: + log.warning( + "Could not read sector size from %s: %s. Using 512-byte default.", + sysfs_path, + exc, + ) + return 512 + + ####### @@ -82,19 +102,23 @@ def unmount(drive: str = None) -> bool: # 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 + import psutil + + mount = None + for part in psutil.disk_partitions(all=True): + if part.device == drive: + mount = part.mountpoint + break + if not drive: log.error("No drive node found. Cannot unmount.") return False if not mount: - log.error("No drive node or mount point found. Cannot remount.") + log.error("No mount point found for drive %s. Cannot remount.", drive) return False log.info("Remounting %s -> %s...", drive, mount) try: @@ -209,37 +233,8 @@ def check_device_bad_blocks() -> bool: passes = 2 if state.check_bad else 1 # Probe the device's logical sector size so badblocks uses the real - # device geometry. Fall back to 4096 bytes if detection fails. - logical_block_size = 4096 - try: - probe = subprocess.run( - [_find_tool("blockdev"), "--getss", drive], - capture_output=True, - text=True, - check=False, - ) - if probe.returncode == 0: - probed = probe.stdout.strip() - if probed.isdigit(): - logical_block_size = int(probed) - else: - log.warning( - "Unexpected blockdev output for %r: %r. Using default block size.", - drive, - probed, - ) - else: - log.warning( - "blockdev failed for %s (exit %d). Using default block size.", - drive, - probe.returncode, - ) - except Exception as exc: - log.warning( - "Could not probe sector size for %s: %s. Using default block size.", - drive, - exc, - ) + # device geometry. Fall back to 512 bytes if detection fails. + logical_block_size = _get_logical_block_size(drive) # -s = show progress, -v = verbose output # -n = non-destructive read-write test (safe default) @@ -320,30 +315,10 @@ def _status(msg: str) -> None: "NTFS", "ntfs-3g", ), - 1: ( - "mkfs.vfat", - lambda: ["-I", "-s", str(sectors_per_cluster), "-F", "32", raw_device], - "FAT32", - "dosfstools", - ), - 2: ( - "mkfs.exfat", - lambda: ["-b", str(block_size), raw_device], - "exFAT", - "exfatprogs or exfat-utils", - ), - 3: ( - "mkfs.ext4", - lambda: ["-b", str(block_size), raw_device], - "ext4", - "e2fsprogs", - ), - 4: ( - "mkudffs", - lambda: ["--blocksize=" + str(sector_size), raw_device], - "UDF", - "udftools", - ), + 1: ("mkfs.vfat", lambda: ["-I", "-s", str(sectors_per_cluster), "-F", "32", raw_device], "FAT32", "dosfstools"), + 2: ("mkfs.exfat", lambda: ["-b", str(block_size), raw_device], "exFAT", "exfatprogs or exfat-utils"), + 3: ("mkfs.ext4", lambda: ["-b", str(block_size), raw_device], "ext4", "e2fsprogs"), + 4: ("mkudffs", lambda: ["--blocksize=" + str(sector_size), raw_device], "UDF", "udftools"), } if fs_type not in fs_configs: @@ -391,30 +366,14 @@ def _apply_partition_scheme(drive: str) -> None: # GPT — used for UEFI targets subprocess.run([_find_tool("parted"), "-s", raw_device, "mklabel", "gpt"], check=True) subprocess.run( - [ - _find_tool("parted"), - "-s", - raw_device, - "mkpart", - "primary", - "1MiB", - "100%", - ], + [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"], check=True, ) else: # MBR — used for BIOS/legacy targets subprocess.run([_find_tool("parted"), "-s", raw_device, "mklabel", "msdos"], check=True) subprocess.run( - [ - _find_tool("parted"), - "-s", - raw_device, - "mkpart", - "primary", - "1MiB", - "100%", - ], + [_find_tool("parted"), "-s", raw_device, "mkpart", "primary", "1MiB", "100%"], check=True, ) log.info("Partition scheme %s applied to %s.", scheme_name, raw_device) @@ -428,11 +387,6 @@ def _apply_partition_scheme(drive: str) -> None: def drive_repair() -> None: - # todo: - # add smartctl check if possible - # use fsck to prevent deletion of files for repair - # use testdisk for partition recovery if possible - # do dd if=/dev/zero of=/dev/sdX bs=1M count=10 conv=notrunc before sfdisk use _, drive, _ = _get_mount_and_drive() if not drive: log.error("No drive node found. Cannot repair.") diff --git a/src/lufus/drives/get_usb_info.py b/src/lufus/drives/get_usb_info.py index f9c7d033..f6938bbb 100644 --- a/src/lufus/drives/get_usb_info.py +++ b/src/lufus/drives/get_usb_info.py @@ -1,6 +1,6 @@ +import pyudev import psutil import os -import subprocess from typing import TypedDict from lufus.lufus_logging import get_logger @@ -25,20 +25,25 @@ def get_usb_info(usb_path: str) -> USBDeviceInfo | None: log.warning("Could not find device node for USB path: %s", usb_path) return None - size_output = subprocess.check_output( - ["lsblk", "-d", "-n", "-b", "-o", "SIZE", device_node], - text=True, - timeout=5, - ).strip() - - usb_size = int(size_output) if size_output.isdigit() else 0 - if not size_output.isdigit(): - log.warning("Could not parse device size: %r", size_output) + context = pyudev.Context() + st = os.stat(device_node) + device = pyudev.Devices.from_device_number(context, "block", st.st_rdev) + + # Size in bytes: udev attributes 'size' is in 512-byte sectors + size_attr = device.attributes.get("size") + try: + usb_size = int(size_attr) * 512 if size_attr is not None else 0 + except (ValueError, TypeError): + log.warning( + "Unexpected non-numeric udev size attribute %r; defaulting USB size to 0", + size_attr, + ) + usb_size = 0 if usb_size > 32 * 1024**3: log.warning("USB device is large (%d bytes); confirm before flashing.", usb_size) - label = subprocess.check_output(["lsblk", "-d", "-n", "-o", "LABEL", device_node], text=True, timeout=5).strip() + label = device.get("ID_FS_LABEL") if not label: label = os.path.basename(usb_path) @@ -49,15 +54,6 @@ def get_usb_info(usb_path: str) -> USBDeviceInfo | None: } log.info("USB Info: %s", usb_info) return usb_info - except subprocess.TimeoutExpired as e: - log.error("Timed out getting USB info for %s: %s", usb_path, e) - return None - except PermissionError: - log.error("Permission denied when trying to get USB info: %s", usb_path) - return None - except subprocess.CalledProcessError as e: - log.error("Error getting USB info: %s", e) - return None except Exception as err: log.error("Unexpected error getting USB info: %s", err) return None diff --git a/src/lufus/gui/dialogs.py b/src/lufus/gui/dialogs.py index 19d7bca6..8be35203 100644 --- a/src/lufus/gui/dialogs.py +++ b/src/lufus/gui/dialogs.py @@ -3,26 +3,22 @@ from PyQt6.QtWidgets import ( QApplication, QComboBox, - QCheckBox, QDialog, QFileDialog, QFrame, QHBoxLayout, QLabel, - QLineEdit, QMessageBox, QPushButton, QTextEdit, QVBoxLayout, ) -from PyQt6.QtCore import Qt, pyqtSignal, QRegularExpression, QUrl -from PyQt6.QtGui import QFont, QRegularExpressionValidator, QDesktopServices -import sys +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QFont from lufus import state as states from lufus.gui.constants import THEME_DIR, _find_resource_dir from lufus.gui.scale import Scale -from lufus.lufus_logging import get_logger class LogWindow(QDialog): @@ -32,6 +28,7 @@ def __init__(self, parent=None): self._T = parent._T if parent else {} self._S: Scale = parent._S if parent else None self.setWindowTitle(self._T.get("log_window_title", "Log Window")) + if self._S: # apply scaled dimensions self.resize(self._S.px(650), self._S.px(450)) @@ -142,12 +139,6 @@ def __init__(self, parent=None): sep.setFrameShadow(QFrame.Shadow.Sunken) layout.addWidget(sep) - # version - lbl_ver = QLabel(states.version) - lbl_ver.setStyleSheet(f"font-family: {font_family}; font-size: {tool_pt}pt;") - lbl_ver.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(lbl_ver) - # im lying ily (context text something area, whatever) self.about_text = QTextEdit() self.about_text.setReadOnly(True) @@ -155,19 +146,6 @@ def __init__(self, parent=None): self.about_text.setFrameShape(QFrame.Shape.NoFrame) self.about_text.setStyleSheet(f"font-family: {font_family}; font-size: {tool_pt}pt;") layout.addWidget(self.about_text, 1) - btn_row0 = QHBoxLayout() - btn_discord = QPushButton(self._T.get("btn_discord", "Join Discord Server")) - btn_discord.setFixedWidth(self._S.px(300) if self._S else 90) - btn_discord.clicked.connect(self.discord_open) - btn_row0.addWidget(btn_discord, alignment=Qt.AlignmentFlag.AlignCenter) - layout.addLayout(btn_row0) - - btn_row1 = QHBoxLayout() - btn_github = QPushButton(self._T.get("btn_github", "Open Github Repo")) - btn_github.setFixedWidth(self._S.px(300) if self._S else 90) - btn_github.clicked.connect(self.github_open) - btn_row1.addWidget(btn_github, alignment=Qt.AlignmentFlag.AlignCenter) - layout.addLayout(btn_row1) btn_row = QHBoxLayout() # close button or smth, whatever @@ -176,15 +154,8 @@ def __init__(self, parent=None): btn_close.clicked.connect(self.hide) btn_row.addWidget(btn_close, alignment=Qt.AlignmentFlag.AlignCenter) layout.addLayout(btn_row) - self.setLayout(layout) - def discord_open(self): - url = QUrl("https://discord.gg/4G6FeBwsxb") - QDesktopServices.openUrl(url) - - def github_open(self): - url = QUrl("https://github.com/Hog185/Lufus") - QDesktopServices.openUrl(url) + self.setLayout(layout) class SettingsDialog(QDialog): @@ -275,102 +246,3 @@ def _detect_themes(): # user themes follow the same folder structure :D custom = sorted(p.parent.name for p in user_themes_dir.glob("*/*_theme.json")) return builtin, custom - - -class WinTweaks(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - re = QRegularExpression("^[a-zA-Z0-9_]*$") - validator = QRegularExpressionValidator(re) - self.setWindowTitle("Windows Tweaks (MAY BREAK! USE CAUTION)") - self.setFixedSize(600, 300) - self.ask_label = QLabel("Do you want to customize your windows installation?") - self.hardware_checkbox = QCheckBox("Remove requirement for 4GB+ RAM, Secure Boot and TPM 2.0") - self.hardware_checkbox.stateChanged.connect(self.update_winhardware) - self.microsoft_checkbox = QCheckBox("Remove requirement for an online Microsoft Account") - self.microsoft_checkbox.stateChanged.connect(self.update_winmicrosoftacc) - self.localacc_checkbox = QCheckBox("Create a local account with username:") - self.localacc_checkbox.stateChanged.connect(self.update_winlocalaccchk) - self.username_input = QLineEdit() - self.username_input.setMaxLength(20) - self.username_input.setValidator(validator) - self.username_input.setPlaceholderText("Enter username here...") - self.microsoft_checkbox.toggled.connect(self.localacc_checkbox.setEnabled) - self.localacc_checkbox.toggled.connect(self.username_input.setEnabled) - self.username_input.setEnabled(self.localacc_checkbox.isChecked()) - self.username_input.textChanged.connect(self.sync_username) - self.data_checkbox = QCheckBox("Disable data collection (skip privacy questions)") - self.data_checkbox.stateChanged.connect(self.update_winprivacy) - self.applytweaks_btn = QPushButton("Apply") - self.applytweaks_btn.clicked.connect(self.applywintweaks) - self.canceltweaks_btn = QPushButton("Cancel") - self.canceltweaks_btn.clicked.connect(self.reject) # closes window - layout = QVBoxLayout() - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) - layout.addWidget(self.ask_label, alignment=Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.hardware_checkbox) - layout.addWidget(self.microsoft_checkbox) - layout.addWidget(self.localacc_checkbox) - layout.addWidget(self.username_input) - layout.addWidget(self.data_checkbox) - button_layout = QHBoxLayout() - button_layout.addWidget(self.applytweaks_btn) - button_layout.addWidget(self.canceltweaks_btn) - layout.addLayout(button_layout) - layout.addStretch(1) - self.setLayout(layout) - - def log_message(self, msg): - # delegate logging to parent window if available, otherwise use module logger - parent = self.parent() - if parent is not None and hasattr(parent, "log_message"): - parent.log_message(msg) - else: - get_logger("wintweaks").info(msg) - - def update_winhardware(self): - # update winhardware req disable setting - states.win_hardware_bypass = 1 if self.hardware_checkbox.isChecked() else 0 - self.log_message( - f"Windows hardware requirement disable: {'enabled' if self.hardware_checkbox.isChecked() else 'disabled'}" - ) - - def update_winmicrosoftacc(self): - # update microsoft acc disable setting - states.win_microsoft_acc = 1 if self.microsoft_checkbox.isChecked() else 0 - self.log_message( - f"Microsoft account requirement disable: {'enabled' if self.microsoft_checkbox.isChecked() else 'disabled'}" - ) - - def update_winlocalaccchk(self): - # update local acc setting - states.win_local_acc_chk = 1 if self.localacc_checkbox.isChecked() else 0 - self.log_message( - f"Windows Local Account Add: {'enabled' if self.localacc_checkbox.isChecked() else 'disabled'}" - ) - - def sync_username(self, new_username): - # changes local username - states.win_local_acc = new_username - - def update_winprivacy(self): - # update win privacy setting - states.win_privacy = 1 if self.data_checkbox.isChecked() else 0 - self.log_message( - f"Windows privacy questions disable: {'enabled' if self.data_checkbox.isChecked() else 'disabled'}" - ) - # main tweaks apply logic function - - def applywintweaks(self): - # closes window - self.accept() - - -# for debug -# if __name__ == "__main__": -# # Standard boilerplate for testing the class standalone -# app = QApplication(sys.argv) -# window = WinTweaks() -# window.exec() -# sys.exit(app.exec()) diff --git a/src/lufus/gui/gui.py b/src/lufus/gui/gui.py index af4cd738..949ae6db 100644 --- a/src/lufus/gui/gui.py +++ b/src/lufus/gui/gui.py @@ -6,10 +6,13 @@ import platform import getpass import time -import ssl +import signal +import termios +import requests import urllib.parse import urllib.request import webbrowser +import psutil from typing import Dict, Any from packaging import version from platformdirs import user_config_dir @@ -43,7 +46,6 @@ from PyQt6.QtGui import QIcon from lufus import state -from lufus import state as states from lufus.drives.autodetect_usb import UsbMonitor from lufus.lufus_logging import get_logger from lufus.gui.themes.icon_utils import svg_icon @@ -51,9 +53,8 @@ from lufus.gui.scale import Scale from lufus.gui.i18n import load_translations from lufus.gui.redirector import StdoutRedirector -from lufus.gui.dialogs import LogWindow, AboutWindow, SettingsDialog, WinTweaks +from lufus.gui.dialogs import LogWindow, AboutWindow, SettingsDialog from lufus.gui.workers import FlashWorker, VerifyWorker -from lufus.writing.windows.tweaks import * # log level mapping for colors and methods _LOG_LEVELS = { @@ -66,6 +67,126 @@ } +# --------------------------------------------------------------------------- +# /proc-based helpers (replace lsof / fuser / pgrep) +# --------------------------------------------------------------------------- + + +def _procs_using_device_fd(device_node: str) -> list[tuple[int, str]]: + """Return ``(pid, comm)`` for each process with *device_node* open as an fd. + + Walks ``/proc//fd/`` and compares ``st_rdev`` against the device. + Requires the caller to have sufficient permissions (typically root). + """ + found: list[tuple[int, str]] = [] + try: + target_rdev = os.stat(device_node).st_rdev + except OSError: + return found + for entry in Path("/proc").iterdir(): + if not entry.name.isdigit(): + continue + try: + for fd_link in (entry / "fd").iterdir(): + try: + if os.stat(str(fd_link)).st_rdev == target_rdev: + comm = (entry / "comm").read_text().strip() + found.append((int(entry.name), comm)) + break + except OSError: + pass + except (PermissionError, OSError): + pass + return found + + +def _procs_with_device_mounted(device_node: str) -> list[tuple[int, str]]: + """Return ``(pid, comm)`` for each process that has *device_node* mounted. + + Walks ``/proc//mountinfo`` and matches the device's ``major:minor``. + """ + found: list[tuple[int, str]] = [] + try: + st = os.stat(device_node) + dev_str = f"{os.major(st.st_rdev)}:{os.minor(st.st_rdev)}" + except OSError: + return found + for entry in Path("/proc").iterdir(): + if not entry.name.isdigit(): + continue + try: + for line in (entry / "mountinfo").read_text().splitlines(): + fields = line.split() + if len(fields) >= 3 and fields[2] == dev_str: + comm = (entry / "comm").read_text().strip() + found.append((int(entry.name), comm)) + break + except (PermissionError, OSError): + pass + return found + + +def _real_user_home() -> Path: + """Return the home directory of the *actual* user, not root's home. + + When Lufus is launched via ``pkexec`` or ``sudo`` (as it must be for raw + device access), ``Path.home()`` resolves to ``/root``, which is wrong for + locating the user's downloads. This function checks the standard privilege- + escalation environment variables to find the invoking user's home instead. + """ + import pwd + + # pkexec sets PKEXEC_UID to the UID of the calling user + pkexec_uid = os.environ.get("PKEXEC_UID", "").strip() + if pkexec_uid.isdigit(): + try: + return Path(pwd.getpwuid(int(pkexec_uid)).pw_dir) + except KeyError: + pass + + # sudo/su preserve SUDO_USER with the original username + sudo_user = os.environ.get("SUDO_USER", "").strip() + if sudo_user and sudo_user != "root": + try: + return Path(pwd.getpwnam(sudo_user).pw_dir) + except KeyError: + pass + + return Path.home() + + +def _xdg_download_dir() -> Path: + """Return the user's XDG download directory without invoking ``xdg-user-dir``. + + Resolution order (mirrors what ``xdg-user-dir`` itself does): + 1. ``$XDG_DOWNLOAD_DIR`` environment variable + 2. ``XDG_DOWNLOAD_DIR`` entry in ``~/.config/user-dirs.dirs`` + 3. ``/Downloads`` as a universal fallback + + Uses :func:`_real_user_home` so that the result is correct even when the + process is running as root via ``pkexec``/``sudo``. + """ + real_home = _real_user_home() + + env_val = os.environ.get("XDG_DOWNLOAD_DIR", "").strip() + if env_val: + return Path(env_val) + user_dirs_file = real_home / ".config" / "user-dirs.dirs" + try: + for raw_line in user_dirs_file.read_text(errors="replace").splitlines(): + line = raw_line.strip() + if line.startswith("#") or "=" not in line: + continue + key, _, val = line.partition("=") + if key.strip() == "XDG_DOWNLOAD_DIR": + # Values are shell-quoted and may use $HOME + path_str = val.strip().strip('"').replace("$HOME", str(real_home)) + return Path(path_str) + except OSError: + pass + return real_home / "Downloads" + + class BackgroundWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) @@ -193,23 +314,13 @@ def __init__(self, usb_devices=None, scale: Scale = None): self.log_message(f"Startup USB devices passed in: {list((usb_devices or {}).keys()) or 'none'}") self.flash_worker = None self.log_message(f"UI scale factor: {self._S.f():.3f} (base 96 DPI)") - self._check_latest_download() - # check for new updates function call QTimer.singleShot(100, self.get_latest_release) def _check_latest_download(self): if state.iso_path: return - try: - result = subprocess.run(["xdg-user-dir", "DOWNLOAD"], capture_output=True, text=True, timeout=2) - downloads = ( - Path(result.stdout.strip()) - if result.returncode == 0 and result.stdout.strip() - else Path.home() / "Downloads" - ) - except Exception: - downloads = Path.home() / "Downloads" + downloads = _xdg_download_dir() if not downloads.is_dir(): return try: @@ -605,19 +716,7 @@ def init_ui(self): # filesystem cluster and flash option selectors :D self.lbl_fs = QLabel(self._T.get("lbl_file_system", "File System")) self.combo_fs = QComboBox() - self.all_fs_options = [ - "NTFS", - "FAT32", - "exFAT", - "ext4", - "UDF", - "HFS+", - "ext2", - "ext3", - "Btrfs", - "XFS", - "ZFS", - ] + self.all_fs_options = ["NTFS", "FAT32", "exFAT", "ext4", "UDF", "HFS+", "ext2", "ext3", "Btrfs", "XFS", "ZFS"] self.combo_fs.addItems(["NTFS", "FAT32", "exFAT"]) self.combo_fs.currentTextChanged.connect(self.updateFS) @@ -676,8 +775,6 @@ def init_ui(self): # sha256 verification checkbox and input :D self.chk_verify = QCheckBox(self._T.get("chk_verify_hash", "Verify SHA256 Checksum")) self.chk_verify.stateChanged.connect(self.update_verify_hash) - self.lbl_expected_hash = QLabel(self._T.get("lbl_expected_hash", "Expected SHA256:")) - self.lbl_expected_hash.setVisible(False) self.input_hash = QLineEdit() self.input_hash.setPlaceholderText(self._T.get("input_hash_placeholder", "Enter expected SHA256 hash here...")) self.input_hash.setEnabled(False) @@ -693,7 +790,6 @@ def init_ui(self): chk_layout.addWidget(self.chk_badblocks) chk_layout.addWidget(self.combo_badblocks) chk_layout.addWidget(self.chk_verify) - chk_layout.addWidget(self.lbl_expected_hash) chk_layout.addWidget(self.input_hash) main_layout.addLayout(chk_layout) @@ -837,10 +933,7 @@ def refresh_usb_devices(self): except Exception as e: # handle scan errors :3 self.statusBar.showMessage(self._T.get("status_scan_failed", "Scan Failed"), 3000) - self.log_message( - f"USB scan raised exception: {type(e).__name__}: {str(e)}", - level="ERROR", - ) + self.log_message(f"USB scan raised exception: {type(e).__name__}: {str(e)}", level="ERROR") QMessageBox.critical( self, self._T.get("msgbox_scan_error_title", "Scan Error"), @@ -998,8 +1091,6 @@ def update_verify_hash(self): # update sha256 verification setting :D state.verify_hash = self.chk_verify.isChecked() self.input_hash.setEnabled(state.verify_hash) - if hasattr(self, "lbl_expected_hash"): - self.lbl_expected_hash.setVisible(state.verify_hash) self._animate_widget(self.input_hash, state.verify_hash, "_anim_hash") self.log_message(f"SHA256 verification: {'enabled' if state.verify_hash else 'disabled'}") @@ -1009,7 +1100,7 @@ def update_expected_hash(self, text): def _load_latest_download_iso(self): # check downloads folder for the most recently modified iso :3 - downloads_dir = Path.home() / "Downloads" + downloads_dir = _xdg_download_dir() if not downloads_dir.is_dir(): return isos = sorted(downloads_dir.glob("*.iso"), key=lambda p: p.stat().st_mtime, reverse=True) @@ -1102,11 +1193,8 @@ def browse_file(self): file_name, _ = QFileDialog.getOpenFileName( self, self._T.get("dlg_select_image_title", "Select Image"), - "", - self._T.get( - "dlg_select_image_filter", - "Disk Images (*.iso *.dmg *.img *.bin *.raw);;All Files (*)", - ), + str(_xdg_download_dir()), + self._T.get("dlg_select_image_filter", "Disk Images (*.iso *.dmg *.img *.bin *.raw);;All Files (*)"), ) if file_name: # load selected image file :3 @@ -1123,10 +1211,9 @@ def _detect_iso_and_update_ui(self, iso_path: str): """Automatically detect ISO type and update UI selectors.""" from lufus.writing.windows.detect import detect_iso_type, IsoType - # Non-ISO raw images (.img, .bin, .raw, .dmg) are always "Other / DD mode" - if not iso_path.lower().endswith(".iso"): - self.log_message(f"Non-ISO image ({Path(iso_path).suffix or 'no ext'}), defaulting to Other/DD mode") - self.combo_image_option.setCurrentIndex(2) # Other + # Support various disk image formats, not just .iso + valid_extensions = (".iso", ".img", ".dmg", ".bin", ".raw") + if not iso_path.lower().endswith(valid_extensions): return self.log_message(f"Detecting ISO type for: {iso_path}...") @@ -1185,10 +1272,7 @@ def show_about(self): if self.about_window: self.about_window.close() self.about_window = AboutWindow(self) - content = self._T.get( - "about_content", - "Lufus - USB Flash Tool\n\nA simple, open-source USB flashing utility.", - ) + content = self._T.get("about_content", "Lufus - USB Flash Tool\n\nA simple, open-source USB flashing utility.") flat = getattr(self, "_flat_theme", {}) font_family = flat.get("fonts_family", "") fg_color = flat.get("colors_fg", "") @@ -1261,13 +1345,6 @@ def _update_ui_text(self): self.btn_cancel.setText(self._T.get("btn_cancel", "Cancel")) self.statusBar.showMessage(self._T.get("status_ready", "Ready"), 0) - # update toolbar button tooltips :3 - self.btn_refresh.setToolTip(self._T.get("tooltip_refresh", "Refresh USB devices (Ctrl+R)")) - self.btn_icon1.setToolTip(self._T.get("tooltip_website", "Website")) - self.btn_icon2.setToolTip(self._T.get("tooltip_about", "About")) - self.btn_icon3.setToolTip(self._T.get("tooltip_settings", "Settings")) - self.btn_icon4.setToolTip(self._T.get("tooltip_log", "Log")) - # update image option combo :D current_img_idx = self.combo_image_option.currentIndex() self.combo_image_option.blockSignals(True) @@ -1301,10 +1378,15 @@ def _update_ui_text(self): # update verification controls :D self.chk_verify.setText(self._T.get("chk_verify_hash", "Verify SHA256 Checksum")) - self.lbl_expected_hash.setText(self._T.get("lbl_expected_hash", "Expected SHA256:")) self.input_hash.setPlaceholderText(self._T.get("input_hash_placeholder", "Enter expected SHA256 hash here...")) self.input_label.setPlaceholderText(self._T.get("lbl_volume_label", "Volume Label")) + self.btn_icon1.setToolTip(self._T.get("tooltip_website", "website")) + self.btn_icon2.setToolTip(self._T.get("tooltip_about", "about")) + self.btn_icon3.setToolTip(self._T.get("tooltip_settings", "settings")) + self.btn_icon4.setToolTip(self._T.get("tooltip_log", "log")) + self.btn_refresh.setToolTip(self._T.get("tooltip_refresh", "refresh")) + # update boot combo default text :3 if self.combo_boot.itemText(0) == "installation_media.iso" or self.combo_boot.itemText(0) == self._T.get( "combo_boot_default", "installation_media.iso" @@ -1336,12 +1418,13 @@ def cancel_process(self): self.log_message(f"Cancellation requested for device {device_node}", level="WARN") try: - # check what processes are using device :3 - lsof = subprocess.run(["lsof", device_node], capture_output=True, text=True) - if lsof.returncode == 0: - self.log_message(f"Processes using {device_node} before kill:\n{lsof.stdout}") + # check what processes have the device open via /proc/*/fd/ + procs = _procs_using_device_fd(device_node) + if procs: + listing = "\n".join(f" pid={pid} ({comm})" for pid, comm in procs) + self.log_message(f"Processes using {device_node} before kill:\n{listing}") except Exception as e: - self.log_message(f"Could not run lsof: {e}") + self.log_message(f"Could not scan /proc for open handles: {e}") if self.flash_worker and self.flash_worker.isRunning(): # terminate flash worker thread :D @@ -1353,11 +1436,22 @@ def cancel_process(self): self.flash_worker.wait(2000) try: - # kill processes using device :3 - subprocess.run(["fuser", "-k", device_node], timeout=5, check=False) - self.log_message("fuser -k executed") + # kill processes that have the device mounted, per /proc/*/mountinfo + killed = [] + for pid, comm in _procs_with_device_mounted(device_node): + try: + os.kill(pid, signal.SIGKILL) + killed.append(f"pid={pid} ({comm})") + except ProcessLookupError: + pass # already gone + except PermissionError as exc: + self.log_message(f"Could not kill pid={pid} ({comm}): {exc}") + if killed: + self.log_message("Sent SIGKILL to: " + ", ".join(killed)) + else: + self.log_message("No mounted-device processes found to kill") except Exception as e: - self.log_message(f"fuser fallback failed: {e}") + self.log_message(f"Device-kill fallback failed: {e}") if hasattr(self, "verify_worker") and self.verify_worker and self.verify_worker.isRunning(): # terminate verify worker :D @@ -1367,12 +1461,19 @@ def cancel_process(self): self.log_message("Verify worker terminated") if self.is_terminal: - # reset terminal state :3 + # reset terminal state using termios instead of stty(1) try: - subprocess.run(["stty", "sane"], timeout=1, check=False) - self.log_message("Terminal reset to sane state") + fd = sys.stdin.fileno() + attrs = termios.tcgetattr(fd) + # Re-enable: NL translation, output processing, + # canonical mode, echo, and signal key processing + attrs[0] |= termios.ICRNL # iflag + attrs[1] |= termios.OPOST # oflag + attrs[3] |= termios.ICANON | termios.ECHO | termios.ISIG # lflag + termios.tcsetattr(fd, termios.TCSANOW, attrs) + self.log_message("Terminal restored to sane state") except Exception as e: - self.log_message(f"Failed to reset terminal: {e}") + self.log_message(f"Failed to restore terminal: {e}") # reset ui state :D self.progress_bar.setRange(0, 100) # exit indeterminate mode @@ -1421,10 +1522,7 @@ def start_process(self): QMessageBox.warning( self, self._T.get("msgbox_invalid_hash_title", "Invalid Hash"), - self._T.get( - "msgbox_invalid_hash_body", - "The provided SHA256 hash is invalid.", - ), + self._T.get("msgbox_invalid_hash_body", "The provided SHA256 hash is invalid."), ) return @@ -1445,10 +1543,6 @@ def start_process(self): self.verify_worker.start() else: # skip verification and start flash :3 - if states.image_option == 0 and states.currentflash == 0: - dlg = WinTweaks(self) - if dlg.exec() == QDialog.DialogCode.Rejected: - return self.perform_flash() def on_verify_finished(self, success: bool): @@ -1456,14 +1550,6 @@ def on_verify_finished(self, success: bool): if success: self.log_message("SHA256 verification successful, proceeding to flash") self._clear_speed_eta() - if states.image_option == 0 and states.currentflash == 0: - dlg = WinTweaks(self) - if dlg.exec() == QDialog.DialogCode.Rejected: - self.btn_start.setEnabled(True) - self.btn_cancel.setEnabled(False) - self.progress_bar.setValue(0) - self.progress_bar.setFormat("") - return self.perform_flash() else: # verification failed (╯°□°)╯( ┻━┻ @@ -1512,7 +1598,6 @@ def perform_flash(self): self.flash_worker.progress.connect(self._on_progress, Qt.ConnectionType.QueuedConnection) self.flash_worker.status.connect(self._on_flash_status, Qt.ConnectionType.QueuedConnection) self.flash_worker.flash_done.connect(self.on_flash_finished, Qt.ConnectionType.QueuedConnection) - self.flash_worker.request_tweaks.connect(self.show_tweak_dialog, Qt.ConnectionType.QueuedConnection) self.flash_worker.start() self.btn_start.setEnabled(False) self.btn_cancel.setEnabled(True) @@ -1552,7 +1637,6 @@ def _start_flash_with_options(self, options: dict) -> None: self.flash_worker.progress.connect(self._on_progress, Qt.ConnectionType.QueuedConnection) self.flash_worker.status.connect(self._on_flash_status, Qt.ConnectionType.QueuedConnection) self.flash_worker.flash_done.connect(self.on_flash_finished, Qt.ConnectionType.QueuedConnection) - self.flash_worker.request_tweaks.connect(self.show_tweak_dialog, Qt.ConnectionType.QueuedConnection) self.flash_worker.start() self.btn_start.setEnabled(False) self.btn_cancel.setEnabled(True) @@ -1576,18 +1660,7 @@ def on_flash_finished(self, success: bool): # flash succeeded :D self.progress_bar.setValue(100) self.progress_bar.setFormat(self._T.get("progress_complete", "Complete")) - # change from fo to tweaks self.log_message("Flash operation finished with result: SUCCESS") - if states.image_option == 0 and states.currentflash == 0: - if getattr(states, "win_hardware_bypass", 0) == 1: - win_hardware_bypass() - if getattr(states, "win_microsoft_acc", 0) == 1: - if getattr(states, "win_local_acc_chk", 0) == 1: - win_local_acc_name() - else: - win_local_acc() - if getattr(states, "win_privacy", 0) == 1: - win_skip_privacy_questions() QMessageBox.information( self, self._T.get("msgbox_success_title", "Success"), @@ -1676,8 +1749,7 @@ def _apply_accessible_names(self) -> None: self.combo_image_option.setAccessibleName(self._T.get("acc_image_option", "Image option selector")) self.combo_image_option.setAccessibleDescription( self._T.get( - "acc_image_option_desc", - "Choose the type of image to write: Windows, Linux, Other, or Format Only", + "acc_image_option_desc", "Choose the type of image to write: Windows, Linux, Other, or Format Only" ) ) self.input_label.setAccessibleName(self._T.get("acc_volume_label", "Volume label input")) @@ -1694,10 +1766,7 @@ def _apply_accessible_names(self) -> None: self.chk_verify.setAccessibleName(self._T.get("acc_verify_hash", "Verify SHA256 checksum checkbox")) self.input_hash.setAccessibleName(self._T.get("acc_hash_input", "Expected SHA256 hash input")) self.input_hash.setAccessibleDescription( - self._T.get( - "acc_hash_input_desc", - "Paste the expected 64-character SHA256 hash here", - ) + self._T.get("acc_hash_input_desc", "Paste the expected 64-character SHA256 hash here") ) self.progress_bar.setAccessibleName(self._T.get("acc_progress", "Operation progress bar")) self.btn_start.setAccessibleName(self._T.get("acc_start", "Start operation")) @@ -1719,23 +1788,24 @@ def keyPressEvent(self, event): def check_polkit_agent(self): # check if a polkit authentication agent is running :3 # returns true if found false otherwise + agents = [ + "polkit-gnome-authentication-agent-1", + "polkit-kde-authentication-agent-1", + "lxqt-policykit-agent", + "mate-polkit", + "polkit-1-agent", + ] try: - # common agent process names :D - agents = [ - "polkit-gnome-authentication-agent-1", - "polkit-kde-authentication-agent-1", - "lxqt-policykit-agent", - "mate-polkit", - "polkit-1-agent", - ] - # use pgrep to search for any of these :3 - for agent in agents: - result = subprocess.run(["pgrep", "-f", agent], capture_output=True) - if result.returncode == 0: - return True + for proc in psutil.process_iter(["cmdline"]): + try: + cmdline = " ".join(proc.info["cmdline"] or []) + if any(agent in cmdline for agent in agents): + return True + except (psutil.NoSuchProcess, psutil.AccessDenied): + pass return False except Exception: - # if pgrep fails assume agent might be present better to try :D + # if process scan fails assume agent might be present better to try :D return True def get_latest_release(self): @@ -1744,41 +1814,25 @@ def get_latest_release(self): url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" current_version = state.version try: - ssl_ctx = ssl.create_default_context() - req = urllib.request.urlopen(url, timeout=5, context=ssl_ctx) + req = urllib.request.urlopen(url, timeout=5) if req.status == 200: data = json.loads(req.read().decode()) tag_name = data.get("tag_name", "") if not tag_name: - self.log_message( - "Update check: missing tag_name in API response", - level="WARNING", - ) + self.log_message("Update check: missing tag_name in API response", level="WARNING") return try: is_newer = version.parse(tag_name) > version.parse(current_version) except Exception: - self.log_message( - f"Update check: could not parse version tag {tag_name!r}", - level="WARNING", - ) + self.log_message(f"Update check: could not parse version tag {tag_name!r}", level="WARNING") return if is_newer: - self.log_message( - f"New version found: {tag_name} > {current_version}", - level="DEBUG", - ) + self.log_message(f"New version found: {tag_name} > {current_version}", level="DEBUG") else: - self.log_message( - f"Running latest release build: {tag_name} <= {current_version}", - level="INFO", - ) + self.log_message(f"Running latest release build: {tag_name} <= {current_version}", level="INFO") return else: - self.log_message( - f"Couldn't get latest release, response: {req.status}", - level="WARNING", - ) + self.log_message(f"Couldn't get latest release, response: {req.status}", level="WARNING") return except Exception as e: self.log_message(f"Update check failed: {e}", level="ERROR") @@ -1799,11 +1853,6 @@ def get_latest_release(self): else: self.log_message(f"download later button clicked", level="DEBUG") - # for win twaks - def show_tweak_dialog(self): - dialog = WinTweaks(self) - dialog.exec() - if __name__ == "__main__": # setup high dpi scaling :3 diff --git a/src/lufus/gui/i18n.py b/src/lufus/gui/i18n.py index ba0d7dd1..e2e995c3 100644 --- a/src/lufus/gui/i18n.py +++ b/src/lufus/gui/i18n.py @@ -16,7 +16,7 @@ def load_translations(language="English"): with open(lang_file, encoding="utf-8-sig", newline="") as f: for row in csv.DictReader(f): key = row.get("key", "").strip() - value = row.get("value", "") + value = row.get("value", "").strip() if key: t[key] = value return t diff --git a/src/lufus/gui/languages/Deutsch.csv b/src/lufus/gui/languages/Deutsch.csv index 58fe1021..2d66d0b3 100644 --- a/src/lufus/gui/languages/Deutsch.csv +++ b/src/lufus/gui/languages/Deutsch.csv @@ -20,7 +20,7 @@ lbl_partition_scheme,Partitionsschema lbl_target_system,Zielsystem lbl_volume_label,Laufwerksbezeichnung lbl_file_system,Dateisystem -lbl_flash_option,Schreibmethode +lbl_flash_option,Flash-Option lbl_cluster_size,Größe der Zuordnungseinheiten combo_image_windows,Standard-Windows-Installation combo_image_linux,Standard-Linux-Installation @@ -59,7 +59,7 @@ tooltip_log,Protokoll status_ready,Bereit status_scanning,Suche nach USB-Geräten... status_scan_failed,Suche fehlgeschlagen -status_flashing,Wird geschrieben... +status_flashing,Flashing... progress_preparing,Vorbereitung... progress_complete,Abgeschlossen! 100% progress_failed,Fehlgeschlagen @@ -69,11 +69,11 @@ progress_formatted,Laufwerk formatiert.. 60% progress_label_changed,Bezeichnung geändert.. 80% progress_mount_done,Einhängen fertig.. Abgeschlossen! 100% msgbox_cancel_title,Abbrechen -msgbox_cancel_body,Sind Sie sicher +msgbox_cancel_body,Sind Sie sicher, dass Sie abbrechen möchten? msgbox_success_title,Erfolg -msgbox_success_body,USB-Laufwerk erfolgreich beschrieben! +msgbox_success_body,USB-Laufwerk erfolgreich geflasht! msgbox_error_title,Fehler -msgbox_error_body,Fehler beim Schreiben auf das USB-Laufwerk. +msgbox_error_body,Fehler beim Flashen des USB-Laufwerks. msgbox_no_image_title,Kein Abbild msgbox_no_image_body,Bitte wählen Sie zuerst eine gültige Installationsdatei aus. msgbox_no_device_title,Kein Gerät @@ -87,9 +87,10 @@ msgbox_scan_error_title,Suchfehler msgbox_scan_error_body,Suche nach USB-Geräten fehlgeschlagen: no_usb_found,Keine USB-Geräte gefunden dlg_select_image_title,Abbild auswählen -dlg_select_image_filter,Abbilder (*.iso *.dmg *.img *.bin *.raw);;Alle Dateien (*) +dlg_select_image_filter,"Abbilder (*.iso *.dmg *.img *.bin *.raw);;Alle Dateien (*)" about_content,"lufus ist ein in Python geschriebener USB-Image-Writer für Linux. Inspiriert vom originalen lufus-Tool für Windows. +Version: v1.0.0b1 GitHub: github.com/hog185/lufus" about_subtitle,USB-Flash-Tool btn_close,Schließen @@ -102,26 +103,25 @@ combo_badblocks_2pass,2 Durchgänge combo_badblocks_3pass,3 Durchgänge status_unmounting_all,Alle Partitionen auf {device} werden ausgehängt... status_unmounting,{part} wird ausgehängt... -status_remounting,Wird wieder eingehängt {part}... status_format_starting,Formatierungsvorgang wird gestartet... status_format_in_progress,Laufwerk wird formatiert... status_format_complete,Formatierung abgeschlossen! status_format_failed,Formatierung FEHLGESCHLAGEN. Prüfen Sie das obige Protokoll für den genauen Fehler. -status_flash_error,Schreibfehler: {error} +status_flash_error,Flash-Fehler: {error} acc_device,Geräteauswahl -acc_device_desc,Wählen Sie das USB-Gerät +acc_device_desc,Wählen Sie das USB-Gerät, das Sie verwenden möchten acc_refresh,Geräte aktualisieren acc_refresh_desc,Nach verbundenen USB-Geräten suchen acc_boot,Auswahl des Start-Images acc_boot_desc,Zeigt die aktuell ausgewählte Image-Datei acc_select,Nach Image-Datei suchen acc_image_option,Auswahl der Image-Option -acc_image_option_desc,Wählen Sie den Typ des zu schreibenden Images: Windows +acc_image_option_desc,Wählen Sie den Typ des zu schreibenden Images: Windows, Linux, Sonstige oder Nur Formatieren acc_volume_label,Eingabe der Volumenbezeichnung acc_volume_label_desc,Geben Sie einen Namen für das USB-Volumen ein acc_filesystem,Auswahl des Dateisystems acc_cluster,Auswahl der Clustergröße -acc_flash_option,Auswahl der Schreibmethode +acc_flash_option,Auswahl der Flash-Methode acc_quick_format,Checkbox für Schnellformatierung acc_extended_label,Checkbox für erweiterte Bezeichnung acc_bad_blocks,Checkbox zur Prüfung auf defekte Blöcke diff --git a/src/lufus/gui/languages/English.csv b/src/lufus/gui/languages/English.csv index 385b5f3e..caf3aa5f 100644 --- a/src/lufus/gui/languages/English.csv +++ b/src/lufus/gui/languages/English.csv @@ -87,9 +87,10 @@ msgbox_scan_error_title,Scan Error msgbox_scan_error_body,Failed to scan for USB devices: no_usb_found,No USB devices found dlg_select_image_title,Select Disk Image -dlg_select_image_filter,Disk Images (*.iso *.dmg *.img *.bin *.raw);;All Files (*) +dlg_select_image_filter,"Disk Images (*.iso *.dmg *.img *.bin *.raw);;All Files (*)" about_content,"Lufus is a disk image writer written in Python for Linux. Inspired by the original Rufus tool for Windows. +Version: v1.0.0b1 GitHub: github.com/hog185/lufus" about_subtitle,USB Flash Tool btn_close,Close @@ -102,7 +103,6 @@ combo_badblocks_2pass,2 Pass combo_badblocks_3pass,3 Pass status_unmounting_all,Unmounting all partitions on {device}... status_unmounting,Unmounting {part}... -status_remounting,Remounting {part}... status_format_starting,Starting format operation... status_format_in_progress,Formatting drive... status_format_complete,Format complete! @@ -116,7 +116,7 @@ acc_boot,Boot image selector acc_boot_desc,Shows the currently selected image file acc_select,Browse for image file acc_image_option,Image option selector -acc_image_option_desc,Choose the type of image to write: Windows +acc_image_option_desc,Choose the type of image to write: Windows, Linux, Other, or Format Only acc_volume_label,Volume label input acc_volume_label_desc,Enter a name for the USB volume acc_filesystem,File system selector diff --git "a/src/lufus/gui/languages/Espa\303\261ol.csv" "b/src/lufus/gui/languages/Espa\303\261ol.csv" index a4d54ada..9068e81e 100644 --- "a/src/lufus/gui/languages/Espa\303\261ol.csv" +++ "b/src/lufus/gui/languages/Espa\303\261ol.csv" @@ -20,11 +20,10 @@ lbl_partition_scheme,Esquema de partición lbl_target_system,Sistema objetivo lbl_volume_label,Etiqueta de volumen lbl_file_system,Sistema de archivos -lbl_flash_option,Opción de grabación +lbl_flash_option,Opciones de flasheo lbl_cluster_size,Tamaño del cluster combo_image_windows,Instalación estandar de Windows combo_image_linux,Instalación de Linux -combo_image_other,Cualquier instalación (modo DD) combo_image_format,Solo formateo combo_partition_gpt,GPT combo_partition_mbr,MBR @@ -33,21 +32,14 @@ combo_target_bios,BIOS (o UEFI-CSM) combo_cluster_4096,4096 bytes (Por defecto) combo_cluster_8192,8192 bytes combo_badblocks_1pass,1 pasada -chk_quick_format,Formateo rápido -chk_extended_label,Crear etiquetas extendidas y archivos de íconos -chk_bad_blocks,Detectar bad blocks -chk_verify_hash,Verificar suma SHA256 -lbl_expected_hash,SHA256 esperado: -progress_verifying,Verificando suma SHA256... -msgbox_verify_fail_title,Verificación fallida -msgbox_verify_fail_body,¡La suma SHA256 no coincide! El archivo puede estar corrupto. -msgbox_invalid_hash_title,Hash inválido -msgbox_invalid_hash_body,El hash SHA256 proporcionado no es válido. -combo_flash_iso,Modo ISO +combo_flash_iso,Modo iso combo_flash_woe,Woe USB combo_flash_ventoy,Ventoy combo_flash_dd,DD combo_flash_none,Ninguno +chk_quick_format,Formateo rápido +chk_extended_label,Crear etiquetas extendidas y archivos de íconos +chk_bad_blocks,Detectar bad blocks btn_select,Seleccionar btn_start,Empezar btn_cancel,Cancelar @@ -59,7 +51,7 @@ tooltip_log,Log status_ready,Listo status_scanning,Escaneando por dispositivos USB... status_scan_failed,Escaneo fallido -status_flashing,Grabando... +status_flashing,Flasheando... progress_preparing,Preparando... progress_complete,Completado! 100% progress_failed,Fallido @@ -71,9 +63,9 @@ progress_mount_done,Montado.. Completado! 100% msgbox_cancel_title,Cancelar msgbox_cancel_body,Estás seguro de que quieres cancelar? msgbox_success_title,Éxito -msgbox_success_body,¡Dispositivo USB grabado con éxito! +msgbox_success_body,Dispositivo USB flasheado con éxito! msgbox_error_title,Error -msgbox_error_body,Error al grabar en el dispositivo USB. +msgbox_error_body,Flasheo de dispositivo USB fallido msgbox_no_image_title,Sin imagen msgbox_no_image_body,Primero selecciona un archivo de instalación válido. msgbox_no_device_title,Sin dispositivo @@ -92,24 +84,27 @@ dlg_select_image_filter,Imagenes ISO (*.iso);;All Files (*) about_content,"Lufus es una utilidad de escritura de discos para Linux hecha en Python. Inspirado por la herramienta original Rufus para Windows. +Version: v1.0.0b1 GitHub: github.com/hog185/lufus" +Traducción por/translation by: saber_03 :3 + +combo_badblocks_2pass,2 pasadas +combo_badblocks_3pass,3 pasadas +chk_verify_hash,Verificar suma SHA256 +input_hash_placeholder,Ingresa el hash SHA256 esperado aquí... about_subtitle,Herramienta de flash USB btn_close,Cerrar btn_ok,OK combo_boot_default,medios_de_instalación.iso tooltip_website,Sitio web settings_label_theme,Tema -input_hash_placeholder,Ingresa el hash SHA256 esperado aquí... -combo_badblocks_2pass,2 pasadas -combo_badblocks_3pass,3 pasadas status_unmounting_all,Desmontando todas las particiones en {device}... status_unmounting,Desmontando {part}... -status_remounting,Remontando {part}... status_format_starting,Iniciando operación de formateo... status_format_in_progress,Formateando disco... status_format_complete,¡Formateo completado! status_format_failed,Formateo FALLIDO. Comprueba el registro anterior para ver el error exacto. -status_flash_error,Error de grabación: {error} +status_flash_error,Error de flasheo: {error} acc_device,Selector de dispositivo acc_device_desc,Seleccione el dispositivo USB que desea usar acc_refresh,Actualizar dispositivos @@ -118,7 +113,7 @@ acc_boot,Selector de imagen de arranque acc_boot_desc,Muestra el archivo de imagen seleccionado actualmente acc_select,Buscar archivo de imagen acc_image_option,Selector de tipo de imagen -acc_image_option_desc,Elija el tipo de imagen a escribir: Windows +acc_image_option_desc,Elija el tipo de imagen a escribir: Windows, Linux, Otro, o Solo Formatear acc_volume_label,Campo de etiqueta de volumen acc_volume_label_desc,Introduzca un nombre para el volumen USB acc_filesystem,Selector de sistema de archivos diff --git "a/src/lufus/gui/languages/Fran\303\247ais.csv" "b/src/lufus/gui/languages/Fran\303\247ais.csv" index f9030b5a..35dea418 100644 --- "a/src/lufus/gui/languages/Fran\303\247ais.csv" +++ "b/src/lufus/gui/languages/Fran\303\247ais.csv" @@ -3,8 +3,8 @@ window_title,lufus log_window_title,Journal de lufus about_window_title,À propos settings_window_title,Paramètres -btn_copy_log,Copier -btn_save_log,Enregistrer... +btn_copy_log,Copier dans le presse-papier +btn_save_log,Enregistrer le journal... dlg_save_log_title,Enregistrer le journal save_failed_title,Échec de l'enregistrement save_failed_body,Impossible d'enregistrer le journal : @@ -20,11 +20,11 @@ lbl_partition_scheme,Schéma de partition lbl_target_system,Système de destination lbl_volume_label,Nom du volume lbl_file_system,Système de fichiers -lbl_flash_option,Option d'écriture +lbl_flash_option,Option de flash lbl_cluster_size,Taille d'unité d'allocation combo_image_windows,Installation standard de Windows combo_image_linux,Installation standard de Linux -combo_image_other,N'importe quelle installation (mode DD) +combo_image_other,Toute installation (mode DD) combo_image_format,Mode formatage uniquement combo_partition_gpt,GPT combo_partition_mbr,MBR @@ -59,7 +59,7 @@ tooltip_log,Journal status_ready,Prêt status_scanning,Recherche de périphériques USB... status_scan_failed,Échec de la recherche -status_flashing,Écriture en cours... +status_flashing,Flashage en cours... progress_preparing,Préparation... progress_complete,Terminé ! 100% progress_failed,Échec @@ -71,9 +71,9 @@ progress_mount_done,Montage terminé.. Terminé ! 100% msgbox_cancel_title,Annuler msgbox_cancel_body,Êtes-vous sûr de vouloir annuler ? msgbox_success_title,Succès -msgbox_success_body,Le lecteur USB a été écrit avec succès ! +msgbox_success_body,Le lecteur USB a été flashé avec succès ! msgbox_error_title,Erreur -msgbox_error_body,Échec de l'écriture sur le lecteur USB. +msgbox_error_body,Échec du flashage du lecteur USB. msgbox_no_image_title,Pas d'image msgbox_no_image_body,Veuillez d'abord sélectionner un fichier d'installation valide. msgbox_no_device_title,Pas de périphérique @@ -87,14 +87,15 @@ msgbox_scan_error_title,Erreur de recherche msgbox_scan_error_body,Échec de la recherche de périphériques USB : no_usb_found,Aucun périphérique USB trouvé dlg_select_image_title,Sélectionner l'image disque -dlg_select_image_filter,Images disque (*.iso *.dmg *.img *.bin *.raw);;Tous les fichiers (*) -about_content,"Lufus est un logiciel libre d'écriture d'images disque écrit en Python pour GNU/Linux, -inspiré par l'outil original Rufus, disponible uniquement pour Windows. -GitHub : https://github.com/Hog185/Lufus/" -about_subtitle,Outil d'écriture USB +dlg_select_image_filter,"Images disque (*.iso *.dmg *.img *.bin *.raw);;Tous les fichiers (*)" +about_content,"Lufus est un programme d'écriture d'images disque écrit en Python pour Linux. +Inspiré par l'outil original Rufus pour Windows. +Version : v1.0.0b1 +GitHub : github.com/hog185/lufus" +about_subtitle,Outil de flash USB btn_close,Fermer btn_ok,OK -combo_boot_default,media_d_installation.iso +combo_boot_default,média_d_installation.iso tooltip_website,Site web settings_label_theme,Thème input_hash_placeholder,Saisissez le hachage SHA256 attendu ici... @@ -102,7 +103,6 @@ combo_badblocks_2pass,2 Passes combo_badblocks_3pass,3 Passes status_unmounting_all,Démontage de toutes les partitions sur {device}... status_unmounting,Démontage de {part}... -status_remounting,Remontage de {part}... status_format_starting,Démarrage de l'opération de formatage... status_format_in_progress,Formatage du lecteur... status_format_complete,Formatage terminé ! @@ -116,17 +116,17 @@ acc_boot,Sélecteur d'image de démarrage acc_boot_desc,Affiche le fichier image actuellement sélectionné acc_select,Parcourir pour trouver un fichier image acc_image_option,Sélecteur d'option d'image -acc_image_option_desc,Choisissez le type d'image à écrire : Windows +acc_image_option_desc,Choisissez le type d'image à écrire : Windows, Linux, Autre ou Formatage uniquement acc_volume_label,Champ de saisie du nom du volume acc_volume_label_desc,Entrez un nom pour le volume USB acc_filesystem,Sélecteur de système de fichiers acc_cluster,Sélecteur de taille de cluster -acc_flash_option,Sélecteur de méthode d'écriture +acc_flash_option,Sélecteur de méthode de flash acc_quick_format,Case à cocher pour le formatage rapide -acc_extended_label,Case à cocher pour créer une étiquette étendue -acc_bad_blocks,Case à cocher pour vérifier les secteurs défectueux +acc_extended_label,Ccase à cocher pour créer une étendue étendue +acc_bad_blocks,Ccase à cocher pour vérifier les blocs défectueux acc_bad_blocks_passes,Sélecteur du nombre de passes pour la vérification des blocs -acc_verify_hash,Case à cocher pour vérifier la somme de contrôle SHA256 +acc_verify_hash,Ccase à cocher pour vérifier la somme de contrôle SHA256 acc_hash_input,Champ de saisie du hachage SHA256 attendu acc_hash_input_desc,Collez ici le hachage SHA256 attendu de 64 caractères acc_progress,Barre de progression de l'opération diff --git "a/src/lufus/gui/languages/Portugue\314\202s Brasileiro.csv" "b/src/lufus/gui/languages/Portugue\314\202s Brasileiro.csv" index 4e240134..83b401d9 100644 --- "a/src/lufus/gui/languages/Portugue\314\202s Brasileiro.csv" +++ "b/src/lufus/gui/languages/Portugue\314\202s Brasileiro.csv" @@ -90,7 +90,9 @@ dlg_select_image_title,Selecionar imagem de disco dlg_select_image_filter,Imagems ISO (*.iso);;Todos os arquivos (*) about_content,"O Lufus é um gravador de imagem escrito em python para o Linux. É inspirado no Rufus para o Windows. +Version: v1.0.0b1 GitHub: github.com/hog185/lufus" +#s1oplus was here meow :3 about_subtitle,Ferramenta de Flash USB btn_close,Fechar btn_ok,OK @@ -102,7 +104,6 @@ combo_badblocks_2pass,2 ciclos combo_badblocks_3pass,3 ciclos status_unmounting_all,Desmontando todas as partições em {device}... status_unmounting,Desmontando {part}... -status_remounting,Remontando {part}... status_format_starting,Iniciando operação de formatação... status_format_in_progress,Formatando disco... status_format_complete,Formatação concluída! @@ -116,7 +117,7 @@ acc_boot,Seletor de imagem de inicialização acc_boot_desc,Mostra o arquivo de imagem atualmente selecionado acc_select,Navegar para arquivo de imagem acc_image_option,Seletor de opção de imagem -acc_image_option_desc,Escolha o tipo de imagem a gravar: Windows +acc_image_option_desc,Escolha o tipo de imagem a gravar: Windows, Linux, Outro ou Somente Formatar acc_volume_label,Campo de nome do volume acc_volume_label_desc,Digite um nome para o volume USB acc_filesystem,Seletor de sistema de arquivos diff --git a/src/lufus/gui/languages/Svenska.csv b/src/lufus/gui/languages/Svenska.csv index c8f3a376..878c3637 100644 --- a/src/lufus/gui/languages/Svenska.csv +++ b/src/lufus/gui/languages/Svenska.csv @@ -20,7 +20,7 @@ lbl_partition_scheme,Partitionsschema lbl_target_system,Målsystem lbl_volume_label,Volymetikett lbl_file_system,Filsystem -lbl_flash_option,Skrivmetod +lbl_flash_option,Flashalternativ lbl_cluster_size,Klusterstorlek combo_image_windows,Standardinstallation av Windows combo_image_linux,Standardinstallation av Linux @@ -59,7 +59,7 @@ tooltip_log,Logg status_ready,Redo status_scanning,Skannar efter USB-enheter... status_scan_failed,Skanning misslyckades -status_flashing,Skriver... +status_flashing,Flashing... progress_preparing,Förbereder... progress_complete,Färdig! 100% progress_failed,Misslyckades @@ -71,9 +71,9 @@ progress_mount_done,Montering Klar... Slutförd! 100% msgbox_cancel_title,Avbryt msgbox_cancel_body,Är du säker på att du vill avbryta? msgbox_success_title,Lyckades -msgbox_success_body,USB-enheten skrevs framgångsrikt! +msgbox_success_body,USB-flashning lyckades! msgbox_error_title,Fel uppstod -msgbox_error_body,Misslyckades med att skriva till USB-enheten. +msgbox_error_body,Misslyckades med att flasha USB-enheten. msgbox_no_image_title,Ingen Avbildning msgbox_no_image_body,Vänligen välj en giltig installationsfil först. msgbox_no_device_title,Ingen Enhet @@ -87,11 +87,12 @@ msgbox_scan_error_title,Fel vid Skanning msgbox_scan_error_body,Misslyckades med att söka efter USB-enheter: no_usb_found,Inga USB-enheter hittade. dlg_select_image_title,Välj enhetsavbildning -dlg_select_image_filter,Enhetsavbildningar (*.iso *.dmg *.img *.bin *.raw);;Alla filer (*) +dlg_select_image_filter,"Enhetsavbildningar (*.iso *.dmg *.img *.bin *.raw);;Alla filer (*)" about_content,"Lufus är ett verktyg för att skriva diskavbildningar, skrivet i Python för Linux. Inspirerat av det ursprungliga Rufus-verktyget för Windows. +Version: v1.0.0b1 GitHub: github.com/hog185/lufus" -about_subtitle,USB-skrivningsverktyg +about_subtitle,USB-flashnings verktyg btn_close,Stäng btn_ok,OK combo_boot_default,installations_media.iso @@ -102,12 +103,11 @@ combo_badblocks_2pass,2 Pass combo_badblocks_3pass,3 Pass status_unmounting_all,Avmonterar alla partitioner på {device}... status_unmounting,Avmonterar {part}... -status_remounting,Monterar om {part}... status_format_starting,Börjar formatering... status_format_in_progress,Formaterar enhet... status_format_complete,Formatering slutförd! status_format_failed,Formatering MISSLYCKADES. Kolla på loggen ovan för att se det exakta felmeddelandet. -status_flash_error,Skrivfel: {error} +status_flash_error,Fel uppstod vid flashning: {error} acc_device,Enhetsväljare acc_device_desc,Välj USB-enheten du vill använda acc_refresh,Uppdatera enheter @@ -116,12 +116,12 @@ acc_boot,Startavbildningsväljare acc_boot_desc,Visar den akutellt valda avbildningsfilen acc_select,Bläddra efter avbildningsfil acc_image_option,Avbildningsval -acc_image_option_desc,Välj vilken typ av avbildning som ska skrivas: Windows +acc_image_option_desc,Välj vilken typ av avbildning som ska skrivas: Windows, Linux, Annat eller endast formatering acc_volume_label,Volymetikettinmatning acc_volume_label_desc,Ange ett namn för USB-volymen acc_filesystem,Filsystemväljare acc_cluster,Klusterstorlekväljare -acc_flash_option,Väljare för skrivmetod +acc_flash_option,Flashningmetodsväljare acc_quick_format,Snabbformateringsruta acc_extended_label,Kryssruta för utökad etikett acc_bad_blocks,Kyrssruta för kontroll av dåliga block diff --git "a/src/lufus/gui/languages/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.csv" "b/src/lufus/gui/languages/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.csv" index 49f913c3..ba7080ae 100644 --- "a/src/lufus/gui/languages/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.csv" +++ "b/src/lufus/gui/languages/\320\240\321\203\321\201\321\201\320\272\320\270\320\271.csv" @@ -25,7 +25,7 @@ lbl_cluster_size,Размер кластера combo_image_windows,Стандартная установка Windows combo_image_linux,Стандартная установка Linux combo_image_other,Любая установка (режим DD) -combo_image_format,"Режим ""Только форматирование""" +combo_image_format,Режим "Только форматирование" combo_partition_gpt,GPT combo_partition_mbr,MBR combo_target_uefi,UEFI (без CSM) @@ -69,15 +69,15 @@ progress_formatted,Форматирование устройства.. 60% progress_label_changed,Изменение лейбла.. 80% progress_mount_done,Монтирование завершено.. Завершено! 100% msgbox_cancel_title,ОТМЕНИТЬ -msgbox_cancel_body,Вы уверены +msgbox_cancel_body,Вы уверены, что хотите отменить процесс? msgbox_success_title,Успешно msgbox_success_body,Успешно записаны данные на USB устройство! msgbox_error_title,Ошибка msgbox_error_body,Не удалось записать данные на USB устройство. msgbox_no_image_title,Без образа -msgbox_no_image_body,Пожалуйста +msgbox_no_image_body,Пожалуйста, выберите сначала правильный установочный носитель. msgbox_no_device_title,Нет устройств -msgbox_no_device_body,Пожалуйста +msgbox_no_device_body,Пожалуйста, выберите сначала USB устройство. msgbox_usb_found_title,Обнаружено новое USB устройство msgbox_usb_found_body,USB устройство найдено msgbox_no_devices_title,Нет устройств @@ -87,9 +87,10 @@ msgbox_scan_error_title,Ошибка сканирования msgbox_scan_error_body,Ошибка в ходе сканирования USB устройств: no_usb_found,USB устройства не обнаружены. dlg_select_image_title,Выберите образ для записи -dlg_select_image_filter,Образы дисков (*.iso *.dmg *.img *.bin *.raw);;Все файлы (*) +dlg_select_image_filter,"Образы дисков (*.iso *.dmg *.img *.bin *.raw);;Все файлы (*)" about_content,"Lufus - это программа для записи образов дисков, написанная на языке Python для Linux. Вдохновлено оригинальным инструментом Rufus для Windows. +Версия: v1.0.0b1 GitHub: github.com/hog185/lufus" about_subtitle,Инструмент для записи USB btn_close,Закрыть @@ -102,21 +103,20 @@ combo_badblocks_2pass,2 прохода combo_badblocks_3pass,3 прохода status_unmounting_all,Размонтирование всех разделов на {device}... status_unmounting,Размонтирование {part}... -status_remounting,Повторное монтирование {part}... status_format_starting,Запуск операции форматирования... status_format_in_progress,Форматирование устройства... status_format_complete,Форматирование завершено! status_format_failed,Форматирование ПРОВАЛЕНО. Проверьте журнал выше для получения точной ошибки. status_flash_error,Ошибка записи: {error} acc_device,Выбор устройства -acc_device_desc,Выберите USB-устройство +acc_device_desc,Выберите USB-устройство, которое хотите использовать acc_refresh,Обновить устройства acc_refresh_desc,Сканировать подключённые USB-устройства acc_boot,Выбор загрузочного образа acc_boot_desc,Показывает текущий выбранный файл образа acc_select,Обзор файла образа acc_image_option,Выбор параметра образа -acc_image_option_desc,Выберите тип образа для записи: Windows +acc_image_option_desc,Выберите тип образа для записи: Windows, Linux, Другое или Только форматирование acc_volume_label,Поле ввода метки тома acc_volume_label_desc,Введите имя для USB-тома acc_filesystem,Выбор файловой системы diff --git "a/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" "b/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" index 4529b4da..61350bc9 100644 --- "a/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" +++ "b/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" @@ -1,135 +1,137 @@ -key,value -window_title,lufus -log_window_title,lufus Журнал -about_window_title,Про програму -settings_window_title,Налаштування -btn_copy_log,Копіювати до буфера обміну -btn_save_log,Зберегти журнал... -dlg_save_log_title,Зберегти журнал -save_failed_title,Помилка збереження -save_failed_body,Не вдалося зберегти журнал: -settings_label_language,Мова -settings_no_languages,Мови не знайдено -header_drive_properties,Властивості пристрою -header_format_options,Параметри форматування -header_status,Статус -lbl_device,Пристрій -lbl_boot_selection,Вибір завантаження -lbl_image_option,Параметр образу -lbl_partition_scheme,Схема розділів -lbl_target_system,Цільова система -lbl_volume_label,Мітка тома -lbl_file_system,Файлова система -lbl_flash_option,Параметр флеш-пам'яті -lbl_cluster_size,Розмір кластера -combo_image_windows,Стандартна установка Windows -combo_image_linux,Стандартний Linux -combo_image_other,Будь-яка установка (режим DD) -combo_image_format,Тільки режим форматування -combo_partition_gpt,GPT -combo_partition_mbr,MBR -combo_target_uefi,UEFI (без CSM) -combo_target_bios,BIOS (або UEFI-CSM) -combo_cluster_4096,4096 байтів (за замовчуванням) -combo_cluster_8192,8192 байтів -combo_badblocks_1pass,1 прохід -chk_quick_format,Швидке форматування -chk_extended_label,Створити розширену мітку та файли піктограм -chk_bad_blocks,Перевірити пристрій на наявність пошкоджених блоків -chk_verify_hash,Перевірити контрольну суму SHA256 -lbl_expected_hash,Очікуваний SHA256: -progress_verifying,Перевірка контрольної суми... -msgbox_verify_fail_title,Перевірка не вдалася -msgbox_verify_fail_body,Контрольна сума SHA256 не збігається! Файл може бути пошкоджений. -msgbox_invalid_hash_title,Невірний хеш -msgbox_invalid_hash_body,Наданий хеш SHA256 є недійсним. -combo_flash_iso,Режим Iso -combo_flash_woe,Woe USB -combo_flash_ventoy,Ventoy -combo_flash_dd,DD -combo_flash_none,Нічого -btn_select,ВИБРАТИ -btn_start,ПОЧАТИ -btn_cancel,СКАСУВАТИ -tooltip_refresh,Оновити USB-пристрої (Ctrl+R) -tooltip_download,Завантажити оновлення -tooltip_about,Про програму -tooltip_settings,Налаштування -tooltip_log,Журнал -status_ready,Готовий -status_scanning,Сканування USB-пристроїв... -status_scan_failed,Помилка ск -status_flashing,Прошивка... -progress_preparing,Підготовка... -progress_complete,Готово! 100% -progress_failed,Помилка -progress_starting,Початок.. 10% -progress_unmounted,Диск демонтовано.. 20% -progress_formatted,Форматування диска.. 60% -progress_label_changed,Мітку змінено.. 80% -progress_mount_done,Монтування виконано.. Завершено! 100% -msgbox_cancel_title,Скасування -msgbox_cancel_body,Ви дійсно хочете скасувати? -msgbox_success_title,Успішно -msgbox_success_body,USB-диск успішно прошито! -msgbox_error_title,Помилка -msgbox_error_body,Не вдалося прошити USB-диск. -msgbox_no_image_title,Немає образу -msgbox_no_image_body,Будь ласка спочатку виберіть дійсний файл установки. -msgbox_no_device_title,Немає пристрою -msgbox_no_device_body,Будь ласка спочатку виберіть USB-пристрій. -msgbox_usb_found_title,Знайдено новий USB-пристрій -msgbox_usb_found_body,USB-пристрій знайдено -msgbox_no_devices_title,Немає пристроїв -msgbox_no_devices_body,USB-диски не знайдено. Будь ласка підключіть диск і повторіть спробу. -msgbox_scan_error_title,Помилка сканування -msgbox_scan_error_body,Не вдалося просканувати USB-пристрої: -no_usb_found,USB-пристроїв не знайдено -dlg_select_image_title,Вибрати образ диска -dlg_select_image_filter,ISO-образи (*.iso);;Усі файли (*) -about_content,lufus — це інструмент запису образів дисків написаний на Python для Linux. -about_subtitle,Інструмент запису USB -btn_close,Закрити -btn_ok,OK -combo_boot_default,installation_media.iso -tooltip_website,Вебсайт -settings_label_theme,Тема -input_hash_placeholder,Введіть очікуваний хеш SHA256 тут... -combo_badblocks_2pass,2 проходи -combo_badblocks_3pass,3 проходи -status_unmounting_all,Демонтування всіх розділів на {device}... -status_unmounting,Демонтування {part}... -status_remounting,Перемонтування {part}... -status_format_starting,Запуск операції форматування... -status_format_in_progress,Форматування диска... -status_format_complete,Форматування завершено! -status_format_failed,Форматування НЕ ВДАЛОСЯ. Перевірте журнал вище для точної помилки. -status_flash_error,Помилка запису: {error} -acc_device,Вибір пристрою -acc_device_desc,Виберіть USB-пристрій -acc_refresh,Оновити пристрої -acc_refresh_desc,Сканувати підключені USB-пристрої -acc_boot,Вибір завантажувального образу -acc_boot_desc,Показує поточно вибраний файл образу -acc_select,Огляд файлів образу -acc_image_option,Вибір параметра образу -acc_image_option_desc,Виберіть тип образу для запису: Windows -acc_volume_label,Поле введення мітки тому -acc_volume_label_desc,Введіть ім'я для USB-тому -acc_filesystem,Вибір файлової системи -acc_cluster,Вибір розміру кластера -acc_flash_option,Вибір методу запису -acc_quick_format,Прапорець швидкого форматування -acc_extended_label,Прапорець створення розширеної мітки -acc_bad_blocks,Прапорець перевірки на пошкоджені блоки -acc_bad_blocks_passes,Вибір кількості проходів перевірки -acc_verify_hash,Прапорець перевірки контрольної суми SHA256 -acc_hash_input,Поле введення очікуваного хешу SHA256 -acc_hash_input_desc,Вставте сюди очікуваний 64-символьний хеш SHA256 -acc_progress,Індикатор прогресу операції -acc_start,Почати операцію -acc_cancel,Скасувати операцію -acc_website,Відкрити сайт Lufus -acc_about,Про Lufus -acc_settings,Відкрити налаштування -acc_log,Відкрити вікно журналу +key,value +window_title,lufus +log_window_title,lufus Журнал +about_window_title,Про програму +settings_window_title,Налаштування +btn_copy_log,Копіювати до буфера обміну +btn_save_log,Зберегти журнал... +dlg_save_log_title,Зберегти журнал +save_failed_title,Помилка збереження +save_failed_body,Не вдалося зберегти журнал: +settings_label_language,Мова +settings_no_languages,Мови не знайдено +header_drive_properties,Властивості пристрою +header_format_options,Параметри форматування +header_status,Статус +lbl_device,Пристрій +lbl_boot_selection,Вибір завантаження +lbl_image_option,Параметр образу +lbl_partition_scheme,Схема розділів +lbl_target_system,Цільова система +lbl_volume_label,Мітка тома +lbl_file_system,Файлова система +lbl_flash_option,Параметр флеш-пам'яті +lbl_cluster_size,Розмір кластера +combo_image_windows,Стандартна установка Windows +combo_image_linux,Стандартний Linux +combo_image_other,Будь-яка установка (режим DD) +combo_image_format,Тільки режим форматування +combo_partition_gpt,GPT +combo_partition_mbr,MBR +combo_target_uefi,UEFI (без CSM) +combo_target_bios,BIOS (або UEFI-CSM) +combo_cluster_4096,4096 байтів (за замовчуванням) +combo_cluster_8192,8192 байтів +combo_badblocks_1pass,1 прохід +combo_flash_iso,Режим Iso +combo_flash_woe,Woe USB +combo_flash_ventoy,Ventoy +combo_flash_dd,DD +combo_flash_none,Нічого +chk_quick_format,Швидке форматування +chk_extended_label,Створити розширену мітку та файли піктограм +chk_bad_blocks,Перевірити пристрій на наявність пошкоджених блоків +chk_verify_hash,Перевірити контрольну суму SHA256 +lbl_expected_hash,Очікуваний SHA256: +btn_select,ВИБРАТИ +btn_start,ПОЧАТИ +btn_cancel,СКАСУВАТИ +tooltip_refresh,Оновити USB-пристрої (Ctrl+R) +tooltip_download,Завантажити оновлення +tooltip_about,Про програму +tooltip_settings,Налаштування +tooltip_log,Журнал +status_ready,Готовий +status_scanning,Сканування USB-пристроїв... +status_scan_failed,Помилка ск +status_flashing,Прошивка... +progress_verifying,Перевірка контрольної суми... +progress_preparing,Підготовка... +progress_complete,Готово! 100% +progress_failed,Помилка +progress_starting,Початок.. 10% +progress_unmounted,Диск демонтовано.. 20% +progress_formatted,Форматування диска.. 60% +progress_label_changed,Мітку змінено.. 80% +progress_mount_done,Монтування виконано.. Завершено! 100% +msgbox_cancel_title,Скасування +msgbox_cancel_body,Ви дійсно хочете скасувати? +msgbox_success_title,Успішно +msgbox_success_body,USB-диск успішно прошито! +msgbox_error_title,Помилка +msgbox_error_body,Не вдалося прошити USB-диск. +msgbox_verify_fail_title,Перевірка не вдалася +msgbox_verify_fail_body,Контрольна сума SHA256 не збігається! Файл може бути пошкоджений. +msgbox_invalid_hash_title,Невірний хеш +msgbox_invalid_hash_body,Наданий хеш SHA256 є недійсним. +msgbox_no_image_title,Немає образу +msgbox_no_image_body,Будь ласка спочатку виберіть дійсний файл установки. +msgbox_no_device_title,Немає пристрою +msgbox_no_device_body,Будь ласка спочатку виберіть USB-пристрій. +msgbox_usb_found_title,Знайдено новий USB-пристрій +msgbox_usb_found_body,USB-пристрій знайдено +msgbox_no_devices_title,Немає пристроїв +msgbox_no_devices_body,USB-диски не знайдено. Будь ласка підключіть диск і повторіть спробу. +msgbox_scan_error_title,Помилка сканування +msgbox_scan_error_body,Не вдалося просканувати USB-пристрої: +no_usb_found,USB-пристроїв не знайдено +dlg_select_image_title,Вибрати образ диска +dlg_select_image_filter,ISO-образи (*.iso);;Усі файли (*) +about_content,lufus — це інструмент запису образів дисків написаний на Python для Linux. +Натхненний оригінальним інструментом lufus для Windows. +Версія: v1.0.0b1 +GitHub: github.com/hog185/lufus +about_subtitle,Інструмент запису USB +btn_close,Закрити +btn_ok,OK +combo_boot_default,installation_media.iso +tooltip_website,Вебсайт +settings_label_theme,Тема +input_hash_placeholder,Введіть очікуваний хеш SHA256 тут... +combo_badblocks_2pass,2 проходи +combo_badblocks_3pass,3 проходи +status_unmounting_all,Демонтування всіх розділів на {device}... +status_unmounting,Демонтування {part}... +status_format_starting,Запуск операції форматування... +status_format_in_progress,Форматування диска... +status_format_complete,Форматування завершено! +status_format_failed,Форматування НЕ ВДАЛОСЯ. Перевірте журнал вище для точної помилки. +status_flash_error,Помилка запису: {error} +acc_device,Вибір пристрою +acc_device_desc,Виберіть USB-пристрій, який хочете використати +acc_refresh,Оновити пристрої +acc_refresh_desc,Сканувати підключені USB-пристрої +acc_boot,Вибір завантажувального образу +acc_boot_desc,Показує поточно вибраний файл образу +acc_select,Огляд файлів образу +acc_image_option,Вибір параметра образу +acc_image_option_desc,Виберіть тип образу для запису: Windows, Linux, Інше або Тільки форматування +acc_volume_label,Поле введення мітки тому +acc_volume_label_desc,Введіть ім'я для USB-тому +acc_filesystem,Вибір файлової системи +acc_cluster,Вибір розміру кластера +acc_flash_option,Вибір методу запису +acc_quick_format,Прапорець швидкого форматування +acc_extended_label,Прапорець створення розширеної мітки +acc_bad_blocks,Прапорець перевірки на пошкоджені блоки +acc_bad_blocks_passes,Вибір кількості проходів перевірки +acc_verify_hash,Прапорець перевірки контрольної суми SHA256 +acc_hash_input,Поле введення очікуваного хешу SHA256 +acc_hash_input_desc,Вставте сюди очікуваний 64-символьний хеш SHA256 +acc_progress,Індикатор прогресу операції +acc_start,Почати операцію +acc_cancel,Скасувати операцію +acc_website,Відкрити сайт Lufus +acc_about,Про Lufus +acc_settings,Відкрити налаштування +acc_log,Відкрити вікно журналу diff --git "a/src/lufus/gui/languages/\330\271\330\261\330\250\331\212.csv" "b/src/lufus/gui/languages/\330\271\330\261\330\250\331\212.csv" index 25bdd611..db7d3049 100644 --- "a/src/lufus/gui/languages/\330\271\330\261\330\250\331\212.csv" +++ "b/src/lufus/gui/languages/\330\271\330\261\330\250\331\212.csv" @@ -87,9 +87,10 @@ msgbox_scan_error_title,حدث خطأ أثناء الفحص msgbox_scan_error_body,:فشل الفحص بحثاً عن أجهزة الـ USB no_usb_found,لم يتم العثور علي أجهزة الـ USB dlg_select_image_title,اختار ملف الصورة -dlg_select_image_filter,Disk Images (*.iso *.dmg *.img *.bin *.raw);;كل الملفات (*) +dlg_select_image_filter,"Disk Images (*.iso *.dmg *.img *.bin *.raw);;كل الملفات (*)" about_content,"لوفس (lufus) هو برنامج مصنوع بلغة بايثون لنظام تشغيل لينيكس. مستوحى من برنامج روفس (rufus) الأصلي لنظام تشغيل ويندوز. +الإصدار: 1.0.0b1 GitHub: github.com/hog185/lufus" about_subtitle,أداة USB Flash btn_close,إغلاق @@ -102,7 +103,6 @@ combo_badblocks_2pass,دورتان combo_badblocks_3pass,ثلاث دورات status_unmounting_all,جارٍ فصل جميع الأقسام على {device}... status_unmounting,جارٍ فصل {part}... -status_remounting,إعادة تحميل {part}... status_format_starting,جارٍ بدء عملية المحي... status_format_in_progress,جارٍ محو وتهيئة القرص... status_format_complete,!اكتملت عملية المحي diff --git "a/src/lufus/gui/languages/\340\246\254\340\246\276\340\246\202\340\246\262\340\246\276.csv" "b/src/lufus/gui/languages/\340\246\254\340\246\276\340\246\202\340\246\262\340\246\276.csv" index dcf7c800..360723b9 100644 --- "a/src/lufus/gui/languages/\340\246\254\340\246\276\340\246\202\340\246\262\340\246\276.csv" +++ "b/src/lufus/gui/languages/\340\246\254\340\246\276\340\246\202\340\246\262\340\246\276.csv" @@ -87,9 +87,10 @@ msgbox_scan_error_title,স্ক্যান ত্রুটি msgbox_scan_error_body,USB ডিভাইস স্ক্যান করতে ব্যর্থ: no_usb_found,কোনো USB ডিভাইস পাওয়া যায়নি dlg_select_image_title,ডিস্ক ইমেজ নির্বাচন করুন -dlg_select_image_filter,ডিস্ক ইমেজ (*.iso *.dmg *.img *.bin *.raw);;সব ফাইল (*) +dlg_select_image_filter,"ডিস্ক ইমেজ (*.iso *.dmg *.img *.bin *.raw);;সব ফাইল (*)" about_content,"লুফাস একটি ডিস্ক ইমেজ লেখার টুল যা লিনাক্সের জন্য Python দিয়ে তৈরি। উইন্ডোজের মূল Rufus টুল থেকে অনুপ্রাণিত। +ভার্সন: v1.0.0b1 GitHub: github.com/hog185/lufus" about_subtitle,USB ফ্ল্যাশ টুল btn_close,বন্ধ @@ -102,7 +103,6 @@ combo_badblocks_2pass,২ বার combo_badblocks_3pass,৩ বার status_unmounting_all,{device}-এ সব পার্টিশন আনমাউন্ট করা হচ্ছে... status_unmounting,{part} আনমাউন্ট করা হচ্ছে... -status_remounting,পুনরায় মাউন্ট করা হচ্ছে {part}... status_format_starting,ফরম্যাট অপারেশন শুরু হচ্ছে... status_format_in_progress,ড্রাইভ ফরম্যাট করা হচ্ছে... status_format_complete,ফরম্যাট সম্পন্ন! @@ -116,7 +116,7 @@ acc_boot,বুট ইমেজ সিলেক্টর acc_boot_desc,বর্তমানে নির্বাচিত ইমেজ ফাইল দেখায় acc_select,ইমেজ ফাইলের জন্য ব্রাউজ করুন acc_image_option,ইমেজ অপশন সিলেক্টর -acc_image_option_desc,লেখার জন্য ইমেজের ধরন নির্বাচন করুন: উইন্ডোজ +acc_image_option_desc,লেখার জন্য ইমেজের ধরন নির্বাচন করুন: উইন্ডোজ, লিনাক্স, অন্যান্য, বা শুধু ফরম্যাট acc_volume_label,ভলিউম লেবেল ইনপুট acc_volume_label_desc,USB ভলিউমের জন্য একটি নাম লিখুন acc_filesystem,ফাইল সিস্টেম সিলেক্টর diff --git a/src/lufus/gui/workers.py b/src/lufus/gui/workers.py index 8b3158d7..2857752c 100644 --- a/src/lufus/gui/workers.py +++ b/src/lufus/gui/workers.py @@ -51,7 +51,6 @@ class FlashWorker(QThread): progress = pyqtSignal(int) status = pyqtSignal(str) flash_done = pyqtSignal(bool) - request_tweaks = pyqtSignal() def __init__(self, options: dict, t: dict): super().__init__() @@ -110,23 +109,20 @@ def run(self): self.status.emit(self._T.get("status_format_complete", "Format complete!")) else: self.status.emit( - self._T.get( - "status_format_failed", - "Format FAILED. Check the log above for the exact error.", - ) + self._T.get("status_format_failed", "Format FAILED. Check the log above for the exact error.") ) elif image_option == 0: # Windows - # ISO mode (flash_mode 0) uses the specialised flash_windows path. - # Any other mode (e.g. DD) uses the generic flash_usb path. - scheme = PartitionScheme.SIMPLE_FAT32 - success = flash_usb( - device_node, - iso_path, - scheme, - progress_cb=self.progress.emit, - status_cb=self.status.emit, - ) + if flash_mode == 0: + # ISO mode for Windows (uses specialized flash_windows) + # For Windows, we currently default to SIMPLE_FAT32 partition scheme + # which handles splitting install.wim if necessary. + scheme = PartitionScheme.SIMPLE_FAT32 + success = flash_usb( + device_node, iso_path, scheme, progress_cb=self.progress.emit, status_cb=self.status.emit + ) + else: + success = False else: # Other flash modes (Linux, Other) fs_text = options.get("fs_text", "ext4") diff --git a/src/lufus/state.py b/src/lufus/state.py index a6c3117f..ccef2dee 100644 --- a/src/lufus/state.py +++ b/src/lufus/state.py @@ -8,7 +8,7 @@ class AppState: """Mutable runtime state shared across the application.""" # App info - version: str = "v1.0.0" + version: str = "v1.0.1b1" # Format options filesystem_index: int = 0 # 0=NTFS, 1=FAT32, 2=exFAT, 3=ext4, 4=UDF @@ -21,7 +21,7 @@ class AppState: check_bad: int = 0 # 0=1 pass, 1=2 passes new_label: str = "USB_DRIVE" flash_mode: int = 0 # 0=ISO, 1=DD - theme: str = "default" + # Runtime state iso_path: str = "" device_node: str = "" @@ -36,7 +36,6 @@ class AppState: # Windows tweaks win_hardware_bypass: int = 0 win_microsoft_acc: int = 0 - win_local_acc_chk: int = 0 win_local_acc: str = "default" win_privacy: int = 0 diff --git a/src/lufus/utils.py b/src/lufus/utils.py index 4065ff4b..0620a784 100644 --- a/src/lufus/utils.py +++ b/src/lufus/utils.py @@ -1,5 +1,15 @@ import os import re +import subprocess +from lufus.lufus_logging import get_logger + +log = get_logger(__name__) + + +def run_cmd(cmd: list[str]): + """Wrapper for subprocess.run with logging and error checking.""" + log.debug("run: %s", cmd) + subprocess.run(cmd, check=True) def elevate_privileges() -> None: @@ -13,28 +23,14 @@ def elevate_privileges() -> None: # 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. - theme_val = str(state.theme) - if re.match(r"^[A-Za-z0-9_\-. ]+$", os.path.basename(theme_val)) and ".." not in theme_val: - env["LUFUS_THEME"] = theme_val - else: - import logging - - logging.getLogger("lufus").warning( - "elevate_privileges: rejected suspicious LUFUS_THEME value %r", - theme_val, - ) + # Validate theme is a safe filename/path: no path separators, no "..", + # and must be a basic filename to prevent path traversal. + theme = os.path.basename(state.theme) + if theme and theme == state.theme and ".." not in theme: + env["LUFUS_THEME"] = theme # Preserve DISPLAY and XAUTHORITY for GUI apps under pkexec/sudo - env_vars = [ - "DISPLAY", - "XAUTHORITY", - "XDG_RUNTIME_DIR", - "WAYLAND_DISPLAY", - "PYTHONPATH", - "LUFUS_THEME", - ] + env_vars = ["DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "WAYLAND_DISPLAY", "PYTHONPATH", "LUFUS_THEME"] cmd = ["pkexec", "env"] for var in env_vars: diff --git a/src/lufus/writing/flash_usb.py b/src/lufus/writing/flash_usb.py index 50e038d5..a1b1e13a 100644 --- a/src/lufus/writing/flash_usb.py +++ b/src/lufus/writing/flash_usb.py @@ -92,8 +92,17 @@ def _status(msg: str) -> None: _status(f"Spawning dd: {' '.join(dd_args)}") _status(f"Writing {iso_size:,} bytes to {shlex.quote(device)}, this may take several minutes...") + # Build the environment with LC_ALL=C so dd's byte-count output uses the + # predictable decimal format regardless of the system locale. Copy + # os.environ rather than mutating it so the global environment is unchanged. + env = {**os.environ, "LC_ALL": "C"} + + # dd_args is constructed from a validated device path (regex-checked above) + # and a user-supplied iso_path. The list form of Popen (not shell=True) + # means each element is passed directly to execve, so there is no shell + # 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) except FileNotFoundError: log.error("Flash failed: 'dd' utility not found. Install coreutils.") _status("Flash failed: 'dd' utility not found. Install coreutils.") @@ -101,30 +110,58 @@ def _status(msg: str) -> None: _status(f"dd process started with PID {process.pid}") + _status("Flash starting...") + if progress_cb: + progress_cb(0) + + # dd status=progress writes updates separated by \r (carriage return), + # not \n. Reading line-by-line from process.stderr blocks until a \n + # is seen, so progress updates would only arrive at the very end. + # read1() returns whatever bytes are currently in the pipe buffer + # without waiting for a newline, letting us process \r-terminated + # progress lines in real time. buf = b"" - last_pct = -1 while True: - chunk = process.stderr.readline() + chunk = process.stderr.read1(65536) if not chunk: + # Flush any remaining partial stderr fragment so it is not lost + if buf: + decoded = buf.decode(errors="replace").rstrip("\r\n") + if decoded: + # Log at info to avoid alarming users with benign dd output + log.info("dd stderr (final fragment): %s", decoded) break buf += chunk - parts = re.split(rb"[\r\n]", buf) - buf = parts[-1] - for line in parts[:-1]: - line = line.strip() - if not line: + segments = re.split(rb"[\r\n]", buf) + # keep the last (possibly incomplete) segment for the next iteration + buf = segments[-1] + for segment_bytes in segments[:-1]: + line_str = segment_bytes.decode("utf-8", errors="replace").strip() + if not line_str: continue - m = re.match(rb"^(\d+)\s+bytes", line) - if m and iso_size > 0: + + # Percentage progress line (e.g. "50% completed" generated from + # dd byte output, or pv-style "50%") + pct_match = re.search(r"(\d+)%", line_str) + if pct_match and iso_size > 0: + pct_int = min(int(pct_match.group(1)), 100) + _status(line_str) + if progress_cb: + progress_cb(pct_int) + elif re.match(r"^\d+\s+bytes", line_str) and iso_size > 0: + # dd status=progress byte-count line + m = re.match(r"^(\d+)", line_str) bytes_done = int(m.group(1)) - pct = min(int(bytes_done * 100 / iso_size), 99) - if pct != last_pct: - _status(f"dd progress: {bytes_done:,} / {iso_size:,} bytes ({pct}%)") - last_pct = pct + pct_int = min(int(bytes_done * 100 / iso_size), 99) + status_str = f"{bytes_done:,} / {iso_size:,} bytes ({pct_int}% completed)" + _status(status_str) if progress_cb: - progress_cb(pct) + progress_cb(pct_int) + elif re.search(r"\brecords (in|out)\b", line_str) or re.search(r"\bcopied\b", line_str): + # dd bookkeeping lines — informational only, not progress events + pass else: - log.warning("dd stderr: %s", line.decode("utf-8", errors="replace")) + log.info("dd stderr: %s", line_str) process.wait() _status(f"dd process exited with return code {process.returncode}") diff --git a/src/lufus/writing/windows/detect.py b/src/lufus/writing/windows/detect.py index 38ee099e..153fe18f 100644 --- a/src/lufus/writing/windows/detect.py +++ b/src/lufus/writing/windows/detect.py @@ -210,22 +210,27 @@ def _get_file_listing(iso_path: str) -> "str | None": # _LINUX_FILE_MARKERS = [ # ---- SysLinux / ISOLINUX (Linux-only bootloaders) ---- + "isolinux/isolinux.bin", "isolinux/isolinux.cfg", + "syslinux/syslinux.bin", "syslinux/syslinux.cfg", "syslinux/ldlinux.c32", # ---- GRUB config files — Linux-only for optical/USB media ---- # Windows uses BCD / bootmgr; it never ships grub.cfg on install media. "boot/grub/grub.cfg", - "boot/grub/i386-pc/", + "boot/grub/i386-pc", "grub/grub.cfg", + "boot/grub/x86_64-efi", # ---- Ubuntu / Kubuntu / Xubuntu / Mint (Casper live system) ---- "casper/filesystem.squashfs", "casper/filesystem.manifest", "casper/vmlinuz", + "casper/initrd", # ---- Debian / Kali / Tails / Parrot (live-boot) ---- "live/filesystem.squashfs", "live/filesystem.manifest", "live/vmlinuz", + "live/initrd.img", # ---- Ubuntu / Debian installer marker ---- ".disk/info", # ---- Arch Linux (specific sub-paths, not the broad "arch/" directory) ---- @@ -233,10 +238,8 @@ def _get_file_listing(iso_path: str) -> "str | None": "arch/boot/x86_64/vmlinuz-linux", # ---- Fedora / RHEL / CentOS installer (Anaconda) ---- "images/pxeboot/vmlinuz", + "images/pxeboot/initrd.img", ".discinfo", - # ---- Generic kernel presence under known Linux-only directories ---- - "boot/vmlinuz", - "boot/bzimage", ] @@ -290,11 +293,12 @@ def detect_iso_type(iso_path: str) -> IsoType: log.info("ISO detection: found Windows marker %r -> Windows", marker) return IsoType.WINDOWS - # Linux markers second — all verified non-overlapping with Windows - for marker in _LINUX_FILE_MARKERS: - if marker in listing: - log.info("ISO detection: found Linux marker %r -> Linux", marker) - return IsoType.LINUX + # Linux markers second — count matches to be more conservative + # Require at least 2 markers to avoid false positives from generic bootloader files + linux_marker_count = sum(1 for marker in _LINUX_FILE_MARKERS if marker in listing) + if linux_marker_count >= 2: + log.info("ISO detection: found %d Linux markers -> Linux", linux_marker_count) + return IsoType.LINUX log.info("ISO detection: no definitive markers found -> Other") return IsoType.OTHER diff --git a/src/lufus/writing/windows/flash.py b/src/lufus/writing/windows/flash.py index a68895fd..5daf47a3 100644 --- a/src/lufus/writing/windows/flash.py +++ b/src/lufus/writing/windows/flash.py @@ -10,22 +10,12 @@ from lufus import state from lufus.lufus_logging import get_logger from lufus.writing.partition_scheme import PartitionScheme +from lufus.utils import run_cmd log = get_logger(__name__) -class PartitionInfo(TypedDict): - role: str - path: str - - -def run_cmd(cmd): - """Wrapper for subprocess.run with logging and error checking.""" - log.debug("run: %s", cmd) - subprocess.run(cmd, check=True) - - def _status_print(msg: str): """Log and print a status message (used during ISO mounting).""" log.info(msg) @@ -155,7 +145,7 @@ def _copy_file(src: str, dst: str) -> str: 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 subprocess.run(["which", candidate], capture_output=True).returncode == 0: + if shutil.which(candidate): return candidate if status_cb: @@ -167,13 +157,13 @@ def _find_ntfs_tool(status_cb=None) -> str | None: ["zypper", "install", "-y", "ntfs-3g"], ] for pm_cmd in pkg_managers: - if subprocess.run(["which", pm_cmd[0]], capture_output=True).returncode == 0: + if shutil.which(pm_cmd[0]): run_cmd(["sudo"] + pm_cmd) break # stop after the first working package manager # Re-check after installation attempt for candidate in ["mkfs.ntfs", "mkntfs"]: - if subprocess.run(["which", candidate], capture_output=True).returncode == 0: + if shutil.which(candidate): return candidate # installation succeeded return None @@ -181,7 +171,7 @@ def _find_ntfs_tool(status_cb=None) -> str | None: def _ensure_wimlib(status_cb=None) -> None: """Install wimlib-imagex if not present. Raises FileNotFoundError if it can't be found after install.""" - if subprocess.run(["which", "wimlib-imagex"], capture_output=True).returncode == 0: + if shutil.which("wimlib-imagex"): return if status_cb: status_cb("wimlib-imagex not found, attempting to install...") @@ -192,10 +182,10 @@ def _ensure_wimlib(status_cb=None) -> None: ["zypper", "install", "-y", "wimtools"], ] for pm_cmd in pkg_managers: - if subprocess.run(["which", pm_cmd[0]], capture_output=True).returncode == 0: + if shutil.which(pm_cmd[0]): run_cmd(["sudo"] + pm_cmd) break - if subprocess.run(["which", "wimlib-imagex"], capture_output=True).returncode != 0: + if not shutil.which("wimlib-imagex"): raise FileNotFoundError( "wimlib-imagex not found. Install manually: sudo pacman -S wimlib / sudo apt install wimtools" ) @@ -267,18 +257,24 @@ def _copy_efi_boot_files(iso_mount, mount_efi, _status): """Copy EFI boot files from ISO to the EFI partition.""" _status("Copying EFI boot files to EFI partition...") + # EFI efi_src = _find_path_case_insensitive(iso_mount, "EFI") if efi_src: efi_items = os.listdir(efi_src) _status(f"Found EFI/ with {len(efi_items)} items: {efi_items}") - run_cmd(["sudo", "cp", "-r"] + [os.path.join(efi_src, i) for i in efi_items] + [mount_efi]) + efi_dst = os.path.join(mount_efi, "EFI") + run_cmd(["sudo", "mkdir", "-p", efi_dst]) + run_cmd(["sudo", "cp", "-r"] + [os.path.join(efi_src, i) for i in efi_items] + [efi_dst]) _status("Copied EFI/ tree to EFI partition") else: _status("WARNING: No EFI directory found in ISO - drive may not be UEFI bootable") + # boot boot_src = _find_path_case_insensitive(iso_mount, "boot") if boot_src: - run_cmd(["sudo", "cp", "-r"] + [os.path.join(boot_src, i) for i in os.listdir(boot_src)] + [mount_efi]) + boot_dst = os.path.join(mount_efi, "boot") + run_cmd(["sudo", "mkdir", "-p", boot_dst]) + run_cmd(["sudo", "cp", "-r"] + [os.path.join(boot_src, i) for i in os.listdir(boot_src)] + [boot_dst]) _status("Copied boot/ tree to EFI partition") for fname in ["bootmgr", "bootmgr.efi"]: @@ -442,7 +438,11 @@ def _status(msg): subprocess.run(["sudo", "umount", iso_mount], capture_output=True) -# ---new--- +class PartitionInfo(TypedDict): + role: str + path: str + + def mount_iso(iso_path: str) -> str | None: """This function mounts an iso file at /mnt/iso/ and returns the location if mount is successfull @@ -534,8 +534,16 @@ def create_partitions(drive: str, scheme: PartitionScheme) -> list[PartitionInfo def _get_disk_size_sectors(drive: str) -> int: - result = subprocess.run(["sudo", "blockdev", "--getsz", drive], capture_output=True, text=True, check=True) - return int(result.stdout.strip()) # returns 512-byte sectors + """Return the size of *drive* in 512-byte sectors. + + Reads ``/sys/class/block//size`` so no external tool is required. + The kernel always expresses block-device size in 512-byte units here, + regardless of the device's physical or logical sector size. + """ + dev_name = os.path.basename(drive) + sysfs_path = f"/sys/class/block/{dev_name}/size" + with open(sysfs_path) as fh: + return int(fh.read().strip()) UEFI_NTFS_URL = "https://github.com/pbatard/rufus/raw/master/res/uefi/uefi-ntfs.img" diff --git a/src/lufus/writing/windows/tweaks.py b/src/lufus/writing/windows/tweaks.py index 095e78a1..4942beef 100644 --- a/src/lufus/writing/windows/tweaks.py +++ b/src/lufus/writing/windows/tweaks.py @@ -9,7 +9,7 @@ import re import subprocess import os -from lufus.utils import get_mount_and_drive +from lufus.utils import get_mount_and_drive, run_cmd from lufus import state from lufus.lufus_logging import get_logger @@ -54,8 +54,8 @@ def win_hardware_bypass(): cmd_string = "\n".join(commands) + "\n" log.info("win_hardware_bypass: injecting registry keys into boot.wim at %s...", mount) try: - subprocess.run(["mkdir", "/media/tempwinmnt"], check=True) - subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True) + run_cmd(["sudo", "mkdir", "-p", "/media/tempwinmnt"]) + run_cmd(["sudo", "wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"]) subprocess.run( ["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SYSTEM"], input=cmd_string, @@ -63,8 +63,8 @@ def win_hardware_bypass(): capture_output=True, check=True, ) - subprocess.run(["wimunmount", "/media/tempwinmnt", "--commit"], check=True) - subprocess.run(["rm", "-rf", "/media/tempwinmnt"], check=True) + run_cmd(["sudo", "wimunmount", "/media/tempwinmnt", "--commit"]) + run_cmd(["sudo", "rm", "-rf", "/media/tempwinmnt"]) log.info("win_hardware_bypass: registry keys injected successfully.") except subprocess.CalledProcessError as e: log.error("win_hardware_bypass: CalledProcessError: %s", e.stderr) @@ -79,8 +79,8 @@ def win_local_acc(): cmd_string = "\n".join(commands) + "\n" log.info("win_local_acc: bypassing online account requirement at %s...", mount) try: - subprocess.run(["mkdir", "/media/tempwinmnt"], check=True) - subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True) + run_cmd(["sudo", "mkdir", "-p", "/media/tempwinmnt"]) + run_cmd(["sudo", "wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"]) subprocess.run( ["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SOFTWARE"], input=cmd_string, @@ -88,8 +88,8 @@ def win_local_acc(): capture_output=True, check=True, ) - subprocess.run(["wimunmount", "/media/tempwinmnt", "--commit"], check=True) - subprocess.run(["rm", "-rf", "/media/tempwinmnt"], check=True) + run_cmd(["sudo", "wimunmount", "/media/tempwinmnt", "--commit"]) + run_cmd(["sudo", "rm", "-rf", "/media/tempwinmnt"]) log.info("win_local_acc: online account bypass applied successfully.") except subprocess.CalledProcessError as e: log.error("win_local_acc: CalledProcessError: %s", e.stderr) diff --git a/tests/test_find_usb.py b/tests/test_find_usb.py index fd0abe20..4620a516 100644 --- a/tests/test_find_usb.py +++ b/tests/test_find_usb.py @@ -1,5 +1,5 @@ from __future__ import annotations -import subprocess +import os as _real_os import sys from pathlib import Path from types import SimpleNamespace @@ -12,6 +12,56 @@ from lufus.drives import find_usb as find_usb_module +# --------------------------------------------------------------------------- +# Shared pyudev mock helpers +# --------------------------------------------------------------------------- + +_ORIG_STAT = _real_os.stat + + +def _dev_stat(*args, **kwargs): + """Return a fake stat result for /dev/* paths, pass through for everything else.""" + path = args[0] if args else kwargs.get("path", "") + if str(path).startswith("/dev/"): + return SimpleNamespace(st_rdev=0x803, st_size=0, st_mtime=0.0, st_mode=0o660) + return _ORIG_STAT(*args, **kwargs) + + +def _dev_stat_raising(*args, **kwargs): + """Raise PermissionError for /dev/* paths, pass through for everything else.""" + path = args[0] if args else kwargs.get("path", "") + if str(path).startswith("/dev/"): + raise PermissionError("no access to device") + return _ORIG_STAT(*args, **kwargs) + + +def _patch_pyudev_label(monkeypatch, label): + """Patch pyudev so find_usb resolves the given label without real udev.""" + + class FakeDevice: + def get(self, key, default=None): + return label if key == "ID_FS_LABEL" else default + + monkeypatch.setattr(find_usb_module.pyudev, "Context", lambda: SimpleNamespace()) + monkeypatch.setattr(find_usb_module.os, "stat", _dev_stat) + monkeypatch.setattr( + find_usb_module.pyudev.Devices, + "from_device_number", + lambda ctx, kind, rdev: FakeDevice(), + ) + + +def _patch_pyudev_failing(monkeypatch): + """Patch os.stat to fail for device nodes, exercising the label fallback path.""" + monkeypatch.setattr(find_usb_module.pyudev, "Context", lambda: SimpleNamespace()) + monkeypatch.setattr(find_usb_module.os, "stat", _dev_stat_raising) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + def test_find_usb_returns_mount_to_label_mapping(monkeypatch) -> None: user = "testuser" mount_path = f"/media/{user}/MY_USB" @@ -37,17 +87,13 @@ def test_find_usb_returns_mount_to_label_mapping(monkeypatch) -> None: "disk_partitions", lambda *args, **kwargs: [SimpleNamespace(mountpoint=mount_path, device="/dev/sdb1")], ) - monkeypatch.setattr( - find_usb_module.subprocess, - "check_output", - lambda *args, **kwargs: "lufus_USB\n", - ) + _patch_pyudev_label(monkeypatch, "lufus_USB") result = find_usb_module.find_usb() assert result == {mount_path: "lufus_USB"} -def test_find_usb_falls_back_to_dir_name_when_lsblk_fails(monkeypatch) -> None: +def test_find_usb_falls_back_to_dir_name_when_udev_fails(monkeypatch) -> None: user = "testuser" mount_path = f"/media/{user}/NO_LABEL" @@ -72,11 +118,7 @@ def test_find_usb_falls_back_to_dir_name_when_lsblk_fails(monkeypatch) -> None: "disk_partitions", lambda *args, **kwargs: [SimpleNamespace(mountpoint=mount_path, device="/dev/sdc1")], ) - - def raise_lsblk_error(*args, **kwargs): - raise subprocess.CalledProcessError(returncode=1, cmd="lsblk") - - monkeypatch.setattr(find_usb_module.subprocess, "check_output", raise_lsblk_error) + _patch_pyudev_failing(monkeypatch) result = find_usb_module.find_usb() assert result == {mount_path: "NO_LABEL"} diff --git a/tests/test_flash_usb_and_find_usb_fixes.py b/tests/test_flash_usb_and_find_usb_fixes.py index 21816874..73d2a052 100644 --- a/tests/test_flash_usb_and_find_usb_fixes.py +++ b/tests/test_flash_usb_and_find_usb_fixes.py @@ -146,7 +146,10 @@ def wait(self): pass class FakePipe: - def readline(self): + def __iter__(self): + return iter([]) + + def read1(self, size=-1): return b"" monkeypatch.setattr(flash_usb_module.subprocess, "Popen", FakeProcess) @@ -285,7 +288,7 @@ def test_empty_device_node_does_not_overwrite_states_dn(self, monkeypatch): class TestFindUsbHappyPath: - def test_find_usb_returns_label_from_lsblk(self, monkeypatch): + def test_find_usb_returns_label_from_udev(self, monkeypatch): user = "testuser" mount_path = f"/media/{user}/MY_USB" @@ -310,10 +313,19 @@ def test_find_usb_returns_label_from_lsblk(self, monkeypatch): "disk_partitions", lambda *args, **kwargs: [SimpleNamespace(mountpoint=mount_path, device="/dev/sdb1")], ) + + from types import SimpleNamespace as SNS + + class FakeDevice: + def get(self, key, default=None): + return "MY_LABEL" if key == "ID_FS_LABEL" else default + + monkeypatch.setattr(find_usb_module.pyudev, "Context", lambda: SNS()) + monkeypatch.setattr(find_usb_module.os, "stat", lambda path: SNS(st_rdev=0x803)) monkeypatch.setattr( - find_usb_module.subprocess, - "check_output", - lambda *a, **kw: "MY_LABEL\n", + find_usb_module.pyudev.Devices, + "from_device_number", + lambda ctx, kind, rdev: FakeDevice(), ) result = find_usb_module.find_usb() @@ -346,3 +358,148 @@ def test_find_dn_returns_device_node(self, monkeypatch): ) assert find_usb_module.find_device_node() == "/dev/sdd1" + + +# --------------------------------------------------------------------------- +# flash_usb — progress reporting, LC_ALL env, and stderr line filtering +# --------------------------------------------------------------------------- + + +class TestFlashUsbProgress: + """Verify progress_cb milestones, dd stderr parsing, LC_ALL handling, and + filtering of bookkeeping lines vs. unexpected stderr warnings. + """ + + def _fake_popen_factory(self, stderr_lines, popen_calls, envs): + class FakePopen: + class FakeStderr: + def __init__(self, lines): + self._lines = [line.encode("utf-8") for line in lines] + self._index = 0 + + def read1(self, size=-1): + if self._index >= len(self._lines): + return b"" + result = self._lines[self._index] + self._index += 1 + return result + + def __init__(self, cmd, **kwargs): + popen_calls.append((cmd, kwargs)) + envs.append(kwargs.get("env")) + self.pid = 99999 + self.returncode = 0 + self.stderr = FakePopen.FakeStderr(stderr_lines) + + def wait(self): + return self.returncode + + return FakePopen + + def test_progress_cb_receives_milestones_and_scaled_dd_progress(self, monkeypatch): + progress_values = [] + status_messages = [] + + def progress_cb(progress): + progress_values.append(progress) + + def status_cb(message): + status_messages.append(message) + + stderr_lines = [ + "0+0 records in\n", + "0+0 records out\n", + "10% completed\n", + "12345+0 records in\n", + "12345+0 records out\n", + "50% completed\n", + "copied, 1.23 s, 4.56 MB/s\n", + "100% completed\n", + ] + + popen_calls = [] + envs = [] + fake_popen = self._fake_popen_factory(stderr_lines, popen_calls, envs) + monkeypatch.setattr(flash_usb_module.subprocess, "Popen", fake_popen) + monkeypatch.setattr(flash_usb_module, "check_iso_signature", lambda p: True) + monkeypatch.setattr(flash_usb_module, "is_windows_iso", lambda p: False) + monkeypatch.setattr(flash_usb_module, "detect_iso_type", lambda p: flash_usb_module.IsoType.LINUX) + + import os + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".img", delete=False) as f: + f.write(b"\x00" * 100) + iso_path = f.name + + original_environ = dict(os.environ) + try: + flash_usb_module.flash_usb("/dev/sdb", iso_path, progress_cb=progress_cb, status_cb=status_cb) + finally: + os.unlink(iso_path) + + # LC_ALL must be "C" and global env must not be mutated + dd_call = next((c for c in popen_calls if c[0] and c[0][0] == "dd"), None) + assert dd_call is not None, "dd Popen should have been called" + env = dd_call[1].get("env") + assert env is not None + assert env.get("LC_ALL") == "C" + assert dict(os.environ) == original_environ + + # progress_cb should have the early 0 milestone, then scaled values + assert progress_values[0] == 0 + assert progress_values[-1] == 100 + assert sorted(progress_values) == progress_values + + # Status messages should contain the raw dd progress lines + assert any("10% completed" in msg for msg in status_messages) + assert any("50% completed" in msg for msg in status_messages) + assert any("100% completed" in msg for msg in status_messages) + + def test_non_progress_and_unexpected_stderr_lines_handling(self, monkeypatch, caplog): + progress_values = [] + status_messages = [] + + def progress_cb(progress): + progress_values.append(progress) + + def status_cb(message): + status_messages.append(message) + + stderr_lines = [ + "1+0 records in\n", + "1+0 records out\n", + "copied, 1.23 s, 4.56 MB/s\n", + "some unexpected warning from dd\n", + ] + + popen_calls = [] + envs = [] + fake_popen = self._fake_popen_factory(stderr_lines, popen_calls, envs) + monkeypatch.setattr(flash_usb_module.subprocess, "Popen", fake_popen) + monkeypatch.setattr(flash_usb_module, "check_iso_signature", lambda p: True) + monkeypatch.setattr(flash_usb_module, "is_windows_iso", lambda p: False) + monkeypatch.setattr(flash_usb_module, "detect_iso_type", lambda p: flash_usb_module.IsoType.LINUX) + + import os + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".img", delete=False) as f: + f.write(b"\x00" * 100) + iso_path = f.name + + with caplog.at_level("INFO", logger="lufus"): + try: + flash_usb_module.flash_usb("/dev/sdb", iso_path, progress_cb=progress_cb, status_cb=status_cb) + finally: + os.unlink(iso_path) + + # Only the initial 0 milestone should be present; + # bookkeeping lines must not trigger additional progress_cb calls + assert progress_values == [0] + + # Non-progress stderr lines now log at info level + unexpected_logs = [ + record for record in caplog.records if "some unexpected warning from dd" in record.getMessage() + ] + assert unexpected_logs, "Unexpected stderr lines should produce an info log entry" diff --git a/tests/test_get_usb_info.py b/tests/test_get_usb_info.py index 3ba23613..9db86862 100644 --- a/tests/test_get_usb_info.py +++ b/tests/test_get_usb_info.py @@ -1,5 +1,4 @@ from __future__ import annotations -import subprocess import sys from pathlib import Path from types import SimpleNamespace @@ -12,6 +11,55 @@ from lufus.drives import get_usb_info as get_usb_info_module +# --------------------------------------------------------------------------- +# Shared pyudev mock helpers +# --------------------------------------------------------------------------- + + +def _make_fake_stat(rdev=0x803): + return SimpleNamespace(st_rdev=rdev) + + +def _make_fake_device(size_sectors=None, label=""): + """Return a minimal pyudev device stub.""" + + class FakeAttributes: + def get(self, key, default=None): + if key == "size": + return str(size_sectors) if size_sectors is not None else None + return default + + class FakeDevice: + attributes = FakeAttributes() + + def get(self, key, default=None): + if key == "ID_FS_LABEL": + return label or None + return default + + return FakeDevice() + + +def _patch_pyudev(monkeypatch, device_stub): + """Patch pyudev.Context and pyudev.Devices.from_device_number.""" + monkeypatch.setattr(get_usb_info_module.pyudev, "Context", lambda: SimpleNamespace()) + monkeypatch.setattr( + get_usb_info_module.pyudev.Devices, + "from_device_number", + lambda ctx, kind, rdev: device_stub, + ) + monkeypatch.setattr( + get_usb_info_module.os, + "stat", + lambda path: _make_fake_stat(), + ) + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + + def test_get_usb_info_returns_empty_when_mount_not_found(monkeypatch) -> None: monkeypatch.setattr( get_usb_info_module.psutil, @@ -31,15 +79,7 @@ def test_get_usb_info_returns_expected_dictionary(monkeypatch) -> None: "disk_partitions", lambda *args, **kwargs: [SimpleNamespace(mountpoint=mount_path, device=device_node)], ) - - def fake_check_output(cmd, text=True, timeout=5): - if cmd[-2:] == ["SIZE", device_node]: - return str(16 * 1024**3) - if cmd[-2:] == ["LABEL", device_node]: - return "MYUSB\n" - raise AssertionError(f"Unexpected command: {cmd}") - - monkeypatch.setattr(get_usb_info_module.subprocess, "check_output", fake_check_output) + _patch_pyudev(monkeypatch, _make_fake_device(size_sectors=16 * 1024**3 // 512, label="MYUSB")) result = get_usb_info_module.get_usb_info(mount_path) assert result == { @@ -58,21 +98,14 @@ def test_get_usb_info_uses_mount_basename_when_label_is_empty(monkeypatch) -> No "disk_partitions", lambda *args, **kwargs: [SimpleNamespace(mountpoint=mount_path, device=device_node)], ) - - def fake_check_output(cmd, text=True, timeout=5): - if cmd[-2:] == ["SIZE", device_node]: - return str(8 * 1024**3) - if cmd[-2:] == ["LABEL", device_node]: - return "\n" - raise AssertionError(f"Unexpected command: {cmd}") - - monkeypatch.setattr(get_usb_info_module.subprocess, "check_output", fake_check_output) + _patch_pyudev(monkeypatch, _make_fake_device(size_sectors=8 * 1024**3 // 512, label="")) 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: + """get_usb_info must return None when pyudev.Devices.from_device_number raises.""" mount_path = "/media/testuser/USB" device_node = "/dev/sdb1" @@ -81,10 +114,21 @@ def test_get_usb_info_returns_empty_when_lsblk_fails(monkeypatch) -> None: "disk_partitions", lambda *args, **kwargs: [SimpleNamespace(mountpoint=mount_path, device=device_node)], ) + monkeypatch.setattr( + get_usb_info_module.os, + "stat", + lambda path: _make_fake_stat(), + ) + monkeypatch.setattr(get_usb_info_module.pyudev, "Context", lambda: SimpleNamespace()) - def raise_lsblk_error(*args, **kwargs): - raise subprocess.CalledProcessError(returncode=1, cmd="lsblk") + def failing_from_device_number(context, device_type, device_number): + raise RuntimeError("pyudev failure in from_device_number") - monkeypatch.setattr(get_usb_info_module.subprocess, "check_output", raise_lsblk_error) + monkeypatch.setattr( + get_usb_info_module.pyudev.Devices, + "from_device_number", + failing_from_device_number, + ) - assert get_usb_info_module.get_usb_info(mount_path) is None + result = get_usb_info_module.get_usb_info(mount_path) + assert result is None diff --git a/tests/test_get_usb_info_and_detect_windows_fixes.py b/tests/test_get_usb_info_and_detect_windows_fixes.py index 2ab845f2..29e8d1c8 100644 --- a/tests/test_get_usb_info_and_detect_windows_fixes.py +++ b/tests/test_get_usb_info_and_detect_windows_fixes.py @@ -1,5 +1,4 @@ from __future__ import annotations -import subprocess import sys from pathlib import Path from types import SimpleNamespace @@ -19,13 +18,29 @@ def _fake_partitions(mount, device): return lambda all=False: [SimpleNamespace(mountpoint=mount, device=device)] -def _fake_check_output(size="1000000000", label="MY_USB"): - def impl(cmd, **kwargs): - if "SIZE" in cmd: - return size + "\n" - return label + "\n" +def _patch_pyudev(monkeypatch, size_sectors=None, label="MY_USB"): + """Patch pyudev so get_usb_info can run without real udev.""" + from types import SimpleNamespace as SNS - return impl + class FakeAttributes: + def get(self, key, default=None): + if key == "size": + return str(size_sectors) if size_sectors is not None else None + return default + + class FakeDevice: + attributes = FakeAttributes() + + def get(self, key, default=None): + return label if key == "ID_FS_LABEL" else default + + monkeypatch.setattr(gui_module.pyudev, "Context", lambda: SNS()) + monkeypatch.setattr( + gui_module.pyudev.Devices, + "from_device_number", + lambda ctx, kind, rdev: FakeDevice(), + ) + monkeypatch.setattr(gui_module.os, "stat", lambda path: SNS(st_rdev=0x803)) class Testget_usb_infoNormalisedMountPath: @@ -36,14 +51,14 @@ class Testget_usb_infoNormalisedMountPath: def test_trailing_slash_is_stripped(self, monkeypatch): monkeypatch.setattr(gui_module.psutil, "disk_partitions", _fake_partitions("/media/u/USB/", "/dev/sdb1")) - monkeypatch.setattr(gui_module.subprocess, "check_output", _fake_check_output()) + _patch_pyudev(monkeypatch) result = get_usb_info("/media/u/USB/") assert result["mount_path"] == "/media/u/USB" def test_normalised_path_matches_normpath(self, monkeypatch, tmp_path): mount = str(tmp_path) monkeypatch.setattr(gui_module.psutil, "disk_partitions", _fake_partitions(mount, "/dev/sdc1")) - monkeypatch.setattr(gui_module.subprocess, "check_output", _fake_check_output()) + _patch_pyudev(monkeypatch) result = get_usb_info(mount) import os @@ -67,26 +82,33 @@ def fake_dp(all=False): assert calls.get("all") is True -class Testget_usb_infoTimeoutExpired: - """TimeoutExpired was previously swallowed by the broad Exception handler - with a generic message. It must now be caught explicitly. +class Testget_usb_infoPyudevError: + """Any exception from pyudev (permission, device-not-found, etc.) must be + caught and return None rather than propagating to the caller. """ - def test_returns_empty_dict_on_timeout(self, monkeypatch): + def test_returns_none_when_pyudev_raises(self, monkeypatch): + from types import SimpleNamespace as SNS + monkeypatch.setattr(gui_module.psutil, "disk_partitions", _fake_partitions("/media/u/USB", "/dev/sdb1")) + monkeypatch.setattr(gui_module.pyudev, "Context", lambda: SNS()) + monkeypatch.setattr(gui_module.os, "stat", lambda path: SNS(st_rdev=0x803)) - def raise_timeout(*args, **kwargs): - raise subprocess.TimeoutExpired(cmd="lsblk", timeout=5) + def raise_error(ctx, kind, rdev): + raise RuntimeError("simulated pyudev failure") - monkeypatch.setattr(gui_module.subprocess, "check_output", raise_timeout) + monkeypatch.setattr(gui_module.pyudev.Devices, "from_device_number", raise_error) result = get_usb_info("/media/u/USB") assert result is None - def test_timeout_handler_is_explicit(self): - import inspect + def test_returns_none_when_os_stat_raises(self, monkeypatch): + from types import SimpleNamespace as SNS - src = inspect.getsource(get_usb_info) - assert "TimeoutExpired" in src + monkeypatch.setattr(gui_module.psutil, "disk_partitions", _fake_partitions("/media/u/USB", "/dev/sdb1")) + monkeypatch.setattr(gui_module.pyudev, "Context", lambda: SNS()) + monkeypatch.setattr(gui_module.os, "stat", lambda path: (_ for _ in ()).throw(PermissionError("no access"))) + result = get_usb_info("/media/u/USB") + assert result is None class Testget_usb_infoForElse: diff --git a/tests/test_linux_iso_detection.py b/tests/test_linux_iso_detection.py index 4a9cb324..99074ee7 100644 --- a/tests/test_linux_iso_detection.py +++ b/tests/test_linux_iso_detection.py @@ -15,9 +15,13 @@ def test_is_linux_iso_detects_marker(monkeypatch): mock_run = MagicMock() mock_run.return_value = subprocess.CompletedProcess( - args=["7z", "l", "test.iso"], returncode=0, stdout="isolinux/isolinux.cfg\nvmlinuz\ninitrd.img", stderr="" + args=["7z", "l", "test.iso"], + returncode=0, + stdout="isolinux/isolinux.cfg\nisolinux/isolinux.bin", + stderr="", ) monkeypatch.setattr(subprocess, "run", mock_run) + monkeypatch.setattr("lufus.writing.windows.detect._read_pvd_label", MagicMock(return_value="")) assert is_linux_iso("test.iso") is True @@ -27,4 +31,5 @@ def test_is_linux_iso_fails_without_marker(monkeypatch): args=["7z", "l", "test.iso"], returncode=0, stdout="random/file/not/linux.txt", stderr="" ) monkeypatch.setattr(subprocess, "run", mock_run) + monkeypatch.setattr("lufus.writing.windows.detect._read_pvd_label", MagicMock(return_value="")) assert is_linux_iso("test.iso") is False