authorgravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2020-08-12 23:50:00+02:00
committergravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2020-08-13 07:08:39+02:00
log3e2e6baee568b0fb9d019fc53974aad30386b824
tree432202df308ca46ee7f8d26c5021eecdea07cca5
parente043396b242d727cd72fecda265bd4b78a86958a

Add std.os.getFdPath and std.fs.Dir.realpath

`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,6 +926,123 @@ pub const Dir = struct {
926 return self.openDir(sub_path, open_dir_options);926 return self.openDir(sub_path, open_dir_options);
927 }927 }
928928
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 /// Changes the current working directory to the open directory handle.1046 /// Changes the current working directory to the open directory handle.
930 /// This modifies global state and can have surprising effects in multi-1047 /// This modifies global state and can have surprising effects in multi-
931 /// threaded applications. Most applications and especially libraries should1048 /// threaded applications. Most applications and especially libraries should
...@@ -2060,7 +2177,7 @@ pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 {...@@ -2060,7 +2177,7 @@ pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 {
2060}2177}
20612178
2062/// `realpath`, except caller must free the returned memory.2179/// `realpath`, except caller must free the returned memory.
2063/// TODO integrate with `Dir`2180/// See also `Dir.realpath`.
2064pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 {2181pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 {
2065 // Use of MAX_PATH_BYTES here is valid as the realpath function does not2182 // Use of MAX_PATH_BYTES here is valid as the realpath function does not
2066 // have a variant that takes an arbitrary-size buffer.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,17 +109,57 @@ test "Dir.Iterator" {
109 testing.expect(contains(&entries, Dir.Entry{ .name = "some_dir", .kind = Dir.Entry.Kind.Directory }));109 testing.expect(contains(&entries, Dir.Entry{ .name = "some_dir", .kind = Dir.Entry.Kind.Directory }));
110}110}
111111
112fn entry_eql(lhs: Dir.Entry, rhs: Dir.Entry) bool {112fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
113 return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;113 return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
114}114}
115115
116fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool {116fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool {
117 for (entries.items) |entry| {117 for (entries.items) |entry| {
118 if (entry_eql(entry, el)) return true;118 if (entryEql(entry, el)) return true;
119 }119 }
120 return false;120 return false;
121}121}
122122
123test "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
123test "readAllAlloc" {163test "readAllAlloc" {
124 var tmp_dir = tmpDir(.{});164 var tmp_dir = tmpDir(.{});
125 defer tmp_dir.cleanup();165 defer tmp_dir.cleanup();
...@@ -167,12 +207,7 @@ test "directory operations on files" {...@@ -167,12 +207,7 @@ test "directory operations on files" {
167 testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name));207 testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name));
168208
169 if (builtin.os.tag != .wasi) {209 if (builtin.os.tag != .wasi) {
170 // TODO: use Dir's realpath function once that exists210 const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name);
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 };
176 defer testing.allocator.free(absolute_path);211 defer testing.allocator.free(absolute_path);
177212
178 testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path));213 testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path));
...@@ -206,12 +241,7 @@ test "file operations on directories" {...@@ -206,12 +241,7 @@ test "file operations on directories" {
206 testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .write = true }));241 testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .write = true }));
207242
208 if (builtin.os.tag != .wasi) {243 if (builtin.os.tag != .wasi) {
209 // TODO: use Dir's realpath function once that exists244 const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name);
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 };
215 defer testing.allocator.free(absolute_path);245 defer testing.allocator.free(absolute_path);
216246
217 testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{}));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,23 +4025,15 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP
4025 const pathname_w = try windows.cStrToPrefixedFileW(pathname);4025 const pathname_w = try windows.cStrToPrefixedFileW(pathname);
4026 return realpathW(pathname_w.span(), out_buffer);4026 return realpathW(pathname_w.span(), out_buffer);
4027 }4027 }
4028 if (builtin.os.tag == .linux and !builtin.link_libc) {4028 if (!builtin.link_libc) {
4029 const fd = openZ(pathname, linux.O_PATH | linux.O_NONBLOCK | linux.O_CLOEXEC, 0) catch |err| switch (err) {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 error.FileLocksNotSupported => unreachable,4031 error.FileLocksNotSupported => unreachable,
4031 else => |e| return e,4032 else => |e| return e,
4032 };4033 };
4033 defer close(fd);4034 defer close(fd);
40344035
4035 var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined;4036 return getFdPath(fd, out_buffer);
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;
4045 }4037 }
4046 const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) {4038 const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) {
4047 EINVAL => unreachable,4039 EINVAL => unreachable,
...@@ -4093,12 +4085,51 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat...@@ -4093,12 +4085,51 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat
4093 };4085 };
4094 defer w.CloseHandle(h_file);4086 defer w.CloseHandle(h_file);
40954087
4096 var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined;4088 return getFdPath(h_file, out_buffer);
4097 const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]);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.
4095pub 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..]);
40984100
4099 // Trust that Windows gives us valid UTF-16LE.4101 // Trust that Windows gives us valid UTF-16LE.
4100 const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;4102 const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;
4101 return out_buffer[0..end_index];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}
41034134
4104/// Spurious wakeups are possible and no precision of timing is guaranteed.4135/// Spurious wakeups are possible and no precision of timing is guaranteed.