diff options
Diffstat (limited to 'vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig | 226 |
1 files changed, 143 insertions, 83 deletions
diff --git a/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig b/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig index bb11e74f..ea64dfe8 100644 --- a/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig +++ b/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig @@ -10,6 +10,7 @@ const Allocator = std.mem.Allocator; const internal_os = @import("../../os/main.zig"); const xdg = internal_os.xdg; const Entry = @import("Entry.zig"); +const global = @import("../../global.zig"); // 512KB - sufficient for approximately 10k entries const MAX_CACHE_SIZE = 512 * 1024; @@ -26,8 +27,12 @@ pub fn defaultPath( alloc: Allocator, program: []const u8, ) ![]const u8 { + var environ_map = try global.environMap(); + defer environ_map.deinit(); const state_dir: []const u8 = xdg.state( + global.io(), alloc, + &environ_map, .{ .subdir = program }, ) catch |err| return switch (err) { error.OutOfMemory => error.OutOfMemory, @@ -41,7 +46,7 @@ pub fn defaultPath( /// This removes the cache file from disk, effectively clearing all cached /// SSH terminfo entries. pub fn clear(self: DiskCache) !void { - std.fs.cwd().deleteFile(self.path) catch |err| switch (err) { + std.Io.Dir.cwd().deleteFile(global.io(), self.path) catch |err| switch (err) { error.FileNotFound => {}, else => return err, }; @@ -53,42 +58,50 @@ pub fn add( self: DiskCache, alloc: Allocator, key: []const u8, + version: []const u8, timestamp: i64, ) !void { if (!isValidCacheKey(key)) return error.InvalidCacheKey; // Create cache directory if needed if (std.fs.path.dirname(self.path)) |dir| { - std.fs.cwd().makePath(dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(global.io(), dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; } // Open or create cache file with secure permissions - const file = std.fs.createFileAbsolute(self.path, .{ + const file = std.Io.Dir.createFileAbsolute(global.io(), self.path, .{ .read = true, .truncate = false, - .mode = 0o600, + .permissions = if (builtin.os.tag != .windows and std.posix.mode_t != u0) + .fromMode(0o600) + else + .default_file, }) catch |err| switch (err) { error.PathAlreadyExists => blk: { - const existing_file = try std.fs.openFileAbsolute( + const existing_file = try std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{ .mode = .read_write }, ); - errdefer existing_file.close(); + errdefer existing_file.close(global.io()); try fixupPermissions(existing_file); break :blk existing_file; }, else => return err, }; - defer file.close(); + defer file.close(global.io()); // Lock // Causes a compile failure in the Zig std library on Windows, see: // https://github.com/ziglang/zig/issues/18430 - if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked; - defer if (comptime builtin.os.tag != .windows) file.unlock(); + if (comptime builtin.os.tag != .windows) _ = file.tryLock( + global.io(), + .exclusive, + ) catch return error.CacheLocked; + defer if (comptime builtin.os.tag != .windows) file.unlock(global.io()); var entries = try readEntries(alloc, file); defer deinitEntries(alloc, &entries); @@ -99,16 +112,19 @@ pub fn add( // `deinitEntries` defer to walk. if (entries.getPtr(key)) |existing| { existing.timestamp = timestamp; + const version_copy = try alloc.dupe(u8, version); + alloc.free(existing.terminfo_version); + existing.terminfo_version = version_copy; } else { const key_copy = try alloc.dupe(u8, key); errdefer alloc.free(key_copy); - const terminfo_copy = try alloc.dupe(u8, "xterm-ghostty"); - errdefer alloc.free(terminfo_copy); + const version_copy = try alloc.dupe(u8, version); + errdefer alloc.free(version_copy); try entries.put(key_copy, .{ .hostname = key_copy, .timestamp = timestamp, - .terminfo_version = terminfo_copy, + .terminfo_version = version_copy, }); } @@ -125,21 +141,25 @@ pub fn remove( if (!isValidCacheKey(key)) return error.InvalidCacheKey; // Open our file - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{ .mode = .read_write }, ) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; - defer file.close(); + defer file.close(global.io()); try fixupPermissions(file); // Lock // Causes a compile failure in the Zig std library on Windows, see: // https://github.com/ziglang/zig/issues/18430 - if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked; - defer if (comptime builtin.os.tag != .windows) file.unlock(); + if (comptime builtin.os.tag != .windows) _ = file.tryLock( + global.io(), + .exclusive, + ) catch return error.CacheLocked; + defer if (comptime builtin.os.tag != .windows) file.unlock(global.io()); // Read existing entries var entries = try readEntries(alloc, file); @@ -165,28 +185,32 @@ pub fn prune( alloc: Allocator, max_age_s: u64, ) !usize { - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{ .mode = .read_write }, ) catch |err| switch (err) { error.FileNotFound => return 0, else => return err, }; - defer file.close(); + defer file.close(global.io()); try fixupPermissions(file); // Lock // Causes a compile failure in the Zig std library on Windows, see: // https://github.com/ziglang/zig/issues/18430 - if (comptime builtin.os.tag != .windows) _ = file.tryLock(.exclusive) catch return error.CacheLocked; - defer if (comptime builtin.os.tag != .windows) file.unlock(); + if (comptime builtin.os.tag != .windows) _ = file.tryLock( + global.io(), + .exclusive, + ) catch return error.CacheLocked; + defer if (comptime builtin.os.tag != .windows) file.unlock(global.io()); // Read existing entries var entries = try readEntries(alloc, file); defer deinitEntries(alloc, &entries); // Drop expired entries from the map, then persist what remains. - const now = std.time.timestamp(); + const now = std.Io.Timestamp.now(global.io(), .real).toSeconds(); var expired: std.ArrayList([]const u8) = .empty; defer expired.deinit(alloc); var iter = entries.iterator(); @@ -205,41 +229,44 @@ pub fn prune( return expired.items.len; } -/// Check if a key exists in the cache. -/// Returns false if the cache file doesn't exist. +/// Check if a key with `version` exists in the cache. +/// Returns false if the cache file doesn't exist or the version doesn't match. pub fn contains( self: DiskCache, alloc: Allocator, key: []const u8, + version: []const u8, ) !bool { if (!isValidCacheKey(key)) return error.InvalidCacheKey; // Open our file - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{}, ) catch |err| switch (err) { error.FileNotFound => return false, else => return err, }; - defer file.close(); + defer file.close(global.io()); // Read existing entries var entries = try readEntries(alloc, file); defer deinitEntries(alloc, &entries); - return entries.contains(key); + const entry = entries.get(key) orelse return false; + return std.mem.eql(u8, entry.terminfo_version, version); } -fn fixupPermissions(file: std.fs.File) !void { +fn fixupPermissions(file: std.Io.File) !void { // Windows does not support chmod if (comptime builtin.os.tag == .windows) return; // Ensure file has correct permissions (readable/writable by // owner only) - const stat = try file.stat(); - if (stat.mode & 0o777 != 0o600) { - try file.chmod(0o600); + const stat = try file.stat(global.io()); + if (stat.permissions.toMode() & 0o777 != 0o600) { + try file.setPermissions(global.io(), .fromMode(0o600)); } } @@ -250,22 +277,27 @@ fn writeCacheFile( const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath; const cache_basename = std.fs.path.basename(self.path); - var dir = try std.fs.cwd().openDir(cache_dir, .{}); - defer dir.close(); + var dir = try std.Io.Dir.cwd().openDir(global.io(), cache_dir, .{}); + defer dir.close(global.io()); var buf: [1024]u8 = undefined; - var atomic_file = try dir.atomicFile(cache_basename, .{ - .mode = 0o600, - .write_buffer = &buf, + var atomic_file = try dir.createFileAtomic(global.io(), cache_basename, .{ + .permissions = if (builtin.os.tag != .windows and std.posix.mode_t != u0) + .fromMode(0o600) + else + .default_file, + .replace = true, }); - defer atomic_file.deinit(); + defer atomic_file.deinit(global.io()); + var file_writer = atomic_file.file.writer(global.io(), &buf); var iter = entries.iterator(); while (iter.next()) |kv| { - try kv.value_ptr.format(&atomic_file.file_writer.interface); + try kv.value_ptr.format(&file_writer.interface); } - try atomic_file.finish(); + try file_writer.flush(); + try atomic_file.replace(global.io()); } /// List all entries in the cache. @@ -276,14 +308,15 @@ pub fn list( alloc: Allocator, ) !std.StringHashMap(Entry) { // Open our file - const file = std.fs.openFileAbsolute( + const file = std.Io.Dir.openFileAbsolute( + global.io(), self.path, .{}, ) catch |err| switch (err) { error.FileNotFound => return .init(alloc), else => return err, }; - defer file.close(); + defer file.close(global.io()); return readEntries(alloc, file); } @@ -306,9 +339,9 @@ pub fn deinitEntries( fn readEntries( alloc: Allocator, - file: std.fs.File, + file: std.Io.File, ) !std.StringHashMap(Entry) { - var reader = file.reader(&.{}); + var reader = file.reader(global.io(), &.{}); const content = try reader.interface.allocRemaining( alloc, .limited(MAX_CACHE_SIZE), @@ -367,6 +400,16 @@ fn readEntries( return entries; } +/// Whether an error from a cache operation reflects a problem with the cache +/// itself. A destination we can't key on and a lock held by another process +/// both leave the cache healthy, so neither is worth reporting. +pub fn isFailure(err: anyerror) bool { + return switch (err) { + error.InvalidCacheKey, error.CacheLocked => false, + else => true, + }; +} + // Supports both standalone hostnames and user@hostname format pub fn isValidCacheKey(key: []const u8) bool { if (key.len == 0) return false; @@ -385,14 +428,14 @@ pub fn isValidCacheKey(key: []const u8) bool { fn isValidHost(host: []const u8) bool { // First check for valid hostnames because this is assumed to be the more // likely ssh host format. - if (internal_os.hostname.isValid(host)) { + if (std.Io.net.HostName.validate(host)) |_| { return true; - } + } else |_| {} // We also accept valid IP addresses. In practice, IPv4 addresses are also // considered valid hostnames due to their overlapping syntax, so we can // simplify this check to be IPv6-specific. - if (std.net.Address.parseIp6(host, 0)) |_| { + if (std.Io.net.IpAddress.parseIp6(host, 0)) |_| { return true; } else |_| { return false; @@ -428,12 +471,12 @@ test "disk cache clear" { defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); - var file_writer = file.writer(&buf); + var file = try tmp.dir.createFile(testing.io, "cache", .{}); + defer file.close(testing.io); + var file_writer = file.writer(testing.io, &buf); try file_writer.interface.writeAll("HELLO!"); } - const path = try tmp.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", alloc); defer alloc.free(path); // Setup our cache @@ -443,7 +486,7 @@ test "disk cache clear" { // Verify the file is gone try testing.expectError( error.FileNotFound, - tmp.dir.openFile("cache", .{}), + tmp.dir.openFile(testing.io, "cache", .{}), ); } @@ -456,33 +499,38 @@ test "disk cache operations" { defer tmp.cleanup(); var buf: [4096]u8 = undefined; { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); - var file_writer = file.writer(&buf); + var file = try tmp.dir.createFile(testing.io, "cache", .{}); + defer file.close(testing.io); + var file_writer = file.writer(testing.io, &buf); const writer = &file_writer.interface; try writer.writeAll("HELLO!"); try writer.flush(); } - const path = try tmp.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", alloc); defer alloc.free(path); // Setup our cache. Adding the same key twice exercises both the new // and existing-entry paths. const cache: DiskCache = .{ .path = path }; - try cache.add(alloc, "example.com", std.time.timestamp()); - try cache.add(alloc, "example.com", std.time.timestamp()); - try testing.expect(try cache.contains(alloc, "example.com")); + try cache.add(alloc, "example.com", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds()); + try testing.expect(!try cache.contains(alloc, "example.com", "v2")); + try cache.add(alloc, "example.com", "v2", std.Io.Timestamp.now(testing.io, .real).toSeconds()); + try testing.expect(try cache.contains(alloc, "example.com", "v2")); // List var entries = try cache.list(alloc); - deinitEntries(alloc, &entries); + defer deinitEntries(alloc, &entries); + try testing.expectEqualStrings( + "v2", + entries.get("example.com").?.terminfo_version, + ); // Remove reports that it removed the entry, and a second remove of the // same key reports nothing to remove. try testing.expect(try cache.remove(alloc, "example.com")); try testing.expect(!try cache.remove(alloc, "example.com")); - try testing.expect(!(try cache.contains(alloc, "example.com"))); - try cache.add(alloc, "example.com", std.time.timestamp()); + try testing.expect(!(try cache.contains(alloc, "example.com", "v2"))); + try cache.add(alloc, "example.com", "v2", std.Io.Timestamp.now(testing.io, .real).toSeconds()); } test "disk cache cleans up temp files" { @@ -492,19 +540,19 @@ test "disk cache cleans up temp files" { var tmp = testing.tmpDir(.{ .iterate = true }); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(testing.io, ".", alloc); defer alloc.free(tmp_path); const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); defer alloc.free(cache_path); const cache: DiskCache = .{ .path = cache_path }; - try cache.add(alloc, "example.com", std.time.timestamp()); - try cache.add(alloc, "example.org", std.time.timestamp()); + try cache.add(alloc, "example.com", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds()); + try cache.add(alloc, "example.org", "v1", std.Io.Timestamp.now(testing.io, .real).toSeconds()); // Verify only the cache file exists and no temp files left behind var count: usize = 0; var iter = tmp.dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(testing.io)) |entry| { count += 1; try testing.expectEqualStrings("cache", entry.name); } @@ -514,10 +562,11 @@ test "disk cache cleans up temp files" { test "disk cache prune" { const testing = std.testing; const alloc = testing.allocator; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc); defer alloc.free(tmp_path); const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); defer alloc.free(cache_path); @@ -527,31 +576,32 @@ test "disk cache prune" { // Back-date one entry an hour old and one 100 days old. const day = std.time.s_per_day; const hour = std.time.s_per_hour; - const now = std.time.timestamp(); - try cache.add(alloc, "recent.com", now - hour); - try cache.add(alloc, "old.com", now - 100 * day); + const now = std.Io.Timestamp.now(testing.io, .real).toSeconds(); + try cache.add(alloc, "recent.com", "v1", now - hour); + try cache.add(alloc, "old.com", "v1", now - 100 * day); // Prune entries older than 90 days: only old.com goes. try testing.expectEqual(@as(usize, 1), try cache.prune(alloc, 90 * day)); - try testing.expect(try cache.contains(alloc, "recent.com")); - try testing.expect(!try cache.contains(alloc, "old.com")); + try testing.expect(try cache.contains(alloc, "recent.com", "v1")); + try testing.expect(!try cache.contains(alloc, "old.com", "v1")); // Pruning again removes nothing. try testing.expectEqual(@as(usize, 0), try cache.prune(alloc, 90 * day)); // Sub-day granularity: a 30-minute max age prunes the hour-old entry. try testing.expectEqual(@as(usize, 1), try cache.prune(alloc, 30 * std.time.s_per_min)); - try testing.expect(!try cache.contains(alloc, "recent.com")); + try testing.expect(!try cache.contains(alloc, "recent.com", "v1")); } test "disk cache prune missing file" { const testing = std.testing; const alloc = testing.allocator; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(alloc, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(io, ".", alloc); defer alloc.free(tmp_path); const cache_path = try std.fs.path.join(alloc, &.{ tmp_path, "cache" }); defer alloc.free(cache_path); @@ -563,6 +613,7 @@ test "disk cache prune missing file" { test "disk cache reads duplicate keys" { const testing = std.testing; const alloc = testing.allocator; + const io = testing.io; var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); @@ -571,16 +622,16 @@ test "disk cache reads duplicate keys" { // key with the updated entry and ensure (via testing.allocator) that // we don't double-free or leak. { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); + var file = try tmp.dir.createFile(io, "cache", .{}); + defer file.close(io); var buf: [256]u8 = undefined; - var file_writer = file.writer(&buf); + var file_writer = file.writer(io, &buf); try file_writer.interface.writeAll( "example.com|100|xterm-ghostty\nexample.com|200|xterm-newer\n", ); try file_writer.interface.flush(); } - const path = try tmp.dir.realpathAlloc(alloc, "cache"); + const path = try tmp.dir.realPathFileAlloc(io, "cache", alloc); defer alloc.free(path); const cache: DiskCache = .{ .path = path }; @@ -602,10 +653,10 @@ test "disk cache reads survive allocation failure" { // Exercise a populated cache containing a duplicate key to ensure // that we hit all of the possible allocation behaviors below. { - var file = try tmp.dir.createFile("cache", .{}); - defer file.close(); + var file = try tmp.dir.createFile(testing.io, "cache", .{}); + defer file.close(testing.io); var buf: [256]u8 = undefined; - var file_writer = file.writer(&buf); + var file_writer = file.writer(testing.io, &buf); try file_writer.interface.writeAll( "a.com|100|xterm-ghostty\n" ++ "b.com|100|xterm-ghostty\n" ++ @@ -614,7 +665,7 @@ test "disk cache reads survive allocation failure" { ); try file_writer.interface.flush(); } - const path = try tmp.dir.realpathAlloc(testing.allocator, "cache"); + const path = try tmp.dir.realPathFileAlloc(testing.io, "cache", testing.allocator); defer testing.allocator.free(path); const cache: DiskCache = .{ .path = path }; @@ -647,7 +698,7 @@ test "disk cache add survives allocation failure" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup(); - const tmp_path = try tmp.dir.realpathAlloc(testing.allocator, "."); + const tmp_path = try tmp.dir.realPathFileAlloc(testing.io, ".", testing.allocator); defer testing.allocator.free(tmp_path); const path = try std.fs.path.join(testing.allocator, &.{ tmp_path, "cache" }); defer testing.allocator.free(path); @@ -660,14 +711,14 @@ test "disk cache add survives allocation failure" { // from a clean cache file. var fail_index: usize = 0; while (true) : (fail_index += 1) { - std.fs.cwd().deleteFile(path) catch {}; + std.Io.Dir.cwd().deleteFile(testing.io, path) catch {}; var failing = std.testing.FailingAllocator.init( testing.allocator, .{ .fail_index = fail_index }, ); const alloc = failing.allocator(); - if (cache.add(alloc, "user@example.com", 100)) |_| { + if (cache.add(alloc, "user@example.com", "v1", 100)) |_| { if (!failing.has_induced_failure) break; } else |err| { try testing.expectEqual(error.OutOfMemory, err); @@ -675,6 +726,15 @@ test "disk cache add survives allocation failure" { } } +test isFailure { + const testing = std.testing; + + try testing.expect(!isFailure(error.InvalidCacheKey)); + try testing.expect(!isFailure(error.CacheLocked)); + try testing.expect(isFailure(error.AccessDenied)); + try testing.expect(isFailure(error.IsDir)); +} + test isValidHost { const testing = std.testing; |
