iso-scalpel/README.md

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.