diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 08:11:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | 6c10e2b37ee7b95485fb86941de32991a1732892 (patch) | |
| tree | a26e3bc3f94318b242cddd75c58f715e749f197e /src/ninep/pane.zig | |
| parent | f7e3441698a4621bec0d7c14eb45303753ffbead (diff) | |
| download | pardes-6c10e2b37ee7b95485fb86941de32991a1732892.tar.gz pardes-6c10e2b37ee7b95485fb86941de32991a1732892.zip | |
A name write is one name, refused with its reason otherwise; a log record is one line of UTF-8
A name took a second line, DEL, a C1 control or bytes that are not UTF-8,
which then went into /index and /log as they were and split or garbled a
reader's lines. `name` strips one trailing newline and refuses the rest,
saying which (`bad character in file name: not UTF-8`); and the log turns
DEL and C1 into spaces, as it did control characters, and escapes bytes
that are not UTF-8 as `\xNN`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/pane.zig')
| -rw-r--r-- | src/ninep/pane.zig | 44 |
1 files changed, 38 insertions, 6 deletions
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 5f8fc00a..66b3728c 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -597,12 +597,27 @@ fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply { /// quietly cut off, so the name a script wrote is the name it gets. 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. +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] == ' ' or name[name.len - 1] == ' ') return e_name_char ++ ": a blank at its end"; + return null; +} + fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { - // One line: its newline ends it, as `echo` writes it. + // One name: its newline ends it, as `echo` writes it, and it is one. const name = if (std.mem.endsWith(u8, req.data, "\n")) req.data[0 .. req.data.len - 1] else req.data; if (name.len == 0) return Reply.fail(req.tag, E.INVAL); - for (name) |c| if (c < ' ') return tree.failText(req.tag, E.INVAL, e_name_char); - if (name[0] == ' ' or name[name.len - 1] == ' ') return tree.failText(req.tag, E.INVAL, e_name_char); + if (nameFault(name)) |why| return tree.failText(req.tag, E.INVAL, why); if (fileOf(pane) == null) return tree.failText(req.tag, E.PERM, if (pane.isTerminal()) "rename not allowed: a terminal is named by its shell's directory; cd there, or Tty in another" else @@ -1105,9 +1120,26 @@ test "name reads the file name and writing it promotes a scratch without touchin try testing.expectEqualStrings("existing target\n", target); for ([_][]const u8{ "", "\n", "bad\x01name\n" }) |bad| try testing.expectEqual(E.INVAL, wr(p, name, bad).errno()); - // A blank at either end is refused as acme refuses one, not cut off. - for ([_][]const u8{ "trailing.zig \n", " leading.zig\n", "tab\t.zig\n" }) |bad| - try testing.expectEqualStrings("bad character in file name", wr(p, name, bad).reply.ename); + // 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. + for ([_][2][]const u8{ + .{ "trailing.zig \n", "a blank at its end" }, + .{ " leading.zig\n", "a blank at its end" }, + .{ "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" }, + }) |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); + } + // 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")); + try testing.expectEqual(Status.ok, wr(p, name, try std.fmt.bufPrint(&line, "{s}\n", .{path})).reply.status); try testing.expectEqualStrings(path, pane.file.?.path); try testing.expectEqual(Status.ok, wr(p, name, "two words.zig\n").reply.status); const spaced = try std.fmt.bufPrint(&path_buffer, "{s}/two words.zig", .{directory}); |
