| author | |
| committer | |
| log | d787b78d2c7e4d6264a27308995e522614e03a7f |
| tree | 05f0d520622d82440c19bacce50131beabec3832 |
| parent | cbf2b1fea43c53cb33a362b277fe688f06744bf1 |
| parent | f5d0664e78211fbc366801868d59d9f909cd0471 |
| signature |
`File.stat`: Support detection of `Kind.sym_link` on Windows3 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 { |
| 844 | 863 | ||
| 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 | } |
| 158 | 158 | ||
| 159 | test "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 | |||
| 159 | test "relative symlink to parent directory" { | 241 | test "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 | }; |
| 2974 | 2974 | ||
| 2975 | pub 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 | ||
| 2982 | pub const reparse_tag_name_surrogate_bit = 0x20000000; | ||
| 2983 | |||
| 2975 | pub const FILE_DISPOSITION_INFORMATION = extern struct { | 2984 | pub const FILE_DISPOSITION_INFORMATION = extern struct { |
| 2976 | DeleteFile: BOOLEAN, | 2985 | DeleteFile: BOOLEAN, |
| 2977 | }; | 2986 | }; |