ghostty/src/terminal/dcs.zig
2023-12-15 08:05:32 -06:00

310 lines
8.7 KiB
Zig

const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminal = @import("main.zig");
const DCS = terminal.DCS;
const log = std.log.scoped(.terminal_dcs);
/// DCS command handler. This should be hooked into a terminal.Stream handler.
/// The hook/put/unhook functions are meant to be called from the
/// terminal.stream dcsHook, dcsPut, and dcsUnhook functions, respectively.
pub const Handler = struct {
state: State = .{ .inactive = {} },
/// Maximum bytes any DCS command can take. This is to prevent
/// malicious input from causing us to allocate too much memory.
/// This is arbitrarily set to 1MB today, increase if needed.
max_bytes: usize = 1024 * 1024,
pub fn deinit(self: *Handler) void {
self.discard();
}
pub fn hook(self: *Handler, alloc: Allocator, dcs: DCS) void {
assert(self.state == .inactive);
self.state = if (tryHook(alloc, dcs)) |state_| state: {
if (state_) |state| break :state state else {
log.info("unknown DCS hook: {}", .{dcs});
break :state .{ .ignore = {} };
}
} else |err| state: {
log.info(
"error initializing DCS hook, will ignore hook err={}",
.{err},
);
break :state .{ .ignore = {} };
};
}
fn tryHook(alloc: Allocator, dcs: DCS) !?State {
return switch (dcs.intermediates.len) {
1 => switch (dcs.intermediates[0]) {
'+' => switch (dcs.final) {
// XTGETTCAP
// https://github.com/mitchellh/ghostty/issues/517
'q' => .{
.xtgettcap = try std.ArrayList(u8).initCapacity(
alloc,
128, // Arbitrary choice
),
},
else => null,
},
'$' => switch (dcs.final) {
// DECRQSS
'q' => .{
.decrqss = .{},
},
else => null,
},
else => null,
},
else => null,
};
}
pub fn put(self: *Handler, byte: u8) void {
self.tryPut(byte) catch |err| {
// On error we just discard our state and ignore the rest
log.info("error putting byte into DCS handler err={}", .{err});
self.discard();
self.state = .{ .ignore = {} };
};
}
fn tryPut(self: *Handler, byte: u8) !void {
switch (self.state) {
.inactive,
.ignore,
=> {},
.xtgettcap => |*list| {
if (list.items.len >= self.max_bytes) {
return error.OutOfMemory;
}
try list.append(byte);
},
.decrqss => |*buffer| {
if (buffer.len >= buffer.data.len) {
return error.OutOfMemory;
}
buffer.data[buffer.len] = byte;
buffer.len += 1;
},
}
}
pub fn unhook(self: *Handler) ?Command {
defer self.state = .{ .inactive = {} };
return switch (self.state) {
.inactive,
.ignore,
=> null,
.xtgettcap => |list| .{ .xtgettcap = .{ .data = list } },
.decrqss => |buffer| .{ .decrqss = switch (buffer.len) {
0 => .none,
1 => switch (buffer.data[0]) {
'm' => .sgr,
'r' => .decstbm,
's' => .decslrm,
else => .none,
},
2 => switch (buffer.data[0]) {
' ' => switch (buffer.data[1]) {
'q' => .decscusr,
else => .none,
},
else => .none,
},
else => unreachable,
} },
};
}
fn discard(self: *Handler) void {
switch (self.state) {
.inactive,
.ignore,
=> {},
.xtgettcap => |*list| list.deinit(),
.decrqss => {},
}
self.state = .{ .inactive = {} };
}
};
pub const Command = union(enum) {
/// XTGETTCAP
xtgettcap: XTGETTCAP,
/// DECRQSS
decrqss: DECRQSS,
pub fn deinit(self: Command) void {
switch (self) {
.xtgettcap => |*v| {
v.data.deinit();
},
.decrqss => {},
}
}
pub const XTGETTCAP = struct {
data: std.ArrayList(u8),
i: usize = 0,
/// Returns the next terminfo key being requested and null
/// when there are no more keys. The returned value is NOT hex-decoded
/// because we expect to use a comptime lookup table.
pub fn next(self: *XTGETTCAP) ?[]const u8 {
if (self.i >= self.data.items.len) return null;
var rem = self.data.items[self.i..];
const idx = std.mem.indexOf(u8, rem, ";") orelse rem.len;
// Note that if we're at the end, idx + 1 is len + 1 so we're over
// the end but that's okay because our check above is >= so we'll
// never read.
self.i += idx + 1;
return rem[0..idx];
}
};
/// Supported DECRQSS settings
pub const DECRQSS = enum {
none,
sgr,
decscusr,
decstbm,
decslrm,
};
};
const State = union(enum) {
/// We're not in a DCS state at the moment.
inactive: void,
/// We're hooked, but its an unknown DCS command or one that went
/// invalid due to some bad input, so we're ignoring the rest.
ignore: void,
/// XTGETTCAP
xtgettcap: std.ArrayList(u8),
/// DECRQSS
decrqss: struct {
data: [2]u8 = undefined,
len: u2 = 0,
},
};
test "unknown DCS command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .final = 'A' });
try testing.expect(h.state == .ignore);
try testing.expect(h.unhook() == null);
try testing.expect(h.state == .inactive);
}
test "XTGETTCAP command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
for ("536D756C78") |byte| h.put(byte);
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .xtgettcap);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null);
}
test "XTGETTCAP command multiple keys" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
for ("536D756C78;536D756C78") |byte| h.put(byte);
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .xtgettcap);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null);
}
test "XTGETTCAP command invalid data" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "+", .final = 'q' });
for ("who;536D756C78") |byte| h.put(byte);
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .xtgettcap);
try testing.expectEqualStrings("who", cmd.xtgettcap.next().?);
try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?);
try testing.expect(cmd.xtgettcap.next() == null);
}
test "DECRQSS command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('m');
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .decrqss);
try testing.expect(cmd.decrqss == .sgr);
}
test "DECRQSS invalid command" {
const testing = std.testing;
const alloc = testing.allocator;
var h: Handler = .{};
defer h.deinit();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('z');
var cmd = h.unhook().?;
defer cmd.deinit();
try testing.expect(cmd == .decrqss);
try testing.expect(cmd.decrqss == .none);
h.discard();
h.hook(alloc, .{ .intermediates = "$", .final = 'q' });
h.put('"');
h.put(' ');
h.put('q');
try testing.expect(h.unhook() == null);
}