Skip to content

Commit 60b9523

Browse files
committed
Make 'stat of a symlink' test case not rely on OpenDirOptions.no_follow behavior
The `no_follow` behavior happened to allow opening a file descriptor of a symlink itself on Windows, but that behavior may change in the future. Instead, we implement the opening of the symlink as a file descriptor manually (and per-platform) in the test case.
1 parent 11a398a commit 60b9523

File tree

1 file changed

+63
-6
lines changed

1 file changed

+63
-6
lines changed

lib/std/fs/test.zig

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,23 +156,80 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
156156
try testing.expectEqualStrings(target_path, given);
157157
}
158158

159-
test "stat on a symlink returns Kind.sym_link" {
159+
test "File.stat on a File that is a symlink returns Kind.sym_link" {
160+
// This test requires getting a file descriptor of a symlink which
161+
// is not possible on all targets
162+
switch (builtin.target.os.tag) {
163+
.windows, .linux => {},
164+
else => return error.SkipZigTest,
165+
}
166+
160167
try testWithAllSupportedPathTypes(struct {
161168
fn impl(ctx: *TestContext) !void {
162169
const dir_target_path = try ctx.transformPath("subdir");
163170
try ctx.dir.makeDir(dir_target_path);
164171

165-
// TODO: Also test a symlink to a file.
166-
// There's currently no way to avoid following symlinks when opening files.
167-
// https://github.com/ziglang/zig/issues/18327
168-
169172
ctx.dir.symLink(dir_target_path, "symlink", .{ .is_directory = true }) catch |err| switch (err) {
170173
// Symlink requires admin privileges on windows, so this test can legitimately fail.
171174
error.AccessDenied => return error.SkipZigTest,
172175
else => return err,
173176
};
174177

175-
var symlink = try ctx.dir.openDir("symlink", .{ .no_follow = true });
178+
var symlink = switch (builtin.target.os.tag) {
179+
.windows => windows_symlink: {
180+
const w = std.os.windows;
181+
182+
const sub_path_w = try std.os.windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");
183+
184+
var result = Dir{
185+
.fd = undefined,
186+
};
187+
188+
const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
189+
var nt_name = w.UNICODE_STRING{
190+
.Length = path_len_bytes,
191+
.MaximumLength = path_len_bytes,
192+
.Buffer = @constCast(sub_path_w),
193+
};
194+
var attr = w.OBJECT_ATTRIBUTES{
195+
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
196+
.RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else ctx.dir.fd,
197+
.Attributes = 0,
198+
.ObjectName = &nt_name,
199+
.SecurityDescriptor = null,
200+
.SecurityQualityOfService = null,
201+
};
202+
var io: w.IO_STATUS_BLOCK = undefined;
203+
const rc = w.ntdll.NtCreateFile(
204+
&result.fd,
205+
w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE,
206+
&attr,
207+
&io,
208+
null,
209+
w.FILE_ATTRIBUTE_NORMAL,
210+
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
211+
w.FILE_OPEN,
212+
// FILE_OPEN_REPARSE_POINT is the important thing here
213+
w.FILE_OPEN_REPARSE_POINT | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT | open_reparse_point,
214+
null,
215+
0,
216+
);
217+
218+
switch (rc) {
219+
.SUCCESS => break :windows_symlink result,
220+
else => return w.unexpectedStatus(rc),
221+
}
222+
},
223+
.linux => linux_symlink: {
224+
const sub_path_c = try os.toPosixPath("symlink");
225+
// the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
226+
// note that if O_DIRECTORY is set, then this will error with ENOTDIR
227+
const flags = os.O.NOFOLLOW | os.O.PATH | os.O.RDONLY | os.O.CLOEXEC;
228+
const fd = try os.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
229+
break :linux_symlink Dir{ .fd = fd };
230+
},
231+
else => unreachable,
232+
};
176233
defer symlink.close();
177234

178235
const stat = try symlink.stat();

0 commit comments

Comments
 (0)