# EFS (SGI Extent File System) Implementation Plan
Read-only support for SGI Volume Header + EFS filesystems on optical disc
images (ISO / BIN+CUE / CHD), parallel to the existing HFS/HFS+ path.
This plan is trackable: tick the `- [ ]` boxes as work lands. Each phase is
self-contained and ends with passing tests.
## Scope decisions
- **XFS is out of scope** for this work. All five sample IRIX CDs in
`~/irixCDs/` (IRIX 4.0.4, IRIX 5.3, IRIX 5.3 "XFS", IRIX 6.4.1, IRIX
6.5.29) carry an SGI Volume Header but the actual filesystem on every
partition is **EFS** (magic `0x00072959` at sb+28). No XFS sample disc
is available, so the XFS port from rusty-backup is deferred indefinitely.
- **Target version bump:** `0.2.0` → **`0.3.0`** (additive feature).
- **Synthetic EFS fixture** is committed to the repo for CI; large IRIX
ISOs stay out of the repo and are exercised by env-var-gated tests.
## Sample-disc reconnaissance (already done)
All sample ISOs have SGI Volume Header magic `0x0BE5A941`. Partition table
at offset `0x138` (sixteen 12-byte entries: `blocks`, `first`, `type`, all
big-endian 32-bit). The IRIX install CDs label the data partition with
**type byte 5 (SYSV)** rather than 7 (EFS); we must detect by reading the
EFS superblock magic, not by trusting the partition-type byte.
| IRIX 5.3 XFS.iso | part[7] type=SYSV | 60768 | EFS `0x00072959` |
| IRIX 6.4.1 (Origin, Octane).iso | part[7] type=SYSV | 62272 | EFS `0x00072959` |
| IRIX-53.iso | part[7] type=SYSV | 56864 | EFS `0x00072959` |
| sgi-mips-irix4.0.4.bin | part[7] type=SYSV | 41408 | EFS `0x00072959` |
| Irix 6.5.29 Installation & Overlays (1-3).iso | part[7] type=SYSV | 49248 | EFS `0x00072959` |
EFS superblock layout (big-endian, alignment-padded, magic at sb+28):
| 0 | be32 | fs_size |
| 4 | be32 | fs_firstcg |
| 8 | be32 | fs_cgfsize |
| 12 | be16 | fs_cgisize |
| 14 | be16 | fs_sectors |
| 16 | be16 | fs_heads |
| 18 | be16 | fs_ncg |
| 20 | be16 | fs_dirty |
| 22 | (2 pad) | — |
| 24 | be32 | fs_time |
| 28 | be32 | fs_magic |
| 32 | char[6] | fs_fname |
| 38 | char[6] | fs_fpack |
| 44 | be32 | fs_bmsize |
| 48 | be32 | fs_tfree |
| 52 | be32 | fs_tinode |
| 56 | be32 | fs_bmblock |
| 60 | be32 | fs_replsb |
| 64 | be32 | fs_lastialloc |
| 88 | be32 | fs_checksum |
## Reference sources
- `~/repos/rusty-backup/src/partition/sgi.rs` — SGI volume header parser.
- `~/repos/rusty-backup/src/fs/efs.rs` — EFS reader (1006 lines, complete).
- `~/repos/rusty-backup/docs/SGI_Filesystems.md` — on-disk format notes.
- Linux v5.15 `fs/efs/` — last kernel with EFS read-only support.
---
## Phase A — Foundations
- [x] Bump `Cargo.toml` `version = "0.2.0"` → `"0.3.0"`.
- [x] Add `FilesystemType::Efs` to `src/formats.rs`; update `display_name()`
(`"EFS"`) and `is_browsable()` (include `Efs`).
- [x] Create `src/sgi.rs`:
- [x] `SgiVolumeHeader` struct with magic, volume directory entries, and
16 partition entries.
- [x] `SgiPartitionEntry { blocks: u32, first: u32, ptype: u32 }`.
- [x] `SgiPartitionType` enum (VOLHDR/SYSV/EFS/XFS/VOLUME/... — keep all
IRIX values for display; only EFS/SYSV are acted on).
- [x] `SgiVolumeHeader::read_from(reader: &mut dyn SectorReader) -> Result<Self>`
using `read_bytes(0, 512)`. Validate magic `0x0BE5A941`; checksum
verified but not enforced (log warning on mismatch).
- [x] Re-export `SgiVolumeHeader` from `lib.rs` (mirror the HFS pattern).
- [x] Unit tests in `sgi.rs`: parse a hand-built header buffer; reject
wrong magic; correctly decode all 16 partition slots.
## Phase B — Detection
- [x] Extend `detect.rs` with a `probe_sgi_detail` helper run alongside
`probe_hfs_detail`:
- [x] If `read_bytes(0, 4)` yields `0x0BE5A941`, parse the SGI volume
header.
- [x] Walk partition entries; for each non-empty / non-wrapper entry
probe at `first × 512 + 512`: read 4 bytes at sb+28 and check
against EFS magic `0x00072959` or `0x0007295A`. First hit wins.
(Accepts any partition type; the IRIX install CDs use SYSV (5)
rather than EFS (7).)
- [x] Return `(SgiVolumeHeader, partition_offset, FilesystemType::Efs)`.
- [x] Extend `DiscImageInfo` (`detect.rs`):
- [x] `pub sgi_header: Option<SgiVolumeHeader>`.
- [x] `pub efs_partition_offset: Option<u64>` (byte offset within the
sector reader).
- [x] Populate the new fields in all three probe paths: `probe_iso`,
`probe_bincue`, `probe_chd`. Factored through a shared
`DiscImageInfo::build` helper.
- [x] Unit tests: synthetic image with SGI header + EFS superblock placed
at the partition offset is detected as `FilesystemType::Efs`;
missing magic returns `None`.
## Phase C — EFS filesystem core (`src/efs.rs`)
Port from `rusty-backup/src/fs/efs.rs`. Adapt every `Read+Seek` call to
`SectorReader::read_bytes`. Drop the bounce-buffer alignment helper —
the existing sector readers already enforce 2048-byte alignment.
- [x] `EfsSuperblock` with explicit byte-offset reads (magic at sb+28).
- [x] `EfsExtent` (8-byte packed record: `magic:8, bn:24, length:8, offset:24`).
- [x] `EfsInode` (128 bytes, 12 inline extents, mode + size + nlinks).
- [x] `inode_byte_offset(sb, inum)` using `firstcg` / `cgisize` /
`cgfsize` (cylinder-group math, ported from Linux v5.15 `efs_iget`).
- [x] Type-predicate helpers (`is_dir`, `is_regular`, `is_symlink`).
- [x] Unit tests: parse / reject bad magic / reject short buffer / label
formatting / inode-byte-offset hand computation / extent decode /
inode type predicates.
- [ ] Directory-block walking and inode/extent I/O happen in Phase D
(they need a `SectorReader`).
## Phase D — `Filesystem` trait impl (`src/browse/efs.rs`)
- [x] `EfsFilesystem`-backed browser implementing `Filesystem`:
- [x] `root()` — inode 2 as a `FileEntry`.
- [x] `list_directory(entry)` — walk inode extents, parse dir blocks
(slot table at byte 4 with `slot*2` offset; skip `.` / `..`).
- [x] `read_file(entry)` — read entire file via inline extents.
- [x] `read_file_range(entry, offset, length)` — byte-window read.
- [x] `read_resource_fork{,_range}` → `Ok(None)`.
- [x] `volume_name()` from `fname` + `fpack` (Latin-1, trimmed).
- [x] Symlinks: mode bit `0o120000`; opticaldiscs' `EntryType` has no
`Symlink` variant, so symlinks surface as `EntryType::File` with
`symlink_target` populated. Matches HFS alias handling.
- [x] `FileEntry.location` carries the EFS inode number (parallel to LBA
for ISO 9660 and CNID for HFS/HFS+).
- [x] Register in `browse/mod.rs::open_disc_filesystem` so the existing
entry point dispatches on `FilesystemType::Efs`.
## Phase E — Public surface
- [x] Re-export `EfsSuperblock`, `EfsExtent`, `EfsInode`, `EfsFilesystem`,
`SgiVolumeHeader`, `SgiPartitionEntry`, `SgiPartitionType` from
`lib.rs`.
- [x] Update the crate-level doc comment to mention SGI EFS support.
- [x] Update the README filesystem-support table to list EFS / SGI
Volume Header (read-only browse + extract).
## Phase F — Tests & fixtures
- [x] Commit a deterministic synthetic-image builder in
`tests/common/mod.rs::build_synth_irix_disc` (192 KiB). Builds an
SGI volume header + EFS partition with: `data` (regular file),
`link` (symlink), `sub/` (subdirectory containing `nested`).
- [x] Integration test `tests/efs_synth.rs` (6 tests):
- [x] `DiscImageInfo::open` reports `format=Iso, filesystem=Efs`.
- [x] Volume label `"synth:pack"` matches the EFS superblock fields.
- [x] Known file bytes (`0xAA` / `0xBB` fills) match read-back.
- [x] Browse descends into the subdirectory.
- [x] Symlink target `"/usr/sbin/init"` resolves.
- [x] `read_resource_fork` returns `Ok(None)`.
- [x] Env-var-gated `tests/efs_irix_samples.rs` (skipped unless
`OPTICALDISCS_IRIX_CDS` is set). Verified against all five
bundled IRIX CDs: each opens as EFS and lists a non-empty root.
- [x] Example binary `examples/inspect_efs.rs` for quick visual
inspection of a disc image (browse demo).
## Phase G — Docs
- [x] Update README with EFS in the supported-filesystem table.
- [x] Tick the checkboxes in this file as phases complete.
---
## Acceptance
When every box above is ticked: `cargo test` passes (including the
synthetic-fixture integration test), `cargo clippy -- -D warnings` is
clean, and opening any of the five IRIX CDs via `DiscImageInfo::open`
yields a browsable EFS filesystem.