mirror of
https://github.com/ziglang/zig.git
synced 2024-11-15 16:45:27 +00: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`.
This commit is contained in:
parent
e043396b24
commit
3e2e6baee5
119
lib/std/fs.zig
119
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.
|
||||
|
@ -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, .{}));
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user