Commit 11a398af3e

Ryan Liptak <squeek502@hotmail.com>
2023-12-20 08:01:57
File.stat: Support detection of Kind.sym_link on Windows
Requires an extra NtQueryInformationFile call when FILE_ATTRIBUTE_REPARSE_POINT is set to determine if it's actually a symlink or some other kind of reparse point (https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags). This is something that `File.Metadata.kind` was already doing, so the same technique is used in `stat`. Also, replace the std.os.windows.DeviceIoControl call in `metadata` with NtQueryInformationFile (NtQueryInformationFile is what gets called during kernel32.GetFileInformationByHandleEx with FileAttributeTagInfo, verified using NtTrace).
1 parent f36ac22
Changed files (3)
lib/std/fs/File.zig
@@ -389,7 +389,26 @@ pub fn stat(self: File) StatError!Stat {
             .inode = info.InternalInformation.IndexNumber,
             .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
             .mode = 0,
-            .kind = if (info.StandardInformation.Directory == 0) .file else .directory,
+            .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: {
+                var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
+                const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
+                switch (tag_rc) {
+                    .SUCCESS => {},
+                    // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
+                    // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
+                    .INFO_LENGTH_MISMATCH => unreachable,
+                    .ACCESS_DENIED => return error.AccessDenied,
+                    else => return windows.unexpectedStatus(rc),
+                }
+                if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) {
+                    break :reparse_point .sym_link;
+                }
+                // Unknown reparse point
+                break :reparse_point .unknown;
+            } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0)
+                .directory
+            else
+                .file,
             .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
             .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
             .ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
@@ -791,7 +810,7 @@ pub const MetadataWindows = struct {
     /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
     pub fn kind(self: Self) Kind {
         if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
-            if (self.reparse_tag & 0x20000000 != 0) {
+            if (self.reparse_tag & windows.reparse_tag_name_surrogate_bit != 0) {
                 return .sym_link;
             }
         } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
@@ -842,10 +861,17 @@ pub fn metadata(self: File) MetadataError!Metadata {
 
                 const reparse_tag: windows.DWORD = reparse_blk: {
                     if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
-                        var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
-                        try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
-                        const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
-                        break :reparse_blk reparse_struct.ReparseTag;
+                        var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
+                        const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
+                        switch (tag_rc) {
+                            .SUCCESS => {},
+                            // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
+                            // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
+                            .INFO_LENGTH_MISMATCH => unreachable,
+                            .ACCESS_DENIED => return error.AccessDenied,
+                            else => return windows.unexpectedStatus(rc),
+                        }
+                        break :reparse_blk tag_info.ReparseTag;
                     }
                     break :reparse_blk 0;
                 };
lib/std/fs/test.zig
@@ -156,6 +156,31 @@ 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" {
+    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 });
+            defer symlink.close();
+
+            const stat = try symlink.stat();
+            try testing.expectEqual(File.Kind.sym_link, stat.kind);
+        }
+    }.impl);
+}
+
 test "relative symlink to parent directory" {
     var tmp = tmpDir(.{});
     defer tmp.cleanup();
lib/std/os/windows.zig
@@ -2972,6 +2972,15 @@ pub const FILE_INFORMATION_CLASS = enum(c_int) {
     FileMaximumInformation,
 };
 
+pub const FILE_ATTRIBUTE_TAG_INFO = extern struct {
+    FileAttributes: DWORD,
+    ReparseTag: DWORD,
+};
+
+/// "If this bit is set, the file or directory represents another named entity in the system."
+/// https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags
+pub const reparse_tag_name_surrogate_bit = 0x20000000;
+
 pub const FILE_DISPOSITION_INFORMATION = extern struct {
     DeleteFile: BOOLEAN,
 };