Skip to content

Commit 68f9b7e

Browse files
committed
std: 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; I think that this is the best way to treat things by default, instead of explicitly listing each Posix-compliant OS. Adds: - File.setPermissions() : Sets permission of a file according to a `Permissions` struct (not available on WASI) - 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 - 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 representing the passed mode (UNIX-only)
1 parent c710d5e commit 68f9b7e

File tree

3 files changed

+367
-0
lines changed

3 files changed

+367
-0
lines changed

lib/std/fs.zig

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,6 +2210,31 @@ pub const Dir = struct {
22102210
}
22112211

22122212
pub const ChownError = File.ChownError;
2213+
2214+
const Permissions = File.Permissions;
2215+
pub const SetPermissionsError = File.SetPermissionsError;
2216+
2217+
/// Sets permissions according to the provided `Permissions` struct.
2218+
/// This method is *NOT* available on WASI
2219+
pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void {
2220+
const file: File = .{
2221+
.handle = self.fd,
2222+
.capable_io_mode = .blocking,
2223+
};
2224+
try file.setPermissions(permissions);
2225+
}
2226+
2227+
const Metadata = File.Metadata;
2228+
pub const MetadataError = File.MetadataError;
2229+
2230+
/// Returns a `Metadata` struct, representing the permissions on the directory
2231+
pub fn metadata(self: Dir) MetadataError!Metadata {
2232+
const file: File = .{
2233+
.handle = self.fd,
2234+
.capable_io_mode = .blocking,
2235+
};
2236+
return try file.metadata();
2237+
}
22132238
};
22142239

22152240
/// Returns a handle to the current working directory. It is not opened with iteration capability.

lib/std/fs/file.zig

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,274 @@ pub const File = struct {
385385
try os.fchown(self.handle, owner, group);
386386
}
387387

388+
/// Cross-platform representation of permissions on a file.
389+
/// The `readonly` and `setReadonly` are the only methods available across all platforms.
390+
/// Unix-only functionality is available through the `unixHas` and `unixSet` methods.
391+
pub const Permissions = switch (builtin.os.tag) {
392+
.windows => struct {
393+
attributes: os.windows.DWORD,
394+
395+
const Self = @This();
396+
397+
/// Returns `true` if permissions represent an unwritable file.
398+
/// On Unix, `true` is returned only if no class has write permissions.
399+
pub fn readOnly(self: Self) bool {
400+
return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0;
401+
}
402+
403+
/// Sets whether write permissions are provided.
404+
/// On Unix, this affects *all* classes. If this is undesired, use `unixSet`
405+
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
406+
pub fn setReadOnly(self: *Self, read_only: bool) void {
407+
if (read_only) {
408+
self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY;
409+
} else {
410+
self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY);
411+
}
412+
}
413+
},
414+
else => struct {
415+
mode: Mode,
416+
417+
const Self = @This();
418+
419+
/// Returns `true` if permissions represent an unwritable file.
420+
/// On Unix, `true` is returned only if no class has write permissions.
421+
pub fn readOnly(self: Self) bool {
422+
return self.mode & 0o222 == 0;
423+
}
424+
425+
/// Sets whether write permissions are provided.
426+
/// On Unix, this affects *all* classes. If this is undesired, use `unixSet`
427+
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
428+
pub fn setReadOnly(self: *Self, read_only: bool) void {
429+
if (read_only) {
430+
self.mode &= ~@as(Mode, 0o222);
431+
} else {
432+
self.mode |= @as(Mode, 0o222);
433+
}
434+
}
435+
436+
pub const Class = enum(u2) {
437+
user = 2,
438+
group = 1,
439+
other = 0,
440+
};
441+
442+
pub const Permission = enum(u3) {
443+
read = 0o4,
444+
write = 0o2,
445+
execute = 0o1,
446+
};
447+
448+
/// Returns `true` if the chosen class has the selected permission.
449+
/// This method is only available on Unix platforms.
450+
pub fn unixHas(self: *Self, class: Class, permission: Permission) bool {
451+
const mask = @as(Mode, @enumToInt(permission)) << @as(u3, @enumToInt(class)) * 3;
452+
return self.mode & mask != 0;
453+
}
454+
455+
/// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
456+
/// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
457+
/// This method is only available on Unix platforms.
458+
pub fn unixSet(self: *Self, class: Class, permissions: struct {
459+
read: ?bool = null,
460+
write: ?bool = null,
461+
execute: ?bool = null,
462+
}) void {
463+
const shift = @as(u3, @enumToInt(class)) * 3;
464+
if (permissions.read) |r| {
465+
if (r) {
466+
self.mode |= @as(Mode, 0o4) << shift;
467+
} else {
468+
self.mode &= ~(@as(Mode, 0o4) << shift);
469+
}
470+
}
471+
if (permissions.write) |w| {
472+
if (w) {
473+
self.mode |= @as(Mode, 0o2) << shift;
474+
} else {
475+
self.mode &= ~(@as(Mode, 0o2) << shift);
476+
}
477+
}
478+
if (permissions.execute) |x| {
479+
if (x) {
480+
self.mode |= @as(Mode, 0o1) << shift;
481+
} else {
482+
self.mode &= ~(@as(Mode, 0o1) << shift);
483+
}
484+
}
485+
}
486+
487+
/// Returns a `Permissions` struct representing the permissions from the passed mode.
488+
/// This method is only available on Unix platforms.
489+
pub fn unixNew(new_mode: Mode) Self {
490+
return Self{
491+
.mode = new_mode,
492+
};
493+
}
494+
},
495+
};
496+
497+
pub const SetPermissionsError = ChmodError;
498+
499+
/// Sets permissions according to the provided `Permissions` struct.
500+
/// This method is *NOT* available on WASI
501+
pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
502+
switch (builtin.os.tag) {
503+
.windows => {
504+
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
505+
var info = windows.FILE_BASIC_INFORMATION{
506+
.CreationTime = 0,
507+
.LastAccessTime = 0,
508+
.LastWriteTime = 0,
509+
.ChangeTime = 0,
510+
.FileAttributes = permissions.attributes,
511+
};
512+
const rc = windows.ntdll.NtSetInformationFile(
513+
self.handle,
514+
&io_status_block,
515+
&info,
516+
@sizeOf(windows.FILE_BASIC_INFORMATION),
517+
.FileBasicInformation,
518+
);
519+
switch (rc) {
520+
.SUCCESS => return,
521+
.INVALID_HANDLE => unreachable,
522+
.ACCESS_DENIED => return error.AccessDenied,
523+
else => return windows.unexpectedStatus(rc),
524+
}
525+
},
526+
.wasi => @compileError("Unsupported OS"), // TODO Waiting for wasi-filesystem to support chmod
527+
else => {
528+
try self.chmod(permissions.mode);
529+
},
530+
}
531+
}
532+
533+
/// Cross-platform representation of file metadata.
534+
/// Can be obtained with `File.metadata()`
535+
pub const Metadata = switch (builtin.os.tag) {
536+
.windows => struct {
537+
attributes: windows.DWORD,
538+
reparse_tag: windows.DWORD,
539+
_size: u64,
540+
atime: i128,
541+
mtime: i128,
542+
543+
const Self = @This();
544+
545+
/// Returns the size of the file
546+
pub fn size(self: Self) u64 {
547+
return self._size;
548+
}
549+
550+
/// Returns a `Permissions` struct, representing the permissions on the file
551+
pub fn permissions(self: Self) Permissions {
552+
return Permissions{ .attributes = self.attributes };
553+
}
554+
555+
/// Returns the `Kind` of the file.
556+
/// On Windows, returns: `.File`, `.Directory`, `.SymLink` or `.Unknown`
557+
pub fn kind(self: Self) Kind {
558+
if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
559+
if (self.reparse_tag & 0x20000000 != 0) {
560+
return .SymLink;
561+
}
562+
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
563+
return .Directory;
564+
} else {
565+
return .File;
566+
}
567+
return .Unknown;
568+
}
569+
570+
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
571+
pub fn accessed(self: Self) i128 {
572+
return self.atime;
573+
}
574+
575+
/// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
576+
pub fn modified(self: Self) i128 {
577+
return self.mtime;
578+
}
579+
},
580+
else => struct {
581+
stat: Stat,
582+
583+
const Self = @This();
584+
585+
/// Returns the size of the file
586+
pub fn size(self: Self) u64 {
587+
return self.stat.size;
588+
}
589+
590+
/// Returns a `Permissions` struct, representing the permissions on the file
591+
pub fn permissions(self: Self) Permissions {
592+
return Permissions{ .mode = self.stat.mode };
593+
}
594+
595+
/// Returns the `Kind` of file.
596+
/// On Windows, returns: `.File`, `.Directory`, `.SymLink` or `.Unknown`
597+
pub fn kind(self: Self) Kind {
598+
return self.stat.kind;
599+
}
600+
601+
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
602+
pub fn accessed(self: Self) i128 {
603+
return self.stat.atime;
604+
}
605+
606+
/// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
607+
pub fn modified(self: Self) i128 {
608+
return self.stat.mtime;
609+
}
610+
},
611+
};
612+
613+
pub const MetadataError = StatError;
614+
615+
/// Returns a `Metadata` struct, representing the permissions on the file
616+
pub fn metadata(self: File) MetadataError!Metadata {
617+
switch (builtin.os.tag) {
618+
.windows => {
619+
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
620+
var info: windows.FILE_ALL_INFORMATION = undefined;
621+
const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
622+
switch (rc) {
623+
.SUCCESS => {},
624+
.BUFFER_OVERFLOW => {},
625+
.INVALID_PARAMETER => unreachable,
626+
.ACCESS_DENIED => return error.AccessDenied,
627+
else => return windows.unexpectedStatus(rc),
628+
}
629+
630+
const reparse_tag: windows.DWORD = blk: {
631+
if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
632+
var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
633+
try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
634+
const reparse_struct = @ptrCast(*const windows.REPARSE_DATA_BUFFER, @alignCast(@alignOf(windows.REPARSE_DATA_BUFFER), &reparse_buf[0]));
635+
break :blk reparse_struct.ReparseTag;
636+
}
637+
break :blk 0;
638+
};
639+
640+
return Metadata{
641+
.attributes = info.BasicInformation.FileAttributes,
642+
.reparse_tag = reparse_tag,
643+
._size = @bitCast(u64, info.StandardInformation.EndOfFile),
644+
.atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
645+
.mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
646+
};
647+
},
648+
else => {
649+
return Metadata{
650+
.stat = try self.stat(),
651+
};
652+
},
653+
}
654+
}
655+
388656
pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;
389657

390658
/// The underlying file system may have a different granularity than nanoseconds,

lib/std/fs/test.zig

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,3 +1068,77 @@ test "chown" {
10681068
defer dir.close();
10691069
try dir.chown(null, null);
10701070
}
1071+
1072+
test "File.metadata" {
1073+
var tmp = tmpDir(.{});
1074+
defer tmp.cleanup();
1075+
1076+
const file = try tmp.dir.createFile("test_file", .{ .read = true });
1077+
defer file.close();
1078+
1079+
const metadata = try file.metadata();
1080+
try testing.expect(metadata.kind() == .File);
1081+
try testing.expect(metadata.size() == 0);
1082+
_ = metadata.accessed();
1083+
}
1084+
1085+
test "Metadata.permissions" {
1086+
if (builtin.os.tag == .wasi)
1087+
return error.SkipZigTest;
1088+
1089+
var tmp = tmpDir(.{});
1090+
defer tmp.cleanup();
1091+
1092+
const file = try tmp.dir.createFile("test_file", .{ .read = true });
1093+
defer file.close();
1094+
1095+
const metadata = try file.metadata();
1096+
var permissions = metadata.permissions();
1097+
1098+
try testing.expect(!permissions.readOnly());
1099+
permissions.setReadOnly(true);
1100+
try testing.expect(permissions.readOnly());
1101+
1102+
try file.setPermissions(permissions);
1103+
const new_permissions = (try file.metadata()).permissions();
1104+
try testing.expect(new_permissions.readOnly());
1105+
1106+
// Must be set to non-read-only to delete
1107+
permissions.setReadOnly(false);
1108+
try file.setPermissions(permissions);
1109+
}
1110+
1111+
test "Metadata.permissions unix" {
1112+
if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
1113+
return error.SkipZigTest;
1114+
1115+
var tmp = tmpDir(.{});
1116+
defer tmp.cleanup();
1117+
1118+
const file = try tmp.dir.createFile("test_file", .{ .mode = 0o666, .read = true });
1119+
defer file.close();
1120+
1121+
const metadata = try file.metadata();
1122+
var permissions = metadata.permissions();
1123+
1124+
permissions.setReadOnly(true);
1125+
try testing.expect(permissions.readOnly());
1126+
try testing.expect(!permissions.unixHas(.user, .write));
1127+
permissions.unixSet(.user, .{ .write = true });
1128+
try testing.expect(!permissions.readOnly());
1129+
try testing.expect(permissions.unixHas(.user, .write));
1130+
try testing.expect(permissions.mode & 0o400 != 0);
1131+
1132+
permissions.setReadOnly(true);
1133+
try file.setPermissions(permissions);
1134+
permissions = (try file.metadata()).permissions();
1135+
try testing.expect(permissions.readOnly());
1136+
1137+
// Must be set to non-read-only to delete
1138+
permissions.setReadOnly(false);
1139+
try file.setPermissions(permissions);
1140+
1141+
permissions = File.Permissions.unixNew(0o754);
1142+
try testing.expect(permissions.unixHas(.user, .execute));
1143+
try testing.expect(!permissions.unixHas(.other, .execute));
1144+
}

0 commit comments

Comments
 (0)