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`.
This commit is contained in:
Jakub Konka 2020-08-12 23:50:00 +02:00
parent e043396b24
commit 3e2e6baee5
3 changed files with 210 additions and 32 deletions

View File

@ -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.

View File

@ -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, .{}));

View File

@ -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);
}
// 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];
/// 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];
},
.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.