Commit 3e2e6baee5

Jakub Konka <kubkon@jakubkonka.com>
2020-08-12 23:50:00
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`.
1 parent e043396
Changed files (3)
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, .{}));
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.
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.