commit 6be202f46633d02e20d0f068a32296113ecb95ca (tree)
parent 09bf51092ba8f11e342af7bd7d148af63f371709
Author: Ryan Liptak <squeek502@hotmail.com>
Date: Sat, 7 Mar 2026 22:17:20 -0800
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
Diffstat:
7 files changed, 77 insertions(+), 8 deletions(-)
diff --git a/lib/std/Io.zig b/lib/std/Io.zig
@@ -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,
diff --git a/lib/std/Io/Dispatch.zig b/lib/std/Io/Dispatch.zig
@@ -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;
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -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,
diff --git a/lib/std/Io/Uring.zig b/lib/std/Io/Uring.zig
@@ -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);
diff --git a/lib/std/process.zig b/lib/std/process.zig
@@ -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,
diff --git a/test/standalone/posix/cwd.zig b/test/standalone/posix/cwd.zig
@@ -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)];
diff --git a/test/standalone/windows_spawn/main.zig b/test/standalone/windows_spawn/main.zig
@@ -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);