os: use GetTempPathW for allocTmpDir on Windows (#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.
This commit is contained in:
Mitchell Hashimoto
2026-04-25 20:49:54 -07:00
committed by GitHub
4 changed files with 43 additions and 26 deletions

View File

@@ -28,7 +28,7 @@ pub fn init() !TempDir {
const dir = dir: {
const cwd = std.fs.cwd();
const tmp_dir = file.allocTmpDir(std.heap.page_allocator) orelse break :dir cwd;
const tmp_dir = try file.allocTmpDir(std.heap.page_allocator);
defer file.freeTmpDir(std.heap.page_allocator, tmp_dir);
break :dir try cwd.openDir(tmp_dir, .{});
};

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const posix = std.posix;
const windows = @import("windows.zig");
const log = std.log.scoped(.os);
@@ -57,27 +58,38 @@ pub fn restoreMaxFiles(lim: rlimit) void {
/// path separator is stripped so callers can safely join with their
/// own separator (e.g. `"{tmp}/{name}"`).
///
/// This may not actually allocate memory; use `freeTmpDir` to
/// properly free the memory when applicable.
pub fn allocTmpDir(allocator: std.mem.Allocator) ?[]const u8 {
/// On Windows this calls `GetTempPathW` and allocates a UTF-8 copy
/// (or duplicates a hard-fallback string if the syscall fails). On
/// POSIX this returns `$TMPDIR`/`$TMP` (or `"/tmp"` as a fallback)
/// without allocating. Always pair with `freeTmpDir` to release any
/// allocation.
pub fn allocTmpDir(allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 {
if (builtin.os.tag == .windows) {
// TODO: what is a good fallback path on windows?
const v = std.process.getenvW(std.unicode.utf8ToUtf16LeStringLiteral("TMP")) orelse return null;
return std.unicode.utf16LeToUtf8Alloc(allocator, v) catch |e| {
log.warn("failed to convert temp dir path from windows string: {}", .{e});
return null;
};
// GetTempPathW guarantees the result fits in MAX_PATH+1.
var buf: [windows.MAX_PATH + 1:0]u16 = undefined;
const len = windows.exp.kernel32.GetTempPathW(buf.len, &buf);
if (len > 0) {
// Trim the UTF-16 string before encoding as UT8-8 so that the
// returned slice's length matches its underlying allocation.
const trimmed = std.mem.trimEnd(u16, buf[0..len], &.{std.fs.path.sep});
if (std.unicode.utf16LeToUtf8Alloc(allocator, trimmed)) |utf8| {
return utf8;
} else |e| switch (e) {
error.OutOfMemory => return error.OutOfMemory,
else => log.warn("failed to convert temp dir path from windows string: {}", .{e}),
}
}
return allocator.dupe(u8, "C:\\Windows\\Temp");
}
const tmpdir = posix.getenv("TMPDIR") orelse posix.getenv("TMP") orelse return "/tmp";
return std.mem.trimEnd(u8, tmpdir, &.{std.fs.path.sep});
}
/// Free a path returned by tmpDir if it allocated memory.
/// This is a "no-op" for all platforms except windows.
/// Free a path returned by `allocTmpDir` if it allocated memory.
/// This is a no-op on POSIX.
pub fn freeTmpDir(allocator: std.mem.Allocator, dir: []const u8) void {
if (builtin.os.tag == .windows) {
allocator.free(dir);
}
if (builtin.os.tag != .windows) return;
allocator.free(dir);
}
const random_basename_bytes = 16;
@@ -109,7 +121,7 @@ pub fn randomTmpPath(
allocator: std.mem.Allocator,
prefix: []const u8,
) std.mem.Allocator.Error![]u8 {
const tmp_dir = allocTmpDir(allocator) orelse "/tmp";
const tmp_dir = try allocTmpDir(allocator);
defer freeTmpDir(allocator, tmp_dir);
var name_buf: [random_basename_len]u8 = undefined;
const basename = randomBasename(&name_buf) catch unreachable;

View File

@@ -18,6 +18,7 @@ pub const HANDLE = windows.HANDLE;
pub const HANDLE_FLAG_INHERIT = windows.HANDLE_FLAG_INHERIT;
pub const INFINITE = windows.INFINITE;
pub const INVALID_HANDLE_VALUE = windows.INVALID_HANDLE_VALUE;
pub const MAX_PATH = windows.MAX_PATH;
pub const OPEN_EXISTING = windows.OPEN_EXISTING;
pub const PIPE_ACCESS_OUTBOUND = windows.PIPE_ACCESS_OUTBOUND;
pub const PIPE_TYPE_BYTE = windows.PIPE_TYPE_BYTE;
@@ -104,6 +105,11 @@ pub const exp = struct {
lpBuffer: windows.LPSTR,
nSize: *windows.DWORD,
) callconv(.winapi) windows.BOOL;
/// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw
pub extern "kernel32" fn GetTempPathW(
nBufferLength: windows.DWORD,
lpBuffer: windows.LPWSTR,
) callconv(.winapi) windows.DWORD;
};
pub const PROC_THREAD_ATTRIBUTE_NUMBER = 0x0000FFFF;

View File

@@ -330,17 +330,16 @@ pub const LoadingImage = struct {
fn isPathInTempDir(path: []const u8) bool {
if (std.mem.startsWith(u8, path, "/tmp")) return true;
if (std.mem.startsWith(u8, path, "/dev/shm")) return true;
if (temp_dir.allocTmpDir(std.heap.page_allocator)) |dir| {
defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
if (std.mem.startsWith(u8, path, dir)) return true;
const dir = temp_dir.allocTmpDir(std.heap.page_allocator) catch return false;
defer temp_dir.freeTmpDir(std.heap.page_allocator, dir);
if (std.mem.startsWith(u8, path, dir)) return true;
// The temporary dir is sometimes a symlink. On macOS for
// example /tmp is /private/var/...
var buf: [std.fs.max_path_bytes]u8 = undefined;
if (posix.realpath(dir, &buf)) |real_dir| {
if (std.mem.startsWith(u8, path, real_dir)) return true;
} else |_| {}
}
// The temporary dir is sometimes a symlink. On macOS for
// example /tmp is /private/var/...
var buf: [std.fs.max_path_bytes]u8 = undefined;
if (posix.realpath(dir, &buf)) |real_dir| {
if (std.mem.startsWith(u8, path, real_dir)) return true;
} else |_| {}
return false;
}