| 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 | 391 | .inode = info.InternalInformation.IndexNumber, |
| 392 | 392 | .size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)), |
| 393 | 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 | 414 | .atime = windows.fromSysTime(info.BasicInformation.LastAccessTime), |
| 396 | 415 | .mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime), |
| 397 | 416 | .ctime = windows.fromSysTime(info.BasicInformation.ChangeTime), |
| ... | ... | @@ -793,7 +812,7 @@ pub const MetadataWindows = struct { |
| 793 | 812 | /// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown` |
| 794 | 813 | pub fn kind(self: Self) Kind { |
| 795 | 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 | 816 | return .sym_link; |
| 798 | 817 | } |
| 799 | 818 | } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) { |
| ... | ... | @@ -844,10 +863,17 @@ pub fn metadata(self: File) MetadataError!Metadata { |
| 844 | 863 | |
| 845 | 864 | const reparse_tag: windows.DWORD = reparse_blk: { |
| 846 | 865 | if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) { |
| 847 | var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined; | |
| 848 | try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]); | |
| 849 | const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0])); | |
| 850 | break :reparse_blk reparse_struct.ReparseTag; | |
| 866 | var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined; | |
| 867 | const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation); | |
| 868 | switch (tag_rc) { | |
| 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 | 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 | 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 | 241 | test "relative symlink to parent directory" { |
| 160 | 242 | var tmp = tmpDir(.{}); |
| 161 | 243 | defer tmp.cleanup(); |
lib/std/os/windows.zig+9| ... | ... | @@ -2972,6 +2972,15 @@ pub const FILE_INFORMATION_CLASS = enum(c_int) { |
| 2972 | 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 | 2984 | pub const FILE_DISPOSITION_INFORMATION = extern struct { |
| 2976 | 2985 | DeleteFile: BOOLEAN, |
| 2977 | 2986 | }; |