Mount a folder of CHD images and expose them as read-only .iso/.bin files via FUSE.
Designed for PS2 (OPL over SMB/UDPBD) and NAS setups where you want CHD space savings but still present ISO-style files to clients. Presents .chd images as .iso (2048-byte Mode1/Mode2-Form1) or .bin (2324-byte Mode2-Form2, optional) on the fly.
Why? Store space-saving CHDs on your NAS, but expose a plain ISO/BIN view for devices that expect uncompressed images (e.g. a real PS2 over SMB/UDPBD or OPL). Works great with RetroNAS.
- 🧰 DVD/2048 passthrough — CHDs from 2048-byte/sector DVDs are presented directly as
.isowith zero-copy. - 💿 CD/2352 payload extraction — For CD CHDs:
- 2048-byte sectors (Mode1 / Mode2-Form1) → exposed as
.iso. - 2324-byte sectors (Mode2-Form2 XA video/audio) → exposed as
.binwhen enabled.
- 2048-byte sectors (Mode1 / Mode2-Form1) → exposed as
- 🧪 Pragmatic fallback — If no DVD/CD metadata is found, safely falls back to raw 2048 passthrough where valid.
- ⚡ LRU cache — Tunable by entry count or memory cap for fast hunk/frame access.
- 🔒 Read-only — No writes, no temp files; streams directly from CHD.
- 🧭 RetroNAS-friendly — Keep
…/playstation2/chdas source, mount at…/playstation2/isofor symlink compatibility. - 🌐 Network-friendly — Works over SMB and UDPBD for PS2 OPL game streaming.
💡 Typical layout:
/mnt/retronas/roms/sony/playstation2/chd # source CHDs (real files) /mnt/retronas/roms/sony/playstation2/iso # FUSE mountpoint (exposed files)
- Linux with FUSE (Debian/Ubuntu:
fuse3,fusermount3). - Runtime permissions:
- read access to your CHDs and permission to mount FUSE FS.
- if using
--allow-other, ensure/etc/fuse.confcontainsuser_allow_other.
- To build from source you need a Rust toolchain (stable).
This project uses tag-driven releases. The Git tag is the single source of truth for the version. On tagged builds, CI updates all versioned artifacts and publishes a GitHub Release with Debian packages.
- Create a tag:
git switch main git pull --rebase export VER=0.4.2 git tag "v$VER" -m "chd2iso-fuse v$VER" git push origin "v$VER"
- GitHub Actions will:
- Set
Cargo.toml→version = "$VER"(no cargo-edit; viascripts/set-cargo-version.sh) - Update
debian/changelog→${VER}-1(viagbp dch/dch) - Regenerate
CHANGELOG.mdfor the tag (viagit-cliff) - Build Debian packages with
debuild - Upload artifacts to the GitHub Release
- Open a PR to commit
Cargo.toml,debian/changelog, andCHANGELOG.mdback tomain
- Set
- Cargo.toml: crate version =
X.Y.Z(matchesvX.Y.Ztag) - debian/changelog: package version =
X.Y.Z-1 - CHANGELOG.md: generated for the tag with
git-cliff
The CI publishes:
chd2iso-fuse_*_amd64.debchd2iso-fuse-dbgsym_*_amd64.deb*.buildinfo,*.changesCHANGELOG.md(as a release attachment)RELEASE_NOTES.md(used for the release body)
You can preview the version sync locally (no build):
export VER=0.4.2
scripts/set-cargo-version.sh "$VER"
scripts/gen-debian-changelog.sh "v$VER" trixie
git-cliff --tag "v$VER" -o CHANGELOG.md- Don’t manually edit
Cargo.tomlordebian/changelogfor a release. - Just create a new tag (
vX.Y.Z) and push — CI takes care of the rest. - For pre-releases, tags like
v0.5.0-rc.1are supported; Debian version becomes0.5.0-rc.1-1.
- Mismatch errors: CI verifies that
Cargo.tomlanddebian/changelogmatch the tag. If it fails, check the CI logs for the “Verify … matches tag” steps. - Missing
Cargo.lock: the build fails ifCargo.lockisn’t committed. - Debian build files missing: CI enforces that files containing
_X.Y.Z-1_exist post-build; reviewdebian/changelogand the build logs if this guard trips.
make
sudo make installInstalls to /usr/local/bin/chd2iso-fuse by default. Override PREFIX if needed, e.g. make PREFIX=/usr make install.
make deb
sudo apt install ../chd2iso-fuse_*.debThe .deb also installs a mount helper (/sbin/mount.chd2iso-fuse) and optional systemd units.
# prepare dirs
sudo mkdir -p /path/to/chd /path/to/iso
# run in foreground for a quick test
sudo chd2iso-fuse --source /path/to/chd --mount /path/to/iso --allow-other
# in another shell
ls /path/to/iso--source <DIR> # CHD source directory
--mount <DIR> # FUSE mountpoint
--allow-other # allow other users (requires fuse.conf: user_allow_other)
--cd-allow-form2 # expose Mode2/Form2 as 2324-byte .bin files
--cache-hunks <N> # cache N CHD hunks/frames
--cache-bytes <BYTES> # global cache limit in bytes
--verbose # info-level logging; otherwise warn+
Run chd2iso-fuse --help for full usage.
You can use either the instance service template or classic .mount/.automount units.
- Create a config file at
/etc/chd2iso-fuse/<name>.conf. Example:
SOURCE=/mnt/retronas/roms/sony/playstation2/chd
TARGET=/mnt/retronas/roms/sony/playstation2/iso
ALLOW_OTHER=yes
CD_ALLOW_FORM2=no
CACHE_HUNKS=512
CACHE_BYTES=536870912
VERBOSE=yes- Enable:
sudo systemctl daemon-reload
sudo systemctl enable --now chd2iso-fuse@<name>.service/etc/systemd/system/mnt-retronas-roms-sony-playstation2-iso.mount
[Unit]
Description=Mount CHD→ISO PS2
RequiresMountsFor=/mnt/retronas/roms/sony/playstation2/chd
After=remote-fs.target
[Mount]
What=/mnt/retronas/roms/sony/playstation2/chd
Where=/mnt/retronas/roms/sony/playstation2/iso
Type=chd2iso-fuse
Options=allow_other,cache_hunks=512,cache_bytes=536870912
TimeoutSec=30
[Install]
WantedBy=multi-user.target/etc/systemd/system/mnt-retronas-roms-sony-playstation2-iso.automount
[Unit]
Description=Automount CHD→ISO PS2
[Automount]
Where=/mnt/retronas/roms/sony/playstation2/iso
[Install]
WantedBy=multi-user.targetEnable:
sudo systemctl daemon-reload
sudo systemctl enable --now mnt-retronas-roms-sony-playstation2-iso.automount
# first access triggers the mount
ls /mnt/retronas/roms/sony/playstation2/isoUnit filenames must match the
Where=path (slashes → dashes).
With the mount helper installed (/sbin/mount.chd2iso-fuse), you can use:
/mnt/retronas/roms/sony/playstation2/chd /mnt/retronas/roms/sony/playstation2/iso chd2iso-fuse allow_other,cache_hunks=512,cache_bytes=536870912,x-systemd.automount,x-systemd.idle-timeout=60s,nofail 0 0
Then:
sudo systemctl daemon-reload
sudo mount -a- DVD titles (most PS2 games): prefer
chdman createdvdwith a raw 2048 ISO:chdman createdvd -i game.iso -o game.chd
- CD titles:
chdman createcdfrom a proper CD dump (.cue/.bin):chdman createcd -i game.cue -o game.chd
- If your tooling doesn’t have
createdvd, fallback:chdman createraw -i game.iso -o game.chd
Verify a quick slice:
dd if=/path/to/iso/game.iso bs=1M count=16 | md5sum dd if=/path/to/mount/game.iso bs=1M count=16 | md5sum # should match for DVD/2048 (Mode1)Form2
.binwill NOT match a 2048-byte.iso(different payload size).
- Cache bytes: set to ~5–20% of RAM for big libraries. Example 1 GiB:
--cache-bytes 1073741824. - Cache hunks: leave default or match your typical CHD hunk size.
- Network: large read sizes help over SMB. UDPBD works well, too.
- Shell “hangs” when mounting: expected when you run the binary directly—it stays in foreground. Use systemd units or the mount helper (which backgrounds).
- Permission denied / empty dir: ensure
/etc/fuse.confhasuser_allow_otherand you passed--allow-other. - Automount “bad unit name”: unit filenames must match
Where=path; slashes → dashes. - Logs:
journalctl -u chd2iso-fuse@<name> -eor the.mountunit you created. - Form2 content missing: enable with
--cd-allow-form2(CLI) orcd_allow_form2in unitOptions=.
- Build:
make - Install:
sudo make install - Package:
make deb(produces../chd2iso-fuse_*.deb)
PRs welcome! Please include a brief description, test notes, and update docs for behavior changes.
This project is licensed under the MIT License.
This project is built and linted automatically using GitHub Actions.
CI runs inside a Debian Trixie container to ensure the generated .deb packages
match the target distribution (e.g., RetroNAS).
- On every push and pull request, CI runs:
cargo fmt -- --checkcargo clippy -D warningscargo build --release- builds a
.debartifact for testing
- When you push a git tag like
v0.1.2, CI builds a release.deband attaches it to the corresponding GitHub Release.
We publish a SHA256SUMS file and a GPG signature SHA256SUMS.asc with every release.
-
Import Lloyd’s release key and verify its fingerprint:
# Option A: from a local file gpg --import docs/KEYS/lloydsmart-release-public-key.gpg # Option B: from a URL curl -sSL https://example.com/lloydsmart-release-public-key.gpg | gpg --import # Option C: from a keyserver gpg --keyserver keyserver.ubuntu.com --recv-keys D91C59CCB2B5AA41 # Check fingerprint gpg --fingerprint D91C59CCB2B5AA41
Expected fingerprint:
28A3 555E 056E 6DFF ED98 84DB D91C 59CC B2B5 AA41 -
Verify the signature over the checksum file:
gpg --verify SHA256SUMS.asc SHA256SUMS
-
Verify the files you downloaded:
sha256sum --check SHA256SUMS # or on macOS: shasum -a 256 -c SHA256SUMS
All lines should report OK. If you see BAD signature or a checksum FAILED, do not use the files.
Details and troubleshooting: see SECURITY.md.