Skip to content

Commit b26ade2

Browse files
committed
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. 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).
1 parent f36ac22 commit b26ade2

File tree

3 files changed

+65
-6
lines changed

3 files changed

+65
-6
lines changed

lib/std/fs/File.zig

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,26 @@ pub fn stat(self: File) StatError!Stat {
389389
.inode = info.InternalInformation.IndexNumber,
390390
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
391391
.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,
393412
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
394413
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
395414
.ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
@@ -791,7 +810,7 @@ pub const MetadataWindows = struct {
791810
/// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
792811
pub fn kind(self: Self) Kind {
793812
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) {
795814
return .sym_link;
796815
}
797816
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
@@ -842,10 +861,17 @@ pub fn metadata(self: File) MetadataError!Metadata {
842861

843862
const reparse_tag: windows.DWORD = reparse_blk: {
844863
if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
845-
var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
846-
try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
847-
const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
848-
break :reparse_blk reparse_struct.ReparseTag;
864+
var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
865+
const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
866+
switch (tag_rc) {
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;
849875
}
850876
break :reparse_blk 0;
851877
};

lib/std/fs/test.zig

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

159+
test "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: Test file symlinks; there's currently no way to avoid following symlinks
166+
// when opening files.
167+
168+
ctx.dir.symLink(dir_target_path, "symlink", .{ .is_directory = true }) catch |err| switch (err) {
169+
// Symlink requires admin privileges on windows, so this test can legitimately fail.
170+
error.AccessDenied => return error.SkipZigTest,
171+
else => return err,
172+
};
173+
174+
var symlink = try ctx.dir.openDir("symlink", .{ .no_follow = true });
175+
defer symlink.close();
176+
177+
const stat = try symlink.stat();
178+
try testing.expectEqual(File.Kind.sym_link, stat.kind);
179+
}
180+
}.impl);
181+
}
182+
159183
test "relative symlink to parent directory" {
160184
var tmp = tmpDir(.{});
161185
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)