From 27f8f6e0a5bb2751e1642db1e7e68b2cf72e50b0 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 21:13:57 -0300 Subject: Forwarding to a running pardes says why in words: a name with a newline is "a file name is one line", never NotOneLine The forwarding refusal printed the error's name for anything the session itself did not answer: NotOneLine, NotAFileName, NoWorkingDirectory. Those are now sentences, a dial failure is said as the message row says one, and any other error is its name's words. Co-Authored-By: Claude Opus 5.5 --- src/main.zig | 27 ++++++++++++++++++++++++++- test/fs.py | 8 ++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main.zig b/src/main.zig index 101a5be3..6c5eaba1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -98,6 +98,30 @@ const help_text = \\ ; +/// A forwarding failure in words, never an error's name: the ones this +/// file raises said outright, any other as its name's words (`NotFound`, +/// `not found`), a dial's as Messages says it. +fn forwardWords(err: anyerror, buf: []u8) []const u8 { + return switch (err) { + error.NotOneLine => "a file name is one line, and this one holds a newline", + error.NotAFileName => "names a directory, not a file", + error.NoWorkingDirectory => "this shell's working directory is gone", + else => if (pardes.Messages.dialReason(err)) |why| why else words: { + var n: usize = 0; + for (@errorName(err), 0..) |c, i| { + if (n + 2 > buf.len) break; + if (std.ascii.isUpper(c) and i > 0) { + buf[n] = ' '; + n += 1; + } + buf[n] = std.ascii.toLower(c); + n += 1; + } + break :words buf[0..n]; + }, + }; +} + /// `--wait` after a forwarded look, as plan9port's E reads acme's log: the /// pane `path` landed in (the one it already had, if open), then /log /// followed on one connection until that pane's `del` -- exit 0 -- or the @@ -401,7 +425,8 @@ fn nativeMain(init: std.process.Init) !void { ninep_io.Client.probe(arena, parent.dial) catch break :forwarding; const Refuse = struct { fn with(io: std.Io, what: []const u8, err: anyerror) noreturn { - const why = if (err == error.Remote) ninep_io.Client.lastSaid() else @errorName(err); + var words_buf: [128]u8 = undefined; + const why = if (err == error.Remote) ninep_io.Client.lastSaid() else forwardWords(err, &words_buf); var buf: [8192]u8 = undefined; const text = std.fmt.bufPrint(&buf, "pardes: {s}: {s}\n", .{ what[0..@min(what.len, 4096)], why }) catch "pardes: refused\n"; std.Io.File.stderr().writeStreamingAll(io, text) catch {}; diff --git a/test/fs.py b/test/fs.py index 9392ffb7..eba5a5fa 100644 --- a/test/fs.py +++ b/test/fs.py @@ -1391,6 +1391,14 @@ def test(binary, quic=False): assert refused.returncode == 1, refused assert b'bad character in file name' in refused.stderr, refused.stderr assert serials() == before, (serials(), before) + # A name with a newline is said in words, not an error's name. + for word, said in (('two\nlines.txt', b'a file name is one line'),): + refused = subprocess.run([binary, word], cwd=root, env=env, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=10) + assert refused.returncode == 1, refused + assert said in refused.stderr, refused.stderr + assert b'NotOneLine' not in refused.stderr and b'NoSuchPane' not in refused.stderr + assert serials() == before, (serials(), before) orphan = launch('--wait') assert orphan.poll() is None assert orphan.wait(timeout=5) == 1 -- cgit v1.3