Regression fixes - #77
Merged
Merged
Conversation
…025) Windows is now 228 pass / 33 xfail / 0 fail — the same numbers as macOS and Linux. The two-case platform gap that has been there since the suite could first compare platforms is closed. `commit_by_replacing` writes a sibling temp and renames it over the image. It held its handle on the target across that rename and said so explicitly, reasoning that nothing reads through the handle afterwards. The reasoning is correct and the conclusion was still wrong: on Windows a file marked for deletion **keeps its name until the last handle closes**, so a rename cannot reuse a name this process is still holding. Unix frees the name immediately, which is why every developer machine and both other platforms passed. The handle is now `Option<RW>`, released before `persist`. `commit_in_place` is the only other reader and runs solely when `backing_file` is `None` — exactly when the replacement path never ran — so it cannot observe the None. FILE_SHARE_DELETE was the obvious fix and does not work. I tried it first, on both `open_image_ro` and `open_image_rw`, and the case failed identically. It permits the delete to *begin*; it does not let the name be reused while a handle is open. What settled it was pulling the mechanism out of the editor entirely: replacing an unheld file succeeded, replacing a held one failed with the same `os error 5`, with no rusty-backup code in the picture. That also ruled out the read-only attribute and a stale temp, both of which fit the symptom. `open_image_ro`/`open_image_rw` are reverted to what they were. Two cases go green: `subcmd.squashfs.put-rebuilds` and `meta.xattr.set-list-rm` — `xattr set` reaches the same rebuild-and-replace path, which is why a SquashFS bug broke an xattr case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (R-027) A Finder-made `.zip` holding a single `.dmg` reported "no obvious disk image and contains multiple files" and demanded `--inside`. The `__MACOSX/._APFS_Image.dmg` sidecar looked like the cause and was not — `is_apple_double` already filtered it, and had a comment saying why. The actual reason is that `.dmg` was absent from this file's own extension list, so neither entry counted as a disk image; the single-entry fallback then saw two files and gave up. That list had drifted from `DISK_IMAGE_EXTS`, which has carried `dmg` all along, plus `adf`, `2mg`, `woz`, `imz`, `dc42`, `moof`, `d88`, `xdf`, `hdm`, `dim`, `po`, `do`, `gho`, `hfv` and `squashfs`. Each was the same defect waiting for a differently-shaped archive, so fixing only `.dmg` would have left the class open. It now derives from the canonical list and cannot drift again. `bin` is the single exclusion: too common in a mixed archive to identify a disk image by, and picking the wrong entry is worse than asking. AppleDouble entries are dropped at collection now, not only in the image filter, so the single-entry fallback and the "Entries:" listing ignore them as well. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`--format csv` on a nested verb printed a message naming USAGE_ERROR and
exited 1. The constant was not wrong — nothing could carry it. `main` mapped
every `Err` to `GENERIC_FAILURE`, so no handler could ask for a code, and the
two places that documented one were describing behaviour the binary had no
way to produce.
`exit::CodedError` carries a code alongside the message, with `usage()` and
`permission_denied()` constructors and `code_for()` reading it back in `main`.
Uncoded errors are still 1, so nothing else moves.
`code_for` walks the whole `anyhow` chain rather than checking the outermost
error. Context is added all over the CLI, and a `.context(..)` on top of a
coded error would otherwise silently downgrade it back to 1 — the same class
of bug this fixes, reintroduced by accident.
Both R-004 instances now use it: `require_non_flat`, and the `.chd`
output-extension check in `rbformats/chd.rs`. `permission_denied` is unused
here and is next: R-034 wants exit 4 for refusing a write to a read-only
filesystem.
The unit test that let this sit asserted only `is_err()`. It asserts the code
now, which is the part that was actually wrong.
Three cases go green: cli.exit.{csv,tsv}-on-nested-verb-is-usage-error and
shrink.rejects-non-chd-output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`new volume prodos --size 2M vp.img` failed every time. The shared `--name` default is `rusty-backup`, ProDOS forbids `-`, and every other filesystem accepts it — so the one verb that could not use the default was the only one that always failed with no arguments. The default is now the constant `DEFAULT_VOLUME_NAME`, and ProDOS substitutes `RUSTY.BACKUP` when it sees that exact value untouched. Substituting only the default matters: `--name my-vol` still fails, because the user asked for something ProDOS cannot store and silently rewriting it would be worse than the error. The message was the second half of the finding. It said "rename the file" about a volume name, because `validate_prodos_name` serves filenames and volume names alike and hard-coded the noun. It takes the noun from the caller now, so volume creation says "volume name contains '-'" and file creation still says "filename". The "rename the file" advice is gone from both, since neither caller is necessarily renaming a file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Writing to a read-only volume said:
editing not yet supported for filesystem type 'unknown' [exit 1]
on a disk `ls` had listed a moment earlier — and whose real name was printed
on the line immediately above. Now:
editing not yet supported for filesystem type 'Apple Lisa File System'
editing not yet supported for filesystem type 'Alto BFS' [exit 4]
The two halves had different causes.
The name: `fs_name_for` had no `lisafs` entry, and Alto carries no type string
at all — its name lives in `PartitionInfo::type_name`, which the write path
never received. Both are fixed: the missing entries, and `type_name` carried
on `PartitionContext`. Only a message that failed to name the filesystem is
rewritten; every other `Unsupported` is more specific than a generic
substitution would be.
The code: nothing could carry an exit code out of a handler until R-004, so
this was blocked on it. `Unsupported` from a write-open is now
`permission_denied` — what exit.rs reserves 4 for — applied at the 15 verbs
that wrap the shared editable-open.
It does NOT also close R-031, which this finding suggested it might. That case
is still red: a real Apple DOS 3.3 disk reaches the write path as `unknown`
for a different reason and needs its own diagnosis. Checked rather than
assumed, and recorded either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`show partmap` called `Apm::parse` on whatever it was given, so an SGI disk that `inspect` reads fine failed with "bad DDR signature: 0x0BE5" — the leading half of the SGI volume-header magic being read as an Apple driver descriptor. It detects the table first now. APM keeps its full DDR and driver-descriptor rendering; every other table gets the generic partition list, which is all those tables carry. Synthesising a DDR for them would be worse than leaving it out. `PartmapPayload` already had a `kind` field, so the structured output had anticipated this. Also records R-015 and R-012 as blocked upstream. Both live in the `opticaldiscs` crate rather than here. I bumped 0.13.0 -> 0.14.0 to check: it builds with no API change and fixes neither, so the bump is reverted rather than carried for nothing. Both findings now hold a minimal cue sheet that reproduces them, ready to hand upstream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`--fs-type` existed on `ls`, `fsck` and `du`, and cli-reference.md described
it as the mechanism for CP/M images "which have no on-disk signature".
`inspect` rejected it during argument parsing, so the nine CP/M DPBs could not
be inspected at all.
Adding the flag was not sufficient, which is the part worth recording. A
signature-less filesystem has no partition table either, so
`PartitionTable::detect` failed before the forced type could be applied —
inspect accepted the flag and still refused the disk. It now mirrors `ls`:
with `--fs-type`, a detection failure means "raw filesystem at byte 0".
rb-cli inspect ManicMiner.dsk --fs-type cpm:amstrad_data
Partition table: None
1 cpm:amstrad_data 0 180.0 KiB
The forced string is also what gets displayed when `fs_name_for` has no entry
for it: `cpm:amstrad_data` names the disk considerably better than "unknown".
A missing image now exits 3. The case requires it — that is how it tells "the
flag parsed and the file was absent" from "the flag was rejected" — and
exit.rs reserves NOT_FOUND for exactly this. It needed the coded-error
machinery from R-004, so this was blocked on that too.
That exposed a contradiction between two cases: `cli.exit.missing-image-file`
asserted exit 1 and described itself as "current documented-free behaviour".
It pinned the status quo rather than the contract, and could not both hold and
let R-010 be fixed. Corrected to 3 deliberately, with the reasoning in the
case, rather than weakening the fix to keep it green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-015 and R-012 are both in the `opticaldiscs` crate, not here, so neither can be closed from this repository. This carries what an upstream session needs: the API surface rusty-backup consumes, a minimal cue sheet for each, the exact error text, and the behaviour that would let the two red cases go green. Verified against 0.14.0 as well as the 0.13.0 in use, so neither is already fixed on the latest release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`optical extract` took only a whole disc and a destination folder, so pulling one file off a CD meant extracting all of it. Four additions: --path PATH a file, or a folder, instead of the whole disc --recursive / -r include subfolders when --path names a folder --tar FILE .tar / .tar.gz / .tar.zst instead of loose files --preserve-permissions apply the disc's POSIX mode to what is written `--tar` is what makes permissions actually survive. A Rock Ridge, HFS+ or EFS disc records mode, uid and gid — `FileEntry::posix` has carried them all along and nothing read it — but a plain extraction drops them on any host that cannot store them, which on Windows is all of them. A tar entry carries them regardless of host, and carries real symlinks rather than silently copying a link's target bytes. Whole-disc extraction is unchanged: no `--path` still means the whole tree, still recursive, so `--recursive` only decides what a named folder means. Naming a folder extracts its contents into `--to` rather than recreating the folder itself, matching `cp -r DIR/. DEST`. Path resolution walks the disc and falls back to a case-insensitive match, because ISO 9660 upper-cases names while the user reads them off `optical browse`. A path that is not on the disc exits 3 (NOT_FOUND) and is resolved before the destination is created, so a typo leaves no empty directory behind. Verified on a real ISO: single file by path, whole-disc tar, and a missing path exiting 3. The optical regression group is unchanged at 19 pass / 2 xfail — the two being R-015 and R-012, still upstream. GUI is not wired up yet; that is the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`--filesystem` selects by *type*, so it always finds the first HFS volume and
there was no way to reach a second — Apple shipped CD-ROMs with two.
`--filesystem-index N` indexes the list `optical info` prints and overrides
the type selection. Out of range is a usage error naming the real count and
pointing at `optical info`, rather than opening the wrong volume.
It lands on `browse` and `extract` alike, through the selector both already
share, so the two cannot drift.
Nine cases for the extract surface, which had none — the optical fixtures were
asserted at `info` + `browse` only, which proves a disc parses and proves
nothing about the bytes coming off it:
whole-disc the existing form still works
single-file-by-path sha256-anchored, so a truncated or
mis-offset read fails on content
path-is-case-insensitive ISO 9660 upper-cases; users read from browse
missing-path-is-not-found exit 3, no destination left behind
tar-archive the archive form writes a real tarball
tar-and-to-are-exclusive two destinations is a usage error
needs-a-destination neither is a usage error, not a silent no-op
filesystem-index-out-of-range names the limit, cites `optical info`
hfs-refused-on-a-pc-disc refuses rather than quietly giving the ISO tree
Suite: 246 pass / 24 xfail / 0 fail on Windows, up from 237.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two entries were already done and the file did not know it.
F-001 (`optical extract` is whole-disc only) shipped yesterday: `--path`,
`--recursive`, `--tar`, `--preserve-permissions`, plus nine cases. Its
"59 errors on Windows for names NTFS rejects" note is largely answered too —
a per-path extract sidesteps it for the common case and `--tar` sidesteps it
entirely, since a tar entry can hold a name NTFS would refuse.
F-004 (`show partmap` is APM-only) shipped as R-026. Worth noting it was
tracked in both files: the defect / missing-feature split is not always
obvious from a symptom, and this one was genuinely both — the verb never
claimed to read other schemes, and its error blamed the image for it.
Three new entries for what is left:
F-005 the GUI cannot extract a single file. Small — browse_view.rs already
calls read_file in three places, so the capability is there and only
the offer to save is missing. Flags that the filesystem selector has
to be surfaced or the GUI can never reach both sides of a hybrid disc.
F-006 IRIX support disks, explicitly marked NEEDS SCOPE with the three
readings I could not choose between: volume-header executables as
browsable entries, a disc that genuinely boots, or richer --from-dir.
Records that the second cannot be verified without hardware, which
puts it where R-020 already sits for Amiga.
F-007 no optical fixture has nested directories, so `--path DIR --recursive`
is implemented and unverified. The fixtures that would exercise it are
already catalogued; only the case is missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records what a fresh session needs: the six unpushed commits still owe a macOS/Linux run, which findings are ready versus decision-blocked versus upstream, and the conventions that cost time to relearn — rebuild BOTH binaries, run a control before believing a diagnosis, scope platform-specific findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… R-003) R-005: --format is a per-verb argument, so by the time an error reached main the selected format was gone with the verb's args — the same structural shape as R-004. rb-cli now parses through clap::ArgMatches, walks to the deepest subcommand for a --format, and records it before dispatch. The error arm emits the envelope output.rs has always documented, with status.code from exit::code_for so the envelope and the process agree. Plain text still goes to stderr. Reading clap's own matches rather than rescanning argv means a file named --format can't be mistaken for the flag, and a verb added later is covered without touching the entry point. R-003: decided in favour of implementing the flag rather than correcting the doc — ls is the most script-facing verb in the CLI. All five formats. Text output is untouched, so no existing case's columns move; the structured paths collect an LsRow per entry with a stable field set. CSV/TSV emission moved from show.rs to output::emit_csv_or_tsv and show delegates, rather than the two verbs growing separate copies. cli.envelope.error-envelope-on-failure asserted exit 1, which contradicted cli.exit.missing-image-file once R-010 landed — a missing image is NOT_FOUND and exit.rs has reserved 3 all along. Corrected to 3 deliberately, not weakened to suit the fix. Windows 248 pass / 22 xfail / 0 fail. Verified against the 1.73 floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decided: `backup` has never claimed to decode containers. --help documents SOURCE as "an image file or a block-device path" and says nothing about non-flat internal layouts, so this is a capability the engine lacks rather than code disagreeing with itself — which is exactly the split Regression_Bugs.md states for the two documents. The full report moves to F-008 in missing_features_from_regression.md: the four-container table, both verification traps, and the route to a fix (inspect already opens all four, so backup taking a different route to the bytes is the whole feature). R-016's section keeps a pointer rather than a copy, so the two documents cannot drift. The four cases are unchanged — they assert the behaviour we want and stay red. Their known-failures entries now cite F-008. That needed a harness change. `validate` required every citation to appear in Regression_Bugs.md, so a known failure could only ever name a defect — which is part of why a feature gap looked like the only available filing. It now accepts a finding from either document, and known-failures.toml documents the F-nnn form. validate: 0 problems, 22 known failures. Container cases 3 pass / 4 xfail, unmoved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…class R-001, R-002 and R-018 were one failure wearing three hats: a hand-kept markdown list drifted from the code or CI config it described, and all three were found by a human reading both halves. rb-regress cannot catch them — it runs the binary, and these are claims *about* the binary. So tests/doc_parity.rs, a plain cargo test, reads both halves. R-001 had aged in both directions. AHDI was already fixed incidentally by e0c9bf6 two days after the report; meanwhile a tenth variant, Dsd, had appeared and was missing too. The same drift, still drifting — which is the argument for a test rather than an edit. README now has X68k and DSD rows. Making PartitionTable::ALL_TYPE_NAMES trustworthy takes two guards: type_name's match is exhaustive so a new variant is a compile error, and a #[cfg(test)] guard pins the variant count against the list length, so extending one without the other fails to build or fails the test. R-002 offered two options; took the second. Regenerating the fs/README table from fs/mod.rs would have produced forty rows two directory levels below the code they describe — the same race with a longer starting line. The table is gone, replaced by a pointer at the live sources, and a test stops one growing back. The 0x07 NTFS-vs-exFAT disambiguation was the one routing fact worth keeping, and stayed. R-018 is covered by the same file, exactly as its "Not covered" entry predicted. Control run: deleting the X68k row makes the test fail naming X68k. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`resize --size 16M` on an 8 MiB volume was reported as a silent no-op. It was worse than that. It warned "the FS may refuse" and then went ahead: the FAT resize rewrote the BPB for 16303 clusters and appended 32 KB of new FAT sectors, leaving a filesystem describing twice the blocks its container held. `resize complete`, exit 0. `inspect` still said 8 MiB — a superfloppy's size is its file length — which is what made it look like nothing had happened. The warning was the bug. Two situations were being treated as one: - The volume is the whole file (a bare superfloppy in a plain image). Nothing else lives there and no table needs to keep in step, so appending zeros is exactly what was asked for. Grow, then resize into it. PartitionContext::whole_file_path already expressed this condition. - Anything else — a partition inside a disk, a decoded container. Its length is set by something resize is not editing, so overrunning it is corruption. Now a hard refusal, exit 2, naming `partmap resize` and `grow` as the verbs that can move the boundary. Verified both directions: an 8 MiB FAT superfloppy holding a file grows to 16 MiB, fsck-clean, file intact; an X68k partition asked for 64 MiB in a 16 MiB slot is refused with the image byte-for-byte untouched and its three files still readable. Growing only. A shrink still leaves the file at its old length — trailing slack rather than damage, and truncation is irreversible enough that it should be asked for, not inferred. Noted in the finding. Windows 249 pass / 21 xfail / 0 fail. Verified against the 1.73 floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The filed diagnosis was wrong and it is worth recording why. Nothing was
lost. `payload.bin` came back as `PAYLOAD.BIN` and the `get` in the next
step asked for the lowercase name and missed. "Exit 0 and the data is
gone" and "exit 0 and a filename changed case" call for different fixes;
only the second was happening.
The real defect is scope. `repack` is documented Human68k-only but
opened Human68kFilesystem directly instead of asking how the read path
had routed the volume — and a plain FAT16 superfloppy opens cleanly as
Human68k, because the two share a BPB layout on purpose
(Human68kBpb::parse accepts the standard little-endian MS-DOS form,
which is what X68000 floppies use). So repack rebuilt a FAT volume
through a driver with no long-filename concept, and the short name is
all it can see.
It now requires type_string == Some("human68k") — the same dispatch
identity that chose the driver, carried by both shapes (X68k partition
entries and bare Human68k superfloppies, gated on the 68000 BRA.S
opcode). Plain FAT is refused with exit 2, naming `resize` instead.
The control is what settled it: repack on a real Human68k volume
already worked perfectly — four files, lowercase name intact,
byte-identical, fsck clean. The clone was sound; the input was wrong.
The case built a plain FAT volume, which repack was never for, so it was
rewritten deliberately to use a real Human68k volume. New sibling
resize.repack.refuses-plain-fat pins the input it used to accept, and
that the refusal leaves the volume untouched, long filename and all.
Windows 251 pass / 20 xfail / 0 fail. Verified against the 1.73 floor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decided: normalise the path, rather than keep it or replace it with a device identity. A device identity would have to be synthesised for the common case — an image file, which has no serial number — and inventing one is a bigger change than the finding asks for. backup::metadata::normalize_source_device reduces the source to a label that names it without naming the machine. Three shapes, because they are not all paths: device nodes (\.\PhysicalDrive2, /dev/sda) are already leaves and keep the useful provenance; rb:// labels name the *source* host, chosen by the user, not the producer's layout; everything else is a path to an image and keeps only its file name. Both production write sites go through it. The four other assignments are test fixtures and were left alone. This resolves the finding rather than masking it, which matters because the report is explicit that masking was unavailable: expect_divergence covers byte ranges, and a shorter string changes the file's length. Two hosts backing up the same image now write the same source_device, so the .cbk lengths agree and there is nothing to align. The information leak goes with it. Verified end to end: backing up an absolute Windows path records "disk.img". Windows 251 pass / 20 xfail / 0 fail. Verified against the 1.73 floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The finding understated this by a lot. It was filed as "not byte-identical"; the restored 2 MB image held 44 non-zero bytes against the source's 604 — the boot sector and nothing else. Superblock, spareblock, bitmaps, root fnode: gone. `backup` wrote no partition file at all and exited 0. Cause is detection, not the round trip. An HPFS boot sector is an x86 VBR: EB 3C 90, OEM "IBM 4.50", a BPB, an 0xAA55 signature. detect_superfloppy had no HPFS probe, and HPFS fails the FAT probe because reserved_sectors and num_fats are both 0 — HPFS has no FATs. So it fell through to the MBR parse, which took the 0xAA55 at face value, read an all-zero partition array and reported "MBR, no partitions". `backup` iterates that list, so it had nothing to copy. Nothing in the chain was wrong about its own job. `ls` and `inspect` masked it: both fall back to "raw filesystem @ byte 0" and read the volume fine, so the disk looked healthy right up until it was backed up. The fix is a probe beside the NTFS/exFAT pair HPFS shares MBR type 0x07 with. fs::hpfs::looks_like_hpfs already existed and wants both the superblock magic at sector 16 and the spareblock magic at sector 17, so a chance match needs 64 bits to line up. Two cases pin the mechanism rather than the symptom, because roundtrip.hpfs.raw cannot tell "backed up nothing" from "backed up slightly wrong". The control in the other direction — a synthesized MBR disk with a type-0x07 HPFS partition at LBA 2048, the ao486 shape — still reports MBR, since the probe reads absolute sectors 16/17 which sit in the pre-partition gap. No HPFS fixture exists to assert that in the suite; noted in the finding. README's superfloppy auto-detect list gains HPFS. Windows 254 pass / 19 xfail / 0 fail. Verified against the 1.73 floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three hosts verified at c6e66fd: 254 pass / 19 xfail / 0 fail, zero XPASS on Windows, macOS and Linux. Tranche A is empty but for R-012 and R-015, both blocked upstream. Tranche B has lost its three highest-value entries and its one decision. Tranche C is untouched — "the symptom is known and the cause is not" is exactly the state that does not clear on its own. Struck-through rows are kept rather than deleted. The "what to do" column records what each fix turned on, and two of them turned on the report being wrong, which is the most reusable thing in the document. Three of the four maintainer decisions are answered and recorded so they are not re-asked: R-003 implement, R-016 feature, R-035 normalise. R-011 (should copy-protected G64 dumps open at all) is still open. Suggested order rewritten. R-033 is promoted to second: a QL Microdrive .mdv failing at MBR detection is very likely R-022's shape, a bare volume falling through because no probe claimed it. Also recorded: there is no HPFS fixture, so the control that mattered for R-022 — a partitioned HPFS disk, the ao486 shape, must not be hijacked by the new probe — had to be synthesized by hand and cannot be asserted by the suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by re-reading R-021's closing note, which claimed a shrink left "trailing slack, not damage". It did not. On a 64 MB FAT volume holding a 30 MB file, `resize --size 16M` rewrote the FAT for the smaller volume (clusters 32695 -> 8119) while the file's chain still ran past the new end, printed "resize complete" and exited 0. `get` then returned 16,629,760 bytes of a 30,720,000-byte file — also exiting 0. fsck found ChainPointerInvalid and SizeExceedsChain afterwards. The image was never truncated either, so the freed space was not given back: destructive and useless at once. That note is corrected in place. Three parts: 1. A data floor. The filesystem is the only thing that knows where its data ends, so ask it — Filesystem::last_data_byte. A shrink below that is refused outright, --confirm-shrink or not, and the message names the smallest safe size. The trait default returns total_size, so a driver without an override refuses every shrink; that is the right default, because without an answer no shrink can be shown safe. 2. --confirm-shrink. A safe shrink still truncates the image, which cannot be undone, so it has to be asked for. Growing needs no flag. 3. Truncation, but only when the volume is the whole file — the same condition R-021 established for growing. A partition inside a larger disk has data after it, so there the filesystem shrinks, the image keeps its length, and the log says to follow up with partmap resize. The read-only probe runs before the read-write open, which is the order repack already uses, so it is known to work on Windows. Resolving the partition is a table parse; the filesystem is only opened when actually shrinking. Verified all three ways on a 64 MB FAT volume: 16M with 40M of data refused (exit 2, image untouched, fsck clean); 40M without the flag refused (exit 2, untouched); 40M with the flag resizes, truncates to 40 MiB, and the file reads back byte-identical with a clean fsck. The first draft of the refusal case was wrong and says so: it put a 104-byte payload in a 64M volume and shrank to 1M expecting a refusal. The guard allowed it and was right — the file ends near 150 KB. The case now fills the volume with a 40M zero-allocation so the data genuinely reaches past the target. Also files R-036: a missing image gets three different exit codes across the verb surface (inspect 3, most verbs 1, locate/tar 2) with a raw platform-specific io::Error for a message. Not yet cased. Not covered, deliberately: the rb:// remote resize path, and the GUI, which calls resize_filesystem_for directly. Both recorded in R-037. Windows 257 pass / 19 xfail / 0 fail. Verified against the 1.73 floor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fs.hpfs.os2-warp45.hd — a real OS/2 Warp 4.52 install, monolithicSparse VMDK, MBR type-0x07 HPFS at LBA 63. 136 MB zstd in the large-fixture annex, following the fs.hfv.populated-macos81.hd precedent (a 314 MB real system disk, likewise unminimised). It carries two things nothing else in the corpus does. A partitioned HPFS volume. R-022's control — the bare-HPFS probe reads absolute sectors 16 and 17, which on a partitioned disk sit in the pre-partition gap, so an ao486-shaped disk must still report MBR — was a hand-synthesized MBR that the suite could not assert. It is now fs.detect.hpfs-partitioned-stays-mbr, on a real disk. A real non-flat container. `backup` refuses it with the same "invalid boot signature: expected 0xAA55, got 0x0000" F-008 documents, while `inspect` reads all 4722 files — so that gap is not an artifact of how the synthetic containers were built. Recorded in F-008 rather than added as a fifth red case, since it would duplicate coverage without adding information. read.hpfs.os2-warp45 reads it end to end: HPFS, volume OS2, a directory name with a space out of the Workplace Shell tree, CONFIG.SYS, and a clean fsck over 4722 files / 471 dirs. A synthetic `new volume hpfs` is empty and 2 MB and can exercise none of that. Stored zstd-compressed because the harness materialises .zst fixtures into a per-run cache; rb-cli itself does not decompress them, which a control against fs.apfs.base.hd.img.zst confirmed. 136 MB compressed against 354 MB raw and 173 MB as the zip it arrived in. The first draft of the read case asserted volume_label; the field is volume_name. Windows 259 pass / 19 xfail / 0 fail, 91 fixtures catalogued. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Release workflow went red on the last push and neither the local regression suite nor the 1.73 proxy build could have caught either cause. 1. normalize_source_device was platform-dependent (mine, from R-035). Path::file_name treats `\` as a separator only on Windows, so a Windows path normalised on Linux kept every directory — the exact leak the function exists to close, and it made the field depend on which OS ran the backup. Now splits on both separators everywhere. The unit test asserted the Windows answer, passed here, and failed on all five Unix jobs. 2. optical.rs named the `zstd` crate directly for `--tar` .tar.zst output. The MiSTer armv7 build has `optical` on and `native-zstd` off (it uses pure-zstd), so that call site does not compile there. Routed through crate::rbformats::zstd_compat, which exists for this and which fs/tar_export.rs already uses for the identical tar+zstd job. The second one is not new — it broke in e0977ea and had been failing for a day without reddening a run, because the rb-cli-mini job is continue-on-error: true. `gh run list` reported that run as success. That is worth knowing about more than the bug was. Documented in CONTRIBUTING § "The pipeline builds more than your machine does": CI tests with `cargo test --release`; the MiSTer feature set is `--no-default-features --features chd,pure-zstd,remote,optical,tui`; read the job list from `gh run view <id>` rather than the run conclusion. Verified: cargo test --release green (2775 lib + all integration targets), and the MiSTer feature set now cargo-checks clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Syncing the OS/2 fixture turned up an assumption the docs did not state. `rb-regress fixtures --sync` needs `corpus_source`, and on both oracle hosts that key is deliberately commented out — neither can reach the distribution share. Their corpus was scp'd from the orchestrating box, and that is still the mechanism. So admitting an annex fixture is three steps per host, and the middle one is easy to miss: the catalogue row has to be appended to that host's fixture-map.tsv, which is gitignored and therefore does not arrive with a pull. Without it the file is present and the case still reports skip-fixture. That failure mode is quiet by design — a missing fixture degrades to skip-fixture rather than a failure — so a forgotten sync looks like a green run with a smaller pass count. Both hosts were sitting at 257 pass + 2 skip-fixture against Windows's 259 for exactly that reason. Compare pass counts across hosts, not just fail counts. Also noted: Windows scp.exe cannot resolve an MSYS /tmp path, so stage anything being copied at a real Windows path first. All three hosts now measured at 93a6d53: 259 pass / 19 xfail / 0 fail, zero XPASS, with the fixture verified by sha256 on each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pushback was fair: this branch has been adding more prose than checks. Three things I had written down are things the harness can assert, so they now are, and the prose they replace is deleted rather than kept alongside. R-036 was the clearest miss. I found the exit-code inconsistency, wrote it up, and recorded "Not yet cased" — in a suite whose entire convention is that a finding gets a red case and a known-failures entry. Now six cases, one per verb, because each is a separate contract a script switches on: ls, du, fsck, show fs-info, locate, tar. All red, all citing R-036, all xfail. cli.exit.missing-image-file stays the green counterpart pinning inspect at 3. The CI preflight checklist is now scripts/preflight.sh: cargo test --release (CI's test step is release), the MiSTer feature set, the 1.73 vintage floor, and doc parity. CONTRIBUTING points at the script instead of listing the commands. "Compare pass counts across hosts, not just fail counts" is now a check. rb-regress consolidate grew a COVERAGE SKEW section: cases that ran on some hosts and reported skip-fixture on others. That is invisible in the pass/fail columns, which is how all three hosts sat at 257/257/259 with 0 fail each. The first version of that check was wrong and the tests now pin why. consolidate reads every results.jsonl ever written, so comparing across all of history reported skew that had been fixed weeks earlier — it named eleven optical cases as missing on Windows that Windows had held for weeks. Caught by verifying one of them actually skips today; it passes. Skew is now computed from each platform's latest run only, and two tests cover both directions. Windows 259 pass / 25 xfail / 0 fail. preflight.sh green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`inspect` was right and alone. It carried a per-verb `if !args.image.exists()` check added with R-010; nothing else copied it, so every other verb surfaced the raw io::Error as the catch-all 1 — indistinguishable from a corrupt image, and with a platform-specific message a case cannot assert on. The check now lives in the shared CLI resolver as resolve::require_source_exists, called from the read-only, streaming and read-write entry points, so a verb added later inherits it. inspect's local copy is gone. Three exemptions, none of them a file whose absence can be judged: a raw device (\.\PhysicalDrive0 does not exists(), and backing up a disk is the app's core job), an rb:// reference, and a backup folder, which is a directory and passes anyway. Four unit tests pin them — the device one especially, since getting it wrong would refuse every disk before it was opened. That exemption is also why the manual control mattered: a bash test of the device path reported "no such file", which looked like the fix breaking raw-disk access. MSYS had eaten a backslash, so the predicate never saw a device path; running it through PowerShell gave the correct "Access is denied". The same backslash-eating then corrupted the unit test's literal via a heredoc. Both artifacts of the shell, not the code. The tar case was also wrong and is corrected with the finding's table. `tar` takes IMAGE, SRC and OUT; the original survey passed two positionals, so clap rejected it before the image was opened. That exit 2 was correct — for a malformed command, not a missing file. Given correct arity tar exited 1 like the rest, so the table said "three exit codes" when there were two. Six cases, red the day they were written and green the next. Windows 265 pass / 19 xfail / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-033) The resume note predicted this was R-022's shape and it was, exactly: the driver and the detection both existed, and nothing consulted them on the path the user takes. `detect_superfloppy` had no MDV probe, so a cartridge fell through to the MBR parse, which read the `ff ff` sync at 0x0A as a bad 0xAA55 and refused the file. The probe that recognises it lives in `detect_filesystem_type`, one layer further in than `inspect` ever got. The probe now sits beside the HPFS one added for R-022, gated on both the exact 174,930-byte cartridge length and the sector-0 header. Two unit tests pin both halves of that gate, because a probe that fires too eagerly is worse than one that never fires: a cartridge header at the wrong length, and cartridge-sized bytes with no header, must both be rejected. That is the control R-022 taught. `inspect` now reports qdos_mdv, 170.8 KiB, no partition table. What this does not do, stated plainly: reaching the driver reveals its directory walk is a stub, so `ls` returns "QDOS microdrive directory walk not implemented". That is not a new finding — it is the `.mdv` reader already tracked in OPEN-WORK.md §7 at ~300 LOC against the two anchored fixtures. R-033 was about reachability and reachability is what it fixed. Windows 266 pass / 18 xfail / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both halves of the reporting had the same flaw, and I only fixed one of them yesterday. `consolidate` reads every results.jsonl ever written — 131 of them here — so an unfiltered rollup describes no run that ever happened. The skew check was already scoped to each platform's latest run. The failing-cases list was not: it listed 69 Windows failures accumulated across weeks, every one of them long green. A triage list nobody can trust is worse than no triage list. `latest_run_per_platform` is now shared by both, and failures are derived from it at render time rather than accumulated during the scan. On the real corpus the list goes from 69 entries to none, which matches three green hosts. preflight.sh gains the harness's own tests. The runner is a separate crate, so nothing else in that script compiled it — the pre-commit hook clippies it but never ran its tests. Six consolidate tests, including both directions for each scoped view: a failure fixed in a later run must drop off, one present in the latest run must stay. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-012 and R-015 were blocked on unpublished upstream work. 0.15.0 is out. The bump is the whole fix for R-015. For R-012 it adds FilesystemType::None — a disc carrying no data track now reports that instead of being refused — and the new variant made our match non-exhaustive, so the compiler pointed at the one line that had to change. It is deliberately distinct from Unknown, which means "there is a filesystem here and we did not recognise it". R-011 was decided: copy-protected dumps should open, and be un-editable. Investigating it corrected the report. G64 was already un-editable — is_editable_container_path never listed it. But the write path then opened the *undecoded container bytes* and reported "Invalid MBR: invalid boot signature", which is the error this finding attributes to copy protection. It reproduces on the WORKING dump: `ls` lists three files, `put` fails with the MBR error. So that error was never evidence of a GCR problem — any verb taking the write path produced it on any G64. The whole class is now refused with PERMISSION_DENIED and a message naming the reason and the conversion that yields an editable copy: G64/G71, MSA, EDSK and Apple-II .dsk, i.e. containers the read path decodes and the write path cannot re-encode. Same shape as R-034. What is NOT closed: whether the two named protected dumps decode. Neither is in the corpus or the dropbox — only the working one is. decode_g64_1541 already tolerates partial decode (it bails only when no track yields sectors), so they may well open today, but claiming that without the files would be a guess. R-011 stays open as Partial with the two filenames recorded as what would close it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four findings, two fixes, and two of the four reports had the wrong cause. R-008a/R-008b/R-024 were one bug. A bitmap block holds 127 words, so it accounts for 4064 blocks; the formatter wrote exactly one whatever the volume size. A 4 MB volume is 8192 blocks, so marking the root in use computed word index 128 and indexed bm[512..516] in a 512-byte block — panic, exit 101, no file (R-008b). Below the panic threshold the same bug was silent: blocks past 4066 had no bit at all (R-008a). R-024 was that bug wearing a third hat. "One put makes fsck report errors" was filed against the editor, explicitly separated from the formatter. It was the formatter: put allocated into the uncovered tail and fsck correctly reported the disagreement. It went green with no change to the edit path. The formatter now writes ceil((blocks - 2) / 4064) pages, records the first 25 in the root's bm_pages, and chains the rest through bitmap extension blocks. Verified 1M/3M/4M/8M/32M: all create, all fsck clean. R-030 was neither OFS-vs-FFS nor an older root layout — two of the three readings it asked to distinguish. AFFS records its size nowhere; the root block sits at the midpoint, so its position IS the size. open() derived the block count from the end of the FILE, which for a partition inside an RDB disk is the end of the disk: a 4040-block partition at LBA 2020 has its root at 2020, and the file tail computed 2062. The candidate is always an upper bound, so locate_root_block steps down until a block validates on tag, secType, hash-table size and its own checksum. Bounded at 2048 blocks; every bare ADF hits on the first try. The Workbench 1.3 disk now lists, and fscks clean at 337 files / 31 dirs. R-020's hypothesis is confirmed by that same disk, which is the point: it was recorded as needing an emulator. The real root reads header_key=0 where ours wrote the block number. create_blank_affs now writes 0. Whether that makes the volume mount on real hardware is still unverified and still needs an oracle — but the open question is now "does this fix it", not "is the hypothesis right". The entry's other half was wrong: affs_fsck never inspected header_key at all. Windows 275 pass / 10 xfail / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R-029 is not an engine defect. The addresses are outside the image because the superblock says so: fs.efs.small.hd declares 7,486,242 blocks across 78 cylinder groups (3.8 GB) while the file is 8,192 blocks (4 MB). The engine was reading exactly where a 3.8 GB EFS volume keeps its cylinder groups. ls and get work because the directory and sampled files survive in the first 4 MB; fsck walks all 78 groups and put allocates against a bitmap spanning the declared size, so both run off the end — at byte 50,065,408 and 344,826,880, the two numbers the finding recorded as its symptom. The catalogue still marks the fixture synthetic-minimal: it was minimised by truncation, and truncating a filesystem breaks it rather than shrinking it. EfsFilesystem::open now compares declared size against what is present and warns, naming both numbers. An uninterpretable "short read at byte 344826880" was most of why this sat in Tranche C. The case, which asserted a put/get round-trip and a clean fsck on a volume that supports neither, now pins what the surviving prefix does support. R-032 is reclassified to F-009. The driver has documented the ceiling all along (CLAUDE.md: SFS "single-leaf btree only"), so hitting it is the limit working as described — the entry's own text said "the known ceiling being hit, not a surprise". How it refused was wrong, and that is fixed. It raised a Parse error, which says the disk is malformed when the disk is fine and reads perfectly, and put mapped it to the catch-all 1. Now Unsupported, routed through write_open_error, exit 4. That turned up a second instance of R-034's gap: the write-OPEN path had been fixed, but create_file had not, so every driver refusing at creation time exited 1. put now shares the mapping. Windows 281 pass / 4 xfail / 0 fail. preflight green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both were closed in this round with full notes in their sections, but their summary-table rows named different source files than the strings I matched on, so the replacements silently did nothing and the table still listed them as open. Caught by counting open findings at the end rather than trusting the edits. Open defects now: R-019 (accepted), R-020 (needs an emulator oracle), R-011 (partial — the two protected G64 dumps are not in the corpus). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My first note called fs.efs.small.hd "broken" and blamed truncation. That was unfair and partly wrong. It is a deliberate prefix capture — the first 4 MB of a real IRIX 5.3 disk — and the project does this on purpose. src/fs/efs.rs has 15 unit-test call sites reading it, and parses_superblock_from_fixture asserts the full disk's fs_size of 7,486,242 precisely because that is what the original volume says. docs/SGI_Filesystems_irix_sb.txt documents the same convention for XFS: "dblocks claims a 93 GiB partition but we only extracted the first 64 MiB. Superblock 0 is at offset 0 and unaffected." So the fixture does what it was built for, and the catalogue's `synthetic-minimal` is accurate. What was wrong is the case, which asked a 4 MB prefix to support a write and a whole-volume fsck — those need every cylinder group present, and nothing about a prefix promises that. The engine change stands: warning when the superblock declares more than is present is still the right diagnostic, and it is what makes this legible instead of a short read at byte 344826880. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ield The R-031 fixture was admitted on "rb-cli inspect opens it". It has no filesystem at all — inspect reported `Unknown` and exited 0, because opening anything is what a universal tool does and a disk with no recognisable filesystem still yields a carve view. So the check proved only that the bytes were readable. Rather than teach the harness to special-case that, the engine answers the stronger question: - Every partition row carries `identified`, false when the type name is the engine admitting it recognised nothing. inspect already printed `Unknown`; nothing made it machine-checkable. - `--expect-fs NAME` asserts the disk carries that filesystem. - `--require-known` asserts every partition was identified. Neither changes the default. Universality is the point, so a disk we cannot identify still opens and still exits 0 — cli.flags.inspect-still-opens-what-it-cannot-identify pins that. Two things the type names forced, both found while writing the tests: `+` normalises to `plus`, not to nothing. The first version stripped it as punctuation, which collapsed HFS+ onto HFS and made `--expect-fs HFS` accept an HFS+ volume — the exact false positive the exact-match rule exists to prevent. Clippy flagged the `== false` in the test I had written to pin that behaviour, which is what sent me back to look at it. A type byte shared by several filesystems is named for all of them, so each alternative matches separately: `--expect-fs HPFS` is satisfied by the OS/2 fixture's `NTFS/HPFS/exFAT`. Verified against the real disk. Predicates live in src/fs/mod.rs so the GUI and TUI can adopt them. Swept the corpus with the new field: of 42 compressed and 11 uncompressed fs.* fixtures the only unidentified one is the known-bad fs.apple-dos.invaders. Three CP/M-family fixtures need their --fs-type preset by design (R-010) and identify cleanly once given it — an admission check needs a per-fixture "requires this --fs-type" field or it will reject three good fixtures. Windows 286 pass / 4 xfail / 0 fail. Clippy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Third verification switch, alongside --expect-fs and --require-known. Answers "is this a bare volume or a whole disk?" — the question that decides whether a fixture belongs to the superfloppy path or the partition-table path, and the one a harvested image is most often wrong about. Takes a scheme name (mbr, gpt, apm, rdb, sgi, sun, ahdi, x68k, dsd, none) or one of four aliases: superfloppy / flat, partitioned / hd. The vocabulary is generated from PartitionTable::ALL_TYPE_NAMES rather than written out, so a new variant becomes askable automatically — and that constant is already pinned against the variant count by type_name_parity, so neither can drift alone. A test asserts every scheme and alias is askable. A word that names no layout exits 2, not 1. Getting that wrong would be the most misleading answer available: a typo would read as "the disk is the wrong shape" and the script would blame the disk. The error lists the whole accepted vocabulary. Dsd is the one judgement call. It has no on-disk table either, but the reader de-interleaves its two sides into two addressable volumes, so it answers "partitioned" — that is what a caller asking the question actually wants to know. Written down in is_partitioned and pinned by a test. Predicates live in src/partition/mod.rs so the GUI and TUI can adopt them, matching where the --expect-fs ones went. Windows 289 pass / 4 xfail / 0 fail. preflight green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removed one commit after adding it. The flag asserted "no partition
reported Unknown", which is a weaker version of a question --expect-fs
answers precisely, and it served a case that does not arise: for fixture
admission the expected filesystem is always known, because the fixture
ID declares it.
The two-step flow is the whole story, and needs no third switch:
rb-cli inspect DISK --format json # discover: identified=false
rb-cli inspect DISK --expect-fs "DOS 3.3" # assert: exit 1
`identified` was already in the JSON payload, so inspect states the fact
machine-readably without a flag to ask for it. Emit the fact; do not add
a switch for it.
Checking it against real disks is what settled it. --require-known
passed a Solaris disk whose swap slice is not a filesystem at all,
because the Sun driver names it "Sun swap (UFS?)" and that is not one of
the six sentinels. So the flag really asserted "the engine had something
to say about every partition", not "every partition holds a mountable
filesystem" — and its name promised the stronger reading. A flag that
lenient, that easy to misread, and that redundant is worth removing
rather than renaming.
is_identified_fs stays: it is what computes the `identified` field.
Windows 288 pass / 4 xfail / 0 fail. preflight green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`origin = harvested-verified` meant "rb-cli inspect opened it", which a universal reader says about anything. An Apple DOS fixture that was really a bare bootloader with no VTOC passed that bar and sat in the corpus for weeks (R-031). "It opened" is not identification. The catalogue gains three optional columns — expect_fs, expect_layout and fs_type — and `rb-regress fixtures --identify` enforces them by running `rb-cli inspect --expect-fs / --expect-layout`. Rows that declare nothing are skipped, so populating is incremental rather than a flag day. Parsing is positional and the columns are appended, so every existing row keeps working. 72 of 91 rows now declare. Three groups are deliberately left out and FIXTURES.md says why: optical discs (inspect is the wrong verb — `optical info` reads those), the two encrypted images (identifying them needs a password the catalogue does not carry), and the invaders disk, which would fail correctly because its ID claims a filesystem it does not have. Renaming that fixture is the honest fix, not weakening the check. It found a bug in its own matcher on the first real run. fs_name_matches split the found name on '/' so that HPFS could satisfy the ambiguous type-byte name NTFS/HPFS/exFAT — which meant the full string no longer matched itself. Whole-name comparison now runs first, with two regression tests: a name always matches itself, and an alternative still satisfies a shared type byte. Control run, then reverted: declaring the invaders disk as `DOS 3.3` makes the check exit 1 naming what it found instead. That is the arrival check R-031 needed. check_identities materialises rather than resolves, because rb-cli does not read .zst — the harness expands fixtures through the same per-run cache a case uses. Windows 288 pass / 4 xfail / 0 fail; identity 72/72; preflight green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… of git Availability was hand-maintained inside the tracked data/oracles.toml, which is how an absolute D:/ROMs/Amiga/Shared/rom ended up committed to a public repo. Same split the corpus already uses: data/oracles.toml tracked what an oracle IS and what it proves data/oracles.local.toml ignored whether THIS box has it, and where `oracles --detect` probes the host and rewrites the overlay; `--export` prints it to seed another machine. An overlay row replaces the tracked one for the same (oracle, platform). All 12 path_hints are gone from the tracked registry, and re-running --detect still finds every tool, because hints come back through the merged registry. The first detection run produced two confidently wrong answers, which is the failure mode that matters for a tool whose job is saying what is true. Both are fixed and both are tested: - It found Cygwin's `mount` on Windows and called AFFS "verified". A mount oracle is the kernel's opinion, so it is now only probed on Linux / WSL / the MiSTer HPS. Windows has no AFFS driver and the tracked registry never claimed it did. - It reported chdman and qemu-img absent on a box that has both, because their path_hint is a directory and the probe tested is_file(). A hint may now name the program or the directory holding it. That fix alone found four real tools: chdman, qemu-img, ghostexp and 7z. Emulator / hardware / roundtrip oracles record `manual`, not `absent` — `which` cannot find a running guest, and absent would read as a missing install rather than a different kind of oracle. verify is unchanged at 18 pass, which is the point: the overlay reproduces the availability the tracked file used to assert. Windows 288 pass / 4 xfail / 0 fail; preflight green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--detect skipped every emulator as `manual`, which was wrong twice over: an emulator binary IS findable, and "manual" hid the difference between "not installed" and "installed somewhere we did not look". Three ways to locate one, in order: 1. Usual install roots per platform, since emulators install as apps and never land on PATH: %ProgramFiles%, C:/Tools, C:/Emulators on Windows; /Applications including .app/Contents/MacOS on macOS; /opt and ~/.local/bin on Linux. One level deep, which is the `Program Files/WinUAE/winuae64.exe` shape. 2. RB_EMULATOR_DIRS, for a machine that keeps them elsewhere. No code change, no hand-edited TOML. 3. `oracles --set <id>=<path>`, and failing that --detect simply asks. The prompt is deliberately narrow. Emulators only: a package oracle that came back absent is genuinely not installed, and asking there would be 18 questions to reach the 5 that matter. Terminals only: the harness runs in CI and over ssh, where a prompt nobody answers is indistinguishable from a hang. --no-prompt forces the quiet path, and piped stdin is verified not to prompt. A blank answer skips. A path that does not exist is refused and re-asked, rather than written and found weeks later as a silent skip — though the check is only that it exists, so a wrong path stays wrong until overwritten. FIXTURES.md says that rather than implying more. A found binary records `installed`, not `verified`: it still needs a configured guest. `verified` would overclaim, `manual` would hide it. The five emulator oracles gain a `program` name in the tracked registry — the binary name is portable knowledge; where it sits is not. Also adds runner clippy to preflight. The hook runs it and preflight did not, so preflight said "all checks passed" and the commit was then rejected — which is precisely the surprise preflight exists to prevent. 10 unit tests, including that package oracles are never asked about and that a refused path leaves the row untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The board is just another host in local.toml. Give that entry an ssh target and `rb-regress oracles --scan mister-core` lists /media/fat/_Computer/*.rbf and matches what it finds against a new `core` field on each mister-core-* oracle. Nothing new to configure. The board's address and key live in local.toml with every other machine, which is the one file that never reaches the repo — so scanning needs no flag for an IP and no place for one to leak. The core names were read off the real board rather than guessed, which mattered: it is `Ti994a`, not `TI-99_4A`, and `CoCo3` rather than `CoCo2`. A guessed mapping would have reported two cores missing on a board that has them. MiSTer stamps a build date into every filename — X68000_20260603.rbf — and it moves on each update, so the bare name is the only stable identifier. core_base_name strips an 8-digit suffix and leaves anything else alone, so a core legitimately containing an underscore survives. A core the registry names and the board lacks is reported MISSING, not skipped: a scan that silently omits what it could not find reads exactly like one where everything was present. Results merge into the overlay rather than replacing it, so a scan never discards what --detect found on this machine. Verified: 23 rows after scanning, holding both the 9 cores and the 6 local tools. Scanned the real board: 72 cores present, all 9 the registry names matched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
--detect searched %ProgramFiles%, C:/Tools and C:/Emulators but not the per-user collection, which is at least as common and is where this project's own emulators actually live. Adding ~/emulators found four of the five in one run: iris, WinUAE, 86Box and MAME, none of which had been registered. WinUAE joins as a sibling of fs-uae rather than replacing it. Same emulator family, same three formats (AFFS, PFS3, RDB), same host-directory results channel; a given machine tends to have one or the other, and whichever is installed is what --detect records. This box has WinUAE and 38 licensed Kickstart ROMs, so fs-uae alone would have left the Amiga oracle looking unavailable on a machine fully equipped for it. Also fixes the detect summary, which printed only `verified` rows and so hid every `installed` one — a report that omitted its own best news. It now prints both, with the status in a column, and counts installed separately. ROM paths stay out of this commit. The Kickstarts and the Mac Plus / SE ROMs pulled off the MiSTer are machine-specific and belong in the gitignored overlay, which is the rule that got D:/ROMs out of the tracked registry in the first place. 43 oracles; 6 verified + 4 installed on this host. preflight green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…we write
amitools' xdftool is the first code that is not ours to read our AFFS
output, and it refuses it:
FSError: Bitmap Block Count Mismatch(15): got=2 want=1
The control is what makes this a finding rather than a broken tool. The
same xdftool reads fs.affs.workbench13.hd.hdf — a real Workbench 1.3
disk — perfectly: full tree, 3421 blocks, 1988 timestamps. So it handles
AFFS, and it handles multi-bitmap-block volumes, since that 3 MB fixture
needs two. It fails identically on artifacts produced on Windows, Linux
and macOS.
Filed as R-038. Stated carefully: amitools is a reimplementation, so
this does not prove a real Amiga refuses the volume — that is still
R-020's question and still needs hardware. What it is, is the first
independent read. Every AFFS check before this was our formatter
agreeing with our fsck, which is the condition R-020 has been stuck in.
The oracle is `structural`, not `authoritative`, for that reason.
Two xdftool quirks are recorded because each cost real time: it takes
its geometry from the file EXTENSION, so a 2 MB volume named .adf is
rejected as an invalid floppy size and must be .hdf; and it opens
read-write before parsing, so a read-only artifact fails with EACCES
having never looked at the image (-r does not help — the open is first).
oracles/amitools_affs.py handles both.
Also adds a {oracles} substitution to verify's check expansion. Checks
run with the artifact's directory as cwd, so a relative wrapper path
resolved inside artifacts/<os>/<format>/ and the interpreter reported a
missing file. fsck_hfs avoided this only by putting its script in
`program`, which the harness absolutises.
Snow's binary is snowemu.exe, not Snow.exe — corrected, and it now
detects.
verify: 18 pass, 3 FAIL (the same R-038 across three producer OSes),
45 skip-no-check, 81 skip-manual, 39 skip-unavailable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brought Iris up as far as a live IRIX shell and recorded exactly how,
because four separate things cost time and none are in --help.
Working invocation:
IRIS_JIT=1 iris --headless --noaudio --ci --scsi1 disks/Indy-IRIX65_dev.chd --cdrom4 <any.iso>
iris-ci boot && iris-ci login && iris-ci run "uname -aR"
-> IRIX IRIS 6.5 6.5.22m 10070055 IP22
What cost time:
* No PROM is needed. It warns about a missing prom.bin and falls back to
an embedded one. I went looking for Indy firmware on the NAS and
across two drives before testing the assumption; the test took one
command and the search took much longer.
* --ci is required. Without it iris listens only on the monitor port
8888 and iris-ci cannot reach 19851.
* A CD-ROM must be attached even when booting from disk.
* IRIX 6.5 boots, 5.3 does not: Indy-IRIX53_dev.chd reaches "The system
is coming up", prints "Find Error: 10" and stops. Not slow — the
serial buffer goes silent and stays silent.
The remaining blocker is getting a file INTO the guest. `iris-ci put`
wants a scratch volume that has no --scratch flag to configure, and
--nfs-dir exports fine on the host side but the guest cannot mount it
("Port mapper failure"), then loops retrying and blocks the shell.
Once a file can cross, the check is IRIX's own fsck on our EFS volume —
an authoritative answer nothing else in the registry can give. That is
12 pairs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first authoritative oracle result this project has had. IRIX 6.5.22, under Iris, checking our EFS volume with its own /sbin/fsck: ** Phase 5 - Check Free List BAD FREE LIST 1 files 1 blocks 3964 free Phases 1-4 pass. IRIX walks our blocks, pathnames, connectivity and reference counts without complaint and reads the volume name. Only the free list is wrong. The control is what makes it a defect and not an artifact: mkfs_efs run on the same device, same guest, same scratch path, produces a volume that passes the identical check. Same device node, same fsck, same session — the only variable is who wrote the filesystem. Every EFS check we had before was our code checked by our code. edit.efs.put-get, fs.new-volume.efs and roundtrip.efs.raw are all green. This is the vendor's own tool, which is what strength="authoritative" means. It is R-038's shape one level stronger: amitools is a reimplementation, IRIX is the implementation. The route in, which --help does not document, is recorded on the oracle: the scratch volume is `scratch = true` on a SCSI device in iris.toml, `scsi` is a map keyed by id rather than an array, every device needs `cdrom`, and iris creates the volume itself. fsck then needs the device node, not a file — "Can't find equivalent raw device", exactly as macOS fsck_hfs does — so the image goes in with `scratch write` and the check runs against /dev/rdsk/dks0d2s0. `< /dev/null` or fsck blocks forever on a SALVAGE prompt. Also fixes a duplicate `notes` key I introduced in 2c0da94, which left the registry unparseable. `validate` had been reporting it; I read only the tail of its output and missed the line above. Merged into one note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every file, directory, and symlink rb-cli created in an SGI EFS image had atime/mtime/ctime = 0. IRIX and any Unix ls -l on the volume showed "Dec 31 1969 19:00" for everything — including the root directory. The on-disk codec was fine; the write paths just never set the fields. Root cause: EfsInode::empty() zeroes every field (correct — it's the free / delete slot representation) and the create paths only assigned mode / nlink / uid / gid on top of it, leaving atime/mtime/ctime at zero. The mkfs root inode did the same. Fix (small, matches the ext.rs pattern): - New EfsInode::now_u32() helper: SystemTime → seconds since UNIX_EPOCH, clamped into EFS's u32 field. Keeps the SystemTime dance in one place instead of scattering it across every write site. - create_file / create_symlink / create_directory (src/fs/efs.rs) each stamp the new inode's atime = mtime = ctime = now, and also bump the parent directory's mtime + ctime (its dir-contents changed). - write_blank_efs (production mkfs) stamps the root inode's three times identically. The zeroed test-helper mkfs at line ~4200 was already covered by a similar patch — kept for consistency. - delete_entry now always rewrites the parent inode (was only doing so for directory deletes) so a plain file delete records the parent's mtime/ctime bump too, not just a stale timestamp. - rename (fast-path + rename_via_remove_insert fallback) bumps the parent's mtime + ctime; the child's own inode times stay put (its data didn't change). - set_permissions / set_owner bump ctime — POSIX metadata-change semantics, atime/mtime left alone. EfsInode::empty() stays zeroed by design: it's what allocate_inode() looks for when hunting a free slot, and what delete_entry writes to a freed slot. Verified on-disk: rebuilt an 8 MiB SGI EFS image via `rb-cli new hd sgi-efs --from-dir`, hex-dumped the root inode + the created child inode, and both carry today's UTC time (was 0 before). The new unit test `create_file_stamps_timestamps_and_bumps_parent` exercises this end-to-end through the write path and reads the inodes back to assert times are >= a captured before-time. Note on the REPRO's tar oracle: `tar -tvzf` still shows 1969 for entries in the resulting archive, but that is a separate, wider bug in src/fs/tar_export.rs:349 (`h.set_mtime(0)` deliberately zeros every entry's mtime because FileEntry.modified is Option<String> — display-only, not a numeric u64 the tar Header can consume). Fixing it needs a numeric `modified_unix: Option<u64>` field on FileEntry populated by every fs driver; deliberately out of scope for this fix. Follow-up (also flagged in the original spec): preserving the HOST file's mtime through --from-dir / put requires an `mtime` field on CreateFileOptions (src/fs/filesystem.rs) and every fs driver honouring it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…families) Follow-up to fix(efs) a436be9. That commit fixed EFS's zero-mtime bug on disk but left two adjacent gaps: `tar -tvzf` of an rb-cli-generated image still showed "Dec 31 1969" (the tar exporter hard-coded mtime = 0), and a `--from-dir` copy stamped `now` on every imported file (the host mtime was never carried across). This commit closes both, generalises the fix to every Unix-family driver, and codifies the distinction between a genuinely new file (stamp now) and a copy/extract (preserve the source date). ## The rule `create_file` / `create_directory` / `create_symlink` use `options.unix_times` when set (dir_import from a host file, tar_import from an archive, Commander stage_copy from another image); otherwise stamp `now` (a genuinely new file — rb-cli `put` from stdin, GUI new- blank-file). Every code path that constructs a copy/extract now captures the source date and passes it through. ## Shared types (new src/fs/times.rs) - `UnixTimes { mtime/atime/ctime: Option<u64> }` — per-field so partial sources (tar has only mtime) don't fabricate the missing ones. - Helpers: `all()`, `mtime_only()`, `mtime_or_now()`, `atime_or_now()`, `ctime_or_now()`, `resolve_or_now()`, `now()`, `now_u32()`. - New `FileEntry.modified_unix: Option<u64>` — numeric twin of the display-string `modified`. What `tar_export` writes into headers and what a `stage_copy` passes into `CreateFileOptions.unix_times`. - New `CreateFileOptions.unix_times` + `CreateDirectoryOptions.unix_times`. - New `AttrOverrides.unix_times` (threaded through the shared importer). - `PreservedDates.unix_mtime` (new field on the Commander cross-image copy record), populated from `FileEntry.modified_unix`. ## Read side (populate `modified_unix`) Every Unix-flavoured driver now surfaces its on-disk mtime as a numeric `modified_unix` alongside the display string: - ext, jfs, squashfs, reiserfs — via `unix_entry_from_inode` helper - ufs, efs, xfs (new — was unread), minix — direct - affs, pfs3 (via `datestamp_to_unix`) — Amiga → Unix - sfs (Amiga 1978 epoch seconds) — direct + 252_460_800 shift - hfs, hfs+ (via new `mac_date_to_unix` helper in hfs_common) — Mac → Unix - xfs: `XfsDinodeCore.mtime` newly parsed from `di_mtime.tv_sec` FAT/exFAT/NTFS are read-only for `modified_unix` on this pass (they already carry their own dates; write-side conversion is out of scope per the "Unix filesystems only" rule the user asked for). ## Write side (honour `options.unix_times`) Every Unix create path now checks `options.unix_times` first and falls back to `now` only when the caller left it None: - efs — create_file / create_directory / create_symlink + mkfs root - ext — build_inode_bytes gains a `times: Option<UnixTimes>` parameter threaded through create_file / create_directory; ext_format passes None for mkfs (fresh mkfs stamp = now). - ufs — create_file / create_symlink / create_directory - xfs — init_file_inode + init_empty_shortform_dir + a new `stamp_v4_times()` helper writing di_atime/di_mtime/di_ctime pairs; do_create_file / do_create_directory thread `times` through - minix — create_file / create_symlink / create_directory - squashfs_edit — create_file / create_directory / create_symlink EFS entry_from_inode is also switched to use the `now_secs` local for parent-dir bumps (was reusing the child's `now`), so a preserved-time copy doesn't accidentally rewrite the parent's mtime to 2020. ## Import paths (capture and pass unix_times) - dir_import (host → image): `host_overrides` captures the host's stat mtime/atime/ctime and threads it through `AttrOverrides.unix_times`. Works on Windows too (Metadata::modified/accessed/created). - tar_import: `archived_overrides` captures the tar Header's mtime. - import_sink pushes `overrides.unix_times` into every create_file / create_symlink / create_directory `CreateFileOptions`. - Commander stage_copy: `PreservedDates.unix_mtime` is populated from the source entry's `modified_unix`; apply_edit converts it into `CreateFileOptions.unix_times` on replay. ## Export paths - tar_export: `base_header` writes `entry.modified_unix.unwrap_or(0)` into `Header::set_mtime` (was hard-coded `set_mtime(0)`). This is the fix for the "tar -tvzf shows 1969" oracle. - fork_export (image → host): a new `apply_host_mtime` helper calls `filetime::set_file_times` after the extract so the host file inherits the source's mtime. `filetime = "0.2"` promoted from transitive dep (already in both lockfiles) to a direct dep — in main and vintage manifests. Best-effort: filesystems that reject set_times (network mounts) leave the OS default, they don't fail the export. ## Regression tests (new tests/timestamp_preservation.rs) Six end-to-end tests covering every hop: - `dir_import_preserves_host_mtime` — host file dated 2018 → EFS → listed inode carries 2018. - `tar_import_preserves_archive_mtime` — tar Header dated 2020 → EFS → same. - `tar_export_carries_source_mtime` — 2020-dated inode → tar → Header reads 2020. - `host_to_image_to_tar_preserves_mtime_end_to_end` — full round-trip: host 2018 → dir_import → tar_export → tar entry reads 2018. - `image_to_host_extract_preserves_mtime` — 2020 inode → fork_export → host file's `stat.mtime` reads 2020. - `genuinely_new_file_stamps_now` — `unix_times=None` still stamps `now`. The "new" branch must not silently regress to "always preserve" (which would zero-mtime every blank GUI-created file). Plus one EFS unit test — `create_file_preserves_unix_times_when_supplied` — that pins the on-disk `atime/mtime/ctime` triple to exact preserved values, distinct from the pre-existing `create_file_stamps_timestamps_ and_bumps_parent` test which covers the `now` path. ## Vintage compat Verified rb-cli-vintage's error set is unchanged before/after (4 pre- existing HWND type-mismatches in src/os/windows.rs on this Windows dev box, all unrelated to this feature). The one new dep — `filetime` — was already a transitive dep via `tar` in both lockfiles (0.2.29 main, 0.2.27 vintage), so promoting it to a direct dep is free. ## Not in scope - FAT / exFAT / NTFS write-side unix_times honouring — they carry their own date scheme; a cross-fs copy INTO those still records `now`. Read side already exposes their dates via `modified_unix` where the driver had them; extending the write side to accept an mtime override needs DOS/FILETIME conversion helpers and is a follow-up. - HFS+ / HFS write-side unix_times — the Mac catalog dates are already set via `set_dates` after create, which the Commander stage_copy path uses via `PreservedDates.mac`. Cross-fs preservation into HFS+ (ext → HFS+) is a smaller follow-up if wanted. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Follow-up to b19256c. That commit added `unix_times` plumbing across CreateFileOptions and taught every Unix-family driver to honour it. Left in scope: FAT / exFAT / NTFS, HFS / HFS+ / MFS, ProDOS, HPFS, OS-9, QDOS, Human68k, ADFS, UCSD — every filesystem we can edit whose on-disk date field is *not* Unix seconds. Each of those drivers needs the same two things: an encoder that turns `u64` Unix seconds into its own on-disk shape (DOS-packed / FILETIME / Mac-epoch / ProDOS-packed / ...), and the inverse for read-side `FileEntry.modified_unix`. Putting one of each in every driver invites drift; centralising them here keeps the round-trip behaviour uniform and lets the driver edits be one-liners. ## Added New in src/fs/times.rs: - `unix_to_dos_datetime` / `dos_datetime_to_unix` — FAT + exFAT + Human68k share the same 16-bit date+time layout. exFAT is (date << 16 | time), wrapped by `unix_to_exfat_timestamp`. - `unix_to_filetime` / `filetime_to_unix` — NTFS 100-ns intervals since 1601-01-01. Sub-second precision is dropped, matching every other source in the import chain. - `unix_to_mac_epoch` / `mac_epoch_to_unix` — HFS / HFS+ / MFS Mac-epoch u32 seconds since 1904-01-01. Duplicates the delta from hfs_common so MFS can reach it without pulling in HFS internals. - `unix_to_prodos_datetime` / `prodos_datetime_to_unix` — ProDOS packed date + time, minute-granular, with ProDOS's own two-digit year convention (0..39 -> 2000..2039, 40..99 -> 1940..1999). - `unix_to_ucsd_date` / `ucsd_date_to_unix` — UCSD Pascal packed 16-bit date, day-granular. Years 0..99 map to 1900..1999. - `unix_to_adfs_time` / `adfs_time_to_unix` — ADFS RISC OS 40-bit centiseconds-since-1900, packed into (load_addr, exec_addr) with the 0xFFF filetype marker in the load address high bits. Encoder takes the filetype so a real load/exec pair doesn't get clobbered. - `unix_to_os9_dat` / `os9_dat_to_unix` — OS-9 FD.DAT (5 bytes: year-1900, month, day, hour, minute), plus `unix_to_os9_dcr` / `os9_dcr_to_unix` for the 3-byte creation-date FD.DCR. - `unix_to_qdos_date` / `qdos_date_to_unix` — Sinclair QL / QDOS u32 seconds since 1961-01-01. Every encoder saturates at its format's earliest representable date rather than underflowing. Every decoder returns `None` for the format's "no date set" sentinel (usually a literal zero on disk) and for values that would land before 1970 — so `FileEntry.modified_unix` never carries a suspicious 1969 / 1970-01-01 for what is on disk "actually unset". Two small civil-date helpers (`ymd_hms` / `secs_from_ymd_hms`) support the encoders without pulling `chrono` into the vintage build. Both use Howard Hinnant's civil_from_days / days_from_civil, matching the existing `format_unix_timestamp` in unix_common::inode. ## Tests 11 unit tests in `fs::times::tests`: round-trip through every encoder/decoder pair using a mid-range date; the zero-sentinel and pre-1970 rejection paths; ProDOS's split-year convention (year 20 -> 2020, year 90 -> 1990); ADFS filetype preservation across the load/ exec split; DOS clamping to 1980-01-01 on pre-DOS-epoch input. ## Not in this commit The driver edits themselves — FAT/exFAT/NTFS/... `create_file` still stamp `now`. Landing helpers first so each driver can be a one-liner: `unix_to_dos_datetime(options.unix_times ... .mtime_or_now())`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 2 of the mtime-preservation sweep (see 77dfc06 for the shared encoders). All three drivers share DOS's 16-bit packed date+time; the plumbing is symmetric. ## Write side - FAT `build_dir_entries` gains `mtime_secs: Option<u64>`. `create_file` and `create_directory` pass `options.unix_times.map(|t| t.mtime_or_now())` — copies / imports stamp the source's mtime, a genuinely-new file stamps `now`. The `rename` callsite passes `None` (preserves the existing behaviour: a rename stamps `now`, matching what every FAT tool does). - exFAT `build_entry_set` gains the same `mtime_secs` param. The pre- existing `current_exfat_timestamp` was a hardcoded 2024-01-01 — now delegates to `unix_to_exfat_timestamp(times::now())` so at least the fallback is honest wall-clock time rather than a fabricated year. `create_file` / `create_directory` thread `options.unix_times` through it; `rename` passes `None`. - Human68k directly stamps bytes 22..24 (time) and 24..26 (date) of the dirent with `unix_to_dos_datetime(mtime)` — the format is FAT- identical, so no wrapper needed. Both `create_file` and `create_directory` honour `options.unix_times`. ## Read side Every driver now populates `FileEntry.modified_unix` alongside the `modified` display string: - FAT: `dos_datetime_to_unix(date_val, time_val)` in `list_directory`. - exFAT: parses `LastModifiedTimestamp` (u32 LE at offset 12..16 of the FILE entry) and runs it through `exfat_timestamp_to_unix`. The parser previously ignored the timestamps entirely. - Human68k: `dos_datetime_to_unix(de.date, de.time)` in `list_directory`. ## Downstream `human68k_clone` (the defrag walker) now populates `CreateFileOptions.unix_times` and `CreateDirectoryOptions.unix_times` from each source entry's `modified_unix` — the module-header note about "file modification dates are not carried across" is corrected. Read- only attribute loss remains, since that's a separate flag with no write- side hook. Every returned `FileEntry` from `create_file` / `create_directory` also carries `modified_unix`, so a `list_directory` after the create matches what the create just wrote. ## Verified `cargo test --lib fs::fat`, `cargo test --lib fs::exfat`, and `cargo test --lib fs::human68k` all pass. `cargo clippy --all-targets -- -D warnings` clean. Cross-fs test coverage lands in phase 6. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 3 of the mtime-preservation sweep. NTFS stores dates as FILETIME (100-ns intervals since 1601-01-01); the shared encoders in src/fs/times.rs handle the conversion. ## Write side `create_file` / `create_directory` compute one `stamp` from `options.unix_times.map(|t| unix_to_filetime(t.mtime_or_now()))` and use it for both the `$STANDARD_INFORMATION` and `$FILE_NAME` timestamp quads. When `unix_times` is None (a genuinely new file), we fall back to `now_ntfs_timestamp()` — same behaviour as before this commit. Both returned entries carry `modified_unix` so `list_directory` after the create matches the value the create just wrote. ## Read side `parse_file_name_entry` reads the 8-byte `LastModifiedTime` FILETIME at offset 16..24 of the `$FILE_NAME` attribute and stamps it as `FileEntry.modified_unix` via `filetime_to_unix`. The parser previously ignored the 4-FILETIME timestamp block entirely — a Windows NTFS mounted through us reported empty `modified_unix` for every file, so a tar-export of an NTFS image showed 1969 for every entry (the same tar oracle the earlier EFS commit turned up). ## Verified `cargo test --lib fs::ntfs` (43 tests) passes; `cargo clippy --all-targets -- -D warnings` clean. Cross-fs regression tests in phase 6. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 4 of the mtime-preservation sweep. Every Mac-family driver we can write now records the source's mtime instead of stamping `now`, and returns it on read via `FileEntry.modified_unix` (already done) plus `mac_dates` (new on MFS). ## Write side - **HFS** — `build_file_record` and `build_dir_record_with_dates` gain an `mtime_secs: Option<u64>` param. `Some(secs)` stamps that Unix time (converted via `unix_to_mac_epoch`) into filCrDat + filMdDat / dirCrDat + dirMdDat; `None` stamps `hfs_now()`. `create_file` and `create_directory` pass `options.unix_times.map(|t| t.mtime_or_now())`; the after-create Commander `PreservedDates.mac` path still overrides both. - **HFS+** — same pattern with `build_file_record` and `build_folder_record_with_dates`. All four HFS+ date fields (createDate, contentModDate, attributeModDate, accessDate) share the one stamp. - **MFS** — the flat-format MFS directory entry gains real `create_date` + `modify_date` (both previously hardcoded to 0, so every file on an rb-cli-generated MFS volume displayed as 1904-01-01 in Finder). Now stamps `unix_to_mac_epoch(mtime_secs)` per the same rule. ## Read side - **HFS / HFS+** — `modified_unix` was already populated (see the earlier b19256c inventory). No change needed. - **MFS** — the flat parser at `list_directory` was skipping the date fields entirely; now populates `modified_unix`, `modified` (display string), and `mac_dates` from the entry's create/modify pair. ## Returned entries Every `create_file` / `create_directory` return path stamps `modified_unix` on the returned `FileEntry`, so a `list_directory` after the create matches what the create just wrote. HFS and MFS also populate `mac_dates` on the returned entry (HFS+ read path already does this on `list_directory`; the returned entry inherits it on the next listing). ## Refactor cleanup The plain `build_file_record`/`build_dir_record`/`build_folder_record` wrappers were replaced by the `_with_dates`-accepting flavour to keep clippy `-D dead-code` clean. Every callsite (production + tests) threads an explicit `None` for the "stamp now" default. ## Verified `cargo test --lib fs::hfs` (69 tests), `--lib fs::hfsplus` (95), `--lib fs::mfs` (28) all pass. `cargo clippy --all-targets -- -D warnings` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 5 of the mtime-preservation sweep — the long tail. Each driver's on-disk timestamp format is different; the shared encoders in src/fs/times.rs make the plumbing symmetric. ## ProDOS `build_file_entry_bytes` / `build_subdir_entry_bytes` / `build_subdir_header_bytes` gain an `mtime_secs: Option<u64>` param (ProDOS packed date+time, minute-granular, split-year convention). `create_file` / `create_directory` thread `options.unix_times` through each of them. Read side: `list_directory` now populates `modified_unix` alongside the display `modified` string, via `prodos_datetime_to_unix` — falls back to the creation date/time when the modification pair is zero. ## HPFS HPFS stores dates as u32 Unix seconds directly, so `de_meta_template` gains an `mtime_secs: Option<u64>` and just clamps to u32 when set; falls back to the reproducible `FIXED_TIME` sentinel otherwise (the formatter path forbids wall-clock reads). `create_entry` (the shared write path) grows an `mtime_secs` parameter; `create_file` / `create_directory` thread it through. Read side: `list_directory` populates `modified_unix = Some(de.write_date as u64)` when non-zero. ## UCSD Pascal `create_file` populates `entry.date` via `unix_to_ucsd_date(mtime)` when `options.unix_times` is set, else 0. Read side: `entry_to_file` now sets `modified_unix` via `ucsd_date_to_unix`. UCSD's year field is 0..99 -> 1900..1999, so a legitimate 1900-01-01 value would underflow `secs_from_ymd_hms` on decode. Added a `year < 1970` guard to `ucsd_date_to_unix` (and to `prodos_datetime_ to_unix` / `os9_dat_to_unix` / `os9_dcr_to_unix` for the same reason) — pre-1970 dates return None, matching the "no meaningful timestamp" semantics the tar oracle expects. ## OS-9 `FileDescriptor.parse` now reads the 5-byte FD.DAT (offset 3..8, last-modified year-month-day-hour-minute). `build_fd` gains `mtime_secs`; `Some` writes both FD.DAT and FD.DCR (creation date), `None` leaves them zero (matching pre-existing behaviour). `create_file` and `create_directory` thread `options.unix_times` into `build_fd`. `list_directory` uses `os9_dat_to_unix(&child_fd.dat)` to populate `modified_unix`. ## QDOS (Sinclair QL / QXL.WIN) `QdosDirEntry` gains a `update_date: u32` field (offset 0x34..0x38, seconds since 1961). `parse_dir_entry` reads it. `create_file` stamps both the directory entry's date word and the file's 64-byte in-payload header via `unix_to_qdos_date(mtime)`. `list_directory` populates `modified_unix` via `qdos_date_to_unix`. ## ADFS The load/exec pair carries either an absolute-file (load, exec) or a datestamp+filetype when load's high 12 bits are 0xFFF. `create_file` / `create_directory` compute both fields via `unix_to_adfs_time(mtime, 0xFFF)` (RISC OS filetype "Data") when `options.unix_times` is set, else keep the legacy `(0xFFFFFFFF, 0)` which RISC OS displays as "no date, Data". `list_directory` populates `modified_unix` via `adfs_time_to_unix` — returns None when the load-addr high bits aren't 0xFFF (meaning the entry carries a real load/exec address, not a timestamp). ## Verified Per-driver unit tests all pass: `fs::prodos` (46), `fs::hpfs` (11), `fs::ucsd` (13), `fs::os9` (10), `fs::qdos` (26), `fs::adfs` (24). `cargo clippy --all-targets -- -D warnings` clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 6 (final): teach AFFS / PFS3 / SFS to accept a Unix-source mtime as a fallback when the caller didn't supply amiga_dates, and lock the whole sweep behind end-to-end regression tests. ## Amiga bridging Before this commit a cross-fs copy INTO AFFS / PFS3 / SFS lost the date whenever the source wasn't Amiga: an HFS+ or ext file dropped onto an AFFS partition landed with the "no date set" sentinel. Fixed by making each driver honour `options.unix_times` as a fallback when `options.amiga_dates` is None — the explicit Amiga triple always wins, but a cross-fs copy from anywhere else still records the source date. - New `affs_common::unix_to_datestamp(secs: u64) -> (i32, i32, i32)` — the inverse of the existing `datestamp_to_unix`. Ticks are zero on the output; Unix seconds are 1-second granular and the tick field would round-trip to zero anyway. - AFFS `create_file` / `create_directory` compute `dates = options.amiga_dates.or_else(unix_to_datestamp(unix_times))` and pass it to `build_file_header_block` / `build_dir_block`. Return entries now carry `amiga_date` and `modified_unix`. - PFS3 same pattern across `create_file`, `create_directory`, `create_symlink`, `create_hardlink` (the four Amiga-attribute- accepting write paths). - SFS is different — its `datemodified` is a single u32 seconds since 1978-01-01 (no days/mins/ticks split). New `sfs_date_from_options` helper converts either amiga_dates or unix_times to that u32. The create paths thread it through `do_create_file`/`do_create_directory`, which stamp it into `build_object`'s date slot. Returned entries carry `modified_unix` derived from the SFS epoch shift. The Commander cross-image copy path (`edit_queue::apply_edit`) already threads `PreservedDates.unix_mtime` into `CreateFileOptions.unix_times` via b19256c — so an HFS→AFFS copy in the GUI now preserves the date end-to-end without any change to that layer. ## Regression tests 12 new tests appended to tests/timestamp_preservation.rs — one per driver Phase 2..5 touched. Each creates a blank volume, `create_file`s a byte with `unix_times = YEAR_2020`, `sync_metadata`s, re-lists the root, and asserts `modified_unix` on the entry is either exact (Mac epoch / NTFS FILETIME / QDOS / OS-9 / ADFS — all >= second-granular) or within 1s of the source (FAT / exFAT / Human68k share DOS's 2-second granularity). - `fat_preserves_mtime_across_create_and_list` - `exfat_preserves_mtime_across_create_and_list` - `ntfs_preserves_mtime_across_create_and_list` - `hfs_preserves_mtime_across_create_and_list` - `hfsplus_preserves_mtime_across_create_and_list` - `mfs_preserves_mtime_across_create_and_list` - `prodos_preserves_mtime_across_create_and_list` - `hpfs_preserves_mtime_across_create_and_list` - `os9_preserves_mtime_across_create_and_list` - `ucsd_preserves_mtime_across_create_and_list` (uses YEAR_1990 since UCSD's year field only reaches 1999) - `adfs_preserves_mtime_across_create_and_list` Human68k isn't in the list because its formatter needs a captured `Human68kFormatTemplate` (BPB + reserved region) that requires an existing source volume; the driver's own unit tests cover the round- trip already. All 17 tests in the file pass (6 from b19256c + this commit's 11 plus the "genuinely new file stamps now" invariant). ## Verified `cargo test --lib fs::affs` (25), `--lib fs::pfs3` (30), `--lib fs::sfs` (18), `--test timestamp_preservation` (17). `cargo clippy --all-targets -- -D warnings` clean. The rb-cli-vintage build's error set is unchanged: 4 pre-existing HWND type-mismatches in src/os/windows.rs on this Windows dev box, unrelated to this sweep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
R-038 filed the AFFS formatter as a High defect on 2026-08-12, on the strength of the first non-ours implementation to read our output: amitools' xdftool rejecting every volume with `Bitmap Block Count Mismatch: got=2 want=1`, identically on Windows, Linux and macOS. It does not reproduce. At HEAD xdftool accepts a freshly written volume at 1M, 2M, 3M, 4M, 8M, 16M and 32M, and accepts one that has had a `put` applied, listing the file back with its date. ## What actually happened The oracle was reading artifacts, and the artifacts were older than the fix. Their own `meta.json` says so: artifacts/windows/fs.affs git_sha 0563cb8 (2026-08-08) artifacts/{linux,macos} git_sha 2117976 (2026-08-08) git merge-base --is-ancestor a190182 0563cb8 -> NO git merge-base --is-ancestor a190182 2117976 -> NO a190182 ("fix(affs): size the bitmap, find the root block, zero header_key") landed 2026-08-10. All three artifacts predate it — which is also why the failure looked so convincingly cross-platform: every host had built from pre-fix code. ## What the bytes say Reading the root block settles the hypothesis the original report left open: Aug-8 artifact header_key=2048 (wrong) bm_pages=1 geometry needs 2 HEAD, fresh header_key=0 (correct) bm_pages=2 geometry needs 2 amitools' rule is in ADFSBitmap: it raises when the count computed from volume geometry differs from the bitmap blocks reachable through the root's bm_pages and extension chain, and the message reads `got=<computed> want=<reached>`. So `got=2 want=1` meant "geometry needs two, the volume supplies one" — R-008a's uncovered tail blocks seen from the outside. a190182 fixed both halves at once. The report's guess ("our root block declares a geometry inconsistent with the bitmap we actually wrote") was right, and had already been fixed two days before it was written down. ## The defect worth keeping is the process one An oracle verdict was attributed to current code when it was produced by old code, and nothing in the run path noticed. `rb-regress verify` now checks it: - `gitinfo::engine_changed_since` asks git whether `src/`, `Cargo.toml` or `Cargo.lock` moved between an artifact's producing commit and HEAD. It refuses a sha this clone does not have rather than answering "unchanged", so a shallow checkout cannot fake freshness. - `Record` carries `artifact_git_sha` and `artifact_stale`, so every recorded verdict can be traced to the code that wrote the bytes. - A stale artifact's line is marked `[STALE ARTIFACT]` inline — a full run scrolls, and a summary alone can be missed — and the run ends with a block naming each one and telling the reader to re-produce. One git query per distinct producing commit, not per artifact. Verified against the case that motivated it: `verify --filter affs` flagged all three stale artifacts; after re-producing on this host, Windows passes and the two remote-host artifacts stay correctly flagged until their owners re-produce. ## Not closed R-020 is untouched. amitools is a reimplementation, so its acceptance is not proof a real Amiga mounts the volume — that still needs the emulator, and is the next thing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ering R-020 asks whether a real Kickstart mounts the AFFS volumes we write, and has been stuck since 2026-08-07 on having no way to ask. amitools closed part of the gap (R-038) but is a reimplementation; its acceptance is not a mount. This is the harness the fs-uae oracle notes described as "not yet built": the config that attaches an artifact, the guest-side probe, and the host-side parser. ## Shape `oracles/fsuae/affs_mount.py <image>` DH0 bootable Workbench host directory, unpacked from the WB1.3 fixture DH1 the volume under test hardfile DH2 RESULTS host directory, the verdict channel No screen scraping: FS-UAE mounts a host directory as an Amiga volume, so the guest writes `RESULTS:info.txt` and the host reads it. `done.txt` is a sentinel; the host polls for it, then kills the emulator. The boot volume is *unpacked* from the fixture with `xdftool unpack` rather than mounted, because DH0 has to be writable to carry the probe and the fixture is sha256-pinned corpus. The fixture is only ever read. Exit codes separate the three outcomes that matter and are easy to conflate: 0 mounted, 1 refused (the R-020 symptom), 3 the guest never reached the sentinel — a harness result, not a verdict — and 2 setup error. ## Where it actually got to Verified from FS-UAE's own log: the ROM loads, all three drives attach at boot priority 0, `FS: mounted virtual unit DH0` / `DH2` appear, and the artifact is presented to Kickstart 3.1 as a hardfile. The guest end does not work yet. `S/Startup-Sequence` never executes, RESULTS: stays empty, and the script reports exit 3 rather than inventing a verdict — which is the behaviour that matters most here, since a silent harness failure reported as "refused" is precisely how a false R-038 gets filed. Two causes ruled out and recorded so they are not re-tried: - **Not the fixture's startup chain.** Appending the probe to the Workbench 1.3 Startup-Sequence never reached it — `Mount NEWCON:`, `Resident`, then Amiga Forever's `Execute S:AFShared-Startup`. The probe now replaces that file with five lines; the replacement is confirmed on disk. No change. - **Not missing commands.** c/Info, c/List and c/Echo are all in the unpacked tree. Diagnosing further needs someone to watch the emulator window once — whether it sits on the Kickstart insert-disk hand, gurus, or boots and fails at the redirect points somewhere different, and one look settles it. The generated config is left at `regression-tests/scratch/fsuae/probe.fs-uae` for exactly that. README § Status and the fs-uae entry in oracles.toml both say so. ## R-020 is unchanged Nothing here has yet put a real Kickstart's opinion on one of our volumes, so the finding stays open on its original evidence. The oracle currently answers "harness not ready", which is the honest answer and not a verdict. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
R-020 has been open since 2026-08-07 on "every AFFS volume we write is unmountable on a real Amiga", waiting on an emulator nobody had wired up. The emulator ran. AmigaOS mounts the volume: Unit Size Used Free Full Errs Status Name DH1: 2.0M 5 4089 0% 0 Read/Write rusty-backup Volumes available: rusty-backup [Mounted] Read/Write, under its own volume name, on an A1200 with Kickstart 3.1 — the same machine and ROM the original finding used. `List DH1:` reads the contents straight back: Directory "DH1:" on Friday 14-Aug-26 HELLO 24 ----rwed 1 file - 2 blocks used The fix was a190182 on 2026-08-10 (bitmap sized to the geometry, root header_key zeroed). This is the confirmation the entry said it needed, and the same commit already closed R-038. ## The control A pass needs a control as much as a failure does — an oracle that reports success for anything it is handed is worthless. 2 MB of os.urandom through the identical config yields no DH1: unit at all and `Can't examine "DH1:": device (or volume) is not mounted`. The harness discriminates. That control also found a real flaw in this script, which is why it was worth running: garbage produces *no DH1: line* rather than "Not a DOS disk", and the first cut reported that as exit 3, "harness failure". It is not — the guest plainly ran and reported the other two drives. `refused` and `absent` are both the guest declining the bytes and both now exit 1. Exit 3 is reserved for the sentinel never appearing, which is the only genuine harness failure. ## Why it looked broken for so long The previous commit landed this harness reporting exit 3, with the guest never executing S/Startup-Sequence, and said it needed someone to watch the screen. It did not. The bug was mine and on the host side: `Path.write_text` applies the platform newline translation on Windows, so the probe went out CRLF-terminated. AmigaDOS scripts are LF-only. The guest read Info >RESULTS:info.txt\r and tried to create a file whose name ends in a carriage return — which, since DH2 is a *host directory*, is an illegal Windows filename. Every redirect failed. FAILAT 21 then did exactly its job and stopped the script aborting, so the run booted, produced nothing, and looked precisely like an emulator that would not boot. `write_probe` now writes bytes with explicit LF, and rewrites the file every run rather than trusting the cached tree. The trap is documented in the script, the README and the oracle notes, because the failure mode points at the wrong component so convincingly. ## Registry fs-uae's fs.affs row moves from `plausible` to `proven`, strength `authoritative` — this is AmigaOS's own filesystem, not a reimplementation — with the check line wired so `rb-regress verify` can run it. ## Recorded, not fixed - The 2026-08-07 observation came through AmigaVision on the MiSTer; this one attaches a bare hardfile under FS-UAE. Both A1200/KS3.1, both asking Kickstart the same question, but not byte-identical setups. The MiSTer path has not been re-run. - The volume's creation date still reads as the 1978 epoch: create_blank_affs never stamps the root block datestamp. Cosmetic, does not affect mounting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ck them
Both findings were resolved in the two preceding commits but the entries
recorded the evidence without recording how to reproduce it. An entry nobody
can re-run is the same shape as an artifact nobody re-produced, which is what
R-038 turned out to be — so each closed entry now carries a copy-pasteable
block, and both were executed as written before committing.
R-038 (seconds, needs only `pip install amitools`):
for sz in 1M 2M 4M 8M 16M 32M; do
rb-cli new volume --size $sz affs "v$sz.hdf"
python regression-tests/oracles/amitools_affs.py "v$sz.hdf" || echo "FAIL $sz"
done
Verified: all six sizes pass.
R-020 (about a minute, mostly the guest booting; needs FS-UAE, a Kickstart 3.1
A1200 ROM and the fs.affs.workbench13.hd.hdf fixture):
rb-cli new volume --size 2M affs ours.hdf
rb-cli put ours.hdf hello.txt /HELLO
python regression-tests/oracles/fsuae/affs_mount.py ours.hdf
Verified: exit 0, `Read/Write rusty-backup`. The negative control is spelled
out beside it — 2 MB of os.urandom, exit 1 — because a pass from an oracle
that has never been shown to fail is not evidence.
## The handoff doc was the actual risk
`RESUME-regression-fixes.md` still read "21 findings fixed, 14 open" from
2026-08-09 and still listed R-020 under BLOCKED, NOT FORGOTTEN as needing an
emulator that now exists. That is precisely the drift that produced R-038: a
document confidently describing a state that had already changed underneath it.
Rather than restate a count that will go stale again, the STATE section now
says outright that its numbers are dated, gives the 2026-08-14 tally, and
points at the table in Regression_Bugs.md as the live source. The BLOCKED entry
is struck through and notes the distinction worth keeping — this unblocked one
emulator oracle, not the 61 others that remain skip-manual.
The "do not conflate the three AFFS bugs" note is kept even though all three
are closed, because it is still the right way to read those entries.
## Still open after this
R-039 (EFS free list) and R-011 (blocked on fixtures), plus R-019 accepted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
R-020 and R-038 were closed in Regression_Bugs.md, but four other documents still described them as open — and two of those are live handoffs someone would act on. Leaving them is the same failure that produced R-038: a document confidently describing a state that changed underneath it. ## regression-fix-prompt.md — the one that mattered A handoff whose instructions are "paste a tranche into a fresh session", so a stale row here becomes someone's afternoon. Its header claimed 14 open as of 2026-08-09; two are. **Tranche C — the eight where "the symptom is known and the cause is not", described as the state that does not clear on its own — has cleared.** Six fixed, two not defects, R-011 the only row still live. Every struck row now records what the investigation actually turned on, which is the column's stated purpose, and in two cases that is more interesting than the fix: - R-020's hypothesis was right about `header_key` and **wrong about the fix needing to land in `affs_fsck`** — fsck never inspected that field at all. - R-030 was neither of its two predicted causes (OFS-vs-FFS, 1.3-era layout): the root block was located from the end of the file rather than the partition. Same commit as R-020, different cause. The "Blocked on something other than effort" entry is struck: the route it suggested — teach `verify` to drive FS-UAE — is the one that worked, and no MiSTer time was needed. ## The other three - **missing_features_from_regression.md** cited R-020 twice as the standing example of "cannot be verified from here". For F-009 (SFS multi-leaf btree) that premise is now false, and the entry says what the SFS version of the harness needs: the same DH1:-plus-host-directory shape, plus the SFS handler staged into the guest's `L:`, since Kickstart has none in ROM. For F-006 the SGI oracle is likewise reframed as a harness nobody has written rather than a thing that cannot be done — `iris` already exists. - **GAPS.md** said emulator oracles cannot be invoked at all. Two now can (`iris`, FS-UAE), both via a host directory the guest writes into. The ~60 others are still `skip-manual`, and the note records why the AFFS one stays on-demand: it wants a GUI session and a minute per run, so making it a first-class `verify` row is a policy call about whether the runner may open a window, not a missing feature. - **COMMAND-COVERAGE.md** justified testing against third-party AFFS volumes on the grounds that no Amiga would mount ours. The premise is gone; the conclusion survives it and is stated more strongly, because third-party volumes are what caught R-030 and R-013, and testing an editor only against its own writer is the circularity that let R-020 stand for a week. ## Three cross-references inside Regression_Bugs.md itself R-008a's fix note said it did "not address R-020" — it did, via the `header_key` half. R-030 wondered whether the two shared a fix — same commit, different causes. R-038's closing paragraph said R-020 "remains open" — true when written, closed hours later by AmigaOS itself. All three are corrected in place rather than rewritten, with the original claim left standing above the correction. Being wrong about which fix closes which finding is the recurring theme of this file and worth being able to see. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
IRIX 6.5's own fsck reported BAD FREE LIST on every EFS volume we wrote.
Phases 1 through 4 always passed: it walked our blocks, pathnames,
connectivity and reference counts without complaint. Only phase 5 failed.
The bitmap packs blocks LSB-first within each byte. We had it MSB-first — in
the reader as well as the writer.
That symmetry is the whole reason this survived. Our formatter and our fsck
agreed with each other perfectly, because both were wrong in the same
direction, and every test fixture we built agreed with them for the same
reason. A self-consistent convention cannot be caught by self-consistent
fixtures. It took the vendor's own tool to see it.
## How the order was established, rather than guessed
mkfs_efs was run on the same scratch device, and its volume pulled back to the
host and decoded both ways. LSB-first, it decomposes into exactly the geometry
its own superblock declares:
run 0..834 = boot(0) + superblock(1) + bitmap(2..33 = 32 blocks)
+ inodes(34..812 = cgisize 779) + data(813..834 = the 22 used)
0..33 is precisely firstcg=34, and each of the six cylinder groups — 21872,
43710, 65548, 87386, 109224 — carries exactly cgisize=779 allocated blocks.
MSB-first, the same bytes give 16 ragged runs on no boundary at all: 776-block
groups, fragments at 22653..22655 and 43704..43705.
The tail says it independently. bmsize is 16383 bytes = 131064 bits covering
fs_size 131062 blocks, so the last two bits are spare and must read in-use.
IRIX writes 0x3F there — bits 6 and 7 clear counting from the LSB. We wrote
0xFC, the same statement backwards.
## The change
24 sites across efs.rs, efs_fsck.rs and efs_resize.rs. One test assertion was
hand-written as a literal mask (0b1000_0000 for block 0) and needed the same
correction.
`bitmap_bit_order_is_lsb_first_per_irix_mkfs` pins the convention against
numbers taken off the mkfs_efs volume rather than derived from our own code —
the only kind of test that could have caught this.
## Verified end to end, not just against fsck
** Phase 5 - Check Free List
1 files 1 blocks 126935 free <- no BAD FREE LIST
then IRIX mounted it (`df -k` reports a 63468 KB efs volume), wrote a file and
a subdirectory into it, unmounted, and fsck came back clean at 4 files 3
blocks. Reading that volume back with rb-cli lists both entries and returns
the file contents verbatim. Full suite: 3217 passed, 0 failed.
## This was not only a write bug
The reader used the same inverted order, so free-space accounting on any real
IRIX EFS disk was wrong, and the allocator could have handed out blocks IRIX
considered in use. Nothing shipped had exercised that path against a real
volume, but the exposure was real.
## Also settled
The entry's second observation — fsck showing the volume name as `rusty-`,
truncated from `rusty-backup` — is correct behaviour. `fname` in the EFS
superblock is a 6-byte field.
## Oracle
regression-tests/oracles/iris/README.md records the run recipe and five things
that cost time, including one that reads as a filesystem defect and is not:
size the volume to s0 (131064 sectors on the 64 MB scratch), not to the whole
device, or fsck reports "filesystem larger than device" because the SGI volume
header occupies the first 8 sectors. fs.efs moves to proven/authoritative.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`rb-regress validate` refused to parse data/oracles.toml: registry will not parse: TOML parse error at line 360, column 63 invalid multiline basic string The fs-uae notes describe the R-020 bug — a probe written with CRLF, so the Amiga guest read a redirect whose filename ended in a carriage return. Writing that sentence, the patch script turned an escaped `\r` into an actual CR byte and embedded it mid-line in a TOML multiline string, which TOML rejects. Documenting a carriage-return bug by committing a carriage-return bug is funnier than it is defensible. The sentence now says what happened in prose instead of quoting a control character. It went unnoticed for three commits because nothing re-ran `validate` after editing the registry. Worth remembering: the file is data the runner parses, not prose, and every edit to it needs the parse checked. 20 manifest(s), 292 case(s), 0 problem(s) registry: 123 format(s), 44 oracle(s), 6 host(s) 4 known failure(s) on the bug list Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.