246 lines
7.0 KiB
Markdown
Executable File
246 lines
7.0 KiB
Markdown
Executable File
# 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 gui or cli.
|
|
|
|
**Author:** Jeremy Anderson · <info@dcos.net> · <https://dcos.net>
|
|
**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 -- <reason>` 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 <info@dcos.net>.
|
|
|
|
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.
|