Compare commits

...

3 Commits

Author SHA1 Message Date
Marko Vejnovic
6e79a73ecb feat(os.zig): Simplify the os.zig interface 2025-10-21 16:21:51 -07:00
autofix-ci[bot]
667404c883 [autofix.ci] apply automated fixes 2025-10-21 00:19:35 +00:00
Marko Vejnovic
ddb7c132a2 feat: Implement low-level os.zig abstractions 2025-10-20 17:17:20 -07:00
21 changed files with 884 additions and 148 deletions

View File

@@ -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

View File

@@ -1,5 +1,6 @@
export const test_skipped = [
"uv_getrusage_thread",
"uv_os_homedir",
"uv_thread_detach",
"uv_thread_getname",
"uv_thread_getpriority",

View File

@@ -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)

View File

@@ -302,80 +302,13 @@ 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| {
switch (bun.os.queryHomeDir()) {
.result => |r| {
return bun.String.cloneUTF8(r);
},
.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;
},
}
}

View File

@@ -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");

View File

@@ -23,7 +23,9 @@
#include "../packages/bun-native-bundler-plugin-api/bundler_plugin.h"
#if POSIX
#include <errno.h>
#include <ifaddrs.h>
#include <limits.h>
#include <netdb.h>
#include <pwd.h>
#include <stdlib.h>

View File

@@ -25,7 +25,7 @@ pub const InstallCompletionsCommand = struct {
// if that fails, try $HOME/.bun/bin
outer: {
if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| {
if (bun.os.queryHomeDir().asValue()) |home_dir| {
target = std.fmt.bufPrint(&target_buf, "{s}/.bun/bin/" ++ bunx_name, .{home_dir}) catch unreachable;
std.posix.symlink(exe, target) catch break :outer;
return;
@@ -34,7 +34,7 @@ pub const InstallCompletionsCommand = struct {
// if that fails, try $HOME/.local/bin
outer: {
if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| {
if (bun.os.queryHomeDir().asValue()) |home_dir| {
target = std.fmt.bufPrint(&target_buf, "{s}/.local/bin/" ++ bunx_name, .{home_dir}) catch unreachable;
std.posix.symlink(exe, target) catch break :outer;
return;
@@ -229,7 +229,7 @@ pub const InstallCompletionsCommand = struct {
}
}
if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| {
if (bun.os.queryHomeDir().asValue()) |home_dir| {
outer: {
var paths = [_]string{ home_dir, "./.config/fish/completions" };
completions_dir = resolve_path.joinAbsString(cwd, &paths, .auto);
@@ -287,7 +287,7 @@ pub const InstallCompletionsCommand = struct {
}
}
if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| {
if (bun.os.queryHomeDir().asValue()) |home_dir| {
{
outer: {
var paths = [_]string{ home_dir, "./.oh-my-zsh/completions" };
@@ -339,7 +339,7 @@ pub const InstallCompletionsCommand = struct {
}
}
if (bun.getenvZ(bun.DotEnv.home_env)) |home_dir| {
if (bun.os.queryHomeDir().asValue()) |home_dir| {
{
outer: {
var paths = [_]string{ home_dir, "./.oh-my-bash/custom/completions" };
@@ -449,7 +449,7 @@ pub const InstallCompletionsCommand = struct {
}
second: {
if (bun.getenvZ(bun.DotEnv.home_env)) |zdot_dir| {
if (bun.os.queryHomeDir().asValue()) |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;
@@ -459,7 +459,7 @@ pub const InstallCompletionsCommand = struct {
}
third: {
if (bun.getenvZ(bun.DotEnv.home_env)) |zdot_dir| {
if (bun.os.queryHomeDir().asValue()) |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;
@@ -531,7 +531,6 @@ pub const InstallCompletionsCommand = struct {
const string = []const u8;
const DotEnv = @import("../env_loader.zig");
const ShellCompletions = @import("./shell_completions.zig");
const fs = @import("../fs.zig");
const resolve_path = @import("../resolver/resolve_path.zig");

View File

@@ -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,13 @@ 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";
const system_root = switch (bun.os.win32.queryWinDir()) {
.err => {
Output.prettyErrorln("<r><red>error:<r> Failed to unzip {s} due to PowerShell not being installed.", .{tmpname});
Global.exit(1);
},
.result => |v| v,
};
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;

View File

@@ -530,43 +530,15 @@ pub const FileSystem = struct {
file_limit: usize = 32,
file_quota: usize = 32,
pub var win_tempdir_cache: ?[]const u8 = undefined;
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 (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 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;
switch (bun.os.querySysTmpDir()) {
.result => |s| {
return s;
},
.mac => "/private/tmp",
else => "/tmp",
};
.err => |e| {
bun.Output.panic("Could not retrieve the system temporary directory: {}", .{e});
},
}
}
pub const Tmpfile = switch (Environment.os) {

View File

@@ -791,7 +791,8 @@ 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| {
if (bun.getenvZ("XDG_CONFIG_HOME") orelse bun.os.queryHomeDir().asValue()) |data_dir| {
var buf: bun.PathBuffer = undefined;
var parts = [_]string{
"./.npmrc",

View File

@@ -224,7 +224,7 @@ 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| {
if (bun.getenvZ("XDG_CACHE_HOME") orelse bun.os.queryHomeDir().asValue()) |home_dir| {
var buf: bun.PathBuffer = undefined;
var parts = [_]string{
".bun",

View File

@@ -16,21 +16,9 @@ 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;
};
const home_dir = (bun.os.queryHomeDir()).unwrap() catch return;
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, &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 {

View File

@@ -321,7 +321,7 @@ BUN_1.2 {
uv_write2;
uv_wtf8_length_as_utf16;
uv_wtf8_to_utf16;
extern "C++" {
v8::*;

View File

@@ -202,6 +202,25 @@ pub fn dropSentinel(ptr: anytype, allocator: std.mem.Allocator) blk: {
return allocator.dupe(Child, slice);
}
extern "C" fn __lsan_ignore_object(object: *const anyopaque) callconv(.C) void;
/// Mark the given object as intentionally leaked, so that leak sanitizers do not report it.
pub fn INTENTIONALLY_LEAK(
comptime alloc: anytype,
object: *anyopaque,
comptime why: []const u8,
) void {
_ = why;
if (comptime bun.Environment.isDebug) {
if (comptime bun.allocators.isDefault(alloc)) {
__lsan_ignore_object(object);
} else {
@compileError("INTENTIONALLY_LEAK is not yet implemented for non-default allocators");
}
}
}
const std = @import("std");
const Allocator = std.mem.Allocator;

41
src/os.zig Normal file
View File

@@ -0,0 +1,41 @@
//! 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.
pub fn queryHomeDir() bun.sys.Maybe([]const u8) {
if (comptime bun.Environment.isWindows) {
return win32.queryUserProfile();
}
return posix.queryHomeDir();
}
/// 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.
pub fn querySysTmpDir() bun.sys.Maybe([]const u8) {
if (comptime bun.Environment.isWindows) {
return win32.querySysTmpDir();
}
return .initResult(posix.getSysTmpDir());
}
pub const win32 = if (bun.Environment.isWindows) @import("./os/win32.zig") else struct {};
pub const posix = if (bun.Environment.isPosix) @import("./os/posix.zig") else struct {};
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");

270
src/os/posix.zig Normal file
View File

@@ -0,0 +1,270 @@
/// Fully managed representation of a passwd entry.
pub const PasswdEntry = struct {
const Self = @This();
allocator: std.mem.Allocator,
buffer: []u8,
entry: bun.c.struct_passwd,
entry_ptr: *bun.c.struct_passwd,
_memo: struct {
// Holds the length of the pw_dir string slice to avoid recomputing it from the C string.
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 pwGidC(self: *Self) std.c.gid_t {
return self.entry_ptr.pw_gid;
}
pub fn pwDir(self: *Self) ?[]u8 {
if (self.entry_ptr.pw_dir == null) {
return null;
}
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;
}
/// Query an entry in the system passwd database by user ID.
/// May spinlock for an extremely brief period of time as it allocates memory.
pub fn query(user: std.c.uid_t, alloc: std.mem.Allocator) bun.sys.Maybe(PasswdEntry) {
const Memo = struct {
var pwbuf_size: std.atomic.Value(usize) = .init(0);
};
// Every iteration we will increase the buffer size by this factor.
// mem_allocated(n) = buf_size_gain^n
const buf_size_gain = 2;
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; // 1MB
// I try to be slightly clever here and memoize the last successful buffer size.
var buffer_size: usize = Memo.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) catch default_buf_size,
);
buffer_size = initial_buf_size;
}
var result: ?*bun.c.struct_passwd = undefined;
var self: PasswdEntry = .{
.allocator = alloc,
.buffer = alloc.alloc(u8, buffer_size) catch {
return .initErr(.fromCode(.NOMEM, .getpwuid_r));
},
.entry = undefined,
.entry_ptr = undefined,
};
var deallocate_buffer: bool = true;
defer {
if (deallocate_buffer) {
self.allocator.free(self.buffer);
}
}
while (buffer_size <= max_buf_size) {
const rc = bun.c.getpwuid_r(
user,
&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.
Memo.pwbuf_size.store(buffer_size, .monotonic);
if (result) |r| {
self.entry_ptr = r;
deallocate_buffer = false;
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));
}
};
/// Deduces the effective user's home directory on POSIX systems.
///
/// Memoizes the result for future calls. There is no way to "unset" the home directory once it has
/// been set. The user must restart the process to reset it.
///
/// Whichever of the following returns a value is returned.
/// - Per doi:10.1109/IEEESTD.2018.8277153, the `$HOME` variable -- does not allocate.
/// - The `getpwuid_r` function -- allocates.
///
/// Ensure you call `.deinit` on the result type.
pub fn queryHomeDir() bun.sys.Maybe([]const u8) {
const allocator = bun.default_allocator;
const Store = struct {
const DataPtr = bun.TaggedPointerUnion(.{
PasswdEntry,
[]const u8,
});
var atomic_data = std.atomic.Value(DataPtr).init(DataPtr.Null);
fn getSlice() ?[]const u8 {
const data = atomic_data.load(.acquire);
return if (data != DataPtr.Null) switch (data.tag()) {
DataPtr.case([]const u8) => data.as([]const u8).*,
DataPtr.case(PasswdEntry) => data.as(PasswdEntry).pwDir().?,
else => unreachable,
} else null;
}
};
if (Store.getSlice()) |s| {
return .initResult(s);
}
if (bun.getenvZ("HOME") orelse bun.getenvZ("USERPROFILE")) |home| use_envvar: {
// The user may override $HOME with a non-absolute path. We know that's wrong.
if (!std.fs.path.isAbsolute(home)) {
break :use_envvar;
}
const hpath = bun.handleOom(allocator.create([]const u8));
hpath.* = home;
// Failure order requires that we observe the changes to DataPtr, so we need acquire.
// Success order requires that we publish our changes to DataPtr, so we need release.
if (Store.atomic_data.cmpxchgStrong(
Store.DataPtr.Null,
.init(hpath),
.release,
.acquire,
)) |d| {
// Another thread beat us to it.
allocator.destroy(hpath);
return .initResult(switch (d.tag()) {
Store.DataPtr.case([]const u8) => d.as([]const u8).*,
Store.DataPtr.case(PasswdEntry) => d.as(PasswdEntry).pwDir().?,
else => unreachable,
});
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(hpath),
"home directory considered singleton",
);
return .initResult(hpath.*);
}
var passwd_entry = PasswdEntry.query(bun.c.geteuid(), allocator);
switch (passwd_entry) {
.err => |e| {
return .initErr(e);
},
.result => |*r| {
if (r.pwDir() == null) {
r.deinit();
return .initErr(.fromCode(.NOENT, .getpwuid_r));
}
const pwent = bun.handleOom(allocator.create(PasswdEntry));
pwent.* = r.*;
// Failure order requires that we observe the changes to DataPtr, so we need acquire.
// Success order requires that we publish our changes to DataPtr, so we need release.
if (Store.atomic_data.cmpxchgStrong(
Store.DataPtr.Null,
.init(pwent),
.release,
.acquire,
)) |d| {
// Another thread beat us to it.
pwent.deinit();
allocator.destroy(pwent);
return .initResult(switch (d.tag()) {
Store.DataPtr.case([]const u8) => d.as([]const u8).*,
Store.DataPtr.case(PasswdEntry) => d.as(PasswdEntry).pwDir().?,
else => unreachable,
});
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(pwent),
"home directory considered singleton",
);
return .initResult(pwent.pwDir().?);
},
}
}
/// Query the system temporary directory.
/// Follows the suggestions of IEEE Std 1003.1-2017 (POSIX 2017).
pub fn getSysTmpDir() []const u8 {
// 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)
return if (bun.getenvZ("TMPDIR")) |tmpdir|
tmpdir
// The /tmp directory is retained in POSIX.1-2017 to accommodate historical applications
// that assume its availability.
else
"/tmp";
}
const bun = @import("bun");
const std = @import("std");

46
src/os/uvinterop.zig Normal file
View File

@@ -0,0 +1,46 @@
//! 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.
switch (bun.os.queryHomeDir()) {
.result => |out| {
// +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;
return 0;
},
.err => |err| {
return translateSysErrorToUv(err.errno);
},
}
}
const bun = @import("bun");
const libuv = @import("../deps/libuv.zig");

398
src/os/win32.zig Normal file
View File

@@ -0,0 +1,398 @@
//! 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.
const AtomicMaybeManagedPath = struct {
const DataPtr = bun.ptr.TaggedPointerUnion(.{ []const u8, []u8 });
fn forceAsSlice(data_ptr: DataPtr) []const u8 {
return switch (data_ptr.tag()) {
DataPtr.case([]const u8) => data_ptr.as([]const u8).*,
DataPtr.case([]u8) => data_ptr.as([]u8).*,
else => unreachable,
};
}
atomic_data: std.atomic.Value(DataPtr) = .init(DataPtr.Null),
fn getSlice(self: AtomicMaybeManagedPath) ?[]const u8 {
const data = self.atomic_data.load(.acquire);
return if (data != DataPtr.Null) forceAsSlice(data) else null;
}
/// Attempt to store the given value into the atomic data pointer, if it is not already set.
/// Returns the existing value if it was already set.
fn tryStore(self: *AtomicMaybeManagedPath, value: DataPtr) ?DataPtr {
// Failure order requires that we observe the changes to DataPtr, so we need acquire.
// Success order requires that we publish our changes to DataPtr, so we need release.
return self.atomic_data.cmpxchgStrong(DataPtr.Null, value, .release, .acquire);
}
};
/// Many Win32 calls require you provide them with a buffer. If the buffer ends up being too small,
/// the call will return the required size, and you are expected to reallocate the buffer and try
/// again.
///
/// This function does all of that for you -- you just tell it what syscall to call.
fn runWin32CallWithResizingBuffer(
allocator: std.mem.Allocator,
comptime syscall_tag: bun.sys.Tag,
comptime queryFn: fn ([]u16) bun.sys.Maybe(usize),
) bun.sys.Maybe(struct {
buf: []u16,
slice: []const u16,
}) {
var wchar_buf = allocator.alloc(u16, bun.windows.MAX_PATH) catch {
return .initErr(.fromCode(.NOMEM, syscall_tag));
};
const rc = queryFn(wchar_buf);
switch (rc) {
.err => |e| {
allocator.free(wchar_buf);
return .initErr(e);
},
.result => |r| {
if (r <= wchar_buf.len) {
return .initResult(.{
.buf = wchar_buf,
.slice = wchar_buf[0..@as(usize, @intCast(r))],
});
}
wchar_buf = allocator.realloc(wchar_buf, @as(usize, @intCast(r))) catch {
allocator.free(wchar_buf);
return .initErr(.fromCode(.NOMEM, syscall_tag));
};
const rc2 = queryFn(wchar_buf);
switch (rc2) {
.err => |e| {
allocator.free(wchar_buf);
return .initErr(e);
},
.result => |r2| {
if (r2 <= wchar_buf.len) {
return .initResult(.{
.buf = wchar_buf,
.slice = wchar_buf[0..@as(usize, @intCast(r2))],
});
}
bun.Output.panic(
@tagName(syscall_tag) ++ "keeps returning a size larger than the " ++
"buffer we provide. This is a bug in Bun. Please report it on Github.",
.{},
);
},
}
},
}
}
/// Get the path to the Windows directory, eg. C:\Windows.
pub fn queryWinDir() bun.sys.Maybe([]const u8) {
const allocator = bun.default_allocator;
const Static = struct {
var path: AtomicMaybeManagedPath = .{};
};
if (Static.path.getSlice()) |s| {
return .initResult(s);
}
if (bun.getenvZ("WINDIR") orelse bun.getenvZ("SYSTEMROOT")) |wd| use_envvar: {
if (!std.fs.path.isAbsolute(wd)) {
break :use_envvar;
}
const hpath = bun.handleOom(allocator.create([]const u8));
hpath.* = wd;
if (Static.path.tryStore(.init(hpath))) |v| {
allocator.destroy(hpath);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(hpath),
"windows directory considered singleton",
);
return .initResult(wd);
}
switch (runWin32CallWithResizingBuffer(
allocator,
.GetSystemWindowsDirectoryW,
(struct {
fn doSyscall(wchar_buf: []u16) bun.sys.Maybe(usize) {
const rc = bun.windows.GetSystemWindowsDirectoryW(
@ptrCast(wchar_buf.ptr),
@intCast(wchar_buf.len),
);
if (rc == 0) {
const err = bun.windows.GetLastError();
// TODO(markovejnovic): This conversion between Win32Error -> SystemErrno -> E
// feels very wrong.
return .initErr(
.fromCode(@enumFromInt(@intFromEnum(err)), .GetSystemWindowsDirectoryW),
);
}
return .initResult(rc);
}
}).doSyscall,
)) {
.err => |e| {
return .initErr(e);
},
.result => |res| {
defer allocator.free(res.buf);
const data_ptr = bun.handleOom(allocator.create([]u8));
data_ptr.* = bun.handleOom(bun.strings.toUTF8Alloc(
allocator,
res.slice,
));
if (Static.path.tryStore(.init(data_ptr))) |v| {
allocator.free(data_ptr.*);
allocator.destroy(data_ptr);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(data_ptr),
"windows directory considered singleton",
);
return .initResult(data_ptr.*);
},
}
}
/// Get the path to the current user directory, eg. C:\Users\<User>.
pub fn queryUserProfile() bun.sys.Maybe([]const u8) {
const allocator = bun.default_allocator;
const Static = struct {
var path: AtomicMaybeManagedPath = .{};
};
if (Static.path.getSlice()) |s| {
return .initResult(s);
}
if (bun.getenvZ("USERPROFILE")) |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));
}
const hpath = bun.handleOom(allocator.create([]const u8));
hpath.* = p;
if (Static.path.tryStore(.init(hpath))) |v| {
allocator.destroy(hpath);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(hpath),
"user profile directory considered singleton",
);
return .initResult(p);
}
switch (runWin32CallWithResizingBuffer(
allocator,
.GetUserProfileDirectoryW,
(struct {
fn doSyscall(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_READ,
&proc_token,
);
if (rc == bun.windows.FALSE) {
const err = bun.windows.GetLastError();
// TODO(markovejnovic): This 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 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));
}
}).doSyscall,
)) {
.err => |e| {
return .initErr(e);
},
.result => |res| {
defer allocator.free(res.buf);
const data_ptr = bun.handleOom(allocator.create([]u8));
data_ptr.* = bun.handleOom(bun.strings.toUTF8Alloc(
allocator,
res.slice,
));
if (Static.path.tryStore(.init(data_ptr))) |v| {
allocator.free(data_ptr.*);
allocator.destroy(data_ptr);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(data_ptr),
"windows directory considered singleton",
);
return .initResult(data_ptr.*);
},
}
}
// Windows-specific TMP/TEMP directory retrieval.
pub fn querySysTmpDir() bun.sys.Maybe([]const u8) {
// 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.
const allocator = bun.default_allocator;
const Static = struct {
var path: AtomicMaybeManagedPath = .{};
};
if (Static.path.getSlice()) |s| {
return .initResult(s);
}
if (bun.getenvZ("TMP") orelse bun.getenvZ("TEMP")) |t| use_envvar: {
if (!std.fs.path.isAbsolute(t)) {
break :use_envvar;
}
const hpath = bun.handleOom(allocator.create([]const u8));
hpath.* = t;
if (Static.path.tryStore(.init(hpath))) |v| {
allocator.destroy(hpath);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
return .initResult(t);
}
const user_profile = queryUserProfile();
if (user_profile.asValue()) |up| {
const result = std.mem.concat(allocator, u8, &.{ up, "\\Temp" }) catch |err| {
switch (err) {
error.OutOfMemory => return .initErr(.fromCode(.NOMEM, .GetTempPath2)),
}
};
const hpath = bun.handleOom(allocator.create([]u8));
hpath.* = result;
if (Static.path.tryStore(.init(hpath))) |v| {
allocator.free(result);
allocator.destroy(hpath);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(hpath),
"temp directory considered singleton",
);
return .initResult(result);
}
const win_dir = queryWinDir();
if (win_dir.asValue()) |wd| {
const result = std.mem.concat(allocator, u8, &.{ wd, "\\Temp" }) catch |err| {
switch (err) {
error.OutOfMemory => return .initErr(.fromCode(.NOMEM, .GetTempPath2)),
}
};
const hpath = bun.handleOom(allocator.create([]u8));
hpath.* = result;
if (Static.path.tryStore(.init(hpath))) |v| {
allocator.destroy(hpath);
return .initResult(AtomicMaybeManagedPath.forceAsSlice(v));
}
bun.memory.INTENTIONALLY_LEAK(
allocator,
@ptrCast(hpath),
"root directory considered singleton",
);
return .initResult(result);
}
return .initErr(
user_profile.asErr() orelse win_dir.asErr() orelse .fromCode(.UNKNOWN, .GetTempPath2),
);
}
const bun = @import("bun");
const std = @import("std");

View File

@@ -88,7 +88,7 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type {
const TagType: type = result.tag_type;
return struct {
return packed struct {
pub const Tag = TagType;
pub const TagInt = TaggedPointer.Tag;
pub const type_map: TypeMap(Types) = result.ty_map;
@@ -136,7 +136,7 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type {
}
pub fn case(comptime Type: type) Tag {
return @field(Tag, bun.meta.typeBaseName(@typeName(Type)));
return @field(Tag, @typeName(Type));
}
/// unsafely cast a tagged pointer to a specific type, without checking that it's really that type
@@ -234,5 +234,4 @@ pub fn TaggedPointerUnion(comptime Types: anytype) type {
};
}
const bun = @import("bun");
const std = @import("std");

View File

@@ -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,34 @@ pub fn dlsymImpl(handle: ?*anyopaque, name: [:0]const u8) ?*anyopaque {
@compileError("dlsym unimplemented for this target");
}
/// Matches the POSIX sysconf function, except returns rich error information.
pub fn sysconf(name: c_int) !u32 {
// This function follows the documentation of sysconf(3) under POSIX.1-2008.
if (comptime !bun.Environment.isPosix) {
@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 >= 0) {
return @intCast(ret);
}
// 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) ++ ".");

View File

@@ -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_READ: DWORD = 0x20008;
/// 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,