From 11a398af3e12f0d1da0a5f95a17e334a063890a9 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Tue, 19 Dec 2023 23:01:57 -0800 Subject: [PATCH 1/2] 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 464e7207dc..96e495822d 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 bba4bc551d..68945e6200 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 55922fe5e0..f6de2b2d6b 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, }; From f5d0664e78211fbc366801868d59d9f909cd0471 Mon Sep 17 00:00:00 2001 From: Ryan Liptak Date: Thu, 21 Dec 2023 03:02:22 -0800 Subject: [PATCH 2/2] 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. --- lib/std/fs/test.zig | 69 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 68945e6200..23eaf06b65 100644 --- a/lib/std/fs/test.zig +++ b/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();