Skip to content

Commit c74f6d5

Browse files
authored
os: use GetTempPathW for allocTmpDir on Windows (ghostty-org#12469)
`allocTmpDir` previously read `%TMP%` via `getenvW` and returned `null` if the variable wasn't set, requiring each caller to to deal with the nullable. Unfortunately, there isn't a platform-neutral default value that makes sense for those cases (i.e. `/tmp` is POSIX-y). We now use `GetTempPathW` on Windows, which is the official way to get this directory: `TMP` → `TEMP` → `USERPROFILE` → `GetWindowsDirectoryW`. With a real system call behind it, the function no longer needs to be nullable: the only remaining failure modes are OOM (propagated) and the syscall itself failing or returning data we can't decode. In those later cases, we use `C:\Windows\Temp` as a fallback, similar to how we use `/tmp` in the POSIX case. The Windows path always allocates so it still must be paired with `freeTmpDir`, which matches the existing contract. --- *AI Disclosure:* I verified the Windows path using Claude and Zig's cross-compilation capabilities because I don't have a Windows environment in which to test this. I do fully understand the code based on my prior life as a Windows game developer though.
2 parents 278041c + 8b90efd commit c74f6d5

4 files changed

Lines changed: 44 additions & 27 deletions

File tree

src/os/TempDir.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub fn init() !TempDir {
2828

2929
const dir = dir: {
3030
const cwd = std.fs.cwd();
31-
const tmp_dir = file.allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
31+
const tmp_dir = try file.allocTmpDir(std.heap.page_allocator);
3232
defer file.freeTmpDir(std.heap.page_allocator, tmp_dir);
3333
break :dir try cwd.openDir(tmp_dir, .{});
3434
};

src/os/file.zig

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const std = @import("std");
22
const builtin = @import("builtin");
33
const posix = std.posix;
4+
const windows = @import("windows.zig");
45

56
const log = std.log.scoped(.os);
67

@@ -57,27 +58,38 @@ pub fn restoreMaxFiles(lim: rlimit) void {
5758
/// path separator is stripped so callers can safely join with their
5859
/// own separator (e.g. `"{tmp}/{name}"`).
5960
///
60-
/// This may not actually allocate memory; use `freeTmpDir` to
61-
/// properly free the memory when applicable.
62-
pub fn allocTmpDir(allocator: std.mem.Allocator) ?[]const u8 {
61+
/// On Windows this calls `GetTempPathW` and allocates a UTF-8 copy
62+
/// (or duplicates a hard-fallback string if the syscall fails). On
63+
/// POSIX this returns `$TMPDIR`/`$TMP` (or `"/tmp"` as a fallback)
64+
/// without allocating. Always pair with `freeTmpDir` to release any
65+
/// allocation.
66+
pub fn allocTmpDir(allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
6367
if (builtin.os.tag == .windows) {
64-
// TODO: what is a good fallback path on windows?
65-
const v = std.process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("TMP")) orelse return null;
66-
return std.unicode.utf16LeToUtf8Alloc(allocator, v) catch |e| {
67-
log.warn("failed to convert temp dir path from windows string: {}", .{e});
68-
return null;
69-
};
68+
// GetTempPathW guarantees the result fits in MAX_PATH+1.
69+
var buf: [windows.MAX_PATH + 1:0]u16 = undefined;
70+
const len = windows.exp.kernel32.GetTempPathW(buf.len, &buf);
71+
if (len > 0) {
72+
// Trim the UTF-16 string before encoding as UT8-8 so that the
73+
// returned slice's length matches its underlying allocation.
74+
const trimmed = std.mem.trimEnd(u16, buf[0..len], &.{std.fs.path.sep});
75+
if (std.unicode.utf16LeToUtf8Alloc(allocator, trimmed)) |utf8| {
76+
return utf8;
77+
} else |e| switch (e) {
78+
error.OutOfMemory => return error.OutOfMemory,
79+
else => log.warn("failed to convert temp dir path from windows string: {}", .{e}),
80+
}
81+
}
82+
return allocator.dupe(u8, "C:\\Windows\\Temp");
7083
}
7184
const tmpdir = posix.getenv("TMPDIR") orelse posix.getenv("TMP") orelse return "/tmp";
7285
return std.mem.trimEnd(u8, tmpdir, &.{std.fs.path.sep});
7386
}
7487

75-
/// Free a path returned by tmpDir if it allocated memory.
76-
/// This is a "no-op" for all platforms except windows.
88+
/// Free a path returned by `allocTmpDir` if it allocated memory.
89+
/// This is a no-op on POSIX.
7790
pub fn freeTmpDir(allocator: std.mem.Allocator, dir: []const u8) void {
78-
if (builtin.os.tag == .windows) {
79-
allocator.free(dir);
80-
}
91+
if (builtin.os.tag != .windows) return;
92+
allocator.free(dir);
8193
}
8294

8395
const random_basename_bytes = 16;
@@ -109,7 +121,7 @@ pub fn randomTmpPath(
109121
allocator: std.mem.Allocator,
110122
prefix: []const u8,
111123
) std.mem.Allocator.Error![]u8 {
112-
const tmp_dir = allocTmpDir(allocator) orelse "/tmp";
124+
const tmp_dir = try allocTmpDir(allocator);
113125
defer freeTmpDir(allocator, tmp_dir);
114126
var name_buf: [random_basename_len]u8 = undefined;
115127
const basename = randomBasename(&name_buf) catch unreachable;

src/os/windows.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub const HANDLE = windows.HANDLE;
1818
pub const HANDLE_FLAG_INHERIT = windows.HANDLE_FLAG_INHERIT;
1919
pub const INFINITE = windows.INFINITE;
2020
pub const INVALID_HANDLE_VALUE = windows.INVALID_HANDLE_VALUE;
21+
pub const MAX_PATH = windows.MAX_PATH;
2122
pub const OPEN_EXISTING = windows.OPEN_EXISTING;
2223
pub const PIPE_ACCESS_OUTBOUND = windows.PIPE_ACCESS_OUTBOUND;
2324
pub const PIPE_TYPE_BYTE = windows.PIPE_TYPE_BYTE;
@@ -104,6 +105,11 @@ pub const exp = struct {
104105
lpBuffer: windows.LPSTR,
105106
nSize: *windows.DWORD,
106107
) callconv(.winapi) windows.BOOL;
108+
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw
109+
pub extern "kernel32" fn GetTempPathW(
110+
nBufferLength: windows.DWORD,
111+
lpBuffer: windows.LPWSTR,
112+
) callconv(.winapi) windows.DWORD;
107113
};
108114

109115
pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF;

src/terminal/kitty/graphics_image.zig

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -330,17 +330,16 @@ pub const LoadingImage = struct {
330330
fn isPathInTempDir(path: []const u8) bool {
331331
if (std.mem.startsWith(u8, path, "/tmp")) return true;
332332
if (std.mem.startsWith(u8, path, "/dev/shm")) return true;
333-
if (temp_dir.allocTmpDir(std.heap.page_allocator)) |dir| {
334-
defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
335-
if (std.mem.startsWith(u8, path, dir)) return true;
336-
337-
// The temporary dir is sometimes a symlink. On macOS for
338-
// example /tmp is /private/var/...
339-
var buf: [std.fs.max_path_bytes]u8 = undefined;
340-
if (posix.realpath(dir, &buf)) |real_dir| {
341-
if (std.mem.startsWith(u8, path, real_dir)) return true;
342-
} else |_| {}
343-
}
333+
const dir = temp_dir.allocTmpDir(std.heap.page_allocator) catch return false;
334+
defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
335+
if (std.mem.startsWith(u8, path, dir)) return true;
336+
337+
// The temporary dir is sometimes a symlink. On macOS for
338+
// example /tmp is /private/var/...
339+
var buf: [std.fs.max_path_bytes]u8 = undefined;
340+
if (posix.realpath(dir, &buf)) |real_dir| {
341+
if (std.mem.startsWith(u8, path, real_dir)) return true;
342+
} else |_| {}
344343

345344
return false;
346345
}

0 commit comments

Comments
 (0)