Skip to content

File.stat: Support detection of Kind.sym_link on Windows #18326

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions lib/std/fs/File.zig
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,26 @@ pub fn stat(self: File) StatError!Stat {
.inode = info.InternalInformation.IndexNumber,
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.mode = 0,
.kind = if (info.StandardInformation.Directory == 0) .file else .directory,
.kind = if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) reparse_point: {
var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
switch (tag_rc) {
.SUCCESS => {},
// INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
.INFO_LENGTH_MISMATCH => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
if (tag_info.ReparseTag & windows.reparse_tag_name_surrogate_bit != 0) {
break :reparse_point .sym_link;
}
// Unknown reparse point
break :reparse_point .unknown;
} else if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0)
.directory
else
.file,
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
.ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
Expand Down Expand Up @@ -791,7 +810,7 @@ pub const MetadataWindows = struct {
/// Can only return: `.file`, `.directory`, `.sym_link` or `.unknown`
pub fn kind(self: Self) Kind {
if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
if (self.reparse_tag & 0x20000000 != 0) {
if (self.reparse_tag & windows.reparse_tag_name_surrogate_bit != 0) {
return .sym_link;
}
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
Expand Down Expand Up @@ -842,10 +861,17 @@ pub fn metadata(self: File) MetadataError!Metadata {

const reparse_tag: windows.DWORD = reparse_blk: {
if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
const reparse_struct: *const windows.REPARSE_DATA_BUFFER = @ptrCast(@alignCast(&reparse_buf[0]));
break :reparse_blk reparse_struct.ReparseTag;
var tag_info: windows.FILE_ATTRIBUTE_TAG_INFO = undefined;
const tag_rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &tag_info, @sizeOf(windows.FILE_ATTRIBUTE_TAG_INFO), .FileAttributeTagInformation);
switch (tag_rc) {
.SUCCESS => {},
// INFO_LENGTH_MISMATCH and ACCESS_DENIED are the only documented possible errors
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/d295752f-ce89-4b98-8553-266d37c84f0e
.INFO_LENGTH_MISMATCH => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
}
break :reparse_blk tag_info.ReparseTag;
}
break :reparse_blk 0;
};
Expand Down
82 changes: 82 additions & 0 deletions lib/std/fs/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,88 @@ fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !vo
try testing.expectEqualStrings(target_path, given);
}

test "File.stat on a File that is a symlink returns Kind.sym_link" {
// This test requires getting a file descriptor of a symlink which
// is not possible on all targets
switch (builtin.target.os.tag) {
.windows, .linux => {},
else => return error.SkipZigTest,
}

try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const dir_target_path = try ctx.transformPath("subdir");
try ctx.dir.makeDir(dir_target_path);

ctx.dir.symLink(dir_target_path, "symlink", .{ .is_directory = true }) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
error.AccessDenied => return error.SkipZigTest,
else => return err,
};

var symlink = switch (builtin.target.os.tag) {
.windows => windows_symlink: {
const w = std.os.windows;

const sub_path_w = try std.os.windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");

var result = Dir{
.fd = undefined,
};

const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
var nt_name = w.UNICODE_STRING{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(&sub_path_w.data),
};
var attr = w.OBJECT_ATTRIBUTES{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd,
.Attributes = 0,
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var io: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtCreateFile(
&result.fd,
w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE,
&attr,
&io,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
w.FILE_OPEN,
// FILE_OPEN_REPARSE_POINT is the important thing here
w.FILE_OPEN_REPARSE_POINT | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT,
null,
0,
);

switch (rc) {
.SUCCESS => break :windows_symlink result,
else => return w.unexpectedStatus(rc),
}
},
.linux => linux_symlink: {
const sub_path_c = try os.toPosixPath("symlink");
// the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
// note that if O_DIRECTORY is set, then this will error with ENOTDIR
const flags = os.O.NOFOLLOW | os.O.PATH | os.O.RDONLY | os.O.CLOEXEC;
const fd = try os.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
break :linux_symlink Dir{ .fd = fd };
},
else => unreachable,
};
defer symlink.close();

const stat = try symlink.stat();
try testing.expectEqual(File.Kind.sym_link, stat.kind);
}
}.impl);
}

test "relative symlink to parent directory" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
Expand Down
9 changes: 9 additions & 0 deletions lib/std/os/windows.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2972,6 +2972,15 @@ pub const FILE_INFORMATION_CLASS = enum(c_int) {
FileMaximumInformation,
};

pub const FILE_ATTRIBUTE_TAG_INFO = extern struct {
FileAttributes: DWORD,
ReparseTag: DWORD,
};

/// "If this bit is set, the file or directory represents another named entity in the system."
/// https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-point-tags
pub const reparse_tag_name_surrogate_bit = 0x20000000;

pub const FILE_DISPOSITION_INFORMATION = extern struct {
DeleteFile: BOOLEAN,
};
Expand Down