diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/fs.zig | 4 | ||||
| -rw-r--r-- | src/ninep/cols.zig | 2 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 26 | ||||
| -rw-r--r-- | src/ninep/events.zig | 4 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 15 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 2 |
6 files changed, 39 insertions, 14 deletions
@@ -224,12 +224,12 @@ pub fn osHandle(p: *pardes.Pardes, req: Req) Reply { if (fd < 0) return Reply.fail(req.tag, E.PERM); defer _ = libc.close(fd); if (req.op == .setattr) { - if (!req.truncate or req.off != 0 or libc.ftruncate(fd, 0) != 0) return Reply.fail(req.tag, E.INVAL); + if (!req.truncate or req.off != 0 or libc.ftruncate(fd, 0) != 0) return tree.failText(req.tag, E.INVAL, "invalid truncate: only to zero"); var truncated = attr; truncated.size = 0; return .{ .tag = req.tag, .attr = truncated }; } - if (req.off > std.math.maxInt(i64)) return Reply.fail(req.tag, E.INVAL); + if (req.off > std.math.maxInt(i64)) return tree.failText(req.tag, E.INVAL, "invalid offset: past what a file may hold"); if (libc.lseek(fd, @intCast(req.off), libc.SEEK.SET) < 0) return Reply.fail(req.tag, E.IO); if (req.op == .write) { const written = libc.write(fd, req.data.ptr, req.data.len); diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index 7f78522a..6fdcda07 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -166,7 +166,7 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply { while (lines.next()) |raw| { const line = std.mem.trim(u8, raw, " \t\r"); if (line.len == 0) continue; - for (line) |c| if (c < ' ' and c != '\t') return tree.failText(req.tag, E.INVAL, pardes.ctlfs.ctl.e_control); + for (line) |c| if ((c < ' ' and c != '\t') or c == 0x7f) return tree.failText(req.tag, E.INVAL, pardes.ctlfs.ctl.e_control); if (pardes.ctlfs.ctl.tooLong(req, line)) |refusal| return refusal; // A pane's word (Undo, Msg, Save) is no word of a tag no pane owns: // refused as the root's ctl refuses it, never done at whichever diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 2f94dd3c..ccba342e 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -179,7 +179,7 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool, in_tag: bool) Rep while (it.next()) |text| { if (text.len == 0) continue; // Only an Edit block holds newlines (Messages). - for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return tree.failText(req.tag, E.INVAL, e_control); + for (text) |c| if ((c < ' ' and c != '\t' and c != '\n') or c == 0x7f) return tree.failText(req.tag, E.INVAL, e_control); if (exec) if (tooLong(req, text)) |refusal| return refusal; if (!apply) continue; const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; @@ -214,7 +214,7 @@ pub fn tooLong(req: Req, line: []const u8) ?Reply { return tree.failText(req.tag, E.INVAL, e_too_long); } -pub const e_control = "invalid command line: it holds a control character other than a tab"; +pub const e_control = "invalid command line: it holds a control character (or DEL) other than a tab"; /// Runs one click (`run`) as a 9P write's: a builtin that fails there fails /// the write, with its words and an err record, and no msg, as a ctl line @@ -1968,6 +1968,28 @@ test "pty/ctl exec in a directory that is gone fails ENOENT; a shell that cannot try testing.expectEqualStrings("shell: access denied", p.fs.late_failure[0..p.fs.late_failure_len]); } +test "every EINVAL a write gets says why, in its err record too; DEL is a control character in a line" { + const p = try withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = serialOf(p); + const log = @intFromEnum(tree.TopFile.log); + const h = call(p, .{ .tag = 1, .op = .open, .node = log, .omode = 2 }).reply.handle; + const Case = struct { node: u64, data: []const u8, words: []const u8, handle: u32 = 0 }; + for ([_]Case{ + .{ .node = root_exec, .data = "echo a\x7fb\n", .words = "control character" }, + .{ .node = root_look, .data = "a\x7fb\n", .words = "control character" }, + .{ .node = Node.of(serial, .name), .data = "\n", .words = "an empty name" }, + .{ .node = log, .data = "bogus\n", .words = "takes `follow`", .handle = h }, + }) |c| { + const refused = call(p, .{ .tag = 2, .op = .write, .node = c.node, .handle = c.handle, .data = c.data }); + try testing.expectEqual(E.INVAL, refused.errno()); + try testing.expect(std.mem.indexOf(u8, refused.reply.ename, c.words) != null); + } + _ = call(p, .{ .tag = 3, .op = .release, .node = log, .handle = h }); + try testing.expect(!th.logHas(p, "Invalid argument")); + try testing.expect(th.logHas(p, "an empty name")); +} + test "size is monotonic: growing is never refused, and a size once taken is taken again" { const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 60 }); defer p.deinit(); diff --git a/src/ninep/events.zig b/src/ninep/events.zig index d7079c5e..f58d970e 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -101,7 +101,7 @@ pub const Queue = struct { /// One record per read; `.again` holds the read until a record arrives. pub fn readQueue(p: *Pardes, req: Req, q: *Queue) Reply { const record = q.peek() orelse return .{ .tag = req.tag, .status = .again }; - if (req.size < record.len) return Reply.fail(req.tag, E.INVAL); + if (req.size < record.len) return tree.failText(req.tag, E.INVAL, "invalid read: shorter than the record waiting"); const out = p.fs.stage(p.gpa); out.appendSlice(p.gpa, record) catch return Reply.fail(req.tag, E.NOMEM); q.pop(); @@ -477,7 +477,7 @@ pub fn writeLog(p: *Pardes, req: Req) Reply { // what was there and waits for what comes after, as tail -n0 -f does. if (std.mem.eql(u8, word, "follow new")) { slot.pos = slot.bytes.len; - } else if (!std.mem.eql(u8, word, "follow")) return Reply.fail(req.tag, E.INVAL); + } else if (!std.mem.eql(u8, word, "follow")) return tree.failText(req.tag, E.INVAL, "invalid write to log: it takes `follow` or `follow new`"); slot.follow = true; return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 66b3728c..4c967fc5 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -481,8 +481,11 @@ fn writeTag(req: Req, pane: *Pane) Reply { return .{ .tag = req.tag, .written = @intCast(take) }; } +/// A write only text takes, to a pane with none: said, as every EINVAL is. +pub const e_no_text = "invalid write: this pane has no text (a terminal, an image or a PDF)"; + fn writeData(p: *Pardes, req: Req, pane: *Pane) Reply { - if (fileOf(pane) == null) return Reply.fail(req.tag, E.INVAL); + if (fileOf(pane) == null) return tree.failText(req.tag, E.INVAL, e_no_text); const pf = &pane.fs; if (pf.addr_failed) return tree.failText(req.tag, E.INVAL, e_addr_failed); clampAddr(pf, bodyOf(pane).len); @@ -501,7 +504,7 @@ fn writeData(p: *Pardes, req: Req, pane: *Pane) Reply { } fn writeSel(p: *Pardes, req: Req, pane: *Pane) Reply { - if (fileOf(pane) == null) return Reply.fail(req.tag, E.INVAL); + if (fileOf(pane) == null) return tree.failText(req.tag, E.INVAL, e_no_text); const d = dotOf(pane); const q0: usize = d.q0; const q1: usize = @max(q0, @as(usize, d.q1)); @@ -558,7 +561,7 @@ fn writeRange(req: Req, pane: *Pane, file: PaneFile) Reply { .limit => pf.limit = range, // Setting dot scrolls to it, which is the whole of acme's `show`. .dot => { - if (fileOf(pane) == null) return Reply.fail(req.tag, E.INVAL); + if (fileOf(pane) == null) return tree.failText(req.tag, E.INVAL, e_no_text); setDot(pane, range); }, else => unreachable, @@ -573,7 +576,7 @@ fn writeFlag(p: *Pardes, req: Req, pane: *Pane, file: PaneFile) Reply { const pf = &pane.fs; switch (file) { .dirty => { - const f = fileOf(pane) orelse return Reply.fail(req.tag, E.INVAL); + const f = fileOf(pane) orelse return tree.failText(req.tag, E.INVAL, e_no_text); f.saved_revision = if (on) f.revision -% 1 else f.revision; }, // acme's nomark joins the writes after it into one undo step. The @@ -616,7 +619,7 @@ fn nameFault(name: []const u8) ?[]const u8 { fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { // 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); + if (name.len == 0) return tree.failText(req.tag, E.INVAL, e_name_char ++ ": an empty name"); 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" @@ -624,7 +627,7 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { "rename not allowed: an image or PDF is named by the file it shows"); const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), name }) catch return Reply.fail(req.tag, E.NOMEM); - if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return Reply.fail(req.tag, E.INVAL); + if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return tree.failText(req.tag, E.INVAL, "invalid file name: longer than a path may be"); nameBuffer(p, id, full, false) catch |err| return Reply.fail(req.tag, switch (err) { error.OutOfMemory => E.NOMEM, else => E.INVAL, diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index ed0e0352..391847f5 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -1007,7 +1007,7 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { if (o.what != .lines and o.what != .ctl) return writeNow(p, req, target); // A clicked line holds no control character: refused at once, not held // to fail unseen at the close. - if (resultsFile(target)) for (req.data) |c| if (c < ' ' and c != '\t' and c != '\n' and c != '\r') return Reply.fail(req.tag, E.INVAL); + if (resultsFile(target)) for (req.data) |c| if ((c < ' ' and c != '\t' and c != '\n' and c != '\r') or c == 0x7f) return failText(req.tag, E.INVAL, ctl.e_control); o.pending.appendSlice(p.gpa, req.data) catch return Reply.fail(req.tag, E.NOMEM); const end = ctl.completeEnd(p, o.pending.items); if (end == 0) { |
