mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal/kitty: graphics S value is exact byte, not max (#13892)
File transmissions with exactly S bytes or trailing file data previously failed because appendRemaining reports StreamTooLong when it reaches its limit. This broke the protocol's partial-file transmission path. Use an exact-length allocation and read when S is nonzero, rejecting premature EOF and values above the image limit. Preserve the existing bounded read-to-EOF behavior for S=0. https://sw.kovidgoyal.net/kitty/graphics-protocol/#local-client
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user