Commit f5d0664e78

Ryan Liptak <squeek502@hotmail.com>
2023-12-21 12:02:22
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
Changed files (1)
lib
std
lib/std/fs/test.zig
@@ -156,23 +156,80 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
     try testing.expectEqualStrings(target_path, given);
 }
 
-test "stat on a symlink returns Kind.sym_link" {
+test "File.stat on a File that is a symlink returns Kind.sym_link" {
+    // This test requires getting a file descriptor of a symlink which
+    // is not possible on all targets
+    switch (builtin.target.os.tag) {
+        .windows, .linux => {},
+        else => return error.SkipZigTest,
+    }
+
     try testWithAllSupportedPathTypes(struct {
         fn impl(ctx: *TestContext) !void {
             const dir_target_path = try ctx.transformPath("subdir");
             try ctx.dir.makeDir(dir_target_path);
 
-            // TODO: Also test a symlink to a file.
-            // There's currently no way to avoid following symlinks when opening files.
-            // https://github.com/ziglang/zig/issues/18327
-
             ctx.dir.symLink(dir_target_path, "symlink", .{ .is_directory = true }) catch |err| switch (err) {
                 // Symlink requires admin privileges on windows, so this test can legitimately fail.
                 error.AccessDenied => return error.SkipZigTest,
                 else => return err,
             };
 
-            var symlink = try ctx.dir.openDir("symlink", .{ .no_follow = true });
+            var symlink = switch (builtin.target.os.tag) {
+                .windows => windows_symlink: {
+                    const w = std.os.windows;
+
+                    const sub_path_w = try std.os.windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");
+
+                    var result = Dir{
+                        .fd = undefined,
+                    };
+
+                    const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
+                    var nt_name = w.UNICODE_STRING{
+                        .Length = path_len_bytes,
+                        .MaximumLength = path_len_bytes,
+                        .Buffer = @constCast(&sub_path_w.data),
+                    };
+                    var attr = w.OBJECT_ATTRIBUTES{
+                        .Length = @sizeOf(w.OBJECT_ATTRIBUTES),
+                        .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd,
+                        .Attributes = 0,
+                        .ObjectName = &nt_name,
+                        .SecurityDescriptor = null,
+                        .SecurityQualityOfService = null,
+                    };
+                    var io: w.IO_STATUS_BLOCK = undefined;
+                    const rc = w.ntdll.NtCreateFile(
+                        &result.fd,
+                        w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE,
+                        &attr,
+                        &io,
+                        null,
+                        w.FILE_ATTRIBUTE_NORMAL,
+                        w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
+                        w.FILE_OPEN,
+                        // FILE_OPEN_REPARSE_POINT is the important thing here
+                        w.FILE_OPEN_REPARSE_POINT | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT,
+                        null,
+                        0,
+                    );
+
+                    switch (rc) {
+                        .SUCCESS => break :windows_symlink result,
+                        else => return w.unexpectedStatus(rc),
+                    }
+                },
+                .linux => linux_symlink: {
+                    const sub_path_c = try os.toPosixPath("symlink");
+                    // the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
+                    // note that if O_DIRECTORY is set, then this will error with ENOTDIR
+                    const flags = os.O.NOFOLLOW | os.O.PATH | os.O.RDONLY | os.O.CLOEXEC;
+                    const fd = try os.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
+                    break :linux_symlink Dir{ .fd = fd };
+                },
+                else => unreachable,
+            };
             defer symlink.close();
 
             const stat = try symlink.stat();