Merge 7f8dc85913 into 629838b9bd
commit
32299973e2
|
|
@ -492,6 +492,55 @@ pub const Parser = struct {
|
|||
}
|
||||
};
|
||||
|
||||
/// Decode tmux control mode octal escaping in place. tmux encodes
|
||||
/// bytes less than ASCII 32 and `\` itself as `\ooo` (backslash +
|
||||
/// three octal digits). The decoded output is always <= the input
|
||||
/// length, so this can safely write back into the same buffer.
|
||||
fn decodeEscapedOutput(data: []u8) []const u8 {
|
||||
var read_idx: usize = 0;
|
||||
var write_idx: usize = 0;
|
||||
|
||||
while (read_idx < data.len) {
|
||||
// Pass through non escape bytes as is.
|
||||
if (data[read_idx] != '\\') {
|
||||
data[write_idx] = data[read_idx];
|
||||
read_idx += 1;
|
||||
write_idx += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
read_idx += 1; // consume '\'
|
||||
|
||||
// Try to decode three octal digits following the backslash.
|
||||
// If fewer than 3 bytes remain or any digit is non-octal,
|
||||
// emit '?' as a visible replacement rather than silently
|
||||
// dropping %output payload
|
||||
var value: u8 = 0;
|
||||
var ok = false;
|
||||
|
||||
// after consuming '\' read_idx is at the first potential octal digit, we need +2 after it.
|
||||
if (read_idx + 2 < data.len) {
|
||||
var i: usize = 0;
|
||||
while (i < 3) : (i += 1) {
|
||||
const digit = data[read_idx];
|
||||
if (digit < '0' or digit > '7') break;
|
||||
|
||||
// tmux encodes bytes < 32 and '\' (92 = \134),
|
||||
// so the max decoded value is 92. No overflow.
|
||||
value = value * 8 + (digit - '0');
|
||||
read_idx += 1;
|
||||
}
|
||||
|
||||
ok = i == 3;
|
||||
}
|
||||
|
||||
data[write_idx] = if (ok) value else '?';
|
||||
write_idx += 1;
|
||||
}
|
||||
|
||||
return data[0..write_idx];
|
||||
}
|
||||
|
||||
/// Possible notification types from tmux control mode. These are documented
|
||||
/// in tmux(1). A lot of the simple documentation was copied from that man
|
||||
/// page here.
|
||||
|
|
@ -517,10 +566,7 @@ pub const Notification = union(enum) {
|
|||
block_err: []const u8,
|
||||
|
||||
/// Raw output from a pane.
|
||||
output: struct {
|
||||
pane_id: usize,
|
||||
data: []const u8, // unescaped
|
||||
},
|
||||
output: Output,
|
||||
|
||||
/// The client is now attached to the session with ID session-id, which is
|
||||
/// named name.
|
||||
|
|
@ -571,6 +617,28 @@ pub const Notification = union(enum) {
|
|||
name: []const u8,
|
||||
},
|
||||
|
||||
pub const Output = struct {
|
||||
pane_id: usize,
|
||||
data: []u8,
|
||||
/// Tracks whether `data` has been decoded for this Output copy.
|
||||
///
|
||||
/// `data` points into the parser buffer and is mutated in place, while this
|
||||
/// flag belongs to this Output copy. A copy made before `decoded()` runs
|
||||
/// keeps the flag false even after another copy decodes the buffer in place,
|
||||
/// so only one copy of an Output may call `decoded()`.
|
||||
is_decoded: bool = false,
|
||||
|
||||
pub fn decoded(self: *Output) []const u8 {
|
||||
if (!self.is_decoded) {
|
||||
const data = decodeEscapedOutput(self.data);
|
||||
self.data = self.data[0..data.len];
|
||||
self.is_decoded = true;
|
||||
}
|
||||
|
||||
return self.data;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn format(self: Notification, writer: *std.Io.Writer) !void {
|
||||
const T = Notification;
|
||||
const info = @typeInfo(T).@"union";
|
||||
|
|
@ -724,6 +792,61 @@ test "tmux output" {
|
|||
try testing.expectEqualStrings("foo bar baz", n.output.data);
|
||||
}
|
||||
|
||||
test "tmux decode octal escape" {
|
||||
const testing = std.testing;
|
||||
|
||||
var input = [_]u8{ '\\', '0', '3', '3', '[', '?', '2', '0', '0', '4', 'h' };
|
||||
const got = decodeEscapedOutput(input[0..]);
|
||||
const expected = [_]u8{ 0x1b, '[', '?', '2', '0', '0', '4', 'h' };
|
||||
|
||||
try testing.expectEqualSlices(u8, expected[0..], got);
|
||||
}
|
||||
|
||||
test "tmux decode malformed escape" {
|
||||
const testing = std.testing;
|
||||
|
||||
var input = [_]u8{ '\\', '0', 'x', '3' };
|
||||
const got = decodeEscapedOutput(input[0..]);
|
||||
const expected = [_]u8{ '?', 'x', '3' };
|
||||
|
||||
try testing.expectEqualSlices(u8, expected[0..], got);
|
||||
}
|
||||
|
||||
test "tmux output decodes escapes" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var c: Parser = .{ .buffer = .init(alloc) };
|
||||
defer c.deinit();
|
||||
for ("%output %42 \\033ktitle\\033\\134") |byte| try testing.expect(try c.put(byte) == null);
|
||||
var n = (try c.put('\n')).?;
|
||||
const expected = [_]u8{ 0x1b, 'k', 't', 'i', 't', 'l', 'e', 0x1b, '\\' };
|
||||
|
||||
try testing.expect(n == .output);
|
||||
try testing.expectEqual(42, n.output.pane_id);
|
||||
try testing.expectEqualStrings("\\033ktitle\\033\\134", n.output.data);
|
||||
try testing.expectEqualSlices(u8, expected[0..], n.output.decoded());
|
||||
}
|
||||
|
||||
test "tmux output decoded is idempotent" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var c: Parser = .{ .buffer = .init(alloc) };
|
||||
defer c.deinit();
|
||||
for ("%output %42 \\033ktitle\\033\\134") |byte| try testing.expect(try c.put(byte) == null);
|
||||
var n = (try c.put('\n')).?;
|
||||
|
||||
try testing.expect(n == .output);
|
||||
|
||||
const first = n.output.decoded();
|
||||
const second = n.output.decoded();
|
||||
|
||||
try testing.expectEqual(@intFromPtr(first.ptr), @intFromPtr(second.ptr));
|
||||
try testing.expectEqual(first.len, second.len);
|
||||
try testing.expectEqualSlices(u8, first, second);
|
||||
}
|
||||
|
||||
test "tmux session-changed" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
|
|
|||
|
|
@ -459,10 +459,7 @@ pub const Viewer = struct {
|
|||
command_consumed = true;
|
||||
},
|
||||
|
||||
.output => |out| self.receivedOutput(
|
||||
out.pane_id,
|
||||
out.data,
|
||||
) catch |err| {
|
||||
.output => |out| self.receivedOutput(out) catch |err| {
|
||||
log.warn(
|
||||
"failed to process output for pane id={}: {}",
|
||||
.{ out.pane_id, err },
|
||||
|
|
@ -1099,15 +1096,16 @@ pub const Viewer = struct {
|
|||
|
||||
fn receivedOutput(
|
||||
self: *Viewer,
|
||||
id: usize,
|
||||
data: []const u8,
|
||||
out: control.Notification.Output,
|
||||
) !void {
|
||||
const entry = self.panes.getEntry(id) orelse {
|
||||
log.info("received output for untracked pane id={}", .{id});
|
||||
const entry = self.panes.getEntry(out.pane_id) orelse {
|
||||
log.info("received output for untracked pane id={}", .{out.pane_id});
|
||||
return;
|
||||
};
|
||||
const pane: *Pane = entry.value_ptr;
|
||||
const t: *Terminal = &pane.terminal;
|
||||
var output_val = out;
|
||||
const data = output_val.decoded();
|
||||
|
||||
var stream = t.vtStream();
|
||||
defer stream.deinit();
|
||||
|
|
@ -1608,6 +1606,9 @@ test "initial flow" {
|
|||
var viewer = try Viewer.init(testing.allocator);
|
||||
defer viewer.deinit();
|
||||
|
||||
var new_output = "new\\134output".*;
|
||||
var ignored_output = "ignored".*;
|
||||
|
||||
try testViewer(&viewer, &.{
|
||||
.{ .input = .{ .tmux = .{ .block_end = "" } } },
|
||||
.{
|
||||
|
|
@ -1755,7 +1756,7 @@ test "initial flow" {
|
|||
.input = .{ .tmux = .{ .block_end = "" } },
|
||||
},
|
||||
.{
|
||||
.input = .{ .tmux = .{ .output = .{ .pane_id = 0, .data = "new output" } } },
|
||||
.input = .{ .tmux = .{ .output = .{ .pane_id = 0, .data = new_output[0..] } } },
|
||||
.check = (struct {
|
||||
fn check(v: *Viewer, actions: []const Viewer.Action) anyerror!void {
|
||||
try testing.expectEqual(0, actions.len);
|
||||
|
|
@ -1766,12 +1767,12 @@ test "initial flow" {
|
|||
.{ .active = .{} },
|
||||
);
|
||||
defer testing.allocator.free(str);
|
||||
try testing.expect(std.mem.containsAtLeast(u8, str, 1, "new output"));
|
||||
try testing.expect(std.mem.containsAtLeast(u8, str, 1, "new\\output"));
|
||||
}
|
||||
}).check,
|
||||
},
|
||||
.{
|
||||
.input = .{ .tmux = .{ .output = .{ .pane_id = 999, .data = "ignored" } } },
|
||||
.input = .{ .tmux = .{ .output = .{ .pane_id = 999, .data = ignored_output[0..] } } },
|
||||
.check = (struct {
|
||||
fn check(_: *Viewer, actions: []const Viewer.Action) anyerror!void {
|
||||
try testing.expectEqual(0, actions.len);
|
||||
|
|
|
|||
Loading…
Reference in New Issue