diff --git a/lib/std/Build/Cache.zig b/lib/std/Build/Cache.zig index 8f88d840b8..b966c25efc 100644 --- a/lib/std/Build/Cache.zig +++ b/lib/std/Build/Cache.zig @@ -1305,7 +1305,7 @@ fn hashFile(file: fs.File, bin_digest: *[Hasher.mac_length]u8) fs.File.PReadErro } // Create/Write a file, close it, then grab its stat.mtime timestamp. -fn testGetCurrentFileTimestamp(dir: fs.Dir) !i128 { +fn testGetCurrentFileTimestamp(dir: fs.Dir) !Io.Timestamp { const test_out_file = "test-filetimestamp.tmp"; var file = try dir.createFile(test_out_file, .{ @@ -1333,8 +1333,8 @@ test "cache file and then recall it" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1399,8 +1399,8 @@ test "check that changing a file makes cache fail" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1517,8 +1517,8 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } var digest1: HexDigest = undefined; @@ -1568,8 +1568,8 @@ test "Manifest with files added after initial hash work" { // Wait for file timestamps to tick const initial_time2 = try testGetCurrentFileTimestamp(tmp.dir); - while ((try testGetCurrentFileTimestamp(tmp.dir)) == initial_time2) { - try std.Io.Duration.sleep(.fromNanoseconds(1), io); + while ((try testGetCurrentFileTimestamp(tmp.dir)).nanoseconds == initial_time2.nanoseconds) { + try std.Io.Clock.Duration.sleep(.{ .clock = .boot, .raw = .fromNanoseconds(1) }, io); } { diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 8d061ad022..320ff3ba2d 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -654,20 +654,20 @@ pub const VTable = struct { conditionWait: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) Cancelable!void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, - dirMake: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, - dirStat: *const fn (?*anyopaque, dir: Dir) Dir.StatError!Dir.Stat, - dirStatPath: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8) Dir.StatError!File.Stat, - fileStat: *const fn (?*anyopaque, file: File) File.StatError!File.Stat, - createFile: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.CreateFlags) File.OpenError!File, - fileOpen: *const fn (?*anyopaque, dir: Dir, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File, + dirMake: *const fn (?*anyopaque, Dir, sub_path: []const u8, mode: Dir.Mode) Dir.MakeError!void, + dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, + dirStatPath: *const fn (?*anyopaque, Dir, sub_path: []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, + fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, + createFile: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.CreateFlags) File.OpenError!File, + fileOpen: *const fn (?*anyopaque, Dir, sub_path: []const u8, File.OpenFlags) File.OpenError!File, fileClose: *const fn (?*anyopaque, File) void, - pwrite: *const fn (?*anyopaque, file: File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, + pwrite: *const fn (?*anyopaque, File, buffer: []const u8, offset: std.posix.off_t) File.PWriteError!usize, /// Returns 0 on end of stream. - fileReadStreaming: *const fn (?*anyopaque, file: File, data: [][]u8) File.ReadStreamingError!usize, + fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.ReadStreamingError!usize, /// Returns 0 on end of stream. - fileReadPositional: *const fn (?*anyopaque, file: File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, - fileSeekBy: *const fn (?*anyopaque, file: File, offset: i64) File.SeekError!void, - fileSeekTo: *const fn (?*anyopaque, file: File, offset: u64) File.SeekError!void, + fileReadPositional: *const fn (?*anyopaque, File, data: [][]u8, offset: u64) File.ReadPositionalError!usize, + fileSeekBy: *const fn (?*anyopaque, File, offset: i64) File.SeekError!void, + fileSeekTo: *const fn (?*anyopaque, File, offset: u64) File.SeekError!void, now: *const fn (?*anyopaque, Clock) Clock.Error!Timestamp, sleep: *const fn (?*anyopaque, Timeout) SleepError!void, @@ -795,6 +795,14 @@ pub const Clock = enum { }; } + pub fn subDuration(from: Clock.Timestamp, duration: Clock.Duration) Clock.Timestamp { + assert(from.clock == duration.clock); + return .{ + .raw = from.raw.subDuration(duration.raw), + .clock = from.clock, + }; + } + pub fn fromNow(io: Io, duration: Clock.Duration) Error!Clock.Timestamp { return .{ .clock = duration.clock, @@ -855,6 +863,10 @@ pub const Timestamp = struct { return .{ .nanoseconds = from.nanoseconds + duration.nanoseconds }; } + pub fn subDuration(from: Timestamp, duration: Duration) Timestamp { + return .{ .nanoseconds = from.nanoseconds - duration.nanoseconds }; + } + pub fn withClock(t: Timestamp, clock: Clock) Clock.Timestamp { return .{ .nanoseconds = t.nanoseconds, .clock = clock }; } diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index 2647efd2c3..5460f6bd60 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -15,6 +15,29 @@ pub fn cwd() Dir { pub const Handle = std.posix.fd_t; +pub const PathNameError = error{ + NameTooLong, + /// File system cannot encode the requested file name bytes. + /// Could be due to invalid WTF-8 on Windows, invalid UTF-8 on WASI, + /// invalid characters on Windows, etc. Filesystem and operating specific. + BadPathName, +}; + +pub const OpenError = error{ + FileNotFound, + NotDir, + AccessDenied, + PermissionDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + NoDevice, + SystemResources, + DeviceBusy, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, +} || PathNameError || Io.Cancelable || Io.UnexpectedError; + pub fn openFile(dir: Dir, io: Io, sub_path: []const u8, flags: File.OpenFlags) File.OpenError!File { return io.vtable.fileOpen(io.userdata, dir, sub_path, flags); } @@ -149,22 +172,15 @@ pub const MakeError = error{ PathAlreadyExists, SymLinkLoop, LinkQuotaExceeded, - NameTooLong, FileNotFound, SystemResources, NoSpaceLeft, NotDir, ReadOnlyFileSystem, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://simonsapin.github.io/wtf-8/ - InvalidWtf8, - BadPathName, NoDevice, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, - /// File system cannot encode the requested file name bytes. - InvalidFileName, -} || Io.Cancelable || Io.UnexpectedError; +} || PathNameError || Io.Cancelable || Io.UnexpectedError; /// Creates a single directory with a relative or absolute path. /// @@ -225,7 +241,7 @@ pub fn makePathStatus(dir: Dir, io: Io, sub_path: []const u8) MakePathError!Make // could cause an infinite loop check_dir: { // workaround for windows, see https://github.com/ziglang/zig/issues/16738 - const fstat = statPath(dir, io, component.path) catch |stat_err| switch (stat_err) { + const fstat = statPath(dir, io, component.path, .{}) catch |stat_err| switch (stat_err) { error.IsDir => break :check_dir, else => |e| return e, }; @@ -251,6 +267,10 @@ pub fn stat(dir: Dir, io: Io) StatError!Stat { pub const StatPathError = File.OpenError || File.StatError; +pub const StatPathOptions = struct { + follow_symlinks: bool = true, +}; + /// Returns metadata for a file inside the directory. /// /// On Windows, this requires three syscalls. On other operating systems, it @@ -263,6 +283,6 @@ pub const StatPathError = File.OpenError || File.StatError; /// * On Windows, `sub_path` should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/). /// * On WASI, `sub_path` should be encoded as valid UTF-8. /// * On other platforms, `sub_path` is an opaque sequence of bytes with no particular encoding. -pub fn statPath(dir: Dir, io: Io, sub_path: []const u8) StatPathError!Stat { - return io.vtable.dirStatPath(io.userdata, dir, sub_path); +pub fn statPath(dir: Dir, io: Io, sub_path: []const u8, options: StatPathOptions) StatPathError!Stat { + return io.vtable.dirStatPath(io.userdata, dir, sub_path, options); } diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index c4e4e5c9c4..d5d08587c7 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -81,7 +81,63 @@ pub fn stat(file: File, io: Io) StatError!Stat { pub const OpenFlags = std.fs.File.OpenFlags; pub const CreateFlags = std.fs.File.CreateFlags; -pub const OpenError = std.fs.File.OpenError || Io.Cancelable; +pub const OpenError = error{ + SharingViolation, + PipeBusy, + NoDevice, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + ProcessNotFound, + /// On Windows, antivirus software is enabled by default. It can be + /// disabled, but Windows Update sometimes ignores the user's preference + /// and re-enables it. When enabled, antivirus software on Windows + /// intercepts file system operations and makes them significantly slower + /// in addition to possibly failing with this error code. + AntivirusInterference, + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to open a new resource relative to it. + AccessDenied, + PermissionDenied, + SymLinkLoop, + ProcessFdQuotaExceeded, + SystemFdQuotaExceeded, + /// Either: + /// * One of the path components does not exist. + /// * Cwd was used, but cwd has been deleted. + /// * The path associated with the open directory handle has been deleted. + /// * On macOS, multiple processes or threads raced to create the same file + /// with `O.EXCL` set to `false`. + FileNotFound, + /// The path exceeded `max_path_bytes` bytes. + /// Insufficient kernel memory was available, or + /// the named file is a FIFO and per-user hard limit on + /// memory allocation for pipes has been reached. + SystemResources, + /// The file is too large to be opened. This error is unreachable + /// for 64-bit targets, as well as when opening directories. + FileTooBig, + /// The path refers to directory but the `DIRECTORY` flag was not provided. + IsDir, + /// A new path cannot be created because the device has no room for the new file. + /// This error is only reachable when the `CREAT` flag is provided. + NoSpaceLeft, + /// A component used as a directory in the path was not, in fact, a directory, or + /// `DIRECTORY` was specified and the path was not a directory. + NotDir, + /// The path already exists and the `CREAT` and `EXCL` flags were provided. + PathAlreadyExists, + DeviceBusy, + FileLocksNotSupported, + /// One of these three things: + /// * pathname refers to an executable image which is currently being + /// executed and write access was requested. + /// * pathname refers to a file that is currently in use as a swap + /// file, and the O_TRUNC flag was specified. + /// * pathname refers to a file that is currently being read by the + /// kernel (e.g., for module/firmware loading), and write access was + /// requested. + FileBusy, +} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub fn close(file: File, io: Io) void { return io.vtable.fileClose(io.userdata, file); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index cab383b7dd..02e68f85bb 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -166,14 +166,23 @@ pub fn io(pool: *Pool) Io { else => dirMakePosix, }, .dirStat = dirStat, - .dirStatPath = dirStatPath, + .dirStatPath = switch (builtin.os.tag) { + .linux => dirStatPathLinux, + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => dirStatPathPosix, + }, .fileStat = switch (builtin.os.tag) { .linux => fileStatLinux, .windows => fileStatWindows, .wasi => fileStatWasi, else => fileStatPosix, }, - .createFile = createFile, + .createFile = switch (builtin.os.tag) { + .windows => @panic("TODO"), + .wasi => @panic("TODO"), + else => createFilePosix, + }, .fileOpen = fileOpen, .fileClose = fileClose, .pwrite = pwrite, @@ -765,8 +774,10 @@ fn conditionWake(userdata: ?*anyopaque, cond: *Io.Condition, wake: Io.Condition. fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: Io.Dir.Mode) Io.Dir.MakeError!void { const pool: *Pool = @ptrCast(@alignCast(userdata)); + var path_buffer: [posix.PATH_MAX]u8 = undefined; - const sub_path_posix = try toPosixPath(sub_path, &path_buffer); + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + while (true) { try pool.checkCancel(); switch (posix.errno(posix.system.mkdirat(dir.handle, sub_path_posix, mode))) { @@ -788,7 +799,7 @@ fn dirMakePosix(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, mode: .ROFS => return error.ReadOnlyFileSystem, // dragonfly: when dir_fd is unlinked from filesystem .NOTCONN => return error.FileNotFound, - .ILSEQ => return error.InvalidFileName, + .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), } } @@ -802,13 +813,82 @@ fn dirStat(userdata: ?*anyopaque, dir: Io.Dir) Io.Dir.StatError!Io.Dir.Stat { @panic("TODO"); } -fn dirStatPath(userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8) Io.Dir.StatError!Io.File.Stat { +fn dirStatPathLinux( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); + const linux = std.os.linux; - _ = dir; - _ = sub_path; - @panic("TODO"); + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = linux.AT.NO_AUTOMOUNT | + @as(u32, if (!options.follow_symlinks) linux.AT.SYMLINK_NOFOLLOW else 0); + + while (true) { + try pool.checkCancel(); + var statx = std.mem.zeroes(linux.Statx); + const rc = linux.statx( + dir.handle, + sub_path_posix, + flags, + linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, + &statx, + ); + switch (linux.E.init(rc)) { + .SUCCESS => return statFromLinux(&statx), + .INTR => continue, + .ACCES => return error.AccessDenied, + .BADF => |err| return errnoBug(err), + .FAULT => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), + .LOOP => return error.SymLinkLoop, + .NAMETOOLONG => |err| return errnoBug(err), // Handled by pathToPosix() above. + .NOENT => return error.FileNotFound, + .NOTDIR => return error.NotDir, + .NOMEM => return error.SystemResources, + else => |err| return posix.unexpectedErrno(err), + } + } +} + +fn dirStatPathPosix( + userdata: ?*anyopaque, + dir: Io.Dir, + sub_path: []const u8, + options: Io.Dir.StatPathOptions, +) Io.Dir.StatPathError!Io.File.Stat { + const pool: *Pool = @ptrCast(@alignCast(userdata)); + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; + const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.system.fstatat; + + while (true) { + try pool.checkCancel(); + var stat = std.mem.zeroes(posix.Stat); + switch (posix.errno(fstatat_sym(dir.handle, sub_path_posix, &stat, flags))) { + .SUCCESS => return statFromPosix(stat), + .INTR => continue, + .INVAL => |err| return errnoBug(err), + .BADF => |err| return errnoBug(err), // Always a race condition. + .NOMEM => return error.SystemResources, + .ACCES => return error.AccessDenied, + .PERM => return error.PermissionDenied, + .FAULT => |err| return errnoBug(err), + .NAMETOOLONG => return error.NameTooLong, + .LOOP => return error.SymLinkLoop, + .NOENT => return error.FileNotFound, + .NOTDIR => return error.FileNotFound, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + } } fn fileStatPosix(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File.Stat { @@ -885,17 +965,127 @@ fn fileStatWasi(userdata: ?*anyopaque, file: Io.File) Io.File.StatError!Io.File. } } -fn createFile( +const have_flock = @TypeOf(posix.system.flock) != void; + +fn createFilePosix( userdata: ?*anyopaque, dir: Io.Dir, sub_path: []const u8, flags: Io.File.CreateFlags, ) Io.File.OpenError!Io.File { const pool: *Pool = @ptrCast(@alignCast(userdata)); - try pool.checkCancel(); - const fs_dir: std.fs.Dir = .{ .fd = dir.handle }; - const fs_file = try fs_dir.createFile(sub_path, flags); - return .{ .handle = fs_file.handle }; + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(sub_path, &path_buffer); + + var os_flags: posix.O = .{ + .ACCMODE = if (flags.read) .RDWR else .WRONLY, + .CREAT = true, + .TRUNC = flags.truncate, + .EXCL = flags.exclusive, + }; + if (@hasField(posix.O, "LARGEFILE")) os_flags.LARGEFILE = true; + if (@hasField(posix.O, "CLOEXEC")) os_flags.CLOEXEC = true; + + // Use the O locking flags if the os supports them to acquire the lock + // atomically. Note that the NONBLOCK flag is removed after the openat() + // call is successful. + const has_flock_open_flags = @hasField(posix.O, "EXLOCK"); + if (has_flock_open_flags) switch (flags.lock) { + .none => {}, + .shared => { + os_flags.SHLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + .exclusive => { + os_flags.EXLOCK = true; + os_flags.NONBLOCK = flags.lock_nonblocking; + }, + }; + + const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; + + const fd: posix.fd_t = while (true) { + try pool.checkCancel(); + const rc = openat_sym(dir.handle, sub_path_posix, os_flags, flags.mode); + switch (posix.errno(rc)) { + .SUCCESS => break @intCast(rc), + .INTR => continue, + + .FAULT => |err| return errnoBug(err), + .INVAL => return error.BadPathName, + .BADF => |err| return errnoBug(err), + .ACCES => return error.AccessDenied, + .FBIG => return error.FileTooBig, + .OVERFLOW => return error.FileTooBig, + .ISDIR => return error.IsDir, + .LOOP => return error.SymLinkLoop, + .MFILE => return error.ProcessFdQuotaExceeded, + .NAMETOOLONG => return error.NameTooLong, + .NFILE => return error.SystemFdQuotaExceeded, + .NODEV => return error.NoDevice, + .NOENT => return error.FileNotFound, + .SRCH => return error.ProcessNotFound, + .NOMEM => return error.SystemResources, + .NOSPC => return error.NoSpaceLeft, + .NOTDIR => return error.NotDir, + .PERM => return error.PermissionDenied, + .EXIST => return error.PathAlreadyExists, + .BUSY => return error.DeviceBusy, + .OPNOTSUPP => return error.FileLocksNotSupported, + //.AGAIN => return error.WouldBlock, + .TXTBSY => return error.FileBusy, + .NXIO => return error.NoDevice, + .ILSEQ => return error.BadPathName, + else => |err| return posix.unexpectedErrno(err), + } + }; + errdefer posix.close(fd); + + if (have_flock and !has_flock_open_flags and flags.lock != .none) { + const lock_nonblocking: i32 = if (flags.lock_nonblocking) posix.LOCK.NB else 0; + const lock_flags = switch (flags.lock) { + .none => unreachable, + .shared => posix.LOCK.SH | lock_nonblocking, + .exclusive => posix.LOCK.EX | lock_nonblocking, + }; + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.flock(fd, lock_flags))) { + .SUCCESS => break, + .INTR => continue, + + .BADF => |err| return errnoBug(err), + .INVAL => |err| return errnoBug(err), // invalid parameters + .NOLCK => return error.SystemResources, + //.AGAIN => return error.WouldBlock, + .OPNOTSUPP => return error.FileLocksNotSupported, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + if (has_flock_open_flags and flags.lock_nonblocking) { + var fl_flags: usize = while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.system.fcntl(fd, posix.F.GETFL, 0))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + }; + fl_flags &= ~@as(usize, 1 << @bitOffsetOf(posix.O, "NONBLOCK")); + while (true) { + try pool.checkCancel(); + switch (posix.errno(posix.fcntl(fd, posix.F.SETFL, fl_flags))) { + .SUCCESS => break, + .INTR => continue, + else => |err| return posix.unexpectedErrno(err), + } + } + } + + return .{ .handle = fd }; } fn fileOpen( @@ -2293,8 +2483,8 @@ fn timestampToPosix(nanoseconds: i96) std.posix.timespec { }; } -fn toPosixPath(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) error{ NameTooLong, InvalidFileName }![:0]u8 { - if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.InvalidFileName; +fn pathToPosix(file_path: []const u8, buffer: *[posix.PATH_MAX]u8) Io.Dir.PathNameError![:0]u8 { + if (std.mem.containsAtLeastScalar2(u8, file_path, 0, 1)) return error.BadPathName; // >= rather than > to make room for the null byte if (file_path.len >= buffer.len) return error.NameTooLong; @memcpy(buffer[0..file_path.len], file_path); diff --git a/lib/std/Io/test.zig b/lib/std/Io/test.zig index 22dc163dc6..22f211d6e4 100644 --- a/lib/std/Io/test.zig +++ b/lib/std/Io/test.zig @@ -11,6 +11,8 @@ const native_endian = @import("builtin").target.cpu.arch.endian(); const tmpDir = std.testing.tmpDir; test "write a file, read it, then delete it" { + const io = std.testing.io; + var tmp = tmpDir(.{}); defer tmp.cleanup(); @@ -45,7 +47,7 @@ test "write a file, read it, then delete it" { try expectEqual(expected_file_size, file_size); var file_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&file_buffer); + var file_reader = file.reader(io, &file_buffer); const contents = try file_reader.interface.allocRemaining(std.testing.allocator, .limited(2 * 1024)); defer std.testing.allocator.free(contents); @@ -114,10 +116,10 @@ test "updateTimes" { const stat_old = try file.stat(); // Set atime and mtime to 5s before try file.updateTimes( - stat_old.atime - 5 * std.time.ns_per_s, - stat_old.mtime - 5 * std.time.ns_per_s, + stat_old.atime.subDuration(.fromSeconds(5)), + stat_old.mtime.subDuration(.fromSeconds(5)), ); const stat_new = try file.stat(); - try expect(stat_new.atime < stat_old.atime); - try expect(stat_new.mtime < stat_old.mtime); + try expect(stat_new.atime.nanoseconds < stat_old.atime.nanoseconds); + try expect(stat_new.mtime.nanoseconds < stat_old.mtime.nanoseconds); } diff --git a/lib/std/Thread.zig b/lib/std/Thread.zig index 618f1209ef..9a602fbd6b 100644 --- a/lib/std/Thread.zig +++ b/lib/std/Thread.zig @@ -318,10 +318,13 @@ pub fn getName(self: Thread, buffer_ptr: *[max_name_len:0]u8) GetNameError!?[]co var buf: [32]u8 = undefined; const path = try std.fmt.bufPrint(&buf, "/proc/self/task/{d}/comm", .{self.getHandle()}); + var threaded: std.Io.Threaded = .init_single_threaded; + const io = threaded.io(); + const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); - var file_reader = file.readerStreaming(&.{}); + var file_reader = file.readerStreaming(io, &.{}); const data_len = file_reader.interface.readSliceShort(buffer_ptr[0 .. max_name_len + 1]) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, }; diff --git a/lib/std/Thread/Condition.zig b/lib/std/Thread/Condition.zig index 91974a44b4..b298e598df 100644 --- a/lib/std/Thread/Condition.zig +++ b/lib/std/Thread/Condition.zig @@ -123,14 +123,9 @@ const SingleThreadedImpl = struct { fn wait(self: *Impl, mutex: *Mutex, timeout: ?u64) error{Timeout}!void { _ = self; _ = mutex; - // There are no other threads to wake us up. // So if we wait without a timeout we would never wake up. - const timeout_ns = timeout orelse { - unreachable; // deadlock detected - }; - - std.Thread.sleep(timeout_ns); + assert(timeout != null); // Deadlock detected. return error.Timeout; } @@ -323,6 +318,8 @@ test "wait and signal" { return error.SkipZigTest; } + const io = testing.io; + const num_threads = 4; const MultiWait = struct { @@ -348,7 +345,7 @@ test "wait and signal" { } while (true) { - std.Thread.sleep(100 * std.time.ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(100) }, io); multi_wait.mutex.lock(); defer multi_wait.mutex.unlock(); @@ -368,6 +365,8 @@ test signal { return error.SkipZigTest; } + const io = testing.io; + const num_threads = 4; const SignalTest = struct { @@ -405,7 +404,7 @@ test signal { } while (true) { - std.Thread.sleep(10 * std.time.ns_per_ms); + try std.Io.Clock.Duration.sleep(.{ .clock = .awake, .raw = .fromMilliseconds(10) }, io); signal_test.mutex.lock(); defer signal_test.mutex.unlock(); diff --git a/lib/std/Uri.zig b/lib/std/Uri.zig index 46903ecf33..a27f208e65 100644 --- a/lib/std/Uri.zig +++ b/lib/std/Uri.zig @@ -441,7 +441,7 @@ pub fn resolveInPlace(base: Uri, new_len: usize, aux_buf: *[]u8) ResolveInPlaceE }; } -fn validateHost(bytes: []const u8) []const u8 { +fn validateHost(bytes: []const u8) HostName.ValidateError![]const u8 { try HostName.validate(bytes); return bytes; } diff --git a/lib/std/dynamic_library.zig b/lib/std/dynamic_library.zig index 92aea780bf..a7326cfd66 100644 --- a/lib/std/dynamic_library.zig +++ b/lib/std/dynamic_library.zig @@ -137,6 +137,7 @@ const ElfDynLibError = error{ ElfStringSectionNotFound, ElfSymSectionNotFound, ElfHashTableNotFound, + Canceled, } || posix.OpenError || posix.MMapError; pub const ElfDynLib = struct { diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig index c2ddf98627..f6455215ac 100644 --- a/lib/std/fs/Dir.zig +++ b/lib/std/fs/Dir.zig @@ -36,10 +36,6 @@ const IteratorError = error{ AccessDenied, PermissionDenied, SystemResources, - /// WASI-only. The path of an entry could not be encoded as valid UTF-8. - /// WASI is unable to handle paths that cannot be encoded as well-formed UTF-8. - /// https://github.com/WebAssembly/wasi-filesystem/issues/17#issuecomment-1430639353 - InvalidUtf8, } || posix.UnexpectedError; pub const Iterator = switch (native_os) { @@ -553,7 +549,6 @@ pub const Iterator = switch (native_os) { .INVAL => unreachable, .NOENT => return error.DirNotFound, // The directory being iterated was deleted during iteration. .NOTCAPABLE => return error.AccessDenied, - .ILSEQ => return error.InvalidUtf8, // An entry's name cannot be encoded as UTF-8. else => |err| return posix.unexpectedErrno(err), } if (bufused == 0) return null; @@ -844,28 +839,7 @@ pub fn walk(self: Dir, allocator: Allocator) Allocator.Error!Walker { }; } -pub const OpenError = error{ - FileNotFound, - NotDir, - AccessDenied, - PermissionDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - NameTooLong, - SystemFdQuotaExceeded, - NoDevice, - SystemResources, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - BadPathName, - DeviceBusy, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, -} || posix.UnexpectedError; +pub const OpenError = Io.Dir.OpenError; pub fn close(self: *Dir) void { posix.close(self.fd); @@ -1311,7 +1285,7 @@ pub fn makeOpenPath(self: Dir, sub_path: []const u8, open_dir_options: OpenOptio }; } -pub const RealPathError = posix.RealPathError; +pub const RealPathError = posix.RealPathError || error{Canceled}; /// This function returns the canonicalized absolute pathname of /// `pathname` relative to this `Dir`. If `pathname` is absolute, ignores this @@ -1369,7 +1343,6 @@ pub fn realpathZ(self: Dir, pathname: [*:0]const u8, out_buffer: []u8) RealPathE error.FileLocksNotSupported => return error.Unexpected, error.FileBusy => return error.Unexpected, error.WouldBlock => return error.Unexpected, - error.InvalidUtf8 => unreachable, // WASI-only else => |e| return e, }; defer posix.close(fd); @@ -1639,6 +1612,10 @@ fn openDirFlagsZ(self: Dir, sub_path_c: [*:0]const u8, flags: posix.O) OpenError error.FileLocksNotSupported => unreachable, // locking folders is not supported error.WouldBlock => unreachable, // can't happen for directories error.FileBusy => unreachable, // can't happen for directories + error.SharingViolation => unreachable, // can't happen for directories + error.PipeBusy => unreachable, // can't happen for directories + error.AntivirusInterference => unreachable, // can't happen for directories + error.ProcessNotFound => unreachable, // can't happen for directories else => |e| return e, }; return Dir{ .fd = fd }; @@ -2095,24 +2072,21 @@ pub const DeleteTreeError = error{ FileBusy, DeviceBusy, ProcessNotFound, - /// One of the path components was not a directory. /// This error is unreachable if `sub_path` does not contain a path separator. NotDir, - /// WASI-only; file paths must be valid UTF-8. InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. /// https://wtf-8.codeberg.page/ InvalidWtf8, - /// On Windows, file paths cannot contain these characters: /// '/', '*', '?', '"', '<', '>', '|' BadPathName, - /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, + + Canceled, } || posix.UnexpectedError; /// Whether `sub_path` describes a symlink, file, or directory, this function @@ -2169,17 +2143,15 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.PermissionDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, - error.ProcessNotFound, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; stack.appendAssumeCapacity(.{ @@ -2266,18 +2238,16 @@ pub fn deleteTree(self: Dir, sub_path: []const u8) DeleteTreeError!void { error.AccessDenied, error.PermissionDenied, error.SymLinkLoop, - error.ProcessNotFound, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; } else { @@ -2374,18 +2344,16 @@ fn deleteTreeMinStackSizeWithKindHint(self: Dir, sub_path: []const u8, kind_hint error.AccessDenied, error.PermissionDenied, error.SymLinkLoop, - error.ProcessNotFound, error.ProcessFdQuotaExceeded, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.NetworkNotFound, error.DeviceBusy, + error.Canceled, => |e| return e, }; if (cleanup_dir_parent) |*d| d.close(); @@ -2476,17 +2444,15 @@ fn deleteTreeOpenInitialSubpath(self: Dir, sub_path: []const u8, kind_hint: File error.PermissionDenied, error.SymLinkLoop, error.ProcessFdQuotaExceeded, - error.ProcessNotFound, error.NameTooLong, error.SystemFdQuotaExceeded, error.NoDevice, error.SystemResources, error.Unexpected, - error.InvalidUtf8, - error.InvalidWtf8, error.BadPathName, error.DeviceBusy, error.NetworkNotFound, + error.Canceled, => |e| return e, }; } else { @@ -2589,7 +2555,7 @@ pub const CopyFileOptions = struct { pub const CopyFileError = File.OpenError || File.StatError || AtomicFile.InitError || AtomicFile.FinishError || - File.ReadError || File.WriteError; + File.ReadError || File.WriteError || error{InvalidFileName}; /// Atomically creates a new file at `dest_path` within `dest_dir` with the /// same contents as `source_path` within `source_dir`, overwriting any already @@ -2690,33 +2656,9 @@ pub fn statFile(self: Dir, sub_path: []const u8) StatFileError!Stat { const st = try std.os.fstatat_wasi(self.fd, sub_path, .{ .SYMLINK_FOLLOW = true }); return Stat.fromWasi(st); } - if (native_os == .linux) { - const sub_path_c = try posix.toPosixPath(sub_path); - var stx = std.mem.zeroes(linux.Statx); - - const rc = linux.statx( - self.fd, - &sub_path_c, - linux.AT.NO_AUTOMOUNT, - linux.STATX_TYPE | linux.STATX_MODE | linux.STATX_ATIME | linux.STATX_MTIME | linux.STATX_CTIME, - &stx, - ); - - return switch (linux.E.init(rc)) { - .SUCCESS => Stat.fromLinux(stx), - .ACCES => error.AccessDenied, - .BADF => unreachable, - .FAULT => unreachable, - .INVAL => unreachable, - .LOOP => error.SymLinkLoop, - .NAMETOOLONG => unreachable, // Handled by posix.toPosixPath() above. - .NOENT, .NOTDIR => error.FileNotFound, - .NOMEM => error.SystemResources, - else => |err| posix.unexpectedErrno(err), - }; - } - const st = try posix.fstatat(self.fd, sub_path, 0); - return Stat.fromPosix(st); + var threaded: Io.Threaded = .init_single_threaded; + const io = threaded.io(); + return Io.Dir.statPath(.{ .handle = self.fd }, io, sub_path, .{}); } pub const ChmodError = File.ChmodError; diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig index b2130bc5da..8253fe61e7 100644 --- a/lib/std/fs/File.zig +++ b/lib/std/fs/File.zig @@ -38,33 +38,8 @@ pub const default_mode = switch (builtin.os.tag) { else => 0o666, }; -pub const OpenError = error{ - SharingViolation, - PathAlreadyExists, - FileNotFound, - AccessDenied, - PipeBusy, - NoDevice, - NameTooLong, - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - /// On Windows, file paths cannot contain these characters: - /// '/', '*', '?', '"', '<', '>', '|' - BadPathName, - Unexpected, - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - ProcessNotFound, - /// On Windows, antivirus software is enabled by default. It can be - /// disabled, but Windows Update sometimes ignores the user's preference - /// and re-enables it. When enabled, antivirus software on Windows - /// intercepts file system operations and makes them significantly slower - /// in addition to possibly failing with this error code. - AntivirusInterference, -} || posix.OpenError || posix.FlockError; +/// Deprecated in favor of `Io.File.OpenError`. +pub const OpenError = Io.File.OpenError || error{WouldBlock}; pub const OpenMode = enum { read_only, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index 245e4bac9a..50d63ad63d 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1542,81 +1542,7 @@ pub fn pwritev(fd: fd_t, iov: []const iovec_const, offset: u64) PWriteError!usiz } } -pub const OpenError = error{ - /// In WASI, this error may occur when the file descriptor does - /// not hold the required rights to open a new resource relative to it. - AccessDenied, - PermissionDenied, - SymLinkLoop, - ProcessFdQuotaExceeded, - SystemFdQuotaExceeded, - NoDevice, - /// Either: - /// * One of the path components does not exist. - /// * Cwd was used, but cwd has been deleted. - /// * The path associated with the open directory handle has been deleted. - /// * On macOS, multiple processes or threads raced to create the same file - /// with `O.EXCL` set to `false`. - FileNotFound, - - /// The path exceeded `max_path_bytes` bytes. - NameTooLong, - - /// Insufficient kernel memory was available, or - /// the named file is a FIFO and per-user hard limit on - /// memory allocation for pipes has been reached. - SystemResources, - - /// The file is too large to be opened. This error is unreachable - /// for 64-bit targets, as well as when opening directories. - FileTooBig, - - /// The path refers to directory but the `DIRECTORY` flag was not provided. - IsDir, - - /// A new path cannot be created because the device has no room for the new file. - /// This error is only reachable when the `CREAT` flag is provided. - NoSpaceLeft, - - /// A component used as a directory in the path was not, in fact, a directory, or - /// `DIRECTORY` was specified and the path was not a directory. - NotDir, - - /// The path already exists and the `CREAT` and `EXCL` flags were provided. - PathAlreadyExists, - DeviceBusy, - - /// The underlying filesystem does not support file locks - FileLocksNotSupported, - - /// Path contains characters that are disallowed by the underlying filesystem. - BadPathName, - - /// WASI-only; file paths must be valid UTF-8. - InvalidUtf8, - - /// Windows-only; file paths provided by the user must be valid WTF-8. - /// https://wtf-8.codeberg.page/ - InvalidWtf8, - - /// On Windows, `\\server` or `\\server\share` was not found. - NetworkNotFound, - - /// This error occurs in Linux if the process to be open was not found. - ProcessNotFound, - - /// One of these three things: - /// * pathname refers to an executable image which is currently being - /// executed and write access was requested. - /// * pathname refers to a file that is currently in use as a swap - /// file, and the O_TRUNC flag was specified. - /// * pathname refers to a file that is currently being read by the - /// kernel (e.g., for module/firmware loading), and write access was - /// requested. - FileBusy, - - WouldBlock, -} || UnexpectedError; +pub const OpenError = std.Io.File.OpenError || error{WouldBlock}; /// Open and possibly create a file. Keeps trying if it gets interrupted. /// On Windows, `file_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index 7318612151..7bf836b583 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -766,27 +766,23 @@ test glibcVerFromLinkName { fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows-only - error.BadPathName => unreachable, - error.DeviceBusy => unreachable, - error.NetworkNotFound => unreachable, // Windows-only + error.NameTooLong => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.DeviceBusy => return error.Unexpected, + error.NetworkNotFound => return error.Unexpected, // Windows-only - error.FileNotFound, - error.NotDir, - error.AccessDenied, - error.PermissionDenied, - error.NoDevice, - => return error.GLibCNotFound, + error.FileNotFound => return error.GLibCNotFound, + error.NotDir => return error.GLibCNotFound, + error.AccessDenied => return error.GLibCNotFound, + error.PermissionDenied => return error.GLibCNotFound, + error.NoDevice => return error.GLibCNotFound, - error.ProcessNotFound, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.SystemResources => |e| return e, + error.SymLinkLoop => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, }; defer dir.close(); @@ -799,38 +795,34 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { // that start with "GLIBC_2.". const glibc_so_basename = "libc.so.6"; var file = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // WASI only - error.InvalidWtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.PipeBusy => unreachable, // Windows-only - error.SharingViolation => unreachable, // Windows-only - error.NetworkNotFound => unreachable, // Windows-only - error.AntivirusInterference => unreachable, // Windows-only - error.FileLocksNotSupported => unreachable, // No lock requested. - error.NoSpaceLeft => unreachable, // read-only - error.PathAlreadyExists => unreachable, // read-only - error.DeviceBusy => unreachable, // read-only - error.FileBusy => unreachable, // read-only - error.WouldBlock => unreachable, // not using O_NONBLOCK - error.NoDevice => unreachable, // not asking for a special device - - error.AccessDenied, - error.PermissionDenied, - error.FileNotFound, - error.NotDir, - error.IsDir, - => return error.GLibCNotFound, - + error.NameTooLong => return error.Unexpected, + error.BadPathName => return error.Unexpected, + error.PipeBusy => return error.Unexpected, // Windows-only + error.SharingViolation => return error.Unexpected, // Windows-only + error.NetworkNotFound => return error.Unexpected, // Windows-only + error.AntivirusInterference => return error.Unexpected, // Windows-only + error.FileLocksNotSupported => return error.Unexpected, // No lock requested. + error.NoSpaceLeft => return error.Unexpected, // read-only + error.PathAlreadyExists => return error.Unexpected, // read-only + error.DeviceBusy => return error.Unexpected, // read-only + error.FileBusy => return error.Unexpected, // read-only + error.NoDevice => return error.Unexpected, // not asking for a special device error.FileTooBig => return error.Unexpected, + error.WouldBlock => return error.Unexpected, // not opened in non-blocking - error.ProcessNotFound, - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, + error.AccessDenied => return error.GLibCNotFound, + error.PermissionDenied => return error.GLibCNotFound, + error.FileNotFound => return error.GLibCNotFound, + error.NotDir => return error.GLibCNotFound, + error.IsDir => return error.GLibCNotFound, + + error.ProcessNotFound => |e| return e, + error.ProcessFdQuotaExceeded => |e| return e, + error.SystemFdQuotaExceeded => |e| return e, + error.SystemResources => |e| return e, + error.SymLinkLoop => |e| return e, + error.Unexpected => |e| return e, + error.Canceled => |e| return e, }; defer file.close(); @@ -1016,12 +1008,9 @@ fn detectAbiAndDynamicLinker(io: Io, cpu: Target.Cpu, os: Target.Os, query: Targ error.NameTooLong => return error.Unexpected, error.PathAlreadyExists => return error.Unexpected, error.SharingViolation => return error.Unexpected, - error.InvalidUtf8 => return error.Unexpected, // WASI only - error.InvalidWtf8 => return error.Unexpected, // Windows only error.BadPathName => return error.Unexpected, error.PipeBusy => return error.Unexpected, error.FileLocksNotSupported => return error.Unexpected, - error.WouldBlock => return error.Unexpected, error.FileBusy => return error.Unexpected, // opened without write permissions error.AntivirusInterference => return error.Unexpected, // Windows-only error