diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
| commit | dcfe11a7588fcd42471159453810abc9b7720a57 (patch) | |
| tree | c32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig | |
| parent | 2dfed860103c4e4505ac7f25def9fe37da1e1e6a (diff) | |
build: update libghostty-vt vendor
vendor ghostty 063ac3e and remove the musl libc++ link workaround.
expose the new selection formatter and hyperlink uri apis for follow-up rendering work.
add manual artifact validation for static linux builds.
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig | 271 |
1 files changed, 240 insertions, 31 deletions
diff --git a/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig b/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig index d2877cfc..8243a632 100644 --- a/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig +++ b/vendor/libghostty-vt/src/terminal/kitty/graphics_image.zig @@ -8,7 +8,7 @@ const posix = std.posix; const fastmem = @import("../../fastmem.zig"); const command = @import("graphics_command.zig"); const PageList = @import("../PageList.zig"); -const wuffs = @import("wuffs"); +const sys = @import("../sys.zig"); const temp_dir = struct { const TempDir = @import("../../os/TempDir.zig"); @@ -44,10 +44,38 @@ pub const LoadingImage = struct { /// used if q isn't set on subsequent chunks. quiet: command.Command.Quiet, + /// The limits of the Kitty Graphics protocol we should allow. + /// + /// This can be used to restrict the type of images and other + /// parameters for resource or security reasons. Note that depending + /// on how libghostty is compiled, some of these may be fully unsupported + /// and ignored (e.g. "file" on wasm32-freestanding). + pub const Limits = packed struct { + file: bool, + temporary_file: bool, + shared_memory: bool, + + pub const all: Limits = .{ + .file = true, + .temporary_file = true, + .shared_memory = true, + }; + + pub const direct: Limits = .{ + .file = false, + .temporary_file = false, + .shared_memory = false, + }; + }; + /// Initialize a chunked immage from the first image transmission. /// If this is a multi-chunk image, this should only be the FIRST /// chunk. - pub fn init(alloc: Allocator, cmd: *const command.Command) !LoadingImage { + pub fn init( + alloc: Allocator, + cmd: *const command.Command, + limits: Limits, + ) !LoadingImage { // Build our initial image from the properties sent via the control. // These can be overwritten by the data loading process. For example, // PNG loading sets the width/height from the data. @@ -72,6 +100,26 @@ pub const LoadingImage = struct { return result; } + // Verify our capabilities and limits allow this. + { + // Special case if we don't support decoding PNGs and the format + // is a PNG we can save a lot of memory/effort buffering the + // data but failing up front. + if (t.format == .png and + sys.decode_png == null) + { + return error.UnsupportedMedium; + } + + // Verify the medium is allowed + switch (t.medium) { + .direct => unreachable, + .file => if (!limits.file) return error.UnsupportedMedium, + .temporary_file => if (!limits.temporary_file) return error.UnsupportedMedium, + .shared_memory => if (!limits.shared_memory) return error.UnsupportedMedium, + } + } + // Otherwise, the payload data is guaranteed to be a path. if (comptime builtin.os.tag != .windows) { @@ -111,8 +159,9 @@ pub const LoadingImage = struct { t: command.Transmission, path: []const u8, ) !void { + // android does not support POSIX shared memory. // windows is currently unsupported, does it support shm? - if (comptime builtin.target.os.tag == .windows) { + if (comptime builtin.abi.isAndroid() or builtin.target.os.tag == .windows) { return error.UnsupportedMedium; } @@ -154,8 +203,8 @@ pub const LoadingImage = struct { .png => stat_size, // For these formats we have a size we must have. - .gray, .gray_alpha, .rgb, .rgba => |f| size: { - const bpp = f.bpp(); + .gray, .gray_alpha, .rgb, .rgba => size: { + const bpp = command.Transmission.formatBpp(self.image.format); break :size self.image.width * self.image.height * bpp; }, }; @@ -281,17 +330,16 @@ pub const LoadingImage = struct { fn isPathInTempDir(path: []const u8) bool { if (std.mem.startsWith(u8, path, "/tmp")) return true; if (std.mem.startsWith(u8, path, "/dev/shm")) return true; - if (temp_dir.allocTmpDir(std.heap.page_allocator)) |dir| { - defer temp_dir.freeTmpDir(std.heap.page_allocator, dir); - if (std.mem.startsWith(u8, path, dir)) return true; - - // The temporary dir is sometimes a symlink. On macOS for - // example /tmp is /private/var/... - var buf: [std.fs.max_path_bytes]u8 = undefined; - if (posix.realpath(dir, &buf)) |real_dir| { - if (std.mem.startsWith(u8, path, real_dir)) return true; - } else |_| {} - } + const dir = temp_dir.allocTmpDir(std.heap.page_allocator) catch return false; + defer temp_dir.freeTmpDir(std.heap.page_allocator, dir); + if (std.mem.startsWith(u8, path, dir)) return true; + + // The temporary dir is sometimes a symlink. On macOS for + // example /tmp is /private/var/... + var buf: [std.fs.max_path_bytes]u8 = undefined; + if (posix.realpath(dir, &buf)) |real_dir| { + if (std.mem.startsWith(u8, path, real_dir)) return true; + } else |_| {} return false; } @@ -342,7 +390,7 @@ pub const LoadingImage = struct { if (img.width > max_dimension or img.height > max_dimension) return error.DimensionsTooLarge; // Data length must be what we expect - const bpp = img.format.bpp(); + const bpp = command.Transmission.formatBpp(img.format); const expected_len = img.width * img.height * bpp; const actual_len = self.data.items.len; if (actual_len != expected_len) { @@ -426,13 +474,14 @@ pub const LoadingImage = struct { fn decodePng(self: *LoadingImage, alloc: Allocator) !void { assert(self.image.format == .png); - const result = wuffs.png.decode( + const decode_png_fn = sys.decode_png orelse + return error.UnsupportedFormat; + const result = decode_png_fn( alloc, self.data.items, ) catch |err| switch (err) { - error.WuffsError => return error.InvalidData, + error.InvalidData => return error.InvalidData, error.OutOfMemory => return error.OutOfMemory, - error.Overflow => return error.InvalidData, }; defer alloc.free(result.data); @@ -522,7 +571,7 @@ test "image load with invalid RGB data" { .data = try alloc.dupe(u8, "AAAA"), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); } @@ -540,7 +589,7 @@ test "image load with image too wide" { .data = try alloc.dupe(u8, "AAAA"), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc)); } @@ -559,7 +608,7 @@ test "image load with image too tall" { .data = try alloc.dupe(u8, "AAAA"), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); try testing.expectError(error.DimensionsTooLarge, loading.complete(alloc)); } @@ -583,7 +632,7 @@ test "image load: rgb, zlib compressed, direct" { ), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); var img = try loading.complete(alloc); defer img.deinit(alloc); @@ -611,7 +660,7 @@ test "image load: rgb, not compressed, direct" { ), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); var img = try loading.complete(alloc); defer img.deinit(alloc); @@ -640,7 +689,7 @@ test "image load: rgb, zlib compressed, direct, chunked" { .data = try alloc.dupe(u8, data[0..1024]), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); // Read our remaining chunks @@ -676,7 +725,7 @@ test "image load: rgb, zlib compressed, direct, chunked with zero initial chunk" } }, }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .direct); defer loading.deinit(alloc); // Read our remaining chunks @@ -720,7 +769,7 @@ test "image load: temporary file without correct path" { .data = try alloc.dupe(u8, path), }; defer cmd.deinit(alloc); - try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd)); + try testing.expectError(error.TemporaryFileNotNamedCorrectly, LoadingImage.init(alloc, &cmd, .all)); // Temporary file should still be there try tmp_dir.dir.access(path, .{}); @@ -753,7 +802,7 @@ test "image load: rgb, not compressed, temporary file" { .data = try alloc.dupe(u8, path), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .all); defer loading.deinit(alloc); var img = try loading.complete(alloc); defer img.deinit(alloc); @@ -790,7 +839,7 @@ test "image load: rgb, not compressed, regular file" { .data = try alloc.dupe(u8, path), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .all); defer loading.deinit(alloc); var img = try loading.complete(alloc); defer img.deinit(alloc); @@ -799,6 +848,8 @@ test "image load: rgb, not compressed, regular file" { } test "image load: png, not compressed, regular file" { + if (sys.decode_png == null) return error.SkipZigTest; + const testing = std.testing; const alloc = testing.allocator; @@ -825,7 +876,7 @@ test "image load: png, not compressed, regular file" { .data = try alloc.dupe(u8, path), }; defer cmd.deinit(alloc); - var loading = try LoadingImage.init(alloc, &cmd); + var loading = try LoadingImage.init(alloc, &cmd, .all); defer loading.deinit(alloc); var img = try loading.complete(alloc); defer img.deinit(alloc); @@ -833,3 +884,161 @@ test "image load: png, not compressed, regular file" { try testing.expect(img.format == .rgba); try tmp_dir.dir.access(path, .{}); } + +test "limits: direct medium always allowed" { + const testing = std.testing; + const alloc = testing.allocator; + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .direct, + .width = 1, + .height = 1, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, "AAAA"), + }; + defer cmd.deinit(alloc); + + // Direct medium should work even with the most restrictive limits + var loading = try LoadingImage.init(alloc, &cmd, .direct); + defer loading.deinit(alloc); +} + +test "limits: file medium blocked by limits" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try temp_dir.TempDir.init(); + defer tmp_dir.deinit(); + const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data"); + try tmp_dir.dir.writeFile(.{ + .sub_path = "image.data", + .data = data, + }); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = try tmp_dir.dir.realpath("image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, path), + }; + defer cmd.deinit(alloc); + try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .direct)); +} + +test "limits: file medium allowed by limits" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try temp_dir.TempDir.init(); + defer tmp_dir.deinit(); + const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data"); + try tmp_dir.dir.writeFile(.{ + .sub_path = "image.data", + .data = data, + }); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = try tmp_dir.dir.realpath("image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, path), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd, .{ + .file = true, + .temporary_file = false, + .shared_memory = false, + }); + defer loading.deinit(alloc); +} + +test "limits: temporary file medium blocked by limits" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try temp_dir.TempDir.init(); + defer tmp_dir.deinit(); + const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data"); + try tmp_dir.dir.writeFile(.{ + .sub_path = "tty-graphics-protocol-image.data", + .data = data, + }); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .temporary_file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, path), + }; + defer cmd.deinit(alloc); + try testing.expectError(error.UnsupportedMedium, LoadingImage.init(alloc, &cmd, .{ + .file = true, + .temporary_file = false, + .shared_memory = true, + })); + + // File should still exist since we blocked before reading + try tmp_dir.dir.access("tty-graphics-protocol-image.data", .{}); +} + +test "limits: temporary file medium allowed by limits" { + const testing = std.testing; + const alloc = testing.allocator; + + var tmp_dir = try temp_dir.TempDir.init(); + defer tmp_dir.deinit(); + const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data"); + try tmp_dir.dir.writeFile(.{ + .sub_path = "tty-graphics-protocol-image.data", + .data = data, + }); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = try tmp_dir.dir.realpath("tty-graphics-protocol-image.data", &buf); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .temporary_file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, path), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(alloc, &cmd, .{ + .file = false, + .temporary_file = true, + .shared_memory = false, + }); + defer loading.deinit(alloc); +} |
