From acf54a91668b524d9a5e6e800c34ce2d08fd4d48 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 5 Mar 2026 08:26:08 -0600 Subject: [PATCH 1/5] windows: use new callconv convention --- src/os/windows.zig | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/os/windows.zig b/src/os/windows.zig index 1853f4162..87d1b662e 100644 --- a/src/os/windows.zig +++ b/src/os/windows.zig @@ -53,22 +53,22 @@ pub const exp = struct { hWritePipe: *windows.HANDLE, lpPipeAttributes: ?*const windows.SECURITY_ATTRIBUTES, nSize: windows.DWORD, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn CreatePseudoConsole( size: windows.COORD, hInput: windows.HANDLE, hOutput: windows.HANDLE, dwFlags: windows.DWORD, phPC: *HPCON, - ) callconv(windows.WINAPI) windows.HRESULT; - pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(windows.WINAPI) windows.HRESULT; - pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(windows.WINAPI) void; + ) callconv(.winapi) windows.HRESULT; + pub extern "kernel32" fn ResizePseudoConsole(hPC: HPCON, size: windows.COORD) callconv(.winapi) windows.HRESULT; + pub extern "kernel32" fn ClosePseudoConsole(hPC: HPCON) callconv(.winapi) void; pub extern "kernel32" fn InitializeProcThreadAttributeList( lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, dwAttributeCount: windows.DWORD, dwFlags: windows.DWORD, lpSize: *windows.SIZE_T, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn UpdateProcThreadAttribute( lpAttributeList: LPPROC_THREAD_ATTRIBUTE_LIST, dwFlags: windows.DWORD, @@ -77,7 +77,7 @@ pub const exp = struct { cbSize: windows.SIZE_T, lpPreviousValue: ?windows.PVOID, lpReturnSize: ?*windows.SIZE_T, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; pub extern "kernel32" fn PeekNamedPipe( hNamedPipe: windows.HANDLE, lpBuffer: ?windows.LPVOID, @@ -85,7 +85,7 @@ pub const exp = struct { lpBytesRead: ?*windows.DWORD, lpTotalBytesAvail: ?*windows.DWORD, lpBytesLeftThisMessage: ?*windows.DWORD, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; // Duplicated here because lpCommandLine is not marked optional in zig std pub extern "kernel32" fn CreateProcessW( lpApplicationName: ?windows.LPWSTR, @@ -98,7 +98,7 @@ pub const exp = struct { lpCurrentDirectory: ?windows.LPWSTR, lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION, - ) callconv(windows.WINAPI) windows.BOOL; + ) callconv(.winapi) windows.BOOL; }; pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF; From e8aad103263297d41335a27d9d1679a7ab47c08b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 5 Mar 2026 09:26:52 -0600 Subject: [PATCH 2/5] windows: avoid the use of wcwidth --- src/benchmark/CodepointWidth.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/benchmark/CodepointWidth.zig b/src/benchmark/CodepointWidth.zig index effabb036..30d3f91e7 100644 --- a/src/benchmark/CodepointWidth.zig +++ b/src/benchmark/CodepointWidth.zig @@ -6,6 +6,7 @@ const CodepointWidth = @This(); const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Benchmark = @import("Benchmark.zig"); @@ -104,6 +105,11 @@ fn stepNoop(ptr: *anyopaque) Benchmark.Error!void { extern "c" fn wcwidth(c: u32) c_int; fn stepWcwidth(ptr: *anyopaque) Benchmark.Error!void { + if (comptime builtin.os.tag == .windows) { + log.warn("wcwidth is not available on Windows", .{}); + return; + } + const self: *CodepointWidth = @ptrCast(@alignCast(ptr)); const f = self.data_f orelse return; From cccdb0d2ade79c0d3ef37635c5c9fe90a0ac14bf Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 5 Mar 2026 09:28:02 -0600 Subject: [PATCH 3/5] windows: add trivial implementation of expandHome --- src/os/homedir.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/os/homedir.zig b/src/os/homedir.zig index 0868a4fa5..14a4558cc 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -13,7 +13,7 @@ const Error = error{ /// is generally an expensive process so the value should be cached. pub inline fn home(buf: []u8) !?[]const u8 { return switch (builtin.os.tag) { - inline .linux, .freebsd, .macos => try homeUnix(buf), + .linux, .freebsd, .macos => try homeUnix(buf), .windows => try homeWindows(buf), // iOS doesn't have a user-writable home directory @@ -122,7 +122,13 @@ pub const ExpandError = error{ pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 { return switch (builtin.os.tag) { .linux, .freebsd, .macos => try expandHomeUnix(path, buf), + + // `~/` is not an idiom generally used on Windows + .windows => return path, + + // iOS doesn't have a user-writable home directory .ios => return path, + else => @compileError("unimplemented"), }; } From d29e1cc1375e0a700df73604c528a550813c8b1a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 5 Mar 2026 09:29:04 -0600 Subject: [PATCH 4/5] windows: use explicit error sets to work around lack of file locking --- src/cli/ssh-cache/DiskCache.zig | 43 ++++++++++++++++++++++++++++----- src/cli/ssh-cache/Entry.zig | 4 ++- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig index 6214d0429..6fa74b43d 100644 --- a/src/cli/ssh-cache/DiskCache.zig +++ b/src/cli/ssh-cache/DiskCache.zig @@ -57,6 +57,16 @@ pub fn clear(self: DiskCache) !void { pub const AddResult = enum { added, updated }; +pub const AddError = std.fs.Dir.MakeError || + std.fs.Dir.StatFileError || + std.fs.File.OpenError || + std.fs.File.ChmodError || + std.io.Reader.LimitedAllocError || + FixupPermissionsError || + ReadEntriesError || + WriteCacheFileError || + Error; + /// Add or update a hostname entry in the cache. /// Returns AddResult.added for new entries or AddResult.updated for existing ones. /// The cache file is created if it doesn't exist with secure permissions (0600). @@ -64,7 +74,7 @@ pub fn add( self: DiskCache, alloc: Allocator, hostname: []const u8, -) !AddResult { +) AddError!AddResult { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Create cache directory if needed @@ -128,13 +138,19 @@ pub fn add( return result; } +pub const RemoveError = std.fs.File.OpenError || + FixupPermissionsError || + ReadEntriesError || + WriteCacheFileError || + Error; + /// Remove a hostname entry from the cache. /// No error is returned if the hostname doesn't exist or the cache file is missing. pub fn remove( self: DiskCache, alloc: Allocator, hostname: []const u8, -) !void { +) RemoveError!void { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Open our file @@ -168,13 +184,17 @@ pub fn remove( try self.writeCacheFile(entries, null); } +pub const ContainsError = std.fs.File.OpenError || + ReadEntriesError || + error{HostnameIsInvalid}; + /// Check if a hostname exists in the cache. /// Returns false if the cache file doesn't exist. pub fn contains( self: DiskCache, alloc: Allocator, hostname: []const u8, -) !bool { +) ContainsError!bool { if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; // Open our file @@ -194,7 +214,9 @@ pub fn contains( return entries.contains(hostname); } -fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.ChmodError)!void { +pub const FixupPermissionsError = (std.fs.File.StatError || std.fs.File.ChmodError); + +fn fixupPermissions(file: std.fs.File) FixupPermissionsError!void { // Windows does not support chmod if (comptime builtin.os.tag == .windows) return; @@ -206,11 +228,18 @@ fn fixupPermissions(file: std.fs.File) (std.fs.File.StatError || std.fs.File.Chm } } +pub const WriteCacheFileError = std.fs.Dir.OpenError || + std.fs.AtomicFile.InitError || + std.fs.AtomicFile.FlushError || + std.fs.AtomicFile.FinishError || + Entry.FormatError || + error{InvalidCachePath}; + fn writeCacheFile( self: DiskCache, entries: std.StringHashMap(Entry), expire_days: ?u32, -) !void { +) WriteCacheFileError!void { const cache_dir = std.fs.path.dirname(self.path) orelse return error.InvalidCachePath; const cache_basename = std.fs.path.basename(self.path); @@ -270,10 +299,12 @@ pub fn deinitEntries( entries.deinit(); } +pub const ReadEntriesError = std.mem.Allocator.Error || std.io.Reader.LimitedAllocError; + fn readEntries( alloc: Allocator, file: std.fs.File, -) !std.StringHashMap(Entry) { +) ReadEntriesError!std.StringHashMap(Entry) { var reader = file.reader(&.{}); const content = try reader.interface.allocRemaining( alloc, diff --git a/src/cli/ssh-cache/Entry.zig b/src/cli/ssh-cache/Entry.zig index f3403dbd4..b586161f2 100644 --- a/src/cli/ssh-cache/Entry.zig +++ b/src/cli/ssh-cache/Entry.zig @@ -33,7 +33,9 @@ pub fn parse(line: []const u8) ?Entry { }; } -pub fn format(self: Entry, writer: *std.Io.Writer) !void { +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 }, From b1d3e36e2ea0d428dd333019c8346b6d4bcbc762 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 5 Mar 2026 10:03:58 -0600 Subject: [PATCH 5/5] windows: add GetComputerNameA so that hostname-related functions work --- src/os/hostname.zig | 37 +++++++++++++++++++++++++++++++------ src/os/windows.zig | 5 +++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index f728a2455..af9148fbf 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const posix = std.posix; pub const LocalHostnameValidationError = error{ @@ -99,9 +100,21 @@ pub fn isLocal(hostname: []const u8) LocalHostnameValidationError!bool { if (std.mem.eql(u8, "localhost", hostname)) return true; // If hostname is not "localhost" it must match our hostname. - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const ourHostname = try posix.gethostname(&buf); - return std.mem.eql(u8, hostname, ourHostname); + switch (builtin.os.tag) { + .windows => { + const windows = @import("windows.zig"); + var buf: [256:0]u8 = undefined; + var nSize: windows.DWORD = buf.len; + if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return false; + const ourHostname = buf[0..nSize]; + return std.mem.eql(u8, hostname, ourHostname); + }, + else => { + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const ourHostname = try posix.gethostname(&buf); + return std.mem.eql(u8, hostname, ourHostname); + }, + } } test "isLocal returns true when provided hostname is localhost" { @@ -109,9 +122,21 @@ test "isLocal returns true when provided hostname is localhost" { } test "isLocal returns true when hostname is local" { - var buf: [posix.HOST_NAME_MAX]u8 = undefined; - const localHostname = try posix.gethostname(&buf); - try std.testing.expect(try isLocal(localHostname)); + switch (builtin.os.tag) { + .windows => { + const windows = @import("windows.zig"); + var buf: [256:0]u8 = undefined; + var nSize: windows.DWORD = buf.len; + if (windows.exp.kernel32.GetComputerNameA(&buf, &nSize) == 0) return error.GetComputerNameFailed; + const localHostname = buf[0..nSize]; + try std.testing.expect(try isLocal(localHostname)); + }, + else => { + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const localHostname = try posix.gethostname(&buf); + try std.testing.expect(try isLocal(localHostname)); + }, + } } test "isLocal returns false when hostname is not local" { diff --git a/src/os/windows.zig b/src/os/windows.zig index 87d1b662e..e92a54537 100644 --- a/src/os/windows.zig +++ b/src/os/windows.zig @@ -99,6 +99,11 @@ pub const exp = struct { lpStartupInfo: *windows.STARTUPINFOW, lpProcessInformation: *windows.PROCESS_INFORMATION, ) callconv(.winapi) windows.BOOL; + /// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getcomputernamea + pub extern "kernel32" fn GetComputerNameA( + lpBuffer: windows.LPSTR, + nSize: *windows.DWORD, + ) callconv(.winapi) windows.BOOL; }; pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF;