diff options
Diffstat (limited to 'src/ninep/cols.zig')
| -rw-r--r-- | src/ninep/cols.zig | 128 |
1 files changed, 122 insertions, 6 deletions
diff --git a/src/ninep/cols.zig b/src/ninep/cols.zig index cc583c93..454cce4a 100644 --- a/src/ninep/cols.zig +++ b/src/ninep/cols.zig @@ -7,6 +7,7 @@ const std = @import("std"); const pardes = @import("../pardes.zig"); const Pardes = pardes.Pardes; const tree = @import("tree.zig"); +const limits = @import("../memory.zig").limits; const tagline = pardes.tagline; const layout = pardes.layout; const Text = pardes.panes.Text; @@ -65,10 +66,11 @@ pub fn headerText(p: *Pardes, serial: ?u32) ?[]const u8 { } pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply { + applyTruncation(p, serial); const text = headerText(p, serial) orelse return Reply.fail(req.tag, E.NOENT); const out = p.fs.stage(p.gpa); + // As a pane tag reads: the text alone, no newline after it. out.appendSlice(p.gpa, text) catch return Reply.fail(req.tag, E.NOMEM); - out.append(p.gpa, '\n') catch return Reply.fail(req.tag, E.NOMEM); return tree.stagedReply(p, req); } @@ -78,7 +80,19 @@ pub fn read(p: *Pardes, req: Req, serial: ?u32) Reply { pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply { const t = (header(p, serial) orelse return Reply.fail(req.tag, E.NOENT)).text; if (req.data.len == 0) return .{ .tag = req.tag, .written = 0 }; - if (pardes.ctlfs.pane.tagFault(req.data)) |why| return tree.failText(req.tag, E.INVAL, why); + const pending = p.fs.header_trunc_pending == rewriteKey(serial); + // The same checks as a pane tag's, whole or not at all; a refused + // write drops the truncation it would have come with. + if (pardes.ctlfs.pane.tagFault(req.data)) |why| { + if (pending) p.fs.header_trunc_pending = null; + return tree.failText(req.tag, E.INVAL, why); + } + const had = if (pending) 0 else headerText(p, serial).?.len; + if (req.data.len > limits.max_tag_tail -| had) { + if (pending) p.fs.header_trunc_pending = null; + return tree.failText(req.tag, E.NOSPC, pardes.ctlfs.pane.e_tag_over); + } + applyTruncation(p, serial); const was = headerText(p, serial).?; var data = req.data; const rewriting = p.fs.header_rewrite == rewriteKey(serial); @@ -99,6 +113,20 @@ pub fn write(p: *Pardes, req: Req, serial: ?u32) Reply { /// Truncating clears it, as a pane tag's `cleartag`: its default words go /// too, and `u` in it brings them back. pub fn truncate(p: *Pardes, serial: ?u32) tree.Status { + if (header(p, serial) == null) return .err; + // Done with the write after it, which may yet be refused. + p.fs.header_trunc_pending = rewriteKey(serial); + return .ok; +} + +/// A pending truncation of this header, done. +pub fn applyTruncation(p: *Pardes, serial: ?u32) void { + if (p.fs.header_trunc_pending != rewriteKey(serial)) return; + p.fs.header_trunc_pending = null; + _ = clear(p, serial); +} + +fn clear(p: *Pardes, serial: ?u32) tree.Status { const t = (header(p, serial) orelse return .err).text; const empty = p.gpa.alloc(u8, 0) catch return .err; t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null); @@ -118,6 +146,7 @@ fn rewriteKey(serial: ?u32) u32 { } pub fn released(p: *Pardes, serial: ?u32) void { + applyTruncation(p, serial); if (p.fs.header_rewrite != rewriteKey(serial)) return; p.fs.header_rewrite = null; p.fs.header_held = false; @@ -176,7 +205,8 @@ pub fn writeExec(p: *Pardes, req: Req, serial: ?u32) Reply { if (paneWord(line, if (serial == null) pardes.config.topbar_str else tagline.column_tag_default)) return pardes.ctlfs.ctl.refuseTo(p, req, "not a session control message", line, "pane/<n>/ctl"); const col = if (serial) |s| layout.columnBySerial(p, s) orelse return Reply.fail(req.tag, E.NOENT) else null; - if (p.panes[p.active] == null) return Reply.fail(req.tag, E.NOENT); + // With no pane at all, a word runs at the stand-in, as a click in + // the empty window's tag does (runBuiltinFrom). p.exec_column = col; defer p.exec_column = null; p.exec_header = true; @@ -198,6 +228,19 @@ test "New written to a column's exec makes its scratch in the session's director try testing.expectEqualStrings("/project/src/+New", p.panes[p.active].?.file.?.path); } +test "a tagexec word that makes, acts on and focuses no pane reads back nothing" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const tagexec = @intFromEnum(tree.TopFile.tagexec); + try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, "Newcol\n").reply.status); + try testing.expectEqualStrings("", th.rd(p, tagexec, 0, 64).bytes); + // A column's New makes one, and answers it. + const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec); + try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status); + var want: [16]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{p.panes[p.active].?.serial}), th.rd(p, col_exec, 0, 64).bytes); +} + test "New written to a column's ctl reads back the new pane alone, as its exec does" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); @@ -218,6 +261,8 @@ fn paneWord(line: []const u8, own: []const u8) bool { const word = cmd[0 .. std.mem.indexOfAny(u8, cmd, " \t+") orelse cmd.len]; const b = std.meta.stringToEnum(pardes.builtins.registry.Builtin(), word) orelse return false; if (pardes.builtins.registry.scope(b) == .session) return false; + // A new pane is every tag's to make, in the session's directory. + if (b == .New or b == .Tty) return false; var words = std.mem.tokenizeScalar(u8, own, ' '); while (words.next()) |w| if (std.mem.eql(u8, w, word)) return false; return true; @@ -238,6 +283,47 @@ test "a pane's word written to /tagexec or a column's exec is refused, and acts try testing.expectEqual(tree.Status.ok, th.wr(p, col_exec, "New\n").reply.status); } +test "Help and Find at /tagexec act in the session's directory, not the keyboard pane's" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "needle.txt", .data = "n\n" }); + var buf: [4096]u8 = undefined; + const session = buf[0..try tmp.dir.realPath(testing.io, &buf)]; + @memcpy(p.session_dir[0..session.len], session); + p.session_dir_len = @intCast(session.len); + const tagexec = @intFromEnum(tree.TopFile.tagexec); + for ([_][]const u8{ "Help\n", "Find needle\n" }) |line| { + try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, line).reply.status); + const made = p.panes[p.active].?.file.?.path; + try testing.expectEqualStrings(session, std.fs.path.dirname(made).?); + } +} + +test "New and Tty run at /tagexec and a column's exec, with no pane at all too" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const tagexec = @intFromEnum(tree.TopFile.tagexec); + const col_exec = tree.Node.ofCol(layout.columnSerial(p, 0), .exec); + for ([_]u64{ tagexec, col_exec }) |node| for ([_][]const u8{ "New\n", "Tty\n" }) |line| { + const before = p.next_serial; + try testing.expectEqual(tree.Status.ok, th.wr(p, node, line).reply.status); + try testing.expectEqual(before + 1, p.next_serial); + }; + // Every pane gone: the workspace tag makes one all the same. + for ([_][]const u8{ "New\n", "Tty\n" }) |line| { + while (p.ncol > 0) { + const first = p.panes[p.col_panes[0][0]].?.serial; + _ = th.wr(p, tree.Node.of(first, .ctl), "delete\n"); + if (p.anyPane() == null) break; + } + try testing.expect(p.anyPane() == null); + try testing.expectEqual(tree.Status.ok, th.wr(p, tagexec, line).reply.status); + try testing.expect(p.anyPane() != null); + } +} + test "every pane word a tag no pane owns does not hold is refused there, Edit included, and the pane is untouched" { const p = try th.withFile(testing.allocator, "keep me\n"); defer p.deinit(); @@ -250,6 +336,7 @@ test "every pane word a tag no pane owns does not hold is refused there, Edit in var checked: usize = 0; for (std.enums.values(Builtin)) |b| { if (pardes.builtins.registry.scope(b) == .session) continue; + if (b == .New or b == .Tty) continue; // every tag's var owned = false; var words = std.mem.tokenizeScalar(u8, tag_file.own, ' '); while (words.next()) |w| owned = owned or std.mem.eql(u8, w, @tagName(b)); @@ -321,15 +408,15 @@ test "a column's tag and the workspace's read, replace and append as a pane tag try testing.expectEqual(E.NOENT, th.look_up(p, col_dir, "777").errno()); const tag = Node.ofCol(serial, .tag); try testing.expectEqual(tree.Status.ok, th.look_up(p, Node.ofCol(serial, .dir), "tag").reply.status); - try testing.expectEqualStrings("New Tty Find Grep Joincol Delcol\n", th.rd(p, tag, 0, 4096).bytes); + try testing.expectEqualStrings("New Tty Find Grep Joincol Delcol", th.rd(p, tag, 0, 4096).bytes); // `echo Make > tag`: cleared, then the newline ending the write dropped. _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = tag, .truncate = true }); _ = th.wr(p, tag, "Make\n"); - try testing.expectEqualStrings("Make\n", th.rd(p, tag, 0, 4096).bytes); + try testing.expectEqualStrings("Make", th.rd(p, tag, 0, 4096).bytes); _ = th.call(p, .{ .tag = 2, .op = .release, .node = tag }); // `>>` appends; a newline in a one-line tag is a space. _ = th.wr(p, tag, " Lint\nNow"); - try testing.expectEqualStrings("Make Lint Now\n", th.rd(p, tag, 0, 4096).bytes); + try testing.expectEqualStrings("Make Lint Now", th.rd(p, tag, 0, 4096).bytes); const top = @intFromEnum(tree.TopFile.tag); _ = th.call(p, .{ .tag = 3, .op = .setattr, .node = top, .truncate = true }); _ = th.wr(p, top, "Newcol Exit\n"); @@ -356,6 +443,35 @@ test "a focus write makes its pane's column the active one: layout says so and p } } +test "a refused write after a truncation leaves every kind of tag as it was, and Dump still works" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const serial = th.serialOf(p); + const col = layout.columnSerial(p, 0); + const big = "w" ** (limits.max_tag_tail + 1); + // Each tag: a truncating open, then a write too long: nothing changes. + for ([_]u64{ Node.of(serial, .tag), Node.ofCol(col, .tag), @intFromEnum(tree.TopFile.tag) }) |node| { + _ = th.wr(p, node, " Mine"); + const before = try testing.allocator.dupe(u8, th.rd(p, node, 0, 1 << 16).bytes); + defer testing.allocator.free(before); + _ = th.call(p, .{ .tag = 1, .op = .setattr, .node = node, .truncate = true }); + const h = th.call(p, .{ .tag = 2, .op = .open, .node = node, .omode = 1 }).reply.handle; + const r = th.call(p, .{ .tag = 3, .op = .write, .node = node, .handle = h, .data = big }); + try testing.expectEqual(E.NOSPC, r.errno()); + try testing.expectEqualStrings("tag: no space: over 4096 bytes", r.reply.ename); + _ = th.call(p, .{ .tag = 4, .op = .release, .node = node, .handle = h, .opened = true }); + try testing.expectEqualStrings(before, th.rd(p, node, 0, 1 << 16).bytes); + // A control character is refused the same way on every kind. + try testing.expectEqual(E.INVAL, th.wr(p, node, "a\x01b").errno()); + // A truncation with no write after it clears at the close. + _ = th.call(p, .{ .tag = 5, .op = .setattr, .node = node, .truncate = true }); + const e = th.call(p, .{ .tag = 6, .op = .open, .node = node, .omode = 1 }).reply.handle; + _ = th.call(p, .{ .tag = 7, .op = .release, .node = node, .handle = e, .opened = true }); + try testing.expect(std.mem.indexOf(u8, th.rd(p, node, 0, 1 << 16).bytes, "Mine") == null); + } + try pardes.dump.dumpState(p); +} + test "a column's ctl and exec act on it as its tag would, and rmdir closes it only empty" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); |
