From 11a398af3e12f0d1da0a5f95a17e334a063890a9 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 19 Dec 2023 23:01:57 -0800 Subject: [PATCH] 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). --- lib/std/fs/File.zig | 38 ++++++++++++++++++++++++++++++++------ lib/std/fs/test.zig | 25 +++++++++++++++++++++++++ lib/std/os/windows.zig | 9 +++++++++ 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index 464e7207d..96e495822 100644 --- a/lib/std/fs/File.zig +++ b/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; }; diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index bba4bc551..68945e620 100644 --- a/lib/std/fs/test.zig +++ b/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(); diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index 55922fe5e..f6de2b2d6 100644 --- a/lib/std/os/windows.zig +++ b/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, };