diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 21:29:39 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 20685b048fcb307840451471679339c9b9704b23 (patch) | |
| tree | 8a803f1551ac117c41d271ff972c26a259500569 /src | |
| parent | 237071bd4865f0220c35c8c37895c8dba48aed84 (diff) | |
| download | pardes-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.zig | 5 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 38 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 4 |
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); } |
