Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion lib/std/fs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
58 changes: 44 additions & 14 deletions lib/std/fs/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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, .{}));
Expand Down
65 changes: 48 additions & 17 deletions lib/std/os.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down