# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.4.13](https://github.com/KarpelesLab/fstool/compare/v0.4.12...v0.4.13) - 2026-06-07
### Added
- *(shell)* add `get SRC [DEST]` — copy a file/dir out of the image to host
- *(shell)* --with-cache opt-in in-memory inode cache
- *(cli)* fstool dd — resilient raw block copy with live progress
### Fixed
- *(hfs+,fat32)* zero only metadata on format, not the whole device
- *(info)* drop stale "read support is scaffold-only" note (NTFS/F2FS/SquashFS)
### Other
- *(changelog)* move dd entry under [Unreleased] after v0.4.12 release
### Added
- *(shell)* new **`get SRC [DEST]`** command — copy a file or directory out of
the image to the host, the inverse of `put`. `SRC` is a path inside the
image; `DEST` is a host path (an existing directory receives `SRC`'s
basename; omitting `DEST` writes the basename into the host's current
directory). A directory `SRC` is copied recursively (regular files and, on
unix, symlinks; other special files are skipped with a note). It only reads
the image, so it works in `--ro` mode, and a long recursive `get` is
cancellable with Ctrl-C.
- *(shell)* new **`fstool shell --with-cache`** flag — an opt-in in-memory
metadata cache. Before the first prompt it walks the whole tree and preloads
every directory listing and inode attribute into RAM, so metadata operations
(`ls`, and especially `find` / `grep` recursion) are served from memory and
run instantly instead of re-parsing on-disk structures on every lookup. File
*contents* are never cached (so `cat` / `grep` body reads still stream from
disk and RAM stays bounded by the tree's metadata). A `put` / `rm` / `mkdir`
invalidates the cache, which then lazily refills; a long preload is
cancellable with Ctrl-C (leaving a partial, still-correct cache). Works with
`--ro` too. The preload prints a one-line `cache: preloaded N dirs / M
entries in T ms` summary.
- *(cli)* new **`fstool dd <SRC> <DST>`** command — a resilient, container-
agnostic raw block copy (a `ddrescue`-lite). Copies bytes directly (a qcow2
file is cloned as-is, not expanded). Reads begin at the largest block
(`--block-size`, default 1 MiB) and **halve on read error**, retrying at the
same offset down to `--min-block-size` (default the source's sector); a
smallest-block read that still fails is **skipped** — left untouched on the
destination — and recorded, so a failing disk copies as much as it can
instead of aborting. A reader thread and a writer thread overlap through a
bounded buffer pool (`--queue`, default 8), and a live progress line shows
the bar, percentage, ETA, **separate read/write throughput**, **buffer-pool
occupancy**, the current (possibly shrunk) block size, and bytes skipped.
Fresh regular-file destinations stay sparse (all-zero chunks aren't written);
block-device / pre-existing destinations require `--force` and are written
faithfully. **Ctrl-C** cancels cleanly and the final summary reports how far
the copy got and what was skipped.
### Fixed
- *(hfs+, fat32)* **near-instant `create` on large block devices.** Both
formatters zeroed the *entire* volume up front (`zero_range(0, whole device)`),
so `fstool create --type hfsplus --output /dev/sdX` on a multi-hundred-GB
device wrote hundreds of GB of zeros — minutes of I/O. They now zero only the
metadata regions (HFS+: the special-file blocks at the start + the alternate
volume header; FAT32: the reserved sectors + both FATs + the root cluster).
Data/free blocks are marked free in the bitmap / FAT and never read back, so
their prior contents don't matter. Formatting an empty 200 GiB HFS+ volume
dropped from minutes to ~0.03 s (FAT32 likewise from a >20 s timeout to
~0.3 s); both stay `fsck.hfsplus` / `fsck.vfat`-clean.
- *(info)* drop the stale "read support is scaffold-only" note that `fstool
info` printed for NTFS, F2FS, and SquashFS — all three have had full read
support for some time (the note dated from when they were detection-only).
NTFS now prints no caveat; F2FS notes its build-once write model and SquashFS
its repack-only write. Also corrected the matching stale doc comments on the
`FsKind` / `AnyFs` variants in `src/inspect.rs`.
## [0.4.12](https://github.com/KarpelesLab/fstool/compare/v0.4.11...v0.4.12) - 2026-06-07
### Added
- *(shell)* richer find (time/sort/limit/types) and grep (-v/-l/-c)
- *(shell)* Ctrl-C cancels a running find/grep without killing the shell
- *(shell)* add `find` and `grep` (binary matches as hexdump -C)
- *(affs)* true incremental in-place editing of OFS/FFS images
### Fixed
- *(affs)* reword editor doc comment to avoid clippy doc_lazy_continuation
### Other
- *(apfs)* make README status accurate (read snapshots/xattrs, write via macOS-mount, honest gaps)
- *(qcow2)* clean errors instead of panics in the compressed writer
## [0.4.11](https://github.com/KarpelesLab/fstool/compare/v0.4.10...v0.4.11) - 2026-06-03
### Added
- *(qcow2)* produce compressed images — --compress on create/build/repack/convert
- *(qcow2)* copy-on-write when writing into a compressed cluster
- *(qcow2)* read compressed clusters (zlib + zstd)
- *(affs)* in-place mutation for Amiga OFS/FFS (phase 3)
- *(affs)* Amiga OFS/FFS writer — generate from scratch (phase 2)
- *(affs)* Amiga OFS/FFS read support (phase 1)
- *(hfs)* in-place mutation — open_writable + add/remove (Phase 3)
- *(hfs)* classic-HFS writer — create / build / repack (Phase 2)
### Fixed
- *(hfs)* zero filStBlk in file records (the last fsck error)
- *(hfs)* 46-byte (Str31) thread records + zero FInfo
- *(hfs)* variable even-padded thread records + root directory valence
- *(hfs)* empty B-tree is header-only (extents-overflow file)
- *(hfs)* even-align catalog records + write MDB volume counts
- *(hfs)* index-node B-tree records must use fixed-length keys
### Other
- *(affs)* don't intra-doc-link the private `writer` module
- *(examples)* add Raspberry Pi, EFI, and legacy-BIOS disk specs
### Added
- *(qcow2)* read **compressed clusters** — both zlib/deflate (qemu's default)
and zstd. So every operation (`info`, `ls`, `cat`, `shell`, `add`, `repack`,
`convert`, FUSE) now works transparently on compressed qcow2 images such as
`qemu-img convert -c` output and distro/cloud images. Deflate clusters decode
with a 4 KiB sliding window (matching qemu's `inflateInit2(-12)` and bounding
per-cluster RAM); the L2 `COMPRESSED` entry and v3 `compression_type` header
field are parsed, and a one-cluster decompression cache keeps sequential
sub-cluster reads cheap. New `src/block/qcow2/compress.rs`; bumps `compcol`
to 0.6 for its `deflate` window knobs. Cross-checked byte-exact against
`qemu-img`-produced zlib and zstd images. Writing into a compressed cluster
copies it out to a plain cluster first (qemu's behaviour) — decompress,
allocate, repoint the L2 entry, and release the old cluster's (possibly
shared) host-range refcounts — so `add`/shell edits of a compressed image
work and stay `qemu-img check`-clean.
- *(qcow2)* **produce** compressed qcow2 images: a `--compress[=SPEC]` flag on
`create` / `build` / `repack` / `convert` (`--compress`, `--compress=9`,
`--compress=zstd`, `--compress=zstd:9`) serialises a fresh compressed image —
each non-zero cluster compressed once (zeros stay sparse), payloads packed
byte-granularly, with L1/L2/refcount tables (and exact shared-host-cluster
refcounts) and a header carrying `compression_type` (+ the COMPRESSION_TYPE
incompatible bit for zstd). Deflate uses a 4 KiB match window so qemu reads
it. Validated with `qemu-img check` + `qemu-img convert -O raw` byte-exact,
for both codecs.
- *(affs)* in-place mutation for **Amiga OFS/FFS**: `Affs::open_writable` loads
an existing `.adf` (every directory plus the bytes of every file) into the
in-memory tree, so `fstool add` / `rm` and shell `put` / `mkdir` / `rm` edit a
volume in place — the whole image is re-laid-out (and re-checksummed) on flush,
preserving untouched files byte-exact. `AnyFs::open_writable` routes AFFS here;
`list`/`open_file_reader` serve pending edits from the model before flush.
- *(affs)* new **Amiga OFS/FFS** (`.adf`) read + generate support. Reads the
boot-block variant (`DOS\0`..`DOS\7`: FFS/OFS, International, directory-cache),
the root block, hash-table directories (and same-hash chains), and files via
the file header + extension blocks — serving both OFS (24-byte per-block data
headers, 488 payload bytes) and FFS (raw 512-byte) data. Names decode as
Latin-1; dates use the Amiga 1978 epoch. **Write**: `fstool create -t affs`
(or `-t ofs`), `build`, and `repack` generate fresh OFS or FFS volumes
(default DOS\3 FFS+INTL; `-O fstype=ofs,intl=false` to vary) via an in-memory
tree serialised block-by-block on flush, with correct block checksums, name
hashing (ASCII + International), file-extension chaining, and a volume bitmap.
New `src/fs/affs/` (`mod.rs` reader, `writer.rs`); wired into detection,
`info`, `ls`, `cat`, `create`, `build`. Layout follows adflib's `adf_blk.h`;
the reader is validated against real OFS/FFS Workbench volumes and the writer's
output is checked for block-checksum / hash-slot / bitmap conformance (the
exact invariants the Linux kernel `affs` driver enforces). In-place mutation
(`add`/`rm`) lands next.
- *(hfs)* classic-HFS is now **read + write**. `fstool create -t hfs`, `build`,
and `repack` generate fresh volumes, and `add` / `rm` / shell `put`/`mkdir`
mutate an existing image **in place** (`Hfs::open_writable` loads the catalog,
mutations rebuild it, `flush` writes catalog + extents + bitmap + MDB). Ports
the HFS+ writer's design (catalog as an in-memory `BTreeMap`, on-disk B-trees
rebuilt by greedy 512-byte node packing) with HFS specifics: MacRoman names +
case-insensitive catalog collation, MDB + volume bitmap, up-to-3-extent B-tree
files. New `src/fs/hfs/writer.rs` and `macroman::encode`/`cmp_ci`. Validated by
reader round-trips (create + in-place; a strict B-tree key-order check) and, on
macOS CI, `fsck_hfs` (via `hdiutil attach`; the Linux `fsck.hfsplus` segfaults
on classic HFS — confirmed on a genuine System 6.0.8 volume — so it is not
used). Classic HFS has no symlinks, so `create_symlink` is `Unsupported`.
## [0.4.10](https://github.com/KarpelesLab/fstool/compare/v0.4.9...v0.4.10) - 2026-05-30
### Added
- *(hfs+)* resource-fork support (read, inventory, decode, extract)
- *(hfs)* resource-fork support — read, inventory, decode, extract
- *(cli)* --path-style {unix|native} + canonical HFS/HFS+ slash handling
- *(cli)* ls -R recursion + readline line editing in the shell
- *(part)* Apple Partition Map (APM) read-only support
- *(hfs)* classic HFS read-only reader (DiskCopy 4.2 floppies, System ≤ 8)
- *(block)* DiskCopy 4.2 container backend (transparent unwrap)
- *(sevenz)* 7-Zip read-only reader (Copy/LZMA/BZip2/Deflate; rest pending compcol)
- *(sit)* StuffIt classic SIT! read-only reader (store; rest pending compcol)
- *(arc)* SEA ARC read-only reader (stored methods; compressed pending compcol)
- *(lha)* LHA/LZH read-only reader (lh0 store; lh-series pending compcol)
### Other
- *(release-plz)* authenticate with RELEASE_PLZ_TOKEN (PAT)
- *(archive)* skip 7z/lha cross-checks when the reference tool misbehaves
- *(archive)* update scaffold test now that 7z/lha/arc/sit decode
### Added
- *(hfs+)* **resource-fork** support for HFS+/HFSX, matching classic HFS:
`HfsPlus::open_resource_fork_reader` reads the fork via the existing
fork-type-`0xFF` extent machinery, `cat --rsrc` / `resources` work on HFS+
files, and `list_xattrs` surfaces `com.apple.ResourceFork`. HFS-compressed
files are excluded (their resource fork holds `decmpfs` storage, not a user
resource fork).
- *(hfs)* classic-HFS **resource-fork** support. The reader now reads each
file's resource fork (its own extents, fork-type `0xFF`) and surfaces it three
ways: `fstool cat --rsrc <img> <path>` streams the raw fork; a new `fstool
resources <img> <path>` command parses the resource map and lists every type
with each resource's id/name/size and a decoded summary for common types
(`vers`, `STR `, `STR#`, `TEXT`, `ICN#`/`ICON`, `DITL`), with `--extract
TYPE:ID` to dump one resource; and `list_xattrs` exposes the fork as the
macOS-standard `com.apple.ResourceFork` xattr (so it shows in `info` and rides
through `repack`/`add` to xattr-capable targets). New filesystem-agnostic
`resfork` module + a crate-level `macroman` module (promoted from the HFS
reader).
- *(cli)* global `--path-style {unix|native}` flag. `unix` (default) separates
every path with `/` and shows a literal `/` inside an HFS/HFS+ name as `:`
(the macOS convention); `native` uses the filesystem's own separator (`:` for
HFS/HFS+, `\` for FAT/exFAT/NTFS, `/` elsewhere) and preserves real
filenames. Translation happens only at the CLI/shell boundary — readers,
`repack`, and on-disk formats are unaffected.
- *(cli)* `fstool ls -R` / `--recursive` — walk subdirectories, printing each
directory under a `path:` header (like `ls -R`). Works on both block-device
images and streamed `.tar.<algo>` archives; never descends the `.`/`..`
self/parent links.
- *(shell)* the interactive `fstool shell` now has **line editing and command
history** on a TTY (↑/↓ to recall, Ctrl-A/E, Ctrl-R reverse search) via
`rustyline`, with history persisted to `~/.fstool_history`. Behind the
default-on `readline` feature; piped/non-TTY input keeps the deterministic
line-buffered reader, and `default-features = false` drops the dependency.
- *(hfs)* classic **HFS** (Hierarchical File System, Mac OS ≤ 8) read-only
reader — parses the Master Directory Block at offset 1024 (`BD` signature),
loads the catalog + extents-overflow B-trees into memory (512-byte nodes,
MacRoman Pascal names) and exposes each file's **data fork**. Resolves nested
paths and streams file contents via allocation-block extents. Validated
against a genuine System 6.0.8 disk image (extracts the real System/Finder/
Read Me contents) plus a synthetic-volume regression test. Resource forks,
HFS-wrapped HFS+ and creation are unsupported.
- *(block)* **DiskCopy 4.2** container backend — a read-only device wrapper
that exposes the inner volume (data fork at file offset `0x54`), probed in
`open_image` after qcow2/dmg so a DiskCopy-wrapped floppy (classic HFS, FAT,
ISO, …) is detected and read transparently like a raw image.
- *(part)* **Apple Partition Map** (APM) read-only support — the classic Mac /
PowerPC / `.toast` partitioning scheme. Detected via the Driver Descriptor
Map (`ER` at block 0) plus the `PM` partition map, surfaced exactly like
GPT/MBR: `fstool info disk.toast` lists the `Apple_HFS` / `Apple_Free` /
`Apple_partition_map` entries and `disk.toast:N` slices partition *N* (e.g.
reading the wrapped classic-HFS volume). Writing an APM is unsupported.
- *(sevenz)* 7-Zip (`.7z`) read-only reader behind the `sevenz` feature —
parses the full container (32-byte signature header, the optionally
LZMA-packed `kEncodedHeader` end header, `StreamsInfo` folders/coders/
substreams and `FilesInfo` UTF-16 names + empty-stream/empty-file vectors)
and maps every file to its folder substream. Single-coder **Copy / LZMA /
BZip2 / Deflate** folders decode (solid folders are decoded once and sliced
per substream; LZMA reuses compcol's `.lzma` decoder via a synthesized
header), cross-checked against the reference `7z` tool. LZMA2 (the 7-Zip
default), BCJ/Delta filters, PPMd, encryption and any multi-coder pipeline
list correctly but read as a clean `Unsupported`, pending a raw-LZMA2 entry
point + branch-filter codecs in `compcol`. Creation is unsupported. This
completes the archive table — **no detection-only scaffolds remain**.
- *(sit)* StuffIt (`.sit`) read-only reader behind the `sit` feature — parses
the **classic** `SIT!` container (22-byte archive header + 112-byte per-file
entry headers, resource + data forks, big-endian) and indexes every member
by its data fork, honouring the folder start/end markers for nested paths.
Data-fork method 0 (store) decodes today; the compressed methods (RLE90,
LZW, Huffman, LZAH, LZ+Huffman, Arsenic, …) and the entire StuffIt 5 format
list/detect but read as a clean `Unsupported` pending StuffIt codecs in
`compcol`. Creation is unsupported.
- *(arc)* SEA ARC (`.arc`) read-only reader behind the `arc` feature — walks
the flat per-file header chain and indexes every member. The stored methods
(1 = old, 2 = with an original-size field) decode today; the compressed
methods (3 RLE90, 4 squeeze, 5–9 crunch/squash) list correctly but read as a
clean `Unsupported` pending ARC codecs in `compcol`. Creation is unsupported.
- *(lha)* LHA / LZH (`.lzh`, `.lha`) read-only reader behind the `lha` feature
— walks the header chain at levels 0, 1 and 2 (incl. the level-1 skip-size /
extended-header math and level-2 ext-header filenames + directory
components) and indexes every member. `-lh0-` store decodes today
(cross-checked against the reference `lha` tool with genuine fixtures at all
three header levels); the lh1/4/5/6/7 LZSS+Huffman methods list correctly but
read as a clean `Unsupported` pending an `lha` codec in `compcol`. Creation
is unsupported.
### Changed
- *(inspect)* the "no recognised filesystem" error no longer enumerates every
supported format — the growing list made the message hard to read. It now
reads simply `no recognised filesystem or archive on this image`.
- *(hfs, hfs+)* a literal `/` inside a classic-Mac filename (legal there, since
the separator is `:`) is now canonicalised to `:` on listing and resolution,
so it can't be mistaken for a path separator. Fixes mis-resolution / `ls -R`
aborting on real volumes (e.g. a directory named `A/ROSE Includes`), and
means such names repack into a tar/zip as `A:ROSE Includes`. HFS+ previously
left the raw `/` in place (latent bug); it now matches classic HFS.
## [0.4.9](https://github.com/KarpelesLab/fstool/compare/v0.4.8...v0.4.9) - 2026-05-30
### Added
- *(dmg)* switch encrypted-DMG crypto to purecrypto
- *(rar)* support solid RAR5 archives, decoding the group once
- *(rar)* RAR5 read-only reader (store + compressed) via compcol::rar5
### Fixed
- *(qcow2)* bound L1 table by file length, not minimum entries
- *(repack)* bound source directory walk against cycles + strip '..'
- *(archive,grf)* bounds-check entry fields and cap untrusted allocations
- *(iso9660,squashfs,tar)* cap untrusted allocations + bound RR/PAX parsing
- *(f2fs,exfat,fat)* cap untrusted-size allocations and validate geometry
- *(apfs)* bound B-tree descent + checked spaceman math against malicious images
- *(hfs+)* harden HFS+ reader against malicious images
- *(xfs)* harden XFS reader against malicious images
- *(ntfs)* harden NTFS reader against malicious images
- *(ext)* harden ext2/3/4 reader against malicious images
- *(block,part)* validate GPT/DMG/qcow2 header fields against malicious images
- *(doc)* drop intra-doc links to private items (cargo doc -D warnings)
### Other
- *(changelog)* record security hardening pass
## [0.4.8](https://github.com/KarpelesLab/fstool/compare/v0.4.7...v0.4.8) - 2026-05-29
### Added
- *(compression)* move lzma to compcol; drop lzma-rs (sole codec backend)
- *(lzx)* Amiga LZX (.lzx) read-only reader via compcol
- *(dmg)* decode bzip2 + LZFSE chunks via compcol; drop bzip2-rs
- *(compression)* move lz4 + lzo to compcol; drop lz4_flex + minilzo-rs
- *(cab)* multi-block MSZIP via compcol 0.4.3 preset dictionary
- *(cab)* read-only Microsoft Cabinet reader via compcol
- *(compression)* retire flate2 — zip/DMG/HFS+ zlib+deflate on compcol
- *(compression)* route gzip/zlib/xz/zstd through compcol
- *(ext4)* arbitrary-depth extent tree writes (rw + streaming)
- *(apfs)* accept hashed-key (case-insensitive) volumes for mutation
- *(apfs)* apfs_drec_name_len_and_hash + DrecKeyLayout in build_drec_record
- *(cli)* fstool shell --ro for safe read-only browsing
- *(apfs)* refuse Apfs::open_writable on case-insensitive volumes
- *(apfs)* wire Filesystem::truncate + override list_xattrs
- *(apfs)* thread mtime through create_*_at + Filesystem create paths
- *(apfs)* wire CLI mutators through Apfs::open_writable
- *(apfs)* ring-buffer the xp_desc area so checkpoints don't exhaust
- *(apfs)* wire Filesystem trait through Write-state mutators
- *(apfs)* Write-state create_file_at / create_dir_at / create_symlink_at + xattr setters
### Fixed
- *(cli)* refuse compressed sources for mutators; refuse streaming FS for shell
- *(apfs)* drop redundant drop(cx) flagged by clippy
### Other
- bump compcol to 0.4.4
- *(cab)* stream folder extraction instead of buffering whole folder
- *(fuzz)* make fuzz core deterministic — BTreeMap instead of HashMap
- *(apfs)* macOS-gated fsck_apfs on hashed-key open_writable creates
- *(apfs)* macOS-gated fsck_apfs run on open_writable create flow
- *(apfs)* fold commit_checkpoint into commit_with_mutator
- *(apfs)* introduce MutatorCx, generalise commit_with_mutator closure
- *(apfs)* extract record builders to pub(crate) free functions
## [0.4.7](https://github.com/KarpelesLab/fstool/compare/v0.4.6...v0.4.7) - 2026-05-27
### Added
- *(ext)* triple-indirect, LARGE_FILE, and prezeroed fast-path
- *(repack)* truncate filename from the left to fit a narrow PTY
- *(repack)* progress bar during the copy phase
### Fixed
- *(qcow2)* keep image sparse for zero writes to unmapped clusters
- *(create)* auto-size from source instead of the 1 MiB default
- *(repack)* reset file counter at each phase, not summed across passes
### Other
- drop private-item intra-doc link in file_block
- cargo fmt the new tests + helper closure
## [0.4.6](https://github.com/KarpelesLab/fstool/compare/v0.4.5...v0.4.6) - 2026-05-27
### Added
- *(merge)* hard links + fix(fat): flush dir batches before read
### Fixed
- *(doc)* resolve merge.rs intra-doc links for `cargo doc -D warnings`
- *(repack)* don't strip Windows drive letters from tar paths
### Other
- *(repack)* unify plain + compressed tar arms in walk_source_into_sink
- *(cli)* stream plain tar sources too — kill the random-access Tar::open
- *(ext)* O(1) data-block allocator via per-group cursor
- *(fat32)* O(1) child_exists via per-parent name index
- *(iso9660)* tree children → BTreeMap, kills O(n²) insert + lookup
- *(f2fs)* lazy `i_addr` Vec — 8× RAM cut on bulk-insert workloads
## [0.4.5](https://github.com/KarpelesLab/fstool/compare/v0.4.4...v0.4.5) - 2026-05-26
### Other
- *(merge)* in-memory model + per-source ordered emission, no tempfile
- *(repack)* stream tar into zip/cpio + tar→tar, drop archive temp files
- *(repack)* stream compressed tar into squashfs/iso/grf, no tempfile
- *(iso9660)* stream file data to the device, no temp file, bounded RAM
- *(grf)* stream body into the archive directly, no temp file
- *(squashfs)* stream file data to the device, no temp files
- *(clone)* buffer small clones in memory instead of a temp file
## [0.4.4](https://github.com/KarpelesLab/fstool/compare/v0.4.3...v0.4.4) - 2026-05-25
### Fixed
- *(ntfs)* size resident $DATA by actual $SI/$FN length (fuzz panic)
### Other
- *(repack)* stop spilling every streamed file to a temp file
- *(hfs+)* bump-cursor allocation — drop O(n²) from large-dir builds
- *(f2fs)* O(1) directory lookups — drop O(n²) from large-dir builds
## [0.4.3](https://github.com/KarpelesLab/fstool/compare/v0.4.2...v0.4.3) - 2026-05-25
### Added
- *(f2fs)* hashed multi-level directories — large dirs pass fsck.f2fs
- *(hfs+)* grow catalog B-tree + correct clump size — 100k files clean
- *(xfs)* 2-level INOBT — 100k+ files in one directory pass xfs_repair
- *(xfs)* leaf + node directories and aligned inode chunks (to ~16k files)
- *(ext4)* incremental depth-2 extent growth for large directories
- *(ext4)* depth-N extent trees + journal/flex_bg sizing for large dirs
- *(analyze)* generic source-analysis API + `fstool analyze` command
- *(repack)* stream compressed-tar sources — no decompress-to-tempfile
- *(repack)* phase markers + wire up the per-file progress counter
- *(shell)* `info <path>` dumps per-file metadata + xattrs
### Fixed
- *(xfs)* escape `bestfree[0]` in doc comment to unbreak cargo doc
- *(xfs)* clean error instead of panic on block-dir overflow
- *(ntfs)* scale directories + $MFT to 100k files (clean ntfs-3g mount)
- *(ext4)* one-shot build path promotes to depth-1 extent tree
### Other
- *(f2fs)* mark large-directory test ignored — known writer limitation
- *(f2fs)* large-directory guard (read-back local, fsck.f2fs in CI)
- *(ntfs)* external scale guard — 4000-file dir mounts ntfsfix-clean
- *(exfat)* batch directory writes via DirBatch + lookup overlay
- *(fat)* batch directory writes via DirBatch + lookup overlay
- *(xfs)* batch directory writes via DirBatch + lookup overlay
- *(ntfs,ext)* batch directory writes; add shared DirBatch cache
- *(squashfs)* multithread block compression by default
- *(repack)* gate compressed-tar stream test to Unix
## [0.4.2](https://github.com/KarpelesLab/fstool/compare/v0.4.1...v0.4.2) - 2026-05-25
### Added
- *(xfs)* refuse open_file_rw on REFLINK files — prevent clone corruption (Phase 3b stage 3)
- *(xfs)* clone_file via shared extents + REFCNTBT records (Phase 3b stage 2)
- *(xfs)* REFLINK feature opt-in + per-AG REFCNTBT root (Phase 3b stage 1)
- *(fs)* clone API — Filesystem::clone_file / clone_range + CloneCapability (Phase 3a)
- *(ntfs)* create_device for char/block via INTX_FILE; sort $I30 entries
- *(hfs+)* create_device — char / block / FIFO / socket nodes
- *(ntfs)* implement remove (file / empty-dir / symlink), the inverse of create
- *(ntfs)* make a reopened image mutable (lazy writer reconstruction)
- *(ntfs)* getattr (times + synthesised mode) and list_xattrs
- *(hfs+)* faithful getattr
- *(iso9660)* faithful getattr from Rock Ridge
- *(apfs)* faithful getattr
- *(archive)* shared archive core + zip/cpio/ar backends, 7 scaffolds
### Fixed
- *(fs)* owned-tempfile FileSource for deferred-write backends; SquashFS getattr
### Other
- fix 5 broken intra-doc links + BSD-ar cross-check on macOS
- *(dmg)* end-to-end against hdiutil on macOS (UDRW / UDZO / UDBZ / ULFO)
- *(fuzz)* NTFS fuzz target + Op::Clone with shares_extents freezing
- F2FS is build-once — correct the in-place-edits column
- cross-backend reopen-mutate sweep; make F2FS advertise build-once
- every repack source reader now surfaces faithful metadata
- move qcow2 / dmg out of the filesystem-support table
- *(repack)* unify pipeline — one walker + sink, no per-pair paths
- lib-level fuzz across 8 mutable backends
- *(ext)* cover multi-open_file_rw write extending file across drops
### Changed
- *(repack)* unified the repack pipeline: one generic source walker feeds
one of two sinks (a streaming-tar sink or a block-device `Filesystem`
sink). The per-`(source,dest)`-type copiers are gone — any readable
source now repacks into any writable destination through a single
trait-driven path. The only branch is streaming (tar / `.tar.<codec>`)
vs non-streaming output. Previously-rejected combinations now work
(e.g. `repack app.zip out.tar`, `repack image.xfs out.tar`).
- *(fs)* `Filesystem` gains `create_file_streaming` (zero-copy body
streaming, no per-file tempfile; ext/fat32/exfat override it) and a
batch `set_xattrs`. Faithful `getattr` (real mode/uid/gid/times, and
xattrs/device numbers where stored) now on tar, f2fs, and XFS sources
in addition to ext — so repacking from them preserves metadata.
### Added
- *(archive)* shared archive core (`src/fs/archive/`) — an indexed-entry
model plus a generic read-only `Filesystem` implementation that archive
formats plug into by supplying a scanner (and, if writable, a builder).
- *(archive)* **zip** — full read (central-directory scan, robust EOCD
search, ZIP64, Unix mode/symlinks, Shift-JIS/EUC-JP/UTF-8 filename
detection) and write (Stored + Deflate, CRC-32, ZIP64 when needed).
Reads archives produced by other tools; output validates with `unzip`.
- *(archive)* **cpio** — read newc/odc + write newc; round-trips through
system `cpio`.
- *(archive)* **ar** — read GNU + BSD long names, write GNU; round-trips
through system `ar`. Flat archive (rejects nested paths).
- *(archive)* detection-only scaffolds for **7z, rar, arc, lha, lzx, cab,
sit** — recognised by `info`, with a clean `Unsupported` on read until
pure-Rust decoders are wired (per format, behind a future Cargo feature).
- *(cli)* `create -t {zip,cpio,ar}`, `repack --fs-type {zip,cpio,ar}`,
`build` with `type = "zip"|"cpio"|"ar"`, and `mount` for all archive
formats; archive output is truncated to its exact length.
## [0.4.1](https://github.com/KarpelesLab/fstool/compare/v0.4.0...v0.4.1) - 2026-05-22
### Added
- *(fuse)* backend-agnostic adapter — mount any Filesystem via FUSE
- *(apfs)* rename, unlink (hardlink-aware), and link()
- *(apfs)* chmod / chown / set_times mutation API
- shared-access wrapper for cross-thread Ext usage (Phase E)
- fuzz harness + crash-injection block device (Phase D)
- *(ext)* inline_data — store small files in the inode
- FUSE adapter — mount ext{2,3,4} images as a userspace filesystem
- *(ext)* post-build mutation API (chmod, chown, set_times, truncate, rename)
- *(ext)* multi-descriptor JBD2 transactions + fix dx_node header
- *(ext)* two-level HTree (dx_node intermediates)
- *(repack)* replay pending JBD2 journal on the source before reading
- *(repack)* preserve sparse files in ext repack
- *(ext)* preserve hard links across repack
- *(ext)* HTree (DIR_INDEX) write-side support for ext4
- *(ext)* multi-block directories, depth-1 extents, repack progress
### Fixed
- *(clippy)* clean up 11 lints exposed by --all-features build
- *(concurrent)* drop unused `FileSource` import from test module
- *(repack)* wire progress sink through tar-output paths
### Other
- *(fuse)* kernel round-trip test via spawn_mount
- fix 7 broken intra-doc links exposed by --all-features doc build
- install libfuse3-dev + pkg-config on Linux for clippy --all-features
- cargo fmt across recent landings
## [0.4.0](https://github.com/KarpelesLab/fstool/compare/v0.3.1...v0.4.0) - 2026-05-21
### Added
- *(cli)* unify create + add -O / [filesystem.options] for FS knobs
### Fixed
- *(spec)* mark FilesystemSpec #[non_exhaustive]
### Other
- *(readme)* refresh FS matrix + limitations for current state
## [0.3.1](https://github.com/KarpelesLab/fstool/compare/v0.3.0...v0.3.1) - 2026-05-21
### Added
- *(ext)* real JBD2 transactions for open_file_rw (Path A)
- *(apfs)* open_file_rw on flushed images via fresh checkpoint COW
- *(xfs)* leaf-form xattrs (read+write) + remove_xattr
- *(hfs+)* decmpfs read support (types 3 + 4 zlib)
- *(ntfs)* real $LogFile LFS records (Path A) for open_file_rw
- *(dmg)* encrcdsa v2 encrypted DMG read support
### Fixed
- *(hfs+)* keep HfsPlusFileReader as struct to preserve public API
### Other
- drop intra-doc links to private items in apfs
## [0.3.0](https://github.com/KarpelesLab/fstool/compare/v0.2.0...v0.3.0) - 2026-05-20
### Added
- *(hfs+)* route flush metadata writes through journal (Path A)
- *(ntfs)* multi-SD $Secure (User + System); defer $LogFile Path A
- *(xfs)* multi-level B-tree dirs + Path A log transactions
- *(ext4)* open_file_rw on depth-1 extent trees
- *(apfs)* populate IP ring, SFQ free-queues, and main-device alloc zone
- *(dmg)* implement ADC, bzip2, LZFSE, and LZMA chunk codecs
- *(hfs+)* real journal transactions (Path A) for open_file_rw
- *(ntfs)* populate $Secure ($SDS/$SDH/$SII) + sort root $I30
- *(xfs)* single-level B-tree directory reader (di_format=BTREE)
- *(ext4)* open_file_rw on depth-0 inline extent trees
- *(dmg)* chunk decoder — zero / raw / zlib over UDIF v4
- *(apfs)* emit a real spaceman bitmap + checkpoint map
- *(fs)* implement open_file_ro for ext/FAT/exFAT/F2FS/HFS+/NTFS/XFS
- *(apfs)* implement Filesystem::open_file_ro
- *(squashfs)* implement Filesystem::open_file_ro
- *(grf)* implement Filesystem::open_file_ro
- *(iso9660)* implement Filesystem::open_file_ro for random-access reads
- *(fs)* add Filesystem::open_file_ro + FileReadHandle
- *(xfs)* implement Filesystem::open_file_rw via clean-unmount bypass
- *(ntfs)* implement Filesystem::open_file_rw for in-place edits
- *(ext3/4)* accept clean-journal images in open_file_rw
- *(hfs+)* implement Filesystem::open_file_rw for in-place edits
- *(f2fs)* implement Filesystem::open_file_rw for in-place edits
- *(ext2)* implement Filesystem::open_file_rw for in-place edits
- *(fat)* implement Filesystem::open_file_rw for in-place edits
- *(exfat)* implement Filesystem::open_file_rw for in-place edits
- *(fs)* add Filesystem::open_file_rw + FileHandle for in-place edits
- *(apfs)* wire library writer through Filesystem trait
- *(hfs+)* make open() return a writable handle for add/rm round-trips
- *(ntfs)* index system files (records 0..=15) in root $I30 on format
- *(exfat)* wire writer into the Filesystem trait
- *(grf)* GRF (Gravity Ragnarok File) read + write + add/rm
- *(fs)* add MutationCapability::WholeFileOnly for future formats
- *(error)* typed Error::RepackOnly for sequential-by-design FSes
### Fixed
- *(hfs+)* clamp VH nextAllocation < totalBlocks for fsck.hfsplus
- *(exfat)* drop unused FileHandle import in open_file_rw tests
- *(iso9660)* emit SUSP SP marker on root's "." dir record
- *(repack)* Source::detect mishandled Windows drive letters
### Other
- replace links to private items with plain backticks
- cargo fmt across drifted files
- *(ext/flex_bg)* tighten leader/follower mapping check + e2fsck-clean
- resume writes from on-disk AGF/AGI/INOBT/BNO after reopen
- *(hfs+)* lock down create_hardlink link-inode invariant
- *(xfs/dir)* cover dahashname, leaf sort, and i8 shortform decode
- *(squashfs)* cover fragment table reader
- *(ext)* end-to-end xattr round-trip through set_xattrs + read_xattrs
- fix broken intra-doc links from public items into pub(crate)
- rustfmt across the tree
- *(error)* split Streaming vs Immutable instead of one RepackOnly
- collapse build-plan walkers through Filesystem::read_symlink
## [0.2.0](https://github.com/KarpelesLab/fstool/compare/v0.1.0...v0.2.0) - 2026-05-20
### Added
- *(inspect)* variant-agnostic public surface — inspect::open + summary
- *(fs)* Filesystem::supports_mutation() gates add/rm cleanly
- *(cli)* repack accepts positional sources — `repack a b … out`
- *(repack)* layered sources with tar-OCI + overlayfs whiteouts
- *(iso9660)* writer + Filesystem trait + repack-to-ISO wiring
- *(iso9660)* read support — PVD + Joliet + Rock Ridge + El Torito
- *(cli,docs)* wire repack to write XFS/HFS+/NTFS/F2FS/SquashFS via the trait
- *(fs)* wire all writable FSes (XFS/HFS+/NTFS/F2FS/SquashFS/FAT32) through one trait
### Other
- collapse sum_*_file_bytes into Filesystem::total_file_bytes
- *(readme)* cover ISO 9660 + layered merge with whiteouts
## [0.1.0](https://github.com/KarpelesLab/fstool/compare/v0.0.5...v0.1.0) - 2026-05-20
### Added
- *(block)* scaffold Apple DMG (UDIF v4) container support
- *(tar)* random-access index + hardlink materialization + tar.<algo>→ext repack
- *(squashfs)* hardlinks + device nodes + multi-fragment + ext-dir promotion
- *(f2fs)* hard links + triple-indirect nodes + multi-block dentry spill
- *(ntfs)* writer — format + create_file/dir/symlink + flush
- *(apfs)* multi-leaf writer + embedded xattrs (read + write)
- *(hfs+)* extents-overflow spill on write + hard links + journal stub
- *(xfs)* journal stub + multi-AG writes + remove + shortform xattrs
- *(ext)* BuildPlan auto-flex_bg + INCOMPAT_64BIT writer + sparse_super2
- *(tar)* TarStreamReader/Writer + CLI streaming integration (no tempfile)
- *(squashfs)* writer + xattr / id-table / export-table coverage
- *(f2fs)* writer (format, create_file/dir/symlink/device, remove, flush)
- *(ntfs)* fill read-side holes (attr-list, $Secure, $UpCase, LZNT1)
- *(apfs)* multi-volume + snapshots (read) + minimal writer
- *(hfs+)* writer (format, create_dir/file/symlink, remove, flush)
- *(xfs)* B+tree directories + write support (format, add_file/dir/symlink/device)
- *(ext)* flex_bg writer (opt-in via FormatOpts)
- *(compression)* codec features for squashfs reads and tar I/O
### Fixed
- *(hfs+)* drop intra-doc link from public to private fold_case
- *(hfs+)* make fsck.hfsplus accept writer output end-to-end
- *(hfs+)* mark Private Data dir invisible in Finder (frFlags |= kIsInvisible)
- *(hfs+)* set HasLinkChain / HasChildLink flags on hardlink records
- *(hfs+)* iNode files need fileType='iNod' / creator='hfs+' + link count
- *(hfs+)* catalog case-folding compare ignores NUL code units
- *(hfs+)* map record fills the rest of the header node
- *(hfs+)* empty B-trees need a header AND one empty leaf node
- *(hfs+)* B-tree forks need clumpSize ≥ nodeSize
- *(f2fs)* populate valid_node/inode/free_segment counts in CP head
- *(f2fs)* SIT valid_map is MSB-first, not LSB-first
- *(f2fs)* I_ADDR_OFFSET must be 0x168 (kernel spec), not 0xD0
- *(f2fs)* inline-dentry INLINE_RESERVED_SIZE is 7 bytes, not 1
- *(f2fs)* inline payload starts at i_addr[1], not i_addr[0]
- *(f2fs)* emit "." and ".." dentries + correct i_blocks
- *(f2fs)* real curseg layout + SIT type bits + node_footer
- *(f2fs)* NAT entries for node_ino / meta_ino + drop bogus NAT/SIT/SSA CRC
- *(f2fs)* write 8-block CP pack + drop bogus reserved-nid NAT entries
- *(f2fs)* SIT segment count must be even + derive bitmap size from geometry
- *(f2fs)* non-zero rsvd / overprov segments + correct user_block_count
- *(f2fs)* write CP footer at end of pack + correct CP flag values
- *(f2fs)* use real crc32_le(F2FS_SUPER_MAGIC, …) + correct CP field offsets
- *(f2fs)* segment0_blkaddr = cp_blkaddr + ignore reverse-read test
- *(f2fs,ci)* correct f2fs SB field offsets + drop deprecated brew ntfs-3g
### Other
- rustfmt insert_journal_entry signature
- *(readme)* update FS support table for current writer coverage
- Revert "fix(hfs+): catalog case-folding compare ignores NUL code units"
- *(hfs+)* diagnostic also tries mkfs.hfsplus (hfsprogs spelling)
- *(hfs+)* add diagnostic test to dump mkfs vs fstool extents header
- rustfmt write.rs after CP-pack restructure
- *(fs)* native-tool external validation for exfat/xfs/hfs+/apfs/ntfs/f2fs/squashfs + codec fixes
- *(release-plz)* fix release-binaries dispatch (tag schema + actions:write)
- cargo fmt --all
## [0.0.5](https://github.com/KarpelesLab/fstool/compare/v0.0.4...v0.0.5) - 2026-05-19
### Added
- *(fs)* fill out xfs/hfs+/apfs/ntfs/f2fs/squashfs read paths + exfat writer
- *(fs)* xfs/exfat/hfs+/apfs read-only + ntfs/f2fs/squashfs scaffolds
- *(tar)* tar as a read/write filesystem — ext↔tar / fat↔tar repack
### Other
- gate Unix-only integration tests for the Windows / macOS matrix
- *(release-plz)* chain release-binaries via workflow_dispatch
## [0.0.4](https://github.com/KarpelesLab/fstool/compare/v0.0.3...v0.0.4) - 2026-05-19
### Added
- *(ext)* xattr support — read inline + block, write block, preserve on repack
- *(cli)* convert + repack — byte-copy and FS-aware resize
- *(block, cli)* qcow2 write + create — Phase B
- *(block)* qcow2 read path — Phase A
- *(cli)* partition-aware target syntax — disk.img:N
- *(block, cli)* real block-device support on Unix
- *(cli)* fstool shell — interactive REPL over any image
- *(ext4)* sparse_super on the write path
- *(fat32, cli)* modify-in-place — add files, add dirs, remove entries
- *(fat32, cli)* read-side parity — FAT32 reader + unified CLI dispatch
### Fixed
- *(cli)* repack as a direct FS-to-FS copy, no host tempdir
### Other
- release-binaries workflow — five archives per release
## [0.0.3](https://github.com/KarpelesLab/fstool/compare/v0.0.2...v0.0.3) - 2026-05-19
### Added
- *(fat32)* write-path FAT32 filesystem + spec/CLI/CI integration
- *(ext)* automatic sparse files — all-zero blocks become holes
- *(cli)* fstool rm — remove a file / symlink / device / empty directory
- *(cli)* fstool add — copy a host file or directory into an image
- *(ext4)* full metadata_csum write path — ext4 emits checksummed images
### Other
- bring README up to date with phases 4-5 + ext4 features
- metadata_csum foundation — csum module + superblock checksum
## [0.0.2](https://github.com/KarpelesLab/fstool/compare/v0.0.1...v0.0.2) - 2026-05-19
### Added
- *(ext4)* read INCOMPAT_64BIT images — 64-byte group descriptors
- *(spec)* partitioned disk-image build + multi-group ext allocation
- *(spec)* TOML image spec + `fstool build` (bare-filesystem mode)
- *(cli)* add fstool subcommands — ext-build / ls / cat / info
- *(ext4)* write extent-tree inodes (INCOMPAT_EXTENTS) + read them back
### Other
- lazy-stage parent inode + dir block on add_*, enabling modify-after-open
- add release-plz workflow for automated releases
- add CI / crates.io / docs.rs badges to README