From b988efcfe584e88a3d0330e2c17c386ffa419d72 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 22 Jul 2026 08:04:27 -0700 Subject: [PATCH] fix some 0.16 translation regressions --- include/ghostty/vt/terminal.h | 16 ++++---- src/Command.zig | 4 +- src/apprt/gtk/class/surface.zig | 5 ++- src/font/opentype/svg.zig | 11 ++++++ src/lib/compat/file.zig | 56 ++++++++++++++++++++------- src/terminal/Terminal.zig | 29 ++++++++------ src/terminal/kitty/graphics_image.zig | 44 ++++++++++++++++++++- src/termio/Exec.zig | 6 ++- src/termio/Termio.zig | 8 ++-- 9 files changed, 136 insertions(+), 43 deletions(-) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 9ae4e6c45..8470607b3 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -785,12 +785,13 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_FILE = 16, /** - * Enable or disable Kitty image loading via the temporary file medium. + * Enable Kitty image loading via the temporary file medium, restricted to + * the provided directory. The string data is copied into the terminal. * - * A NULL value pointer is a no-op. Has no effect when Kitty graphics - * are disabled at build time. + * A NULL value pointer disables the temporary file medium. Has no effect + * when Kitty graphics are disabled at build time. * - * Input type: bool* + * Input type: GhosttyString* */ GHOSTTY_TERMINAL_OPT_KITTY_IMAGE_MEDIUM_TEMP_FILE = 17, @@ -1139,12 +1140,13 @@ typedef enum GHOSTTY_ENUM_TYPED { GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_FILE = 27, /** - * Whether the temporary file medium is enabled for Kitty image loading - * on the active screen. + * The directory allowed for Kitty image loading via the temporary file + * medium on the active screen. The string is empty when the medium is + * disabled. * * Returns GHOSTTY_NO_VALUE when Kitty graphics are disabled at build time. * - * Output type: bool * + * Output type: GhosttyString * */ GHOSTTY_TERMINAL_DATA_KITTY_IMAGE_MEDIUM_TEMP_FILE = 28, diff --git a/src/Command.zig b/src/Command.zig index ec363e770..3fc498a9a 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -236,12 +236,12 @@ fn startPosix(self: *Command, arena: Allocator) !void { var path_expanded_buf: [std.fs.max_path_bytes]u8 = undefined; const PATH = global.environ().getPosix("PATH") orelse "/usr/local/bin:/bin/:/usr/bin"; var it = std.mem.tokenizeScalar(u8, PATH, ':'); - var err: posix.system.E = undefined; + var err: posix.system.E = .NOENT; var seen_eacces = false; while (it.next()) |search_path| { const path_len = search_path.len + file_slice.len + 1; - if (path_expanded_buf.len < path_len + 1) return error.NameTooLong; + if (path_expanded_buf.len < path_len + 1) break :execve .NAMETOOLONG; @memcpy(path_expanded_buf[0..search_path.len], search_path); path_expanded_buf[search_path.len] = '/'; @memcpy(path_expanded_buf[search_path.len + 1 ..][0..file_slice.len], file_slice); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 14bae2f6a..3a950a6aa 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -1593,7 +1593,10 @@ pub const Surface = extern struct { pub fn defaultTermioEnv(self: *Self) !std.process.Environ.Map { const app = Application.default(); const alloc = app.allocator(); - var env = try global.environMap(); + var env = if (internal_os.isFlatpak()) + std.process.Environ.Map.init(alloc) + else + try global.environMap(); errdefer env.deinit(); if (app.savedLanguage()) |language| { diff --git a/src/font/opentype/svg.zig b/src/font/opentype/svg.zig index abc782460..9115fe62f 100644 --- a/src/font/opentype/svg.zig +++ b/src/font/opentype/svg.zig @@ -34,6 +34,7 @@ pub const SVG = struct { const offset = try compat_reader.readerInt(&reader, u32, .big); // Seek to the offset to get our document list + if (offset > data.len) return error.EndOfStream; reader.seek = offset; // Get our document records along with the start/end glyph range. @@ -108,3 +109,13 @@ test "SVG" { try testing.expectEqual(11482, svg.end_glyph_id); try testing.expect(svg.hasGlyph(11482)); } + +test "SVG document list offset past end" { + const testing = std.testing; + const table = [_]u8{ + 0, 0, // Version + 0, 0, 0, 7, // Document list offset, one byte past end + }; + + try testing.expectError(error.EndOfStream, SVG.init(&table)); +} diff --git a/src/lib/compat/file.zig b/src/lib/compat/file.zig index 10236794a..efb31a525 100644 --- a/src/lib/compat/file.zig +++ b/src/lib/compat/file.zig @@ -4,27 +4,53 @@ const builtin = @import("builtin"); const std = @import("std"); const global = @import("../../global.zig"); -pub const ReadToEndAllocError = error{ FileTooBig, BytesReadMismatch } || - std.Io.File.StatError || +pub const ReadToEndAllocError = error{FileTooBig} || std.Io.File.ReadStreamingError || std.mem.Allocator.Error; -/// This is a much simpler `readToEndAlloc` that just pre-allocates the memory -/// for the file ahead of time, and errors out if the size is larger than -/// `max_bytes`. +/// Read the file from its current position through end-of-stream, returning +/// `error.FileTooBig` if the result exceeds `max_bytes`. /// /// Caller owns the memory. pub fn readToEndAlloc(file: std.Io.File, alloc: std.mem.Allocator, max_bytes: usize) ReadToEndAllocError![]u8 { - const size = (try file.stat(global.io())).size; - if (size > max_bytes) { - return error.FileTooBig; + var read_buf: [4096]u8 = undefined; + var result: std.ArrayList(u8) = .empty; + defer result.deinit(alloc); + + while (true) { + const n = file.readStreaming(global.io(), &.{&read_buf}) catch |err| switch (err) { + error.EndOfStream => break, + else => |e| return e, + }; + if (n == 0) continue; + if (n > max_bytes - result.items.len) return error.FileTooBig; + try result.appendSlice(alloc, read_buf[0..n]); } - const buf = try alloc.alloc(u8, size); - errdefer alloc.free(buf); - if (try file.readStreaming(global.io(), &.{buf}) != size) { - return error.BytesReadMismatch; - } - - return buf; + return result.toOwnedSlice(alloc); +} + +test "readToEndAlloc reads through EOF and permits exact limit" { + const testing = std.testing; + const contents = "hello, world"; + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + try tmp_dir.dir.writeFile(testing.io, .{ + .sub_path = "data", + .data = contents, + }); + + const file = try tmp_dir.dir.openFile(testing.io, "data", .{}); + defer file.close(testing.io); + const result = try readToEndAlloc(file, testing.allocator, contents.len); + defer testing.allocator.free(result); + try testing.expectEqualStrings(contents, result); + + const too_big_file = try tmp_dir.dir.openFile(testing.io, "data", .{}); + defer too_big_file.close(testing.io); + try testing.expectError( + error.FileTooBig, + readToEndAlloc(too_big_file, testing.allocator, contents.len - 1), + ); } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 6ec768f4f..52c066ebc 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -3816,18 +3816,23 @@ pub fn resize( // we just go back to the primary screen. Not great, but best // we can do. tw.check(.alternate_screen_init) catch break :alt; - const replacement = self.screens.getInit(alt.io, alloc, .alternate, .{ - .cols = opts.cols, - .rows = opts.rows, - .max_scrollback = 0, - .kitty_image_storage_limit = if (comptime build_options.kitty_graphics) - primary.kitty_images.total_limit - else - 0, - .kitty_image_loading_limits = if (comptime build_options.kitty_graphics) - primary.kitty_images.image_limits - else {}, - }) catch |init_err| { + const replacement = self.screens.getInit( + self.io(), + alloc, + .alternate, + .{ + .cols = opts.cols, + .rows = opts.rows, + .max_scrollback = 0, + .kitty_image_storage_limit = if (comptime build_options.kitty_graphics) + primary.kitty_images.total_limit + else + 0, + .kitty_image_loading_limits = if (comptime build_options.kitty_graphics) + primary.kitty_images.image_limits + else {}, + }, + ) catch |init_err| { log.warn( "alternate screen replacement failed, falling back to primary err={}", .{init_err}, diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index 5fafa6995..ef584c09a 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -150,7 +150,7 @@ pub const LoadingImage = struct { const path = switch (t.medium) { .direct => unreachable, // handled above .file, .temporary_file => path: { - const len = std.Io.Dir.realPathFileAbsolute( + const len = std.Io.Dir.cwd().realPathFile( io, cmd.data, &abs_buf, @@ -364,7 +364,7 @@ pub const LoadingImage = struct { // The temporary dir is sometimes a symlink. On macOS for // example /tmp is /private/var/... var buf: [std.fs.max_path_bytes]u8 = undefined; - const real_dir = buf[0 .. std.Io.Dir.realPathFileAbsolute( + const real_dir = buf[0 .. std.Io.Dir.cwd().realPathFile( io, dir, &buf, @@ -912,6 +912,46 @@ test "image load: rgb, not compressed, regular file" { try tmp_dir.dir.access(testing.io, path, .{}); } +test "image load: rgb, not compressed, relative regular file" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + const data = @embedFile("testdata/image-rgb-none-20x15-2147483647-raw.data"); + try tmp_dir.dir.writeFile(testing.io, .{ + .sub_path = "image.data", + .data = data, + }); + + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .file, + .compression = .none, + .width = 20, + .height = 15, + .image_id = 31, + } }, + .data = try std.fmt.allocPrint( + alloc, + ".zig-cache/tmp/{s}/image.data", + .{tmp_dir.sub_path}, + ), + }; + defer cmd.deinit(alloc); + var loading = try LoadingImage.init(io, alloc, &cmd, .{ + .file = true, + .temporary_file = .disabled, + .shared_memory = false, + }); + defer loading.deinit(alloc); + var img = try loading.complete(alloc); + defer img.deinit(alloc); + try testing.expect(img.compression == .none); +} + test "image load: png, not compressed, regular file" { if (sys.decode_png == null) return error.SkipZigTest; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 26f8cd838..7b520a759 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1196,7 +1196,11 @@ const Subprocess = struct { // The gist is that it lets us detect when children // are still alive without blocking so that we can // kill them again. - const res_pid = posix.system.waitpid(pid, null, std.c.W.NOHANG); + const res_pid = while (true) { + const rc = posix.system.waitpid(pid, null, std.c.W.NOHANG); + if (posix.errno(rc) == .INTR) continue; + break rc; + }; log.debug("waitpid result={}", .{res_pid}); if (res_pid != 0) break; try std.Io.sleep(global.io(), .fromMilliseconds(10), .awake); diff --git a/src/termio/Termio.zig b/src/termio/Termio.zig index e931c379f..5d7a47d25 100644 --- a/src/termio/Termio.zig +++ b/src/termio/Termio.zig @@ -658,9 +658,11 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void { // HEAVY read load, we don't want to send a ton of these so we // use a timer under the covers const now = std.Io.Timestamp.now(global.io(), .awake); - if (self.last_cursor_reset) |last| cursor_reset: { - if (last.durationTo(now).toMilliseconds() <= 500) { - break :cursor_reset; + cursor_reset: { + if (self.last_cursor_reset) |last| { + if (last.durationTo(now).toMilliseconds() <= 500) { + break :cursor_reset; + } } self.last_cursor_reset = now;