reversefs

A filesystem in reverse: serve ordinary directories as complete, byte-exact block devices in 14 on-disk formats — live, over USB, IDE, or the network. And the other way around: FUSE-mount, format, initialize, inspect and flash real console disks.

What is reversefs?

A normal filesystem driver reads a disk and shows you files. reversefs does the opposite: it synthesizes a disk from your files, on the fly. Point it at a directory, pick a format, and any consumer that speaks "raw sectors" — a kernel driver, a game console, an emulator, QEMU, dd — sees a perfectly ordinary disk.

Architecture: consumers, transports, shared backend, format synthesizers, source directory
Consumers → transport → per-format synthesizer → your directory.

Quick start

# build (rustc >= 1.87) — or grab a release binary (x86_64 / aarch64 / armv7)
cargo build --release
alias reversefs=./target/release/reversefs

# 1. a directory as a FAT32 disk image, ready to flash anywhere:
reversefs dump --fat32-part /data/stuff=8G --out disk.img

# 2. the same disk LIVE as /dev/ublkb0 (Linux >= 6.0, root):
sudo reversefs serve-ublk --fat32-part /data/stuff=8G --dev 0
sudo partprobe /dev/ublkb0 && sudo mount -t vfat -o utf8 /dev/ublkb0p1 /mnt

# 3. the same disk over the network, no root:
reversefs serve-nbd --fat32-part /data/stuff=8G --listen 0.0.0.0:10809

# 4. real-disk tools — what is on this disk / write an image safely:
reversefs inspect /dev/sdX
reversefs flash disk.img /dev/sdX

# teardown of #2; the cookbook and the FATX geometry helper:
sudo reversefs clean-ublk --dev 0
reversefs examples
reversefs info --size 200G

Choosing a mode

Every format works with every mode — pick by where the consumer is:

You want…CommandProsCons
a disk image file (to flash onto real media, archive, or inspect) dump works everywhere, no root, sparse output a snapshot — source changes after the dump aren't in it
a local block device on this machine (mount it, partition it, benchmark it) mount (ublk) fastest local path, full RW + live sync Linux ≥ 6.0 with ublk_drv, root
the same, on a kernel too old for ublk serve-nbd --attach nbd.ko is ancient and everywhere root; slightly more moving parts
a disk over TCP (VMs, another Linux box, a thin head) serve-nbd no root, multi-client, qemu speaks it natively consumer needs an NBD client
a disk for a PS2 over the LAN serve-udpbd OPL/Neutrino speak it natively — zero console setup wired LAN only (UDP, no retransmissions)
network boot of real PCs (iPXE), ESXi/Windows initiators serve-iscsi iPXE sanboot, standard ecosystem chattier protocol; one LUN, no auth
this box to BE a USB drive (plug into console/TV/PC) serve-usb consumer needs nothing — it just sees a USB stick needs a USB device controller (Pi Zero/4/5 OTG), root
a read-only export for MCUs / scripts / browsers serve-http any HTTP client works; trivial firmware read-only by nature
a thin head: sources on the NAS, this box only forwards serve-usb --connect head needs no storage/config; NAS does the work adds network latency to every miss
Rule of thumb: start with dump to sanity-check the result (mount the image with -o loop or open it in 7-Zip), then switch the same flags to a live mode.

Format cookbook

Recipes below assume the binary is on PATH. Every recipe also works with --daemon (background), :ro / --read-only (protect sources), and any transport from the table above — only the most typical pairings are shown. RW = consumer writes flow back to your directory; RO = read-only medium, consumer writes stay in a session overlay and vanish on restart.

Share a folder as FAT32 / FAT16 / FAT12 RW

The universal format: PCs, TVs, printers, cameras, microcontrollers, DOS. Up to 4 MBR partitions (one directory each); FAT12 makes floppy images with canonical geometry (360K…2880K) that real drives and Gotek emulators accept.

# USB-stick disk for a TV, from two folders:
sudo reversefs serve-ublk --fat32-part /data/movies=64G --fat32-part /data/music=16G --dev 0

# a DOS boot floppy image:
reversefs dump --fat12-disc /data/floppy=1440K --out boot.img

# game library for a console, write-protected:
reversefs serve-nbd --fat32-part /data/games=32G:ro --listen 0.0.0.0:10809

Limits: 4 GiB−1 per file (format limit; bigger files are skipped with a warning). Mount with -o utf8 on Linux for correct long names.

Share a folder as exFAT RW

The modern removable-media format — no 4 GiB file limit. Best choice for video collections, cameras, and anything made after ~2010.

sudo reversefs serve-ublk --exfat-part /data/video=128G --dev 0
sudo partprobe /dev/ublkb0 && sudo mount -t exfat /dev/ublkb0p1 /mnt

Share a folder as NTFS RW

Full NTFS 3.1 with write-back — Windows machines mount it natively, and PS3 homebrew (webMAN/multiMAN) reads NTFS USB disks.

reversefs serve-nbd --ntfs-part /data/share=64G --listen 0.0.0.0:10809
# on the consumer: any NBD client, or serve-usb on a Pi head

Serve a folder as a CD-ROM RO

ISO9660 with Joliet (long Unicode names for Windows) and Rock Ridge (case-sensitive names + POSIX permissions for Linux/macOS/retro Unixes). Optional El Torito boot for BIOS and UEFI.

# burnable/mountable .iso from a folder:
reversefs dump --iso9660-disc /data/cd --iso9660-label MY_CD --out cd.iso

# live virtual CD drive:
sudo reversefs serve-ublk --iso9660-disc /data/cd --size 700M --dev 0

# bootable hybrid (BIOS + UEFI):
reversefs dump --iso9660-disc /data/cd \
    --iso9660-boot isolinux.bin --iso9660-boot-efi espfat.img --out boot.iso

Serve games as an Xbox / Xbox 360 disc (XISO) RO

The XDVDFS format both Xboxes use on game discs (not ISO9660). Consumers: xemu, Cxbx-Reloaded, ODE boards, extract-xiso tooling. A retail 360 still needs ODE-class hardware to accept it.

reversefs dump --xdvdfs-disc /data/game --out game.iso
reversefs serve-nbd --xdvdfs-disc game-files.zip --listen 0.0.0.0:10809  # zip works too

Serve a GameCube game RO

Source = a game extracted with wit (the standard sys/ + files/ layout). reversefs rebuilds the disc: header, FST, aligned file data. Consumers: Dolphin, GC Loader, Swiss.

wit extract game.iso /data/gcgame          # once, with Wiimms ISO Tools
reversefs dump --gc-disc /data/gcgame --out rebuilt.gcm
# edit files under /data/gcgame/files/ and re-dump — instant modding loop

Feed a real Xbox 360 (FATX360 HDD) RW

Big-endian XTAF, calibrated against real console disks — a physical RGH3 console recognizes the result. Partition keys: sc gc se1 se2 x1 x2 (x2 = games/Content).

# games only — simplest:
sudo reversefs serve-ublk --fatx360-part /data/x360=x2 --size 200G --dev 0

# full console-ready image: capture the security-sector prefix once from the
# console's own disk, bake it in, flash the result (size-checked + verified):
reversefs capture /dev/sdX --fatx360-template template.bin
reversefs dump --fatx360-disc /data/x360 --size <disk_size> \
    --fatx360-template template.bin --out disk.img
reversefs flash disk.img /dev/sdX

The security sector is RSA-signed per disk, so the template must come from the disk the console originally used. capture is read-only, runs detection first, and refuses to extract from a non-FATX disk (override with --force). The template identifies your hardware — share it only on purpose.

Without a template every fatx360 hd image carries an unsigned HDDSS at 0x2000 (sector count + stock logo, no RSA — what a console writes when it formats a disk itself). init -t fatx360 fills in the target drive's real ATA serial, and flash /dev/sdX --fatx360-hddss refreshes the block in place after flashing an image. RGH consoles skip the signature check; stock consoles demand the MS-signed original.

Beyond the retail hard disk, --fatx360-table selects the other 360 media: mu (memory unit), usb (USB storage — the concatenation of the console's Xbox360/Data0000..NNNN files) and kit (devkit disk with the xdv dashboard partition).

Feed an Xbox OG (FATX-OG HDD) RW

Little-endian FATX with an XBpartitioner table in sector 0, so modded BIOSes and tools see custom F/G partitions. Pairs naturally with a USB→IDE bridge head.

sudo reversefs serve-ublk --fatxog-disc /data/xboxog --size 40G --dev 0

Feed a PlayStation 2 — files and GAMES RW

A full APA-partitioned PS2 disk: PFS partitions for files (OPL/uLaunchELF read them) and HDL partitions for games — each disc image becomes a partition OPL lists and boots. Combined with serve-udpbd, a real PS2 boots games straight over the LAN, and the images may even stay compressed:

reversefs serve-udpbd \
    --ps2-part /data/opl=+OPL=512M \
    --ps2-hdl "or2006.iso.zst=OutRun 2006" \
    --ps2-hdl "gt4.cso=GT4" \
    --size 120G --listen 0.0.0.0:48573

The boot file name is read from each disc's own SYSTEM.CNF; CD/DVD media type is set automatically. HDL partitions are RO; PFS partitions keep full write-back.

Feed an Amiga (AFFS, with a real partition table) RW

AmigaDOS FFS volumes: ADF floppy images, HDF hard files, and — with --affs-rdb — a proper Rigid Disk Block partition table, so a real Amiga (or WinUAE, or your USB→IDE bridge) sees a bootable hard disk.

reversefs dump --affs-disc /data/amiga --size 1760K --out disk.adf   # HD floppy
sudo reversefs serve-ublk --affs-disc /data/amiga --size 200M --affs-rdb --dev 0  # partitioned HDD

Feed a classic Mac (HFS) RW

reversefs dump --hfs-disc /data/mac --size 32M --hfs-label MACDISK --out mac.img

System 6/7-era HFS (not HFS+); names ASCII ≤ 31 chars, period-correct.

Feed an Atari ST RW

One flag switches the FAT family to ST conventions: floppies get the TOS executable-boot checksum, hard disks get an AHDI root sector instead of a DOS MBR. PC tools still read the filesystems.

# TOS-bootable floppy (boot code + checksum 0x1234):
reversefs dump --fat12-disc /data/st=720K \
    --fat12-bootcode boot.st --atari --out floppy.st
# AHDI-partitioned hard disk:
reversefs dump --fat16-part /data/st=20M --atari --out hd.img

Serve an existing image — including Wii RO

--raw-disc is the passthrough mode: an existing disk or disc image served byte-exact with a session overlay, over any transport. The image may be compressed — decompressed on the fly, never unpacked to disk:

reversefs serve-nbd --raw-disc disk.img.zst --listen 0.0.0.0:10809
reversefs serve-nbd --raw-disc game.wbfs    --listen 0.0.0.0:10809  # Wii, from a WBFS container
reversefs dump      --raw-disc game.cso --out game.iso              # doubles as a converter

Write semantics: default = session overlay (image pristine, writes gone on restart); --raw-writable writes into the plain image file (qemu-nbd style — an OS-install target disk); --raw-overlay FILE keeps the image pristine and persists writes in a side file (compressed bases work too; delete the pair for a factory reset).

Accepted containers: plain files, .zst (seekable or plain), .gz, .cso/.zso, .wbfs, and zip/tar holding exactly one image. Wii discs are AES-encrypted with hash trees on-disc — passthrough of an existing image is the correct (and only sensible) approach; GameCube has no encryption, hence its full synthesizer above.

Archive sources — serve without unpacking

Anywhere a source directory is accepted, you may pass an archive file instead: --iso9660-disc game.zip, --fat32-part games.zip=8G, --fatx360-part games.zip=x2. It is indexed once and streamed on demand — never extracted to disk — and the partition is forced read-only (the archive is never modified).

ContainerRandom access strategy
ZIP, storeddirect pread of the member's bytes
ZIP, deflatelive inflate cursor per entry + decompressor-state checkpoints (~44 KiB each 8 MiB) — backward seeks resume from the nearest checkpoint; RAM-bounded, no disk spill
TARdirect pread (members are contiguous)
tar.gz / .gzsame deflate cursor + checkpoints
tar.zst / .zst (seekable format)frame table → one frame per read + small LRU cache
tar.zst / .zst (plain stream)restartable sequential cursor (a warning suggests the seekable format for big sources)
CSO/ZSO, WBFSnative block/frame index of the format
# a zipped game library, straight to a console, still compressed on the NAS:
sudo reversefs serve-ublk --fat32-part /nas/games.zip=32G --dev 0

Transport guide — which client where

AoE (serve-aoe): raw layer-2 ATA-over-Ethernet — no IP/TCP, the simplest consumer side (FPGA/MCU heads); Linux initiator aoe.ko (/dev/etherd/eN.M), iPXE sanboot aoe:eN.M; same L2 segment only.

All transports serve the same built device; they differ in who the consumer is and which side does the heavy lifting.

TransportConsumerRoot?Notes
dumpanything that reads filesno sparse snapshot image
mount (ublk)this machine's kernelyes Linux ≥ 6.0; fastest local RW path
serve-nbdany NBD client (qemu, nbd.ko, busybox, your firmware)no multi-connection; advertises the read-only flag for RO media
serve-nbd --attachthis machine's kernel via nbd.koyes local /dev/nbdX without ublk
serve-udpbdPS2 (OPL/Neutrino)no wired LAN only — UDP has no retransmissions
serve-iscsiiPXE, ESXi, Windows/Linux initiatorsno RFC 7143 subset, one LUN, no CHAP
serve-usbwhatever the cable plugs intoyes needs a UDC; three backings, see below
serve-httpcurl, browsers, MCU HTTP clientsno GET/HEAD + Range on /disk.img; read-only

serve-usb backings

BackingData pathUse when
nbd (default)kernel nbd client → f_mass_storage normal case — kernel readahead & merging for free
ublk/dev/ublkbN → f_mass_storage kernel ≥ 6.0 and you already use ublk
ffsour own SCSI/BOT in userspace → USB endpoints kernels without nbd.ko/ublk; no block device anywhere in the path

--usb-cdrom presents the LUN as a USB CD/DVD-ROM drive: hosts see an optical drive and BIOSes boot it via El Torito — plug a Pi with --raw-disc installer.iso --usb-cdrom into any machine that boots from USB optical media and install an OS without burning a disc. Implies read-only; works with every backing (kernel MMC for nbd/ublk, our own MMC subset — READ TOC, GET CONFIGURATION, event polling — for ffs).

NAS + edge heads — remote consumers

The synthesis brain and the physical connector don't have to share a box. Run reversefs on the NAS as the block server; put a small head next to the console:

# NAS — holds sources (even compressed), does all filesystem work:
reversefs serve-nbd --ps2-hdl "gt4.cso=GT4" --size 120G --listen 0.0.0.0:10809

# Pi Zero 2 W plugged into the console's USB port — a THIN HEAD:
sudo reversefs serve-usb --connect nbd://nas:10809

--connect skips the local build: no sources, no layout flags, no storage on the head. Which NBD client carries the data depends on the backing — the rule: the kernel client whenever the kernel consumes the blocks; our userspace client only where no block device exists in the path:

ScenarioNBD clientWhy
--connect + backing nbd kernel, attached straight to the NAS kernel readahead/merging work against the network; best throughput
--connect + backing ffs userspace (built-in) no nbd.ko needed; NAS → socket → USB endpoint, zero kernel block devices
MCU head (ESP32 firmware)your own (~200 lines on lwIP) TCP gives retransmissions on WiFi; requests are fixed 28-byte frames; the server accepts minimal clients (EXPORT_NAME, zero flags) and its read-only flag lets firmware skip the write path entirely

Edge cache (--cache-dir DIR --cache-size N, ffs/ublk backings): the head keeps fetched blocks in a local shadow file (SD/NVMe, or tmpfs for RAM), so the game you are playing streams locally after the first run. Freshness is journal-driven: the NAS adds --status-listen :10810 (optionally --status-state FILE to survive restarts); every content change bumps a generation with its byte ranges and the head invalidates only those ranges — adding a game to a 16 TB library does not re-download the 2 TB you cached. Chunk-aligned 1 MiB writes keep SD wear minimal; LRU eviction punches holes. Foreign NBD servers get no persistent cache unless you declare --cache-immutable.

Throughput reality check — the head's slowest link caps everything:
HeadRealistic ceilingGood for
ESP32 (WiFi) + RP2040 (Full-Speed USB / PIO-IDE)~1 MB/s PS1, floppies, Amiga, Atari, DOS — the whole CD era and older
Pi Zero 2 W (WiFi, USB2 UDC)~5–10 MB/s PS2, GameCube, most USB consumers
Pi 4/5 (Ethernet, USB2/3 UDC)30–100+ MB/s everything, incl. DVD-era streaming
UDPBD is wired-LAN-only; on WiFi prefer NBD (TCP retransmissions).

Mounting real disks (reversefs mount)

The other direction: FUSE-mount partitions of existing disks and images. The mounting layer is format-agnostic (one backend per format); currently mountable: FATX/XTAF — both Xbox consoles, variant auto-detected from the superblock magic. -t/--type is the partition table type (like mount -t): file, fatx360, fatxog, mu, usb, kit.

reversefs mount --list /dev/sdX            # what is mountable and how
reversefs mount --all /dev/sdX /mnt/xbox   # every partition: /mnt/xbox/<key>/
reversefs mount -t fatx360 -p x2 /dev/sdX /mnt/p
reversefs mount -t fatxog  -p c  /dev/sdX /mnt/p
reversefs mount -r image.fatx /mnt/p       # read-only, bare image

Consistency by construction: a single allocation path fails with ENOSPC before mutating anything, free space always derives from the FAT (df can never go negative), writes are write-through, fresh clusters are zeroed.

Formatting & initializing (mkfs / init)

reversefs mkfs -p x1 /dev/sdX                 # one partition, retail cluster sizes
reversefs init -t fatx360 --fatx360-template hddss.bin /dev/sdX   # whole console disk
reversefs init -t fatxog /dev/sdX             # Xbox OG + XBpartitioner table

init erases foreign partition tables first — the first MiB (MBR + primary GPT) and the last MiB (the backup GPT lives at the disk end). Both commands are destructive and ask for confirmation (--yes when scripting). Judged by the independent C++ fusefatx/fsck.fatx (360) and mborgerson fatxfs (OG).

Inspecting, capturing & flashing (inspect / capture / flash)

reversefs inspect /dev/sdX                    # unified view: tables, partitions,
                                              # hidden regions, verdict — read-only
reversefs capture /dev/sdX --fatx360-template t.bin   # extract the fatx360 template
reversefs capture /dev/sdX --boot-mbr m.bin --boot-gap g.bin  # GRUB from a real disk
reversefs flash disk.img /dev/sdX             # size-checked write + verify
reversefs flash /dev/sdX --boot-mbr m.bin     # patch one region, touch nothing else

inspect merges every partition table (MBR, GPT, XBpartitioner, the fixed Xbox layouts) into one partition list and recognizes foreign filesystems (ext2/3/4, LUKS, LVM2, swap, btrfs, XFS, squashfs, md-RAID) info-only. capture flags reuse the synthesis vocabulary — what it extracts is exactly what dump injects and flash patches back. Where inspect answers “what is on this disk?”, mount --list answers “what can I mount and how?”.

Operations: background, read-only, live sync

Booting

Boot material is supplied as files and injected identically in every mode: --boot-mbr (x86 MBR code), --boot-gap, --boot-active, --boot-esp (UEFI System Partition type), --fat12-bootcode (floppy boot sector merge), --affs-bootblock (Amiga, checksum recomputed), --iso9660-boot/--iso9660-boot-efi (El Torito), --atari (TOS checksum / AHDI). --boot-state DIR persists sectors written by consumer-side installers across restarts. Verified end-to-end in QEMU (SeaBIOS + OVMF), from disk and from CD.

Limitations worth knowing

How it's verified

Every format was built against an oracle (a reference image from the ecosystem's own formatter) and is continuously verified by independent judges — real drivers, fscks and scene tools:

FormatJudges
FATX360unmodified fusefatx, fsck.fatx, a physical RGH3 Xbox 360
FATX-OGmborgerson fatxfs, reversefs mount (in-tree consumer check)
FAT32/16/12kernel vfat, fsck.fat, mtools, file(1)
exFATkernel exfat, fsck.exfat, exfat-fuse
NTFSntfs-3g (ro + rw), kernel ntfs3
ISO9660kernel isofs, isoinfo (incl. -R Rock Ridge), 7z, QEMU SeaBIOS/OVMF
XDVDFSextract-xiso: list + extraction round-trip
GameCube / WBFSwit (Wiimms ISO Tools): list + extraction; WBFS readback equals wit's own conversion
Amiga FFS / RDBxdftool, kernel affs, rdbtool (table parse + export)
HFShfsutils (read + write), kernel hfs
PS2 APA/PFS / HDLpfsshell; hdl_dump (TOC, game list, byte-exact extraction)
Atari STmtools at the AHDI offset, checksum/table bytes pinned in tests
Transportsqemu (NBD + libiscsi), busybox nbd-client path, curl (HTTP ranges), real PS2 protocol frames (UDPBD)

≈150 automated tests plus a root live-test script (scripts/live-test.sh) exercising real kernel mounts over ublk. The complete reference (incl. format internals) lives in the repository README.md — Polish version: README.pl.md; MCU client blueprint: esp32-nbd-client.md.