fstool 0.4.13

Build disk images and filesystems (ext2/3/4, MBR, GPT) from a directory tree and TOML spec, in the spirit of genext2fs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
# 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