KarpelesLab/fstool
GitHub: KarpelesLab/fstool
一个 Rust 实现的多格式磁盘映像与文件系统构建、检查、转换和重新打包工具,同时提供 CLI 与可嵌入的库。
Stars: 1 | Forks: 0
# fstool
[](https://github.com/KarpelesLab/fstool/actions/workflows/ci.yml)
[](https://crates.io/crates/fstool)
[](https://docs.rs/fstool)
构建、检查、修改和重新打包磁盘映像与文件系统映像。
秉承 `genext2fs` 的理念,但支持整个磁盘、多种文件系统,
以及格式间的往返转换——所有操作均可通过 TOML 规范或直接从
命令行完成。
fstool 以 Rust 库(`fstool`)加上一个轻量级 CLI 二进制程序(`fstool`)的形式发布。
在 v0.5 版本之前,公共 API 是**不稳定**的。
```
cargo install fstool
fstool create -t ext4 ./src -o out.img # build an ext4 image from a dir
fstool create -t squashfs ./src -o out.sqsh \
-O compression=zstd,block_size=128KiB # FS-specific knobs via -O
fstool info out.img # what's inside
fstool ls out.img / # walk it
fstool repack out.img out.tar # convert ext4 → tar (and back)
fstool repack base.tar patch.tar flat.tar # OCI-style layer merge with .wh.* whiteouts
```
## 文件系统支持
| 文件系统 | 读取 | 写入 | 原地编辑 | 备注 |
|------------|------|-------|----------------|--------------------------------------------------------------------------------------------------------------------|
| ext2 | ✅ | ✅ | ✅ | 在相同输入下与 `genext2fs` 字节完全一致 |
| ext3 | ✅ | ✅ | ✅ | + JBD2 日志 — 在 `open_file_rw`(Path A)上进行真实事务 |
| ext4 | ✅ | ✅ | ✅ | extents(读取 + 写入:任意深度),FILETYPE,`metadata_csum`,xattrs,JBD2 |
| FAT32 | ✅ | ✅ | ✅ | VFAT LFN 条目,8.3 短文件名别名 |
| exFAT | ✅ | ✅ | ✅ | 格式化 + 创建 + 移除 + flush + `open_file_rw` |
| tar | ✅ | ✅ | — | ustar + PAX,用于 xattrs 的 `SCHILY.xattr.*`;仅限流式处理 |
| XFS | ✅ | ✅ | ✅ | shortform + block / leaf / node + 多级 B-tree 目录 + BMBT;leaf-form xattrs;真实 XLOG 事务 (Path A);通过 `xfs_repair -n` 单一及多 AG 测试 |
| HFS+/HFSX | ✅ | ✅ | ✅ | inline + extents-overflow,符号链接,硬链接;decmpfs 读取(zlib 类型 3 + 4);**资源分叉**(`cat --rsrc`,`resources`,`com.apple.ResourceFork` xattr);真实日志 (Path A);通过 `fsck.hfsplus` 测试 |
| HFS | ✅ | ✅ | ✅ | 经典 HFS (Mac OS ≤ 8):MDB + catalog/extents B-trees,MacRoman 名称,数据 + **资源**分叉读取;透明解包 **DiskCopy 4.2** 映像。**写入**:`create -t hfs` / `build` / `repack` 生成全新卷,而 `add` / `rm` / shell `put`/`mkdir` 会原地修改现有映像(在 flush 时重建 catalog) |
| AFFS | ✅ | ✅ | ✅ | Amiga OFS/FFS(`.adf`):启动块变体检测(`DOS\0`..`DOS\7`),哈希表目录,文件头 + 扩展块,OFS(24 字节数据头)+ FFS 原始数据,BCPL/Latin-1 名称,Amiga 1978 纪元日期;读取已针对真实 OFS/FFS Workbench 卷进行验证。**写入**:`create -t affs` / `-t ofs` / `build` / `repack` 生成全新的 OFS 或 FFS 卷(默认为 DOS\3 FFS+INTL;`-O fstype=ofs,intl=false`),而 `add` / `rm` / shell `put`/`mkdir` 会在磁盘上**增量**修改现有映像 — 仅触及受影响的块(卷位图、父目录的哈希链以及新增/移除文件的文件头 + 数据 + 扩展块);未被触及的文件保留其原始块,且内存使用量由位图决定,而非文件内容。符合规范(块校验和 + 名称哈希放置 + 位图,即 Linux 内核 `affs` 驱动程序强制执行的不变量) |
| APFS | ✅ | ✅ | 🚧 | **读取**:多级 omap + fs-tree,目录列表 + 文件 extents,内嵌 xattrs,快照(只读,单叶 snap-meta)。**写入**:格式化 + `create_dir`/`create_file`/`create_symlink` + `chmod`/`chown`/`set_times`/`rename`/`unlink`/`link`,通过全新的 COW 检查点(带有 IP 环 + SFQ 空闲队列的 spaceman),往返于真实的 macOS 挂载。**缺口**:原地编辑为整文件覆盖(无部分 extent COW);`UF_COMPRESSED`/decmpfs 文件读取为空;加密、sealed-volume 完整性、Fusion 分层以及基于 dstream 的 xattrs 被拒绝;尚未达到 `fsck_apfs` 清洁标准 |
| NTFS | ✅ | ✅ | ✅ | MFT,属性,$DATA + ADS,索引;xattr 映射;多类 `$Secure`($SDS/$SDH/$SII);真实的 `$LogFile` LFS 记录 (Path A) |
| F2FS | ✅ | ✅ | — | CP / NAT / dnodes / inline data + dentries;写入器通过 `fsck.f2fs` 测试;**一次构建** — 写入器在 flush 时从内存序列化整个 FS,因此重新打开的映像是只读的(报告为 `Immutable`) |
| SquashFS | ✅ | ✅ | — | 通过 Cargo 特性支持 gzip / xz / lz4 / zstd / lzo / lzma;写入器通过 `unsquashfs` 进行往返转换;仅限重新打包 |
| ISO 9660 | ✅ | ✅ | — | PVD + Joliet (UCS-2) + Rock Ridge (PX/NM/SL/TF) + El Torito 启动目录;仅限重新打包 |
| GRF | ✅ | ✅ | ✅ | Gravity Ragnarok Online 归档 — v0x102 / v0x103 / v0x200;置换密码(`MIXCRYPT` / `DES`);CP949 文件名 |
| zip | ✅ | ✅ | — | central-directory 索引,ZIP64,Stored + Deflate,Unix mode/symlinks,UTF-8/Shift-JIS/EUC-JP 文件名检测;仅限重新打包写入器 |
| cpio | ✅ | ✅ | — | 读取 newc / newc-crc / odc;写入 newc;仅限重新打包 |
| ar | ✅ | ✅ | — | 读取 GNU + BSD 长文件名,写入 GNU;扁平结构(无目录);仅限重新打包 |
| cab | ✅ | — | — | 只读 Microsoft Cabinet:Store / MSZIP / LZX / Quantum 文件夹通过 `compcol` 解码(与 `cabextract` 交叉验证)。不支持 Spanned cabinet 和创建 |
| lzx | ✅ | — | — | 只读 Amiga LZX:通过 `compcol::amiga_lzx` 处理 Store + LZX(mode 2)合并组;容器已与 `unlzx` 交叉验证。不支持创建 |
| rar | ✅ | — | — | 只读 RAR5,包含 **solid** 归档(顺序遍历 / `repack` 仅解码该组一次):通过 `compcol::rar5` 处理 Store + 压缩(无过滤器 / x86 E8E9);已与 `unrar` 交叉验证。不支持 RAR4、加密、stored-in-solid、其他过滤器及创建 |
| lha | ✅ | — | — | 只读 LHA / LZH:遍历 level-0/1/2 头(长文件名 + 目录)。`-lh0-` store 解码并已与 `lha` 交叉验证;lh1/4/5/6/7 LZSS+Huffman 方法已列出,但在 `compcol` 中等待 `lha` 编解码器支持,读取时返回 `Unsupported`。不支持创建 |
| arc | ✅ | — | — | 只读 SEA ARC:遍历扁平头链。Stored 方法(1 old / 2)解码;压缩方法(RLE90 / squeeze / crunch / squash)已列出,但在 `compcol` 中等待 ARC 编解码器支持,读取时返回 `Unsupported`。不支持创建 |
| sit | ✅ | — | — | 只读 StuffIt:经典 `SIT!` 容器(数据分叉索引,文件夹标记)。Method 0(store)解码;压缩方法 + StuffIt 5 可列出/检测,但在 `compcol` 中等待 StuffIt 编解码器支持,读取时返回 `Unsupported`。不支持创建 |
| 7z | ✅ | — | — | 只读 7-Zip:解析容器(包含 LZMA 打包头 + 按子流切分的 solid 文件夹);单编码器 **Copy / LZMA / BZip2 / Deflate** 文件夹解码(已与 `7z` 交叉验证)。**LZMA2**(默认)、BCJ 过滤器、PPMd、加密及多编码器流水线已列出,但在 `compcol` 中等待原生 LZMA2 + 分支过滤器编解码器支持,读取时返回 `Unsupported`。不支持创建 |
`🚧` 标记了存在已知缺陷的写入器/修改路径(参见限制)。
所有可写的文件系统 — ext2/3/4, FAT32, exFAT, XFS, HFS+, NTFS,
APFS, F2FS, SquashFS, ISO 9660, GRF — 均实现了统一的
`Filesystem` trait,因此 CLI(`build`, `repack`, `add`, `rm`)和
TOML `[filesystem] type = "…"` 规范会通过同一个
代码路径分发;可通过在 `repack` 上设置 `--fs-type` 或在
TOML 规范中设置 `type = "hfsplus"`(等等)来选择目标 FS。“原地编辑”
意味着一个已经 flush 过的映像可以被重新打开以进行 `add` / `rm` /
`open_file_rw` — 对于带有日志的文件系统,该路径会通过
真实的事务提交,因此写入中途崩溃也会留下一个可被
宿主机 `fsck` 重放的映像。
`qcow2` 和 `dmg` **未**包含在上表中:它们不是
文件系统,而是*磁盘映像容器*。它们位于下一层,作为
`BlockDevice` 后端(参见架构图和“分区、块设备、qcow2”),呈现为一个扁平的可按字节寻址的设备,
随后上述任何文件系统都可以被布局到其*内部* — fstool
会透明地通过它们进行读写。qcow2 支持读/写(v2 + v3,
allocate-on-write),包括**压缩的** cluster — 透明读取 zlib 和 zstd
(写入压缩 cluster 时会将其拷贝出并转换为普通 cluster);
dmg 是只读的(UDIF v4 mish 块:zero / raw / zlib / ADC / bzip2 /
LZFSE / LZMA,以及加密的 v2 `encrcdsa`)。
每个 FS 的读取器都是流式的:无论文件多大,其内容都绝不会完全驻留在
内存中。写入器也是如此,采用两遍处理:扫描以测量
几何结构,然后将字节从每个源文件流式传输到映像中。
没有 POSIX 类比的 NTFS 元数据(DOS 属性、ADS、安全
描述符、NT-FILETIME 时间戳、短名称、重解析数据)通过 `user.ntfs.*` 和 `system.ntfs_security` 下的 xattrs 进行往返转换。
## CLI 命令
| 命令 | 功能 |
|---------------|-------------------------------------------------------------------------|
| `create` | 从宿主机目录树构建任意受支持的 FS(`-t ext4` / `fat32` / `xfs` / `hfs+` / `fs` / `f2fs` / `squashfs` / `iso` / `apfs` / `exfat` / `grf` / `zip` / `cpio` / `ar`)的裸映像。特定于 FS 的调节参数通过 `-O key=val,key=val` 传递。 |
| `build` | 从 TOML 规范构建 — 裸 FS 或分区磁盘映像。 |
| `info` | 打印分区表(整盘)或 FS 摘要 + 根目录列表。 |
| `ls` | 列出映像内的目录;`-R` 递归遍历子目录。 |
| `cat` | 将映像中文件的字节流式输出到标准输出。`--rsrc` 流式传输资源分叉 (HFS / HFS+)。 |
| `resources` | 清点 HFS / HFS+ 文件的资源分叉(ResEdit 风格:`vers`/`ICN#`/`DITL`/… 及解码摘要);`--extract TYPE:ID` 导出单个资源。 |
| `add` | 将宿主机文件 / 目录树复制到现有映像中(任何可变的 FS)。 |
| `rm` | 取消链接文件、符号链接、设备或空目录。 |
| `shell` | SFTP 风格的 REPL — `ls cd pwd cat put get rm mkdir info`(`get` 将文件/目录从映像复制到宿主机 — 与 `put` 相反),外加 `find`(name/type/mtime 过滤器,`-sort`/`-limit` 用于例如查找最新的 N 个文件)和 `grep`(`-i`/`-n`/`-r`/`-v`/`-l`/`-c`;二进制匹配打印为 `hexdump -C`)。Ctrl-C 可取消正在运行的 `find`/`grep` 而不会退出 shell。`--with-cache` 将所有 inode 预加载到 RAM 中,使 `find`/`ls` 瞬间完成;`--ro` 以只读方式浏览(包括 tar/ISO/SquashFS)。在 TTY 上具有行编辑 + ↑/↓ 命令历史功能。 |
| `convert` | 字节级别的 raw ↔ qcow2 转换,可选扩容。 |
| `repack` | 遍历一个或多个源 FS,使用 whiteouts 自底向上合并,并重建为全新的映像。 |
| `dd` | 弹性原始块拷贝(文件/设备 → 文件/设备),`ddrescue` 风格:以 1 MiB 块为单位读取,出错时将块大小减半直至源扇区大小,并跳过无法读取的点。带有多线程 读/写器流水线及实时进度条(%,ETA,独立的读/写速度,缓冲区占用率,当前块,跳过的字节数)。Ctrl-C 可干净地取消。 |
所有命令均接受支持分区的 `disk.img:N` 目标(从 1 开始索引) — 参见
下文的“分区、块设备、qcow2”。
所有检查/修改命令均接受 `disk.img:N`(从 1 开始索引)
目标,以遍历进入 GPT、MBR 或 Apple Partition Map 磁盘映像的分区
。不带后缀的 `fstool info disk.img` 会打印分区表
本身。
### 路径风格 (`--path-style`)
经典 Mac 文件系统使用 `:` 分隔路径组件,因此 `/` 是一个合法的
*文件名* 字符(真实的目录可以命名为 `A/ROSE Includes`)。全局
`--path-style` 标志用于选择路径的拼写方式:
- **`unix`**(默认) — 到处使用 `/` 分隔;HFS/HFS+ 名称中
的字面量 `/` 显示为 `:`(macOS 自身使用的约定)。因此
`fstool ls disk.toast:2 …` 列出 `A:ROSE Includes`,并且**重新打包到 tar/zip
时也会以相同方式呈现名称**(`A:ROSE Includes`) — 如果不希望字面量 `/` 被读取为目录分隔符,它就不能
放入归档成员名中。
- **`native`** — 文件系统自身的分隔符(HFS/HFS+ 使用 `:`,FAT/exFAT/NTFS 使用 `\`,其他地方使用 `/`);保留真实文件名。使用
原生分隔符导航,例如
`fstool ls --path-style native disk.toast:2 ':Apple Software Library:…:A/ROSE Includes'`。
`native` 仅更改 CLI 和 shell *显示和接受* 路径的方式;磁盘上的
格式(以及 `repack`/`add` 使用的规范名称)不受影响。
### 特定于 FS 的选项 (`-O`)
大多数文件系统通过通用的 `-O
key=value,key=value` 标志公开可调参数(块大小、标签、压缩编解码器、
卷名、日志开关等),该标志可重复使用,模仿自 `mke2fs -O`:
```
# 4 KiB blocks + ext4 上的自定义标签
fstool create -t ext4 ./rootfs -o out.img -O block_size=4096,volume_label=ROOT
# 选择一个 SquashFS codec 并收紧 block size
fstool create -t squashfs ./rootfs -o out.sqsh \
-O compression=zstd,block_size=128KiB
# 强制使用 deflate level 9 的 v0x103 GRF
fstool create -t grf ./rootfs -o out.grf -O version=0x103,compression_level=9
```
每个后端的 `apply_options` 都会验证键;未知的键会被拒绝,
并返回一条引用该 FS 类型的明确错误。相同的选项可通过
TOML 规范使用 — 参见下文的“[filesystem.options]”。
## 分区、块设备、qcow2
- **分区表** — MBR(4 个主分区)和 GPT(128 条目,在
表头 + 条目数组上进行 CRC32 校验,主 + 备,保护性 MBR)。已与
`sgdisk -v` 和 `fdisk -l` 交叉验证。**Apple Partition Map**(经典
Mac / `.toast` 方案)是只读的:`info` 列出 `Apple_HFS` /
`Apple_Free` / `Apple_partition_map` 条目,`disk.toast:N` 可对其进行切片。
- **块设备** — 在 Unix 上,fstool 可以格式化和修改真实的块
设备(`/dev/sdX`, `/dev/nvme0n1`, loop 设备)。容量通过
内核 ioctl 查询(Linux 上的 `BLKGETSIZE64`,macOS 上的 `DKIOCGETBLOCK*`),
并且 open 使用 `O_EXCL`,因此如果有任何分区被挂载,内核将拒绝操作。
当输出是块设备时,Build 命令需要 `--force`。
- **qcow2** — `Qcow2Backend` 读取 QEMU v2 / v3 映像并写入全新的 v3
映像(支持 allocate-on-write)。**压缩的 cluster** 被透明读取
(zlib/deflate 和 zstd,使用 4 KiB 窗口解码以匹配 qemu 并限制
RAM);对压缩 cluster 的写入会将其拷贝出并转换为普通 cluster。要
*生成*压缩映像,请向 `create` / `build` /
`repack` / `convert` 传递 `--compress`(例如 `--compress`、`--compress=9`、`--compress=zstd`、
`--compress=zstd:9`);结果可通过 `qemu-img check` 检查。基于路径的
工厂(`block::open_image`, `block::create_image`)通过 qcow2
魔数或文件扩展名自动分发,因此 `fstool create -t ext4 src -o out.qcow2` 即可正常工作。
## TOML 规范
声明式映像描述 — 要么是裸文件系统(`[filesystem]`)
要么是分区磁盘(`[image]` + `[[partitions]]`):
```
[image]
size = "64MiB"
partition_table = "gpt"
[[partitions]]
name = "EFI"
type = "esp"
size = "16MiB"
[[partitions]]
name = "root"
type = "linux"
size = "remaining"
[partitions.filesystem]
type = "ext4"
source = "./rootfs"
```
```
fstool build disk.toml -o disk.img
sgdisk -v disk.img # "No problems found."
```
### `source` — 用于填充 FS 的内容
`source` 接受三种形式,根据字符串指向的内容自动检测:
```
[partitions.filesystem]
type = "ext4"
source = "./rootfs" # a host directory — walk it recursively
```
```
[partitions.filesystem]
type = "ext4"
source = "./rootfs.tar.gz" # a tar archive — repack entries into the FS
```
```
[partitions.filesystem]
type = "ext4"
source = "./old-disk.img:2" # an existing image, optional :N partition
# — walks the source FS, copies every
# entry into the new partition
```
识别的 tar 扩展名:`.tar`, `.tar.gz`, `.tgz`, `.tar.xz`, `.txz`,
`.tar.zst`, `.tar.lz4`, `.tar.lzma`, `.tar.lzo`(编解码器受限于
匹配的 Cargo 特性)。对于映像,`:N` 后缀用于选择分区
*N*(从 1 开始索引);不带此后缀,则将源作为裸文件系统打开。
源 FS 可以是任何可读类型 — `ext{2,3,4}`, FAT32, exFAT,
XFS, HFS+, APFS, NTFS, F2FS, SquashFS, ISO 9660, tar, 或 GRF — 并且
除非显式设置了 `size`,否则目标大小会自动调整以适应
内容。
### `[filesystem.options]` — 特定于 FS 的可调参数
CLI 公开的相同 `-O key=val` 旋钮可通过
自由格式的 `[filesystem.options]` 表在 TOML 中使用:
```
[filesystem]
type = "squashfs"
source = "./rootfs"
[filesystem.options]
compression = "zstd"
block_size = 131072
[partitions.filesystem]
type = "ext4"
source = "./rootfs"
[partitions.filesystem.options]
block_size = 4096
volume_label = "ROOT"
```
识别的键记录在每个后端的
`FormatOpts::apply_options` 旁边;未知的键会在规范解析
阶段被拒绝,并返回一条引用该 FS 类型的明确错误。现有的扁平字段
(`block_size`, `volume_label`, `mtime`, …)继续工作以保持向后
兼容。
## 架构
```
┌────────────────────────────────────────────┐
│ CLI (clap) — bin/fstool │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Spec layer (TOML → ImageSpec / FsSpec) │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ Filesystem trait → ext, fat, xfs, ntfs, … │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ PartitionTable trait → Mbr, Gpt │
└────────────────────────────────────────────┘
│
┌────────────────────────────────────────────┐
│ BlockDevice trait → File, Mem, Sliced, │
│ Qcow2, Dmg │
└────────────────────────────────────────────┘
```
每一层都是可替换的。文件系统实现只与
`BlockDevice` 通信;它不知道也不关心设备是真实的文件、
测试中的内存缓冲区、由分区表切割出的更大磁盘的切片,
还是 qcow2 支持的稀疏容器。DMG(`.dmg`)也以
相同方式处理:打开映像,遍历 mish 表以获取
块布局,然后堆栈的其余部分将其作为扁平的
块设备读取 — 包括加密(`encrcdsa` v2)变体
(前提是提供了解锁密码)。
## ext 特定的细节
- `BuildPlan` 自动将文件系统调整为恰好适合源目录树的大小
(genext2fs 风格的“自适应大小”)。
- `Ext::populate_rootdevs` 放入一个 `Minimal` 或 `Standard` 的 `/dev/*` 目录树
(console, null, zero, ptmx, tty, fuse, random, urandom — 外加 tty0..15,
ttyS0..3, kmsg, mem, port, hda..hdd, sda..sdd 及适用于
`Standard` 的分区),因此非 root 用户构建 Linux 根 FS 时不需要
`CAP_MKNOD`。
- xattrs 通过重新打包进行往返转换:inline(扩展 inode body)和
外部 `file_acl` 块源均会被读取;当开启 `metadata_csum` 时,目的地会
写入带有正确计算的 CRC32C 的外部块。
`debugfs ea_get` 确认重新打包后的值完全相同。
## 跨 FS 重新打包
`fstool repack` 遍历源文件系统并将目录树重建为
全新的映像。使用 `--fs-type` 可动态更改文件系统;`--shrink`
会将输出自动调整为恰好容纳内容的最小尺寸。
该流水线是**一个通用的遍历器馈送到两个 sink 之一** — 一个
流式 tar sink(tar / `.tar.`)或一个块设备 `Filesystem`
sink — 没有
针对每个 `(source,dest)` 类型的特殊情况。因此 **任何可读的源均可通过单一路径重新打包为任何可写的目的地**
(`fstool repack app.zip out.tar`、`fstool repack disk.xfs out.iso` 等)。
遍历器通过源的 trait
`getattr` / `list_xattrs` / `read_symlink` 读取每个条目的元数据,因此 mode、uid/gid、mtime、
符号链接、设备号、xattrs 和硬链接在两端都能表示的情况下均可实现往返转换。文件主体直接从源流式传输到
目的地(`create_file_streaming`,无每个文件的临时文件)。硬链接
在目的地支持时进行去重,在其他情况下
则表现为拷贝;不支持符号链接/设备/xattr 的目的地会丢弃它们并发出警告。
每个读取器都会呈现其格式实际存储的元数据:
ext、tar、归档格式、F2FS、XFS、SquashFS、APFS 和 HFS+ 带有
完整的 POSIX mode/uid/gid + 时间戳(HFS+ 转换其 1904 纪元);
当存在 Rock Ridge 时,ISO 9660 也会如此(纯/Joliet 没有);
NTFS — 没有 POSIX 所有权 — 呈现真实的时间戳 + 从其 DOS 属性合成的 mode,并在重新打包时携带其原生元数据
(DOS 属性、ADS、安全描述符、重解析数据等),作为 `user.ntfs.*` / `system.ntfs_security` xattrs。
`fstool repack` 会写入任何实现了 `Filesystem`
trait 的目的地 — `ext2/3/4`、FAT32、exFAT、tar、XFS、HFS+、FS、NTFS、F2FS、
SquashFS、ISO 9660、GRF。`add` / `rm` 通过相同的 trait,
这意味着它们适用于任何其写入器能重新打开现有
映像的 FS;目前这包括所有可变的后端 — ext、FAT32、exFAT、
F2FS、XFS、HFS+、NTFS、APFS 和 GRF。SquashFS、ISO 9660 和 tar
仅限重新打包(它们的 `MutationCapability` 为 `Immutable` 或
`Streaming`,因此 `add` / `rm` 会快速失败并返回可操作的错误,
引导用户使用 `repack`)。
## 带 whiteouts 的分层合并
`repack` 接受一个或多个源位置参数,后面跟着
目的地。如果是单个源,则行为与之前相同;如果有两个或更多
源,它会在写入前自底向上合并这些源 — 后面的层
会覆盖相同路径的文件,上层中的 tombstones 会移除下层的
路径。有两种 tombstone 约定会被自动检测:
| 约定 | 标记 | 效果 |
|------------|--------|--------|
| tar-OCI | 目录 D 中的 `.wh.` | 删除 `D/` |
| tar-OCI | 目录 D 中的 `.wh..wh..opq` | 在当前层自身内容落下之前,丢弃 D 的所有下层子项 |
| OverlayFS | major=0, minor=0 的字符设备 | 删除此路径 |
| OverlayFS | 目录上的 xattr `trusted.overlay.opaque = "y"` | 该目录的 opaque-dir 语义 |
Tombstones 本身永远不会出现在输出中。源可以是
宿主机目录、tar 归档(压缩或普通)或文件系统
映像 — 任何混合均可使用。
```
# OCI 风格:将一堆 layers 重建为一个扁平的 tar
fstool repack base.tar layer1.tar layer2.tar flat.tar
# 使用包含替换文件的 tar 来 patch 一个 ISO
fstool repack disc.iso patch.tar updated.iso --fs-type iso
# Shell globs 有效 — 最后一个位置参数是目标
fstool repack layer*.tar merged.tar
```
在内部,合并会将所有层折叠到保存在临时文件中的单个未压缩 tar 中,
然后驱动现有的单源重新打包
流水线;目标 FS 并不知道它来自多个
源。
## ISO 9660
ISO 9660 读取涵盖裸 ECMA-119 布局以及四种
常见扩展中的三种:
- **Joliet** (Microsoft) — 通过补充卷描述符使用 UCS-2 BE 长名称。
- **Rock Ridge** (IEEE P1282) — 通过 `PX` 实现 POSIX mode + uid + gid,通过
`NM` 实现长名称,通过 `SL` 实现符号链接,通过 `TF` 实现
时间戳。跨扇区边界跟随继续区域(`CE`)。
- **El Torito** — 启动目录:验证条目、默认条目和
段头(`0x90` / `0x91`);解析后的目录会呈现在
`fstool info` 中。
写入器仅限重新打包 — ISO 是顺序格式的,单个 `flush()`
即可写入整个映像。它会生成一个 PVD 以及可选的 Joliet SVD,
同时生成 L 型和 M 型路径表、双重目录记录树(一个
用于 PVD,一个用于 Joliet),以及附加在 PVD 记录上的 Rock Ridge System Use Areas(`NM` /
`PX` / `SL`)。输出可以通过 `isoinfo -lR` 进行往返转换
并重新通过 fstool 自身的读取器读回。
```
# 从 host 目录构建一个 ISO
fstool repack ./rootfs disc.iso --fs-type iso
# 遍历现有的 ISO
fstool ls disc.iso /
fstool cat disc.iso /README.TXT
# ISO → tar → ISO 往返转换
fstool repack disc.iso plain.tar
fstool repack plain.tar disc2.iso --fs-type iso
```
## 归档格式
归档通过与 tar 和 GRF 相同的 `Filesystem` trait 被视为文件系统,因此 `info` / `ls` / `cat` / `repack` 可以在它们上面统一运作。它们
共享一个公共核心(`src/fs/archive/`):每种格式提供一个*扫描器*,它
将归档索引到内存中的树状结构,并且 — 如果可写 — 提供一个*构建器*;
该核心提供通用的读取路径,并通过现有的压缩编解码器解码每个条目的字节范围。
```
fstool create -t zip ./rootfs -o out.zip # build a zip from a dir
fstool create -t zip ./rootfs -o out.zip -O compression=stored
fstool ls app.zip / # walk any zip/cpio/ar
fstool cat app.zip /etc/config
fstool repack app.zip out.cpio --fs-type cpio # convert between archives
```
| 格式 | 读取 | 写入 | 备注 |
|--------|------|-------|-------|
| zip | ✅ | ✅ | ZIP64, Stored + Deflate, Unix mode + symlinks;读取任何工具生成的归档;文件名解码为 UTF-8(有标记),否则自动检测。写入时,UTF-8 标记仅对非 ASCII 名称设置。 |
| cpio | ✅ | ✅ | 读取 newc / newc-crc / odc;写入 newc。 |
| ar | ✅ | ✅ | 读取 GNU + BSD 长文件名,写入 GNU。扁平结构 — 嵌套的源目录树会被拒绝,并提示使用 tar/zip/cpio。 |
写入器仅限重新打包(`MutationCapability::Streaming`,类似于 tar):现有归档无法进行原地编辑 — `add` / `rm` 会引导您使用
`repack`,它会进行重建。`cab`(Store/MSZIP/LZX/Quantum)、`lzx`(Amiga
LZX)和 `rar`(RAR5 Store/压缩,包含 **solid** 组)是位于 `cab` / `amiga-lzx` / `rar` 特性之后的通过 `compcol` 实现的只读
读取器。solid RAR 组被解码为一个连续的流;像 `repack` 这样的顺序遍历
恰好只解压一次(对早期成员的后向/随机读取会从组开头重新解码,有内存限制)。`lha`
(LHA/LZH,在 `lha` 特性之后)遍历 level-0/1/2 表头并读取
`-lh0-` store 成员;其 LZSS+Huffman 方法已列出,但在 `compcol` 中等待 `lha` 编解码器支持,读取时返回 `Unsupported`。`arc`(SEA ARC,在
`arc` 特性之后)遍历扁平头链并读取 store 成员;其
压缩方法已列出,但在 `compcol` 中等待 ARC 编解码器支持,读取时返回 `Unsupported`。`sit`(StuffIt,在 `sit` 特性之后)解析经典
`SIT!` 容器并读取 store 成员;其压缩方法和整个
StuffIt 5 格式可列出/检测,但在 `compcol` 中等待 StuffIt 编解码器支持,读取时返回 `Unsupported`。`7z`(在 `sevenz` 特性之后)解析整个
容器(LZMA 打包头,按子流切分的 solid 文件夹)并读取
单编码器 Copy / LZMA / BZip2 / Deflate 文件夹;LZMA2(默认)、BCJ
过滤器、PPMd、加密和多编码器流水线已列出,但在 `compcol` 中等待原生 LZMA2 + 分支过滤器编解码器支持,读取时返回 `Unsupported`。每种
归档格式现在都有读取器 — 不再有仅用于检测的脚手架代码。
(`rar` 和 `sit` 充其量是只读的 — 它们的创建是专有的;RAR4、
加密、stored-in-solid 以及已过滤但不受支持的 RAR5 流仍保持
`Unsupported`。)
zip 的 Deflate 支持依托于现有的 `gzip` Cargo 特性(通过
`compcol` 进行原生 DEFLATE);不带该特性的构建会回退到 Stored。`cpio` 和 `ar` 不需要
编解码器。归档到 `ext`/`fat`/`tar` 的重新打包使用专用的 FS 到 FS
拷贝器,目前尚未连接(与 XFS/HFS+ 源的限制相同) — 请通过通用 trait 路径在归档之间转换,或转换为
`iso`/`grf`。
## 压缩
`fstool` 默认启用了六种压缩编解码器。每种都有
自己的 Cargo 特性标志,因此您可以精简二进制文件:
| 编解码器 | 特性 | 用于 |
|-------|---------|----------|
| gzip | `gzip` | SquashFS, `.tar.gz` / `.tgz` |
| xz | `xz` | SquashFS, `.tar.xz` / `.txz` |
| lzma | `lzma` | SquashFS, `.tar.lzma` |
| lz4 | `lz4` | SquashFS, `.tar.lz4` |
| zstd | `zstd` | SquashFS, `.tar.zst` |
| lzo | `lzo` | SquashFS, `.tar.lzo` |
压缩的 tar 输入 / 输出通过文件扩展名检测(如果输入没有可识别的扩展名,则通过魔数检测):`fstool ls
disk.tar.zst /` 和 `fstool repack ext.img out.tar.gz` 即可正常工作。
在内部,编解码器通过临时文件流式传输,因此整个
归档永远不会驻留在 RAM 中。
要在构建时禁用编解码器,例如为了避免在受限系统上捆绑 C 语言编写的 `zstd` 构建版本:
```
cargo install fstool --no-default-features --features gzip,lz4,xz,lzma
```
## 限制
目前明确不在范围内的内容,按可能发生更改的概率粗略排序:
- **ext4 写入路径**:写入路径上的 `flex_bg`(读取器正常)。
- **APFS 原地编辑**:`open_file_rw` 会在整个文件内容之上重建一个新的 COW
检查点,因此它是整文件
粒度的 — 部分 extent COW 尚未实现,
并且 rw 路径上的 `create_file` / `remove` 搭载在同一个
检查点上。多次连续提交受限于
`xp_desc` 环(读取器尚未对其进行轮转)。
- **APFS 读取器**:快照是只读的,且仅支持单叶 snap-meta
(多级 snap 树返回 `Unsupported`)。`UF_COMPRESSED`/decmpfs 文件
内容读取为空(数据尚未解码,尽管可以重用 HFS+ decmpfs
解码器)。加密、sealed-volume 完整性(hash/integrity
树)、Fusion 分层以及基于 dstream 的(`XATTR_DATA_STREAM`)xattrs 不在
范围内。
- **APFS / NTFS 严格检查器测试**:spaceman + `$Secure` /
`$LogFile` 结构现已填充数据,但 `fsck_apfs` 和
`ntfs-3g` 挂载仍然可能因为一些细节问题而标记映像
(空闲队列 B-trees、日志元数据布局)。读取 + 写入可端到端
工作;宿主机工具门槛是剩下的完善工作。
- **NTFS 读取器**:压缩和加密的 `$DATA`,`$ATTRIBUTE_LIST`
溢出,以及通过 `$Secure` 进行的超出驻留路径处理范围的
安全描述符间接寻址均返回 `Unsupported`。
- **XFS 读取器**:深于叶节点上一级的 B-tree 格式(`di_format=BTREE`)目录
返回 `Error::Unsupported`
(shortform / block / leaf / node 和单级 B-tree 目录受
支持);写入器假定是 shortform / extent 目录。Node 格式
(多叶 dabtree)的 xattrs 是只读的。
- **HFS+ decmpfs**:支持 type 3(zlib inline)+ type 4(zlib resource
fork)。LZVN(类型 7/8)和 LZFSE(类型 11/12)返回
`Unsupported`。
- **DMG**:只读 — 没有 DMG 写入器 / `convert` 路径。加密的 v1
(`cdsaencr` 遗留 3DES)块返回 `Unsupported`;v2 受
支持。
- **Trait 接口上的部分文件重写** — `open_file_rw`
在所有安全的地方都存在,但在句柄上的 `Read + Write +
Seek` 之外,尚未公开带有类型的“在已知大文件上修补此字节范围
”的 API。
## 试一试
```
cargo install fstool # or: cargo install --path .
mkdir -p /tmp/src/etc && echo hi > /tmp/src/greeting.txt
fstool create -t ext4 /tmp/src -o /tmp/out.img
fstool info /tmp/out.img
fstool ls /tmp/out.img /
fstool cat /tmp/out.img /greeting.txt
e2fsck -fn /tmp/out.img # must report clean
```
运行测试套件:
```
cargo test # unit tests + external cross-checks if tools present
```
CI 会在 Linux 上运行完整的套件(安装了 `apt` 包管理器提供的 `e2fsprogs`、
`dosfstools`、`mtools`、`gdisk`、`qemu-utils` 用于交叉验证),外加在 macOS(Homebrew `qemu`)和 Windows 上的构建 + 测试通过。
## 许可证
MIT。版权所有 © 2026 Karpelès Lab Inc. 参见 [LICENSE](LICENSE)。
标签:Python安全, Rust, 可视化界面, 文件系统, 文档结构分析, 格式转换, 磁盘镜像, 网络流量审计, 通知系统