Commit 11b4cc589c

ominitay <37453713+ominitay@users.noreply.github.com>
2022-01-08 22:22:23
std.fs: Implement cross-platform metadata API
Implements a cross-platform metadata API, aiming to reduce unnecessary Unix-dependence of the `std.fs` api. Presently, all OSes beside Windows are treated as Unix; this is likely the best way to treat things by default, instead of explicitly listing each Unix-like OS. Platform-specific operations are not provided by `File.Metadata`, and instead are to be accessed from `File.Metadata.inner`. Adds: - File.setPermissions() : Sets permission of a file according to a `Permissions` struct (not available on WASI) - File.Permissions : A cross-platform representation of file permissions - Permissions.readOnly() : Returns whether the file is read-only - Permissions.setReadOnly() : Sets whether the file is read-only - Permissions.unixSet() : Sets permissions for a class (UNIX-only) - Permissions.unixGet() : Checks a permission for a class (UNIX-only) - Permissions.unixNew() : Returns a new Permissions struct to represent the passed mode (UNIX-only) - File.Metadata : A cross-platform representation of file metadata - Metadata.size() : Returns the size of a file - Metadata.permissions() : Returns a `Permissions` struct, representing permissions on the file - Metadata.kind() : Returns the `Kind` of the file - Metadata.accessed() : Returns the time the file was last accessed - Metadata.modified() : Returns the time the file was last modified - Metadata.created() : Returns the time the file was created (this is an optional, as the underlying filesystem, or OS may not support this) Methods of `File.Metadata` are also available for the below, so I won't repeat myself The below may be used for platform-specific functionality - File.MetadataUnix : The internal implementation of `File.Metadata` on Unices - File.MetadataLinux : The internal implementation of `File.Metadata` on Linux - File.MetadataWindows : The implementation of `File.Metadata` on Windows
1 parent b261064
Changed files (3)
lib/std/fs/file.zig
@@ -398,6 +398,485 @@ pub const File = struct {
         try os.fchown(self.handle, owner, group);
     }
 
+    /// Cross-platform representation of permissions on a file.
+    /// The `readonly` and `setReadonly` are the only methods available across all platforms.
+    /// Platform-specific functionality is available through the `inner` field.
+    pub const Permissions = struct {
+        /// You may use the `inner` field to use platform-specific functionality
+        inner: switch (builtin.os.tag) {
+            .windows => PermissionsWindows,
+            else => PermissionsUnix,
+        },
+
+        const Self = @This();
+
+        /// Returns `true` if permissions represent an unwritable file.
+        /// On Unix, `true` is returned only if no class has write permissions.
+        pub fn readOnly(self: Self) bool {
+            return self.inner.readOnly();
+        }
+
+        /// Sets whether write permissions are provided.
+        /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`
+        /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
+        pub fn setReadOnly(self: *Self, read_only: bool) void {
+            self.inner.setReadOnly(read_only);
+        }
+    };
+
+    pub const PermissionsWindows = struct {
+        attributes: os.windows.DWORD,
+
+        const Self = @This();
+
+        /// Returns `true` if permissions represent an unwritable file.
+        pub fn readOnly(self: Self) bool {
+            return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0;
+        }
+
+        /// Sets whether write permissions are provided.
+        /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
+        pub fn setReadOnly(self: *Self, read_only: bool) void {
+            if (read_only) {
+                self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY;
+            } else {
+                self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY);
+            }
+        }
+    };
+
+    pub const PermissionsUnix = struct {
+        mode: Mode,
+
+        const Self = @This();
+
+        /// Returns `true` if permissions represent an unwritable file.
+        /// `true` is returned only if no class has write permissions.
+        pub fn readOnly(self: Self) bool {
+            return self.mode & 0o222 == 0;
+        }
+
+        /// Sets whether write permissions are provided.
+        /// This affects *all* classes. If this is undesired, use `unixSet`
+        /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
+        pub fn setReadOnly(self: *Self, read_only: bool) void {
+            if (read_only) {
+                self.mode &= ~@as(Mode, 0o222);
+            } else {
+                self.mode |= @as(Mode, 0o222);
+            }
+        }
+
+        pub const Class = enum(u2) {
+            user = 2,
+            group = 1,
+            other = 0,
+        };
+
+        pub const Permission = enum(u3) {
+            read = 0o4,
+            write = 0o2,
+            execute = 0o1,
+        };
+
+        /// Returns `true` if the chosen class has the selected permission.
+        /// This method is only available on Unix platforms.
+        pub fn unixHas(self: Self, class: Class, permission: Permission) bool {
+            const mask = @as(Mode, @enumToInt(permission)) << @as(u3, @enumToInt(class)) * 3;
+            return self.mode & mask != 0;
+        }
+
+        /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
+        /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
+        pub fn unixSet(self: *Self, class: Class, permissions: struct {
+            read: ?bool = null,
+            write: ?bool = null,
+            execute: ?bool = null,
+        }) void {
+            const shift = @as(u3, @enumToInt(class)) * 3;
+            if (permissions.read) |r| {
+                if (r) {
+                    self.mode |= @as(Mode, 0o4) << shift;
+                } else {
+                    self.mode &= ~(@as(Mode, 0o4) << shift);
+                }
+            }
+            if (permissions.write) |w| {
+                if (w) {
+                    self.mode |= @as(Mode, 0o2) << shift;
+                } else {
+                    self.mode &= ~(@as(Mode, 0o2) << shift);
+                }
+            }
+            if (permissions.execute) |x| {
+                if (x) {
+                    self.mode |= @as(Mode, 0o1) << shift;
+                } else {
+                    self.mode &= ~(@as(Mode, 0o1) << shift);
+                }
+            }
+        }
+
+        /// Returns a `Permissions` struct representing the permissions from the passed mode.
+        pub fn unixNew(new_mode: Mode) Self {
+            return Self{
+                .mode = new_mode,
+            };
+        }
+    };
+
+    pub const SetPermissionsError = ChmodError;
+
+    /// Sets permissions according to the provided `Permissions` struct.
+    /// This method is *NOT* available on WASI
+    pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
+        switch (builtin.os.tag) {
+            .windows => {
+                var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+                var info = windows.FILE_BASIC_INFORMATION{
+                    .CreationTime = 0,
+                    .LastAccessTime = 0,
+                    .LastWriteTime = 0,
+                    .ChangeTime = 0,
+                    .FileAttributes = permissions.inner.attributes,
+                };
+                const rc = windows.ntdll.NtSetInformationFile(
+                    self.handle,
+                    &io_status_block,
+                    &info,
+                    @sizeOf(windows.FILE_BASIC_INFORMATION),
+                    .FileBasicInformation,
+                );
+                switch (rc) {
+                    .SUCCESS => return,
+                    .INVALID_HANDLE => unreachable,
+                    .ACCESS_DENIED => return error.AccessDenied,
+                    else => return windows.unexpectedStatus(rc),
+                }
+            },
+            .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod
+            else => {
+                try self.chmod(permissions.inner.mode);
+            },
+        }
+    }
+
+    /// Cross-platform representation of file metadata.
+    /// Platform-specific functionality is available through the `inner` field.
+    pub const Metadata = struct {
+        /// You may use the `inner` field to use platform-specific functionality
+        inner: switch (builtin.os.tag) {
+            .windows => MetadataWindows,
+            .linux => MetadataLinux,
+            else => MetadataUnix,
+        },
+
+        const Self = @This();
+
+        /// Returns the size of the file
+        pub fn size(self: Self) u64 {
+            return self.inner.size();
+        }
+
+        /// Returns a `Permissions` struct, representing the permissions on the file
+        pub fn permissions(self: Self) Permissions {
+            return self.inner.permissions();
+        }
+
+        /// Returns the `Kind` of file.
+        /// On Windows, can only return: `.File`, `.Directory`, `.SymLink` or `.Unknown`
+        pub fn kind(self: Self) Kind {
+            return self.inner.kind();
+        }
+
+        /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
+        pub fn accessed(self: Self) i128 {
+            return self.inner.accessed();
+        }
+
+        /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
+        pub fn modified(self: Self) i128 {
+            return self.inner.modified();
+        }
+
+        /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
+        /// On Windows, this cannot return null
+        /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11
+        /// On Unices, this returns null if the filesystem or OS does not support creation times
+        /// On MacOS, this returns the ctime if the filesystem does not support creation times; this is insanity, and yet another reason to hate on Apple
+        pub fn created(self: Self) ?i128 {
+            return self.inner.created();
+        }
+    };
+
+    pub const MetadataUnix = struct {
+        stat: os.Stat,
+
+        const Self = @This();
+
+        /// Returns the size of the file
+        pub fn size(self: Self) u64 {
+            return @intCast(u64, self.stat.size);
+        }
+
+        /// Returns a `Permissions` struct, representing the permissions on the file
+        pub fn permissions(self: Self) Permissions {
+            return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } };
+        }
+
+        /// Returns the `Kind` of the file
+        pub fn kind(self: Self) Kind {
+            if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) {
+                .BLOCK_DEVICE => Kind.BlockDevice,
+                .CHARACTER_DEVICE => Kind.CharacterDevice,
+                .DIRECTORY => Kind.Directory,
+                .SYMBOLIC_LINK => Kind.SymLink,
+                .REGULAR_FILE => Kind.File,
+                .SOCKET_STREAM, .SOCKET_DGRAM => Kind.UnixDomainSocket,
+                else => Kind.Unknown,
+            };
+
+            const m = self.stat.mode & os.S.IFMT;
+
+            switch (m) {
+                os.S.IFBLK => return Kind.BlockDevice,
+                os.S.IFCHR => return Kind.CharacterDevice,
+                os.S.IFDIR => return Kind.Directory,
+                os.S.IFIFO => return Kind.NamedPipe,
+                os.S.IFLNK => return Kind.SymLink,
+                os.S.IFREG => return Kind.File,
+                os.S.IFSOCK => return Kind.UnixDomainSocket,
+                else => {},
+            }
+
+            if (builtin.os.tag == .solaris) switch (m) {
+                os.S.IFDOOR => return Kind.Door,
+                os.S.IFPORT => return Kind.EventPort,
+                else => {},
+            };
+
+            return .Unknown;
+        }
+
+        /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
+        pub fn accessed(self: Self) i128 {
+            const atime = self.stat.atime();
+            return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec;
+        }
+
+        /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
+        pub fn modified(self: Self) i128 {
+            const mtime = self.stat.mtime();
+            return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec;
+        }
+
+        /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
+        /// Returns null if this is not supported by the OS or filesystem
+        pub fn created(self: Self) ?i128 {
+            if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null;
+            const birthtime = self.stat.birthtime();
+
+            // If the filesystem doesn't support this the value *should* be:
+            // On FreeBSD: tv_nsec = 0, tv_sec = -1
+            // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0
+            // On MacOS, it is set to ctime -- we cannot detect this!!
+            switch (builtin.os.tag) {
+                .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null,
+                .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null,
+                .macos => {},
+                else => @compileError("Creation time detection not implemented for OS"),
+            }
+
+            return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec;
+        }
+    };
+
+    /// `MetadataUnix`, but using Linux's `statx` syscall.
+    /// On Linux versions below 4.11, `statx` will be filled with data from stat.
+    pub const MetadataLinux = struct {
+        statx: os.linux.Statx,
+
+        const Self = @This();
+
+        /// Returns the size of the file
+        pub fn size(self: Self) u64 {
+            return self.statx.size;
+        }
+
+        /// Returns a `Permissions` struct, representing the permissions on the file
+        pub fn permissions(self: Self) Permissions {
+            return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } };
+        }
+
+        /// Returns the `Kind` of the file
+        pub fn kind(self: Self) Kind {
+            const m = self.statx.mode & os.S.IFMT;
+
+            switch (m) {
+                os.S.IFBLK => return Kind.BlockDevice,
+                os.S.IFCHR => return Kind.CharacterDevice,
+                os.S.IFDIR => return Kind.Directory,
+                os.S.IFIFO => return Kind.NamedPipe,
+                os.S.IFLNK => return Kind.SymLink,
+                os.S.IFREG => return Kind.File,
+                os.S.IFSOCK => return Kind.UnixDomainSocket,
+                else => {},
+            }
+
+            return .Unknown;
+        }
+
+        /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
+        pub fn accessed(self: Self) i128 {
+            return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec;
+        }
+
+        /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
+        pub fn modified(self: Self) i128 {
+            return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec;
+        }
+
+        /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
+        /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11
+        pub fn created(self: Self) ?i128 {
+            if (self.statx.mask & os.linux.STATX_BTIME == 0) return null;
+            return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec;
+        }
+    };
+
+    pub const MetadataWindows = struct {
+        attributes: windows.DWORD,
+        reparse_tag: windows.DWORD,
+        _size: u64,
+        access_time: i128,
+        modified_time: i128,
+        creation_time: i128,
+
+        const Self = @This();
+
+        /// Returns the size of the file
+        pub fn size(self: Self) u64 {
+            return self._size;
+        }
+
+        /// Returns a `Permissions` struct, representing the permissions on the file
+        pub fn permissions(self: Self) Permissions {
+            return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } };
+        }
+
+        /// Returns the `Kind` of the file.
+        /// Can only return: `.File`, `.Directory`, `.SymLink` or `.Unknown`
+        pub fn kind(self: Self) Kind {
+            if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
+                if (self.reparse_tag & 0x20000000 != 0) {
+                    return .SymLink;
+                }
+            } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
+                return .Directory;
+            } else {
+                return .File;
+            }
+            return .Unknown;
+        }
+
+        /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
+        pub fn accessed(self: Self) i128 {
+            return self.access_time;
+        }
+
+        /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
+        pub fn modified(self: Self) i128 {
+            return self.modified_time;
+        }
+
+        /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
+        /// This never returns null, only returning an optional for compatibility with other OSes
+        pub fn created(self: Self) ?i128 {
+            return self.creation_time;
+        }
+    };
+
+    pub const MetadataError = os.FStatError;
+
+    pub fn metadata(self: File) MetadataError!Metadata {
+        return Metadata{
+            .inner = switch (builtin.os.tag) {
+                .windows => blk: {
+                    var io_status_block: windows.IO_STATUS_BLOCK = undefined;
+                    var info: windows.FILE_ALL_INFORMATION = undefined;
+
+                    const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
+                    switch (rc) {
+                        .SUCCESS => {},
+                        .BUFFER_OVERFLOW => {},
+                        .INVALID_PARAMETER => unreachable,
+                        .ACCESS_DENIED => return error.AccessDenied,
+                        else => return windows.unexpectedStatus(rc),
+                    }
+
+                    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 = @ptrCast(*const windows.REPARSE_DATA_BUFFER, @alignCast(@alignOf(windows.REPARSE_DATA_BUFFER), &reparse_buf[0]));
+                            break :reparse_blk reparse_struct.ReparseTag;
+                        }
+                        break :reparse_blk 0;
+                    };
+
+                    break :blk MetadataWindows{
+                        .attributes = info.BasicInformation.FileAttributes,
+                        .reparse_tag = reparse_tag,
+                        ._size = @bitCast(u64, info.StandardInformation.EndOfFile),
+                        .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime),
+                        .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime),
+                        .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime),
+                    };
+                },
+                .linux => blk: {
+                    var stx = mem.zeroes(os.linux.Statx);
+                    const rcx = os.linux.statx(self.handle, "\x00", os.linux.AT.EMPTY_PATH, os.linux.STATX_TYPE | os.linux.STATX_MODE | os.linux.STATX_ATIME | os.linux.STATX_MTIME | os.linux.STATX_BTIME, &stx);
+
+                    switch (os.errno(rcx)) {
+                        .SUCCESS => {},
+                        // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11
+                        // Here, we call `fstat` and fill `stx` with the data we need
+                        .NOSYS => {
+                            const st = try os.fstat(self.handle);
+
+                            stx.mode = @intCast(u16, st.mode);
+
+                            // Hacky conversion from timespec to statx_timestamp
+                            stx.atime = std.mem.zeroes(os.linux.statx_timestamp);
+                            stx.atime.tv_sec = st.atim.tv_sec;
+                            stx.atime.tv_nsec = @intCast(u32, st.atim.tv_nsec); // Guaranteed to succeed (tv_nsec is always below 10^9)
+
+                            stx.mtime = std.mem.zeroes(os.linux.statx_timestamp);
+                            stx.mtime.tv_sec = st.mtim.tv_sec;
+                            stx.mtime.tv_nsec = @intCast(u32, st.mtim.tv_nsec);
+
+                            stx.mask = os.linux.STATX_BASIC_STATS | os.linux.STATX_MTIME;
+                        },
+                        .BADF => unreachable,
+                        .FAULT => unreachable,
+                        .NOMEM => return error.SystemResources,
+                        else => |err| return os.unexpectedErrno(err),
+                    }
+
+                    break :blk MetadataLinux{
+                        .statx = stx,
+                    };
+                },
+                else => blk: {
+                    const st = try os.fstat(self.handle);
+                    break :blk MetadataUnix{
+                        .stat = st,
+                    };
+                },
+            },
+        };
+    }
+
     pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;
 
     /// The underlying file system may have a different granularity than nanoseconds,
lib/std/fs/test.zig
@@ -1101,3 +1101,79 @@ test "chown" {
     defer dir.close();
     try dir.chown(null, null);
 }
+
+test "File.Metadata" {
+    var tmp = tmpDir(.{});
+    defer tmp.cleanup();
+
+    const file = try tmp.dir.createFile("test_file", .{ .read = true });
+    defer file.close();
+
+    const metadata = try file.metadata();
+    try testing.expect(metadata.kind() == .File);
+    try testing.expect(metadata.size() == 0);
+    _ = metadata.accessed();
+    _ = metadata.modified();
+    _ = metadata.created();
+}
+
+test "File.Permissions" {
+    if (builtin.os.tag == .wasi)
+        return error.SkipZigTest;
+
+    var tmp = tmpDir(.{});
+    defer tmp.cleanup();
+
+    const file = try tmp.dir.createFile("test_file", .{ .read = true });
+    defer file.close();
+
+    const metadata = try file.metadata();
+    var permissions = metadata.permissions();
+
+    try testing.expect(!permissions.readOnly());
+    permissions.setReadOnly(true);
+    try testing.expect(permissions.readOnly());
+
+    try file.setPermissions(permissions);
+    const new_permissions = (try file.metadata()).permissions();
+    try testing.expect(new_permissions.readOnly());
+
+    // Must be set to non-read-only to delete
+    permissions.setReadOnly(false);
+    try file.setPermissions(permissions);
+}
+
+test "File.PermissionsUnix" {
+    if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
+        return error.SkipZigTest;
+
+    var tmp = tmpDir(.{});
+    defer tmp.cleanup();
+
+    const file = try tmp.dir.createFile("test_file", .{ .mode = 0o666, .read = true });
+    defer file.close();
+
+    const metadata = try file.metadata();
+    var permissions = metadata.permissions();
+
+    permissions.setReadOnly(true);
+    try testing.expect(permissions.readOnly());
+    try testing.expect(!permissions.inner.unixHas(.user, .write));
+    permissions.inner.unixSet(.user, .{ .write = true });
+    try testing.expect(!permissions.readOnly());
+    try testing.expect(permissions.inner.unixHas(.user, .write));
+    try testing.expect(permissions.inner.mode & 0o400 != 0);
+
+    permissions.setReadOnly(true);
+    try file.setPermissions(permissions);
+    permissions = (try file.metadata()).permissions();
+    try testing.expect(permissions.readOnly());
+
+    // Must be set to non-read-only to delete
+    permissions.setReadOnly(false);
+    try file.setPermissions(permissions);
+
+    const permissions_unix = File.PermissionsUnix.unixNew(0o754);
+    try testing.expect(permissions_unix.unixHas(.user, .execute));
+    try testing.expect(!permissions_unix.unixHas(.other, .execute));
+}
lib/std/fs.zig
@@ -2231,6 +2231,31 @@ pub const Dir = struct {
     }
 
     pub const ChownError = File.ChownError;
+
+    const Permissions = File.Permissions;
+    pub const SetPermissionsError = File.SetPermissionsError;
+
+    /// Sets permissions according to the provided `Permissions` struct.
+    /// This method is *NOT* available on WASI
+    pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void {
+        const file: File = .{
+            .handle = self.fd,
+            .capable_io_mode = .blocking,
+        };
+        try file.setPermissions(permissions);
+    }
+
+    const Metadata = File.Metadata;
+    pub const MetadataError = File.MetadataError;
+
+    /// Returns a `Metadata` struct, representing the permissions on the directory
+    pub fn metadata(self: Dir) MetadataError!Metadata {
+        const file: File = .{
+            .handle = self.fd,
+            .capable_io_mode = .blocking,
+        };
+        return try file.metadata();
+    }
 };
 
 /// Returns a handle to the current working directory. It is not opened with iteration capability.