From ddb7c132a21359c537ff7c262e06eb2f60a75eb7 Mon Sep 17 00:00:00 2001 From: Marko Vejnovic Date: Mon, 20 Oct 2025 16:00:52 -0700 Subject: [PATCH] feat: Implement low-level os.zig abstractions --- cmake/targets/BuildBun.cmake | 17 +- .../generate_uv_posix_stubs_constants.ts | 1 + src/bun.js/bindings/uv-posix-stubs.c | 4 +- src/bun.js/node.zig | 21 ++ src/bun.js/node/node_os.zig | 81 +---- src/bun.zig | 2 + src/c-headers-for-zig.h | 2 + src/cli/install_completions_command.zig | 58 +-- src/cli/upgrade_command.zig | 12 +- src/env.zig | 47 +++ src/fs.zig | 68 ++-- src/install/PackageManager.zig | 6 +- .../PackageManager/PackageManagerOptions.zig | 5 +- src/install/repository.zig | 16 +- src/linker.lds | 2 +- src/os.zig | 66 ++++ src/os/posix.zig | 249 +++++++++++++ src/os/uvinterop.zig | 50 +++ src/os/win32.zig | 338 ++++++++++++++++++ src/sys.zig | 64 ++++ src/windows.zig | 25 ++ 21 files changed, 979 insertions(+), 155 deletions(-) create mode 100644 src/os.zig create mode 100644 src/os/posix.zig create mode 100644 src/os/uvinterop.zig create mode 100644 src/os/win32.zig diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index c31c8a4de5..7ad2afb8ab 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -1260,6 +1260,7 @@ endif() if(WIN32) target_link_libraries(${bun} PRIVATE + advapi32 winmm bcrypt ntdll @@ -1308,32 +1309,32 @@ if(NOT BUN_CPP_ONLY) OUTPUTS ${BUILD_PATH}/${bunStripExe} ) - + # Then sign both executables on Windows if(WIN32 AND ENABLE_WINDOWS_CODESIGNING) set(SIGN_SCRIPT "${CMAKE_SOURCE_DIR}/.buildkite/scripts/sign-windows.ps1") - + # Verify signing script exists if(NOT EXISTS "${SIGN_SCRIPT}") message(FATAL_ERROR "Windows signing script not found: ${SIGN_SCRIPT}") endif() - + # Use PowerShell for Windows code signing (native Windows, no path issues) - find_program(POWERSHELL_EXECUTABLE + find_program(POWERSHELL_EXECUTABLE NAMES pwsh.exe powershell.exe - PATHS + PATHS "C:/Program Files/PowerShell/7" "C:/Program Files (x86)/PowerShell/7" "C:/Windows/System32/WindowsPowerShell/v1.0" DOC "Path to PowerShell executable" ) - + if(NOT POWERSHELL_EXECUTABLE) set(POWERSHELL_EXECUTABLE "powershell.exe") endif() - + message(STATUS "Using PowerShell executable: ${POWERSHELL_EXECUTABLE}") - + # Sign both bun-profile.exe and bun.exe after stripping register_command( TARGET diff --git a/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts b/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts index b28f652ed9..884564476d 100644 --- a/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts +++ b/src/bun.js/bindings/libuv/generate_uv_posix_stubs_constants.ts @@ -1,5 +1,6 @@ export const test_skipped = [ "uv_getrusage_thread", + "uv_os_homedir", "uv_thread_detach", "uv_thread_getname", "uv_thread_getpriority", diff --git a/src/bun.js/bindings/uv-posix-stubs.c b/src/bun.js/bindings/uv-posix-stubs.c index 2ed27895e7..3be3fd0121 100644 --- a/src/bun.js/bindings/uv-posix-stubs.c +++ b/src/bun.js/bindings/uv-posix-stubs.c @@ -1129,10 +1129,10 @@ UV_EXTERN int uv_os_getpriority(uv_pid_t pid, int* priority) __builtin_unreachable(); } +extern int bunuv__os_homedir(char* buffer, size_t* size); UV_EXTERN int uv_os_homedir(char* buffer, size_t* size) { - __bun_throw_not_implemented("uv_os_homedir"); - __builtin_unreachable(); + return bunuv__os_homedir(buffer, size); } UV_EXTERN int uv_os_setenv(const char* name, const char* value) diff --git a/src/bun.js/node.zig b/src/bun.js/node.zig index 176f09f7d7..4395c9f0d4 100644 --- a/src/bun.js/node.zig +++ b/src/bun.js/node.zig @@ -152,6 +152,27 @@ pub fn Maybe(comptime ReturnTypeT: type, comptime ErrorTypeT: type) type { return null; } + /// Grab a const pointer to the value if it exists. + pub inline fn asValueConstPtr(this: *const @This()) ?*const ReturnType { + if (comptime ReturnType == void) + @compileError("Maybe.asValueConstPtr is invalid for ReturnType == void"); + return switch (this.*) { + .result => |*r| r, + .err => null, + }; + } + + /// Grab a mutable pointer to the value if it exists. + pub inline fn asValuePtr(this: *@This()) ?*ReturnType { + if (comptime ReturnType == void) + @compileError("Maybe.asValuePtr is invalid for ReturnType == void"); + + return switch (this.*) { + .result => |*r| r, + .err => null, + }; + } + pub inline fn isOk(this: *const @This()) bool { return switch (this.*) { .result => true, diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 186e60377e..4cc49bfa7e 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -302,80 +302,15 @@ pub fn getPriority(global: *jsc.JSGlobalObject, pid: i32) bun.JSError!i32 { } pub fn homedir(global: *jsc.JSGlobalObject) !bun.String { - // In Node.js, this is a wrapper around uv_os_homedir. - if (Environment.isWindows) { - var out: bun.PathBuffer = undefined; - var size: usize = out.len; - if (libuv.uv_os_homedir(&out, &size).toError(.uv_os_homedir)) |err| { + var home_res = bun.os.HomeDir.query(bun.default_allocator); + switch (home_res) { + .result => |*r| { + defer r.deinit(); + return bun.String.cloneUTF8(r.slice()); + }, + .err => |err| { return global.throwValue(err.toJS(global)); - } - return bun.String.cloneUTF8(out[0..size]); - } else { - - // The posix implementation of uv_os_homedir first checks the HOME - // environment variable, then falls back to reading the passwd entry. - if (bun.getenvZ("HOME")) |home| { - if (home.len > 0) - return bun.String.init(home); - } - - // From libuv: - // > Calling sysconf(_SC_GETPW_R_SIZE_MAX) would get the suggested size, but it - // > is frequently 1024 or 4096, so we can just use that directly. The pwent - // > will not usually be large. - // Instead of always using an allocation, first try a stack allocation - // of 4096, then fallback to heap. - var stack_string_bytes: [4096]u8 = undefined; - var string_bytes: []u8 = &stack_string_bytes; - defer if (string_bytes.ptr != &stack_string_bytes) - bun.default_allocator.free(string_bytes); - - var pw: bun.c.passwd = undefined; - var result: ?*bun.c.passwd = null; - - const ret = while (true) { - const ret = bun.c.getpwuid_r( - bun.c.geteuid(), - &pw, - string_bytes.ptr, - string_bytes.len, - &result, - ); - - if (ret == @intFromEnum(bun.sys.E.INTR)) - continue; - - // If the system call wants more memory, double it. - if (ret == @intFromEnum(bun.sys.E.RANGE)) { - const len = string_bytes.len; - bun.default_allocator.free(string_bytes); - string_bytes = ""; - string_bytes = try bun.default_allocator.alloc(u8, len * 2); - continue; - } - - break ret; - }; - - if (ret != 0) { - return global.throwValue(bun.sys.Error.fromCode( - @enumFromInt(ret), - .uv_os_homedir, - ).toJS(global)); - } - - if (result == null) { - // in uv__getpwuid_r, null result throws UV_ENOENT. - return global.throwValue(bun.sys.Error.fromCode( - .NOENT, - .uv_os_homedir, - ).toJS(global)); - } - - return if (pw.pw_dir) |dir| - bun.String.cloneUTF8(bun.span(dir)) - else - bun.String.empty; + }, } } diff --git a/src/bun.zig b/src/bun.zig index acaa0ec66a..364809df09 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3798,6 +3798,8 @@ pub fn getUseSystemCA(globalObject: *jsc.JSGlobalObject, callFrame: *jsc.CallFra return jsc.JSValue.jsBoolean(Arguments.Bun__Node__UseSystemCA); } +pub const os = @import("./os.zig"); + const CopyFile = @import("./copy_file.zig"); const builtin = @import("builtin"); const std = @import("std"); diff --git a/src/c-headers-for-zig.h b/src/c-headers-for-zig.h index daf0ead276..a4437dc6b5 100644 --- a/src/c-headers-for-zig.h +++ b/src/c-headers-for-zig.h @@ -23,7 +23,9 @@ #include "../packages/bun-native-bundler-plugin-api/bundler_plugin.h" #if POSIX +#include #include +#include #include #include #include diff --git a/src/cli/install_completions_command.zig b/src/cli/install_completions_command.zig index a205cf6c95..b5e08eb98a 100644 --- a/src/cli/install_completions_command.zig +++ b/src/cli/install_completions_command.zig @@ -25,8 +25,10 @@ pub const InstallCompletionsCommand = struct { // if that fails, try $HOME/.bun/bin outer: { - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { - target = std.fmt.bufPrint(&target_buf, "{s}/.bun/bin/" ++ bunx_name, .{home_dir}) catch unreachable; + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |home_dir| { + defer home_dir.deinit(); + target = std.fmt.bufPrint(&target_buf, "{s}/.bun/bin/" ++ bunx_name, .{home_dir.slice()}) catch unreachable; std.posix.symlink(exe, target) catch break :outer; return; } @@ -34,8 +36,10 @@ pub const InstallCompletionsCommand = struct { // if that fails, try $HOME/.local/bin outer: { - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { - target = std.fmt.bufPrint(&target_buf, "{s}/.local/bin/" ++ bunx_name, .{home_dir}) catch unreachable; + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |home_dir| { + defer home_dir.deinit(); + target = std.fmt.bufPrint(&target_buf, "{s}/.local/bin/" ++ bunx_name, .{home_dir.slice()}) catch unreachable; std.posix.symlink(exe, target) catch break :outer; return; } @@ -229,9 +233,11 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |home_dir| { + defer home_dir.deinit(); outer: { - var paths = [_]string{ home_dir, "./.config/fish/completions" }; + var paths = [_]string{ home_dir.slice(), "./.config/fish/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); break :found std.fs.openDirAbsolute(completions_dir, .{}) catch break :outer; @@ -287,10 +293,12 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |home_dir| { + defer home_dir.deinit(); { outer: { - var paths = [_]string{ home_dir, "./.oh-my-zsh/completions" }; + var paths = [_]string{ home_dir.slice(), "./.oh-my-zsh/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); break :found std.fs.openDirAbsolute(completions_dir, .{}) catch break :outer; @@ -299,7 +307,7 @@ pub const InstallCompletionsCommand = struct { { outer: { - var paths = [_]string{ home_dir, "./.bun" }; + var paths = [_]string{ home_dir.slice(), "./.bun" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); break :found std.fs.openDirAbsolute(completions_dir, .{}) catch break :outer; @@ -339,10 +347,12 @@ pub const InstallCompletionsCommand = struct { } } - if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |home_dir| { + defer home_dir.deinit(); { outer: { - var paths = [_]string{ home_dir, "./.oh-my-bash/custom/completions" }; + var paths = [_]string{ home_dir.slice(), "./.oh-my-bash/custom/completions" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); break :found std.fs.openDirAbsolute(completions_dir, .{}) catch @@ -351,7 +361,7 @@ pub const InstallCompletionsCommand = struct { } { outer: { - var paths = [_]string{ home_dir, "./.bash_completion.d" }; + var paths = [_]string{ home_dir.slice(), "./.bash_completion.d" }; completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto); break :found std.fs.openDirAbsolute(completions_dir, .{}) catch @@ -449,21 +459,25 @@ pub const InstallCompletionsCommand = struct { } second: { - if (bun.getenvZ(bun.DotEnv.home_env)) |zdot_dir| { - bun.copy(u8, &zshrc_filepath, zdot_dir); - bun.copy(u8, zshrc_filepath[zdot_dir.len..], "/.zshrc"); - zshrc_filepath[zdot_dir.len + "/.zshrc".len] = 0; - const filepath = zshrc_filepath[0 .. zdot_dir.len + "/.zshrc".len :0]; + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |zdot_dir| { + defer zdot_dir.deinit(); + bun.copy(u8, &zshrc_filepath, zdot_dir.slice()); + bun.copy(u8, zshrc_filepath[zdot_dir.slice().len..], "/.zshrc"); + zshrc_filepath[zdot_dir.slice().len + "/.zshrc".len] = 0; + const filepath = zshrc_filepath[0 .. zdot_dir.slice().len + "/.zshrc".len :0]; break :zshrc std.fs.openFileAbsoluteZ(filepath, .{ .mode = .read_write }) catch break :second; } } third: { - if (bun.getenvZ(bun.DotEnv.home_env)) |zdot_dir| { - bun.copy(u8, &zshrc_filepath, zdot_dir); - bun.copy(u8, zshrc_filepath[zdot_dir.len..], "/.zshenv"); - zshrc_filepath[zdot_dir.len + "/.zshenv".len] = 0; - const filepath = zshrc_filepath[0 .. zdot_dir.len + "/.zshenv".len :0]; + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + if (maybe_home_dir.asValuePtr()) |zdot_dir| { + defer zdot_dir.deinit(); + bun.copy(u8, &zshrc_filepath, zdot_dir.slice()); + bun.copy(u8, zshrc_filepath[zdot_dir.slice().len..], "/.zshenv"); + zshrc_filepath[zdot_dir.slice().len + "/.zshenv".len] = 0; + const filepath = zshrc_filepath[0 .. zdot_dir.slice().len + "/.zshenv".len :0]; break :zshrc std.fs.openFileAbsoluteZ(filepath, .{ .mode = .read_write }) catch break :third; } } diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index a372098a8b..65d00fc179 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -557,7 +557,7 @@ pub const UpgradeCommand = struct { save_dir.deleteFileZ(tmpname) catch {}; Global.exit(1); } - } else if (Environment.isWindows) { + } else if (comptime Environment.isWindows) { // Run a powershell script to unzip the file const unzip_script = try std.fmt.allocPrint( ctx.allocator, @@ -572,7 +572,15 @@ pub const UpgradeCommand = struct { const powershell_path = bun.which(&buf, bun.getenvZ("PATH") orelse "", "", "powershell") orelse hardcoded_system_powershell: { - const system_root = bun.getenvZ("SystemRoot") orelse "C:\\Windows"; + var windir = switch (bun.os.win32.WinDir.query(ctx.allocator)) { + .err => { + Output.prettyErrorln("error: Failed to unzip {s} due to PowerShell not being installed.", .{tmpname}); + Global.exit(1); + }, + .result => |v| v, + }; + defer windir.deinit(); + const system_root = windir.slice(); const hardcoded_system_powershell = bun.path.joinAbsStringBuf(system_root, &buf, &.{ system_root, "System32\\WindowsPowerShell\\v1.0\\powershell.exe" }, .windows); if (bun.sys.exists(hardcoded_system_powershell)) { break :hardcoded_system_powershell hardcoded_system_powershell; diff --git a/src/env.zig b/src/env.zig index 3e2881a53f..d0bb192445 100644 --- a/src/env.zig +++ b/src/env.zig @@ -172,6 +172,53 @@ else if (isAarch64) else @compileError("Please add your architecture to the Architecture enum"); +pub const OsTypeSet = struct { + posix: ?type = null, + win: ?type = null, + macos: ?type = null, + linux: ?type = null, +}; + +/// Given a type set, which contains possible types for different platforms, selects the correct +/// type for the current platform. +pub fn OsTypeSelect(comptime opts: OsTypeSet) type { + const err_fmt = "You must specify a type for {} when compiling for {}"; + + switch (comptime bun.Environment.os) { + .windows => { + if (comptime opts.win) |w| { + return w; + } + @compileError(std.fmt.comptimePrint(err_fmt, .{ "win", "Windows" })); + }, + .mac => { + if (comptime opts.macos) |m| { + return m; + } + + if (comptime opts.posix) |p| { + return p; + } + + @compileError(std.fmt.comptimePrint(err_fmt, .{ "macos", "macOS" })); + }, + .linux => { + if (comptime opts.linux) |l| { + return l; + } + + if (comptime opts.posix) |p| { + return p; + } + + @compileError(std.fmt.comptimePrint(err_fmt, .{ "linux", "Linux" })); + }, + .wasm => { + @compileError("OsTypeSelect is not supported for wasm targets. Please add support."); + }, + } +} + const builtin = @import("builtin"); const bun = @import("bun"); const std = @import("std"); diff --git a/src/fs.zig b/src/fs.zig index 912cdbcecb..30aefbc6b4 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -530,43 +530,47 @@ pub const FileSystem = struct { file_limit: usize = 32, file_quota: usize = 32, - pub var win_tempdir_cache: ?[]const u8 = undefined; + pub var sys_tempdir_cache: ?bun.os.SysTmpDir = null; + var sys_tempdir_mutex: Mutex = .{}; + + fn cleanupSysTempdirCache() callconv(.C) void { + if (sys_tempdir_cache) |*c| { + c.deinit(); + } + } pub fn platformTempDir() []const u8 { - return switch (Environment.os) { - // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-gettemppathw#remarks - .windows => win_tempdir_cache orelse { - const value = bun.getenvZ("TEMP") orelse bun.getenvZ("TMP") orelse brk: { - if (bun.getenvZ("SystemRoot") orelse bun.getenvZ("windir")) |windir| { - break :brk std.fmt.allocPrint( - bun.default_allocator, - "{s}\\Temp", - .{strings.withoutTrailingSlash(windir)}, - ) catch |err| bun.handleOom(err); - } + if (sys_tempdir_cache) |w| { + return w.slice(); + } - if (bun.getenvZ("USERPROFILE")) |profile| { - var buf: bun.PathBuffer = undefined; - var parts = [_]string{"AppData\\Local\\Temp"}; - const out = bun.path.joinAbsStringBuf(profile, &buf, &parts, .loose); - break :brk bun.handleOom(bun.default_allocator.dupe(u8, out)); - } + var temp_dir = bun.os.SysTmpDir.query(bun.default_allocator); + switch (temp_dir) { + .result => |*s| { + sys_tempdir_mutex.lock(); + defer sys_tempdir_mutex.unlock(); + // After acquiring the mutex, we need to check sys_tempdir_cache again. There + // is a race condition -- another thread may have acquired the lock just prior + // to this thread but this thread will enter the current scope since it there + // is a race condition on sys_temp_dir_cache. Consequently, this thread has to + // check sys_tempdir_cache again. + if (sys_tempdir_cache) |w| { + return w.slice(); + } - var tmp_buf: bun.PathBuffer = undefined; - const cwd = std.posix.getcwd(&tmp_buf) catch @panic("Failed to get cwd for platformTempDir"); - const root = bun.path.windowsFilesystemRoot(cwd); - break :brk std.fmt.allocPrint( - bun.default_allocator, - "{s}\\Windows\\Temp", - .{strings.withoutTrailingSlash(root)}, - ) catch |err| bun.handleOom(err); - }; - win_tempdir_cache = value; - return value; + defer s.deinit(); + + sys_tempdir_cache = s.*; + // TODO(markovejnovic): Using atexit for cleanup is pretty bad practice. + // Ideally we would have a bun mechanism for cleanup that could register within + // __fini, but my PR is not responsible to that right now. + _ = bun.c.atexit(cleanupSysTempdirCache); + return sys_tempdir_cache.?.slice(); }, - .mac => "/private/tmp", - else => "/tmp", - }; + .err => |e| { + bun.Output.panic("Could not retrieve the system temporary directory: {}", .{e}); + }, + } } pub const Tmpfile = switch (Environment.os) { diff --git a/src/install/PackageManager.zig b/src/install/PackageManager.zig index 41ebb0629f..8a29414842 100644 --- a/src/install/PackageManager.zig +++ b/src/install/PackageManager.zig @@ -791,7 +791,11 @@ pub fn init( try env.load(entries_option.entries, &[_][]u8{}, .production, false); initializeStore(); - if (bun.getenvZ("XDG_CONFIG_HOME") orelse bun.getenvZ(bun.DotEnv.home_env)) |data_dir| { + + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + defer if (maybe_home_dir.asValuePtr()) |h| h.deinit(); + const home_dir = if (maybe_home_dir.asValuePtr()) |h| h.slice() else null; + if (bun.getenvZ("XDG_CONFIG_HOME") orelse home_dir) |data_dir| { var buf: bun.PathBuffer = undefined; var parts = [_]string{ "./.npmrc", diff --git a/src/install/PackageManager/PackageManagerOptions.zig b/src/install/PackageManager/PackageManagerOptions.zig index 2437c1ec44..6d83950355 100644 --- a/src/install/PackageManager/PackageManagerOptions.zig +++ b/src/install/PackageManager/PackageManagerOptions.zig @@ -224,7 +224,10 @@ pub fn openGlobalBinDir(opts_: ?*const Api.BunInstall) !std.fs.Dir { return try std.fs.cwd().makeOpenPath(path, .{}); } - if (bun.getenvZ("XDG_CACHE_HOME") orelse bun.getenvZ(bun.DotEnv.home_env)) |home_dir| { + var maybe_home_dir = bun.os.HomeDir.query(bun.default_allocator); + defer if (maybe_home_dir.asValuePtr()) |h| h.deinit(); + const hdir = if (maybe_home_dir.asValuePtr()) |h| h.slice() else null; + if (bun.getenvZ("XDG_CACHE_HOME") orelse hdir) |home_dir| { var buf: bun.PathBuffer = undefined; var parts = [_]string{ ".bun", diff --git a/src/install/repository.zig b/src/install/repository.zig index 59feeef8b6..7a0f8a0c18 100644 --- a/src/install/repository.zig +++ b/src/install/repository.zig @@ -16,21 +16,11 @@ const SloppyGlobalGitConfig = struct { } pub fn loadAndParse() void { - const home_dir_path = brk: { - if (comptime Environment.isWindows) { - if (bun.getenvZ("USERPROFILE")) |env| - break :brk env; - } else { - if (bun.getenvZ("HOME")) |env| - break :brk env; - } - - // won't find anything - return; - }; + var home_dir = (bun.os.HomeDir.query(bun.default_allocator)).unwrap() catch return; + defer home_dir.deinit(); var config_file_path_buf: bun.PathBuffer = undefined; - const config_file_path = bun.path.joinAbsStringBufZ(home_dir_path, &config_file_path_buf, &.{".gitconfig"}, .auto); + const config_file_path = bun.path.joinAbsStringBufZ(home_dir.slice(), &config_file_path_buf, &.{".gitconfig"}, .auto); var stack_fallback = std.heap.stackFallback(4096, bun.default_allocator); const allocator = stack_fallback.get(); const source = File.toSource(config_file_path, allocator, .{ .convert_bom = true }).unwrap() catch { diff --git a/src/linker.lds b/src/linker.lds index 401cc35b60..81f6bbfd7a 100644 --- a/src/linker.lds +++ b/src/linker.lds @@ -321,7 +321,7 @@ BUN_1.2 { uv_write2; uv_wtf8_length_as_utf16; uv_wtf8_to_utf16; - + extern "C++" { v8::*; diff --git a/src/os.zig b/src/os.zig new file mode 100644 index 0000000000..cc968cf346 --- /dev/null +++ b/src/os.zig @@ -0,0 +1,66 @@ +//! OS-level functionality. +//! Designed to be slightly higher level than sys.zig + +/// Platform-agnostic utility for fetching and interacting with the current system home directory. +/// +/// Implements the following concept: +/// ``` +/// fn HomeDirConcept(T type) concept { +/// fn deinit(self: *T) void; +/// /// Deduces the current user's home directory. +/// /// +/// /// Has multiple strategies depending on the platform and environment. Mostly compliant with +/// /// the POSIX standard for POSIX systems. +/// /// +/// /// Tries to be clever on Windows, but no compliance guarantees are made. +/// /// +/// /// There is actually a reasonable desire to be able to query other users' home directories. +/// /// However, we don't have a need to do that at this moment. See +/// /// doi:10.1109/IEEESTD.2018.8277153 2.6.1 for further details. +/// /// +/// /// Very close to how uv_os_homedir works in libuv. +/// fn query(allocator: std.mem.Allocator) bun.sys.Maybe(T); +/// fn slice(self: *const T) []const u8; +/// } +/// ``` +pub const HomeDir = bun.Environment.OsTypeSelect(.{ + .posix = posix.HomeDir, + .win = win32.UserProfile, +}); + +/// Interact with the current system temporary directory. +/// +/// The system temporary directory is a directory equivalent to `/tmp` on POSIX systems. Note that +/// this is not ALWAYS /tmp, as `$TMPDIR` and other mechanisms MAY allow overriding this. Treat +/// this as a black box for maximum compatibility. +/// +/// Note: Bun also allows users to explicitly specify the temporary directory bun should use, so +/// this function alone does not tell you which temporary directory you should commonly use. +/// +/// Implements the following concept: +/// ``` +/// fn SysTempDirConcept(T type) concept { +/// fn deinit(self: *T) void; +/// fn query(allocator: std.mem.Allocator) bun.sys.Maybe(T); +/// fn slice(self: *const T) []const u8; +/// } +/// ``` +pub const SysTmpDir = bun.Environment.OsTypeSelect(.{ + .posix = posix.SysTmpDir, + .win = win32.TempDir, +}); + +pub const win32 = @import("./os/win32.zig"); +pub const posix = @import("./os/posix.zig"); + +comptime { + // TODO(markovejnovic): This probably shouldn't exist in src/os, but rather in another spot. + if (bun.Environment.isPosix) { + // uvinterop only supports POSIX at the time of writing. No need to pollute the Windows + // binary with it. + _ = @import("./os/uvinterop.zig"); // Necessary to link into the binary. + } +} + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/os/posix.zig b/src/os/posix.zig new file mode 100644 index 0000000000..ba20923030 --- /dev/null +++ b/src/os/posix.zig @@ -0,0 +1,249 @@ +/// Opaque type representing the user ID. +pub const Uid = if (bun.Environment.isPosix) struct { + const Self = @This(); + + _underlying: std.c.uid_t, + + pub fn queryEffective() Self { + // TODO(markovejnovic): Unfortunately, geteuid is not available in Zig's stdlib at the + // moment, so we need to use @cImport above. + return .{ ._underlying = bun.c.geteuid() }; + } + + pub fn queryReal() Self { + // TODO(markovejnovic): Unfortunately, getuid is not available in Zig's stdlib at the + // moment, so we need to use @cImport above. + return .{ ._underlying = bun.c.getuid() }; + } + + /// Falls back to queryEffective. + pub fn queryCurrent() Self { + return queryEffective(); + } + + fn init(underlying: std.c.uid_t) Self { + return .{ ._underlying = underlying }; + } +} else struct {}; + +/// Fully managed representation of a passwd entry. +pub const PasswdEntry = if (bun.Environment.isPosix) struct { + const Self = @This(); + + var _pwbuf_size: std.atomic.Value(usize) = .init(0); + + allocator: std.mem.Allocator, + buffer: []u8, + entry: bun.c.struct_passwd, + entry_ptr: *bun.c.struct_passwd, + + #memo: struct { + pw_dir_len: ?usize = null, + } = .{}, + + pub fn deinit(self: *Self) void { + self.allocator.free(self.buffer); + } + + pub fn pwNameZ(self: *Self) ?[*:0]u8 { + return self.entry_ptr.pw_name; + } + + pub fn pwUidC(self: *const Self) std.c.uid_t { + return self.entry_ptr.pw_uid; + } + + pub fn pwUid(self: *const Self) Uid { + return .init(self.pwUidC()); + } + + pub fn pwGidC(self: *Self) std.c.gid_t { + return self.entry_ptr.pw_gid; + } + + pub fn pwDir(self: *Self) ?[]u8 { + self.#memo.pw_dir_len = std.mem.len(self.entry_ptr.pw_dir); + return self.entry_ptr.pw_dir[0..self.#memo.pw_dir_len.?]; + } + + pub fn pwDirZ(self: *Self) ?[*:0]u8 { + return self.entry_ptr.pw_dir; + } + + pub fn pwShellZ(self: *Self) ?[*:0]u8 { + return self.entry_ptr.pw_shell; + } + + /// Attempt to retrieve the result of getpwuid_r. + /// + /// May spinlock for an extremely brief period of time as it allocates memory. + pub fn init(user: Uid, alloc: std.mem.Allocator) bun.sys.Maybe(Self) { + // Every iteration we will increase the buffer size by this factor. + const buf_size_gain = 2; + + // Claude said that _SC_GETPW_R_SIZE_MAX is by default 1024 on Linux. I don't trust Claude + // with anything so we're going 4X that. + const default_buf_size: usize = 4096; + + // The maximum total number of attempts we will have at reading getpwuid_r before giving + // up. There are a few cases which benefit from re-attempting a read. + const max_buf_size: comptime_int = 1 * 1024 * 1024; // If we need to load a 1MB buffer, + // something is very wrong. + + var buffer_size: usize = Self._pwbuf_size.load(.monotonic); + if (buffer_size == 0) { + // We know that we haven't initialized it yet. Let's try to do so now. + const initial_buf_size: usize = + @intCast(bun.sys.sysconf(bun.c._SC_GETPW_R_SIZE_MAX, .{ + .default = default_buf_size, + })); + + buffer_size = initial_buf_size; + } + + var result: ?*bun.c.struct_passwd = undefined; + var self: Self = .{ + .allocator = alloc, + .buffer = alloc.alloc(u8, buffer_size) catch { + return .initErr(.fromCode(.NOMEM, .getpwuid_r)); + }, + .entry = undefined, + .entry_ptr = undefined, + }; + errdefer self.allocator.free(self.buffer); + + while (buffer_size <= max_buf_size) { + const rc = bun.c.getpwuid_r( + user._underlying, + &self.entry, + self.buffer.ptr, + self.buffer.len, + @ptrCast(&result), + ); + if (rc == 0) { + // Since we found the correct buffer size, let's store it for future use. + Self._pwbuf_size.store(buffer_size, .monotonic); + + if (result) |r| { + self.entry_ptr = r; + return .initResult(self); + } + + return .initErr(.fromCode(.NOENT, .getpwuid_r)); + } + + const err = std.posix.errno(rc); + switch (err) { + .NOMEM, .RANGE => { + // ENOMEM -- Insufficient memory to allocate passwd structure. + // ERANGE -- Insufficient buffer space supplied. + buffer_size *= buf_size_gain; + self.buffer = alloc.realloc(self.buffer, buffer_size) catch { + return .initErr(.fromCode(.NOMEM, .getpwuid_r)); + }; + continue; + }, + .INTR => { + // We got hit by a signal, let's just try again. + continue; + }, + .IO, .MFILE, .NFILE => { + return .initErr(.fromCode(err, .getpwuid_r)); + }, + else => { + // The man page says we have covered the total set of error codes. + @branchHint(.cold); + return .initErr(.fromCode(err, .getpwuid_r)); + }, + } + } + + return .initErr(.fromCode(.SRCH, .getpwuid_r)); + } +} else struct {}; + +/// Utilities for fetching and interacting with the current user's home directory. +pub const HomeDir = if (bun.Environment.isPosix) struct { + const Self = @This(); + + #path: union(enum) { + managed: PasswdEntry, + unmanaged: []const u8, + }, + + pub fn slice(self: *Self) []const u8 { + return switch (self.#path) { + // Safe to unwrap since all processes which add a .managed field validate that the + // field is non-null. + .managed => self.#path.managed.pwDir().?, + .unmanaged => self.#path.unmanaged, + }; + } + + pub fn deinit(self: *Self) void { + switch (self.#path) { + .managed => self.#path.managed.deinit(), + .unmanaged => {}, + } + } + + /// Deduces the current user's home directory on POSIX systems. + /// + /// Whichever of the following returns a value is returned. + /// - Per doi:10.1109/IEEESTD.2018.8277153, the `$HOME` variable. + /// - The `getpwuid_r` function. + pub fn query(allocator: std.mem.Allocator) bun.sys.Maybe(Self) { + if (bun.getenvZ("HOME") orelse bun.getenvZ("USERPROFILE")) |home| { + // The user may override $HOME with a non-absolute path. We know that's wrong. + if (std.fs.path.isAbsolute(home)) { + return .initResult(.{ .#path = .{ .unmanaged = home } }); + } + } + + var passwd_entry = PasswdEntry.init(Uid.queryCurrent(), allocator); + switch (passwd_entry) { + .err => |e| { + return .initErr(e); + }, + .result => |*r| { + return if (r.pwDir() != null) + .initResult(.{ .#path = .{ .managed = r.* } }) + else + .initErr(.fromCode(.NOENT, .getpwuid_r)); + }, + } + } +} else struct {}; + +/// Work with the system temporary directory. +pub const SysTmpDir = if (bun.Environment.isPosix) struct { + const Self = @This(); + + pub fn deinit(self: *Self) void { + _ = self; // self is unused, it's there for concept compatibility. + } + + pub fn slice(self: *const Self) []const u8 { + _ = self; // self is unused, it's there for concept compatibility. + + // Implementations are encouraged to provide suitable directory names in the environment + // variable TMPDIR and applications are encouraged to use the contents of TMPDIR for + // creating temporary files. + // - IEEE Std 1003_1-2017 (POSIX 2017) + if (bun.getenvZ("TMPDIR")) |tmpdir| { + return tmpdir; + } + + // The /tmp directory is retained in POSIX.1-2017 to accommodate historical applications + // that assume its availability. + return "/tmp"; + } + + pub fn query(allocator: std.mem.Allocator) bun.sys.Maybe(Self) { + _ = allocator; // Never allocates, used for conformance to + return .initResult(.{}); + } +} else struct {}; + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/os/uvinterop.zig b/src/os/uvinterop.zig new file mode 100644 index 0000000000..1bbc1086a0 --- /dev/null +++ b/src/os/uvinterop.zig @@ -0,0 +1,50 @@ +//! Utilities for interoping with libuv. +//! +//! TODO(markovejnovic): This file probably doesn't belong in src/os. Consider moving it to +//! src/bunuv or similar. +extern fn uv_translate_sys_error(sys_errno: c_int) callconv(.C) c_int; +fn translateSysErrorToUv(sys_errno: c_int) c_int { + // TODO(markovejnovic): This is a hack because uv_translate_sys_error is stubbed out on POSIX. + if (bun.Environment.isPosix) { + // Exactly matches libuv's behavior. + return if (sys_errno <= 0) sys_errno else -sys_errno; + } + + if (bun.Environment.isWindows) { + return uv_translate_sys_error(sys_errno); + } +} + +pub export fn bunuv__os_homedir(buffer: ?[*]u8, size: ?*usize) callconv(.C) c_int { + if (buffer == null or size == null) { + return translateSysErrorToUv(@intFromEnum(bun.sys.E.INVAL)); + } + + // TODO(markovejnovic): This implementation could be slightly better. I don't know how to + // return the total size needed (from os.HomeDir.query) if the buffer is + // too small. + var homedir = bun.os.HomeDir.query(bun.default_allocator); + switch (homedir) { + .result => |*r| { + defer r.deinit(); + + const out = r.slice(); + // +1 for null terminator + if (out.len + 1 > size.?.*) { + size.?.* = out.len + 1; + return libuv.UV_ENOBUFS; + } + + @memcpy(buffer.?[0..out.len], out); + buffer.?[out.len] = 0; // null terminator + size.?.* = out.len + 1; + return 0; + }, + .err => |err| { + return translateSysErrorToUv(err.errno); + }, + } +} + +const bun = @import("bun"); +const libuv = @import("../deps/libuv.zig"); diff --git a/src/os/win32.zig b/src/os/win32.zig new file mode 100644 index 0000000000..0f1116feea --- /dev/null +++ b/src/os/win32.zig @@ -0,0 +1,338 @@ +//! Module for higher-level Windows utilities. Unlike sys.zig, this module has a slightly higher +//! level of abstraction and is not a direct mapping to Win32 APIs, but is still very +//! Windows-specific. + +/// Utilities for retrieving C:\Windows, D:\Windows or whatnot. Does not support POSIX. +pub const WinDir = if (bun.Environment.isWindows) struct { + const Self = @This(); + + #path: union(enum) { + managed: paths.AutoAbsPath, + unmanaged: []const u8, + }, + + pub fn slice(self: *const Self) []const u8 { + return switch (self.#path) { + .managed => self.#path.managed.slice(), + .unmanaged => self.#path.unmanaged, + }; + } + + pub fn deinit(self: *Self) void { + switch (self.#path) { + .managed => return self.#path.managed.deinit(), + .unmanaged => {}, + } + } + + /// Query the system installation directory, typically C:\Windows. + /// + /// May or may not allocate. Make no assumptions -- call .deinit(), as that will handle both + /// cases for you. + pub fn query(allocator: std.mem.Allocator) bun.sys.Maybe(Self) { + const env_path = bun.getenvZ("WINDIR") orelse bun.getenvZ("SYSTEMROOT"); + if (env_path) |p| { + return .initResult(.{ .#path = .{ .unmanaged = p } }); + } + + // This is the slow and expensive path -- we have to actually query the system. Oof. + var stack = std.heap.stackFallback(bun.windows.MAX_PATH * @sizeOf(u16), allocator); + var alloc = stack.get(); + + var wchar_buf = alloc.alloc(u16, bun.windows.MAX_PATH) catch { + return .initErr(.fromCode(.NOMEM, .GetSystemWindowsDirectoryW)); + }; + defer alloc.free(wchar_buf); + + const rc = queryIntoSlice(wchar_buf); + switch (rc) { + .err => |e| { + return .initErr(e); + }, + .result => |r| { + if (r <= wchar_buf.len) { + var path = paths.AutoAbsPath.init(); + // GetSystemWindowsDirectoryW returns the length without the null terminator. + path.append(wchar_buf[0..@as(usize, @intCast(r))]); + return .initResult(.{ .#path = .{ .managed = path } }); + } + + // We need a bigger buffer. + wchar_buf = alloc.realloc(wchar_buf, @as(usize, @intCast(r))) catch { + return .initErr(.fromCode(.NOMEM, .GetSystemWindowsDirectoryW)); + }; + const rc2 = queryIntoSlice(wchar_buf); + + switch (rc2) { + .err => |e| { + return .initErr(e); + }, + .result => |r2| { + if (r2 <= wchar_buf.len) { + var path = paths.AutoAbsPath.init(); + // GetSystemWindowsDirectoryW returns the length without the null + // terminator. + path.append(wchar_buf[0..@as(usize, @intCast(r2))]); + return .initResult(.{ .#path = .{ .managed = path } }); + } + }, + } + + // If the buffer is STILL too small, then the system is pulling our leg. + bun.Output.panic("GetSystemWindowsDirectoryW keeps returning a size larger than " ++ + "the buffer we provide. This is a bug in Bun. Please report it.", .{}); + }, + } + } + + fn queryIntoSlice(wchar_buf: []u16) bun.sys.Maybe(usize) { + const rc = bun.windows.GetSystemWindowsDirectoryW( + @ptrCast(wchar_buf.ptr), + @intCast(wchar_buf.len), + ); + + // If the function fails, the return value is zero. To get extended error information, call + // GetLastError. + if (rc == 0) { + const err = bun.windows.GetLastError(); + // TODO(markovejnovic): This whole conversion between Win32Error -> SystemErrno -> E + // feels very wrong. + return .initErr(.fromCode(@enumFromInt(@intFromEnum(err)), .GetSystemWindowsDirectoryW)); + } + + // If the function succeeds, the return value is the length of the string copied to the + // buffer, in TCHARs, not including the terminating null character. + // If the length is greater than the size of the buffer, the return value is the size of + // the buffer required to hold the path. + // + // In both cases, we return the actual length. + return .initResult(rc); + } +} else struct {}; + +/// Utilities for retrieving C:\Users\ or whatnot. +pub const UserProfile = if (bun.Environment.isWindows) struct { + const Self = @This(); + + #path: union(enum) { + managed: paths.AutoAbsPath, + unmanaged: []const u8, + }, + + pub fn slice(self: *const Self) []const u8 { + return switch (self.#path) { + .managed => self.#path.managed.slice(), + .unmanaged => self.#path.unmanaged, + }; + } + + pub fn deinit(self: *Self) void { + switch (self.#path) { + .managed => return self.#path.managed.deinit(), + .unmanaged => {}, + } + } + + pub fn query(allocator: std.mem.Allocator) bun.sys.Maybe(Self) { + const env_path = bun.getenvZ("HOME"); + if (env_path) |p| { + // @markovejnovic: + // This feels very counter-intuitive to me but it matches what libuv does -- we run + // some heuristics to test the quality of the environment variable. I think this is + // kind of misguided, but decided not to deviate from libuv here. + if (p.len < "C:\\".len) { + return .initErr(.fromCode(.NOENT, .GetUserProfileDirectoryW)); + } + + return .initResult(.{ .#path = .{ .unmanaged = p } }); + } + + var stack = std.heap.stackFallback(bun.windows.MAX_PATH * @sizeOf(u16), allocator); + var alloc = stack.get(); + + var wchar_buf = alloc.alloc(u16, bun.windows.MAX_PATH) catch { + return .initErr(.fromCode(.NOMEM, .GetUserProfileDirectoryW)); + }; + defer alloc.free(wchar_buf); + + const rc = queryIntoSlice(wchar_buf); + switch (rc) { + .err => |e| { + return .initErr(e); + }, + .result => |r| { + if (r <= wchar_buf.len) { + var path = paths.AutoAbsPath.init(); + // -1 because we don't want the null terminator. + path.append(wchar_buf[0..@as(usize, if (r == 0) 0 else @intCast(r - 1))]); + return .initResult(.{ .#path = .{ .managed = path } }); + } + + // We need a bigger buffer. + wchar_buf = alloc.realloc(wchar_buf, @as(usize, @intCast(r))) catch { + return .initErr(.fromCode(.NOMEM, .GetUserProfileDirectoryW)); + }; + const rc2 = queryIntoSlice(wchar_buf); + + switch (rc2) { + .err => |e| { + return .initErr(e); + }, + .result => |r2| { + if (r2 <= wchar_buf.len) { + var path = paths.AutoAbsPath.init(); + // -1 because we don't want the null terminator. + path.append( + wchar_buf[0..@as(usize, if (r2 == 0) 0 else @intCast(r2 - 1))], + ); + return .initResult(.{ .#path = .{ .managed = path } }); + } + }, + } + + // If the buffer is STILL too small, then the system is pulling our leg. + bun.Output.panic("GetUserProfileDirectoryW keeps returning a size larger than " ++ + "the buffer we provide. This is a bug in Bun. Please report it.", .{}); + }, + } + } + + fn queryIntoSlice(wchar_buf: []u16) bun.sys.Maybe(usize) { + var proc_token: bun.windows.HANDLE = undefined; + const proc_hndl = bun.windows.GetCurrentProcess(); + var rc = bun.windows.OpenProcessToken(proc_hndl, bun.windows.TOKEN_QUERY, &proc_token); + if (rc == bun.windows.FALSE) { + const err = bun.windows.GetLastError(); + // TODO(markovejnovic): This whole conversion between Win32Error -> SystemErrno -> E + // feels very wrong. + return .initErr(.fromCode(@enumFromInt(@intFromEnum(err)), .OpenProcessToken)); + } + defer _ = bun.windows.CloseHandle(proc_token); + + var path_size: bun.windows.DWORD = @as(bun.windows.DWORD, @intCast(wchar_buf.len)); + rc = bun.windows.GetUserProfileDirectoryW( + proc_token, + @ptrCast(wchar_buf.ptr), + &path_size, + ); + + if (rc == bun.windows.FALSE and + path_size <= @as(bun.windows.DWORD, @intCast(wchar_buf.len))) + { + // Otherwise we found ourselves some really weird error. + const err = bun.windows.GetLastError(); + // TODO(markovejnovic): This whole conversion between Win32Error -> SystemErrno -> E + // feels very wrong. + return .initErr(.fromCode(@enumFromInt(@intFromEnum(err)), .GetUserProfileDirectoryW)); + } + + // Either the buffer was too small (in which case path_size contains the required size), + // or we succeeded (in which case path_size contains the actual size). + return .initResult(@intCast(path_size)); + } +} else struct {}; + +// Windows-specific TMP/TEMP directory retrieval. +pub const TempDir = if (bun.Environment.isWindows) struct { + const Self = @This(); + + #path: union(enum) { + unmanaged: []const u8, + managed: struct { + allocator: std.mem.Allocator, + buf: []u8, + }, + }, + + pub fn slice(self: *const Self) []const u8 { + return switch (self.#path) { + .unmanaged => self.#path.unmanaged, + .managed => |*m| m.buf, + }; + } + + pub fn deinit(self: *Self) void { + switch (self.#path) { + .unmanaged => {}, + .managed => |*m| { + m.allocator.free(m.buf); + }, + } + } + + pub fn query(allocator: std.mem.Allocator) bun.sys.Maybe(Self) { + // Kind of mimics what GetTempPath2 does. + // + // Microsoft says: + // For non-system processes, the GetTempPath2 function checks for the existence of + // environment variables in the following order and uses the first path found: + // 1. The path specified by the TMP environment variable. + // 2. The path specified by the TEMP environment variable. + // 3. The path specified by the USERPROFILE environment variable. + // 4. The Windows directory. + // The maximum possible return value is MAX_PATH+1 (261). + // + // They also say: + // + // When calling this function from a process running as SYSTEM it will + // return the path C:\Windows\SystemTemp, which is inaccessible to non-SYSTEM processes. + // For non-SYSTEM processes, GetTempPath2 will behave the same as GetTemppaths. + // + // For system processes, the GetTempPath2 function checks for the existence of the + // environment variable SystemTemp. If this environment variable is set, it will use the + // value of the environment variable as the path instead of the default system provided + // path on the C: drive. + // + // We do not implement this SYSTEM-specific behavior. There is no reason to call Bun as a + // SYSTEM process. + // + // Furthermore, we deviate from the documented behavior of GetTempPath2 since it's fucking + // stupid -- why on earth would we ever want to polute the user directory, or worse even + // the Windows directory? That makes zero sense. Therefore, if we do fall that far down the + // list, we will append temp to the path. + // + // This matches Node and is close to what CPython does. + if (bun.getenvZ("TMP")) |t| { + return .initResult(.{ .#path = .{ .unmanaged = t } }); + } + + if (bun.getenvZ("TEMP")) |t| { + return .initResult(.{ .#path = .{ .unmanaged = t } }); + } + + var user_profile = UserProfile.query(allocator); + if (user_profile.asValuePtr()) |up| { + defer up.deinit(); + return .initResult(.{ .#path = .{ .managed = .{ + .allocator = allocator, + .buf = std.mem.concat(allocator, u8, &.{ up.slice(), "\\Temp" }) catch |err| { + switch (err) { + error.OutOfMemory => return .initErr(.fromCode(.NOMEM, .GetTempPath2)), + } + }, + } } }); + } + + var win_dir = WinDir.query(allocator); + if (win_dir.asValuePtr()) |wd| { + defer wd.deinit(); + return .initResult(.{ .#path = .{ .managed = .{ + .allocator = allocator, + .buf = std.mem.concat(allocator, u8, &.{ wd.slice(), "\\Temp" }) catch |err| { + switch (err) { + error.OutOfMemory => return .initErr(.fromCode(.NOMEM, .GetTempPath2)), + } + }, + } } }); + } + + return .initErr(user_profile.asErr() orelse + win_dir.asErr() orelse + .fromCode(.UNKNOWN, .GetTempPath2)); + } +} else struct {}; + +const std = @import("std"); + +const bun = @import("bun"); +const paths = bun.paths; diff --git a/src/sys.zig b/src/sys.zig index e34ebf055b..f1f21b04cb 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -275,6 +275,7 @@ pub const Tag = enum(u8) { setsockopt, statx, rm, + getpwuid_r, uv_spawn, uv_pipe, @@ -291,6 +292,10 @@ pub const Tag = enum(u8) { CloseHandle, SetFilePointerEx, SetEndOfFile, + OpenProcessToken, + GetUserProfileDirectoryW, + GetSystemWindowsDirectoryW, + GetTempPath2, pub fn isWindows(this: Tag) bool { return @intFromEnum(this) > @intFromEnum(Tag.WriteFile); @@ -4260,6 +4265,65 @@ pub fn dlsymImpl(handle: ?*anyopaque, name: [:0]const u8) ?*anyopaque { @compileError("dlsym unimplemented for this target"); } +const SysconfOpts = struct { + default: ?i64 = null, + + const SysconfErrorSet = error{ + IndeterminateLimit, + InvalidName, + Unexpected, + }; + + /// Deduce the return type of sysconf based on whether a default was provided. + fn sysconfReturnType(self: SysconfOpts) type { + return if (self.default != null) i64 else SysconfErrorSet!i64; + } +}; + +/// Matches the POSIX sysconf function, except returns rich error information. +/// +/// Providing a default will cause that value to be returned instead of an error in the case of +/// error. You can use this to provide fallback values for systems that don't support certain +/// sysconf names. +/// +/// On platforms which do not support sysconf, this will return the default if provided, or a +/// compile-time error if not. +pub fn sysconf(name: c_int, comptime opts: SysconfOpts) opts.sysconfReturnType() { + // This function follows the documentation of sysconf(3) under POSIX.1-2008. + if (comptime !bun.Environment.isPosix) { + if (comptime opts.default) |d| { + return d; + } + + @compileError("sysconf is not supported on this platform. Either provide a default " ++ + "value or avoid calling sysconf on this platform."); + } + + // Man page says we need to set errno to 0 before calling sysconf. + std.c._errno().* = 0; + const ret = std.c.sysconf(name); + if (ret != -1) { + return ret; + } + + // As per the docblock, don't return errors if a default was provided. + if (comptime opts.default) |d| { + return d; + } + + // Lots of different error cases that may happen in sysconf. + const err_code: std.posix.E = @enumFromInt(std.c._errno().*); + return switch (err_code) { + // If name corresponds to a maximum or minimum limit, and that limit is indeterminate, -1 + // is returned and errno is not changed. (To distinguish an indeterminate limit from + // an error, set errno to zero before the call, and then check whether errno is nonzero + // when -1 is returned.) + .SUCCESS => error.IndeterminateLimit, + .INVAL => error.InvalidName, + else => std.posix.unexpectedErrno(err_code), + }; +} + pub fn dlsymWithHandle(comptime Type: type, comptime name: [:0]const u8, comptime handle_getter: fn () ?*anyopaque) ?Type { if (comptime @typeInfo(Type) != .pointer) { @compileError("dlsym must be a pointer type (e.g. ?const *fn()). Received " ++ @typeName(Type) ++ "."); diff --git a/src/windows.zig b/src/windows.zig index d3bfd16598..570e3bbad5 100644 --- a/src/windows.zig +++ b/src/windows.zig @@ -86,6 +86,31 @@ pub const WPathBuffer = if (Environment.isWindows) bun.WPathBuffer else void; pub const HANDLE = win32.HANDLE; pub const HMODULE = win32.HMODULE; +/// learn.microsoft.com/en-us/windows/win32/secauthz/access-rights-for-access-token-objects +pub const TOKEN_QUERY: DWORD = 0x0008; + +/// learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken +pub extern "advapi32" fn OpenProcessToken( + ProcessHandle: HANDLE, + DesiredAccess: DWORD, + TokenHandle: *HANDLE, +) callconv(windows.WINAPI) BOOL; + +/// learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getuserprofiledirectoryw +pub extern "userenv" fn GetUserProfileDirectoryW( + hToken: HANDLE, + lpProfileDir: LPWSTR, + lpcchSize: *DWORD, +) callconv(windows.WINAPI) BOOL; + +// learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemwindowsdirectoryw +pub extern "kernel32" fn GetSystemWindowsDirectoryW( + lpBuffer: LPWSTR, + uSize: UINT, +) callconv(windows.WINAPI) UINT; + +pub const GetCurrentProcess = windows.GetCurrentProcess; + /// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfileinformationbyhandle pub extern "kernel32" fn GetFileInformationByHandle( hFile: HANDLE,