| author | |
| committer | |
| log | 3e2e6baee568b0fb9d019fc53974aad30386b824 |
| tree | 432202df308ca46ee7f8d26c5021eecdea07cca5 |
| parent | e043396b242d727cd72fecda265bd4b78a86958a |
`std.os.getFdPath` is very platform-specific and can be used to query
the OS for a canonical path to a file handle. Currently supported hosts
are Linux, macOS and Windows.
`std.fs.Dir.realpath` (and null-terminated, plus WTF16 versions) are
similar to `std.os.realpath`, however, they resolve a path wrt to this
`Dir` instance.
If the input pathname argument turns out to be an absolute path, this
function reverts to calling `realpath` on that pathname completely
ignoring this `Dir`.3 files changed, 210 insertions(+), 32 deletions(-)
lib/std/fs.zig+118-1| ... | ... | @@ -926,6 +926,123 @@ pub const Dir = struct { |
| 926 | 926 | return self.openDir(sub_path, open_dir_options); |
| 927 | 927 | } |
| 928 | 928 | |
| 929 | /// This function returns the canonicalized absolute pathname of | |
| 930 | /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this | |
| 931 | /// `Dir` handle and returns the canonicalized absolute pathname of `pathname` | |
| 932 | /// argument. | |
| 933 | /// This function is not universally supported by all platforms. | |
| 934 | /// Currently supported hosts are: Linux, macOS, and Windows. | |
| 935 | /// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`. | |
| 936 | pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 { | |
| 937 | if (builtin.os.tag == .wasi) { | |
| 938 | @compileError("realpath is unsupported in WASI"); | |
| 939 | } | |
| 940 | if (builtin.os.tag == .windows) { | |
| 941 | const pathname_w = try os.windows.sliceToPrefixedFileW(pathname); | |
| 942 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 943 | } | |
| 944 | const pathname_c = try os.toPosixPath(pathname); | |
| 945 | return self.realpathZ(&pathname_c, out_buffer); | |
| 946 | } | |
| 947 | ||
| 948 | /// Same as `Dir.realpath` except `pathname` is null-terminated. | |
| 949 | /// See also `Dir.realpath`, `realpathZ`. | |
| 950 | pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 { | |
| 951 | if (builtin.os.tag == .windows) { | |
| 952 | const pathname_w = try os.windows.cStrToPrefixedFileW(pathname); | |
| 953 | return self.realpathW(pathname_w.span(), out_buffer); | |
| 954 | } | |
| 955 | ||
| 956 | const flags = if (builtin.os.tag == .linux) os.O_PATH | os.O_NONBLOCK | os.O_CLOEXEC else os.O_NONBLOCK | os.O_CLOEXEC; | |
| 957 | const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) { | |
| 958 | error.FileLocksNotSupported => unreachable, | |
| 959 | else => |e| return e, | |
| 960 | }; | |
| 961 | defer os.close(fd); | |
| 962 | ||
| 963 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 964 | // have a variant that takes an arbitrary-size buffer. | |
| 965 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 966 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 967 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 968 | // anyway. | |
| 969 | var buffer: [MAX_PATH_BYTES]u8 = undefined; | |
| 970 | const out_path = try os.getFdPath(fd, &buffer); | |
| 971 | ||
| 972 | if (out_path.len > out_buffer.len) { | |
| 973 | return error.NameTooLong; | |
| 974 | } | |
| 975 | ||
| 976 | mem.copy(u8, out_buffer, out_path); | |
| 977 | ||
| 978 | return out_buffer[0..out_path.len]; | |
| 979 | } | |
| 980 | ||
| 981 | /// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded. | |
| 982 | /// See also `Dir.realpath`, `realpathW`. | |
| 983 | pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 { | |
| 984 | const w = os.windows; | |
| 985 | ||
| 986 | const access_mask = w.GENERIC_READ | w.SYNCHRONIZE; | |
| 987 | const share_access = w.FILE_SHARE_READ; | |
| 988 | const creation = w.FILE_OPEN; | |
| 989 | const h_file = blk: { | |
| 990 | const res = w.OpenFile(pathname, .{ | |
| 991 | .dir = self.fd, | |
| 992 | .access_mask = access_mask, | |
| 993 | .share_access = share_access, | |
| 994 | .creation = creation, | |
| 995 | .io_mode = .blocking, | |
| 996 | }) catch |err| switch (err) { | |
| 997 | error.IsDir => break :blk w.OpenFile(pathname, .{ | |
| 998 | .dir = self.fd, | |
| 999 | .access_mask = access_mask, | |
| 1000 | .share_access = share_access, | |
| 1001 | .creation = creation, | |
| 1002 | .io_mode = .blocking, | |
| 1003 | .open_dir = true, | |
| 1004 | }) catch |er| switch (er) { | |
| 1005 | error.WouldBlock => unreachable, | |
| 1006 | else => |e2| return e2, | |
| 1007 | }, | |
| 1008 | error.WouldBlock => unreachable, | |
| 1009 | else => |e| return e, | |
| 1010 | }; | |
| 1011 | break :blk res; | |
| 1012 | }; | |
| 1013 | defer w.CloseHandle(h_file); | |
| 1014 | ||
| 1015 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1016 | // have a variant that takes an arbitrary-size buffer. | |
| 1017 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1018 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1019 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1020 | // anyway. | |
| 1021 | var buffer: [MAX_PATH_BYTES]u8 = undefined; | |
| 1022 | const out_path = try os.getFdPath(h_file, &buffer); | |
| 1023 | ||
| 1024 | if (out_path.len > out_buffer.len) { | |
| 1025 | return error.NameTooLong; | |
| 1026 | } | |
| 1027 | ||
| 1028 | mem.copy(u8, out_buffer, out_path); | |
| 1029 | ||
| 1030 | return out_buffer[0..out_path.len]; | |
| 1031 | } | |
| 1032 | ||
| 1033 | /// Same as `Dir.realpath` except caller must free the returned memory. | |
| 1034 | /// See also `Dir.realpath`. | |
| 1035 | pub fn realpathAlloc(self: Dir, allocator: *Allocator, pathname: []const u8) ![]u8 { | |
| 1036 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not | |
| 1037 | // have a variant that takes an arbitrary-size buffer. | |
| 1038 | // TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008 | |
| 1039 | // NULL out parameter (GNU's canonicalize_file_name) to handle overelong | |
| 1040 | // paths. musl supports passing NULL but restricts the output to PATH_MAX | |
| 1041 | // anyway. | |
| 1042 | var buf: [MAX_PATH_BYTES]u8 = undefined; | |
| 1043 | return allocator.dupe(u8, try self.realpath(pathname, buf[0..])); | |
| 1044 | } | |
| 1045 | ||
| 929 | 1046 | /// Changes the current working directory to the open directory handle. |
| 930 | 1047 | /// This modifies global state and can have surprising effects in multi- |
| 931 | 1048 | /// threaded applications. Most applications and especially libraries should |
| ... | ... | @@ -2060,7 +2177,7 @@ pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 { |
| 2060 | 2177 | } |
| 2061 | 2178 | |
| 2062 | 2179 | /// `realpath`, except caller must free the returned memory. |
| 2063 | /// TODO integrate with `Dir` | |
| 2180 | /// See also `Dir.realpath`. | |
| 2064 | 2181 | pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 { |
| 2065 | 2182 | // Use of MAX_PATH_BYTES here is valid as the realpath function does not |
| 2066 | 2183 | // have a variant that takes an arbitrary-size buffer. |
lib/std/fs/test.zig+44-14| ... | ... | @@ -109,17 +109,57 @@ test "Dir.Iterator" { |
| 109 | 109 | testing.expect(contains(&entries, Dir.Entry{ .name = "some_dir", .kind = Dir.Entry.Kind.Directory })); |
| 110 | 110 | } |
| 111 | 111 | |
| 112 | fn entry_eql(lhs: Dir.Entry, rhs: Dir.Entry) bool { | |
| 112 | fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool { | |
| 113 | 113 | return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind; |
| 114 | 114 | } |
| 115 | 115 | |
| 116 | 116 | fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool { |
| 117 | 117 | for (entries.items) |entry| { |
| 118 | if (entry_eql(entry, el)) return true; | |
| 118 | if (entryEql(entry, el)) return true; | |
| 119 | 119 | } |
| 120 | 120 | return false; |
| 121 | 121 | } |
| 122 | 122 | |
| 123 | test "Dir.realpath smoke test" { | |
| 124 | switch (builtin.os.tag) { | |
| 125 | .linux, .windows, .macosx, .ios, .watchos, .tvos => {}, | |
| 126 | else => return error.SkipZigTest, | |
| 127 | } | |
| 128 | ||
| 129 | var tmp_dir = tmpDir(.{}); | |
| 130 | defer tmp_dir.cleanup(); | |
| 131 | ||
| 132 | var file = try tmp_dir.dir.createFile("test_file", .{ .lock = File.Lock.Shared }); | |
| 133 | // We need to close the file immediately as otherwise on Windows we'll end up | |
| 134 | // with a sharing violation. | |
| 135 | file.close(); | |
| 136 | ||
| 137 | var arena = ArenaAllocator.init(testing.allocator); | |
| 138 | defer arena.deinit(); | |
| 139 | ||
| 140 | const base_path = blk: { | |
| 141 | const relative_path = try fs.path.join(&arena.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] }); | |
| 142 | break :blk try fs.realpathAlloc(&arena.allocator, relative_path); | |
| 143 | }; | |
| 144 | ||
| 145 | // First, test non-alloc version | |
| 146 | { | |
| 147 | var buf1: [fs.MAX_PATH_BYTES]u8 = undefined; | |
| 148 | const file_path = try tmp_dir.dir.realpath("test_file", buf1[0..]); | |
| 149 | const expected_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "test_file" }); | |
| 150 | ||
| 151 | testing.expect(mem.eql(u8, file_path, expected_path)); | |
| 152 | } | |
| 153 | ||
| 154 | // Next, test alloc version | |
| 155 | { | |
| 156 | const file_path = try tmp_dir.dir.realpathAlloc(&arena.allocator, "test_file"); | |
| 157 | const expected_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "test_file" }); | |
| 158 | ||
| 159 | testing.expect(mem.eql(u8, file_path, expected_path)); | |
| 160 | } | |
| 161 | } | |
| 162 | ||
| 123 | 163 | test "readAllAlloc" { |
| 124 | 164 | var tmp_dir = tmpDir(.{}); |
| 125 | 165 | defer tmp_dir.cleanup(); |
| ... | ... | @@ -167,12 +207,7 @@ test "directory operations on files" { |
| 167 | 207 | testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name)); |
| 168 | 208 | |
| 169 | 209 | if (builtin.os.tag != .wasi) { |
| 170 | // TODO: use Dir's realpath function once that exists | |
| 171 | const absolute_path = blk: { | |
| 172 | const relative_path = try fs.path.join(testing.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..], test_file_name }); | |
| 173 | defer testing.allocator.free(relative_path); | |
| 174 | break :blk try fs.realpathAlloc(testing.allocator, relative_path); | |
| 175 | }; | |
| 210 | const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name); | |
| 176 | 211 | defer testing.allocator.free(absolute_path); |
| 177 | 212 | |
| 178 | 213 | testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path)); |
| ... | ... | @@ -206,12 +241,7 @@ test "file operations on directories" { |
| 206 | 241 | testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .write = true })); |
| 207 | 242 | |
| 208 | 243 | if (builtin.os.tag != .wasi) { |
| 209 | // TODO: use Dir's realpath function once that exists | |
| 210 | const absolute_path = blk: { | |
| 211 | const relative_path = try fs.path.join(testing.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..], test_dir_name }); | |
| 212 | defer testing.allocator.free(relative_path); | |
| 213 | break :blk try fs.realpathAlloc(testing.allocator, relative_path); | |
| 214 | }; | |
| 244 | const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name); | |
| 215 | 245 | defer testing.allocator.free(absolute_path); |
| 216 | 246 | |
| 217 | 247 | testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{})); |
lib/std/os.zig+48-17| ... | ... | @@ -4025,23 +4025,15 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP |
| 4025 | 4025 | const pathname_w = try windows.cStrToPrefixedFileW(pathname); |
| 4026 | 4026 | return realpathW(pathname_w.span(), out_buffer); |
| 4027 | 4027 | } |
| 4028 | if (builtin.os.tag == .linux and !builtin.link_libc) { | |
| 4029 | const fd = openZ(pathname, linux.O_PATH | linux.O_NONBLOCK | linux.O_CLOEXEC, 0) catch |err| switch (err) { | |
| 4028 | if (!builtin.link_libc) { | |
| 4029 | const flags = if (builtin.os.tag == .linux) O_PATH | O_NONBLOCK | O_CLOEXEC else O_NONBLOCK | O_CLOEXEC; | |
| 4030 | const fd = openZ(pathname, flags, 0) catch |err| switch (err) { | |
| 4030 | 4031 | error.FileLocksNotSupported => unreachable, |
| 4031 | 4032 | else => |e| return e, |
| 4032 | 4033 | }; |
| 4033 | 4034 | defer close(fd); |
| 4034 | 4035 | |
| 4035 | var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; | |
| 4036 | const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; | |
| 4037 | ||
| 4038 | const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| { | |
| 4039 | switch (err) { | |
| 4040 | error.UnsupportedReparsePointType => unreachable, // Windows only, | |
| 4041 | else => |e| return e, | |
| 4042 | } | |
| 4043 | }; | |
| 4044 | return target; | |
| 4036 | return getFdPath(fd, out_buffer); | |
| 4045 | 4037 | } |
| 4046 | 4038 | const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) { |
| 4047 | 4039 | EINVAL => unreachable, |
| ... | ... | @@ -4093,12 +4085,51 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat |
| 4093 | 4085 | }; |
| 4094 | 4086 | defer w.CloseHandle(h_file); |
| 4095 | 4087 | |
| 4096 | var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined; | |
| 4097 | const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]); | |
| 4088 | return getFdPath(h_file, out_buffer); | |
| 4089 | } | |
| 4090 | ||
| 4091 | /// Return canonical path of handle `fd`. | |
| 4092 | /// This function is very host-specific and is not universally supported by all hosts. | |
| 4093 | /// For example, while it generally works on Linux, macOS or Windows, it is unsupported | |
| 4094 | /// on FreeBSD, or WASI. | |
| 4095 | pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 { | |
| 4096 | switch (builtin.os.tag) { | |
| 4097 | .windows => { | |
| 4098 | var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined; | |
| 4099 | const wide_slice = try windows.GetFinalPathNameByHandle(fd, .{}, wide_buf[0..]); | |
| 4098 | 4100 | |
| 4099 | // Trust that Windows gives us valid UTF-16LE. | |
| 4100 | const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable; | |
| 4101 | return out_buffer[0..end_index]; | |
| 4101 | // Trust that Windows gives us valid UTF-16LE. | |
| 4102 | const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable; | |
| 4103 | return out_buffer[0..end_index]; | |
| 4104 | }, | |
| 4105 | .macosx, .ios, .watchos, .tvos => { | |
| 4106 | // On macOS, we can use F_GETPATH fcntl command to query the OS for | |
| 4107 | // the path to the file descriptor. | |
| 4108 | @memset(out_buffer, 0, MAX_PATH_BYTES); | |
| 4109 | switch (errno(system.fcntl(fd, F_GETPATH, out_buffer))) { | |
| 4110 | 0 => {}, | |
| 4111 | EBADF => return error.FileNotFound, | |
| 4112 | // TODO man pages for fcntl on macOS don't really tell you what | |
| 4113 | // errno values to expect when command is F_GETPATH... | |
| 4114 | else => |err| return unexpectedErrno(err), | |
| 4115 | } | |
| 4116 | const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES; | |
| 4117 | return out_buffer[0..len]; | |
| 4118 | }, | |
| 4119 | .linux => { | |
| 4120 | var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined; | |
| 4121 | const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable; | |
| 4122 | ||
| 4123 | const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| { | |
| 4124 | switch (err) { | |
| 4125 | error.UnsupportedReparsePointType => unreachable, // Windows only, | |
| 4126 | else => |e| return e, | |
| 4127 | } | |
| 4128 | }; | |
| 4129 | return target; | |
| 4130 | }, | |
| 4131 | else => @compileError("querying for canonical path of a handle is unsupported on this host"), | |
| 4132 | } | |
| 4102 | 4133 | } |
| 4103 | 4134 | |
| 4104 | 4135 | /// Spurious wakeups are possible and no precision of timing is guaranteed. |