# ISO Scalpel A disc-image editor for ISO 9660 and UDF, built with PySide6 (Qt 6) and [pycdlib](https://github.com/clalancette/pycdlib). Runs as a graphical application or from the command line. **Author:** Jeremy Anderson · · **License:** GPL-2.0 --- ## Features - ISO 9660 interchange levels 1, 2, 3 - Rock Ridge 1.09 / 1.12 - Joliet 1 / 2 / 3 - UDF 2.50 / 2.60 - El Torito boot images (BIOS and EFI) - Add, extract, rename, delete files and folders - Volume metadata editing - Filesystem diff between two images - Command-line interface - Split-navigation GUI with tabs, breadcrumbs, and filtering ## Requirements - Python 3.10+ - PySide6 >= 6.6 - pycdlib >= 1.13 On a minimal Linux install you also need the Qt runtime libraries: ```bash # Debian / Ubuntu sudo apt install libegl1 libgl1 libglib2.0-0 libfontconfig1 \ libdbus-1-3 libxkbcommon0 libxcb-cursor0 ``` ## Install ```bash tar xzf iso-scalpel-1.1.0.tar.gz cd iso_scalpel_py python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` ## Running the GUI ```bash python main.py # empty window python main.py my_image.iso # open an image ``` The interface is a split-nav file manager: two panes side by side, each with breadcrumb navigation, back/forward/up history, a live filter box, and tabs. Either pane can host the filesystem or the ISO image; press `Ctrl+Shift+X` to swap them. ## Running the CLI ```bash python cli.py --help ``` Commands: | Command | Description | |---------|-------------| | `new` | Create a new ISO image | | `list` | List the contents of an image | | `info` | Show volume metadata | | `add` | Add a file or directory to an image | | `extract` | Extract a file or directory | | `rm` | Remove an entry | | `boot` | Show or configure El Torito boot | | `diff` | Compare the filesystems of two images | | `gui` | Launch the graphical interface | Examples: ```bash # create a UDF image with a file in it python cli.py new disc.iso -l MYDISC --joliet 3 --rock-ridge 1.09 --udf 2.60 \ --add readme.txt # list contents python cli.py list disc.iso python cli.py list disc.iso / --view udf # show metadata python cli.py info disc.iso # add a directory tree python cli.py add disc.iso ./myfolder /sub # extract a file python cli.py extract disc.iso /readme.txt ./out.txt # remove an entry python cli.py rm disc.iso /old.txt # compare two images (filesystem diff) python cli.py diff old.iso new.iso python cli.py diff old.iso new.iso --all ``` The `diff` command follows the POSIX `diff(1)` exit-code convention: `0` when the images are identical, `1` when they differ, `2` on error. This makes it scriptable in a shell `if` / `&&` pipeline. ## The diff view `diff` compares two images at the filesystem level — which entries were added, removed, or modified — without comparing file contents. The GUI exposes the same comparison under **Tools → Compare Images…** (`Ctrl+D`), showing a unified tree with status indicators: - `+` added (only in B) - `-` removed (only in A) - `M` modified (in both, different size or date) - `=` unchanged Output example: ``` --- old.iso +++ new.iso 2 added, 1 removed, 1 modified, 14 unchanged + /newfile.txt (2048 bytes) - /oldfile.txt (1024 bytes) M /readme.txt (12 -> 48 bytes) (14 entries unchanged) ``` ## Keyboard shortcuts | Shortcut | Action | |----------|--------| | `Ctrl+N` | New image | | `Ctrl+O` | Open image | | `Ctrl+S` | Save | | `Ctrl+Shift+S` | Save As… | | `Ctrl+W` | Close image / close tab | | `Ctrl+Q` | Quit | | `Alt+←` / `Alt+→` | Back / forward | | `Alt+↑` | Up to parent | | `Ctrl+L` | Go to location | | `Ctrl+Shift+X` | Swap panes | | `Ctrl+T` | New tab | | `Insert` | Add to image | | `Ctrl+E` | Extract | | `Ctrl+Shift+N` | New folder | | `F2` | Rename | | `Delete` | Delete | | `Ctrl+F` | Filter | | `Ctrl+A` | Select all | | `Alt+Enter` | Properties | | `Ctrl+B` | Boot image | | `Ctrl+D` | Compare images | | `F5` | Refresh | | `Ctrl+,` | Preferences | ## Project layout ``` iso_scalpel_py/ ├── main.py # GUI entry point ├── cli.py # CLI entry point ├── requirements.txt ├── README.md ├── QUICKSTART.md ├── BLOG.md ├── LICENSE └── iso_scalpel/ ├── __init__.py # version metadata ├── app.py # QApplication + stylesheet ├── main_window.py # split-nav window, menus, toolbar ├── iso_handler.py # pycdlib wrapper (no Qt dependency) ├── iso_record.py # record abstraction ├── iso_model.py # Qt tree model ├── diff.py # filesystem diff engine (no Qt dependency) ├── config.py # settings persistence ├── dialogs/ # New, Properties, Boot, Extract, Diff, Settings, About └── widgets/ # filesystem + ISO panes ``` `iso_handler.py` and `diff.py` are GUI-agnostic. They never import PySide6, so the entire engine can be driven from a script or test harness with no display attached. ## Configuration Settings are stored as JSON: - Linux: `~/.config/iso-scalpel/settings.json` - macOS: `~/Library/Application Support/iso-scalpel/settings.json` - Windows: `%APPDATA%\iso-scalpel\settings.json` ## Coding standards The codebase is held to a fixed set of rules enforced by [`ruff`](https://docs.astral.sh/ruff/) (configured in `pyproject.toml`): - **PEP 8** style and **PEP 585 / 604** annotations (`list[str]`, `str | None`). - **SEI CERT** error handling: no blind `except Exception` and no silent `try`/`except`/`pass`. pycdlib calls catch a narrow tuple (`PyCdlibException`, `OSError`, `ValueError`, `KeyError`); top-level CLI and GUI error boundaries opt out per-occurrence with an auditable `# noqa: BLE001 -- ` comment. - **MISRA-aligned** immutability: no mutable default arguments, no function calls in defaults (`parent=QModelIndex()` uses a module-level singleton). - **POSIX** discipline: entry-point scripts (`main.py`, `cli.py`) carry a shebang on line 1 and the executable bit; `cli.py diff` exits `0`/`1`/`2` per `diff(1)`. - **Dispatch tables over if/elif chains** for status, name-type, and flag parsing (`_FLAG_TABLE`, `_PATH_KWARG`, `_NAME_TYPE_PROBES`, `_PKG_MANAGERS`, `VolumeProperties.extensions`). - **Comprehensions and `next()` over explicit loops** where the body is a pure filter/map (`list_dir`, `available_name_types`, `from_pycdlib` modified-time probe, `_human_size`). - **Step-down / guard clauses** at choice forks: early `return` on the unhappy path, main logic at the top indentation level. Run the checks with: ```bash pip install -e '.[dev]' # installs pytest + ruff ruff check . # lint (must be clean) python -m pytest # test suite ``` ## License Copyright © 2025 Jeremy Anderson . GPL-2.0. See [LICENSE](LICENSE) for the full text. This program comes with ABSOLUTELY NO WARRANTY. ISO Scalpel is an independent project. No source code from any other ISO editing tool was used.