aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/cli/ssh-cache/Entry.zig
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/libghostty-vt/src/cli/ssh-cache/Entry.zig')
-rw-r--r--vendor/libghostty-vt/src/cli/ssh-cache/Entry.zig156
1 files changed, 156 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/cli/ssh-cache/Entry.zig b/vendor/libghostty-vt/src/cli/ssh-cache/Entry.zig
new file mode 100644
index 00000000..b586161f
--- /dev/null
+++ b/vendor/libghostty-vt/src/cli/ssh-cache/Entry.zig
@@ -0,0 +1,156 @@
+/// A single entry within our SSH entry cache. Our SSH entry cache
+/// stores which hosts we've sent our terminfo to so that we don't have
+/// to send it again. It doesn't store any sensitive information.
+const Entry = @This();
+
+const std = @import("std");
+
+hostname: []const u8,
+timestamp: i64,
+terminfo_version: []const u8,
+
+pub fn parse(line: []const u8) ?Entry {
+ const trimmed = std.mem.trim(u8, line, " \t\r\n");
+ if (trimmed.len == 0) return null;
+
+ // Parse format: hostname|timestamp|terminfo_version
+ var iter = std.mem.tokenizeScalar(u8, trimmed, '|');
+ const hostname = iter.next() orelse return null;
+ const timestamp_str = iter.next() orelse return null;
+ const terminfo_version = iter.next() orelse "xterm-ghostty";
+ const timestamp = std.fmt.parseInt(i64, timestamp_str, 10) catch |err| {
+ std.log.warn(
+ "Invalid timestamp in cache entry: {s} err={}",
+ .{ timestamp_str, err },
+ );
+ return null;
+ };
+
+ return .{
+ .hostname = hostname,
+ .timestamp = timestamp,
+ .terminfo_version = terminfo_version,
+ };
+}
+
+pub const FormatError = std.Io.Writer.Error;
+
+pub fn format(self: Entry, writer: *std.Io.Writer) FormatError!void {
+ try writer.print(
+ "{s}|{d}|{s}\n",
+ .{ self.hostname, self.timestamp, self.terminfo_version },
+ );
+}
+
+pub fn isExpired(self: Entry, expire_days_: ?u32) bool {
+ const expire_days = expire_days_ orelse return false;
+ const now = std.time.timestamp();
+ const age_days = @divTrunc(now -| self.timestamp, std.time.s_per_day);
+ return age_days > expire_days;
+}
+
+test "cache entry expiration" {
+ const testing = std.testing;
+ const now = std.time.timestamp();
+
+ const fresh_entry: Entry = .{
+ .hostname = "test.com",
+ .timestamp = now - std.time.s_per_day, // 1 day old
+ .terminfo_version = "xterm-ghostty",
+ };
+ try testing.expect(!fresh_entry.isExpired(90));
+
+ const old_entry: Entry = .{
+ .hostname = "old.com",
+ .timestamp = now - (std.time.s_per_day * 100), // 100 days old
+ .terminfo_version = "xterm-ghostty",
+ };
+ try testing.expect(old_entry.isExpired(90));
+
+ // Test never-expire case
+ try testing.expect(!old_entry.isExpired(null));
+}
+
+test "cache entry expiration exact boundary" {
+ const testing = std.testing;
+ const now = std.time.timestamp();
+
+ // Exactly at expiration boundary
+ const boundary_entry: Entry = .{
+ .hostname = "example.com",
+ .timestamp = now - (std.time.s_per_day * 30),
+ .terminfo_version = "xterm-ghostty",
+ };
+ try testing.expect(!boundary_entry.isExpired(30));
+ try testing.expect(boundary_entry.isExpired(29));
+}
+
+test "cache entry expiration large timestamp" {
+ const testing = std.testing;
+ const now = std.time.timestamp();
+
+ const boundary_entry: Entry = .{
+ .hostname = "example.com",
+ .timestamp = now + (std.time.s_per_day * 30),
+ .terminfo_version = "xterm-ghostty",
+ };
+ try testing.expect(!boundary_entry.isExpired(30));
+}
+
+test "cache entry parsing valid formats" {
+ const testing = std.testing;
+
+ const entry = Entry.parse("example.com|1640995200|xterm-ghostty").?;
+ try testing.expectEqualStrings("example.com", entry.hostname);
+ try testing.expectEqual(@as(i64, 1640995200), entry.timestamp);
+ try testing.expectEqualStrings("xterm-ghostty", entry.terminfo_version);
+
+ // Test default terminfo version
+ const entry_no_version = Entry.parse("test.com|1640995200").?;
+ try testing.expectEqualStrings(
+ "xterm-ghostty",
+ entry_no_version.terminfo_version,
+ );
+
+ // Test complex hostnames
+ const complex_entry = Entry.parse("user@server.example.com|1640995200|xterm-ghostty").?;
+ try testing.expectEqualStrings(
+ "user@server.example.com",
+ complex_entry.hostname,
+ );
+}
+
+test "cache entry parsing invalid formats" {
+ const testing = std.testing;
+
+ try testing.expect(Entry.parse("") == null);
+
+ // Invalid format (no pipe)
+ try testing.expect(Entry.parse("v1") == null);
+
+ // Missing timestamp
+ try testing.expect(Entry.parse("example.com") == null);
+
+ // Invalid timestamp
+ try testing.expect(Entry.parse("example.com|invalid") == null);
+
+ // Empty terminfo should default
+ try testing.expect(Entry.parse("example.com|1640995200|") != null);
+}
+
+test "cache entry parsing malformed data resilience" {
+ const testing = std.testing;
+
+ // Extra pipes should not break parsing
+ try testing.expect(Entry.parse("host|123|term|extra") != null);
+
+ // Whitespace handling
+ try testing.expect(Entry.parse(" host|123|term ") != null);
+ try testing.expect(Entry.parse("\n") == null);
+ try testing.expect(Entry.parse(" \t \n") == null);
+
+ // Extremely large timestamp
+ try testing.expect(
+ Entry.parse("host|999999999999999999999999999999999999999999999999|xterm-ghostty") == null,
+ );
+}