Merge pull request #10486 from ominitay/metadata

std: Implement cross-platform metadata API
This commit is contained in:
Veikka Tuominen 2022-02-14 12:33:49 +02:00 committed by GitHub
commit 90f2a8d9c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 611 additions and 40 deletions

View File

@ -372,14 +372,10 @@ pub const Stat = extern struct {
uid: uid_t,
gid: gid_t,
rdev: i32,
atimesec: isize,
atimensec: isize,
mtimesec: isize,
mtimensec: isize,
ctimesec: isize,
ctimensec: isize,
birthtimesec: isize,
birthtimensec: isize,
atimespec: timespec,
mtimespec: timespec,
ctimespec: timespec,
birthtimespec: timespec,
size: off_t,
blocks: i64,
blksize: i32,
@ -389,24 +385,19 @@ pub const Stat = extern struct {
qspare: [2]i64,
pub fn atime(self: @This()) timespec {
return timespec{
.tv_sec = self.atimesec,
.tv_nsec = self.atimensec,
};
return self.atimespec;
}
pub fn mtime(self: @This()) timespec {
return timespec{
.tv_sec = self.mtimesec,
.tv_nsec = self.mtimensec,
};
return self.mtimespec;
}
pub fn ctime(self: @This()) timespec {
return timespec{
.tv_sec = self.ctimesec,
.tv_nsec = self.ctimensec,
};
return self.ctimespec;
}
pub fn birthtime(self: @This()) timespec {
return self.birthtimespec;
}
};

View File

@ -283,6 +283,10 @@ pub const Stat = extern struct {
pub fn ctime(self: @This()) timespec {
return self.ctim;
}
pub fn birthtime(self: @This()) timespec {
return self.birthtim;
}
};
pub const timespec = extern struct {

View File

@ -226,7 +226,7 @@ pub const Stat = extern struct {
pub fn ctime(self: @This()) timespec {
return self.ctim;
}
pub fn crtime(self: @This()) timespec {
pub fn birthtime(self: @This()) timespec {
return self.crtim;
}
};

View File

@ -312,6 +312,10 @@ pub const Stat = extern struct {
pub fn ctime(self: @This()) timespec {
return self.ctim;
}
pub fn birthtime(self: @This()) timespec {
return self.birthtim;
}
};
pub const timespec = extern struct {

View File

@ -235,6 +235,10 @@ pub const Stat = extern struct {
pub fn ctime(self: @This()) timespec {
return self.ctim;
}
pub fn birthtime(self: @This()) timespec {
return self.birthtim;
}
};
pub const timespec = extern struct {

View File

@ -41,32 +41,20 @@ pub const Stat = extern struct {
blksize: i32,
blocks: i64,
atimesec: time_t,
atimensec: isize,
mtimesec: time_t,
mtimensec: isize,
ctimesec: time_t,
ctimensec: isize,
atim: timespec,
mtim: timespec,
ctim: timespec,
pub fn atime(self: @This()) timespec {
return timespec{
.tv_sec = self.atimesec,
.tv_nsec = self.atimensec,
};
return self.atim;
}
pub fn mtime(self: @This()) timespec {
return timespec{
.tv_sec = self.mtimesec,
.tv_nsec = self.mtimensec,
};
return self.mtim;
}
pub fn ctime(self: @This()) timespec {
return timespec{
.tv_sec = self.ctimesec,
.tv_nsec = self.ctimensec,
};
return self.ctim;
}
};

View File

@ -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.

View File

@ -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,

View File

@ -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));
}

View File

@ -88,7 +88,7 @@ pub const mode_t = u32;
pub const time_t = i64; // match https://github.com/CraneStation/wasi-libc
pub const timespec = struct {
pub const timespec = extern struct {
tv_sec: time_t,
tv_nsec: isize,