diff options
| -rw-r--r-- | docs/fs.md | 2 | ||||
| -rw-r--r-- | src/ninep/pty.zig | 27 | ||||
| -rw-r--r-- | test/fs.py | 11 |
3 files changed, 32 insertions, 8 deletions
@@ -574,7 +574,7 @@ Terminal panes also have `pty/`: replay). - `pty/status`: one line, `cols rows busy`; busy is 1 while a command runs or text is typed at the prompt. -- `pty/ctl`: `winsize C R` (at least 2 rows), `sig INT|TERM|HUP|QUIT|KILL`, +- `pty/ctl`: `winsize C R` (at least 2 rows, fewer refused `invalid winsize: at least 2 rows`; at most 4096 a side), `sig INT|TERM|HUP|QUIT|KILL`, `exec` (restart the shell in its directory: refused on a command pane, `a command pane does not restart`; `exec: <dir>: no such directory` if it is gone; a shell that cannot start fails and leaves the old one running). diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 390ae58d..2245255b 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -33,7 +33,17 @@ pub const pty_rows_min = 2; /// As the root ctl's `size` (4096 a side). const winsize_max = 4096; -const e_winsize_range = std.fmt.comptimePrint("invalid winsize: out of range, 1x1 to {d}x{d}", .{ winsize_max, winsize_max }); +const e_winsize_range = std.fmt.comptimePrint("invalid winsize: out of range, 1x{d} to {d}x{d}", .{ pty_rows_min, winsize_max, winsize_max }); +const e_winsize_rows = std.fmt.comptimePrint("invalid winsize: at least {d} rows", .{pty_rows_min}); + +/// `winsize C R` whose R is a size but fewer rows than a pty takes. +fn tooFewRows(line: []const u8) bool { + var words = std.mem.tokenizeAny(u8, line, " \t"); + if (!std.mem.eql(u8, words.next() orelse return false, "winsize")) return false; + _ = dimension(words.next() orelse return false) orelse return false; + const rows = dimension(words.next() orelse return false) orelse return false; + return words.next() == null and rows < pty_rows_min; +} /// A winsize whose numbers are numbers but not sizes: said as such. fn outOfRange(line: []const u8) bool { @@ -79,7 +89,7 @@ pub fn writeCtl(p: *Pardes, req: Req, id: usize) Reply { if (@import("../host_io.zig").Shell.refusal(bin[0..@min(bin.len, 200)], &why)) |refused| return tree.failText(req.tag, E.NOENT, std.fmt.bufPrint(&p.fs.ename, "exec: {s}", .{refused}) catch "exec: no such shell"); }; - if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, if (outOfRange(line)) e_winsize_range else e_bad_pty_ctl); + if (!verb(p, id, line, apply)) return tree.failText(req.tag, E.INVAL, if (tooFewRows(line)) e_winsize_rows else if (outOfRange(line)) e_winsize_range else e_bad_pty_ctl); } } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; @@ -93,8 +103,10 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool { .winsize => { const cols = dimension(words.next() orelse return false) orelse return false; // Two rows at least, as a pane is laid out: a one-row pty - // loses its prompt's input mark and reads busy for ever. - const rows = @max(pty_rows_min, dimension(words.next() orelse return false) orelse return false); + // loses its prompt's input mark and reads busy for ever. Fewer + // is refused, never quietly raised. + const rows = dimension(words.next() orelse return false) orelse return false; + if (rows < pty_rows_min) return false; if (words.next() != null) return false; if (!apply) return true; p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } }); @@ -619,6 +631,13 @@ test "every pty/ctl verb, and every refusal" { try testing.expect(a.winsize == null); } + // One row is a size, but fewer than a pty takes: refused, and said so. + const one = wr(p, ctl, "winsize 80 1"); + try testing.expectEqual(E.INVAL, one.errno()); + try testing.expectEqualStrings("invalid winsize: at least 2 rows", one.reply.ename); + try testing.expect(one.winsize == null); + try testing.expectEqualStrings("invalid winsize: out of range, 1x2 to 4096x4096", e_winsize_range); + const spaced = wr(p, ctl, "\n winsize 90 20 \n\n"); try testing.expectEqual(Status.ok, spaced.reply.status); try testing.expectEqual(@as(u16, 90), spaced.winsize.?.cols); @@ -436,9 +436,14 @@ def run_file(binary): assert run(client, term, b'fi\n') == b'error not run\n' # a syntax error runs nothing assert run(client, term, b"echo 'open\n") == b'error not run\n' # nor does an open quote assert run(client, term, b'true\n') == b'exit 0\n' # ...and the shell is clear after - # A pty asked for one row gets two: one loses the prompt's input - # mark, and the shell would read busy for ever. - client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n') + # A pty is not given one row (it loses the prompt's input mark, + # and the shell would read busy for ever): refused, and said. + try: + client.write(f'/pane/{term}/pty/ctl', b'winsize 20 1\n') + raise AssertionError('winsize 20 1 was taken') + except OSError as refused: + assert 'at least 2 rows' in str(refused), refused + client.write(f'/pane/{term}/pty/ctl', b'winsize 20 2\n') time.sleep(.5) assert run(client, term, b'true\n') == b'exit 0\n' assert client.read(f'/pane/{term}/pty/ctl').split()[2] == b'2', client.read(f'/pane/{term}/pty/ctl') |
