Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,8 @@ inspect-tab Edit Mode.
| exFAT | Yes | Yes | Yes (in-place + defragmenting clone) | — | Modern removable media (e.g. MiSTer SD cards). In-place resize trims trailing free space; the defragmenting clone (Compact Space toggle / shrink-to-minimum) repacks allocated clusters into a fresh, smaller volume, so a fragmented card backs up to ~its real data size. |
| NTFS | Yes | Yes | Yes (in-place + defragmenting clone) | — | Windows NT / 2000 / XP. In-place resize trims trailing free space; the defragmenting clone (Compact Space toggle / shrink-to-minimum) repacks into a fresh, smaller NTFS volume (from-scratch clean-room formatter, validated to mount under ntfs-3g). Create blank volumes with `rb-cli new --fs ntfs` (selectable `--cluster-size` / `--sector-size`, 512 B–2 MiB clusters); the defragmenting clone inherits the source volume's cluster and sector size. |
| ext2 / ext3 / ext4 | Yes | Yes | Yes | — | Early Linux installs onward |
| HFS (Mac OS Standard) | Yes | Yes | Yes | Yes (check + repair: replica copy, bitmap fixup, lost+found for orphans) | Classic Mac OS 68k / early PowerPC. Includes block-size expansion via clone (`Expand HFS Volume…`). |
| HFS+ / HFSX | Yes | Yes | Yes (defrag clone) | Yes (check + repair) | Mac OS Extended; hardlink resolution. |
| HFS (Mac OS Standard) | Yes | Yes | Yes | Yes (check + repair: replica copy, bitmap fixup, lost+found for orphans) | Classic Mac OS 68k / early PowerPC. Includes block-size expansion via clone (`Expand HFS Volume…`). Volumes written by rusty-backup are `fsck_hfs`-clean and mountable on real Mac OS (verified on Mac OS X 10.4). |
| HFS+ / HFSX | Yes | Yes | Yes (defrag clone) | Yes (check + repair) | Mac OS Extended; hardlink resolution. Create blank volumes with `rb-cli new --fs hfsplus` (`--case-sensitive` for HFSX). The catalog / extents-overflow / attributes B-trees grow their backing fork on demand, so an under-sized or foreign catalog fills in place without a spurious "disk full"; all written volumes are `fsck_hfs`-clean and mountable on macOS. |
| btrfs | Yes | No | No | — | Modern Linux; read-only browse |
| ProDOS | Yes | Yes | Yes | — | Apple II / IIgs |
| CBM DOS (1541 / 1571 / 1581 / 8050 / 8250) | Yes | Yes | — (floppy, fixed geometry) | — | Commodore C64 / C128 / C16 / VIC-20 / PET. PETSCII names, bit-set-is-free BAM, linked-sector files. `.d64` / `.d71` / `.d81` / `.d80` / `.d82`; `.g64` GCR decoded to sectors. |
Expand Down
56 changes: 46 additions & 10 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ Usage: rb-cli [OPTIONS] <COMMAND>
- `--log-file` — Mirror full trace-level log output to PATH regardless of `--log-level`. Useful on Windows cmd where redirection is awkward
- `--config` — Path to a config file. Overrides the platform default location. See `rb-cli config path` for what that location is

## Path grammar (in-image paths)

Verbs that take a path *inside* an image (`ls`, `get`, `get-binhex`, `put`,
`put-binhex`, `mkdir`, `rm`, `cp`, `locate`) address it with one of two
grammars:

- **Slash** (default, every filesystem): `/` is the separator. A literal `/`
inside a single name — legal on classic-Mac HFS / HFS+ volumes, e.g.
`Oxyd b/w` — is written `\/`; a literal backslash is written `\\`. So
`rb-cli get-binhex IMG "/Games/Oxyd 3.6/Oxyd b\/w" out.hqx` extracts the
single file `Oxyd b/w` from the folder `Oxyd 3.6`.
- **Colon** (HFS / HFS+ only): because classic Mac OS reserves `:` as its path
separator, `:` can never appear in a name, so you may instead write the path
with `:` separators — the native Mac convention — and then `/` is ordinary
data needing no escape: `rb-cli get-binhex IMG ":Games:Oxyd 3.6:Oxyd b/w"
out.hqx`. A colon-grammar path is always literal (it never globs).

On every other filesystem `:` is an ordinary filename byte and only the slash
grammar applies. Glob patterns (`*`, `?`, `[`, `{`) use the slash grammar; pass
`--literal` (or use the colon grammar) to address a name containing those
characters verbatim.

## Verbs

### `api`
Expand Down Expand Up @@ -643,7 +665,7 @@ Usage: get [OPTIONS] <IMAGE> <SRC> <DST>
**Arguments**

- `<IMAGE>` — Image reference (`path` or `path@N` for the 1-based partition index)
- `<SRC>` — Source path or glob inside the filesystem. Patterns containing `*`, `?`, `[`, or `{` walk the volume and extract every match. Pass `--literal` to extract a single path verbatim when its name contains those characters
- `<SRC>` — Source path or glob inside the filesystem. Patterns containing `*`, `?`, `[`, or `{` walk the volume and extract every match. Pass `--literal` to extract a single path verbatim when its name contains those characters. A literal `/` in a name is written `\/` (or use a `:`-separated path on HFS / HFS+, which also forces literal)
- `<DST>` — Destination path on the host. Single-match: the literal target file. Multi-match or directory source: a directory under which matched entries are laid out (created if it doesn't exist)

**Options**
Expand Down Expand Up @@ -677,7 +699,7 @@ Usage: get-binhex [OPTIONS] <IMAGE> <SRC> <DST>
**Options**

- `--password` — Password for encrypted containers (currently: WinImage IMZ)
- `-L` / `--literal` — Accepted for consistency with `ls`/`get`/`rm`; `get-binhex` always treats the source as an exact literal path (it never globs), so glob metacharacters in a name are addressed verbatim with or without it
- `-L` / `--literal` — Accepted for consistency with `ls`/`get`/`rm`; `get-binhex` always treats the source as an exact literal path (it never globs), so glob metacharacters in a name are addressed verbatim with or without it. A literal `/` in a name is written `\/` (or use a `:`-separated path on HFS / HFS+)

### `grow`

Expand Down Expand Up @@ -739,7 +761,7 @@ Usage: locate [OPTIONS] <IMAGE> <PATH>
**Arguments**

- `<IMAGE>` — Image reference (`path` or `path@N` for the 1-based partition index)
- `<PATH>` — Path inside the filesystem (Mac path conventions; `/` is the separator — `:` is rejected for the same reason as the other verbs)
- `<PATH>` — Path inside the filesystem. `/` is the separator; a literal `/` in a name is written `\/`. You may instead use `:` as the separator (the native classic-Mac convention), in which case `/` is plain data — e.g. `:System Folder:Oxyd b/w`

**Options**

Expand Down Expand Up @@ -827,7 +849,7 @@ Usage: mkdir [OPTIONS] <IMAGE> <PATH>
**Arguments**

- `<IMAGE>` — Image reference (`path` or `path@N` for the 1-based partition index)
- `<PATH>` — Directory path to create. The parent must exist (no `-p`-style auto-creation in Phase B)
- `<PATH>` — Directory path to create. The parent must exist (no `-p`-style auto-creation in Phase B). A literal `/` in the new name is written `\/`; on HFS / HFS+ a `:`-separated path also works

**Options**

Expand All @@ -847,12 +869,14 @@ Usage: new [OPTIONS] --fs <FS> <IMAGE>

**Options**

- `--fs` — Filesystem to format. One of: hfs, hfv, fat, efs, affs, ntfs
- `--fs` — Filesystem to format. One of: hfs, hfsplus, hfv, fat, efs, affs, ntfs
- `--size` — Volume size, accepting plain bytes or `K`/`KiB`/`M`/`MiB`/`G`/`GiB` suffixes (e.g. `800K`, `5M`). Defaults to 800K (an 800 KiB floppy)
- `--name` — Volume label/name. Defaults to `rusty-backup`. HFS: up to 27 Mac Roman bytes. FAT: up to 11 chars (uppercased; non-ASCII → `_`). EFS: 6-byte fname/fpack. AFFS: up to 30 bytes
- `--block-size` — HFS allocation block size in bytes. Must be a non-zero multiple of 512. When unset, the smallest size that keeps `total_blocks <= 65535` is chosen automatically. Ignored for other filesystems
- `--catalog-size` — HFS Catalog B-tree initial size in bytes (rounded up to a whole allocation block). When unset, scales with volume size like hformat (~0.5%, clump-aligned, 24-block floor). Ignored for other filesystems
- `--extents-size` — HFS Extents-overflow B-tree initial size in bytes (rounded up to a whole allocation block). When unset, ~half the catalog size. Ignored for other filesystems
- `--case-sensitive` — HFS+ only: format a case-sensitive (HFSX) volume instead of the default case-insensitive HFS+. Ignored for other filesystems
- `--min-catalog` — HFS+ only: minimum catalog B-tree size in bytes (a floor, rounded up to whole 4096-byte nodes). Set this *small* to make the catalog easy to outgrow and exercise the fork grow-on-full path. Ignored for other filesystems
- `--affs-variant` — AFFS variant byte (0=OFS, 1=FFS, 2=OFS+intl, 3=FFS+intl, 4=OFS+dircache, 5=FFS+dircache). Defaults to 1 (FFS)
- `--inodes` — EFS only: approximate total inode count. The formatter scales its cylinder groups to hit roughly this many inodes. Mutually exclusive with `--bytes-per-inode`; default density is ~1 inode/4 KiB
- `--bytes-per-inode` — EFS only: inode density in bytes per inode (smaller = more inodes), floored at one inode per 512-byte block. Mutually exclusive with `--inodes`
Expand Down Expand Up @@ -924,7 +948,7 @@ Usage: new-x68k-hdd [OPTIONS] <IMAGE>

### `optical`

Optical-media verbs (rip / convert / browse / extract)
Optical-media verbs (drives / rip / convert / browse / extract)

```
Usage: optical <COMMAND>
Expand Down Expand Up @@ -959,6 +983,18 @@ Usage: convert --format <FORMAT> <SOURCE> <DEST>

- `--format` — Output format

### `optical drives`

List connected physical optical drives and their device paths

```
Usage: drives [OPTIONS]
```

**Options**

- `--remote` — Also query these daemons for their optical drives (repeatable), e.g. `--remote mister.local:7341`. Remote rows print an `rb://...` device arg you can pass straight to `optical rip --device`

### `optical extract`

Extract files from an optical disc image into a host folder
Expand Down Expand Up @@ -986,7 +1022,7 @@ Usage: rip [OPTIONS] --device <DEVICE> --output <OUTPUT>

**Options**

- `--device` — Source drive (e.g. `/dev/sr0`, `disk6`, `\\.\E:`). See `rb-cli show devices`
- `--device` — Source drive: a local path (e.g. `/dev/sr0`, `disk6`, `\\.\E:`) or a remote daemon's drive as `rb://host:port/dev/sr0` (the daemon issues the SCSI reads; this side does the encoding). `rb-cli optical drives` lists local drives
- `--output` — Output path: `.iso` for `--format iso`, `.cue` for `--format bincue`
- `--format` —
- `--eject` — Eject the disc after a successful rip
Expand Down Expand Up @@ -1130,7 +1166,7 @@ Usage: put [OPTIONS] <IMAGE> [HOST_FILE] [DST]

- `<IMAGE>` — Image reference (`path` or `path@N` for the 1-based partition index)
- `<HOST_FILE>` — Host file to copy. Required when not using `--zero` or `--boot`
- `<DST>` — Destination path inside the filesystem (cp-like positional)
- `<DST>` — Destination path inside the filesystem (cp-like positional). A literal `/` in the name is written `\/`; on HFS / HFS+ a `:`-separated path also works (so `/` is plain data)

**Options**

Expand Down Expand Up @@ -1161,7 +1197,7 @@ Usage: put-binhex [OPTIONS] <IMAGE> <HOST_FILE>

**Options**

- `--dst-dir` — Destination directory inside the filesystem (`/` for root). The filename comes from the BinHex header. Defaults to `/`
- `--dst-dir` — Destination directory inside the filesystem (`/` for root). The filename comes from the BinHex header. Defaults to `/`. A literal `/` in a directory name is written `\/`; on HFS / HFS+ a `:`-separated path also works (so `/` is plain data). Defaults to `/`
- `--rename` — Override the filename from the BinHex header
- `--force` — Overwrite an existing entry at the destination path
- `--clear-inited` — Clear the `hasBeenInited` Finder flag (0x0100) on the written file. Use when injecting an app onto a fresh disk so the Finder re-reads its `BNDL` and registers real icons (a file copied with `hasBeenInited` already set is treated as already-catalogued, so it shows a generic icon until a desktop rebuild). Mirrors what a MacBinary install does to byte 73
Expand Down Expand Up @@ -1271,7 +1307,7 @@ Usage: rm [OPTIONS] <IMAGE> <PATH>
**Arguments**

- `<IMAGE>` — Image reference (`path` or `path@N` for the 1-based partition index)
- `<PATH>` — Path or glob pattern inside the filesystem. Patterns containing `*`, `?`, `[`, or `{` walk the volume and delete every match. Pass `--literal` to delete a single path verbatim when its name contains those characters
- `<PATH>` — Path or glob pattern inside the filesystem. Patterns containing `*`, `?`, `[`, or `{` walk the volume and delete every match. Pass `--literal` to delete a single path verbatim when its name contains those characters. A literal `/` in a name is written `\/` (or use a `:`-separated path on HFS / HFS+, which also forces literal)

**Options**

Expand Down
13 changes: 11 additions & 2 deletions docs/hfsplus_btree_growth_plan.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# HFS+ catalog B-tree growth & variable-length keys — implementation plan

**Status:** complete (P1–P5 landed; §4b grow-on-full intentionally deferred).
**Status:** complete (P1–P5 landed; §4b grow-on-full now also landed — see
[`docs/todo_hfsplus_fork_growth.md`](todo_hfsplus_fork_growth.md), Phases A/B/C
shipped and `fsck_hfs`-validated on macOS).
- **P1 (key-format descriptor / variable-length index keys, §4a) — landed.** The
shared B-tree helpers in `hfs_common.rs` are now key-format-aware
(`BTreeKeyFormat`), the HFS+ catalog/attributes inserts and the defrag builders
Expand Down Expand Up @@ -171,7 +173,14 @@ Back-compat: classic HFS passes a `BTreeKeyFormat { big_keys:false,
variable_index_keys:false, max_key_len:37 }` and the behaviour is byte-identical
to today (lock this with a golden test).

### 4b. B-tree file growth on full — DEFERRED
### 4b. B-tree file growth on full — DONE (2026-06-29)

> **Landed (2026-06-29).** Implemented as `grow_btree_fork` (Phases A/B/C) and
> validated `fsck_hfs`-clean on macOS — see
> [`docs/todo_hfsplus_fork_growth.md`](todo_hfsplus_fork_growth.md) for the full
> writeup, including the two foundational blank-builder bugs (Invalid BTH length,
> empty-tree representation) that a real Mac surfaced. The historical deferral
> note below is retained for context.

> **Deferred (2026-06-28).** Implemented sizing (4c) instead and mirrored classic
> HFS, which has **no** grow-on-full path at all — it relies entirely on
Expand Down
34 changes: 33 additions & 1 deletion docs/todo_hfsplus_fork_growth.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
# HFS+ B-tree fork grow-on-full (§4b) — implementation + macOS validation plan

**Status:** TODO / not started. This is the one deferred step of
**Status: DONE — Phases A, B, and C all shipped and `fsck_hfs`-validated on
macOS.** The catalog / extents-overflow / attributes B-trees now grow their
backing fork when they run out of nodes via `HfsPlusFilesystem::grow_btree_fork`
(`src/fs/hfsplus.rs`), with a clean-`DiskFull` + retry-once at each of the three
insert methods. Phase A is contiguous tail growth; Phase B spills the 9th+ extent
into the extents-overflow B-tree (overflow-aware `write_fork_data_with_overflow`);
Phase C appends map nodes past the `(node_size-256)*8` ≈ 30,720-node header cap.

**Two foundational bugs surfaced and fixed along the way** (every HFS+ image
rusty-backup *built from scratch* tripped them, but only a real Mac caught them —
which is exactly why this work was deferred until a Mac was available):

1. **Invalid BTH length.** `write_blank_btree_header_node` laid the BTHeaderRec
(record 0) out as 128 bytes (record 1 at offset 142); Apple requires it to be
exactly 106 bytes (record 1 at 120, bitmap at 248). `fsck_hfs` rejected every
blank with "Invalid BTH length". Fixing it to the canonical 120/248 layout
also resolved the §5 header-vs-map-node capacity discrepancy — the bitmap now
addresses exactly `(node_size-256)*8` nodes, matching `map_nodes_required`.
2. **Empty-tree representation + trailing block.** An empty B-tree was written as
depth-1 with an empty root leaf (Apple uses depth-0 / no leaf), and the
allocation bitmap didn't reserve the trailing block holding the alternate
volume header. Both made `fsck_hfs` report "Invalid node structure" /
"Invalid volume free block count". `btree_insert_full` now bootstraps the root
leaf on the first insert into a depth-0 tree.

Validated on macOS via `rb-cli new --fs hfsplus` (Phase 0) + `rb-cli batch`
(real `create_file` puts past the 8-extent limit) and the in-repo grow tests'
emitted images (`RB_EMIT_GROW_IMAGE` / `_SPILL_` / `_MAPC_` / `_DEFRAG_`), each
`fsck_hfs -f -n` "appears to be OK" and mountable.

---

This is the one deferred step of
[`docs/hfsplus_btree_growth_plan.md`](hfsplus_btree_growth_plan.md) (§4b). P1–P5
(variable-length index keys, blank catalog sizing, extents/attributes splitting,
defrag re-verification, and density rotation) all shipped; this doc is the
Expand Down
31 changes: 31 additions & 0 deletions example/generate_cli_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ fn main() -> std::io::Result<()> {
writeln!(md, "## Global options\n").unwrap();
render_args(&mut md, &cmd, &[]);

md.push_str(PATH_GRAMMAR);

writeln!(md, "## Verbs\n").unwrap();
let mut sub_path = Vec::new();
render_subcommands(&mut md, &cmd, &mut sub_path);
Expand All @@ -46,6 +48,35 @@ fn main() -> std::io::Result<()> {
Ok(())
}

/// Static section documenting how in-image paths are tokenised. Lives in the
/// generator (not a per-arg help string) because it applies uniformly to every
/// path-taking verb (`ls`, `get`, `get-binhex`, `put`, `put-binhex`, `mkdir`,
/// `rm`, `cp`, `locate`).
const PATH_GRAMMAR: &str = "\
## Path grammar (in-image paths)

Verbs that take a path *inside* an image (`ls`, `get`, `get-binhex`, `put`,
`put-binhex`, `mkdir`, `rm`, `cp`, `locate`) address it with one of two
grammars:

- **Slash** (default, every filesystem): `/` is the separator. A literal `/`
inside a single name — legal on classic-Mac HFS / HFS+ volumes, e.g.
`Oxyd b/w` — is written `\\/`; a literal backslash is written `\\\\`. So
`rb-cli get-binhex IMG \"/Games/Oxyd 3.6/Oxyd b\\/w\" out.hqx` extracts the
single file `Oxyd b/w` from the folder `Oxyd 3.6`.
- **Colon** (HFS / HFS+ only): because classic Mac OS reserves `:` as its path
separator, `:` can never appear in a name, so you may instead write the path
with `:` separators — the native Mac convention — and then `/` is ordinary
data needing no escape: `rb-cli get-binhex IMG \":Games:Oxyd 3.6:Oxyd b/w\"
out.hqx`. A colon-grammar path is always literal (it never globs).

On every other filesystem `:` is an ordinary filename byte and only the slash
grammar applies. Glob patterns (`*`, `?`, `[`, `{`) use the slash grammar; pass
`--literal` (or use the colon grammar) to address a name containing those
characters verbatim.

";

fn render_subcommands(out: &mut String, cmd: &Command, path: &mut Vec<String>) {
let mut subs: Vec<&Command> = cmd.get_subcommands().collect();
subs.sort_by_key(|c| c.get_name());
Expand Down
Loading
Loading