a73x

118be829

fix: refuseFile's exit code names whose fault it was, not the file's own bad line

a73x   2026-08-28 19:53

Commit message
fix: refuseFile's exit code names whose fault it was, not the file's own bad line

src/cli/mux_main.zig
Old New
@@ -631,15 +631,15 @@ fn printOut(fd: std.posix.fd_t, comptime fmt: []const u8, args: anytype) void {
631 /// all is not their spelling (1). 631 /// all is not their spelling (1).
632 fn refuseFile(arena: std.mem.Allocator, who: []const u8, path: []const u8, err: anyerror) u8 { 632 fn refuseFile(arena: std.mem.Allocator, who: []const u8, path: []const u8, err: anyerror) u8 {
633 std.debug.print("{s}: {s}: {s}\n", .{ who, path, hosts.reason(err) }); 633 std.debug.print("{s}: {s}: {s}\n", .{ who, path, hosts.reason(err) });
634 const lines = wall.loadLines(arena, path) catch return 1; 634 // Whose fault it was is decided by the error, never by re-parsing the
635 var found = false; 635 // file: an `OutOfMemory` on a file that also holds one stale line used
636 // to print "OutOfMemory" and then exit 2 with a list of lines to fix.
637 if (!hosts.isParse(err)) return 1;
638 const lines = wall.loadLines(arena, path) catch return 2;
636 for (lines.items) |l| { 639 for (lines.items) |l| {
637 _ = hosts.parse(l) catch { 640 _ = hosts.parse(l) catch std.debug.print(" {s}\n", .{l});
638 std.debug.print(" {s}\n", .{l});
639 found = true;
640 };
641 } 641 }
642 return if (found) 2 else 1; 642 return 2;
643 } 643 }
644 644
645 /// Sessions in a `sessions_reply` body: one name per line, however the 645 /// Sessions in a `sessions_reply` body: one name per line, however the
@@ -927,6 +927,26 @@ test "hosts list: a line the grammar refuses is named, not a refusal of the list
927 try std.testing.expect(std.mem.indexOf(u8, text, "\t[unreachable]") != null); 927 try std.testing.expect(std.mem.indexOf(u8, text, "\t[unreachable]") != null);
928 } 928 }
929 929
930 test "refuseFile: the exit code names whose fault it was, and a bad line in the file does not change it" {
931 // 2 is "your spelling, here are the lines"; 1 is "this file could not be
932 // read". Re-parsing the file to choose between them let an allocator
933 // failure on a file that ALSO holds one stale line print "OutOfMemory"
934 // and then exit 2 with a repair list for a fault the user cannot fix.
935 const alloc = std.testing.allocator;
936 var tmp = try TmpDir.make();
937 defer tmp.cleanup();
938 var buf: [512]u8 = undefined;
939 const path = try std.fmt.bufPrint(&buf, "{s}/hosts", .{tmp.path()});
940 try wall.saveBytes(path, "box#old\nbox\n");
941
942 var arena_state = std.heap.ArenaAllocator.init(alloc);
943 defer arena_state.deinit();
944 const arena = arena_state.allocator();
945 try std.testing.expectEqual(@as(u8, 2), refuseFile(arena, "t", path, error.HasSession));
946 try std.testing.expectEqual(@as(u8, 1), refuseFile(arena, "t", path, error.OutOfMemory));
947 try std.testing.expectEqual(@as(u8, 1), refuseFile(arena, "t", path, error.AccessDenied));
948 }
949
930 test "hosts list: a line longer than the row buffer is still shown, because rm matches what was shown" { 950 test "hosts list: a line longer than the row buffer is still shown, because rm matches what was shown" {
931 // `wall.loadLines` takes lines up to a MiB and `mux hosts rm` matches 951 // `wall.loadLines` takes lines up to a MiB and `mux hosts rm` matches
932 // byte for byte, so a row this command drops is a line the user can 952 // byte for byte, so a row this command drops is a line the user can
src/hosts.zig
Old New
@@ -33,6 +33,15 @@ pub fn parse(line: []const u8) ParseError!Spec {
33 return .{ .host = line }; 33 return .{ .host = line };
34 } 34 }
35 35
36 /// Whether the grammar refused a line — the user's spelling to fix — as
37 /// opposed to the file not being readable at all, which is not.
38 pub fn isParse(err: anyerror) bool {
39 inline for (@typeInfo(ParseError).error_set.?) |e| {
40 if (err == @field(anyerror, e.name)) return true;
41 }
42 return false;
43 }
44
36 pub fn reason(err: anyerror) []const u8 { 45 pub fn reason(err: anyerror) []const u8 {
37 return switch (err) { 46 return switch (err) {
38 error.HasSession => "names a session after '#': the wall lists daemons and shows every session they have", 47 error.HasSession => "names a session after '#': the wall lists daemons and shows every session they have",