mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
terminal/kitty-gfx: temporary file medium
This commit is contained in:
@ -266,6 +266,8 @@ fn encodeError(r: *Response, err: EncodeableError) void {
|
|||||||
error.OutOfMemory => r.message = "ENOMEM: out of memory",
|
error.OutOfMemory => r.message = "ENOMEM: out of memory",
|
||||||
error.InvalidData => r.message = "EINVAL: invalid data",
|
error.InvalidData => r.message = "EINVAL: invalid data",
|
||||||
error.DecompressionFailed => r.message = "EINVAL: decompression failed",
|
error.DecompressionFailed => r.message = "EINVAL: decompression failed",
|
||||||
|
error.FilePathTooLong => r.message = "EINVAL: file path too long",
|
||||||
|
error.TemporaryFileNotInTempDir => r.message = "EINVAL: temporary file not in temp dir",
|
||||||
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
|
error.UnsupportedFormat => r.message = "EINVAL: unsupported format",
|
||||||
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
|
error.UnsupportedMedium => r.message = "EINVAL: unsupported medium",
|
||||||
error.DimensionsRequired => r.message = "EINVAL: dimensions required",
|
error.DimensionsRequired => r.message = "EINVAL: dimensions required",
|
||||||
|
@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
const ArenaAllocator = std.heap.ArenaAllocator;
|
const ArenaAllocator = std.heap.ArenaAllocator;
|
||||||
|
|
||||||
const command = @import("graphics_command.zig");
|
const command = @import("graphics_command.zig");
|
||||||
|
const internal_os = @import("../../os/main.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.kitty_gfx);
|
const log = std.log.scoped(.kitty_gfx);
|
||||||
|
|
||||||
@ -52,27 +53,114 @@ pub const LoadingImage = struct {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load the base64 encoded data from the transmission medium.
|
// Special case for the direct medium, we just add it directly
|
||||||
const raw_data = switch (t.medium) {
|
// which will handle copying the data, base64 decoding, etc.
|
||||||
.direct => direct: {
|
if (t.medium == .direct) {
|
||||||
const data = cmd.data;
|
try result.addData(alloc, cmd.data);
|
||||||
_ = cmd.toOwnedData();
|
return result;
|
||||||
break :direct data;
|
}
|
||||||
},
|
|
||||||
|
// For every other medium, we'll need to at least base64 decode
|
||||||
|
// the data to make it useful so let's do that. Also, all the data
|
||||||
|
// has to be path data so we can put it in a stack-allocated buffer.
|
||||||
|
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
|
const Base64Decoder = std.base64.standard.Decoder;
|
||||||
|
const size = Base64Decoder.calcSizeForSlice(cmd.data) catch |err| {
|
||||||
|
log.warn("failed to calculate base64 size for file path: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
if (size > buf.len) return error.FilePathTooLong;
|
||||||
|
Base64Decoder.decode(&buf, cmd.data) catch |err| {
|
||||||
|
log.warn("failed to decode base64 data: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
var abs_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||||
|
const path = std.os.realpath(buf[0..size], &abs_buf) catch |err| {
|
||||||
|
log.warn("failed to get absolute path: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Depending on the medium, load the data from the path.
|
||||||
|
switch (t.medium) {
|
||||||
|
.direct => unreachable, // handled above
|
||||||
|
|
||||||
|
.temporary_file => try result.readTemporaryFile(alloc, t, path),
|
||||||
|
|
||||||
else => {
|
else => {
|
||||||
std.log.warn("unimplemented medium={}", .{t.medium});
|
std.log.warn("unimplemented medium={}", .{t.medium});
|
||||||
return error.UnsupportedMedium;
|
return error.UnsupportedMedium;
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
defer alloc.free(raw_data);
|
|
||||||
|
|
||||||
// Add the data
|
|
||||||
try result.addData(alloc, raw_data);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reads the data from a temporary file and returns it. This allocates
|
||||||
|
/// and does not free any of the data, so the caller must free it.
|
||||||
|
///
|
||||||
|
/// This will also delete the temporary file if it is in a safe location.
|
||||||
|
fn readTemporaryFile(
|
||||||
|
self: *LoadingImage,
|
||||||
|
alloc: Allocator,
|
||||||
|
t: command.Transmission,
|
||||||
|
path: []const u8,
|
||||||
|
) !void {
|
||||||
|
if (!isPathInTempDir(path)) return error.TemporaryFileNotInTempDir;
|
||||||
|
|
||||||
|
// Delete the temporary file
|
||||||
|
defer std.os.unlink(path) catch |err| {
|
||||||
|
log.warn("failed to delete temporary file: {}", .{err});
|
||||||
|
};
|
||||||
|
|
||||||
|
var file = std.fs.cwd().openFile(path, .{}) catch |err| {
|
||||||
|
log.warn("failed to open temporary file: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
defer file.close();
|
||||||
|
|
||||||
|
if (t.offset > 0) {
|
||||||
|
file.seekTo(@intCast(t.offset)) catch |err| {
|
||||||
|
log.warn("failed to seek to offset {}: {}", .{ t.offset, err });
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf_reader = std.io.bufferedReader(file.reader());
|
||||||
|
const reader = buf_reader.reader();
|
||||||
|
|
||||||
|
// Read the file
|
||||||
|
var managed = std.ArrayList(u8).init(alloc);
|
||||||
|
errdefer managed.deinit();
|
||||||
|
const size: usize = if (t.size > 0) @min(t.size, max_size) else max_size;
|
||||||
|
reader.readAllArrayList(&managed, size) catch |err| {
|
||||||
|
log.warn("failed to read temporary file: {}", .{err});
|
||||||
|
return error.InvalidData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set our data
|
||||||
|
assert(self.data.items.len == 0);
|
||||||
|
self.data = .{ .items = managed.items, .capacity = managed.capacity };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if path appears to be in a temporary directory.
|
||||||
|
/// Copies logic from Kitty.
|
||||||
|
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 (internal_os.tmpDir()) |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 (std.os.realpath(dir, &buf)) |real_dir| {
|
||||||
|
if (std.mem.startsWith(u8, path, real_dir)) return true;
|
||||||
|
} else |_| {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *LoadingImage, alloc: Allocator) void {
|
pub fn deinit(self: *LoadingImage, alloc: Allocator) void {
|
||||||
self.image.deinit(alloc);
|
self.image.deinit(alloc);
|
||||||
self.data.deinit(alloc);
|
self.data.deinit(alloc);
|
||||||
@ -123,11 +211,13 @@ pub const LoadingImage = struct {
|
|||||||
};
|
};
|
||||||
const expected_len = img.width * img.height * bpp;
|
const expected_len = img.width * img.height * bpp;
|
||||||
const actual_len = self.data.items.len;
|
const actual_len = self.data.items.len;
|
||||||
std.log.debug(
|
if (actual_len != expected_len) {
|
||||||
"complete image id={} width={} height={} bpp={} expected_len={} actual_len={}",
|
std.log.warn(
|
||||||
.{ img.id, img.width, img.height, bpp, expected_len, actual_len },
|
"unexpected length image id={} width={} height={} bpp={} expected_len={} actual_len={}",
|
||||||
);
|
.{ img.id, img.width, img.height, bpp, expected_len, actual_len },
|
||||||
if (actual_len != expected_len) return error.InvalidData;
|
);
|
||||||
|
return error.InvalidData;
|
||||||
|
}
|
||||||
|
|
||||||
// Everything looks good, copy the image data over.
|
// Everything looks good, copy the image data over.
|
||||||
var result = self.image;
|
var result = self.image;
|
||||||
@ -188,6 +278,8 @@ pub const Image = struct {
|
|||||||
DecompressionFailed,
|
DecompressionFailed,
|
||||||
DimensionsRequired,
|
DimensionsRequired,
|
||||||
DimensionsTooLarge,
|
DimensionsTooLarge,
|
||||||
|
FilePathTooLong,
|
||||||
|
TemporaryFileNotInTempDir,
|
||||||
UnsupportedFormat,
|
UnsupportedFormat,
|
||||||
UnsupportedMedium,
|
UnsupportedMedium,
|
||||||
};
|
};
|
||||||
@ -229,25 +321,7 @@ pub const Image = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Helper to base64 decode some data. No data is freed.
|
/// Easy base64 encoding function.
|
||||||
fn base64Decode(alloc: Allocator, data: []const u8) ![]const u8 {
|
|
||||||
const Base64Decoder = std.base64.standard.Decoder;
|
|
||||||
const size = Base64Decoder.calcSizeForSlice(data) catch |err| {
|
|
||||||
log.warn("failed to calculate base64 decoded size: {}", .{err});
|
|
||||||
return error.InvalidData;
|
|
||||||
};
|
|
||||||
|
|
||||||
var buf = try alloc.alloc(u8, size);
|
|
||||||
errdefer alloc.free(buf);
|
|
||||||
Base64Decoder.decode(buf, data) catch |err| {
|
|
||||||
log.warn("failed to decode base64 data: {}", .{err});
|
|
||||||
return error.InvalidData;
|
|
||||||
};
|
|
||||||
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Loads test data from a file path and base64 encodes it.
|
|
||||||
fn testB64(alloc: Allocator, data: []const u8) ![]const u8 {
|
fn testB64(alloc: Allocator, data: []const u8) ![]const u8 {
|
||||||
const B64Encoder = std.base64.standard.Encoder;
|
const B64Encoder = std.base64.standard.Encoder;
|
||||||
var b64 = try alloc.alloc(u8, B64Encoder.calcSize(data.len));
|
var b64 = try alloc.alloc(u8, B64Encoder.calcSize(data.len));
|
||||||
@ -255,6 +329,15 @@ fn testB64(alloc: Allocator, data: []const u8) ![]const u8 {
|
|||||||
return B64Encoder.encode(b64, data);
|
return B64Encoder.encode(b64, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Easy base64 decoding function.
|
||||||
|
fn testB64Decode(alloc: Allocator, data: []const u8) ![]const u8 {
|
||||||
|
const B64Decoder = std.base64.standard.Decoder;
|
||||||
|
var result = try alloc.alloc(u8, try B64Decoder.calcSizeForSlice(data));
|
||||||
|
errdefer alloc.free(result);
|
||||||
|
try B64Decoder.decode(result, data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// This specifically tests we ALLOW invalid RGB data because Kitty
|
// This specifically tests we ALLOW invalid RGB data because Kitty
|
||||||
// documents that this should work.
|
// documents that this should work.
|
||||||
test "image load with invalid RGB data" {
|
test "image load with invalid RGB data" {
|
||||||
@ -389,6 +472,7 @@ test "image load: rgb, zlib compressed, direct, chunked" {
|
|||||||
} },
|
} },
|
||||||
.data = try alloc.dupe(u8, data[0..1024]),
|
.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);
|
||||||
defer loading.deinit(alloc);
|
defer loading.deinit(alloc);
|
||||||
|
|
||||||
@ -405,3 +489,41 @@ test "image load: rgb, zlib compressed, direct, chunked" {
|
|||||||
defer img.deinit(alloc);
|
defer img.deinit(alloc);
|
||||||
try testing.expect(img.compression == .none);
|
try testing.expect(img.compression == .none);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "image load: rgb, not compressed, temporary file" {
|
||||||
|
const testing = std.testing;
|
||||||
|
const alloc = testing.allocator;
|
||||||
|
|
||||||
|
var tmp_dir = try internal_os.TempDir.init();
|
||||||
|
defer tmp_dir.deinit();
|
||||||
|
const data = try testB64Decode(
|
||||||
|
alloc,
|
||||||
|
@embedFile("testdata/image-rgb-none-20x15-2147483647.data"),
|
||||||
|
);
|
||||||
|
defer alloc.free(data);
|
||||||
|
try tmp_dir.dir.writeFile("image.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 = .temporary_file,
|
||||||
|
.compression = .none,
|
||||||
|
.width = 20,
|
||||||
|
.height = 15,
|
||||||
|
.image_id = 31,
|
||||||
|
} },
|
||||||
|
.data = try testB64(alloc, path),
|
||||||
|
};
|
||||||
|
defer cmd.deinit(alloc);
|
||||||
|
var loading = try LoadingImage.init(alloc, &cmd);
|
||||||
|
defer loading.deinit(alloc);
|
||||||
|
var img = try loading.complete(alloc);
|
||||||
|
defer img.deinit(alloc);
|
||||||
|
try testing.expect(img.compression == .none);
|
||||||
|
|
||||||
|
// Temporary file should be gone
|
||||||
|
try testing.expectError(error.FileNotFound, tmp_dir.dir.access(path, .{}));
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user