diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 57c1534e976f..a492a43499e5 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -926,6 +926,123 @@ pub const Dir = struct { return self.openDir(sub_path, open_dir_options); } + /// This function returns the canonicalized absolute pathname of + /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this + /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` + /// argument. + /// This function is not universally supported by all platforms. + /// Currently supported hosts are: Linux, macOS, and Windows. + /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. + pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { + if (builtin.os.tag == .wasi) { + @compileError("realpath is unsupported in WASI"); + } + if (builtin.os.tag == .windows) { + const pathname_w = try os.windows.sliceToPrefixedFileW(pathname); + return self.realpathW(pathname_w.span(), out_buffer); + } + const pathname_c = try os.toPosixPath(pathname); + return self.realpathZ(&pathname_c, out_buffer); + } + + /// Same as `Dir.realpath` except `pathname` is null-terminated. + /// See also `Dir.realpath`, `realpathZ`. + pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { + if (builtin.os.tag == .windows) { + const pathname_w = try os.windows.cStrToPrefixedFileW(pathname); + return self.realpathW(pathname_w.span(), out_buffer); + } + + const flags = if (builtin.os.tag == .linux) os.O_PATH | os.O_NONBLOCK | os.O_CLOEXEC else os.O_NONBLOCK | os.O_CLOEXEC; + const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { + error.FileLocksNotSupported => unreachable, + else => |e| return e, + }; + defer os.close(fd); + + // Use of MAX_PATH_BYTES here is valid as the realpath function does not + // have a variant that takes an arbitrary-size buffer. + // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 + // NULL out parameter (GNU's canonicalize_file_name) to handle overelong + // paths. musl supports passing NULL but restricts the output to PATH_MAX + // anyway. + var buffer: [MAX_PATH_BYTES]u8 = undefined; + const out_path = try os.getFdPath(fd, &buffer); + + if (out_path.len > out_buffer.len) { + return error.NameTooLong; + } + + mem.copy(u8, out_buffer, out_path); + + return out_buffer[0..out_path.len]; + } + + /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. + /// See also `Dir.realpath`, `realpathW`. + pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { + const w = os.windows; + + const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; + const share_access = w.FILE_SHARE_READ; + const creation = w.FILE_OPEN; + const h_file = blk: { + const res = w.OpenFile(pathname, .{ + .dir = self.fd, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .io_mode = .blocking, + }) catch |err| switch (err) { + error.IsDir => break :blk w.OpenFile(pathname, .{ + .dir = self.fd, + .access_mask = access_mask, + .share_access = share_access, + .creation = creation, + .io_mode = .blocking, + .open_dir = true, + }) catch |er| switch (er) { + error.WouldBlock => unreachable, + else => |e2| return e2, + }, + error.WouldBlock => unreachable, + else => |e| return e, + }; + break :blk res; + }; + defer w.CloseHandle(h_file); + + // Use of MAX_PATH_BYTES here is valid as the realpath function does not + // have a variant that takes an arbitrary-size buffer. + // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 + // NULL out parameter (GNU's canonicalize_file_name) to handle overelong + // paths. musl supports passing NULL but restricts the output to PATH_MAX + // anyway. + var buffer: [MAX_PATH_BYTES]u8 = undefined; + const out_path = try os.getFdPath(h_file, &buffer); + + if (out_path.len > out_buffer.len) { + return error.NameTooLong; + } + + mem.copy(u8, out_buffer, out_path); + + return out_buffer[0..out_path.len]; + } + + /// Same as `Dir.realpath` except caller must free the returned memory. + /// See also `Dir.realpath`. + pub fn realpathAlloc(self: Dir, allocator: *Allocator, pathname: []const u8) ![]u8 { + // Use of MAX_PATH_BYTES here is valid as the realpath function does not + // have a variant that takes an arbitrary-size buffer. + // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 + // NULL out parameter (GNU's canonicalize_file_name) to handle overelong + // paths. musl supports passing NULL but restricts the output to PATH_MAX + // anyway. + var buf: [MAX_PATH_BYTES]u8 = undefined; + return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); + } + /// Changes the current working directory to the open directory handle. /// This modifies global state and can have surprising effects in multi- /// threaded applications. Most applications and especially libraries should @@ -2060,7 +2177,7 @@ pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 { } /// `realpath`, except caller must free the returned memory. -/// TODO integrate with `Dir` +/// See also `Dir.realpath`. pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 { // Use of MAX_PATH_BYTES here is valid as the realpath function does not // have a variant that takes an arbitrary-size buffer. diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index c9f171196776..3d8403fdedbf 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -109,17 +109,57 @@ test "Dir.Iterator" { testing.expect(contains(&entries, Dir.Entry{ .name = "some_dir", .kind = Dir.Entry.Kind.Directory })); } -fn entry_eql(lhs: Dir.Entry, rhs: Dir.Entry) bool { +fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool { return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind; } fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool { for (entries.items) |entry| { - if (entry_eql(entry, el)) return true; + if (entryEql(entry, el)) return true; } return false; } +test "Dir.realpath smoke test" { + switch (builtin.os.tag) { + .linux, .windows, .macosx, .ios, .watchos, .tvos => {}, + else => return error.SkipZigTest, + } + + var tmp_dir = tmpDir(.{}); + defer tmp_dir.cleanup(); + + var file = try tmp_dir.dir.createFile("test_file", .{ .lock = File.Lock.Shared }); + // We need to close the file immediately as otherwise on Windows we'll end up + // with a sharing violation. + file.close(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + + const base_path = blk: { + const relative_path = try fs.path.join(&arena.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] }); + break :blk try fs.realpathAlloc(&arena.allocator, relative_path); + }; + + // First, test non-alloc version + { + var buf1: [fs.MAX_PATH_BYTES]u8 = undefined; + const file_path = try tmp_dir.dir.realpath("test_file", buf1[0..]); + const expected_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "test_file" }); + + testing.expect(mem.eql(u8, file_path, expected_path)); + } + + // Next, test alloc version + { + const file_path = try tmp_dir.dir.realpathAlloc(&arena.allocator, "test_file"); + const expected_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "test_file" }); + + testing.expect(mem.eql(u8, file_path, expected_path)); + } +} + test "readAllAlloc" { var tmp_dir = tmpDir(.{}); defer tmp_dir.cleanup(); @@ -167,12 +207,7 @@ test "directory operations on files" { testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name)); if (builtin.os.tag != .wasi) { - // TODO: use Dir's realpath function once that exists - const absolute_path = blk: { - const relative_path = try fs.path.join(testing.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..], test_file_name }); - defer testing.allocator.free(relative_path); - break :blk try fs.realpathAlloc(testing.allocator, relative_path); - }; + const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name); defer testing.allocator.free(absolute_path); testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path)); @@ -206,12 +241,7 @@ test "file operations on directories" { testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .write = true })); if (builtin.os.tag != .wasi) { - // TODO: use Dir's realpath function once that exists - const absolute_path = blk: { - const relative_path = try fs.path.join(testing.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..], test_dir_name }); - defer testing.allocator.free(relative_path); - break :blk try fs.realpathAlloc(testing.allocator, relative_path); - }; + const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name); defer testing.allocator.free(absolute_path); testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{})); diff --git a/lib/std/os.zig b/lib/std/os.zig index 04c2340cad94..d5fe347cfb36 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -4025,23 +4025,15 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP const pathname_w = try windows.cStrToPrefixedFileW(pathname); return realpathW(pathname_w.span(), out_buffer); } - if (builtin.os.tag == .linux and !builtin.link_libc) { - const fd = openZ(pathname, linux.O_PATH | linux.O_NONBLOCK | linux.O_CLOEXEC, 0) catch |err| switch (err) { + if (!builtin.link_libc) { + const flags = if (builtin.os.tag == .linux) O_PATH | O_NONBLOCK | O_CLOEXEC else O_NONBLOCK | O_CLOEXEC; + const fd = openZ(pathname, flags, 0) catch |err| switch (err) { error.FileLocksNotSupported => unreachable, else => |e| return e, }; defer close(fd); - var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; - const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; - - const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| { - switch (err) { - error.UnsupportedReparsePointType => unreachable, // Windows only, - else => |e| return e, - } - }; - return target; + return getFdPath(fd, out_buffer); } const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) { EINVAL => unreachable, @@ -4093,12 +4085,51 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat }; defer w.CloseHandle(h_file); - var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined; - const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]); + return getFdPath(h_file, out_buffer); +} + +/// Return canonical path of handle `fd`. +/// This function is very host-specific and is not universally supported by all hosts. +/// For example, while it generally works on Linux, macOS or Windows, it is unsupported +/// on FreeBSD, or WASI. +pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { + switch (builtin.os.tag) { + .windows => { + var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; + const wide_slice = try windows.GetFinalPathNameByHandle(fd, .{}, wide_buf[0..]); - // Trust that Windows gives us valid UTF-16LE. - const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable; - return out_buffer[0..end_index]; + // Trust that Windows gives us valid UTF-16LE. + const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable; + return out_buffer[0..end_index]; + }, + .macosx, .ios, .watchos, .tvos => { + // On macOS, we can use F_GETPATH fcntl command to query the OS for + // the path to the file descriptor. + @memset(out_buffer, 0, MAX_PATH_BYTES); + switch (errno(system.fcntl(fd, F_GETPATH, out_buffer))) { + 0 => {}, + EBADF => return error.FileNotFound, + // TODO man pages for fcntl on macOS don't really tell you what + // errno values to expect when command is F_GETPATH... + else => |err| return unexpectedErrno(err), + } + const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES; + return out_buffer[0..len]; + }, + .linux => { + var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; + const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; + + const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| { + switch (err) { + error.UnsupportedReparsePointType => unreachable, // Windows only, + else => |e| return e, + } + }; + return target; + }, + else => @compileError("querying for canonical path of a handle is unsupported on this host"), + } } /// Spurious wakeups are possible and no precision of timing is guaranteed.