Skip to content

Commit dedffe4

Browse files
committed
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 2b28ceb commit dedffe4

File tree

3 files changed

+210
-32
lines changed

3 files changed

+210
-32
lines changed

lib/std/fs.zig

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,123 @@ pub const Dir = struct {
926926
return self.openDir(sub_path, open_dir_options);
927927
}
928928

929+
/// This function returns the canonicalized absolute pathname of
930+
/// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this
931+
/// `Dir` handle and returns the canonicalized absolute pathname of `pathname`
932+
/// argument.
933+
/// This function is not universally supported by all platforms.
934+
/// Currently supported hosts are: Linux, macOS, and Windows.
935+
/// See also `Dir.realpathZ`, `Dir.realpathW`, and `Dir.realpathAlloc`.
936+
pub fn realpath(self: Dir, pathname: []const u8, out_buffer: []u8) ![]u8 {
937+
if (builtin.os.tag == .wasi) {
938+
@compileError("realpath is unsupported in WASI");
939+
}
940+
if (builtin.os.tag == .windows) {
941+
const pathname_w = try os.windows.sliceToPrefixedFileW(pathname);
942+
return self.realpathW(pathname_w.span(), out_buffer);
943+
}
944+
const pathname_c = try os.toPosixPath(pathname);
945+
return self.realpathZ(&pathname_c, out_buffer);
946+
}
947+
948+
/// Same as `Dir.realpath` except `pathname` is null-terminated.
949+
/// See also `Dir.realpath`, `realpathZ`.
950+
pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) ![]u8 {
951+
if (builtin.os.tag == .windows) {
952+
const pathname_w = try os.windows.cStrToPrefixedFileW(pathname);
953+
return self.realpathW(pathname_w.span(), out_buffer);
954+
}
955+
956+
const flags = if (builtin.os.tag == .linux) os.O_PATH | os.O_NONBLOCK | os.O_CLOEXEC else os.O_NONBLOCK | os.O_CLOEXEC;
957+
const fd = os.openatZ(self.fd, pathname, flags, 0) catch |err| switch (err) {
958+
error.FileLocksNotSupported => unreachable,
959+
else => |e| return e,
960+
};
961+
defer os.close(fd);
962+
963+
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
964+
// have a variant that takes an arbitrary-size buffer.
965+
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
966+
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
967+
// paths. musl supports passing NULL but restricts the output to PATH_MAX
968+
// anyway.
969+
var buffer: [MAX_PATH_BYTES]u8 = undefined;
970+
const out_path = try os.getFdPath(fd, &buffer);
971+
972+
if (out_path.len > out_buffer.len) {
973+
return error.NameTooLong;
974+
}
975+
976+
mem.copy(u8, out_buffer, out_path);
977+
978+
return out_buffer[0..out_path.len];
979+
}
980+
981+
/// Windows-only. Same as `Dir.realpath` except `pathname` is WTF16 encoded.
982+
/// See also `Dir.realpath`, `realpathW`.
983+
pub fn realpathW(self: Dir, pathname: []const u16, out_buffer: []u8) ![]u8 {
984+
const w = os.windows;
985+
986+
const access_mask = w.GENERIC_READ | w.SYNCHRONIZE;
987+
const share_access = w.FILE_SHARE_READ;
988+
const creation = w.FILE_OPEN;
989+
const h_file = blk: {
990+
const res = w.OpenFile(pathname, .{
991+
.dir = self.fd,
992+
.access_mask = access_mask,
993+
.share_access = share_access,
994+
.creation = creation,
995+
.io_mode = .blocking,
996+
}) catch |err| switch (err) {
997+
error.IsDir => break :blk w.OpenFile(pathname, .{
998+
.dir = self.fd,
999+
.access_mask = access_mask,
1000+
.share_access = share_access,
1001+
.creation = creation,
1002+
.io_mode = .blocking,
1003+
.open_dir = true,
1004+
}) catch |er| switch (er) {
1005+
error.WouldBlock => unreachable,
1006+
else => |e2| return e2,
1007+
},
1008+
error.WouldBlock => unreachable,
1009+
else => |e| return e,
1010+
};
1011+
break :blk res;
1012+
};
1013+
defer w.CloseHandle(h_file);
1014+
1015+
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
1016+
// have a variant that takes an arbitrary-size buffer.
1017+
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
1018+
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
1019+
// paths. musl supports passing NULL but restricts the output to PATH_MAX
1020+
// anyway.
1021+
var buffer: [MAX_PATH_BYTES]u8 = undefined;
1022+
const out_path = try os.getFdPath(h_file, &buffer);
1023+
1024+
if (out_path.len > out_buffer.len) {
1025+
return error.NameTooLong;
1026+
}
1027+
1028+
mem.copy(u8, out_buffer, out_path);
1029+
1030+
return out_buffer[0..out_path.len];
1031+
}
1032+
1033+
/// Same as `Dir.realpath` except caller must free the returned memory.
1034+
/// See also `Dir.realpath`.
1035+
pub fn realpathAlloc(self: Dir, allocator: *Allocator, pathname: []const u8) ![]u8 {
1036+
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
1037+
// have a variant that takes an arbitrary-size buffer.
1038+
// TODO(#4812): Consider reimplementing realpath or using the POSIX.1-2008
1039+
// NULL out parameter (GNU's canonicalize_file_name) to handle overelong
1040+
// paths. musl supports passing NULL but restricts the output to PATH_MAX
1041+
// anyway.
1042+
var buf: [MAX_PATH_BYTES]u8 = undefined;
1043+
return allocator.dupe(u8, try self.realpath(pathname, buf[0..]));
1044+
}
1045+
9291046
/// Changes the current working directory to the open directory handle.
9301047
/// This modifies global state and can have surprising effects in multi-
9311048
/// threaded applications. Most applications and especially libraries should
@@ -2060,7 +2177,7 @@ pub fn selfExeDirPath(out_buffer: []u8) SelfExePathError![]const u8 {
20602177
}
20612178

20622179
/// `realpath`, except caller must free the returned memory.
2063-
/// TODO integrate with `Dir`
2180+
/// See also `Dir.realpath`.
20642181
pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 {
20652182
// Use of MAX_PATH_BYTES here is valid as the realpath function does not
20662183
// have a variant that takes an arbitrary-size buffer.

lib/std/fs/test.zig

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,57 @@ test "Dir.Iterator" {
109109
testing.expect(contains(&entries, Dir.Entry{ .name = "some_dir", .kind = Dir.Entry.Kind.Directory }));
110110
}
111111

112-
fn entry_eql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
112+
fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
113113
return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
114114
}
115115

116116
fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool {
117117
for (entries.items) |entry| {
118-
if (entry_eql(entry, el)) return true;
118+
if (entryEql(entry, el)) return true;
119119
}
120120
return false;
121121
}
122122

123+
test "Dir.realpath smoke test" {
124+
switch (builtin.os.tag) {
125+
.linux, .windows, .macosx, .ios, .watchos, .tvos => {},
126+
else => return error.SkipZigTest,
127+
}
128+
129+
var tmp_dir = tmpDir(.{});
130+
defer tmp_dir.cleanup();
131+
132+
var file = try tmp_dir.dir.createFile("test_file", .{ .lock = File.Lock.Shared });
133+
// We need to close the file immediately as otherwise on Windows we'll end up
134+
// with a sharing violation.
135+
file.close();
136+
137+
var arena = ArenaAllocator.init(testing.allocator);
138+
defer arena.deinit();
139+
140+
const base_path = blk: {
141+
const relative_path = try fs.path.join(&arena.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..] });
142+
break :blk try fs.realpathAlloc(&arena.allocator, relative_path);
143+
};
144+
145+
// First, test non-alloc version
146+
{
147+
var buf1: [fs.MAX_PATH_BYTES]u8 = undefined;
148+
const file_path = try tmp_dir.dir.realpath("test_file", buf1[0..]);
149+
const expected_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "test_file" });
150+
151+
testing.expect(mem.eql(u8, file_path, expected_path));
152+
}
153+
154+
// Next, test alloc version
155+
{
156+
const file_path = try tmp_dir.dir.realpathAlloc(&arena.allocator, "test_file");
157+
const expected_path = try fs.path.join(&arena.allocator, &[_][]const u8{ base_path, "test_file" });
158+
159+
testing.expect(mem.eql(u8, file_path, expected_path));
160+
}
161+
}
162+
123163
test "readAllAlloc" {
124164
var tmp_dir = tmpDir(.{});
125165
defer tmp_dir.cleanup();
@@ -167,12 +207,7 @@ test "directory operations on files" {
167207
testing.expectError(error.NotDir, tmp_dir.dir.deleteDir(test_file_name));
168208

169209
if (builtin.os.tag != .wasi) {
170-
// TODO: use Dir's realpath function once that exists
171-
const absolute_path = blk: {
172-
const relative_path = try fs.path.join(testing.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..], test_file_name });
173-
defer testing.allocator.free(relative_path);
174-
break :blk try fs.realpathAlloc(testing.allocator, relative_path);
175-
};
210+
const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_file_name);
176211
defer testing.allocator.free(absolute_path);
177212

178213
testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(absolute_path));
@@ -206,12 +241,7 @@ test "file operations on directories" {
206241
testing.expectError(error.IsDir, tmp_dir.dir.openFile(test_dir_name, .{ .write = true }));
207242

208243
if (builtin.os.tag != .wasi) {
209-
// TODO: use Dir's realpath function once that exists
210-
const absolute_path = blk: {
211-
const relative_path = try fs.path.join(testing.allocator, &[_][]const u8{ "zig-cache", "tmp", tmp_dir.sub_path[0..], test_dir_name });
212-
defer testing.allocator.free(relative_path);
213-
break :blk try fs.realpathAlloc(testing.allocator, relative_path);
214-
};
244+
const absolute_path = try tmp_dir.dir.realpathAlloc(testing.allocator, test_dir_name);
215245
defer testing.allocator.free(absolute_path);
216246

217247
testing.expectError(error.IsDir, fs.createFileAbsolute(absolute_path, .{}));

lib/std/os.zig

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4025,23 +4025,15 @@ pub fn realpathZ(pathname: [*:0]const u8, out_buffer: *[MAX_PATH_BYTES]u8) RealP
40254025
const pathname_w = try windows.cStrToPrefixedFileW(pathname);
40264026
return realpathW(pathname_w.span(), out_buffer);
40274027
}
4028-
if (builtin.os.tag == .linux and !builtin.link_libc) {
4029-
const fd = openZ(pathname, linux.O_PATH | linux.O_NONBLOCK | linux.O_CLOEXEC, 0) catch |err| switch (err) {
4028+
if (!builtin.link_libc) {
4029+
const flags = if (builtin.os.tag == .linux) O_PATH | O_NONBLOCK | O_CLOEXEC else O_NONBLOCK | O_CLOEXEC;
4030+
const fd = openZ(pathname, flags, 0) catch |err| switch (err) {
40304031
error.FileLocksNotSupported => unreachable,
40314032
else => |e| return e,
40324033
};
40334034
defer close(fd);
40344035

4035-
var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined;
4036-
const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable;
4037-
4038-
const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| {
4039-
switch (err) {
4040-
error.UnsupportedReparsePointType => unreachable, // Windows only,
4041-
else => |e| return e,
4042-
}
4043-
};
4044-
return target;
4036+
return getFdPath(fd, out_buffer);
40454037
}
40464038
const result_path = std.c.realpath(pathname, out_buffer) orelse switch (std.c._errno().*) {
40474039
EINVAL => unreachable,
@@ -4093,12 +4085,51 @@ pub fn realpathW(pathname: []const u16, out_buffer: *[MAX_PATH_BYTES]u8) RealPat
40934085
};
40944086
defer w.CloseHandle(h_file);
40954087

4096-
var wide_buf: [w.PATH_MAX_WIDE]u16 = undefined;
4097-
const wide_slice = try w.GetFinalPathNameByHandle(h_file, .{}, wide_buf[0..]);
4088+
return getFdPath(h_file, out_buffer);
4089+
}
4090+
4091+
/// Return canonical path of handle `fd`.
4092+
/// This function is very host-specific and is not universally supported by all hosts.
4093+
/// For example, while it generally works on Linux, macOS or Windows, it is unsupported
4094+
/// on FreeBSD, or WASI.
4095+
pub fn getFdPath(fd: fd_t, out_buffer: *[MAX_PATH_BYTES]u8) RealPathError![]u8 {
4096+
switch (builtin.os.tag) {
4097+
.windows => {
4098+
var wide_buf: [windows.PATH_MAX_WIDE]u16 = undefined;
4099+
const wide_slice = try windows.GetFinalPathNameByHandle(fd, .{}, wide_buf[0..]);
40984100

4099-
// Trust that Windows gives us valid UTF-16LE.
4100-
const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;
4101-
return out_buffer[0..end_index];
4101+
// Trust that Windows gives us valid UTF-16LE.
4102+
const end_index = std.unicode.utf16leToUtf8(out_buffer, wide_slice) catch unreachable;
4103+
return out_buffer[0..end_index];
4104+
},
4105+
.macosx, .ios, .watchos, .tvos => {
4106+
// On macOS, we can use F_GETPATH fcntl command to query the OS for
4107+
// the path to the file descriptor.
4108+
@memset(out_buffer, 0, MAX_PATH_BYTES);
4109+
switch (errno(system.fcntl(fd, F_GETPATH, out_buffer))) {
4110+
0 => {},
4111+
EBADF => return error.FileNotFound,
4112+
// TODO man pages for fcntl on macOS don't really tell you what
4113+
// errno values to expect when command is F_GETPATH...
4114+
else => |err| return unexpectedErrno(err),
4115+
}
4116+
const len = mem.indexOfScalar(u8, out_buffer[0..], @as(u8, 0)) orelse MAX_PATH_BYTES;
4117+
return out_buffer[0..len];
4118+
},
4119+
.linux => {
4120+
var procfs_buf: ["/proc/self/fd/-2147483648".len:0]u8 = undefined;
4121+
const proc_path = std.fmt.bufPrint(procfs_buf[0..], "/proc/self/fd/{}\x00", .{fd}) catch unreachable;
4122+
4123+
const target = readlinkZ(@ptrCast([*:0]const u8, proc_path.ptr), out_buffer) catch |err| {
4124+
switch (err) {
4125+
error.UnsupportedReparsePointType => unreachable, // Windows only,
4126+
else => |e| return e,
4127+
}
4128+
};
4129+
return target;
4130+
},
4131+
else => @compileError("querying for canonical path of a handle is unsupported on this host"),
4132+
}
41024133
}
41034134

41044135
/// Spurious wakeups are possible and no precision of timing is guaranteed.

0 commit comments

Comments
 (0)