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
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", |