diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 7b06ccfb6a..932879c99e 100644 --- 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 index 947e8e3070..a9bd960a34 100644 --- 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 index b1804c4605..ca658f0569 100644 --- 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 index 6031b1b298..843c1a0c3c 100644 --- 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 index 701eb434b5..857a79dd93 100644 --- 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 index 2588bde34f..56b4f1ba70 100644 --- 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 index ea28900dac..853f6125fb 100644 --- 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);