Skip to content
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

fs.Dir: Give the option to stat a symbolic link #20843

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
104 changes: 100 additions & 4 deletions lib/std/fs/Dir.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1849,6 +1849,42 @@ pub fn symLinkW(

pub const ReadLinkError = posix.ReadLinkError;

/// Same as `File.stat`, but if the path points to a symbolic link,
/// it will stat the link rather than the file it points to.
/// Note: if the target is a regular file, this function has the same
/// behaviour that `Dir.statFile`
pub fn statLink(self: Dir, sub_path: []const u8) StatFileError!Stat {
if (native_os == .windows) {
const path_w = windows.sliceToPrefixedFileW(self.fd, sub_path) catch return error.InvalidWtf8;
return self.statFileW(path_w.span().ptr, false);
}
if (native_os == .wasi and !builtin.link_libc) {
const st = try std.os.fstatat_wasi(self.fd, sub_path, .{ .SYMLINK_FOLLOW = false });
return Stat.fromWasi(st);
}
squeek502 marked this conversation as resolved.
Show resolved Hide resolved
const st = try posix.fstatat(self.fd, sub_path, posix.AT.SYMLINK_NOFOLLOW);
return Stat.fromSystem(st);
}

/// Same as `Dir.statLink`
pub fn statLinkZ(self: Dir, sub_path_c: [*:0]const u8) StatFileError!Stat {
if (native_os == .windows) {
const path_w = windows.cStrToPrefixedFileW(self.fd, sub_path_c) catch return error.InvalidWtf8;
return self.statFileW(path_w.span().ptr, false);
}
if (native_os == .wasi and !builtin.link_libc) {
const st = try std.os.fstatat_wasi(self.fd, mem.sliceTo(sub_path_c, 0), .{ .SYMLINK_FOLLOW = false });
return Stat.fromWasi(st);
}
const st = try posix.fstatatZ(self.fd, sub_path_c, posix.AT.SYMLINK_NOFOLLOW);
return Stat.fromSystem(st);
}

/// Windows only. Same as `Dir.statLink`
pub fn statLinkW(self: Dir, sub_path_w: [*:0]const u16) StatFileError!Stat {
return self.statFileW(sub_path_w, false);
}

/// Read value of a symbolic link.
/// The return value is a slice of `buffer`, from index `0`.
/// Asserts that the path parameter has no null bytes.
Expand Down Expand Up @@ -2623,7 +2659,7 @@ pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError

/// Returns metadata for a file inside the directory.
///
/// On Windows, this requires three syscalls. On other operating systems, it
/// On Windows, this requires two syscalls. On other operating systems, it
/// only takes one.
///
/// Symlinks are followed.
Expand All @@ -2634,9 +2670,8 @@ pub const StatFileError = File.OpenError || File.StatError || posix.FStatAtError
/// On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding.
pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat {
if (native_os == .windows) {
var file = try self.openFile(sub_path, .{});
defer file.close();
return file.stat();
const path_w = windows.sliceToPrefixedFileW(self.fd, sub_path) catch return error.InvalidWtf8;
samy-00007 marked this conversation as resolved.
Show resolved Hide resolved
return self.statFileW(path_w.span().ptr, true);
}
if (native_os == .wasi and !builtin.link_libc) {
const st = try std.os.fstatat_wasi(self.fd, sub_path, .{ .SYMLINK_FOLLOW = true });
Expand All @@ -2646,6 +2681,67 @@ pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat {
return Stat.fromSystem(st);
}

fn statFileW(self: Dir, sub_path_w: [*:0]const u16, follow_symlinks: bool) StatFileError!Stat {
const path_len_bytes = std.math.cast(u16, mem.sliceTo(sub_path_w, 0).len * 2) orelse return error.NameTooLong;
var nt_name = windows.UNICODE_STRING{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(sub_path_w),
};
// windows.OBJ_OPENLINK means it opens the link directly rather than its target
const attributes: u32 = if (follow_symlinks) 0 else windows.OBJ_OPENLINK;
var attr = windows.OBJECT_ATTRIBUTES{
.Length = @sizeOf(windows.OBJECT_ATTRIBUTES),
.RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w)) null else self.fd,
.Attributes = attributes,
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
var info: windows.FILE_ALL_INFORMATION = undefined;

const rc = windows.ntdll.NtQueryInformationByName(&attr, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
switch (rc) {
.SUCCESS => {},
// Buffer overflow here indicates that there is more information available than was able to be stored in the buffer
// size provided. This is treated as success because the type of variable-length information that this would be relevant for
// (name, volume name, etc) we don't care about.
.BUFFER_OVERFLOW => {},
.INVALID_PARAMETER => unreachable,
.ACCESS_DENIED => return error.AccessDenied,
else => return windows.unexpectedStatus(rc),
squeek502 marked this conversation as resolved.
Show resolved Hide resolved
}
return .{
.inode = info.InternalInformation.IndexNumber,
.size = @as(u64, @bitCast(info.StandardInformation.EndOfFile)),
.mode = 0,
.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.NtQueryInformationByName(&attr, &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.ChangeTime),
};
}

pub const ChmodError = File.ChmodError;

/// Changes the mode of the directory.
Expand Down
19 changes: 19 additions & 0 deletions lib/std/fs/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,25 @@ test "File.stat on a File that is a symlink returns Kind.sym_link" {
}.impl);
}

test "Dir.statLink" {
samy-00007 marked this conversation as resolved.
Show resolved Hide resolved
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const dir_target_path = try ctx.transformPath("test_file");
const file = try ctx.dir.createFile(dir_target_path, .{});
try file.writeAll("Some test content");
file.close();

try setupSymlink(ctx.dir, dir_target_path, "symlink", .{});

const file_stat = try ctx.dir.statLink("test_file");
const link_stat = try ctx.dir.statLink("symlink");

try testing.expectEqual(file_stat.kind, File.Kind.file);
try testing.expectEqual(link_stat.kind, File.Kind.sym_link);
samy-00007 marked this conversation as resolved.
Show resolved Hide resolved
}
}.impl);
}

test "openDir" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
Expand Down
7 changes: 7 additions & 0 deletions lib/std/os/windows/ntdll.zig
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ pub extern "ntdll" fn RtlVirtualUnwind(
EstablisherFrame: *DWORD64,
ContextPointers: ?*KNONVOLATILE_CONTEXT_POINTERS,
) callconv(WINAPI) *EXCEPTION_ROUTINE;
pub extern "ntdll" fn NtQueryInformationByName(
ObjectAttributes: *OBJECT_ATTRIBUTES,
IoStatusBlock: *IO_STATUS_BLOCK,
FileInformation: *anyopaque,
Length: ULONG,
FileInformationClass: FILE_INFORMATION_CLASS,
) callconv(WINAPI) NTSTATUS;
pub extern "ntdll" fn NtQueryInformationFile(
FileHandle: HANDLE,
IoStatusBlock: *IO_STATUS_BLOCK,
Expand Down
Loading