Skip to content

Commit 76b22b5

Browse files
committed
h
1 parent bd0ad48 commit 76b22b5

7 files changed

Lines changed: 131 additions & 54 deletions

File tree

renovate.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
3+
"extends": [
4+
"config:recommended"
5+
],
6+
"schedule": ["every weekend"],
7+
"packageRules": [
8+
{
9+
"matchUpdateTypes": ["patch"],
10+
"automerge": true,
11+
"automergeType": "pr"
12+
},
13+
{
14+
"matchUpdateTypes": ["major"],
15+
"dependencyDashboardApproval": true,
16+
"labels": ["dependencies", "major-update"]
17+
},
18+
{
19+
"matchUpdateTypes": ["minor"],
20+
"groupName": "minor dependencies",
21+
"labels": ["dependencies"]
22+
},
23+
{
24+
"matchDepTypes": ["action"],
25+
"groupName": "GitHub Actions",
26+
"pinDigests": true
27+
}
28+
],
29+
"labels": ["dependencies"],
30+
"prCreation": "not-pending",
31+
"rebaseWhen": "behind-base-branch"
32+
}

src/lufus/drives/formatting.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@ def unmount(drive: str = None) -> bool:
6060
log.info("Unmounting %s...", drive)
6161
for target in targets:
6262
try:
63-
result = subprocess.run(["umount", "-l", target])
64-
# exit 32 = "not mounted" on Linux — acceptable when clearing a drive
65-
if result.returncode not in (0, 32):
66-
log.error("umount -l %s exited with code %d", target, result.returncode)
67-
unmount_fail()
68-
return False
63+
subprocess.run(["umount", "-l", target], check=True)
6964
time.sleep(0.5)
7065
log.info("Unmounted %s successfully.", target)
66+
except subprocess.CalledProcessError as e:
67+
# exit 32 = "not mounted" on Linux — acceptable when clearing a drive
68+
if e.returncode == 32:
69+
log.info("Unmounted %s (was already unmounted).", target)
70+
continue
71+
log.error("umount -l %s exited with code %d", target, e.returncode)
72+
unmount_fail()
73+
return False
7174
except Exception as e:
7275
log.error("(UMNTFUNC) Unexpected error type: %s — %s", type(e).__name__, e)
7376
log_unexpected_error()

src/lufus/gui/gui.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1687,9 +1687,7 @@ def get_latest_release(self):
16871687
if is_newer:
16881688
self.log_message(f"New version found: {tag_name} > {current_version}", level="DEBUG")
16891689
else:
1690-
self.log_message(
1691-
f"Running latest release build: {tag_name} <= {current_version}", level="INFO"
1692-
)
1690+
self.log_message(f"Running latest release build: {tag_name} <= {current_version}", level="INFO")
16931691
return
16941692
else:
16951693
self.log_message(f"Couldn't get latest release, response: {req.status}", level="WARNING")

src/lufus/writing/flash_usb.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import subprocess
55
from lufus.utils import strip_partition_suffix
66
from lufus.writing.check_file_sig import check_iso_signature
7-
from lufus.writing.windows.detect import detect_iso_type, IsoType
7+
from lufus.writing.windows.detect import detect_iso_type, IsoType, is_windows_iso
88
from lufus.writing.windows.flash import flash_windows
99
from lufus.lufus_logging import get_logger
1010
from lufus.writing.partition_scheme import PartitionScheme
@@ -33,18 +33,21 @@ def _status(msg: str) -> None:
3333

3434
_status(f"flash_usb called: iso={iso_path}, device={device}")
3535

36-
# Validate device path before any operation — prevents accidental writes to
37-
# system disks if a bad options file or UI bug passes a wrong path.
38-
if not re.match(r"^/dev/(sd[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+)$", device):
39-
log.error("flash_usb: invalid device path %r — aborting", device)
40-
_status(f"Flash aborted: invalid device path {device!r}")
41-
return False
42-
36+
# Strip any partition suffix first (e.g. /dev/nvme0n1p1 -> /dev/nvme0n1,
37+
# /dev/sdb1 -> /dev/sdb) so that validation operates on the whole-disk path.
4338
original_device = device
4439
device = strip_partition_suffix(device)
4540
if device != original_device:
4641
_status(f"Stripped partition suffix: {original_device} -> {device}")
4742

43+
# Validate the (already-stripped) device path before any operation —
44+
# prevents accidental writes to system disks if a bad options file or
45+
# UI bug passes a wrong path.
46+
if not re.match(r"^/dev/(sd[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+)$", device):
47+
log.error("flash_usb: invalid device path %r — aborting", device)
48+
_status(f"Flash aborted: invalid device path {device!r}")
49+
return False
50+
4851
try:
4952
iso_size = os.path.getsize(iso_path)
5053
_status(f"File size: {iso_size:,} bytes ({iso_size / (1024**3):.2f} GiB)")
@@ -60,8 +63,7 @@ def _status(msg: str) -> None:
6063
_status(f"Not an ISO file ({os.path.basename(iso_path)}), skipping ISO signature check")
6164

6265
_status("Checking if image contains installation markers...")
63-
iso_type = detect_iso_type(iso_path)
64-
if iso_type == IsoType.WINDOWS:
66+
if is_windows_iso(iso_path):
6567
_status("Windows Installation media detected, routing to flash_windows (ISO mode)")
6668
return flash_windows(
6769
device,
@@ -70,7 +72,9 @@ def _status(msg: str) -> None:
7072
progress_cb=progress_cb,
7173
status_cb=status_cb,
7274
)
73-
elif iso_type == IsoType.LINUX:
75+
76+
iso_type = detect_iso_type(iso_path)
77+
if iso_type == IsoType.LINUX:
7478
_status("Linux Installation media detected, will use dd for flashing")
7579
else:
7680
_status("Generic or unknown image, will use dd for flashing")

src/lufus/writing/install_ventoy.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,16 @@ def download_wimboot(dest_path: str) -> bool:
5656
if _WIMBOOT_SHA256:
5757
if not _verify_sha256(dest_path, _WIMBOOT_SHA256):
5858
os.unlink(dest_path)
59-
print("ERROR: wimboot SHA-256 verification failed — file deleted. "
60-
"Check the download source or update _WIMBOOT_SHA256.")
59+
print(
60+
"ERROR: wimboot SHA-256 verification failed — file deleted. "
61+
"Check the download source or update _WIMBOOT_SHA256."
62+
)
6163
return False
6264
else:
63-
print("WARNING: wimboot downloaded without hash verification. "
64-
"Set _WIMBOOT_SHA256 before shipping to production.")
65+
print(
66+
"WARNING: wimboot downloaded without hash verification. "
67+
"Set _WIMBOOT_SHA256 before shipping to production."
68+
)
6569

6670
print("wimboot downloaded successfully.")
6771
return True
@@ -129,15 +133,20 @@ def install_grub(target_device: str) -> bool:
129133
for partition in glob.glob(f"{target_device}*"):
130134
subprocess.run(["umount", partition], check=False)
131135

136+
# Partition separator: NVMe and MMC block devices use 'p' between the
137+
# disk name and the partition number (e.g. /dev/nvme0n1p1, /dev/mmcblk0p1).
138+
# Standard SCSI/SATA/USB drives use no separator (e.g. /dev/sdb1).
139+
sep = "p" if re.search(r"(nvme\d+n\d+|mmcblk\d+)$", target_device) else ""
140+
132141
# Partitioning Definition
133142
sfdisk_input = f"""
134143
label: gpt
135144
device: {target_device}
136145
unit: sectors
137146
138-
{target_device}1 : start=2048, size=2048, type=21686148-6449-6E6F-7444-6961676F6E61
139-
{target_device}2 : start=4096, size=204800, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
140-
{target_device}3 : start=208896, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
147+
{target_device}{sep}1 : start=2048, size=2048, type=21686148-6449-6E6F-7444-6961676F6E61
148+
{target_device}{sep}2 : start=4096, size=204800, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
149+
{target_device}{sep}3 : start=208896, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
141150
"""
142151

143152
# Use unique temp dirs instead of hardcoded /tmp paths to avoid stale-mount collisions.
@@ -156,8 +165,8 @@ def install_grub(target_device: str) -> bool:
156165
subprocess.run(["sync"], check=True)
157166

158167
# Wait for device nodes to be created by udev
159-
efi_part = f"{target_device}2"
160-
data_part = f"{target_device}3"
168+
efi_part = f"{target_device}{sep}2"
169+
data_part = f"{target_device}{sep}3"
161170
for _ in range(10):
162171
if os.path.exists(data_part):
163172
break

src/lufus/writing/windows/detect.py

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@
2727
# IsoType enum
2828
# ---------------------------------------------------------------------------
2929

30+
3031
class IsoType(str, Enum):
3132
WINDOWS = "windows"
32-
LINUX = "linux"
33-
OTHER = "other"
33+
LINUX = "linux"
34+
OTHER = "other"
3435

3536

3637
# ---------------------------------------------------------------------------
@@ -40,11 +41,11 @@ class IsoType(str, Enum):
4041
# ISO 9660: sector 16 = byte 32768. Within the PVD:
4142
# byte 1– 5 : Standard Identifier "CD001"
4243
# byte 40–71 : Volume Identifier (32 a-characters)
43-
_PVD_OFFSET = 16 * 2048 # 32768
44-
_PVD_MAGIC_OFFSET = _PVD_OFFSET + 1 # where "CD001" lives
45-
_PVD_MAGIC = b"CD001"
46-
_PVD_LABEL_OFFSET = _PVD_OFFSET + 40
47-
_PVD_LABEL_SIZE = 32
44+
_PVD_OFFSET = 16 * 2048 # 32768
45+
_PVD_MAGIC_OFFSET = _PVD_OFFSET + 1 # where "CD001" lives
46+
_PVD_MAGIC = b"CD001"
47+
_PVD_LABEL_OFFSET = _PVD_OFFSET + 40
48+
_PVD_LABEL_SIZE = 32
4849

4950

5051
def _read_pvd_label(iso_path: str) -> str:
@@ -63,16 +64,49 @@ def _read_pvd_label(iso_path: str) -> str:
6364
return ""
6465

6566

67+
def _read_iso_label(iso_path: str) -> str:
68+
"""Read the ISO 9660 volume label at the fixed sector-16 offset.
69+
70+
Unlike _read_pvd_label this does not check the CD001 magic, making it
71+
useful for unit tests that write a minimal label-only fixture. Returns
72+
an empty string on OSError (e.g. missing file) or when the file is too
73+
small to contain a label.
74+
"""
75+
try:
76+
with open(iso_path, "rb") as f:
77+
f.seek(_PVD_LABEL_OFFSET)
78+
raw = f.read(_PVD_LABEL_SIZE)
79+
if len(raw) < _PVD_LABEL_SIZE:
80+
return ""
81+
return raw.decode("ascii", errors="replace").strip()
82+
except OSError as e:
83+
log.error("detect: cannot read label from %s: %s", iso_path, e)
84+
return ""
85+
86+
87+
def _label_is_windows(label: str) -> bool:
88+
"""Return True if *label* matches a known Windows ISO volume identifier.
89+
90+
Uses the pre-compiled _WIN_LABEL_RE regex. Any label beginning with
91+
"WIN" already covers all "WINDOWS…" variants, so no redundant prefix
92+
check is needed.
93+
"""
94+
return bool(_WIN_LABEL_RE.match(label))
95+
96+
6697
# ---------------------------------------------------------------------------
6798
# File-listing helpers — 7z first, isoinfo as fallback
6899
# ---------------------------------------------------------------------------
69100

101+
70102
def _list_with_7z(iso_path: str) -> "str | None":
71103
"""Return the lowercased 7z file listing, or None on failure / not installed."""
72104
try:
73105
r = subprocess.run(
74106
["7z", "l", iso_path],
75-
capture_output=True, text=True, timeout=30,
107+
capture_output=True,
108+
text=True,
109+
timeout=30,
76110
)
77111
if r.returncode == 0:
78112
return r.stdout.lower()
@@ -98,13 +132,15 @@ def _list_with_isoinfo(iso_path: str) -> "str | None":
98132
# If the ISO has no RR, isoinfo falls back to ISO 9660 names gracefully.
99133
r = subprocess.run(
100134
["isoinfo", "-f", "-R", "-i", iso_path],
101-
capture_output=True, text=True, timeout=30,
135+
capture_output=True,
136+
text=True,
137+
timeout=30,
102138
)
103139
if r.returncode == 0:
104140
lines = []
105141
for line in r.stdout.splitlines():
106142
line = line.strip().lstrip("/").lower()
107-
line = re.sub(r";[0-9]+$", "", line) # strip ;1 version suffix
143+
line = re.sub(r";[0-9]+$", "", line) # strip ;1 version suffix
108144
lines.append(line)
109145
return "\n".join(lines)
110146
log.warning("detect: isoinfo exited %d", r.returncode)
@@ -139,10 +175,10 @@ def _get_file_listing(iso_path: str) -> "str | None":
139175
# Files that exist ONLY in Windows installation media.
140176
# NOTE: EFI directories are deliberately absent — Windows ISOs also have efi/boot/.
141177
_WIN_FILE_MARKERS = [
142-
"sources/install.wim", # Windows setup image (retail / OEM)
143-
"sources/install.esd", # Windows setup image (ESD download)
144-
"sources/install.swm", # Split setup image (multi-disc)
145-
"sources/boot.wim", # Windows PE boot image
178+
"sources/install.wim", # Windows setup image (retail / OEM)
179+
"sources/install.esd", # Windows setup image (ESD download)
180+
"sources/install.swm", # Split setup image (multi-disc)
181+
"sources/boot.wim", # Windows PE boot image
146182
]
147183

148184

@@ -177,34 +213,27 @@ def _get_file_listing(iso_path: str) -> "str | None":
177213
"isolinux/isolinux.cfg",
178214
"syslinux/syslinux.cfg",
179215
"syslinux/ldlinux.c32",
180-
181216
# ---- GRUB config files — Linux-only for optical/USB media ----
182217
# Windows uses BCD / bootmgr; it never ships grub.cfg on install media.
183218
"boot/grub/grub.cfg",
184219
"boot/grub/i386-pc/",
185220
"grub/grub.cfg",
186-
187221
# ---- Ubuntu / Kubuntu / Xubuntu / Mint (Casper live system) ----
188222
"casper/filesystem.squashfs",
189223
"casper/filesystem.manifest",
190224
"casper/vmlinuz",
191-
192225
# ---- Debian / Kali / Tails / Parrot (live-boot) ----
193226
"live/filesystem.squashfs",
194227
"live/filesystem.manifest",
195228
"live/vmlinuz",
196-
197229
# ---- Ubuntu / Debian installer marker ----
198230
".disk/info",
199-
200231
# ---- Arch Linux (specific sub-paths, not the broad "arch/" directory) ----
201232
"arch/pkglist.x86_64.txt",
202233
"arch/boot/x86_64/vmlinuz-linux",
203-
204234
# ---- Fedora / RHEL / CentOS installer (Anaconda) ----
205235
"images/pxeboot/vmlinuz",
206236
".discinfo",
207-
208237
# ---- Generic kernel presence under known Linux-only directories ----
209238
"boot/vmlinuz",
210239
"boot/bzimage",
@@ -215,6 +244,7 @@ def _get_file_listing(iso_path: str) -> "str | None":
215244
# Main detection entry point
216245
# ---------------------------------------------------------------------------
217246

247+
218248
def detect_iso_type(iso_path: str) -> IsoType:
219249
"""Detect the OS family of an ISO image.
220250
@@ -274,6 +304,7 @@ def detect_iso_type(iso_path: str) -> IsoType:
274304
# Backward-compatible wrappers
275305
# ---------------------------------------------------------------------------
276306

307+
277308
def is_windows_iso(iso_path: str) -> bool:
278309
"""Return True if iso_path is a Windows installation image."""
279310
return detect_iso_type(iso_path) == IsoType.WINDOWS

src/lufus/writing/windows/flash.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,7 @@ def _get_disk_size_sectors(drive: str) -> int:
550550
def _verify_sha256(path: str, expected: str) -> bool:
551551
"""Return True if the SHA-256 of *path* matches *expected* (hex, case-insensitive)."""
552552
import hashlib
553+
553554
h = hashlib.sha256()
554555
with open(path, "rb") as f:
555556
for chunk in iter(lambda: f.read(65536), b""):
@@ -570,8 +571,7 @@ def find_uefi_ntfs_img(status_cb=None) -> str:
570571
if _UEFI_NTFS_SHA256:
571572
if not _verify_sha256(candidate, _UEFI_NTFS_SHA256):
572573
raise FileNotFoundError(
573-
f"uefi-ntfs.img failed SHA-256 verification. "
574-
f"Re-bundle the file and update _UEFI_NTFS_SHA256."
574+
f"uefi-ntfs.img failed SHA-256 verification. Re-bundle the file and update _UEFI_NTFS_SHA256."
575575
)
576576
return candidate
577577

@@ -580,12 +580,12 @@ def find_uefi_ntfs_img(status_cb=None) -> str:
580580

581581
if not _UEFI_NTFS_SHA256:
582582
log.warning(
583-
"Downloading uefi-ntfs.img without a pinned hash — "
584-
"set _UEFI_NTFS_SHA256 before shipping to production."
583+
"Downloading uefi-ntfs.img without a pinned hash — set _UEFI_NTFS_SHA256 before shipping to production."
585584
)
586585

587586
try:
588587
import urllib.request
588+
589589
urllib.request.urlretrieve(UEFI_NTFS_URL, candidate)
590590
if status_cb:
591591
status_cb(f"Downloaded uefi-ntfs.img to {candidate}")

0 commit comments

Comments
 (0)