Skip to content

Commit d787b78

Browse files
authored
Merge pull request #18326 from squeek502/stat-symlink
`File.stat`: Support detection of `Kind.sym_link` on Windows
2 parents cbf2b1f + f5d0664 commit d787b78

File tree

3 files changed

+123
-6
lines changed

3 files changed

+123
-6
lines changed

lib/std/fs/File.zig

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,26 @@ pub fn stat(self: File) StatError!Stat {
391391
.inode = info.InternalInformation.IndexNumber,
392392
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
393393
.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,
395414
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
396415
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
397416
.ctime = windows.fromSysTime(info.BasicInformation.ChangeTime),
@@ -793,7 +812,7 @@ pub const MetadataWindows = struct {
793812
/// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
794813
pub fn kind(self: Self) Kind {
795814
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) {
797816
return .sym_link;
798817
}
799818
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
@@ -844,10 +863,17 @@ pub fn metadata(self: File) MetadataError!Metadata {
844863

845864
const reparse_tag: windows.DWORD = reparse_blk: {
846865
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;
851877
}
852878
break :reparse_blk 0;
853879
};

lib/std/fs/test.zig

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,88 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
156156
try testing.expectEqualStrings(target_path, given);
157157
}
158158

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+
159241
test "relative symlink to parent directory" {
160242
var tmp = tmpDir(.{});
161243
defer tmp.cleanup();

lib/std/os/windows.zig

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2972,6 +2972,15 @@ pub const FILE_INFORMATION_CLASS = enum(c_int) {
29722972
FileMaximumInformation,
29732973
};
29742974

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+
29752984
pub const FILE_DISPOSITION_INFORMATION = extern struct {
29762985
DeleteFile: BOOLEAN,
29772986
};

0 commit comments

Comments
 (0)