aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/apc.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/apc.zig
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff)
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/apc.zig')
-rw-r--r--vendor/libghostty-vt/src/terminal/apc.zig360
1 files changed, 302 insertions, 58 deletions
diff --git a/vendor/libghostty-vt/src/terminal/apc.zig b/vendor/libghostty-vt/src/terminal/apc.zig
index 121f906a..1ae70f5f 100644
--- a/vendor/libghostty-vt/src/terminal/apc.zig
+++ b/vendor/libghostty-vt/src/terminal/apc.zig
@@ -13,6 +13,10 @@ const log = std.log.scoped(.terminal_apc);
pub const Handler = struct {
state: State = .inactive,
+ /// Maximum content bytes retained for unsupported APC identifiers. Zero
+ /// drops and ignores unknown APC values.
+ unknown_max_bytes: usize = 0,
+
/// Maximum bytes each APC protocol can buffer. This is to prevent
/// malicious input from causing us to allocate too much memory.
/// If you want to be lazy and set a single value for all protocols,
@@ -23,8 +27,7 @@ pub const Handler = struct {
}),
/// Protocols recognized by this APC handler. When a protocol is absent,
- /// matching APC sequences are ignored so callers see the same behavior as
- /// an unsupported protocol: no command execution and no response.
+ /// matching APC sequences are ignored and are not reported as unknown.
enabled: std.EnumSet(Protocol) = .initFull(),
pub fn deinit(self: *Handler) void {
@@ -50,51 +53,74 @@ pub const Handler = struct {
// recognize it so there is no need to store the data in memory.
.ignore => return,
+ // Unsupported APC content is retained only when enabled.
+ .unknown => |*unknown| unknown.append(&.{byte}),
+
// We identify the APC command by the first byte.
.identify => |*id| id: {
// Kitty graphics is detected immediately on the `G` byte,
// since commands begin immediately after with no termination
// character after the 'G'.
- if (comptime build_options.kitty_graphics) {
- if (id.len == 0 and
- byte == 'G' and
- self.enabled.contains(.kitty))
- {
- self.state = .{ .kitty = .init(
- alloc,
- self.max_bytes.get(.kitty) orelse
- Protocol.defaultMaxBytes(.kitty),
- ) };
- break :id;
+ if (id.len == 0 and byte == 'G') {
+ if (comptime build_options.kitty_graphics) {
+ if (self.enabled.contains(.kitty)) {
+ self.state = .{ .kitty = .init(
+ alloc,
+ self.max_bytes.get(.kitty) orelse
+ Protocol.defaultMaxBytes(.kitty),
+ ) };
+ } else {
+ self.state = .ignore;
+ }
+ } else {
+ self.state = .ignore;
}
+ break :id;
}
// If we hit `;` then identify...
if (byte == ';') {
const str = id.buf[0..id.len];
- if (std.mem.eql(u8, str, "25a1") and
- self.enabled.contains(.glyph))
- {
- self.state = .{ .glyph = .init(
- alloc,
- self.max_bytes.get(.glyph) orelse
- Protocol.defaultMaxBytes(.glyph),
- ) };
+ if (std.mem.eql(u8, str, glyph.identifier)) {
+ if (comptime build_options.glyph_protocol) {
+ if (self.enabled.contains(.glyph)) {
+ self.state = .{ .glyph = .init(
+ alloc,
+ self.max_bytes.get(.glyph) orelse
+ Protocol.defaultMaxBytes(.glyph),
+ ) };
+ } else {
+ self.state = .ignore;
+ }
+ } else {
+ self.state = .ignore;
+ }
} else {
- self.state = .ignore;
+ self.beginUnknown(alloc, str, &.{byte});
}
break :id;
}
- // If we're out of space to buffer then we're done.
+ // If we're out of identification space, the identifier is
+ // unsupported. Preserve the buffered prefix before replacing
+ // the identify union state.
if (id.len >= id.buf.len) {
- self.state = .ignore;
+ self.beginUnknown(alloc, id.buf[0..id.len], &.{byte});
break :id;
}
+ const expected_idx: usize = id.len;
id.buf[id.len] = byte;
id.len += 1;
+
+ // Once the buffered input is no longer a prefix of a known
+ // protocol, it is an unsupported identifier.
+ if (self.unknown_max_bytes > 0 and
+ byte != glyph.identifier[expected_idx])
+ {
+ self.beginUnknown(alloc, id.buf[0..id.len], &.{});
+ }
},
.kitty => |*p| if (comptime build_options.kitty_graphics) {
@@ -105,12 +131,35 @@ pub const Handler = struct {
};
} else unreachable,
- .glyph => |*p| p.feed(byte) catch |err| {
- log.warn("glyph protocol error: {}", .{err});
- p.deinit();
- self.state = .ignore;
- },
+ .glyph => |*p| if (comptime build_options.glyph_protocol) {
+ p.feed(byte) catch |err| {
+ log.warn("glyph protocol error: {}", .{err});
+ p.deinit();
+ self.state = .ignore;
+ };
+ } else unreachable,
+ }
+ }
+
+ /// Transition from protocol identification to bounded unknown capture.
+ fn beginUnknown(
+ self: *Handler,
+ alloc: Allocator,
+ prefix: []const u8,
+ suffix: []const u8,
+ ) void {
+ const max_bytes = self.unknown_max_bytes;
+ if (max_bytes == 0) {
+ self.state = .ignore;
+ return;
}
+
+ // Build the replacement before overwriting identify because prefix
+ // points into that union field.
+ var unknown: UnknownBuilder = .init(alloc, max_bytes);
+ unknown.append(prefix);
+ unknown.append(suffix);
+ self.state = .{ .unknown = unknown };
}
/// Feed a slice of bytes to the handler. This is equivalent to
@@ -126,6 +175,12 @@ pub const Handler = struct {
// We're ignoring this APC command; drop the whole slice.
.ignore => return,
+ // We're capturing an unknown APC command, so store it.
+ .unknown => |*unknown| {
+ unknown.append(rem);
+ return;
+ },
+
// Identification consumes at most a few bytes; step
// through them one at a time until the state changes.
.identify => {
@@ -142,18 +197,20 @@ pub const Handler = struct {
return;
} else unreachable,
- .glyph => |*p| {
+ .glyph => |*p| if (comptime build_options.glyph_protocol) {
p.feedSlice(rem) catch |err| {
log.warn("glyph protocol error: {}", .{err});
p.deinit();
self.state = .ignore;
};
return;
- },
+ } else unreachable,
}
}
}
+ /// Complete the current APC. The caller owns a returned result and must
+ /// call `Command.deinit` with the allocator used while feeding the APC.
pub fn end(self: *Handler) ?Command {
defer {
self.state.deinit();
@@ -163,11 +220,12 @@ pub const Handler = struct {
return switch (self.state) {
.inactive => unreachable,
.ignore, .identify => null,
+ .unknown => |*unknown| .{ .unknown = unknown.toOwned() },
.kitty => |*p| kitty: {
if (comptime !build_options.kitty_graphics) unreachable;
// Use the same allocator that was used to create the parser.
- const alloc = p.arena.child_allocator;
+ const alloc = p.alloc;
const command = p.complete(alloc) catch |err| {
log.warn("kitty graphics protocol error: {}", .{err});
break :kitty null;
@@ -177,6 +235,8 @@ pub const Handler = struct {
},
.glyph => |*p| glyph_cmd: {
+ if (comptime !build_options.glyph_protocol) unreachable;
+
const command = p.complete(p.alloc) catch |err| {
log.warn("glyph protocol error: {}", .{err});
break :glyph_cmd null;
@@ -207,7 +267,7 @@ pub const State = union(enum) {
///
identify: struct {
len: u3 = 0,
- buf: [4]u8 = undefined,
+ buf: [glyph.identifier.len]u8 = undefined,
},
/// Kitty graphics protocol
@@ -217,12 +277,24 @@ pub const State = union(enum) {
void,
/// Glyph protocol
- glyph: glyph.CommandParser,
+ glyph: if (build_options.glyph_protocol)
+ glyph.CommandParser
+ else
+ void,
+
+ /// An unsupported APC retained for the optional unknown callback.
+ /// Keep this after recognized protocol states so their tag values and
+ /// generated dispatch stay stable when unknown capture is unused.
+ unknown: UnknownBuilder,
pub fn deinit(self: *State) void {
switch (self.*) {
.inactive, .ignore, .identify => {},
- .glyph => |*v| v.deinit(),
+ .unknown => |*v| v.deinit(),
+ .glyph => |*v| if (comptime build_options.glyph_protocol)
+ v.deinit()
+ else
+ unreachable,
.kitty => |*v| if (comptime build_options.kitty_graphics)
v.deinit()
else
@@ -231,6 +303,95 @@ pub const State = union(enum) {
}
};
+/// UnknownBuilder is responsible for accumulating bytes for an
+/// unidentified APC command if unknown capture is enabled.
+const UnknownBuilder = struct {
+ data: std.ArrayList(u8) = .empty,
+ alloc: Allocator,
+ max_bytes: usize,
+ truncated: bool = false,
+
+ fn init(alloc: Allocator, max_bytes: usize) UnknownBuilder {
+ return .{
+ .alloc = alloc,
+ .max_bytes = max_bytes,
+ };
+ }
+
+ fn deinit(self: *UnknownBuilder) void {
+ self.data.deinit(self.alloc);
+ self.data = .empty;
+ }
+
+ // Append some bytes to the unknown capture. This flags as truncated
+ // if allocation fails or we reach our byte limit, therefore
+ // it can't fail.
+ fn append(self: *UnknownBuilder, bytes: []const u8) void {
+ if (bytes.len == 0) return;
+ const current = self.data.items.len;
+
+ // Determine how many bytes we can store in this append and
+ // if it is less than our input, then we have to note we're
+ // truncating.
+ const retained = @min(bytes.len, self.max_bytes -| current);
+ if (retained < bytes.len) self.truncated = true;
+
+ // If we require more bytes than our capacity allows then we
+ // need to grow.
+ const required = current + retained;
+ if (required > self.data.capacity) {
+ const capacity = @min(
+ self.max_bytes,
+ @max(required, @max(self.data.capacity *| 2, 1)),
+ );
+ self.data.ensureTotalCapacityPrecise(
+ self.alloc,
+ capacity,
+ ) catch {
+ self.truncated = true;
+ return;
+ };
+ }
+
+ self.data.appendSliceAssumeCapacity(bytes[0..retained]);
+ }
+
+ /// Convert the current capture state to an Unknown where allocator
+ /// ownership shifts to Unknown. Removes any accumulated unknown
+ /// capture in this struct.
+ ///
+ /// This can't fail because if there is an allocator issue we return
+ /// an empty truncate-flagged Unknown.
+ fn toOwned(self: *UnknownBuilder) Unknown {
+ // toOwnedSlice allows us to reuse data but this makes error
+ // handling a little simpler.
+ const content = self.data.toOwnedSlice(self.alloc) catch {
+ self.data.deinit(self.alloc);
+ self.data = .empty;
+ return .{
+ .content = self.data.items,
+ .truncated = true,
+ };
+ };
+
+ return .{
+ .content = content,
+ .truncated = self.truncated,
+ };
+ }
+};
+
+/// An unsupported APC returned by `Handler.end`.
+pub const Unknown = struct {
+ content: []u8,
+ truncated: bool,
+
+ pub fn deinit(self: *Unknown, alloc: Allocator) void {
+ if (self.content.len > 0) alloc.free(self.content);
+ self.* = undefined;
+ }
+};
+
/// Possible APC command types.
pub const Protocol = enum {
kitty,
@@ -248,16 +409,32 @@ pub const Protocol = enum {
.glyph => 1 * 1024 * 1024,
};
}
+
+ /// Return the largest default buffer limit across every APC protocol.
+ /// Consumers that must retain any unfinished APC can derive their limit
+ /// here instead of duplicating a particular protocol's current default.
+ pub fn maxDefaultBytes() usize {
+ var result: usize = 0;
+ for (std.enums.values(Protocol)) |protocol| {
+ result = @max(result, protocol.defaultMaxBytes());
+ }
+ return result;
+ }
};
-/// Possible APC commands.
-pub const Command = union(Protocol) {
+/// A recognized or unsupported APC command.
+pub const Command = union(enum) {
kitty: if (build_options.kitty_graphics)
kitty_gfx.Command
else
void,
- glyph: glyph.Request,
+ glyph: if (build_options.glyph_protocol)
+ glyph.Request
+ else
+ void,
+
+ unknown: Unknown,
pub fn deinit(self: *Command, alloc: Allocator) void {
switch (self.*) {
@@ -266,7 +443,12 @@ pub const Command = union(Protocol) {
else
unreachable,
- .glyph => |*v| v.deinit(alloc),
+ .glyph => |*v| if (comptime build_options.glyph_protocol)
+ v.deinit(alloc)
+ else
+ unreachable,
+
+ .unknown => |*v| v.deinit(alloc),
}
}
};
@@ -281,6 +463,64 @@ test "unknown APC command" {
try testing.expect(h.end() == null);
}
+test "capture unknown APC command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{ .unknown_max_bytes = 5 };
+ defer h.deinit();
+ h.start();
+ h.feedSlice(alloc, "abcd;payload");
+
+ var result = h.end().?;
+ defer result.deinit(alloc);
+ const unknown = &result.unknown;
+ try testing.expectEqualStrings("abcd;", unknown.content);
+ try testing.expect(unknown.truncated);
+}
+
+test "capture short unknown APC command" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{ .unknown_max_bytes = 16 };
+ defer h.deinit();
+ h.start();
+ h.feed(alloc, 'X');
+
+ var result = h.end().?;
+ const unknown = &result.unknown;
+ try testing.expectEqualStrings("X", unknown.content);
+ try testing.expect(!unknown.truncated);
+ result.deinit(alloc);
+
+ h.unknown_max_bytes = 1;
+ h.start();
+ h.feedSlice(alloc, "XYZ");
+ result = h.end().?;
+ const truncated = &result.unknown;
+ try testing.expectEqualStrings("X", truncated.content);
+ try testing.expect(truncated.truncated);
+ result.deinit(alloc);
+}
+
+test "disabled known APC protocol is not unknown" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var h: Handler = .{ .unknown_max_bytes = 64 };
+ defer h.deinit();
+ h.enable(.glyph, false);
+ h.start();
+ h.feedSlice(alloc, "25a1;q;cp=E0A0");
+ try testing.expect(h.end() == null);
+
+ // An incomplete known protocol identifier is malformed, not unknown.
+ h.start();
+ h.feedSlice(alloc, "25a");
+ try testing.expect(h.end() == null);
+}
+
test "garbage Kitty command" {
if (comptime !build_options.kitty_graphics) return error.SkipZigTest;
@@ -363,9 +603,9 @@ test "valid Kitty command" {
const input = "Gf=24,s=10,v=20,hello=world";
for (input) |c| h.feed(alloc, c);
- var cmd = h.end().?;
- defer cmd.deinit(alloc);
- try testing.expect(cmd == .kitty);
+ var result = h.end().?;
+ defer result.deinit(alloc);
+ try testing.expect(result == .kitty);
}
test "identify with unrecognized command" {
@@ -418,6 +658,8 @@ test "garbage glyph command" {
}
test "valid glyph command" {
+ if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
@@ -425,10 +667,10 @@ test "valid glyph command" {
h.start();
for ("25a1;q;cp=E0A0") |c| h.feed(alloc, c);
- var cmd = h.end().?;
- defer cmd.deinit(alloc);
- try testing.expect(cmd == .glyph);
- try testing.expect(cmd.glyph == .query);
+ var result = h.end().?;
+ defer result.deinit(alloc);
+ try testing.expect(result == .glyph);
+ try testing.expect(result.glyph == .query);
}
test "feedSlice valid Kitty command" {
@@ -441,12 +683,12 @@ test "feedSlice valid Kitty command" {
h.start();
h.feedSlice(alloc, "Gf=24,s=10,v=20;aGVsbG8=");
- var cmd = h.end().?;
- defer cmd.deinit(alloc);
- try testing.expect(cmd == .kitty);
+ var result = h.end().?;
+ defer result.deinit(alloc);
+ try testing.expect(result == .kitty);
// The payload is base64-decoded by the parser on completion.
- try testing.expectEqualStrings("hello", cmd.kitty.data);
+ try testing.expectEqualStrings("hello", result.kitty.data);
}
test "feedSlice identify split across slices" {
@@ -461,12 +703,12 @@ test "feedSlice identify split across slices" {
h.feedSlice(alloc, "f=24,s=10,");
h.feedSlice(alloc, "v=20;aGVsbG8=");
- var cmd = h.end().?;
- defer cmd.deinit(alloc);
- try testing.expect(cmd == .kitty);
+ var result = h.end().?;
+ defer result.deinit(alloc);
+ try testing.expect(result == .kitty);
// The payload is base64-decoded by the parser on completion.
- try testing.expectEqualStrings("hello", cmd.kitty.data);
+ try testing.expectEqualStrings("hello", result.kitty.data);
}
test "feedSlice unknown APC command is ignored" {
@@ -482,6 +724,8 @@ test "feedSlice unknown APC command is ignored" {
}
test "feedSlice valid glyph command" {
+ if (comptime !build_options.glyph_protocol) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
@@ -489,10 +733,10 @@ test "feedSlice valid glyph command" {
h.start();
h.feedSlice(alloc, "25a1;q;cp=E0A0");
- var cmd = h.end().?;
- defer cmd.deinit(alloc);
- try testing.expect(cmd == .glyph);
- try testing.expect(cmd.glyph == .query);
+ var result = h.end().?;
+ defer result.deinit(alloc);
+ try testing.expect(result == .glyph);
+ try testing.expect(result.glyph == .query);
}
test "feedSlice kitty max bytes exceeded" {