fstool
Build, inspect, modify, and repack disk images and filesystem images.
In the spirit of genext2fs, but covering whole disks, multiple filesystems,
and round-tripping between formats — all from a TOML spec or directly from
the command line.
fstool ships as a Rust library (fstool) plus a thin CLI binary (fstool).
Public API is unstable until v0.5.
Filesystem support
| Filesystem | Read | Write | Notes |
|---|---|---|---|
| ext2 | ✅ | ✅ | byte-exact with genext2fs on the same input |
| ext3 | ✅ | ✅ | + JBD2 journal |
| ext4 | ✅ | ✅ | extents, FILETYPE, metadata_csum, xattrs |
| FAT32 | ✅ | ✅ | VFAT LFN entries, 8.3 short-name aliases |
| exFAT | ✅ | ✅ | format + create + remove + flush |
| tar | ✅ | ✅ | ustar + PAX, SCHILY.xattr.* for xattrs |
| XFS | ✅ (shortform + block / leaf / node dirs + BMBT) | — | + inline + remote symlinks; B-tree dirs deferred |
| HFS+ / HFSX | ✅ | — | inline + extents-overflow, symlinks, hard links |
| APFS | ✅ | — | multi-level omap + fs-tree; no snapshots / crypto |
| NTFS | ✅ | — | MFT, attributes, $DATA + ADS, indexes; xattr map |
| F2FS | ✅ | — | CP / NAT / dnodes / inline data + dentries |
| SquashFS | ✅ (uncompressed) | — | gzip/xz/lz4/zstd refused until compression lands |
| qcow2 | ✅ | ✅ | v2 + v3, allocate-on-write writer |
The reader for each FS streams: file contents are never fully resident in memory regardless of size. The writers do the same, two-pass: scan to size the geometry, then stream bytes from each source file into the image.
NTFS metadata that has no POSIX analogue (DOS attributes, ADS, security
descriptors, NT-FILETIME timestamps, short names, reparse data) round-trips
through xattrs under user.ntfs.* and system.ntfs_security.
CLI commands
| Command | What it does |
|---|---|
build |
Build from a TOML spec — bare FS or a partitioned disk image. |
ext-build |
Bare ext2 / ext3 / ext4 image from a host directory tree. |
fat-build |
Bare FAT32 image from a host directory tree. |
info |
Print partition table (whole-disk) or FS summary + root listing. |
ls |
List a directory inside an image. |
cat |
Stream a file's bytes out of an image to stdout. |
add |
Copy a host file / tree into an existing image (ext or FAT). |
rm |
Unlink a file, symlink, device, or empty directory. |
shell |
SFTP-style REPL — ls cd pwd cat put rm mkdir info. |
convert |
Byte-level raw ↔ qcow2 conversion with optional grow. |
repack |
Walk source FS, rebuild into a fresh image, optionally a different FS. |
fstool … |
Plus ext-build, fat-build, partition-aware disk.img:N targets. |
All inspection / modification commands accept a disk.img:N (1-indexed)
target to walk into a partition of a GPT or MBR disk image. fstool info disk.img without the suffix prints the partition table itself.
Partitions, block devices, qcow2
- Partition tables — MBR (4 primaries) and GPT (128-entry, CRC32 on
header + entry array, primary + backup, protective MBR). Cross-checked
against
sgdisk -vandfdisk -l. - Block devices — on Unix, fstool can format and mutate real block
devices (
/dev/sdX,/dev/nvme0n1, loop devices). Capacity is queried via the kernel ioctl (BLKGETSIZE64on Linux,DKIOCGETBLOCK*on macOS) and open usesO_EXCLso the kernel refuses if any partition is mounted. Build commands require--forcewhen the output is a block device. - qcow2 —
Qcow2Backendreads QEMU v2 / v3 images and writes fresh v3 ones with allocate-on-write. Path-based factories (block::open_image,block::create_image) auto-dispatch by qcow2 magic or file extension, sofstool ext-build src -o out.qcow2Just Works.
TOML spec
Declarative image descriptions — either a bare filesystem ([filesystem])
or a partitioned disk ([image] + [[partitions]]):
[]
= "64MiB"
= "gpt"
[[]]
= "EFI"
= "esp"
= "16MiB"
[[]]
= "root"
= "linux"
= "remaining"
[]
= "ext4"
= "./rootfs"
Architecture
┌────────────────────────────────────────────┐
│ CLI (clap) — bin/fstool │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Spec layer (TOML → ImageSpec / FsSpec) │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Filesystem trait → ext, fat, xfs, ntfs, … │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ PartitionTable trait → Mbr, Gpt │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ BlockDevice trait → File, Mem, Sliced, │
│ Qcow2 │
└────────────────────────────────────────────┘
Each layer is substitutable. A filesystem implementation talks only to a
BlockDevice; it doesn't know or care whether the device is a real file,
an in-memory buffer in a test, a slice carved out of a larger disk by a
partition table, or a qcow2-backed sparse container.
ext-specific niceties
BuildPlanauto-sizes a filesystem to fit a source tree exactly (genext2fs-style "size to fit").Ext::populate_rootdevsdrops aMinimalorStandard/dev/*tree (console, null, zero, ptmx, tty, fuse, random, urandom — plus tty0..15, ttyS0..3, kmsg, mem, port, hda..hdd, sda..sdd + partitions forStandard), so a non-root user can build a Linux root FS withoutCAP_MKNOD.- xattrs round-trip through repack: both inline (extended-inode-body) and
external
file_acl-block sources are read; the destination writes to an external block with a correctly-computed CRC32C whenmetadata_csumis on.debugfs ea_getconfirms identical values after repack.
Cross-FS repack
fstool repack walks the source filesystem and rebuilds the tree into a
fresh image. With --fs-type it changes filesystem on the fly; --shrink
auto-sizes the output to the minimum that fits the content. The ext → ext
path uses a direct FS-to-FS copier (no host-filesystem intermediation),
preserving symlinks, device nodes, mode, uid/gid, and xattrs. tar in either
direction round-trips content + mode + uid/gid + mtime + symlinks + device
nodes + xattrs.
For the read-only filesystems (XFS, HFS+, APFS, NTFS, F2FS, SquashFS), repack works from them. Repacking to them isn't supported until their writers land.
Limitations
Things explicitly out of scope today, in rough order of likely-to-change:
- SquashFS compression decode (needs flate2 / zstd / xz / lz4 dependencies).
Compressed blocks return a clean
Unsupportednaming the algorithm. - NTFS / F2FS / XFS / APFS / HFS+ writers.
- NTFS compressed and encrypted
$DATA,$ATTRIBUTE_LISTspill,$Securesecurity-descriptor indirection — all returnUnsupported. - APFS snapshots, encryption, sealed-volume integrity.
- XFS B-tree-format directories (block / leaf / node formats are covered).
- ext4
flex_bgon the write path (the reader handles it). - Partial-file rewrites — in-place modification is whole-file granularity.
Try it
&&
Run the test suite:
CI runs the full suite on Linux (with apt-installed e2fsprogs,
dosfstools, mtools, gdisk, qemu-utils for cross-validation) plus a
build + test pass on macOS (Homebrew qemu) and Windows.
Licence
MIT. Copyright © 2026 Karpelès Lab Inc. See LICENSE.