authorgravatar for git@vexu.euVeikka Tuominen <git@vexu.eu> 2023-12-22 12:06:10+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2023-12-22 12:06:10+02:00
logd787b78d2c7e4d6264a27308995e522614e03a7f
tree05f0d520622d82440c19bacce50131beabec3832
parentcbf2b1fea43c53cb33a362b277fe688f06744bf1
parentf5d0664e78211fbc366801868d59d9f909cd0471
signaturebadge-question-mark Signed by PGP key 4AEE18F83AFDEB23

Merge pull request #18326 from squeek502/stat-symlink

`File.stat`: Support detection of `Kind.sym_link` on Windows

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

lib/std/fs/File.zig+32-6
...@@ -391,7 +391,26 @@ pub fn stat(self: File) StatError!Stat {...@@ -391,7 +391,26 @@ pub fn stat(self: File) StatError!Stat {
391 .inode = info.InternalInformation.IndexNumber,391 .inode = info.InternalInformation.IndexNumber,
392 .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),392 .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
393 .mode = 0,393 .mode = 0,
394 .kind = if (info.StandardInformation.Directory == 0) .file else .directory,394 .kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: {
395 var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
396 const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
397 switch (tag_rc) {
398 .SUCCESS => {},
399 // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
400 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
401 .INFO_LENGTH_MISMATCH => unreachable,
402 .ACCESS_DENIED => return error.AccessDenied,
403 else => return windows.unexpectedStatus(rc),
404 }
405 if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) {
406 break :reparse_point .sym_link;
407 }
408 // Unknown reparse point
409 break :reparse_point .unknown;
410 } else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0)
411 .directory
412 else
413 .file,
395 .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),414 .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
396 .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),415 .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
397 .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),416 .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),
...@@ -793,7 +812,7 @@ pub const MetadataWindows = struct {...@@ -793,7 +812,7 @@ pub const MetadataWindows = struct {
793 /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`812 /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
794 pub fn kind(self: Self) Kind {813 pub fn kind(self: Self) Kind {
795 if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {814 if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
796 if (self.reparse_tag & 0x20000000 != 0) {815 if (self.reparse_tag & windows.reparse_tag_name_surrogate_bit != 0) {
797 return .sym_link;816 return .sym_link;
798 }817 }
799 } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {818 } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
...@@ -844,10 +863,17 @@ pub fn metadata(self: File) MetadataError!Metadata {...@@ -844,10 +863,17 @@ pub fn metadata(self: File) MetadataError!Metadata {
844863
845 const reparse_tag: windows.DWORD = reparse_blk: {864 const reparse_tag: windows.DWORD = reparse_blk: {
846 if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {865 if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
847 var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;866 var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
848 try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);867 const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
849 const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));868 switch (tag_rc) {
850 break :reparse_blk reparse_struct.ReparseTag;869 .SUCCESS => {},
870 // INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
871 // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
872 .INFO_LENGTH_MISMATCH => unreachable,
873 .ACCESS_DENIED => return error.AccessDenied,
874 else => return windows.unexpectedStatus(rc),
875 }
876 break :reparse_blk tag_info.ReparseTag;
851 }877 }
852 break :reparse_blk 0;878 break :reparse_blk 0;
853 };879 };
lib/std/fs/test.zig+82
...@@ -156,6 +156,88 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo...@@ -156,6 +156,88 @@ 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 "File.stat on a File that is a symlink returns Kind.sym_link" {
160 // This test requires getting a file descriptor of a symlink which
161 // is not possible on all targets
162 switch (builtin.target.os.tag) {
163 .windows, .linux => {},
164 else => return error.SkipZigTest,
165 }
166
167 try testWithAllSupportedPathTypes(struct {
168 fn impl(ctx: *TestContext) !void {
169 const dir_target_path = try ctx.transformPath("subdir");
170 try ctx.dir.makeDir(dir_target_path);
171
172 ctx.dir.symLink(dir_target_path, "symlink", .{ .is_directory = true }) catch |err| switch (err) {
173 // Symlink requires admin privileges on windows, so this test can legitimately fail.
174 error.AccessDenied => return error.SkipZigTest,
175 else => return err,
176 };
177
178 var symlink = switch (builtin.target.os.tag) {
179 .windows => windows_symlink: {
180 const w = std.os.windows;
181
182 const sub_path_w = try std.os.windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");
183
184 var result = Dir{
185 .fd = undefined,
186 };
187
188 const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
189 var nt_name = w.UNICODE_STRING{
190 .Length = path_len_bytes,
191 .MaximumLength = path_len_bytes,
192 .Buffer = @constCast(&sub_path_w.data),
193 };
194 var attr = w.OBJECT_ATTRIBUTES{
195 .Length = @sizeOf(w.OBJECT_ATTRIBUTES),
196 .RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd,
197 .Attributes = 0,
198 .ObjectName = &nt_name,
199 .SecurityDescriptor = null,
200 .SecurityQualityOfService = null,
201 };
202 var io: w.IO_STATUS_BLOCK = undefined;
203 const rc = w.ntdll.NtCreateFile(
204 &result.fd,
205 w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE,
206 &attr,
207 &io,
208 null,
209 w.FILE_ATTRIBUTE_NORMAL,
210 w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
211 w.FILE_OPEN,
212 // FILE_OPEN_REPARSE_POINT is the important thing here
213 w.FILE_OPEN_REPARSE_POINT | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT,
214 null,
215 0,
216 );
217
218 switch (rc) {
219 .SUCCESS => break :windows_symlink result,
220 else => return w.unexpectedStatus(rc),
221 }
222 },
223 .linux => linux_symlink: {
224 const sub_path_c = try os.toPosixPath("symlink");
225 // the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
226 // note that if O_DIRECTORY is set, then this will error with ENOTDIR
227 const flags = os.O.NOFOLLOW | os.O.PATH | os.O.RDONLY | os.O.CLOEXEC;
228 const fd = try os.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
229 break :linux_symlink Dir{ .fd = fd };
230 },
231 else => unreachable,
232 };
233 defer symlink.close();
234
235 const stat = try symlink.stat();
236 try testing.expectEqual(File.Kind.sym_link, stat.kind);
237 }
238 }.impl);
239}
240
159test "relative symlink to parent directory" {241test "relative symlink to parent directory" {
160 var tmp = tmpDir(.{});242 var tmp = tmpDir(.{});
161 defer tmp.cleanup();243 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};