authorgravatar for 37453713+ominitay@users.noreply.github.comominitay <37453713+ominitay@users.noreply.github.com> 2022-01-08 21:22:23+00:00
committergravatar for 37453713+ominitay@users.noreply.github.comominitay <37453713+ominitay@users.noreply.github.com> 2022-02-13 20:56:06+00:00
log11b4cc589c85b6330824ac9a64bfb0829cc1adb0
treee7d013c8c80ce78c1b12285ddee4fd5f2cc41822
parentb2610649fcf43c678fd81f3bef71f3ba42ee3606
signaturelock-open Commit is signed but in an unrecognized format.

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

3 files changed, 580 insertions(+), 0 deletions(-)

lib/std/fs.zig+25
...@@ -2231,6 +2231,31 @@ pub const Dir = struct {...@@ -2231,6 +2231,31 @@ pub const Dir = struct {
2231 }2231 }
22322232
2233 pub const ChownError = File.ChownError;2233 pub const ChownError = File.ChownError;
2234
2235 const Permissions = File.Permissions;
2236 pub const SetPermissionsError = File.SetPermissionsError;
2237
2238 /// Sets permissions according to the provided `Permissions` struct.
2239 /// This method is *NOT* available on WASI
2240 pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!void {
2241 const file: File = .{
2242 .handle = self.fd,
2243 .capable_io_mode = .blocking,
2244 };
2245 try file.setPermissions(permissions);
2246 }
2247
2248 const Metadata = File.Metadata;
2249 pub const MetadataError = File.MetadataError;
2250
2251 /// Returns a `Metadata` struct, representing the permissions on the directory
2252 pub fn metadata(self: Dir) MetadataError!Metadata {
2253 const file: File = .{
2254 .handle = self.fd,
2255 .capable_io_mode = .blocking,
2256 };
2257 return try file.metadata();
2258 }
2234};2259};
22352260
2236/// Returns a handle to the current working directory. It is not opened with iteration capability.2261/// Returns a handle to the current working directory. It is not opened with iteration capability.
lib/std/fs/file.zig+479
...@@ -398,6 +398,485 @@ pub const File = struct {...@@ -398,6 +398,485 @@ pub const File = struct {
398 try os.fchown(self.handle, owner, group);398 try os.fchown(self.handle, owner, group);
399 }399 }
400400
401 /// Cross-platform representation of permissions on a file.
402 /// The `readonly` and `setReadonly` are the only methods available across all platforms.
403 /// Platform-specific functionality is available through the `inner` field.
404 pub const Permissions = struct {
405 /// You may use the `inner` field to use platform-specific functionality
406 inner: switch (builtin.os.tag) {
407 .windows => PermissionsWindows,
408 else => PermissionsUnix,
409 },
410
411 const Self = @This();
412
413 /// Returns `true` if permissions represent an unwritable file.
414 /// On Unix, `true` is returned only if no class has write permissions.
415 pub fn readOnly(self: Self) bool {
416 return self.inner.readOnly();
417 }
418
419 /// Sets whether write permissions are provided.
420 /// On Unix, this affects *all* classes. If this is undesired, use `unixSet`
421 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
422 pub fn setReadOnly(self: *Self, read_only: bool) void {
423 self.inner.setReadOnly(read_only);
424 }
425 };
426
427 pub const PermissionsWindows = struct {
428 attributes: os.windows.DWORD,
429
430 const Self = @This();
431
432 /// Returns `true` if permissions represent an unwritable file.
433 pub fn readOnly(self: Self) bool {
434 return self.attributes & os.windows.FILE_ATTRIBUTE_READONLY != 0;
435 }
436
437 /// Sets whether write permissions are provided.
438 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
439 pub fn setReadOnly(self: *Self, read_only: bool) void {
440 if (read_only) {
441 self.attributes |= os.windows.FILE_ATTRIBUTE_READONLY;
442 } else {
443 self.attributes &= ~@as(os.windows.DWORD, os.windows.FILE_ATTRIBUTE_READONLY);
444 }
445 }
446 };
447
448 pub const PermissionsUnix = struct {
449 mode: Mode,
450
451 const Self = @This();
452
453 /// Returns `true` if permissions represent an unwritable file.
454 /// `true` is returned only if no class has write permissions.
455 pub fn readOnly(self: Self) bool {
456 return self.mode & 0o222 == 0;
457 }
458
459 /// Sets whether write permissions are provided.
460 /// This affects *all* classes. If this is undesired, use `unixSet`
461 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
462 pub fn setReadOnly(self: *Self, read_only: bool) void {
463 if (read_only) {
464 self.mode &= ~@as(Mode, 0o222);
465 } else {
466 self.mode |= @as(Mode, 0o222);
467 }
468 }
469
470 pub const Class = enum(u2) {
471 user = 2,
472 group = 1,
473 other = 0,
474 };
475
476 pub const Permission = enum(u3) {
477 read = 0o4,
478 write = 0o2,
479 execute = 0o1,
480 };
481
482 /// Returns `true` if the chosen class has the selected permission.
483 /// This method is only available on Unix platforms.
484 pub fn unixHas(self: Self, class: Class, permission: Permission) bool {
485 const mask = @as(Mode, @enumToInt(permission)) << @as(u3, @enumToInt(class)) * 3;
486 return self.mode & mask != 0;
487 }
488
489 /// Sets the permissions for the chosen class. Any permissions set to `null` are left unchanged.
490 /// This method *DOES NOT* set permissions on the filesystem: use `File.setPermissions(permissions)`
491 pub fn unixSet(self: *Self, class: Class, permissions: struct {
492 read: ?bool = null,
493 write: ?bool = null,
494 execute: ?bool = null,
495 }) void {
496 const shift = @as(u3, @enumToInt(class)) * 3;
497 if (permissions.read) |r| {
498 if (r) {
499 self.mode |= @as(Mode, 0o4) << shift;
500 } else {
501 self.mode &= ~(@as(Mode, 0o4) << shift);
502 }
503 }
504 if (permissions.write) |w| {
505 if (w) {
506 self.mode |= @as(Mode, 0o2) << shift;
507 } else {
508 self.mode &= ~(@as(Mode, 0o2) << shift);
509 }
510 }
511 if (permissions.execute) |x| {
512 if (x) {
513 self.mode |= @as(Mode, 0o1) << shift;
514 } else {
515 self.mode &= ~(@as(Mode, 0o1) << shift);
516 }
517 }
518 }
519
520 /// Returns a `Permissions` struct representing the permissions from the passed mode.
521 pub fn unixNew(new_mode: Mode) Self {
522 return Self{
523 .mode = new_mode,
524 };
525 }
526 };
527
528 pub const SetPermissionsError = ChmodError;
529
530 /// Sets permissions according to the provided `Permissions` struct.
531 /// This method is *NOT* available on WASI
532 pub fn setPermissions(self: File, permissions: Permissions) SetPermissionsError!void {
533 switch (builtin.os.tag) {
534 .windows => {
535 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
536 var info = windows.FILE_BASIC_INFORMATION{
537 .CreationTime = 0,
538 .LastAccessTime = 0,
539 .LastWriteTime = 0,
540 .ChangeTime = 0,
541 .FileAttributes = permissions.inner.attributes,
542 };
543 const rc = windows.ntdll.NtSetInformationFile(
544 self.handle,
545 &io_status_block,
546 &info,
547 @sizeOf(windows.FILE_BASIC_INFORMATION),
548 .FileBasicInformation,
549 );
550 switch (rc) {
551 .SUCCESS => return,
552 .INVALID_HANDLE => unreachable,
553 .ACCESS_DENIED => return error.AccessDenied,
554 else => return windows.unexpectedStatus(rc),
555 }
556 },
557 .wasi => @compileError("Unsupported OS"), // Wasi filesystem does not *yet* support chmod
558 else => {
559 try self.chmod(permissions.inner.mode);
560 },
561 }
562 }
563
564 /// Cross-platform representation of file metadata.
565 /// Platform-specific functionality is available through the `inner` field.
566 pub const Metadata = struct {
567 /// You may use the `inner` field to use platform-specific functionality
568 inner: switch (builtin.os.tag) {
569 .windows => MetadataWindows,
570 .linux => MetadataLinux,
571 else => MetadataUnix,
572 },
573
574 const Self = @This();
575
576 /// Returns the size of the file
577 pub fn size(self: Self) u64 {
578 return self.inner.size();
579 }
580
581 /// Returns a `Permissions` struct, representing the permissions on the file
582 pub fn permissions(self: Self) Permissions {
583 return self.inner.permissions();
584 }
585
586 /// Returns the `Kind` of file.
587 /// On Windows, can only return: `.File`, `.Directory`, `.SymLink` or `.Unknown`
588 pub fn kind(self: Self) Kind {
589 return self.inner.kind();
590 }
591
592 /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
593 pub fn accessed(self: Self) i128 {
594 return self.inner.accessed();
595 }
596
597 /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
598 pub fn modified(self: Self) i128 {
599 return self.inner.modified();
600 }
601
602 /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
603 /// On Windows, this cannot return null
604 /// On Linux, this returns null if the filesystem does not support creation times, or if the kernel is older than 4.11
605 /// On Unices, this returns null if the filesystem or OS does not support creation times
606 /// 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
607 pub fn created(self: Self) ?i128 {
608 return self.inner.created();
609 }
610 };
611
612 pub const MetadataUnix = struct {
613 stat: os.Stat,
614
615 const Self = @This();
616
617 /// Returns the size of the file
618 pub fn size(self: Self) u64 {
619 return @intCast(u64, self.stat.size);
620 }
621
622 /// Returns a `Permissions` struct, representing the permissions on the file
623 pub fn permissions(self: Self) Permissions {
624 return Permissions{ .inner = PermissionsUnix{ .mode = self.stat.mode } };
625 }
626
627 /// Returns the `Kind` of the file
628 pub fn kind(self: Self) Kind {
629 if (builtin.os.tag == .wasi and !builtin.link_libc) return switch (self.stat.filetype) {
630 .BLOCK_DEVICE => Kind.BlockDevice,
631 .CHARACTER_DEVICE => Kind.CharacterDevice,
632 .DIRECTORY => Kind.Directory,
633 .SYMBOLIC_LINK => Kind.SymLink,
634 .REGULAR_FILE => Kind.File,
635 .SOCKET_STREAM, .SOCKET_DGRAM => Kind.UnixDomainSocket,
636 else => Kind.Unknown,
637 };
638
639 const m = self.stat.mode & os.S.IFMT;
640
641 switch (m) {
642 os.S.IFBLK => return Kind.BlockDevice,
643 os.S.IFCHR => return Kind.CharacterDevice,
644 os.S.IFDIR => return Kind.Directory,
645 os.S.IFIFO => return Kind.NamedPipe,
646 os.S.IFLNK => return Kind.SymLink,
647 os.S.IFREG => return Kind.File,
648 os.S.IFSOCK => return Kind.UnixDomainSocket,
649 else => {},
650 }
651
652 if (builtin.os.tag == .solaris) switch (m) {
653 os.S.IFDOOR => return Kind.Door,
654 os.S.IFPORT => return Kind.EventPort,
655 else => {},
656 };
657
658 return .Unknown;
659 }
660
661 /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
662 pub fn accessed(self: Self) i128 {
663 const atime = self.stat.atime();
664 return @as(i128, atime.tv_sec) * std.time.ns_per_s + atime.tv_nsec;
665 }
666
667 /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
668 pub fn modified(self: Self) i128 {
669 const mtime = self.stat.mtime();
670 return @as(i128, mtime.tv_sec) * std.time.ns_per_s + mtime.tv_nsec;
671 }
672
673 /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
674 /// Returns null if this is not supported by the OS or filesystem
675 pub fn created(self: Self) ?i128 {
676 if (!@hasDecl(@TypeOf(self.stat), "birthtime")) return null;
677 const birthtime = self.stat.birthtime();
678
679 // If the filesystem doesn't support this the value *should* be:
680 // On FreeBSD: tv_nsec = 0, tv_sec = -1
681 // On NetBSD and OpenBSD: tv_nsec = 0, tv_sec = 0
682 // On MacOS, it is set to ctime -- we cannot detect this!!
683 switch (builtin.os.tag) {
684 .freebsd => if (birthtime.tv_sec == -1 and birthtime.tv_nsec == 0) return null,
685 .netbsd, .openbsd => if (birthtime.tv_sec == 0 and birthtime.tv_nsec == 0) return null,
686 .macos => {},
687 else => @compileError("Creation time detection not implemented for OS"),
688 }
689
690 return @as(i128, birthtime.tv_sec) * std.time.ns_per_s + birthtime.tv_nsec;
691 }
692 };
693
694 /// `MetadataUnix`, but using Linux's `statx` syscall.
695 /// On Linux versions below 4.11, `statx` will be filled with data from stat.
696 pub const MetadataLinux = struct {
697 statx: os.linux.Statx,
698
699 const Self = @This();
700
701 /// Returns the size of the file
702 pub fn size(self: Self) u64 {
703 return self.statx.size;
704 }
705
706 /// Returns a `Permissions` struct, representing the permissions on the file
707 pub fn permissions(self: Self) Permissions {
708 return Permissions{ .inner = PermissionsUnix{ .mode = self.statx.mode } };
709 }
710
711 /// Returns the `Kind` of the file
712 pub fn kind(self: Self) Kind {
713 const m = self.statx.mode & os.S.IFMT;
714
715 switch (m) {
716 os.S.IFBLK => return Kind.BlockDevice,
717 os.S.IFCHR => return Kind.CharacterDevice,
718 os.S.IFDIR => return Kind.Directory,
719 os.S.IFIFO => return Kind.NamedPipe,
720 os.S.IFLNK => return Kind.SymLink,
721 os.S.IFREG => return Kind.File,
722 os.S.IFSOCK => return Kind.UnixDomainSocket,
723 else => {},
724 }
725
726 return .Unknown;
727 }
728
729 /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
730 pub fn accessed(self: Self) i128 {
731 return @as(i128, self.statx.atime.tv_sec) * std.time.ns_per_s + self.statx.atime.tv_nsec;
732 }
733
734 /// Returns the last time the file was modified in nanoseconds since UTC 1970-01-01
735 pub fn modified(self: Self) i128 {
736 return @as(i128, self.statx.mtime.tv_sec) * std.time.ns_per_s + self.statx.mtime.tv_nsec;
737 }
738
739 /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
740 /// Returns null if this is not supported by the filesystem, or on kernels before than version 4.11
741 pub fn created(self: Self) ?i128 {
742 if (self.statx.mask & os.linux.STATX_BTIME == 0) return null;
743 return @as(i128, self.statx.btime.tv_sec) * std.time.ns_per_s + self.statx.btime.tv_nsec;
744 }
745 };
746
747 pub const MetadataWindows = struct {
748 attributes: windows.DWORD,
749 reparse_tag: windows.DWORD,
750 _size: u64,
751 access_time: i128,
752 modified_time: i128,
753 creation_time: i128,
754
755 const Self = @This();
756
757 /// Returns the size of the file
758 pub fn size(self: Self) u64 {
759 return self._size;
760 }
761
762 /// Returns a `Permissions` struct, representing the permissions on the file
763 pub fn permissions(self: Self) Permissions {
764 return Permissions{ .inner = PermissionsWindows{ .attributes = self.attributes } };
765 }
766
767 /// Returns the `Kind` of the file.
768 /// Can only return: `.File`, `.Directory`, `.SymLink` or `.Unknown`
769 pub fn kind(self: Self) Kind {
770 if (self.attributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
771 if (self.reparse_tag & 0x20000000 != 0) {
772 return .SymLink;
773 }
774 } else if (self.attributes & windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
775 return .Directory;
776 } else {
777 return .File;
778 }
779 return .Unknown;
780 }
781
782 /// Returns the last time the file was accessed in nanoseconds since UTC 1970-01-01
783 pub fn accessed(self: Self) i128 {
784 return self.access_time;
785 }
786
787 /// Returns the time the file was modified in nanoseconds since UTC 1970-01-01
788 pub fn modified(self: Self) i128 {
789 return self.modified_time;
790 }
791
792 /// Returns the time the file was created in nanoseconds since UTC 1970-01-01
793 /// This never returns null, only returning an optional for compatibility with other OSes
794 pub fn created(self: Self) ?i128 {
795 return self.creation_time;
796 }
797 };
798
799 pub const MetadataError = os.FStatError;
800
801 pub fn metadata(self: File) MetadataError!Metadata {
802 return Metadata{
803 .inner = switch (builtin.os.tag) {
804 .windows => blk: {
805 var io_status_block: windows.IO_STATUS_BLOCK = undefined;
806 var info: windows.FILE_ALL_INFORMATION = undefined;
807
808 const rc = windows.ntdll.NtQueryInformationFile(self.handle, &io_status_block, &info, @sizeOf(windows.FILE_ALL_INFORMATION), .FileAllInformation);
809 switch (rc) {
810 .SUCCESS => {},
811 .BUFFER_OVERFLOW => {},
812 .INVALID_PARAMETER => unreachable,
813 .ACCESS_DENIED => return error.AccessDenied,
814 else => return windows.unexpectedStatus(rc),
815 }
816
817 const reparse_tag: windows.DWORD = reparse_blk: {
818 if (info.BasicInformation.FileAttributes & windows.FILE_ATTRIBUTE_REPARSE_POINT != 0) {
819 var reparse_buf: [windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE]u8 = undefined;
820 try windows.DeviceIoControl(self.handle, windows.FSCTL_GET_REPARSE_POINT, null, reparse_buf[0..]);
821 const reparse_struct = @ptrCast(*const windows.REPARSE_DATA_BUFFER, @alignCast(@alignOf(windows.REPARSE_DATA_BUFFER), &reparse_buf[0]));
822 break :reparse_blk reparse_struct.ReparseTag;
823 }
824 break :reparse_blk 0;
825 };
826
827 break :blk MetadataWindows{
828 .attributes = info.BasicInformation.FileAttributes,
829 .reparse_tag = reparse_tag,
830 ._size = @bitCast(u64, info.StandardInformation.EndOfFile),
831 .access_time = windows.fromSysTime(info.BasicInformation.LastAccessTime),
832 .modified_time = windows.fromSysTime(info.BasicInformation.LastWriteTime),
833 .creation_time = windows.fromSysTime(info.BasicInformation.CreationTime),
834 };
835 },
836 .linux => blk: {
837 var stx = mem.zeroes(os.linux.Statx);
838 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);
839
840 switch (os.errno(rcx)) {
841 .SUCCESS => {},
842 // NOSYS happens when `statx` is unsupported, which is the case on kernel versions before 4.11
843 // Here, we call `fstat` and fill `stx` with the data we need
844 .NOSYS => {
845 const st = try os.fstat(self.handle);
846
847 stx.mode = @intCast(u16, st.mode);
848
849 // Hacky conversion from timespec to statx_timestamp
850 stx.atime = std.mem.zeroes(os.linux.statx_timestamp);
851 stx.atime.tv_sec = st.atim.tv_sec;
852 stx.atime.tv_nsec = @intCast(u32, st.atim.tv_nsec); // Guaranteed to succeed (tv_nsec is always below 10^9)
853
854 stx.mtime = std.mem.zeroes(os.linux.statx_timestamp);
855 stx.mtime.tv_sec = st.mtim.tv_sec;
856 stx.mtime.tv_nsec = @intCast(u32, st.mtim.tv_nsec);
857
858 stx.mask = os.linux.STATX_BASIC_STATS | os.linux.STATX_MTIME;
859 },
860 .BADF => unreachable,
861 .FAULT => unreachable,
862 .NOMEM => return error.SystemResources,
863 else => |err| return os.unexpectedErrno(err),
864 }
865
866 break :blk MetadataLinux{
867 .statx = stx,
868 };
869 },
870 else => blk: {
871 const st = try os.fstat(self.handle);
872 break :blk MetadataUnix{
873 .stat = st,
874 };
875 },
876 },
877 };
878 }
879
401 pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;880 pub const UpdateTimesError = os.FutimensError || windows.SetFileTimeError;
402881
403 /// The underlying file system may have a different granularity than nanoseconds,882 /// The underlying file system may have a different granularity than nanoseconds,
lib/std/fs/test.zig+76
...@@ -1101,3 +1101,79 @@ test "chown" {...@@ -1101,3 +1101,79 @@ test "chown" {
1101 defer dir.close();1101 defer dir.close();
1102 try dir.chown(null, null);1102 try dir.chown(null, null);
1103}1103}
1104
1105test "File.Metadata" {
1106 var tmp = tmpDir(.{});
1107 defer tmp.cleanup();
1108
1109 const file = try tmp.dir.createFile("test_file", .{ .read = true });
1110 defer file.close();
1111
1112 const metadata = try file.metadata();
1113 try testing.expect(metadata.kind() == .File);
1114 try testing.expect(metadata.size() == 0);
1115 _ = metadata.accessed();
1116 _ = metadata.modified();
1117 _ = metadata.created();
1118}
1119
1120test "File.Permissions" {
1121 if (builtin.os.tag == .wasi)
1122 return error.SkipZigTest;
1123
1124 var tmp = tmpDir(.{});
1125 defer tmp.cleanup();
1126
1127 const file = try tmp.dir.createFile("test_file", .{ .read = true });
1128 defer file.close();
1129
1130 const metadata = try file.metadata();
1131 var permissions = metadata.permissions();
1132
1133 try testing.expect(!permissions.readOnly());
1134 permissions.setReadOnly(true);
1135 try testing.expect(permissions.readOnly());
1136
1137 try file.setPermissions(permissions);
1138 const new_permissions = (try file.metadata()).permissions();
1139 try testing.expect(new_permissions.readOnly());
1140
1141 // Must be set to non-read-only to delete
1142 permissions.setReadOnly(false);
1143 try file.setPermissions(permissions);
1144}
1145
1146test "File.PermissionsUnix" {
1147 if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
1148 return error.SkipZigTest;
1149
1150 var tmp = tmpDir(.{});
1151 defer tmp.cleanup();
1152
1153 const file = try tmp.dir.createFile("test_file", .{ .mode = 0o666, .read = true });
1154 defer file.close();
1155
1156 const metadata = try file.metadata();
1157 var permissions = metadata.permissions();
1158
1159 permissions.setReadOnly(true);
1160 try testing.expect(permissions.readOnly());
1161 try testing.expect(!permissions.inner.unixHas(.user, .write));
1162 permissions.inner.unixSet(.user, .{ .write = true });
1163 try testing.expect(!permissions.readOnly());
1164 try testing.expect(permissions.inner.unixHas(.user, .write));
1165 try testing.expect(permissions.inner.mode & 0o400 != 0);
1166
1167 permissions.setReadOnly(true);
1168 try file.setPermissions(permissions);
1169 permissions = (try file.metadata()).permissions();
1170 try testing.expect(permissions.readOnly());
1171
1172 // Must be set to non-read-only to delete
1173 permissions.setReadOnly(false);
1174 try file.setPermissions(permissions);
1175
1176 const permissions_unix = File.PermissionsUnix.unixNew(0o754);
1177 try testing.expect(permissions_unix.unixHas(.user, .execute));
1178 try testing.expect(!permissions_unix.unixHas(.other, .execute));
1179}