diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/terminal/apc.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig | 360 |
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" { |
