diff --git a/src/terminal/tmux/control.zig b/src/terminal/tmux/control.zig index dbc64b340..f68a748f1 100644 --- a/src/terminal/tmux/control.zig +++ b/src/terminal/tmux/control.zig @@ -48,6 +48,11 @@ pub const Parser = struct { block, }; + const BlockTerminator = enum { + end, + err, + }; + pub fn deinit(self: *Parser) void { // If we're in a broken state, we already deinited // the buffer, so we don't need to do anything. @@ -116,10 +121,8 @@ pub const Parser = struct { )) |v| v + 1 else 0; const line = written[idx..]; - if (std.mem.startsWith(u8, line, "%end") or - std.mem.startsWith(u8, line, "%error")) - { - const err = std.mem.startsWith(u8, line, "%error"); + if (parseBlockTerminator(line)) |terminator| { + const err = terminator == .err; const output = std.mem.trimRight(u8, written[0..idx], "\r\n"); // If it is an error then log it. @@ -144,6 +147,36 @@ pub const Parser = struct { const ParseError = error{RegexError}; + /// Block payload is raw data, so a line only terminates a block if it + /// exactly matches tmux's `%end`/`%error` guard-line shape. + fn parseBlockTerminator(line_raw: []const u8) ?BlockTerminator { + var line = line_raw; + if (line.len > 0 and line[line.len - 1] == '\r') { + line = line[0 .. line.len - 1]; + } + + var fields = std.mem.tokenizeScalar(u8, line, ' '); + const cmd = fields.next() orelse return null; + const terminator: BlockTerminator = if (std.mem.eql(u8, cmd, "%end")) + .end + else if (std.mem.eql(u8, cmd, "%error")) + .err + else + return null; + + const time = fields.next() orelse return null; + const command_id = fields.next() orelse return null; + const flags = fields.next() orelse return null; + const extra = fields.next(); + + _ = std.fmt.parseInt(usize, time, 10) catch return null; + _ = std.fmt.parseInt(usize, command_id, 10) catch return null; + _ = std.fmt.parseInt(usize, flags, 10) catch return null; + if (extra != null) return null; + + return terminator; + } + fn parseNotification(self: *Parser) ParseError!?Notification { assert(self.state == .notification); @@ -597,6 +630,81 @@ test "tmux begin/end data" { try testing.expectEqualStrings("hello\nworld", n.block_end); } +test "tmux block payload may start with %end" { + const testing = std.testing; + const alloc = testing.allocator; + + var c: Parser = .{ .buffer = .init(alloc) }; + defer c.deinit(); + for ("%begin 1 1 1\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end not really\n") |byte| try testing.expect(try c.put(byte) == null); + for ("hello\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end 1 1 1") |byte| try testing.expect(try c.put(byte) == null); + const n = (try c.put('\n')).?; + try testing.expect(n == .block_end); + try testing.expectEqualStrings("%end not really\nhello", n.block_end); +} + +test "tmux block payload may start with %error" { + const testing = std.testing; + const alloc = testing.allocator; + + var c: Parser = .{ .buffer = .init(alloc) }; + defer c.deinit(); + for ("%begin 1 1 1\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%error not really\n") |byte| try testing.expect(try c.put(byte) == null); + for ("hello\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end 1 1 1") |byte| try testing.expect(try c.put(byte) == null); + const n = (try c.put('\n')).?; + try testing.expect(n == .block_end); + try testing.expectEqualStrings("%error not really\nhello", n.block_end); +} + +test "tmux block may terminate with real %error after misleading payload" { + const testing = std.testing; + const alloc = testing.allocator; + + var c: Parser = .{ .buffer = .init(alloc) }; + defer c.deinit(); + for ("%begin 1 1 1\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%error not really\n") |byte| try testing.expect(try c.put(byte) == null); + for ("hello\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%error 1 1 1") |byte| try testing.expect(try c.put(byte) == null); + const n = (try c.put('\n')).?; + try testing.expect(n == .block_err); + try testing.expectEqualStrings("%error not really\nhello", n.block_err); +} + +test "tmux block terminator requires exact token count" { + const testing = std.testing; + const alloc = testing.allocator; + + var c: Parser = .{ .buffer = .init(alloc) }; + defer c.deinit(); + for ("%begin 1 1 1\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end 1 1 1 trailing\n") |byte| try testing.expect(try c.put(byte) == null); + for ("hello\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end 1 1 1") |byte| try testing.expect(try c.put(byte) == null); + const n = (try c.put('\n')).?; + try testing.expect(n == .block_end); + try testing.expectEqualStrings("%end 1 1 1 trailing\nhello", n.block_end); +} + +test "tmux block terminator requires numeric metadata" { + const testing = std.testing; + const alloc = testing.allocator; + + var c: Parser = .{ .buffer = .init(alloc) }; + defer c.deinit(); + for ("%begin 1 1 1\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end foo bar baz\n") |byte| try testing.expect(try c.put(byte) == null); + for ("hello\n") |byte| try testing.expect(try c.put(byte) == null); + for ("%end 1 1 1") |byte| try testing.expect(try c.put(byte) == null); + const n = (try c.put('\n')).?; + try testing.expect(n == .block_end); + try testing.expectEqualStrings("%end foo bar baz\nhello", n.block_end); +} + test "tmux output" { const testing = std.testing; const alloc = testing.allocator;