diff --git a/lib/std/Io.zig b/lib/std/Io.zig index a1e38135bd..7ef4b85142 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -667,6 +667,7 @@ pub const VTable = struct { dirStatFile: *const fn (?*anyopaque, Dir, []const u8, Dir.StatFileOptions) Dir.StatFileError!File.Stat, dirAccess: *const fn (?*anyopaque, Dir, []const u8, Dir.AccessOptions) Dir.AccessError!void, dirCreateFile: *const fn (?*anyopaque, Dir, []const u8, File.CreateFlags) File.OpenError!File, + dirCreateFileAtomic: *const fn (?*anyopaque, Dir, []const u8, Dir.CreateFileAtomicOptions) Dir.CreateFileAtomicError!File.Atomic, dirOpenFile: *const fn (?*anyopaque, Dir, []const u8, File.OpenFlags) File.OpenError!File, dirClose: *const fn (?*anyopaque, []const Dir) void, dirRead: *const fn (?*anyopaque, *Dir.Reader, []Dir.Entry) Dir.Reader.Error!usize, @@ -710,6 +711,7 @@ pub const VTable = struct { fileUnlock: *const fn (?*anyopaque, File) void, fileDowngradeLock: *const fn (?*anyopaque, File) File.DowngradeLockError!void, fileRealPath: *const fn (?*anyopaque, File, out_buffer: []u8) File.RealPathError!usize, + fileHardLink: *const fn (?*anyopaque, File, Dir, []const u8, File.HardLinkOptions) File.HardLinkError!void, processExecutableOpen: *const fn (?*anyopaque, File.OpenFlags) std.process.OpenExecutableError!File, processExecutablePath: *const fn (?*anyopaque, buffer: []u8) std.process.ExecutablePathError!usize, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index cc1dec8efe..9ec1abe32e 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -454,7 +454,6 @@ pub const OpenError = error{ SystemFdQuotaExceeded, NoDevice, SystemResources, - DeviceBusy, /// On Windows, `\\server` or `\\server\share` was not found. NetworkNotFound, } || PathNameError || Io.Cancelable || Io.UnexpectedError; @@ -598,30 +597,29 @@ pub fn updateFile( } } - if (path.dirname(dest_path)) |dirname| { - try dest_dir.createDirPath(io, dirname); - } - - var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available. - var atomic_file = try dest_dir.atomicFile(io, dest_path, .{ + var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{ .permissions = actual_permissions, - .write_buffer = &buffer, + .make_path = true, + .replace = true, }); - defer atomic_file.deinit(); + defer atomic_file.deinit(io); + + var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available. + var file_writer = atomic_file.file.writer(io, &buffer); var src_reader: File.Reader = .initSize(src_file, io, &.{}, src_stat.size); - const dest_writer = &atomic_file.file_writer.interface; + const dest_writer = &file_writer.interface; _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) { error.ReadFailed => return src_reader.err.?, - error.WriteFailed => return atomic_file.file_writer.err.?, + error.WriteFailed => return file_writer.err.?, }; - try atomic_file.flush(); - try atomic_file.file_writer.file.setTimestamps(io, .{ + try file_writer.flush(); + try file_writer.file.setTimestamps(io, .{ .access_timestamp = .init(src_stat.atime), .modify_timestamp = .init(src_stat.mtime), }); - try atomic_file.renameIntoPlace(); + try atomic_file.replace(io); return .stale; } @@ -995,27 +993,9 @@ pub fn renameAbsolute(old_path: []const u8, new_path: []const u8, io: Io) Rename return io.vtable.dirRename(io.userdata, my_cwd, old_path, my_cwd, new_path); } -pub const HardLinkOptions = struct { - follow_symlinks: bool = true, -}; +pub const HardLinkOptions = File.HardLinkOptions; -pub const HardLinkError = error{ - AccessDenied, - PermissionDenied, - DiskQuota, - PathAlreadyExists, - HardwareFailure, - /// Either the OS or the filesystem does not support hard links. - OperationUnsupported, - SymLinkLoop, - LinkQuotaExceeded, - FileNotFound, - SystemResources, - NoSpaceLeft, - ReadOnlyFileSystem, - NotSameFileSystem, - NotDir, -} || Io.Cancelable || PathNameError || Io.UnexpectedError; +pub const HardLinkError = File.HardLinkError; pub fn hardLink( old_dir: Dir, @@ -1251,7 +1231,6 @@ pub const DeleteTreeError = error{ ReadOnlyFileSystem, FileSystem, FileBusy, - DeviceBusy, /// One of the path components was not a directory. /// This error is unreachable if `sub_path` does not contain a path separator. NotDir, @@ -1322,7 +1301,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void { error.Unexpected, error.BadPathName, error.NetworkNotFound, - error.DeviceBusy, error.Canceled, => |e| return e, }; @@ -1417,7 +1395,6 @@ pub fn deleteTree(dir: Dir, io: Io, sub_path: []const u8) DeleteTreeError!void { error.Unexpected, error.BadPathName, error.NetworkNotFound, - error.DeviceBusy, error.Canceled, => |e| return e, }; @@ -1522,7 +1499,6 @@ fn deleteTreeMinStackSizeWithKindHint(parent: Dir, io: Io, sub_path: []const u8, error.Unexpected, error.BadPathName, error.NetworkNotFound, - error.DeviceBusy, error.Canceled, => |e| return e, }; @@ -1619,7 +1595,6 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin error.SystemResources, error.Unexpected, error.BadPathName, - error.DeviceBusy, error.NetworkNotFound, error.Canceled, => |e| return e, @@ -1658,15 +1633,18 @@ fn deleteTreeOpenInitialSubpath(dir: Dir, io: Io, sub_path: []const u8, kind_hin pub const CopyFileOptions = struct { /// When this is `null` the permissions are copied from the source file. permissions: ?File.Permissions = null, + make_path: bool = false, + replace: bool = true, }; pub const CopyFileError = File.OpenError || File.StatError || - File.Atomic.InitError || File.Atomic.FinishError || + CreateFileAtomicError || File.Atomic.ReplaceError || File.Atomic.LinkError || File.Reader.Error || File.Writer.Error || 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 -/// existing file. +/// same contents as `source_path` within `source_dir`. +/// +/// Whether to overwrite the existing file is determined by `options`. /// /// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and /// readily available, there is a possibility of power loss or application @@ -1695,19 +1673,27 @@ pub fn copyFile( break :blk st.permissions; }; - var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available. - var atomic_file = try dest_dir.atomicFile(io, dest_path, .{ + var atomic_file = try dest_dir.createFileAtomic(io, dest_path, .{ .permissions = permissions, - .write_buffer = &buffer, + .make_path = options.make_path, + .replace = options.replace, }); - defer atomic_file.deinit(); + defer atomic_file.deinit(io); - _ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { + var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available. + var file_writer = atomic_file.file.writer(io, &buffer); + + _ = file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) { error.ReadFailed => return file_reader.err.?, - error.WriteFailed => return atomic_file.file_writer.err.?, + error.WriteFailed => return file_writer.err.?, }; - try atomic_file.finish(); + try file_writer.flush(); + + switch (options.replace) { + true => try atomic_file.replace(io), + false => try atomic_file.link(io), + } } /// Same as `copyFile`, except asserts that both `source_path` and `dest_path` @@ -1730,33 +1716,65 @@ pub fn copyFileAbsolute( test copyFileAbsolute {} -pub const AtomicFileOptions = struct { +pub const CreateFileAtomicOptions = struct { permissions: File.Permissions = .default_file, make_path: bool = false, - write_buffer: []u8, + /// Tells whether the unnamed file will be ultimately created with + /// `File.Atomic.link` or `File.Atomic.replace`. + /// + /// If this value is incorrect it will cause an assertion failure in + /// `File.Atomic.replace`. + replace: bool = false, }; -/// Directly access the `.file` field, and then call `File.Atomic.finish` to -/// atomically replace `dest_path` with contents. -/// -/// Always call `File.Atomic.deinit` to clean up, regardless of whether -/// `File.Atomic.finish` succeeded. `dest_path` must remain valid until -/// `File.Atomic.deinit` is called. -/// -/// On Windows, `dest_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, `dest_path` should be encoded as valid UTF-8. -/// On other platforms, `dest_path` is an opaque sequence of bytes with no particular encoding. -pub fn atomicFile(parent: Dir, io: Io, dest_path: []const u8, options: AtomicFileOptions) !File.Atomic { - if (path.dirname(dest_path)) |dirname| { - const dir = if (options.make_path) - try parent.createDirPathOpen(io, dirname, .{}) - else - try parent.openDir(io, dirname, .{}); +pub const CreateFileAtomicError = error{ + NoDevice, + /// On Windows, `\\server` or `\\server\share` was not found. + NetworkNotFound, + /// 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. + FileNotFound, + /// Insufficient kernel memory was available. + SystemResources, + /// A new path cannot be created because the device has no room for the new file. + NoSpaceLeft, + /// A component used as a directory in the path was not, in fact, a directory. + NotDir, + WouldBlock, + ReadOnlyFileSystem, +} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; - return .init(io, path.basename(dest_path), options.permissions, dir, true, options.write_buffer); - } else { - return .init(io, dest_path, options.permissions, parent, false, options.write_buffer); - } +/// Create an unnamed ephemeral file that can eventually be atomically +/// materialized into `sub_path`. +/// +/// The returned `File.Atomic` provides API to emulate the behavior in case it +/// is not directly supported by the underlying operating system. +/// +/// * On Windows, `sub_path` should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// * 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 createFileAtomic( + dir: Dir, + io: Io, + sub_path: []const u8, + options: CreateFileAtomicOptions, +) CreateFileAtomicError!File.Atomic { + return io.vtable.dirCreateFileAtomic(io.userdata, dir, sub_path, options); } pub const SetPermissionsError = File.SetPermissionsError; diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index bbe550a9cc..389c8fb8b1 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -278,7 +278,7 @@ pub const OpenError = error{ FileBusy, /// Non-blocking was requested and the operation cannot return immediately. WouldBlock, -} || Io.Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; +} || Dir.PathNameError || Io.Cancelable || Io.UnexpectedError; pub fn close(file: File, io: Io) void { return io.vtable.fileClose(io.userdata, (&file)[0..1]); @@ -708,6 +708,38 @@ pub fn realPath(file: File, io: Io, out_buffer: []u8) RealPathError!usize { return io.vtable.fileRealPath(io.userdata, file, out_buffer); } +pub const HardLinkOptions = struct { + follow_symlinks: bool = true, +}; + +pub const HardLinkError = error{ + AccessDenied, + PermissionDenied, + DiskQuota, + PathAlreadyExists, + HardwareFailure, + /// Either the OS or the filesystem does not support hard links. + OperationUnsupported, + SymLinkLoop, + LinkQuotaExceeded, + FileNotFound, + SystemResources, + NoSpaceLeft, + ReadOnlyFileSystem, + NotSameFileSystem, + NotDir, +} || Io.Cancelable || Dir.PathNameError || Io.UnexpectedError; + +pub fn hardLink( + file: File, + io: Io, + new_dir: Dir, + new_sub_path: []const u8, + options: HardLinkOptions, +) HardLinkError!void { + return io.vtable.fileHardLink(io.userdata, file, new_dir, new_sub_path, options); +} + test { _ = Reader; _ = Writer; diff --git a/lib/std/Io/File/Atomic.zig b/lib/std/Io/File/Atomic.zig index 340303ca39..440430e04c 100644 --- a/lib/std/Io/File/Atomic.zig +++ b/lib/std/Io/File/Atomic.zig @@ -6,97 +6,78 @@ const File = std.Io.File; const Dir = std.Io.Dir; const assert = std.debug.assert; -file_writer: File.Writer, -random_integer: u64, -dest_basename: []const u8, +file: File, +file_basename_hex: u64, file_open: bool, file_exists: bool, -close_dir_on_deinit: bool, + dir: Dir, +close_dir_on_deinit: bool, + +dest_sub_path: []const u8, pub const InitError = File.OpenError; -/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function. -pub fn init( - io: Io, - dest_basename: []const u8, - permissions: File.Permissions, - dir: Dir, - close_dir_on_deinit: bool, - write_buffer: []u8, -) InitError!Atomic { - while (true) { - const random_integer = std.crypto.random.int(u64); - const tmp_sub_path = std.fmt.hex(random_integer); - const file = dir.createFile(io, &tmp_sub_path, .{ - .permissions = permissions, - .exclusive = true, - }) catch |err| switch (err) { - error.PathAlreadyExists => continue, - else => |e| return e, - }; - return .{ - .file_writer = file.writer(io, write_buffer), - .random_integer = random_integer, - .dest_basename = dest_basename, - .file_open = true, - .file_exists = true, - .close_dir_on_deinit = close_dir_on_deinit, - .dir = dir, - }; - } -} - -/// Always call deinit, even after a successful finish(). -pub fn deinit(af: *Atomic) void { - const io = af.file_writer.io; - +/// To release all resources, always call `deinit`, even after a successful +/// `finish`. +pub fn deinit(af: *Atomic, io: Io) void { if (af.file_open) { - af.file_writer.file.close(io); + af.file.close(io); af.file_open = false; } if (af.file_exists) { - const tmp_sub_path = std.fmt.hex(af.random_integer); + const tmp_sub_path = std.fmt.hex(af.file_basename_hex); af.dir.deleteFile(io, &tmp_sub_path) catch {}; af.file_exists = false; } if (af.close_dir_on_deinit) { af.dir.close(io); + af.close_dir_on_deinit = false; } af.* = undefined; } -pub const FlushError = File.Writer.Error; +pub const LinkError = Dir.HardLinkError; -pub fn flush(af: *Atomic) FlushError!void { - af.file_writer.interface.flush() catch |err| switch (err) { - error.WriteFailed => return af.file_writer.err.?, - }; +/// Atomically materializes the file into place, failing with +/// `error.PathAlreadyExists` if something already exists there. +pub fn link(af: *Atomic, io: Io) LinkError!void { + if (af.file_exists) { + if (af.file_open) { + af.file.close(io); + af.file_open = false; + } + const tmp_sub_path = std.fmt.hex(af.file_basename_hex); + try af.dir.hardLink(&tmp_sub_path, af.dir, af.dest_sub_path, io, .{}); + af.dir.deleteFile(io, &tmp_sub_path) catch {}; + af.file_exists = false; + } else { + assert(af.file_open); + try af.file.hardLink(io, af.dir, af.dest_sub_path, .{}); + af.file.close(io); + af.file_open = false; + } } -pub const RenameIntoPlaceError = Dir.RenameError; +pub const ReplaceError = Dir.RenameError; +/// Atomically materializes the file into place, replacing any file that +/// already exists there. +/// +/// Calling this function requires setting `CreateFileAtomicOptions.replace` to +/// `true`. +/// /// On Windows, this function introduces a period of time where some file /// system operations on the destination file will result in /// `error.AccessDenied`, including rename operations (such as the one used in /// this function). -pub fn renameIntoPlace(af: *Atomic) RenameIntoPlaceError!void { - const io = af.file_writer.io; - - assert(af.file_exists); +pub fn replace(af: *Atomic, io: Io) ReplaceError!void { + assert(af.file_exists); // Wrong value for `CreateFileAtomicOptions.replace`. if (af.file_open) { - af.file_writer.file.close(io); + af.file.close(io); af.file_open = false; } - const tmp_sub_path = std.fmt.hex(af.random_integer); - try af.dir.rename(&tmp_sub_path, af.dir, af.dest_basename, io); + const tmp_sub_path = std.fmt.hex(af.file_basename_hex); + try af.dir.rename(&tmp_sub_path, af.dir, af.dest_sub_path, io); af.file_exists = false; } - -pub const FinishError = FlushError || RenameIntoPlaceError; - -/// Combination of `flush` followed by `renameIntoPlace`. -pub fn finish(af: *Atomic) FinishError!void { - try af.flush(); - try af.renameIntoPlace(); -} diff --git a/lib/std/Io/File/Writer.zig b/lib/std/Io/File/Writer.zig index bf8c0bf289..52bbe83513 100644 --- a/lib/std/Io/File/Writer.zig +++ b/lib/std/Io/File/Writer.zig @@ -272,3 +272,11 @@ pub fn end(w: *Writer) EndError!void { => {}, } } + +/// Convenience method for calling `Io.Writer.flush` and returning the +/// underlying error. +pub fn flush(w: *Writer) Error!void { + w.interface.flush() catch |err| switch (err) { + error.WriteFailed => return w.err.?, + }; +} diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 19aa154bba..ed40914950 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1403,6 +1403,7 @@ pub fn io(t: *Threaded) Io { .dirStatFile = dirStatFile, .dirAccess = dirAccess, .dirCreateFile = dirCreateFile, + .dirCreateFileAtomic = dirCreateFileAtomic, .dirOpenFile = dirOpenFile, .dirOpenDir = dirOpenDir, .dirClose = dirClose, @@ -1445,6 +1446,7 @@ pub fn io(t: *Threaded) Io { .fileUnlock = fileUnlock, .fileDowngradeLock = fileDowngradeLock, .fileRealPath = fileRealPath, + .fileHardLink = fileHardLink, .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, @@ -1549,6 +1551,7 @@ pub fn ioBasic(t: *Threaded) Io { .dirStatFile = dirStatFile, .dirAccess = dirAccess, .dirCreateFile = dirCreateFile, + .dirCreateFileAtomic = dirCreateFileAtomic, .dirOpenFile = dirOpenFile, .dirOpenDir = dirOpenDir, .dirClose = dirClose, @@ -1591,6 +1594,7 @@ pub fn ioBasic(t: *Threaded) Io { .fileUnlock = fileUnlock, .fileDowngradeLock = fileDowngradeLock, .fileRealPath = fileRealPath, + .fileHardLink = fileHardLink, .processExecutableOpen = processExecutableOpen, .processExecutablePath = processExecutablePath, @@ -3413,6 +3417,170 @@ fn dirCreateFileWasi( } } +fn dirCreateFileAtomic( + userdata: ?*anyopaque, + dir: Dir, + dest_path: []const u8, + options: Dir.CreateFileAtomicOptions, +) Dir.CreateFileAtomicError!File.Atomic { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + const t_io = ioBasic(t); + + // Linux has O_TMPFILE, but linkat() does not support AT_REPLACE, so it's + // useless when we have to make up a bogus path name to do the rename() + // anyway. + if (native_os == .linux and !options.replace) tmpfile: { + const flags: posix.O = if (@hasField(posix.O, "TMPFILE")) .{ + .ACCMODE = .RDWR, + .TMPFILE = true, + .DIRECTORY = true, + .CLOEXEC = true, + } else if (@hasField(posix.O, "TMPFILE0") and !@hasField(posix.O, "TMPFILE2")) .{ + .ACCMODE = .RDWR, + .TMPFILE0 = true, + .TMPFILE1 = true, + .DIRECTORY = true, + .CLOEXEC = true, + } else break :tmpfile; + + const dest_dirname = Dir.path.dirname(dest_path); + if (dest_dirname) |dirname| { + // This has a nice side effect of preemptively triggering EISDIR or + // ENOENT, avoiding the ambiguity below. + dir.createDirPath(t_io, dirname) catch |err| switch (err) { + // None of these make sense in this context. + error.IsDir, + error.Streaming, + error.DiskQuota, + error.PathAlreadyExists, + error.LinkQuotaExceeded, + error.SharingViolation, + error.PipeBusy, + error.FileTooBig, + error.DeviceBusy, + error.FileLocksUnsupported, + error.FileBusy, + => return error.Unexpected, + + else => |e| return e, + }; + } + + var path_buffer: [posix.PATH_MAX]u8 = undefined; + const sub_path_posix = try pathToPosix(dest_dirname orelse ".", &path_buffer); + + const syscall: Syscall = try .start(); + while (true) { + const rc = openat_sym(dir.handle, sub_path_posix, flags, options.permissions.toMode()); + switch (posix.errno(rc)) { + .SUCCESS => { + syscall.finish(); + return .{ + .file = .{ .handle = @intCast(rc) }, + .file_basename_hex = 0, + .dest_sub_path = dest_path, + .file_open = true, + .file_exists = false, + .close_dir_on_deinit = false, + .dir = dir, + }; + }, + .INTR => { + try syscall.checkCancel(); + continue; + }, + .ISDIR, .NOENT => { + // Ambiguous error code. It might mean the file system + // does not support O_TMPFILE. Therefore, we must fall + // back to not using O_TMPFILE. + syscall.finish(); + break :tmpfile; + }, + .INVAL => return syscall.fail(error.BadPathName), + .ACCES => return syscall.fail(error.AccessDenied), + .LOOP => return syscall.fail(error.SymLinkLoop), + .MFILE => return syscall.fail(error.ProcessFdQuotaExceeded), + .NAMETOOLONG => return syscall.fail(error.NameTooLong), + .NFILE => return syscall.fail(error.SystemFdQuotaExceeded), + .NODEV => return syscall.fail(error.NoDevice), + .NOMEM => return syscall.fail(error.SystemResources), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .NOTDIR => return syscall.fail(error.NotDir), + .PERM => return syscall.fail(error.PermissionDenied), + .AGAIN => return syscall.fail(error.WouldBlock), + .NXIO => return syscall.fail(error.NoDevice), + .ILSEQ => return syscall.fail(error.BadPathName), + else => |err| return syscall.unexpectedErrno(err), + } + } + } + + if (Dir.path.dirname(dest_path)) |dirname| { + const new_dir = if (options.make_path) + dir.createDirPathOpen(t_io, dirname, .{}) catch |err| switch (err) { + // None of these make sense in this context. + error.IsDir, + error.Streaming, + error.DiskQuota, + error.PathAlreadyExists, + error.LinkQuotaExceeded, + error.SharingViolation, + error.PipeBusy, + error.FileTooBig, + error.FileLocksUnsupported, + error.FileBusy, + error.DeviceBusy, + => return error.Unexpected, + + else => |e| return e, + } + else + try dir.openDir(t_io, dirname, .{}); + + return atomicFileInit(t_io, Dir.path.basename(dest_path), options.permissions, new_dir, true); + } + + return atomicFileInit(t_io, dest_path, options.permissions, dir, false); +} + +fn atomicFileInit( + t_io: Io, + dest_basename: []const u8, + permissions: File.Permissions, + dir: Dir, + close_dir_on_deinit: bool, +) Dir.CreateFileAtomicError!File.Atomic { + while (true) { + const random_integer = std.crypto.random.int(u64); + const tmp_sub_path = std.fmt.hex(random_integer); + const file = dir.createFile(t_io, &tmp_sub_path, .{ + .permissions = permissions, + .exclusive = true, + }) catch |err| switch (err) { + error.PathAlreadyExists => continue, + error.DeviceBusy => continue, + error.FileBusy => continue, + error.SharingViolation => continue, + + error.IsDir => return error.Unexpected, // No path components. + error.FileTooBig => return error.Unexpected, // Creating, not opening. + error.FileLocksUnsupported => return error.Unexpected, // Not asking for locks. + error.PipeBusy => return error.Unexpected, // Not opening a pipe. + + else => |e| return e, + }; + return .{ + .file = file, + .file_basename_hex = random_integer, + .dest_sub_path = dest_basename, + .file_open = true, + .file_exists = true, + .close_dir_on_deinit = close_dir_on_deinit, + .dir = dir, + }; + } +} + const dirOpenFile = switch (native_os) { .windows => dirOpenFileWindows, .wasi => dirOpenFileWasi, @@ -3925,7 +4093,7 @@ fn dirOpenDirPosix( .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, .PERM => return error.PermissionDenied, - .BUSY => return error.DeviceBusy, + .BUSY => |err| return errnoBug(err), // O_EXCL not passed .NXIO => return error.NoDevice, .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), @@ -4985,6 +5153,64 @@ fn realPathPosix(fd: posix.fd_t, out_buffer: []u8) File.RealPathError!usize { comptime unreachable; } +fn fileHardLink( + userdata: ?*anyopaque, + file: File, + new_dir: Dir, + new_sub_path: []const u8, + options: File.HardLinkOptions, +) File.HardLinkError!void { + _ = userdata; + if (native_os != .linux) return error.OperationUnsupported; + + var new_path_buffer: [posix.PATH_MAX]u8 = undefined; + const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer); + + const flags: u32 = if (!options.follow_symlinks) + posix.AT.SYMLINK_NOFOLLOW | posix.AT.EMPTY_PATH + else + posix.AT.EMPTY_PATH; + + return linkat(file.handle, "", new_dir.handle, new_sub_path_posix, flags); +} + +fn linkat( + old_dir: posix.fd_t, + old_path: [*:0]const u8, + new_dir: posix.fd_t, + new_path: [*:0]const u8, + flags: u32, +) File.HardLinkError!void { + const syscall: Syscall = try .start(); + while (true) { + switch (posix.errno(posix.system.linkat(old_dir, old_path, new_dir, new_path, flags))) { + .SUCCESS => return syscall.finish(), + .INTR => { + try syscall.checkCancel(); + continue; + }, + .ACCES => return syscall.fail(error.AccessDenied), + .DQUOT => return syscall.fail(error.DiskQuota), + .EXIST => return syscall.fail(error.PathAlreadyExists), + .IO => return syscall.fail(error.HardwareFailure), + .LOOP => return syscall.fail(error.SymLinkLoop), + .MLINK => return syscall.fail(error.LinkQuotaExceeded), + .NAMETOOLONG => return syscall.fail(error.NameTooLong), + .NOENT => return syscall.fail(error.FileNotFound), + .NOMEM => return syscall.fail(error.SystemResources), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .NOTDIR => return syscall.fail(error.NotDir), + .PERM => return syscall.fail(error.PermissionDenied), + .ROFS => return syscall.fail(error.ReadOnlyFileSystem), + .XDEV => return syscall.fail(error.NotSameFileSystem), + .ILSEQ => return syscall.fail(error.BadPathName), + .FAULT => |err| return syscall.errnoBug(err), + .INVAL => |err| return syscall.errnoBug(err), + else => |err| return syscall.unexpectedErrno(err), + } + } +} + const dirDeleteFile = switch (native_os) { .windows => dirDeleteFileWindows, .wasi => dirDeleteFileWasi, @@ -7325,7 +7551,6 @@ fn dirOpenDirWasi( .NOMEM => return error.SystemResources, .NOTDIR => return error.NotDir, .PERM => return error.PermissionDenied, - .BUSY => return error.DeviceBusy, .NOTCAPABLE => return error.AccessDenied, .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), @@ -7401,46 +7626,7 @@ fn dirHardLink( const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer); const flags: u32 = if (!options.follow_symlinks) posix.AT.SYMLINK_NOFOLLOW else 0; - - const syscall: Syscall = try .start(); - while (true) { - switch (posix.errno(posix.system.linkat( - old_dir.handle, - old_sub_path_posix, - new_dir.handle, - new_sub_path_posix, - flags, - ))) { - .SUCCESS => return syscall.finish(), - .INTR => { - try syscall.checkCancel(); - continue; - }, - else => |e| { - syscall.finish(); - switch (e) { - .ACCES => return error.AccessDenied, - .DQUOT => return error.DiskQuota, - .EXIST => return error.PathAlreadyExists, - .FAULT => |err| return errnoBug(err), - .IO => return error.HardwareFailure, - .LOOP => return error.SymLinkLoop, - .MLINK => return error.LinkQuotaExceeded, - .NAMETOOLONG => return error.NameTooLong, - .NOENT => return error.FileNotFound, - .NOMEM => return error.SystemResources, - .NOSPC => return error.NoSpaceLeft, - .NOTDIR => return error.NotDir, - .PERM => return error.PermissionDenied, - .ROFS => return error.ReadOnlyFileSystem, - .XDEV => return error.NotSameFileSystem, - .INVAL => |err| return errnoBug(err), - .ILSEQ => return error.BadPathName, - else => |err| return posix.unexpectedErrno(err), - } - }, - } - } + return linkat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix, flags); } fn fileClose(userdata: ?*anyopaque, files: []const File) void { @@ -13831,7 +14017,6 @@ fn windowsCreateProcessPathExt( error.NetworkNotFound, error.NameTooLong, error.BadPathName, - error.DeviceBusy, => return error.FileNotFound, }; }; diff --git a/lib/std/c.zig b/lib/std/c.zig index 0e620082b4..3eea82d3da 100644 --- a/lib/std/c.zig +++ b/lib/std/c.zig @@ -8427,6 +8427,7 @@ pub const O = switch (native_os) { CLOEXEC: bool = false, SYNC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _: u9 = 0, }, @@ -8615,6 +8616,7 @@ pub const O = switch (native_os) { _19: u1 = 0, CLOEXEC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _: u9 = 0, }, diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index 7158eb734e..c53ce18996 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1652,11 +1652,10 @@ test "AtomicFile" { ; { - var buffer: [100]u8 = undefined; - var af = try ctx.dir.atomicFile(io, test_out_file, .{ .write_buffer = &buffer }); - defer af.deinit(); - try af.file_writer.interface.writeAll(test_content); - try af.finish(); + var af = try ctx.dir.createFileAtomic(io, test_out_file, .{ .replace = true }); + defer af.deinit(io); + try af.file.writeStreamingAll(io, test_content); + try af.replace(io); } const content = try ctx.dir.readFileAlloc(io, test_out_file, allocator, .limited(9999)); try expectEqualStrings(test_content, content); diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index a1626113ac..812e07f7e8 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -324,6 +324,7 @@ pub const O = switch (native_arch) { CLOEXEC: bool = false, SYNC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _23: u9 = 0, }, @@ -346,6 +347,7 @@ pub const O = switch (native_arch) { CLOEXEC: bool = false, SYNC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _23: u9 = 0, }, @@ -368,6 +370,7 @@ pub const O = switch (native_arch) { CLOEXEC: bool = false, SYNC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _23: u9 = 0, }, @@ -393,6 +396,7 @@ pub const O = switch (native_arch) { CLOEXEC: bool = false, SYNC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _27: u6 = 0, }, @@ -417,6 +421,7 @@ pub const O = switch (native_arch) { CLOEXEC: bool = false, _20: u1 = 0, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _23: u9 = 0, }, @@ -439,6 +444,7 @@ pub const O = switch (native_arch) { CLOEXEC: bool = false, SYNC: bool = false, PATH: bool = false, + /// This is typically invalid without also setting `DIRECTORY`. TMPFILE: bool = false, _23: u9 = 0, }, @@ -459,13 +465,16 @@ pub const O = switch (native_arch) { NOFOLLOW: bool = false, NOATIME: bool = false, CLOEXEC: bool = false, - _20: u1 = 0, + /// This is typically invalid without also setting `TMPFILE1` and `DIRECTORY`. + TMPFILE0: bool = false, PATH: bool = false, - _22: u10 = 0, + _22: u4 = 0, + /// This is typically invalid without also setting `TMPFILE0` and `DIRECTORY`. + TMPFILE1: bool = false, + _27: u5 = 0, // #define O_RSYNC 04010000 // #define O_SYNC 04010000 - // #define O_TMPFILE 020200000 // #define O_NDELAY O_NONBLOCK }, .m68k => packed struct(u32) { diff --git a/lib/std/zig/system.zig b/lib/std/zig/system.zig index dfa10b84b6..d94ae1180c 100644 --- a/lib/std/zig/system.zig +++ b/lib/std/zig/system.zig @@ -793,7 +793,6 @@ fn glibcVerFromRPath(io: Io, rpath: []const u8) !std.SemanticVersion { var dir = cwd.openDir(io, rpath, .{}) catch |err| switch (err) { error.NameTooLong => return error.Unexpected, error.BadPathName => return error.Unexpected, - error.DeviceBusy => return error.Unexpected, error.NetworkNotFound => return error.Unexpected, // Windows-only error.FileNotFound => return error.GLibCNotFound, diff --git a/src/Builtin.zig b/src/Builtin.zig index a097e88734..92a0e40ae3 100644 --- a/src/Builtin.zig +++ b/src/Builtin.zig @@ -343,10 +343,10 @@ pub fn updateFileOnDisk(file: *File, comp: *Compilation) !void { } // `make_path` matters because the dir hasn't actually been created yet. - var af = try root_dir.atomicFile(io, sub_path, .{ .make_path = true, .write_buffer = &.{} }); - defer af.deinit(); - try af.file_writer.interface.writeAll(file.source.?); - af.finish() catch |err| switch (err) { + var af = try root_dir.createFileAtomic(io, sub_path, .{ .make_path = true, .replace = true }); + defer af.deinit(io); + try af.file.writeStreamingAll(io, file.source.?); + af.replace(io) catch |err| switch (err) { error.AccessDenied => switch (builtin.os.tag) { .windows => { // Very likely happened due to another process or thread diff --git a/src/Compilation.zig b/src/Compilation.zig index 82f45ee9f0..957e0fadcf 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -3916,11 +3916,14 @@ pub fn saveState(comp: *Compilation) !void { // Using an atomic file prevents a crash or power failure from corrupting // the previous incremental compilation state. + var af = try lf.emit.root_dir.handle.createFileAtomic(io, basename, .{ .replace = true }); + defer af.deinit(io); + var write_buffer: [1024]u8 = undefined; - var af = try lf.emit.root_dir.handle.atomicFile(io, basename, .{ .write_buffer = &write_buffer }); - defer af.deinit(); - try af.file_writer.interface.writeVecAll(bufs.items); - try af.finish(); + var file_writer = af.file.writer(io, &write_buffer); + try file_writer.interface.writeVecAll(bufs.items); + try file_writer.interface.flush(); + try af.replace(io); } fn addBuf(list: *std.array_list.Managed([]const u8), buf: []const u8) void { @@ -5244,26 +5247,31 @@ fn processOneJob( } } -fn createDepFile(comp: *Compilation, depfile: []const u8, binfile: Cache.Path) anyerror!void { +fn createDepFile(comp: *Compilation, dep_file: []const u8, bin_file: Cache.Path) anyerror!void { const io = comp.io; + + var af = try Io.Dir.cwd().createFileAtomic(io, dep_file, .{ .replace = true }); + defer af.deinit(io); + var buf: [4096]u8 = undefined; - var af = try Io.Dir.cwd().atomicFile(io, depfile, .{ .write_buffer = &buf }); - defer af.deinit(); + var file_writer = af.file.writer(io, &buf); - comp.writeDepFile(binfile, &af.file_writer.interface) catch return af.file_writer.err.?; - - try af.finish(); + comp.writeDepFile(bin_file, &file_writer.interface) catch |err| switch (err) { + error.WriteFailed => return file_writer.err.?, + }; + try file_writer.flush(); + try af.replace(io); } fn writeDepFile( comp: *Compilation, - binfile: Cache.Path, + bin_file: Cache.Path, w: *std.Io.Writer, ) std.Io.Writer.Error!void { const prefixes = comp.cache_parent.prefixes(); const fsi = comp.file_system_inputs.?.items; - try w.print("{f}:", .{binfile}); + try w.print("{f}:", .{bin_file}); { var it = std.mem.splitScalar(u8, fsi, 0); diff --git a/src/fmt.zig b/src/fmt.zig index b1903aad53..a3cd7c8d74 100644 --- a/src/fmt.zig +++ b/src/fmt.zig @@ -355,11 +355,11 @@ fn fmtPathFile( try fmt.stdout_writer.interface.print("{s}\n", .{file_path}); fmt.any_error = true; } else { - var af = try dir.atomicFile(io, sub_path, .{ .permissions = stat.permissions, .write_buffer = &.{} }); - defer af.deinit(); + var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true }); + defer af.deinit(io); - try af.file_writer.interface.writeAll(fmt.out_buffer.written()); - try af.finish(); + try af.file.writeStreamingAll(io, fmt.out_buffer.written()); + try af.replace(io); try fmt.stdout_writer.interface.print("{s}\n", .{file_path}); } }