Commit 3e2e6baee5
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.