aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/ssh-cache/DiskCache.zig226
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;