authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2023-12-19 23:01:57-08:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2023-12-19 23:36:23-08:00
log11a398af3e12f0d1da0a5f95a17e334a063890a9
tree5b8be54db331c73e8a3fafd7e3f959d42d68418a
parentf36ac227b13884f42f259bc38c5b0aa012273799

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).

3 files changed, 66 insertions(+), 6 deletions(-)

lib/std/fs/File.zig+32-6
...@@ -389,7 +389,26 @@ pub fn stat(self: File) StatError!Stat {...@@ -389,7 +389,26 @@ pub fn stat(self: File) StatError!Stat {
389 .inode = info.InternalInformation.IndexNumber,389 .inode = info.InternalInformation.IndexNumber,
390 .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),390 .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
391 .mode = 0,391 .mode = 0,
392 .kind = if (info.StandardInformation.Directory == 0) .file else .directory,392 .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: {
393 var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
394 const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
395 switch (tag_rc) {
396 .SUCCESS => {},
397 // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
398 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
399 .INFO_LENGTH_MISMATCH => unreachable,
400 .ACCESS_DENIED => return error.AccessDenied,
401 else => return windows.unexpectedStatus(rc),
402 }
403 if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) {
404 break :reparse_point .sym_link;
405 }
406 // Unknown reparse point
407 break :reparse_point .unknown;
408 } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0)
409 .directory
410 else
411 .file,
393 .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),412 .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
394 .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),413 .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
395 .ctime = windows.fromSysTime(info.BasicInformation.CreationTime),414 .ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
...@@ -791,7 +810,7 @@ pub const MetadataWindows = struct {...@@ -791,7 +810,7 @@ pub const MetadataWindows = struct {
791 /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`810 /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
792 pub fn kind(self: Self) Kind {811 pub fn kind(self: Self) Kind {
793 if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {812 if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
794 if (self.reparse_tag & 0x20000000 != 0) {813 if (self.reparse_tag & windows.reparse_tag_name_surrogate_bit != 0) {
795 return .sym_link;814 return .sym_link;
796 }815 }
797 } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {816 } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
...@@ -842,10 +861,17 @@ pub fn metadata(self: File) MetadataError!Metadata {...@@ -842,10 +861,17 @@ pub fn metadata(self: File) MetadataError!Metadata {
842861
843 const reparse_tag: windows.DWORD = reparse_blk: {862 const reparse_tag: windows.DWORD = reparse_blk: {
844 if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {863 if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
845 var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;864 var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
846 try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);865 const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
847 const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));866 switch (tag_rc) {
848 break :reparse_blk reparse_struct.ReparseTag;867 .SUCCESS => {},
868 // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
869 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
870 .INFO_LENGTH_MISMATCH => unreachable,
871 .ACCESS_DENIED => return error.AccessDenied,
872 else => return windows.unexpectedStatus(rc),
873 }
874 break :reparse_blk tag_info.ReparseTag;
849 }875 }
850 break :reparse_blk 0;876 break :reparse_blk 0;
851 };877 };
lib/std/fs/test.zig+25
...@@ -156,6 +156,31 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo...@@ -156,6 +156,31 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
156 try testing.expectEqualStrings(target_path, given);156 try testing.expectEqualStrings(target_path, given);
157}157}
158158
159test "stat on a symlink returns Kind.sym_link" {
160 try testWithAllSupportedPathTypes(struct {
161 fn impl(ctx: *TestContext) !void {
162 const dir_target_path = try ctx.transformPath("subdir");
163 try ctx.dir.makeDir(dir_target_path);
164
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
169 ctx.dir.symLink(dir_target_path, "symlink", .{ .is_directory = true }) catch |err| switch (err) {
170 // Symlink requires admin privileges on windows, so this test can legitimately fail.
171 error.AccessDenied => return error.SkipZigTest,
172 else => return err,
173 };
174
175 var symlink = try ctx.dir.openDir("symlink", .{ .no_follow = true });
176 defer symlink.close();
177
178 const stat = try symlink.stat();
179 try testing.expectEqual(File.Kind.sym_link, stat.kind);
180 }
181 }.impl);
182}
183
159test "relative symlink to parent directory" {184test "relative symlink to parent directory" {
160 var tmp = tmpDir(.{});185 var tmp = tmpDir(.{});
161 defer tmp.cleanup();186 defer tmp.cleanup();
lib/std/os/windows.zig+9
...@@ -2972,6 +2972,15 @@ pub const FILE_INFORMATION_CLASS = enum(c_int) {...@@ -2972,6 +2972,15 @@ pub const FILE_INFORMATION_CLASS = enum(c_int) {
2972 FileMaximumInformation,2972 FileMaximumInformation,
2973};2973};
29742974
2975pub const FILE_ATTRIBUTE_TAG_INFO = extern struct {
2976 FileAttributes: DWORD,
2977 ReparseTag: DWORD,
2978};
2979
2980/// "If this bit is set, the file or directory represents another named entity in the system."
2981/// https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags
2982pub const reparse_tag_name_surrogate_bit = 0x20000000;
2983
2975pub const FILE_DISPOSITION_INFORMATION = extern struct {2984pub const FILE_DISPOSITION_INFORMATION = extern struct {
2976 DeleteFile: BOOLEAN,2985 DeleteFile: BOOLEAN,
2977};2986};