fstool 0.4.21

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
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
# 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.21]https://github.com/KarpelesLab/fstool/compare/v0.4.20...v0.4.21 - 2026-08-17

### Added

- *(fat)* read + write FAT12 and FAT16 alongside FAT32

### Fixed

- *(ext)* 60-byte symlinks inline, and used_dirs_count charged to group 0
- *(fat)* don't link private `DirLayout` from the public module docs

### Other

- *(fat)* cross-validate FAT12/16 against dosfstools and mtools; document
- *(deps)* bump compcol to 0.6.10

### Added

- *(fat)* read + write FAT12 and FAT16 alongside FAT32. One backend serves
  all three: `table::FatKind` owns the 12/16/32-bit entry width (including
  FAT12's 1.5-byte packing) and `DirLayout` reduces both directory shapes —
  a cluster chain and the FAT12/16 fixed root region — to a list of device
  extents. A volume's flavour is derived from its data-cluster count per the
  spec, so images that mislabel their `fs_type` string still open correctly,
  and `detect_fs` probes the BPB rather than looking for a magic string
  FAT12/16 don't have. Reachable as `create -t fat12|fat16`, as a `repack`
  destination, and as a TOML spec `type`; `-O root_entries=` sizes the fixed
  root, which cannot grow once formatted.

### Fixed

- *(ext)* a symlink target of exactly 60 bytes was stored inline in `i_block`,
  producing an inode `e2fsck` rejects ("Symlink … is invalid") and the kernel
  refuses to look up ("invalid fast symlink length 60"). ext carries no
  "is inline" flag — the reader infers it from size alone, and Linux tests
  `i_size < 60` — so the bound is now strict in both the writer and the size
  planner, which had the same off-by-one and would have under-reserved a block
  per such symlink. ([#33]https://github.com/KarpelesLab/fstool/issues/33)
- *(ext)* `bg_used_dirs_count` was charged to block group 0 for every
  directory, so any image whose inodes reached a second group failed `e2fsck`
  with "Directories count wrong for group #N". Each directory is now counted in
  its own group, which also removes the only way to overflow the `u16` — per
  group it is bounded by `inodes_per_group`.
  ([#34]https://github.com/KarpelesLab/fstool/issues/34)

### Changed

- *(fat)* `FatFormatOpts` gained `kind` and `root_entries` — struct literals
  need `..Default::default()`. `Fat32::geometry` now takes the flavour and a
  root size and returns a `Geometry`. `table::{EOC, EOC_MIN, ENTRY_MASK}` are
  replaced by width-aware `FatKind` methods. FAT32 geometry, error text and
  on-disk output are unchanged.
- *(deps)* bump `compcol` floor to 0.6.10.

## [0.4.20]https://github.com/KarpelesLab/fstool/compare/v0.4.19...v0.4.20 - 2026-07-12

### Fixed

- *(squashfs)* converge inode/dir metablock offsets (Alpine root truncation)

### Other

- *(repack)* generic walk_stream over ArchiveStream; deprecate walk_tar_stream

## [0.4.19]https://github.com/KarpelesLab/fstool/compare/v0.4.18...v0.4.19 - 2026-07-12

### Added

- *(web)* in-browser WebAssembly UI + in-memory inspect/convert API

### Fixed

- *(hfs+)* restore create_file_streaming override clobbered in merge
- *(web)* keep [hidden] elements hidden over author display rules

### Other

- de-link private intra-doc references (rustdoc -D warnings)
- *(f2fs)* use map values()/keys() to satisfy clippy for_kv_map
- *(fs)* sequential archives hold no index — forward-scan on read
- *(fs)* honest per-file read seekability — no RAM-faked Seek
- *(fs)* finish write-path streaming; add AccessMode capability
- *(fs)* stream file bodies on write; drop tempfile dependency
- *(web)* rebuild UI in Vue 3 + Vite; fold wasm bindings into fstool crate
- link the live web demo in the README
- lower MSRV to 1.88 (purecrypto 0.6.14)

## [0.4.18]https://github.com/KarpelesLab/fstool/compare/v0.4.17...v0.4.18 - 2026-06-16

### Added

- *(hfs+)* set_attrs for cross-filesystem chmod/chown
- *(xfs)* implement set_attrs for cross-filesystem chmod/chown/utimes
- *(exfat)* set_attrs for cross-fs chmod via the READ-ONLY bit
- *(fat)* implement set_attrs so cross-fs chmod works on FAT32
- *(ntfs)* implement set_attrs so cross-fs chmod works on NTFS
- *(shell)* add `chmod MODE PATH` + AnyFs::set_attrs wrapper
- *(shell)* quote-aware argument parsing; preserve host timestamps on put

### Fixed

- *(ext)* scan all directory blocks when unlinking an entry ([#29]https://github.com/KarpelesLab/fstool/pull/29)
- *(hfs+)* preserve mtime on symlinks and device nodes
- *(hfs+)* store per-file modification times in catalog records
- *(fat)* store and surface file modification times
- *(exfat)* surface on-disk timestamps in getattr

### Other

- *(cli)* cross-backend chmod end-to-end; fix ntfs set_attrs doc links
- *(ntfs)* pin Everyone-access security descriptor on the put path

## [0.4.17]https://github.com/KarpelesLab/fstool/compare/v0.4.16...v0.4.17 - 2026-06-12

### Fixed

- *(inspect)* drain NTFS/XFS/exFAT dir batches in AnyFs::flush

### Other

- *(inspect)* drop intra-doc link to private as_filesystem_dyn
- *(inspect)* collapse AnyFs::flush + kind_string to exhaustive dispatch

## [0.4.16]https://github.com/KarpelesLab/fstool/compare/v0.4.15...v0.4.16 - 2026-06-12

### Fixed

- *(fstool)* escape image-supplied names on TTY; saturating LBA math (CLI-2, CLI-4)
- *(fstool)* validate image entry names before host path join (CLI-1, CLI-3)
- *(repack,merge)* bound tar walkers and drop `..` in merge (CORE-1, CORE-2)
- *(fs)* guard total_file_bytes against directory cycles (MISC-4)
- *(ramfs)* cap capacity hints and resize ceilings (MISC-3, MISC-5)
- *(grf)* bound read_entry allocation by device size (MISC-2)
- *(iso9660)* bound read_directory allocation by device size (MISC-1)
- *(gpt)* checked_mul for entries_start_lba * 512 (BLK-4)
- *(dmg)* bound attacker-controlled allocations on malformed images
- *(archive)* strip `..` in shared normalise_path to block traversal
- *(lha)* guard short tail reads in the header scan loop
- *(sevenz)* bound untrusted 7z counts and cap encoded-header decode
- *(xfs)* extent-driven dir walk and checked byte-offset math
- *(f2fs)* bound superblock block_count by device capacity (SQF2-5)
- *(squashfs)* bound untrusted allocations and guard short-block underflow
- *(affs)* bound chain walks with range checks + visited-sets (FATX-3, FATX-4, FATX-5)
- *(fat)* bound chain walks by cluster_count, validate sectors_per_cluster (FATX-2, FATX-6)
- *(exfat)* bound cluster-chain walks by ClusterCount (FATX-1)
- *(hfs+)* bound journal replay and validate ring geometry (HFS-2, HFS-3, HFS-5)
- *(hfs+)* guard writer leaf-chain walks and bitmap sizing (HFS-1, HFS-4)
- *(apfs)* harden reader/writer against malformed images
- *(ntfs)* harden malformed-image handling (NTFS-1..5)
- *(ext)* bound extent-append, dx lookup, symlink, and dir scan on malformed input (EXT-4, EXT-5, EXT-6, EXT-8)
- *(ext)* harden indirect/extent tree walks against OOB, cycles, depth (EXT-3, EXT-7)
- *(ext)* cap journal replay at ring size to stop cyclic descriptors (EXT-2)
- *(ext)* bound attacker-controlled group count before allocating (EXT-1)

### Other

- *(fstool)* make safe_component drive-prefix test platform-aware (CLI-1)
- apply rustfmt to security-hardening changes

## [0.4.15]https://github.com/KarpelesLab/fstool/compare/v0.4.14...v0.4.15 - 2026-06-11

### Added

- *(cli)* surface statfs in `fstool info` and add a shell `df` command
- *(ramfs)* `fstool mount --new-ramfs` — mount an in-memory tree over FUSE
- *(ramfs)* AnyFs::Ramfs variant + `fstool shell --new-ramfs` with save
- *(ramfs)* in-memory Filesystem with repack_to + generic walk_filesystem
- *(ntfs)* analytic FsSizePlan + contiguous $MFTMirr for content-fit create
- *(hfs)* analytic FsSizePlan for content-fit classic-HFS create
- *(create)* HFS+ analytic FsSizePlan (catalog B-tree)
- *(create)* F2FS analytic FsSizePlan
- *(create)* XFS analytic FsSizePlan
- *(create)* two-phase analytic builder — FsSizePlan via FilesystemFactory, AFFS reference
- *(create)* writer-determined exact sizing for all block filesystems
- *(create)* exact content-fit sizing infra + FAT32 (FsSizePlan)

### Fixed

- *(hfs+)* fsck-clean create at any size (alt-VH alignment + bitmap padding)

### Other

- *(analyze)* drop the binary-search writer_required_size fallback
- *(hfs+)* drop unresolved intra-doc link to trait method total_size

### Fixed

- *(hfs+)* `create -t hfsplus` now produces `fsck.hfsplus`-clean volumes at
  **any** size, not only block-aligned ones. Two pre-existing writer bugs:
  (1) the image could be a non-multiple of the 4 KiB allocation block (the
  `2× + 64 MiB` auto-size, or an arbitrary `--size`), leaving a trailing
  partial block past the alternate volume header where `fsck` reads a
  misplaced header — `HfsPlus` now reports `image_len`, so the create/repack
  paths truncate the output to a whole number of allocation blocks; (2) when
  the bitmap's last byte was partial, its padding bits were written as `1`,
  but TN1150 requires bits beyond `total_blocks` to read as `0` — they're now
  cleared on disk (the in-memory allocator still keeps them set). Verified
  `fsck.hfsplus`-clean across previously-failing block counts and 500/5000-file
  trees.

### Added

- *(create)* **content-fit sizing, exact per filesystem.** `fstool create
  <fs> <source-dir>` without `--size` now computes the *minimal* image that
  holds the content, instead of a `2× + 64 MiB` over-provision (or, for FAT32,
  requiring `--size`). A new `FsSizePlan` trait (mirroring ext's `BuildPlan`)
  lets each filesystem accumulate the exact on-disk allocation from the single
  analysis walk and return the smallest image its writer accepts, rounded only
  to that filesystem's native unit. **FAT32** is the first wired up: it
  searches the authoritative `Fat32::geometry`, so e.g. 250 MiB of content
  produces a ~254 MiB image (≈1 % overhead, all but one cluster used) and a
  source-backed `create -t fat32` no longer needs `--size`.
- *(create)* **the writer now determines its own size for every block
  filesystem** (hfs+, hfs, affs, xfs, ntfs, f2fs). Rather than a parallel size
  model that could drift, `create <fs> <dir>` (no `--size`) does a dry-run:
  it formats and populates the *real* writer against a sparse, write-discarding
  `SizingDevice` — assigning inodes/CNIDs, encoding names, building B-trees and
  directory blocks exactly as for a real build, with file data written as
  zeros so the probe is metadata-only — and binary-searches the smallest size
  the writer's own allocator accepts. Result: tight, `fsck`-clean images
  (hfs +0 %, affs +1 %, hfs+ +3 %, xfs +13 %; ntfs and f2fs report their
  writers' genuine minimums) versus the former `2× + 64 MiB`. Compressing /
  archive backends (squashfs, iso, grf, …) keep their grow-then-truncate path.
  The probe tolerates writers that *panic* on degenerate small sizes (caught
  and treated as "doesn't fit"), e.g. a pre-existing f2fs format panic.

## [0.4.14]https://github.com/KarpelesLab/fstool/compare/v0.4.13...v0.4.14 - 2026-06-07

### Fixed

- *(xfs)* empty `create` failed with "flush_writes called before begin_writes()"

### Other

- *(changelog)* move xfs fix to [Unreleased], drop release-plz dup blocks

## [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

## [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