"""El Torito boot image configuration dialog.""" # This file is part of ISO Scalpel. # Copyright (C) 2025 Jeremy Anderson # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, see # or write to the Free Software Foundation, Inc., 51 Franklin Street, # Fifth Floor, Boston, MA 02110-1301 USA. from __future__ import annotations from PySide6.QtWidgets import ( QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QMessageBox, QPushButton, QSpinBox, QVBoxLayout, QWidget, ) from ..iso_handler import BootInfo, IsoHandler class BootDialog(QDialog): """Configure or clear an El Torito boot record.""" def __init__(self, handler: IsoHandler, parent=None): super().__init__(parent) self.setWindowTitle("Boot Image (El Torito)") self.setMinimumWidth(480) self._handler = handler info = handler.get_boot_info() grp = QGroupBox("Boot Configuration") f = QFormLayout(grp) self.bootable = QCheckBox("Bootable") self.bootable.setChecked(info.bootable) f.addRow(self.bootable) self.boot_file = QLineEdit(info.boot_image_path) self.boot_file.setPlaceholderText("Path inside ISO, e.g. /BOOT.IMG;1") browse = QPushButton("Browse host file…") browse.clicked.connect(self._browse) row = QHBoxLayout() row.addWidget(self.boot_file, 1) row.addWidget(browse) w = QWidget() w.setLayout(row) f.addRow("Boot image:", w) self._local_file: str | None = None self.platform = QComboBox() self.platform.addItems(["0 — x86 (BIOS)", "1 — PowerPC", "2 — Mac", "0xEF — EFI"]) f.addRow("Platform ID:", self.platform) if info.efi: self.platform.setCurrentIndex(3) self.media = QComboBox() # pycdlib accepts: 'noemul', 'floppy', 'hdemul'. Floppy geometry is # conveyed via boot_load_size, so we offer the common sizes as a hint. self._media_items = [ ("noemul (no emulation)", "noemul"), ("floppy 1.2 MiB", "floppy"), ("floppy 1.44 MiB", "floppy"), ("floppy 2.88 MiB", "floppy"), ("hdemul (hard disk)", "hdemul"), ] for label, _val in self._media_items: self.media.addItem(label) cur = {"noemul": 0, "floppy": 1, "hdemul": 4}.get(info.media_name, 0) self.media.setCurrentIndex(cur) f.addRow("Media:", self.media) self.load_seg = QSpinBox() self.load_seg.setRange(0, 0xFFFF) self.load_seg.setValue(info.load_segment or 0x07C0) self.load_seg.setDisplayIntegerBase(16) self.load_seg.setPrefix("0x") f.addRow("Load segment:", self.load_seg) self.load_size = QSpinBox() self.load_size.setRange(0, 100000) self.load_size.setSpecialValueText("auto (whole file)") self.load_size.setValue(info.load_size or 0) f.addRow("Load size (sectors):", self.load_size) self.info_table = QCheckBox("Patch boot-info-table (common for ISOLINUX)") self.info_table.setChecked(info.boot_info_table) f.addRow(self.info_table) # clear button self.clear_btn = QPushButton("Remove boot record") self.clear_btn.clicked.connect(self._clear) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self._apply) buttons.rejected.connect(self.reject) layout = QVBoxLayout(self) layout.addWidget(grp) layout.addWidget(QLabel("Tip: the boot image file is added to the ISO automatically " "when you select a host file.")) layout.addStretch(1) bl = QHBoxLayout() bl.addWidget(self.clear_btn) bl.addStretch(1) bl.addWidget(buttons) layout.addLayout(bl) # ------------------------------------------------------------------ handlers def _browse(self) -> None: path, _ = QFileDialog.getOpenFileName(self, "Select boot image") if path: self._local_file = path self.boot_file.setText(path) def _clear(self) -> None: self._handler.clear_boot() QMessageBox.information(self, "Boot", "Boot record removed. Save the image to persist.") self.accept() def _apply(self) -> None: info = self._collect() try: self._handler.set_boot(info, boot_file_local=self._local_file) except Exception as exc: # noqa: BLE001 -- user-facing error boundary QMessageBox.critical(self, "Boot", f"Failed to set boot record:\n{exc}") return self.accept() def _collect(self) -> BootInfo: info = BootInfo() info.bootable = self.bootable.isChecked() info.boot_image_path = self.boot_file.text().strip() info.platform_id = int(self.platform.currentText().split(" ", 1)[0], 0) info.media_name = self._media_items[self.media.currentIndex()][1] info.load_segment = self.load_seg.value() info.load_size = self.load_size.value() or None info.boot_info_table = self.info_table.isChecked() info.efi = info.platform_id == 0xEF info.boot_catalog_path = "BOOT.CAT;1" return info