Fix round 1 for Task 3, addressing task-3-review.md's I1-I3 (reproduced
under dash) plus M3 and M10 from the same review.
- I1: WRITE_SCRIPT read the target's hash through `sha256sum | cut … ||
exit 1`. POSIX sh has no pipefail, so that `|| exit 1` tested only
cut's exit status — an unreadable target (EACCES, EIO) left $actual
empty, which never equals $expect, so the script silently reported
exit 3 (conflict) instead of a real error. The user got a misleading
"changed on disk" banner whose "Overwrite on save" could never
succeed, since the next poll hit the same read error. Fixed by
reading the hash from a plain command substitution
(`actual=$(sha256sum -- "$target") || exit 1`) and splitting out the
hash field in shell instead of piping into `cut`.
- I2 (+ M3): a failed `cp` into the staged file (ENOSPC, quota, EFBIG,
EIO) left a partial `.<name>.triple-c-<pid>` behind in the user's
own directory — the EXIT trap only ever removed $tmp. Fixed by
creating the staged file with `mktemp` (M3: exclusive, unpredictable
name, so it can't be planted or follow an existing symlink) and
adding it to the trap as soon as it's assigned
(`trap 'rm -f -- "$tmp" ${staged:+"$staged"}' EXIT`), so any later
failure — cp, chmod, mv, or a signal — cleans it up too.
- I3 (controller ruling): the script only ever checked `[ -w "$dir" ]`,
so a 0444 file (or one owned by another uid) was silently replaced
via rename, defeating the file's own write protection even though
spec §5 step 3 reads that way literally. Added `[ -w "$target" ]`
before the branch; a non-writable target is refused with "The file
is read-only for the container user." on stderr and a distinct exit
code (5, `EXIT_READ_ONLY`) that `classify_write` maps to that same
message rather than falling into the generic clipped-stderr arm.
- M10: added six `#[cfg(unix)]` tests that run WRITE_SCRIPT for real
via `sh -c` against a temp directory on the host (not just needle
matches against the script text) — clean save, stale-base conflict,
gone target, unreadable target (I1), read-only target (I3), and a
failed stage leaving no partial file behind (I2). The unreadable/
read-only tests self-skip with a message if permission bits turn out
not to block root, rather than false-failing under a root test
runner.
Verified: `cargo test --offline file_viewer` — 19/19 passing, pristine
(up from 12; 6 new host-execution tests plus 1 for the new exit-5
classify_write arm). `cargo clippy --offline` (and `--tests`) — no
warnings in file_viewer::write; the 28 warnings clippy reports are
all pre-existing, in unrelated files.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>