diff --git a/src/terminal/kitty/graphics_image.zig b/src/terminal/kitty/graphics_image.zig index 584195620..a6a8be014 100644 --- a/src/terminal/kitty/graphics_image.zig +++ b/src/terminal/kitty/graphics_image.zig @@ -377,11 +377,28 @@ pub const LoadingImage = struct { // Read the file var managed: std.ArrayList(u8) = .empty; errdefer managed.deinit(alloc); - const size: usize = if (t.size > 0) @min(t.size, max_size) else max_size; - reader.appendRemaining(alloc, &managed, .limited(size)) catch { - log.warn("failed to read temporary file: {?}", .{buf_reader.err}); - return error.InvalidData; - }; + if (t.size > 0) { + // S is the exact number of bytes to read, not a maximum: + // https://sw.kovidgoyal.net/kitty/graphics-protocol/#local-client + const size = std.math.cast(usize, t.size) orelse + return error.InvalidData; + if (size > max_size) return error.InvalidData; + + // Read exact size + const data = reader.readAlloc(alloc, size) catch |err| { + log.warn("failed to read image file: {}", .{err}); + return switch (err) { + error.OutOfMemory => error.OutOfMemory, + else => error.InvalidData, + }; + }; + managed = .{ .items = data, .capacity = data.len }; + } else { + reader.appendRemaining(alloc, &managed, .limited(max_size)) catch { + log.warn("failed to read image file: {?}", .{buf_reader.err}); + return error.InvalidData; + }; + } // Set our data assert(self.data.items.len == 0); @@ -1151,6 +1168,101 @@ test "image load: rgb, not compressed, regular file" { try tmp_dir.dir.access(testing.io, path, .{}); } +test "image load: regular file size reads exactly requested bytes" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + try tmp_dir.dir.writeFile(io, .{ + .sub_path = "image.data", + .data = &.{ 1, 2, 3, 4, 5, 6 }, + }); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const path = path_buf[0..try tmp_dir.dir.realPathFile( + io, + "image.data", + &path_buf, + )]; + + const cases = [_]struct { + offset: u32, + expected: [3]u8, + }{ + .{ .offset = 0, .expected = .{ 1, 2, 3 } }, + .{ .offset = 3, .expected = .{ 4, 5, 6 } }, + }; + for (cases) |case| { + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .file, + .width = 1, + .height = 1, + .size = 3, + .offset = case.offset, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, 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.expectEqualSlices(u8, &case.expected, img.data.complete); + } +} + +test "image load: regular file size rejects short data" { + const testing = std.testing; + const alloc = testing.allocator; + const io = testing.io; + + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + try tmp_dir.dir.writeFile(io, .{ + .sub_path = "image.data", + .data = &.{ 1, 2 }, + }); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const path = path_buf[0..try tmp_dir.dir.realPathFile( + io, + "image.data", + &path_buf, + )]; + var cmd: command.Command = .{ + .control = .{ .transmit = .{ + .format = .rgb, + .medium = .file, + .width = 1, + .height = 1, + .size = 3, + .image_id = 31, + } }, + .data = try alloc.dupe(u8, path), + }; + defer cmd.deinit(alloc); + + try testing.expectError( + error.InvalidData, + LoadingImage.init(io, alloc, &cmd, .{ + .file = true, + .temporary_file = .disabled, + .shared_memory = false, + }), + ); +} + test "image load: rgb, not compressed, relative regular file" { const testing = std.testing; const alloc = testing.allocator;