diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-03 15:39:43 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-03 15:39:43 -0300 |
| commit | d89c0b532df23ed5b48495f83725893d5d82042b (patch) | |
| tree | a4a2eff66096fb90414bf5cd84fe016c930e0d68 /src/host.zig | |
| parent | 3d8d4425c969d3df21915c9c14b144460a1c0086 (diff) | |
| download | pardes-d89c0b532df23ed5b48495f83725893d5d82042b.tar.gz pardes-d89c0b532df23ed5b48495f83725893d5d82042b.zip | |
errors: a save that could not happen, and two panics on an ordinary click
A review of what this program does when the environment says no. The finding
that reframes it: there were almost NO panics on ordinary paths — the rule
already held — but there was a great deal of silence, and one case worse than
any panic.
SILENT DATA LOSS ON SAVE. `saveFile` marked the pane saved the moment it
QUEUED the effect, before any host had tried; `host_io.writeFd` returned void,
so a short or failed write was indistinguishable from a complete one; and
`writeFileBytes` returned true regardless. A save to a read-only file, or into
a directory removed under the pane, therefore cleared the tag's ` *` and posted
nothing — and `Del` makes no dirty check, so the next click threw the edits
away with the screen saying they were safe. On a full disk it was worse: the
file is already `O_TRUNC`'d when `write` fails, so the message row said `saved`
over a file that had just been emptied.
Now: `writeFd` reports, `writeFileBytes` returns WHY (`PermissionDenied`,
`NoSpaceLeft`, `ReadOnlyFilesystem`, …) including a failed `close`, which is
where write-back filesystems report at all; the core marks the pane saved
around `perform` rather than at emit, which is also where the bytes are read;
and a host that could not write calls `Pardes.saveFailed`, which puts the
reason on the message row and takes the clean mark back. That is a CALL and
not a return value because host.zig enforces, at comptime, that a `push_`
method reaching every host in a fan-out cannot have one answer — the first
attempt at this changed the signature and the compiler was right to refuse it.
TWO PANICS ON AN ORDINARY KEYSTROKE, in look.zig's number scans. `v = v * 10 +
d` over caller-supplied digits, reached from `parsePathLine` and the `@pN` scan
— which every Look, every right-click and every n/N motion runs on whatever
word is under the pointer. A hash in a log, a CSV column, any output shaped
`foo:99999999999999999999`, and the editor died with "integer overflow". Both
saturate now, the same way acmefs.zig's address parser already did; a saturated
line is refused by `file_pane.open`'s `line <= total` and a saturated pane id
by `focusPaneLine`'s `id < MAX_PANES`, so nothing addressable changes.
A BOOT FILE THAT WILL NOT OPEN joins the missing-name case in the `+Errors`
pane instead of taking the launch down: `pardes /root` resolves as a `.file`,
could not be read, and left `error: PermissionDenied` and a return trace.
`look.readFile` now says which errno it was, so the pane can say "permission
denied" rather than a word from the source code.
The tag-marker test drained no effects and passed anyway, which is exactly the
defect; it drains now.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf
Diffstat (limited to 'src/host.zig')
| -rw-r--r-- | src/host.zig | 24 |
1 files changed, 20 insertions, 4 deletions
diff --git a/src/host.zig b/src/host.zig index d5f5c983..2c6bd208 100644 --- a/src/host.zig +++ b/src/host.zig @@ -116,6 +116,17 @@ pub const Host = struct { /// `pane` travels with the bytes only so a host that posts a "saved" /// message row can name the right pane; the core already resolved the /// path and the content, so save_file and save_text both land here. + /// + /// A HOST THAT COULD NOT WRITE MUST CALL `Pardes.saveFailed`, and the + /// reason it is a call rather than a return value is the rule twenty + /// lines below: a `push_` reaches every host in a fan-out, so there is + /// no single answer to give back. The core marks the pane saved + /// optimistically around this call and `saveFailed` takes it back, so a + /// write that could not happen — a read-only file, a directory removed + /// under the pane, a full disk — leaves the ` *` in the tag where it + /// was. Until that existed the pane came clean on a save that never + /// happened, and `Del` makes no dirty check: the edits were one click + /// from gone with the screen saying they were safe. push_write_file: ?*const fn (ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void = null, /// The session dump. Separate because the host also chooses WHERE it /// goes (dump.outPath is libc-bound; the freestanding core cannot). @@ -188,21 +199,26 @@ pub const Fallback = struct { f.link.deinit(f.gpa); } - pub fn writeFile(f: *Fallback, path: []const u8, bytes: []const u8) void { - const copy = f.gpa.dupe(u8, bytes) catch return; + /// True when the bytes are in the map. The core turns a false into the same + /// `saveFailed` a real host reports, so a virtual filesystem that could not + /// allocate does not leave a pane looking saved either. + pub fn writeFile(f: *Fallback, path: []const u8, bytes: []const u8) bool { + const copy = f.gpa.dupe(u8, bytes) catch return false; if (f.files.getEntry(path)) |e| { f.gpa.free(e.value_ptr.*); e.value_ptr.* = copy; - return; + return true; } const key = f.gpa.dupe(u8, path) catch { f.gpa.free(copy); - return; + return false; }; f.files.put(f.gpa, key, copy) catch { f.gpa.free(key); f.gpa.free(copy); + return false; }; + return true; } /// What this path holds now: the session's own write, else the embedded |
