Io: Add processSetCurrentPath

The logic used to allow providing a path for setting the CWD of a child process in https://codeberg.org/ziglang/zig/pulls/31090 applies here as well:

- Windows must provide a path when setting the CWD, so the path of an `Io.Dir` must be resolved before actually calling RtlSetCurrentDirectory_U
- A directory handle may have multiple paths associated with it, so providing the CWD as a string retains a legitimate use case in cases where the precise path matters
This commit is contained in:
Ryan Liptak
2026-03-07 22:17:20 -08:00
committed by Andrew Kelley
parent 09bf51092b
commit 6be202f466
7 changed files with 77 additions and 8 deletions
+1
View File
@@ -218,6 +218,7 @@ pub const VTable = struct {
unlockStderr: *const fn (?*anyopaque) void,
processCurrentPath: *const fn (?*anyopaque, buffer: []u8) std.process.CurrentPathError!usize,
processSetCurrentDir: *const fn (?*anyopaque, Dir) std.process.SetCurrentDirError!void,
processSetCurrentPath: *const fn (?*anyopaque, []const u8) std.process.SetCurrentPathError!void,
processReplace: *const fn (?*anyopaque, std.process.ReplaceOptions) std.process.ReplaceError,
processReplacePath: *const fn (?*anyopaque, Dir, std.process.ReplaceOptions) std.process.ReplaceError,
processSpawn: *const fn (?*anyopaque, std.process.SpawnOptions) std.process.SpawnError!std.process.Child,
+2 -1
View File
@@ -434,6 +434,7 @@ pub fn io(ev: *Evented) Io {
.unlockStderr = unlockStderr,
.processCurrentPath = processCurrentPath,
.processSetCurrentDir = processSetCurrentDir,
.processSetCurrentPath = processSetCurrentPath,
.processReplace = processReplace,
.processReplacePath = processReplacePath,
.processSpawn = processSpawn,
@@ -4046,7 +4047,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
};
}
fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) ChdirError!void {
fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) process.SetCurrentPathError!void {
const ev: *Evented = @ptrCast(@alignCast(userdata));
_ = ev;
var path_buffer: [c.PATH_MAX]u8 = undefined;
+39
View File
@@ -1841,6 +1841,7 @@ pub fn io(t: *Threaded) Io {
.unlockStderr = unlockStderr,
.processCurrentPath = processCurrentPath,
.processSetCurrentDir = processSetCurrentDir,
.processSetCurrentPath = processSetCurrentPath,
.processReplace = processReplace,
.processReplacePath = processReplacePath,
.processSpawn = processSpawn,
@@ -2006,6 +2007,7 @@ pub fn ioBasic(t: *Threaded) Io {
.unlockStderr = unlockStderr,
.processCurrentPath = processCurrentPath,
.processSetCurrentDir = processSetCurrentDir,
.processSetCurrentPath = processSetCurrentPath,
.processReplace = processReplace,
.processReplacePath = processReplacePath,
.processSpawn = processSpawn,
@@ -14176,6 +14178,43 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
return fchdir(dir.handle);
}
fn processSetCurrentPath(userdata: ?*anyopaque, path: []const u8) process.SetCurrentPathError!void {
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
if (native_os == .wasi) return error.OperationUnsupported;
if (is_windows) {
var path_w_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
const len = std.unicode.calcWtf16LeLen(path) catch return error.InvalidWtf8;
if (len > path_w_buf.len) return error.NameTooLong;
const path_w_len = std.unicode.wtf8ToWtf16Le(&path_w_buf, path) catch |err| switch (err) {
error.InvalidWtf8 => unreachable, // already validated
};
const path_w = path_w_buf[0..path_w_len];
const syscall: Syscall = try .start();
while (true) switch (windows.ntdll.RtlSetCurrentDirectory_U(&.init(path_w))) {
.SUCCESS => return syscall.finish(),
.OBJECT_NAME_INVALID => return syscall.fail(error.BadPathName),
.OBJECT_NAME_NOT_FOUND => return syscall.fail(error.FileNotFound),
.OBJECT_PATH_NOT_FOUND => return syscall.fail(error.FileNotFound),
.NO_MEDIA_IN_DEVICE => return syscall.fail(error.NoDevice),
.INVALID_PARAMETER => |err| return syscall.ntstatusBug(err),
.ACCESS_DENIED => return syscall.fail(error.AccessDenied),
.OBJECT_PATH_SYNTAX_BAD => |err| return syscall.ntstatusBug(err),
.NOT_A_DIRECTORY => return syscall.fail(error.NotDir),
.CANCELLED => {
try syscall.checkCancel();
continue;
},
else => |status| return syscall.unexpectedNtstatus(status),
};
}
return chdir(path);
}
pub const PosixAddress = extern union {
any: posix.sockaddr,
in: posix.sockaddr.in,
+2 -1
View File
@@ -752,6 +752,7 @@ pub fn io(ev: *Evented) Io {
.unlockStderr = unlockStderr,
.processCurrentPath = processCurrentPath,
.processSetCurrentDir = processSetCurrentDir,
.processSetCurrentPath = processSetCurrentPath,
.processReplace = processReplace,
.processReplacePath = processReplacePath,
.processSpawn = processSpawn,
@@ -4176,7 +4177,7 @@ fn processSetCurrentDir(userdata: ?*anyopaque, dir: Dir) process.SetCurrentDirEr
return fchdir(&sync, dir.handle);
}
fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) ChdirError!void {
fn processSetCurrentPath(userdata: ?*anyopaque, dir_path: []const u8) process.SetCurrentPathError!void {
const ev: *Evented = @ptrCast(@alignCast(userdata));
var path_buffer: [PATH_MAX]u8 = undefined;
const dir_path_posix = try pathToPosix(dir_path, &path_buffer);
+29
View File
@@ -900,6 +900,35 @@ pub fn setCurrentDir(io: Io, dir: Io.Dir) !void {
return io.vtable.processSetCurrentDir(io.userdata, dir);
}
pub const SetCurrentPathError = error{
AccessDenied,
SymLinkLoop,
SystemResources,
BadPathName,
FileNotFound,
FileSystem,
NoDevice,
NotDir,
NameTooLong,
OperationUnsupported,
/// Windows-only. The path is invalid WTF-8.
/// https://wtf-8.codeberg.page/
InvalidWtf8,
} || Io.Cancelable || Io.UnexpectedError;
/// Changes the current working directory to the given path.
/// Corresponds to "chdir" in libc.
///
/// This modifies global process state and can have surprising effects in
/// multithreaded applications. Most applications and especially libraries
/// should not call this function as a general rule, however it can have use
/// cases in, for example, implementing a shell, or child process execution.
///
/// Calling this function makes code less portable and less reusable.
pub fn setCurrentPath(io: Io, path: []const u8) !void {
return io.vtable.processSetCurrentPath(io.userdata, path);
}
pub const LockMemoryError = error{
UnsupportedOperation,
PermissionDenied,
+3 -3
View File
@@ -37,7 +37,7 @@ fn test_chdir_self(io: Io) !void {
const old_cwd = old_cwd_buf[0..try std.process.currentPath(io, &old_cwd_buf)];
// Try changing to the current directory
try std.Io.Threaded.chdir(old_cwd);
try std.process.setCurrentPath(io, old_cwd);
try expect_cwd(io, old_cwd);
}
@@ -48,7 +48,7 @@ fn test_chdir_absolute(io: Io) !void {
const parent = std.fs.path.dirname(old_cwd) orelse unreachable; // old_cwd should be absolute
// Try changing to the parent via a full path
try std.Io.Threaded.chdir(parent);
try std.process.setCurrentPath(io, parent);
try expect_cwd(io, parent);
}
@@ -68,7 +68,7 @@ fn test_chdir_relative(gpa: Allocator, io: Io, tmp_dir: Io.Dir) !void {
defer gpa.free(expected_path);
// change current working directory to new test directory
try std.Io.Threaded.chdir(subdir_path);
try std.process.setCurrentPath(io, subdir_path);
var new_cwd_buf: [path_max]u8 = undefined;
const new_cwd = new_cwd_buf[0..try std.process.currentPath(io, &new_cwd_buf)];
+1 -3
View File
@@ -9,8 +9,6 @@ pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
const process_cwd_path = try std.process.currentPathAlloc(io, init.arena.allocator());
var initial_process_cwd = try Io.Dir.cwd().openDir(io, ".", .{});
defer initial_process_cwd.close(io);
var it = try init.minimal.args.iterateAllocator(gpa);
defer it.deinit();
@@ -127,7 +125,7 @@ pub fn main(init: std.process.Init) !void {
// Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir
try std.process.setCurrentDir(io, tmp_dir);
defer std.process.setCurrentDir(io, initial_process_cwd) catch {};
defer std.process.setCurrentPath(io, process_cwd_path) catch {};
const something_subdir_abs_path = try std.mem.concatWithSentinel(gpa, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0);
defer gpa.free(something_subdir_abs_path);