summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 21:29:39 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit20685b048fcb307840451471679339c9b9704b23 (patch)
tree8a803f1551ac117c41d271ff972c26a259500569 /src
parent237071bd4865f0220c35c8c37895c8dba48aed84 (diff)
downloadpardes-20685b048fcb307840451471679339c9b9704b23.tar.gz
pardes-20685b048fcb307840451471679339c9b9704b23.zip
name takes any byte a file name can hold, controls, edge blanks and bytes not UTF-8 included, so what name reads writes back as it was
A pane opened on a file whose name held a tab, a control byte or bytes not UTF-8 read that name from its name file, and the same bytes written back were refused. Only what no file name holds is refused now: a second line and a NUL. The ctl word name still refuses a second blank after the word, as a line's words are split at blanks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ninep/ctl.zig5
-rw-r--r--src/ninep/pane.zig38
-rw-r--r--src/ninep/tree.zig4
3 files changed, 25 insertions, 22 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index fc0558f5..5a199f55 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -947,7 +947,10 @@ fn acmeCtl(p: *Pardes, req: Req, serial: u32, line: []const u8, apply: bool) Acm
// A word written to one of the pane's own files, as acme's is to its.
const Into = struct { file: File, data: []const u8 };
// `name` takes all after its one blank, as the name file takes what is
- // written: a blank at its start is refused there, not trimmed here.
+ // written. A second blank after the word is refused, not taken as the
+ // name's first byte: a line's words are split at blanks.
+ if (std.mem.eql(u8, word, "name") and rest.len > 0 and line[word.len + 1] == ' ')
+ return .{ .refused = tree.failText(req.tag, E.INVAL, "bad character in file name: a blank at its start") };
const into: ?Into = if (std.mem.eql(u8, word, "name") and rest.len > 0)
.{ .file = .name, .data = line[word.len + 1 ..] }
else if (std.mem.eql(u8, line, "clean"))
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 3b5f2e6c..852a9ec0 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -898,17 +898,12 @@ const e_name_char = "bad character in file name";
/// Why `name` is not one file name, with the reason, or null: a newline, a
/// control byte, DEL or a C1 control (U+0080-U+009F), a blank at either
/// end, or bytes that are not UTF-8.
+/// What no file name can hold, said: a newline (a name is one line) and
+/// a NUL. Any other byte a file system takes is taken, controls and bytes
+/// not UTF-8 included, so a name read from `name` writes back as it was.
fn nameFault(name: []const u8) ?[]const u8 {
if (std.mem.indexOfScalar(u8, name, '\n') != null) return e_name_char ++ ": a newline (a name is one line)";
- for (name) |c| if (c < ' ' or c == 0x7f) return e_name_char ++ ": a control character";
- if (!std.unicode.utf8ValidateSlice(name)) return e_name_char ++ ": not UTF-8";
- if (std.mem.indexOf(u8, name, "\xc2") != null) {
- var i: usize = 0;
- while (std.mem.indexOfScalarPos(u8, name, i, 0xC2)) |at| : (i = at + 1)
- if (at + 1 < name.len and name[at + 1] <= 0x9F) return e_name_char ++ ": a control character";
- }
- if (name[0] == ' ') return e_name_char ++ ": a blank at its start";
- if (name[name.len - 1] == ' ') return e_name_char ++ ": a blank at its end";
+ if (std.mem.indexOfScalar(u8, name, 0) != null) return e_name_char ++ ": a NUL";
// No file system takes a longer one (NAME_MAX): a Save would only fail.
var parts = std.mem.splitScalar(u8, name, '/');
while (parts.next()) |part| if (part.len > 255) return "invalid file name: a component over 255 bytes";
@@ -947,7 +942,7 @@ pub fn nameBuffer(p: *Pardes, id: usize, full: []const u8, preserve_tag: bool) !
const f = fileOf(pane) orelse return error.NotFile;
if (!std.fs.path.isAbsolute(full) or full.len == 0 or full.len >= 4096)
return error.InvalidName;
- for (full) |c| if (c < ' ') return error.InvalidName;
+ if (std.mem.indexOfAny(u8, full, "\x00\n") != null) return error.InvalidName;
if (std.mem.eql(u8, f.path, full)) return;
const copy = try p.gpa.dupe(u8, full);
p.gpa.free(f.path);
@@ -1656,24 +1651,29 @@ test "name reads the file name and writing it promotes a scratch without touchin
const target = try fs.readFile(gpa, path);
defer gpa.free(target);
try testing.expectEqualStrings("existing target\n", target);
- for ([_][]const u8{ "", "\n", "bad\x01name\n" }) |bad|
+ for ([_][]const u8{ "", "\n" }) |bad|
try testing.expectEqual(E.INVAL, wr(p, name, bad).errno());
- // One name, in acme's words and why: a blank at either end (not cut
- // off), a second line, a control byte, DEL, a C1 control, not UTF-8.
+ // One name, and what no file name holds: a second line, a NUL.
for ([_][2][]const u8{
- .{ "trailing.zig \n", "a blank at its end" },
- .{ " leading.zig\n", "a blank at its start" },
- .{ "tab\t.zig\n", "a control character" },
.{ "two\nlines\n", "a newline" },
- .{ "del\x7f.zig\n", "a control character" },
- .{ "c1\xc2\x85.zig\n", "a control character" },
- .{ "bad\xff.zig\n", "not UTF-8" },
+ .{ "nul\x00.zig\n", "a NUL" },
}) |c| {
const refused = wr(p, name, c[0]);
try testing.expectEqual(E.INVAL, refused.errno());
try testing.expectStringStartsWith(refused.reply.ename, "bad character in file name: ");
try testing.expect(std.mem.indexOf(u8, refused.reply.ename, c[1]) != null);
}
+ // Any other byte a file name holds is taken, and reads back as written:
+ // blanks at its ends, a tab, a control byte, DEL, C1, bytes not UTF-8.
+ for ([_][]const u8{ " lead.zig ", "tab\t.zig", "c\x01.zig", "del\x7f.zig", "c1\xc2\x85.zig", "bad\xff.zig" }) |odd| {
+ try testing.expectEqual(Status.ok, wr(p, name, try std.fmt.bufPrint(&line, "{s}\n", .{odd})).reply.status);
+ const read_back = std.mem.trimEnd(u8, rd(p, name, 0, 4096).bytes, "\n");
+ try testing.expect(std.mem.endsWith(u8, read_back, odd));
+ var again: [4200]u8 = undefined;
+ @memcpy(again[0..read_back.len], read_back);
+ try testing.expectEqual(Status.ok, wr(p, name, again[0..read_back.len]).reply.status);
+ try testing.expectEqualStrings(again[0..read_back.len], pane.file.?.path);
+ }
// A name that is one line and UTF-8 is taken, é and all.
try testing.expectEqual(Status.ok, wr(p, name, "caf\xc3\xa9.zig\n").reply.status);
try testing.expect(std.mem.endsWith(u8, pane.file.?.path, "/caf\xc3\xa9.zig"));
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index a7f3f2a4..be03e8f0 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -1802,9 +1802,9 @@ test "a write with no newline, whole in its Twrite, runs then and fails the writ
// And a name: a bad one fails its write, not the close after it.
const name = Node.of(serial, .name);
const n = call(p, .{ .tag = 10, .op = .open, .node = name, .omode = 1 }).reply.handle;
- const bad = call(p, .{ .tag = 11, .op = .write, .node = name, .handle = n, .data = "delete " });
+ const bad = call(p, .{ .tag = 11, .op = .write, .node = name, .handle = n, .data = "del\x00ete" });
try testing.expectEqual(Status.err, bad.reply.status);
- try testing.expect(std.mem.indexOf(u8, bad.reply.ename, "a blank at its end") != null);
+ try testing.expect(std.mem.indexOf(u8, bad.reply.ename, "a NUL") != null);
try testing.expectEqual(Status.ok, call(p, .{ .tag = 12, .op = .release, .node = name, .handle = n, .opened = true }).reply.status);
}