Skip to content

Commit 36bf76d

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 - 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.atime() : Returns the last time the file was accessed - Metadata.mtime() : Returns the last time the file was modified - Metadata.ctime() : Returns the time the file was created - 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 ae8d6fd commit 36bf76d

File tree

3 files changed

+368
-0
lines changed

3 files changed

+368
-0
lines changed

lib/std/fs.zig

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2210,6 +2210,30 @@ 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+
pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void {
2219+
const file: File = .{
2220+
.handle = self.fd,
2221+
.capable_io_mode = .blocking,
2222+
};
2223+
try file.setPermissions(permissions);
2224+
}
2225+
2226+
const Metadata = File.Metadata;
2227+
pub const MetadataError = File.MetadataError;
2228+
2229+
/// Returns a `Metadata` struct, representing the permissions on the directory
2230+
pub fn metadata(self: Dir) MetadataError!Metadata {
2231+
const file: File = .{
2232+
.handle = self.fd,
2233+
.capable_io_mode = .blocking,
2234+
};
2235+
return try file.metadata();
2236+
}
22132237
};
22142238

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

lib/std/fs/file.zig

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,284 @@ 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 = if (is_windows) error{AccessDenied} || os.UnexpectedError else ChmodError;
498+
499+
/// Sets permissions according to the provided `Permissions` struct.
500+
pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
501+
switch (builtin.os.tag) {
502+
.windows => {
503+
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
504+
var info = windows.FILE_BASIC_INFORMATION{
505+
.CreationTime = 0,
506+
.LastAccessTime = 0,
507+
.LastWriteTime = 0,
508+
.ChangeTime = 0,
509+
.FileAttributes = permissions.attributes,
510+
};
511+
const rc = windows.ntdll.NtSetInformationFile(
512+
self.handle,
513+
&io_status_block,
514+
&info,
515+
@sizeOf(windows.FILE_BASIC_INFORMATION),
516+
.FileBasicInformation,
517+
);
518+
switch (rc) {
519+
.SUCCESS => return,
520+
.INVALID_HANDLE => unreachable,
521+
.ACCESS_DENIED => return error.AccessDenied,
522+
else => return windows.unexpectedStatus(rc),
523+
}
524+
},
525+
else => {
526+
try self.chmod(permissions.mode);
527+
},
528+
}
529+
}
530+
531+
/// Cross-platform representation of file metadata.
532+
/// Can be obtained with `File.metadata()`
533+
pub const Metadata = switch (builtin.os.tag) {
534+
.windows => struct {
535+
attributes: windows.DWORD,
536+
reparse_tag: windows.DWORD,
537+
_size: u64,
538+
_atime: i128,
539+
_mtime: i128,
540+
_ctime: i128,
541+
542+
const Self = @This();
543+
544+
/// Returns the size of the file
545+
pub fn size(self: Self) u64 {
546+
return self._size;
547+
}
548+
549+
/// Returns a `Permissions` struct, representing the permissions on the file
550+
pub fn permissions(self: Self) Permissions {
551+
return Permissions{ .attributes = self.attributes };
552+
}
553+
554+
/// Returns the `Kind` of the file.
555+
/// On Windows, returns: `.File`, `.Directory`, `.SymLink` or `.Unknown`
556+
pub fn kind(self: Self) Kind {
557+
if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
558+
if (self.reparse_tag & 0x20000000 != 0) {
559+
return .SymLink;
560+
}
561+
} else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
562+
return .Directory;
563+
} else {
564+
return .File;
565+
}
566+
return .Unknown;
567+
}
568+
569+
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
570+
pub fn atime(self: Self) i128 {
571+
return self._atime;
572+
}
573+
574+
/// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
575+
pub fn mtime(self: Self) i128 {
576+
return self._mtime;
577+
}
578+
579+
/// Returns the time the file was created in nanoseconds since UTC 1970-01-01
580+
pub fn ctime(self: Self) i128 {
581+
return self._ctime;
582+
}
583+
},
584+
else => struct {
585+
stat: Stat,
586+
587+
const Self = @This();
588+
589+
/// Returns the size of the file
590+
pub fn size(self: Self) u64 {
591+
return self.stat.size;
592+
}
593+
594+
/// Returns a `Permissions` struct, representing the permissions on the file
595+
pub fn permissions(self: Self) Permissions {
596+
return Permissions{ .mode = self.stat.mode };
597+
}
598+
599+
/// Returns the `Kind` of file.
600+
/// On Windows, returns: `.File`, `.Directory`, `.SymLink` or `.Unknown`
601+
pub fn kind(self: Self) Kind {
602+
return self.stat.kind;
603+
}
604+
605+
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
606+
pub fn atime(self: Self) i128 {
607+
return self.stat.atime;
608+
}
609+
610+
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
611+
pub fn mtime(self: Self) i128 {
612+
return self.stat.mtime;
613+
}
614+
615+
/// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
616+
pub fn ctime(self: Self) i128 {
617+
return self.stat.ctime;
618+
}
619+
},
620+
};
621+
622+
pub const MetadataError = StatError;
623+
624+
/// Returns a `Metadata` struct, representing the permissions on the file
625+
pub fn metadata(self: File) MetadataError!Metadata {
626+
switch (builtin.os.tag) {
627+
.windows => {
628+
var io_status_block: windows.IO_STATUS_BLOCK = undefined;
629+
var info: windows.FILE_ALL_INFORMATION = undefined;
630+
const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
631+
switch (rc) {
632+
.SUCCESS => {},
633+
.BUFFER_OVERFLOW => {},
634+
.INVALID_PARAMETER => unreachable,
635+
.ACCESS_DENIED => return error.AccessDenied,
636+
else => return windows.unexpectedStatus(rc),
637+
}
638+
639+
const reparse_tag: windows.DWORD = blk: {
640+
if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
641+
var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
642+
try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
643+
const reparse_struct = @ptrCast(*const windows.REPARSE_DATA_BUFFER, @alignCast(@alignOf(windows.REPARSE_DATA_BUFFER), &reparse_buf[0]));
644+
break :blk reparse_struct.ReparseTag;
645+
}
646+
break :blk 0;
647+
};
648+
649+
return Metadata{
650+
.attributes = info.BasicInformation.FileAttributes,
651+
.reparse_tag = reparse_tag,
652+
._size = @bitCast(u64, info.StandardInformation.EndOfFile),
653+
._atime = windows.fromSysTime(info.BasicInformation.LastAccessTime),
654+
._mtime = windows.fromSysTime(info.BasicInformation.LastWriteTime),
655+
._ctime = windows.fromSysTime(info.BasicInformation.CreationTime),
656+
};
657+
},
658+
else => {
659+
return Metadata{
660+
.stat = try self.stat(),
661+
};
662+
},
663+
}
664+
}
665+
388666
pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;
389667

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

lib/std/fs/test.zig

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,3 +1068,69 @@ 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", .{});
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.atime();
1083+
}
1084+
1085+
test "Metadata.permissions" {
1086+
var tmp = tmpDir(.{});
1087+
defer tmp.cleanup();
1088+
1089+
const file = try tmp.dir.createFile("test_file", .{});
1090+
defer file.close();
1091+
1092+
const metadata = try file.metadata();
1093+
var permissions = metadata.permissions();
1094+
1095+
try testing.expect(!permissions.readonly());
1096+
permissions.setReadonly(true);
1097+
try testing.expect(permissions.readonly());
1098+
1099+
try file.setPermissions(permissions);
1100+
1101+
const new_metadata = try file.metadata();
1102+
const new_permissions = new_metadata.permissions();
1103+
1104+
try testing.expect(new_permissions.readonly());
1105+
}
1106+
1107+
test "Metadata.permissions unix" {
1108+
if (builtin.os.tag == .windows)
1109+
return error.ZigSkipTest;
1110+
1111+
var tmp = tmpDir(.{});
1112+
defer tmp.cleanup();
1113+
1114+
const file = try tmp.dir.createFile("test_file", .{ .mode = 0o666 });
1115+
defer file.close();
1116+
1117+
const metadata = try file.metadata();
1118+
var permissions = metadata.permissions();
1119+
1120+
permissions.setReadonly(true);
1121+
try testing.expect(permissions.readonly());
1122+
try testing.expect(!permissions.unixHas(.user, .write));
1123+
permissions.unixSet(.user, .{ .write = true });
1124+
try testing.expect(!permissions.readonly());
1125+
try testing.expect(permissions.unixHas(.user, .write));
1126+
try testing.expect(permissions.mode & 0o400 != 0);
1127+
1128+
permissions.setReadonly(true);
1129+
try file.setPermissions(permissions);
1130+
permissions = (try file.metadata()).permissions();
1131+
try testing.expect(permissions.readonly());
1132+
1133+
permissions = File.Permissions.unixNew(0o754);
1134+
try testing.expect(permissions.unixHas(.user, .execute));
1135+
try testing.expect(!permissions.unixHas(.other, .execute));
1136+
}

0 commit comments

Comments
 (0)