{"id":50368959,"url":"https://github.com/karpeleslab/fstool","last_synced_at":"2026-06-11T02:03:26.948Z","repository":{"id":358746801,"uuid":"1242897871","full_name":"KarpelesLab/fstool","owner":"KarpelesLab","description":"Build, inspect, convert, and repack disk images and filesystems (ext2/3/4, FAT32, tar; raw and qcow2 containers; MBR/GPT partition tables) — Rust CLI and library.","archived":false,"fork":false,"pushed_at":"2026-05-26T01:21:09.000Z","size":3044,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-26T01:22:33.510Z","etag":null,"topics":["cli","disk-image","ext2","ext3","ext4","fat32","filesystem","genext2fs","gpt","mbr","partition-table","qcow2","rust","tar"],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KarpelesLab.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-18T21:30:52.000Z","updated_at":"2026-05-26T01:20:25.000Z","dependencies_parsed_at":"2026-05-19T00:00:11.997Z","dependency_job_id":"6dcd170f-e147-4f46-af78-8bd62e868ddc","html_url":"https://github.com/KarpelesLab/fstool","commit_stats":null,"previous_names":["karpeleslab/genfs"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/KarpelesLab/fstool","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KarpelesLab%2Ffstool","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KarpelesLab%2Ffstool/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KarpelesLab%2Ffstool/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KarpelesLab%2Ffstool/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KarpelesLab","download_url":"https://codeload.github.com/KarpelesLab/fstool/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KarpelesLab%2Ffstool/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33681809,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","disk-image","ext2","ext3","ext4","fat32","filesystem","genext2fs","gpt","mbr","partition-table","qcow2","rust","tar"],"created_at":"2026-05-30T06:00:28.579Z","updated_at":"2026-06-07T04:00:51.028Z","avatar_url":"https://github.com/KarpelesLab.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fstool\n\n[![CI](https://github.com/KarpelesLab/fstool/actions/workflows/ci.yml/badge.svg)](https://github.com/KarpelesLab/fstool/actions/workflows/ci.yml)\n[![Crates.io](https://img.shields.io/crates/v/fstool.svg)](https://crates.io/crates/fstool)\n[![docs.rs](https://docs.rs/fstool/badge.svg)](https://docs.rs/fstool)\n\nBuild, inspect, modify, and repack disk images and filesystem images.\nIn the spirit of `genext2fs`, but covering whole disks, multiple filesystems,\nand round-tripping between formats — all from a TOML spec or directly from\nthe command line.\n\nfstool ships as a Rust library (`fstool`) plus a thin CLI binary (`fstool`).\nPublic API is **unstable** until v0.5.\n\n```sh\ncargo install fstool\nfstool create -t ext4 ./src -o out.img           # build an ext4 image from a dir\nfstool create -t squashfs ./src -o out.sqsh \\\n       -O compression=zstd,block_size=128KiB     # FS-specific knobs via -O\nfstool info out.img                              # what's inside\nfstool ls   out.img /                            # walk it\nfstool repack out.img out.tar                    # convert ext4 → tar (and back)\nfstool repack base.tar patch.tar flat.tar        # OCI-style layer merge with .wh.* whiteouts\n```\n\n## Filesystem support\n\n| Filesystem | Read | Write | In-place edits | Notes                                                                                                              |\n|------------|------|-------|----------------|--------------------------------------------------------------------------------------------------------------------|\n| ext2       | ✅    | ✅     | ✅              | byte-exact with `genext2fs` on the same input                                                                      |\n| ext3       | ✅    | ✅     | ✅              | + JBD2 journal — real transactions on `open_file_rw` (Path A)                                                      |\n| ext4       | ✅    | ✅     | ✅              | extents (read + write: any depth), FILETYPE, `metadata_csum`, xattrs, JBD2                                         |\n| FAT32      | ✅    | ✅     | ✅              | VFAT LFN entries, 8.3 short-name aliases                                                                           |\n| exFAT      | ✅    | ✅     | ✅              | format + create + remove + flush + `open_file_rw`                                                                  |\n| tar        | ✅    | ✅     | —              | ustar + PAX, `SCHILY.xattr.*` for xattrs; streaming-only                                                           |\n| XFS        | ✅    | ✅     | ✅              | shortform + block / leaf / node + multi-level B-tree dirs + BMBT; leaf-form xattrs; real XLOG transactions (Path A); passes `xfs_repair -n` single + multi-AG |\n| HFS+/HFSX  | ✅    | ✅     | ✅              | inline + extents-overflow, symlinks, hard links; decmpfs read (zlib types 3 + 4); **resource forks** (`cat --rsrc`, `resources`, `com.apple.ResourceFork` xattr); real journal (Path A); passes `fsck.hfsplus` |\n| HFS        | ✅    | ✅     | ✅              | classic HFS (Mac OS ≤ 8): MDB + catalog/extents B-trees, MacRoman names, data + **resource** fork read; transparently unwraps **DiskCopy 4.2** images. **Write**: `create -t hfs` / `build` / `repack` generate fresh volumes, and `add` / `rm` / shell `put`/`mkdir` mutate an existing image in place (catalog rebuilt on flush) |\n| AFFS       | ✅    | ✅     | ✅              | Amiga OFS/FFS (`.adf`): boot-block variant detect (`DOS\\0`..`DOS\\7`), hash-table dirs, file header + extension blocks, OFS (24-byte data headers) + FFS raw data, BCPL/Latin-1 names, Amiga 1978 epoch dates; read validated against real OFS/FFS Workbench volumes. **Write**: `create -t affs` / `-t ofs` / `build` / `repack` generate fresh OFS or FFS volumes (default DOS\\3 FFS+INTL; `-O fstype=ofs,intl=false`), and `add` / `rm` / shell `put`/`mkdir` mutate an existing image **incrementally on disk** — only the affected blocks (volume bitmap, the parent directory's hash chain, and the new/removed file's header + data + extension blocks) are touched; untouched files keep their exact blocks, and RAM use is bounded by the bitmap, not file contents. Spec-conformant (block checksums + name-hash placement + bitmap, the invariants the Linux kernel `affs` driver enforces) |\n| APFS       | ✅    | ✅     | 🚧             | **Read**: multi-level omap + fs-tree, directory listings + file extents, embedded xattrs, snapshots (read-only, single-leaf snap-meta). **Write**: format + `create_dir`/`create_file`/`create_symlink` + `chmod`/`chown`/`set_times`/`rename`/`unlink`/`link` via fresh COW checkpoints (spaceman with IP ring + SFQ free-queues), round-tripped through a real macOS mount. **Gaps**: in-place edits are whole-file overwrite (no partial-extent COW); `UF_COMPRESSED`/decmpfs files read as empty; encryption, sealed-volume integrity, Fusion tiering, and dstream-backed xattrs are refused; not yet `fsck_apfs`-clean |\n| NTFS       | ✅    | ✅     | ✅              | MFT, attributes, $DATA + ADS, indexes; xattr map; multi-class `$Secure` ($SDS/$SDH/$SII); real `$LogFile` LFS records (Path A) |\n| F2FS       | ✅    | ✅     | —              | CP / NAT / dnodes / inline data + dentries; writer passes `fsck.f2fs`; **build-once** — the writer serializes the whole FS from memory at flush, so a re-opened image is read-only (reports `Immutable`) |\n| SquashFS   | ✅    | ✅     | —              | gzip / xz / lz4 / zstd / lzo / lzma via Cargo features; writer round-trips via `unsquashfs`; repack-only           |\n| ISO 9660   | ✅    | ✅     | —              | PVD + Joliet (UCS-2) + Rock Ridge (PX/NM/SL/TF) + El Torito boot catalog; repack-only                              |\n| GRF        | ✅    | ✅     | ✅              | Gravity Ragnarok Online archive — v0x102 / v0x103 / v0x200; permutation cipher (`MIXCRYPT` / `DES`); CP949 filenames |\n| zip        | ✅    | ✅     | —              | central-directory index, ZIP64, Stored + Deflate, Unix mode/symlinks, UTF-8/Shift-JIS/EUC-JP filename detection; repack-only writer |\n| cpio       | ✅    | ✅     | —              | newc / newc-crc / odc read; newc write; repack-only                                                              |\n| ar         | ✅    | ✅     | —              | GNU + BSD long names (read), GNU write; flat (no directories); repack-only                                       |\n| cab        | ✅    | —     | —              | Microsoft Cabinet read-only: Store / MSZIP / LZX / Quantum folders decode via `compcol` (cross-checked with `cabextract`). Spanned cabinets and creation are unsupported |\n| lzx        | ✅    | —     | —              | Amiga LZX read-only: Store + LZX (mode 2) merged groups via `compcol::amiga_lzx`; container cross-checked with `unlzx`. Creation unsupported |\n| rar        | ✅    | —     | —              | RAR5 read-only incl. **solid** archives (a sequential walk / `repack` decodes the group once): Store + compressed (no-filter / x86 E8E9) via `compcol::rar5`; cross-checked with `unrar`. RAR4, encryption, stored-in-solid, other filters and creation are unsupported |\n| lha        | ✅    | —     | —              | LHA / LZH read-only: walks level-0/1/2 headers (long names + directories). `-lh0-` store decodes + is cross-checked with `lha`; the lh1/4/5/6/7 LZSS+Huffman methods list but read as `Unsupported` pending an `lha` codec in `compcol`. Creation unsupported |\n| arc        | ✅    | —     | —              | SEA ARC read-only: walks the flat header chain. Stored methods (1 old / 2) decode; the compressed methods (RLE90 / squeeze / crunch / squash) list but read as `Unsupported` pending ARC codecs in `compcol`. Creation unsupported |\n| sit        | ✅    | —     | —              | StuffIt read-only: classic `SIT!` container (data-fork indexing, folder markers). Method 0 (store) decodes; compressed methods + StuffIt 5 list/detect but read as `Unsupported` pending StuffIt codecs in `compcol`. Creation unsupported |\n| 7z         | ✅    | —     | —              | 7-Zip read-only: parses the container (incl. LZMA-packed headers + solid folders sliced per substream); single-coder **Copy / LZMA / BZip2 / Deflate** folders decode (cross-checked with `7z`). **LZMA2** (the default), BCJ filters, PPMd, encryption and multi-coder pipelines list but read as `Unsupported` pending raw-LZMA2 + branch-filter codecs in `compcol`. Creation unsupported |\n\n`🚧` marks writers / mutation paths with known gaps (see Limitations).\nAll writable filesystems — ext2/3/4, FAT32, exFAT, XFS, HFS+, NTFS,\nAPFS, F2FS, SquashFS, ISO 9660, GRF — implement a single\n`Filesystem` trait, so the CLI (`build`, `repack`, `add`, `rm`) and\nthe TOML `[filesystem] type = \"…\"` spec dispatch through one\ncodepath; pick a target FS by setting `--fs-type` on `repack` or\n`type = \"hfsplus\"` (etc.) in the TOML spec. \"In-place edits\"\nmeans an already-flushed image can be re-opened for `add` / `rm` /\n`open_file_rw` — for filesystems with a journal, that path commits\nthrough a real transaction so a crash mid-write leaves an image the\nhost's `fsck` can replay.\n\n`qcow2` and `dmg` are **not** in the table above: they aren't\nfilesystems but *disk-image containers*. They live one layer down, as\n`BlockDevice` backends (see the architecture diagram and \"Partitions,\nblock devices, qcow2\"), presenting a flat byte-addressable device that\nany of the filesystems above is then laid down *inside* — fstool reads\nand writes through them transparently. qcow2 is read/write (v2 + v3,\nallocate-on-write), including **compressed** clusters — reads zlib and zstd\ntransparently (writing to a compressed cluster copies it out to a plain one);\ndmg is read-only (UDIF v4 mish chunks: zero / raw / zlib / ADC / bzip2 /\nLZFSE / LZMA, plus encrypted v2 `encrcdsa`).\n\nThe reader for each FS streams: file contents are never fully resident in\nmemory regardless of size. The writers do the same, two-pass: scan to size\nthe geometry, then stream bytes from each source file into the image.\n\nNTFS metadata that has no POSIX analogue (DOS attributes, ADS, security\ndescriptors, NT-FILETIME timestamps, short names, reparse data) round-trips\nthrough xattrs under `user.ntfs.*` and `system.ntfs_security`.\n\n## CLI commands\n\n| Command       | What it does                                                            |\n|---------------|-------------------------------------------------------------------------|\n| `create`      | Build a bare image of any supported FS (`-t ext4` / `fat32` / `xfs` / `hfs+` / `ntfs` / `f2fs` / `squashfs` / `iso` / `apfs` / `exfat` / `grf` / `zip` / `cpio` / `ar`) from a host directory tree. FS-specific knobs go through `-O key=val,key=val`. |\n| `build`       | Build from a TOML spec — bare FS or a partitioned disk image.           |\n| `info`        | Print partition table (whole-disk) or FS summary + root listing.        |\n| `ls`          | List a directory inside an image; `-R` walks subdirectories recursively. |\n| `cat`         | Stream a file's bytes out of an image to stdout. `--rsrc` streams the resource fork (HFS / HFS+). |\n| `resources`   | Inventory an HFS / HFS+ file's resource fork (ResEdit-style: `vers`/`ICN#`/`DITL`/… with decoded summaries); `--extract TYPE:ID` dumps one resource. |\n| `add`         | Copy a host file / tree into an existing image (any mutable FS).        |\n| `rm`          | Unlink a file, symlink, device, or empty directory.                     |\n| `shell`       | SFTP-style REPL — `ls cd pwd cat put rm mkdir info`, plus `find` (name/type/mtime filters, `-sort`/`-limit` for e.g. the N newest files) and `grep` (`-i`/`-n`/`-r`/`-v`/`-l`/`-c`; binary matches print as `hexdump -C`). Ctrl-C cancels a running `find`/`grep` without leaving the shell. `--with-cache` preloads all inodes into RAM so `find`/`ls` are instant; `--ro` browses read-only (incl. tar/ISO/SquashFS). On a TTY it has line editing + ↑/↓ command history (rustyline). |\n| `convert`     | Byte-level raw ↔ qcow2 conversion with optional grow.                   |\n| `repack`      | Walk one or more source FSes, merge bottom→top with whiteouts, rebuild into a fresh image. |\n| `dd`          | Resilient raw block copy (file/device → file/device), `ddrescue`-style: reads in 1 MiB blocks that halve on error down to the source sector and skip unreadable spots. Threaded reader/writer pipeline with a live progress bar (%, ETA, separate read/write speed, buffer occupancy, current block, bytes skipped). Ctrl-C cancels cleanly. |\n\nAll commands accept partition-aware `disk.img:N` targets (1-indexed) — see\n\"Partitions, block devices, qcow2\" below.\n\nAll inspection / modification commands accept a `disk.img:N` (1-indexed)\ntarget to walk into a partition of a GPT, MBR, or Apple Partition Map disk\nimage. `fstool info disk.img` without the suffix prints the partition table\nitself.\n\n### Path style (`--path-style`)\n\nClassic Mac filesystems separate path components with `:`, so `/` is a legal\n*filename* character (a real directory can be named `A/ROSE Includes`). The\nglobal `--path-style` flag picks how paths are spelled:\n\n- **`unix`** (default) — `/` separates everywhere; a literal `/` inside an\n  HFS/HFS+ name is shown as `:` (the convention macOS itself uses). So\n  `fstool ls disk.toast:2 …` lists `A:ROSE Includes`, and **repack to a tar/zip\n  renders the name the same way** (`A:ROSE Includes`) — a literal `/` can't go\n  in an archive member name without being read as a directory separator.\n- **`native`** — the filesystem's own separator (`:` for HFS/HFS+, `\\` for\n  FAT/exFAT/NTFS, `/` elsewhere); real filenames are preserved. Navigate with\n  the native separator, e.g.\n  `fstool ls --path-style native disk.toast:2 ':Apple Software Library:…:A/ROSE Includes'`.\n\n`native` only changes how the CLI and shell *display and accept* paths; on-disk\nformats (and the canonical names used by `repack`/`add`) are unaffected.\n\n### FS-specific options (`-O`)\n\nMost filesystems expose tunables (block size, label, compression codec,\nvolume name, journaling on/off, etc.) through a generic `-O\nkey=value,key=value` flag that is repeatable, modelled on `mke2fs -O`:\n\n```sh\n# 4 KiB blocks + custom label on ext4\nfstool create -t ext4 ./rootfs -o out.img -O block_size=4096,volume_label=ROOT\n\n# Pick a SquashFS codec and tighten the block size\nfstool create -t squashfs ./rootfs -o out.sqsh \\\n       -O compression=zstd,block_size=128KiB\n\n# Force a v0x103 GRF with deflate level 9\nfstool create -t grf ./rootfs -o out.grf -O version=0x103,compression_level=9\n```\n\nEach backend's `apply_options` validates keys; unknown keys are rejected\nwith a clear error citing the FS type. The same options are available\nthrough the TOML spec — see \"[filesystem.options]\" below.\n\n## Partitions, block devices, qcow2\n\n- **Partition tables** — MBR (4 primaries) and GPT (128-entry, CRC32 on\n  header + entry array, primary + backup, protective MBR). Cross-checked\n  against `sgdisk -v` and `fdisk -l`. **Apple Partition Map** (the classic\n  Mac / `.toast` scheme) is read-only: `info` lists the `Apple_HFS` /\n  `Apple_Free` / `Apple_partition_map` entries and `disk.toast:N` slices one.\n- **Block devices** — on Unix, fstool can format and mutate real block\n  devices (`/dev/sdX`, `/dev/nvme0n1`, loop devices). Capacity is queried via\n  the kernel ioctl (`BLKGETSIZE64` on Linux, `DKIOCGETBLOCK*` on macOS) and\n  open uses `O_EXCL` so the kernel refuses if any partition is mounted.\n  Build commands require `--force` when the output is a block device.\n- **qcow2** — `Qcow2Backend` reads QEMU v2 / v3 images and writes fresh v3\n  ones with allocate-on-write. **Compressed clusters** are read transparently\n  (zlib/deflate and zstd, decoded with a 4 KiB window to match qemu and bound\n  RAM); a write to a compressed cluster copies it out to a plain cluster. To\n  *produce* a compressed image, pass `--compress` to `create` / `build` /\n  `repack` / `convert` (e.g. `--compress`, `--compress=9`, `--compress=zstd`,\n  `--compress=zstd:9`); the result passes `qemu-img check`. Path-based\n  factories (`block::open_image`, `block::create_image`) auto-dispatch by qcow2\n  magic or file extension, so `fstool create -t ext4 src -o out.qcow2` Just\n  Works.\n\n## TOML spec\n\nDeclarative image descriptions — either a bare filesystem (`[filesystem]`)\nor a partitioned disk (`[image]` + `[[partitions]]`):\n\n```toml\n[image]\nsize = \"64MiB\"\npartition_table = \"gpt\"\n\n[[partitions]]\nname = \"EFI\"\ntype = \"esp\"\nsize = \"16MiB\"\n\n[[partitions]]\nname = \"root\"\ntype = \"linux\"\nsize = \"remaining\"\n\n[partitions.filesystem]\ntype = \"ext4\"\nsource = \"./rootfs\"\n```\n\n```sh\nfstool build disk.toml -o disk.img\nsgdisk -v disk.img             # \"No problems found.\"\n```\n\n### `source` — what to populate the FS with\n\n`source` accepts three shapes, auto-detected by what the string points at:\n\n```toml\n[partitions.filesystem]\ntype = \"ext4\"\nsource = \"./rootfs\"            # a host directory — walk it recursively\n```\n\n```toml\n[partitions.filesystem]\ntype = \"ext4\"\nsource = \"./rootfs.tar.gz\"     # a tar archive — repack entries into the FS\n```\n\n```toml\n[partitions.filesystem]\ntype = \"ext4\"\nsource = \"./old-disk.img:2\"    # an existing image, optional :N partition\n                               # — walks the source FS, copies every\n                               # entry into the new partition\n```\n\nRecognised tar extensions: `.tar`, `.tar.gz`, `.tgz`, `.tar.xz`, `.txz`,\n`.tar.zst`, `.tar.lz4`, `.tar.lzma`, `.tar.lzo` (codecs gated on the\nmatching Cargo feature). For images, the `:N` suffix selects partition\n*N* (1-indexed); without it, the source is opened as a bare filesystem.\nThe source FS may be any readable type — `ext{2,3,4}`, FAT32, exFAT,\nXFS, HFS+, APFS, NTFS, F2FS, SquashFS, ISO 9660, tar, or GRF — and the\ndestination is sized automatically to fit unless `size` is set\nexplicitly.\n\n### `[filesystem.options]` — FS-specific tunables\n\nThe same `-O key=val` knobs the CLI exposes are available in TOML\nthrough a free-form `[filesystem.options]` table:\n\n```toml\n[filesystem]\ntype = \"squashfs\"\nsource = \"./rootfs\"\n\n[filesystem.options]\ncompression = \"zstd\"\nblock_size  = 131072\n\n[partitions.filesystem]\ntype = \"ext4\"\nsource = \"./rootfs\"\n\n[partitions.filesystem.options]\nblock_size   = 4096\nvolume_label = \"ROOT\"\n```\n\nRecognised keys are documented next to each backend's\n`FormatOpts::apply_options`; unknown keys are rejected at spec parse\ntime with a clear error citing the FS type. The existing flat fields\n(`block_size`, `volume_label`, `mtime`, …) continue to work for\nbackward compatibility.\n\n## Architecture\n\n```\n              ┌────────────────────────────────────────────┐\n              │           CLI (clap) — bin/fstool          │\n              └────────────────────────────────────────────┘\n                                  │\n              ┌────────────────────────────────────────────┐\n              │  Spec layer (TOML → ImageSpec / FsSpec)    │\n              └────────────────────────────────────────────┘\n                                  │\n              ┌────────────────────────────────────────────┐\n              │  Filesystem trait → ext, fat, xfs, ntfs, … │\n              └────────────────────────────────────────────┘\n                                  │\n              ┌────────────────────────────────────────────┐\n              │  PartitionTable trait → Mbr, Gpt           │\n              └────────────────────────────────────────────┘\n                                  │\n              ┌────────────────────────────────────────────┐\n              │  BlockDevice trait → File, Mem, Sliced,    │\n              │                       Qcow2, Dmg           │\n              └────────────────────────────────────────────┘\n```\n\nEach layer is substitutable. A filesystem implementation talks only to a\n`BlockDevice`; it doesn't know or care whether the device is a real file,\nan in-memory buffer in a test, a slice carved out of a larger disk by a\npartition table, or a qcow2-backed sparse container. DMG (`.dmg`) is\ntreated the same way: open the image, walk the mish table for the\nchunk layout, and the rest of the stack reads through it as if it were\na flat block device — including the encrypted (`encrcdsa` v2) variant\nwhen an unlock password is supplied.\n\n## ext-specific niceties\n\n- `BuildPlan` auto-sizes a filesystem to fit a source tree exactly\n  (genext2fs-style \"size to fit\").\n- `Ext::populate_rootdevs` drops a `Minimal` or `Standard` `/dev/*` tree\n  (console, null, zero, ptmx, tty, fuse, random, urandom — plus tty0..15,\n  ttyS0..3, kmsg, mem, port, hda..hdd, sda..sdd + partitions for\n  `Standard`), so a non-root user can build a Linux root FS without\n  `CAP_MKNOD`.\n- xattrs round-trip through repack: both inline (extended-inode-body) and\n  external `file_acl`-block sources are read; the destination writes to an\n  external block with a correctly-computed CRC32C when `metadata_csum` is on.\n  `debugfs ea_get` confirms identical values after repack.\n\n## Cross-FS repack\n\n`fstool repack` walks the source filesystem and rebuilds the tree into a\nfresh image. With `--fs-type` it changes filesystem on the fly; `--shrink`\nauto-sizes the output to the minimum that fits the content.\n\nThe pipeline is **one generic walker feeding one of two sinks** — a\nstreaming-tar sink (tar / `.tar.\u003ccodec\u003e`) or a block-device `Filesystem`\nsink — with no per-`(source,dest)`-type special cases. So **any readable\nsource repacks into any writable destination** through a single path\n(`fstool repack app.zip out.tar`, `fstool repack disk.xfs out.iso`, …).\nThe walker reads each entry's metadata through the source's trait\n`getattr` / `list_xattrs` / `read_symlink`, so mode, uid/gid, mtime,\nsymlinks, device numbers, xattrs, and hard links round-trip wherever both\nends can represent them. File bodies stream straight from source to\ndestination (`create_file_streaming`, no per-file tempfile). Hard links\nare de-duplicated when the destination supports them (ext) and\nmaterialised as copies otherwise (tar, FAT, …); a destination that can't\nhold a symlink/device/xattr (FAT) drops it with a warning.\n\nEvery reader surfaces the metadata its format actually stores:\next, tar, the archive formats, F2FS, XFS, SquashFS, APFS, and HFS+ carry\nfull POSIX mode/uid/gid + timestamps (HFS+ converts its 1904 epoch);\nISO 9660 does too when Rock Ridge is present (plain/Joliet have none);\nNTFS — which has no POSIX ownership — surfaces real timestamps + a mode\nsynthesised from its DOS attributes, and carries its native metadata\n(DOS attrs, ADS, security descriptor, reparse data, …) through repack as\n`user.ntfs.*` / `system.ntfs_security` xattrs.\n\n`fstool repack` writes any destination implementing the `Filesystem`\ntrait — `ext2/3/4`, FAT32, exFAT, tar, XFS, HFS+, APFS, NTFS, F2FS,\nSquashFS, ISO 9660, GRF. `add` / `rm` go through the same trait,\nwhich means they work on any FS whose writer can re-open an existing\nimage; today that's all of the mutable backends — ext, FAT32, exFAT,\nF2FS, XFS, HFS+, NTFS, APFS, and GRF. SquashFS, ISO 9660, and tar\nare repack-only (their `MutationCapability` is `Immutable` or\n`Streaming`, so `add` / `rm` fail fast with an actionable error and\nthe user is steered to `repack`).\n\n## Layered merge with whiteouts\n\n`repack` takes one or more source positional arguments followed by the\ndestination. With one source it behaves as before; with two or more\nit merges the sources bottom→top before writing — later layers\noverride files of the same path, and tombstones from the upper\nlayer remove paths from the lower one. Two tombstone conventions are\nauto-detected:\n\n| Convention | Marker | Effect |\n|------------|--------|--------|\n| tar-OCI    | `.wh.\u003cname\u003e` in directory D | delete `D/\u003cname\u003e` |\n| tar-OCI    | `.wh..wh..opq` in directory D | drop all lower-layer children of D before this layer's own land |\n| OverlayFS  | character device with major=0, minor=0 | delete this path |\n| OverlayFS  | xattr `trusted.overlay.opaque = \"y\"` on a dir | opaque-dir semantics on that dir |\n\nThe tombstones themselves never appear in the output. Sources may be\nhost directories, tar archives (compressed or plain), or filesystem\nimages — any mix works.\n\n```sh\n# OCI-style: rebuild a stack of layers into a flat tar\nfstool repack base.tar layer1.tar layer2.tar flat.tar\n\n# Patch an ISO with a tar of replacement files\nfstool repack disc.iso patch.tar updated.iso --fs-type iso\n\n# Shell globs work — last positional is the destination\nfstool repack layer*.tar merged.tar\n```\n\nInternally the merge folds all layers into a single uncompressed tar\nheld in a tempfile, then drives the existing single-source repack\npipeline; the destination FS doesn't know it came from multiple\nsources.\n\n## ISO 9660\n\nISO 9660 reads cover the bare ECMA-119 layout plus three of the four\ncommon extensions:\n\n- **Joliet** (Microsoft) — UCS-2 BE long names via the supplementary\n  volume descriptor.\n- **Rock Ridge** (IEEE P1282) — POSIX mode + uid + gid via `PX`, long\n  names via `NM`, symlinks via `SL`, timestamps via `TF`. Continuation\n  areas (`CE`) are followed across sector boundaries.\n- **El Torito** — boot catalog: validation entry, default entry, and\n  section headers (`0x90` / `0x91`); the parsed catalog is surfaced\n  in `fstool info`.\n\nThe writer is repack-only — ISO is sequential and a single `flush()`\nwrites the whole image. It emits a PVD plus optional Joliet SVD,\nboth L-type and M-type path tables, dual directory record trees (one\nfor PVD, one for Joliet), and Rock Ridge System Use Areas (`NM` /\n`PX` / `SL`) attached to the PVD records. The output round-trips\nthrough `isoinfo -lR` and back through fstool's own reader.\n\n```sh\n# Build an ISO from a host directory\nfstool repack ./rootfs disc.iso --fs-type iso\n\n# Walk an existing ISO\nfstool ls   disc.iso /\nfstool cat  disc.iso /README.TXT\n\n# Round-trip ISO → tar → ISO\nfstool repack disc.iso plain.tar\nfstool repack plain.tar disc2.iso --fs-type iso\n```\n\n## Archive formats\n\nArchives are treated as filesystems through the same `Filesystem` trait as\ntar and GRF, so `info` / `ls` / `cat` / `repack` work on them uniformly. They\nshare a common core (`src/fs/archive/`): each format supplies a *scanner* that\nindexes the archive into an in-memory tree, and — where writable — a *builder*;\nthe core provides the generic read path and decodes each entry's byte range\nthrough the existing compression codecs.\n\n```sh\nfstool create -t zip ./rootfs -o out.zip          # build a zip from a dir\nfstool create -t zip ./rootfs -o out.zip -O compression=stored\nfstool ls   app.zip /                             # walk any zip/cpio/ar\nfstool cat  app.zip /etc/config\nfstool repack app.zip out.cpio --fs-type cpio     # convert between archives\n```\n\n| Format | Read | Write | Notes |\n|--------|------|-------|-------|\n| zip    | ✅    | ✅     | ZIP64, Stored + Deflate, Unix mode + symlinks; reads archives from any tool; filenames decoded as UTF-8 (flagged) else auto-detected (Shift-JIS / EUC-JP / Latin-9). On write the UTF-8 flag is set only for non-ASCII names. |\n| cpio   | ✅    | ✅     | newc / newc-crc / odc read; newc write. |\n| ar     | ✅    | ✅     | GNU + BSD long names on read, GNU on write. Flat — a nested source tree is rejected with a pointer to tar/zip/cpio. |\n\nThe writers are repack-only (`MutationCapability::Streaming`, like tar): an\nexisting archive can't be edited in place — `add` / `rm` steer you to\n`repack`, which rebuilds. `cab` (Store/MSZIP/LZX/Quantum), `lzx` (Amiga\nLZX), and `rar` (RAR5 Store/compressed, incl. **solid** groups) are read-only\nreaders via `compcol`, behind the `cab` / `amiga-lzx` / `rar` features. A\nsolid RAR group is decoded as one continuous stream; a sequential walk such\nas `repack` decompresses it exactly once (a backward/random read of an\nearlier member re-decodes from the group start, bounded memory). `lha`\n(LHA/LZH, behind the `lha` feature) walks level-0/1/2 headers and reads\n`-lh0-` store members; its LZSS+Huffman methods list but read as\n`Unsupported` pending an `lha` codec in `compcol`. `arc` (SEA ARC, behind the\n`arc` feature) walks the flat header chain and reads stored members; its\ncompressed methods list but read as `Unsupported` pending ARC codecs in\n`compcol`. `sit` (StuffIt, behind the `sit` feature) parses the classic\n`SIT!` container and reads stored members; its compressed methods and the\nwhole StuffIt 5 format list/detect but read as `Unsupported` pending StuffIt\ncodecs in `compcol`. `7z` (behind the `sevenz` feature) parses the full\ncontainer (LZMA-packed headers, solid folders sliced per substream) and reads\nsingle-coder Copy / LZMA / BZip2 / Deflate folders; LZMA2 (the default), BCJ\nfilters, PPMd, encryption and multi-coder pipelines list but read as\n`Unsupported` pending raw-LZMA2 + branch-filter codecs in `compcol`. Every\narchive format now has a reader — there are no detection-only scaffolds left.\n(`rar` and `sit` are read-only-at-best — their creation is proprietary; RAR4,\nencrypted, stored-in-solid, and filtered-but-unsupported RAR5 streams stay\n`Unsupported`.)\n\nzip's Deflate support rides the existing `gzip` Cargo feature (raw DEFLATE via\n`compcol`); a build without it falls back to Stored. `cpio` and `ar` need no\ncodec. Archive-to-`ext`/`fat`/`tar` repack uses the specialised FS-to-FS\ncopiers and isn't wired yet (same limitation as XFS/HFS+ sources) — convert\nbetween archives, or to `iso`/`grf`, via the generic trait path.\n\n## Compression\n\n`fstool` ships with six compression codecs enabled by default. Each has\nits own Cargo feature flag so you can trim the binary down:\n\n| Codec | Feature | Used for |\n|-------|---------|----------|\n| gzip  | `gzip`  | SquashFS, `.tar.gz` / `.tgz` |\n| xz    | `xz`    | SquashFS, `.tar.xz` / `.txz` |\n| lzma  | `lzma`  | SquashFS, `.tar.lzma` |\n| lz4   | `lz4`   | SquashFS, `.tar.lz4` |\n| zstd  | `zstd`  | SquashFS, `.tar.zst` |\n| lzo   | `lzo`   | SquashFS, `.tar.lzo` |\n\nCompressed tar input / output is detected by filename extension (or by\nmagic for inputs without a recognisable extension): `fstool ls\ndisk.tar.zst /` and `fstool repack ext.img out.tar.gz` Just Work.\nInternally the codec is streamed through a temp file so the whole\narchive is never resident in RAM.\n\nTo disable a codec at build time, e.g. to avoid the bundled C `zstd`\nbuild on a constrained system:\n\n```sh\ncargo install fstool --no-default-features --features gzip,lz4,xz,lzma\n```\n\n## Limitations\n\nThings explicitly out of scope today, in rough order of likely-to-change:\n\n- **ext4 write path**: `flex_bg` on the write path (reader is fine).\n- **APFS in-place edits**: `open_file_rw` rebuilds a fresh COW\n  checkpoint over the entire file content, so it's whole-file\n  granularity — partial-extent COW is not yet implemented, and\n  `create_file` / `remove` over the rw path piggyback on the same\n  checkpoint. Multiple back-to-back commits are bounded by the\n  `xp_desc` ring (the reader doesn't rotate it yet).\n- **APFS reader**: snapshots are read-only and single-leaf snap-meta only\n  (multi-level snap trees return `Unsupported`). `UF_COMPRESSED`/decmpfs file\n  contents read as empty (the data isn't decoded yet, though the HFS+ decmpfs\n  decoder could be reused). Encryption, sealed-volume integrity (hash/integrity\n  tree), Fusion tiering, and dstream-backed (`XATTR_DATA_STREAM`) xattrs are out\n  of scope.\n- **APFS / NTFS strict-checker pass**: the spaceman + `$Secure` /\n  `$LogFile` structures are now populated, but `fsck_apfs` and\n  `ntfs-3g` mount can still flag the images for finer points\n  (free-queue B-trees, journal metadata layout). Read + write work\n  end-to-end; the host-tool gate is the remaining polish.\n- **NTFS reader**: compressed and encrypted `$DATA`, `$ATTRIBUTE_LIST`\n  spill, and security-descriptor indirection through `$Secure`\n  beyond what the resident path handles all return `Unsupported`.\n- **XFS reader**: B-tree-format (`di_format=BTREE`) directories\n  deeper than one level above the leaves return `Error::Unsupported`\n  (shortform / block / leaf / node and single-level B-tree dirs are\n  covered); writer assumes shortform / extent dirs. Node-form\n  (multi-leaf dabtree) xattrs are read-only.\n- **HFS+ decmpfs**: type 3 (zlib inline) + type 4 (zlib resource\n  fork) work. LZVN (types 7/8) and LZFSE (types 11/12) return\n  `Unsupported`.\n- **DMG**: read-only — no DMG writer / `convert` path. Encrypted v1\n  (`cdsaencr` legacy 3DES) chunks return `Unsupported`; v2 is\n  covered.\n- **Partial-file rewrites** on the trait surface — `open_file_rw`\n  exists everywhere it's safe, but a typed \"patch this byte range\n  on a known-large file\" API is not surfaced beyond `Read + Write +\n  Seek` on the handle.\n\n## Try it\n\n```sh\ncargo install fstool                          # or: cargo install --path .\nmkdir -p /tmp/src/etc \u0026\u0026 echo hi \u003e /tmp/src/greeting.txt\nfstool create -t ext4 /tmp/src -o /tmp/out.img\nfstool info /tmp/out.img\nfstool ls   /tmp/out.img /\nfstool cat  /tmp/out.img /greeting.txt\ne2fsck -fn  /tmp/out.img                      # must report clean\n```\n\nRun the test suite:\n\n```sh\ncargo test                    # unit tests + external cross-checks if tools present\n```\n\nCI runs the full suite on Linux (with `apt`-installed `e2fsprogs`,\n`dosfstools`, `mtools`, `gdisk`, `qemu-utils` for cross-validation) plus a\nbuild + test pass on macOS (Homebrew `qemu`) and Windows.\n\n## Licence\n\nMIT. Copyright © 2026 Karpelès Lab Inc. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarpeleslab%2Ffstool","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkarpeleslab%2Ffstool","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarpeleslab%2Ffstool/lists"}