diff --git a/lib/std/Io.zig b/lib/std/Io.zig index 1afef86601..5796ee7eb0 100644 --- a/lib/std/Io.zig +++ b/lib/std/Io.zig @@ -676,6 +676,7 @@ pub const VTable = struct { dirDeleteFile: *const fn (?*anyopaque, Dir, []const u8) Dir.DeleteFileError!void, dirDeleteDir: *const fn (?*anyopaque, Dir, []const u8) Dir.DeleteDirError!void, dirRename: *const fn (?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) Dir.RenameError!void, + dirRenamePreserve: *const fn (?*anyopaque, old_dir: Dir, old_sub_path: []const u8, new_dir: Dir, new_sub_path: []const u8) Dir.RenamePreserveError!void, dirSymLink: *const fn (?*anyopaque, Dir, target_path: []const u8, sym_link_path: []const u8, Dir.SymLinkFlags) Dir.SymLinkError!void, dirReadLink: *const fn (?*anyopaque, Dir, sub_path: []const u8, buffer: []u8) Dir.ReadLinkError!usize, dirSetOwner: *const fn (?*anyopaque, Dir, ?File.Uid, ?File.Gid) Dir.SetOwnerError!void, diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index e2f2440c8a..67d1ec849c 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -936,10 +936,9 @@ pub fn deleteDirAbsolute(io: Io, absolute_path: []const u8) DeleteDirError!void pub const RenameError = error{ /// In WASI, this error may occur when the file descriptor does /// not hold the required rights to rename a resource by path relative to it. - /// - /// On Windows, this error may be returned instead of PathAlreadyExists when - /// renaming a directory over an existing directory. AccessDenied, + /// Attempted to replace a nonempty directory. + DirNotEmpty, PermissionDenied, FileBusy, DiskQuota, @@ -950,9 +949,8 @@ pub const RenameError = error{ NotDir, SystemResources, NoSpaceLeft, - PathAlreadyExists, ReadOnlyFileSystem, - RenameAcrossMountPoints, + CrossDevice, NoDevice, SharingViolation, PipeBusy, @@ -964,6 +962,7 @@ pub const RenameError = error{ /// intercepts file system operations and makes them significantly slower /// in addition to possibly failing with this error code. AntivirusInterference, + HardwareFailure, } || PathNameError || Io.Cancelable || Io.UnexpectedError; /// Change the name or location of a file or directory. @@ -973,9 +972,9 @@ pub const RenameError = error{ /// Renaming a file over an existing directory or a directory over an existing /// file will fail with `error.IsDir` or `error.NotDir` /// -/// On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). -/// On WASI, both paths should be encoded as valid UTF-8. -/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding. +/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// * On WASI, both paths should be encoded as valid UTF-8. +/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding. pub fn rename( old_dir: Dir, old_sub_path: []const u8, @@ -993,6 +992,39 @@ 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 RenamePreserveError = error{ + /// In WASI, this error may occur when the file descriptor does + /// not hold the required rights to rename a resource by path relative to it. + /// + /// On Windows, this error may be returned instead of PathAlreadyExists when + /// renaming a directory over an existing directory. + AccessDenied, + PathAlreadyExists, + /// Operating system or file system does not support atomic nonreplacing + /// rename. + OperationUnsupported, +} || RenameError; + +/// Change the name or location of a file or directory. +/// +/// If `new_sub_path` already exists, `error.PathAlreadyExists` will be returned. +/// +/// Renaming a file over an existing directory or a directory over an existing +/// file will fail with `error.IsDir` or `error.NotDir` +/// +/// * On Windows, both paths should be encoded as [WTF-8](https://wtf-8.codeberg.page/). +/// * On WASI, both paths should be encoded as valid UTF-8. +/// * On other platforms, both paths are an opaque sequence of bytes with no particular encoding. +pub fn renamePreserve( + old_dir: Dir, + old_sub_path: []const u8, + new_dir: Dir, + new_sub_path: []const u8, + io: Io, +) RenamePreserveError!void { + return io.vtable.dirRenamePreserve(io.userdata, old_dir, old_sub_path, new_dir, new_sub_path); +} + pub const HardLinkOptions = File.HardLinkOptions; pub const HardLinkError = File.HardLinkError; diff --git a/lib/std/Io/File.zig b/lib/std/Io/File.zig index 389c8fb8b1..cf8ca4e638 100644 --- a/lib/std/Io/File.zig +++ b/lib/std/Io/File.zig @@ -726,7 +726,7 @@ pub const HardLinkError = error{ SystemResources, NoSpaceLeft, ReadOnlyFileSystem, - NotSameFileSystem, + CrossDevice, NotDir, } || Io.Cancelable || Dir.PathNameError || Io.UnexpectedError; diff --git a/lib/std/Io/File/Atomic.zig b/lib/std/Io/File/Atomic.zig index 440430e04c..595aa2b08b 100644 --- a/lib/std/Io/File/Atomic.zig +++ b/lib/std/Io/File/Atomic.zig @@ -37,10 +37,14 @@ pub fn deinit(af: *Atomic, io: Io) void { af.* = undefined; } -pub const LinkError = Dir.HardLinkError; +pub const LinkError = Dir.HardLinkError || Dir.RenamePreserveError; /// Atomically materializes the file into place, failing with /// `error.PathAlreadyExists` if something already exists there. +/// +/// If this operation could not be done with an unnamed temporary file, the +/// named temporary file will be deleted in a following operation, which may +/// independently fail. The result of that operation is stored in `delete_err`. pub fn link(af: *Atomic, io: Io) LinkError!void { if (af.file_exists) { if (af.file_open) { @@ -48,8 +52,7 @@ pub fn link(af: *Atomic, io: Io) LinkError!void { 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 {}; + try af.dir.renamePreserve(&tmp_sub_path, af.dir, af.dest_sub_path, io); af.file_exists = false; } else { assert(af.file_open); diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index a6c3a623da..b8fcbfb70c 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -1442,6 +1442,7 @@ pub fn io(t: *Threaded) Io { .dirDeleteFile = dirDeleteFile, .dirDeleteDir = dirDeleteDir, .dirRename = dirRename, + .dirRenamePreserve = dirRenamePreserve, .dirSymLink = dirSymLink, .dirReadLink = dirReadLink, .dirSetOwner = dirSetOwner, @@ -1593,6 +1594,7 @@ pub fn ioBasic(t: *Threaded) Io { .dirDeleteFile = dirDeleteFile, .dirDeleteDir = dirDeleteDir, .dirRename = dirRename, + .dirRenamePreserve = dirRenamePreserve, .dirSymLink = dirSymLink, .dirReadLink = dirReadLink, .dirSetOwner = dirSetOwner, @@ -5283,7 +5285,7 @@ fn linkat( .NOTDIR => return syscall.fail(error.NotDir), .PERM => return syscall.fail(error.PermissionDenied), .ROFS => return syscall.fail(error.ReadOnlyFileSystem), - .XDEV => return syscall.fail(error.NotSameFileSystem), + .XDEV => return syscall.fail(error.CrossDevice), .ILSEQ => return syscall.fail(error.BadPathName), .FAULT => |err| return syscall.errnoBug(err), .INVAL => |err| return syscall.errnoBug(err), @@ -5692,15 +5694,44 @@ fn dirRenameWindows( new_dir: Dir, new_sub_path: []const u8, ) Dir.RenameError!void { - const w = windows; const t: *Threaded = @ptrCast(@alignCast(userdata)); _ = t; + return dirRenameWindowsInner(old_dir, old_sub_path, new_dir, new_sub_path, true) catch |err| switch (err) { + error.PathAlreadyExists => return error.Unexpected, + error.OperationUnsupported => return error.Unexpected, + else => |e| return e, + }; +} +fn dirRenamePreserve( + userdata: ?*anyopaque, + old_dir: Dir, + old_sub_path: []const u8, + new_dir: Dir, + new_sub_path: []const u8, +) Dir.RenamePreserveError!void { + const t: *Threaded = @ptrCast(@alignCast(userdata)); + if (is_windows) return dirRenameWindowsInner(old_dir, old_sub_path, new_dir, new_sub_path, false); + if (native_os == .linux) return dirRenamePreserveLinux(old_dir, old_sub_path, new_dir, new_sub_path); + // Make a hard link then delete the original. + try dirHardLink(t, old_dir, old_sub_path, new_dir, new_sub_path, .{ .follow_symlinks = false }); + const prev = swapCancelProtection(t, .blocked); + defer _ = swapCancelProtection(t, prev); + dirDeleteFile(t, old_dir, old_sub_path) catch {}; +} + +fn dirRenameWindowsInner( + old_dir: Dir, + old_sub_path: []const u8, + new_dir: Dir, + new_sub_path: []const u8, + replace_if_exists: bool, +) Dir.RenamePreserveError!void { + const w = windows; const old_path_w_buf = try windows.sliceToPrefixedFileW(old_dir.handle, old_sub_path); const old_path_w = old_path_w_buf.span(); const new_path_w_buf = try windows.sliceToPrefixedFileW(new_dir.handle, new_sub_path); const new_path_w = new_path_w_buf.span(); - const replace_if_exists = true; const src_fd = src_fd: { const syscall: Syscall = try .start(); @@ -5802,9 +5833,9 @@ fn dirRenameWindows( .ACCESS_DENIED => return error.AccessDenied, .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_SAME_DEVICE => return error.RenameAcrossMountPoints, + .NOT_SAME_DEVICE => return error.CrossDevice, .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, - .DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists, + .DIRECTORY_NOT_EMPTY => return error.DirNotEmpty, .FILE_IS_A_DIRECTORY => return error.IsDir, .NOT_A_DIRECTORY => return error.NotDir, else => return w.unexpectedStatus(rc), @@ -5848,10 +5879,10 @@ fn dirRenameWasi( .NOTDIR => return error.NotDir, .NOMEM => return error.SystemResources, .NOSPC => return error.NoSpaceLeft, - .EXIST => return error.PathAlreadyExists, - .NOTEMPTY => return error.PathAlreadyExists, + .EXIST => return error.DirNotEmpty, + .NOTEMPTY => return error.DirNotEmpty, .ROFS => return error.ReadOnlyFileSystem, - .XDEV => return error.RenameAcrossMountPoints, + .XDEV => return error.CrossDevice, .NOTCAPABLE => return error.AccessDenied, .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), @@ -5877,9 +5908,105 @@ fn dirRenamePosix( const old_sub_path_posix = try pathToPosix(old_sub_path, &old_path_buffer); const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer); + return renameat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix); +} + +fn dirRenamePreserveLinux( + old_dir: Dir, + old_sub_path: []const u8, + new_dir: Dir, + new_sub_path: []const u8, +) Dir.RenamePreserveError!void { + const linux = std.os.linux; + + var old_path_buffer: [linux.PATH_MAX]u8 = undefined; + var new_path_buffer: [linux.PATH_MAX]u8 = undefined; + + const old_sub_path_posix = try pathToPosix(old_sub_path, &old_path_buffer); + const new_sub_path_posix = try pathToPosix(new_sub_path, &new_path_buffer); + + const syscall: Syscall = try .start(); + while (true) switch (linux.errno(linux.renameat2( + old_dir.handle, + old_sub_path_posix, + new_dir.handle, + new_sub_path_posix, + .{ .NOREPLACE = true }, + ))) { + .SUCCESS => return syscall.finish(), + .INTR => { + try syscall.checkCancel(); + continue; + }, + .ACCES => return syscall.fail(error.AccessDenied), + .PERM => return syscall.fail(error.PermissionDenied), + .BUSY => return syscall.fail(error.FileBusy), + .DQUOT => return syscall.fail(error.DiskQuota), + .ISDIR => return syscall.fail(error.IsDir), + .LOOP => return syscall.fail(error.SymLinkLoop), + .MLINK => return syscall.fail(error.LinkQuotaExceeded), + .NAMETOOLONG => return syscall.fail(error.NameTooLong), + .NOENT => return syscall.fail(error.FileNotFound), + .NOTDIR => return syscall.fail(error.NotDir), + .NOMEM => return syscall.fail(error.SystemResources), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .EXIST => return syscall.fail(error.PathAlreadyExists), + .NOTEMPTY => return syscall.fail(error.DirNotEmpty), + .ROFS => return syscall.fail(error.ReadOnlyFileSystem), + .XDEV => return syscall.fail(error.CrossDevice), + .ILSEQ => return syscall.fail(error.BadPathName), + .FAULT => |err| return syscall.errnoBug(err), + .INVAL => |err| return syscall.errnoBug(err), + else => |err| return syscall.unexpectedErrno(err), + }; +} + +fn renameat( + old_dir: posix.fd_t, + old_sub_path: [*:0]const u8, + new_dir: posix.fd_t, + new_sub_path: [*:0]const u8, +) Dir.RenameError!void { + const syscall: Syscall = try .start(); + while (true) switch (posix.errno(posix.system.renameat(old_dir, old_sub_path, new_dir, new_sub_path))) { + .SUCCESS => return syscall.finish(), + .INTR => { + try syscall.checkCancel(); + continue; + }, + .ACCES => return syscall.fail(error.AccessDenied), + .PERM => return syscall.fail(error.PermissionDenied), + .BUSY => return syscall.fail(error.FileBusy), + .DQUOT => return syscall.fail(error.DiskQuota), + .ISDIR => return syscall.fail(error.IsDir), + .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), + .NOTDIR => return syscall.fail(error.NotDir), + .NOMEM => return syscall.fail(error.SystemResources), + .NOSPC => return syscall.fail(error.NoSpaceLeft), + .EXIST => return syscall.fail(error.DirNotEmpty), + .NOTEMPTY => return syscall.fail(error.DirNotEmpty), + .ROFS => return syscall.fail(error.ReadOnlyFileSystem), + .XDEV => return syscall.fail(error.CrossDevice), + .ILSEQ => return syscall.fail(error.BadPathName), + .FAULT => |err| return syscall.errnoBug(err), + .INVAL => |err| return syscall.errnoBug(err), + else => |err| return syscall.unexpectedErrno(err), + }; +} + +fn renameatPreserve( + old_dir: posix.fd_t, + old_sub_path: [*:0]const u8, + new_dir: posix.fd_t, + new_sub_path: [*:0]const u8, +) Dir.RenameError!void { const syscall: Syscall = try .start(); while (true) { - switch (posix.errno(posix.system.renameat(old_dir.handle, old_sub_path_posix, new_dir.handle, new_sub_path_posix))) { + switch (posix.errno(posix.system.renameat(old_dir, old_sub_path, new_dir, new_sub_path))) { .SUCCESS => return syscall.finish(), .INTR => { try syscall.checkCancel(); @@ -5905,7 +6032,7 @@ fn dirRenamePosix( .EXIST => return error.PathAlreadyExists, .NOTEMPTY => return error.PathAlreadyExists, .ROFS => return error.ReadOnlyFileSystem, - .XDEV => return error.RenameAcrossMountPoints, + .XDEV => return error.CrossDevice, .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), } @@ -7686,7 +7813,7 @@ fn dirHardLink( .NOTDIR => return error.NotDir, .PERM => return error.PermissionDenied, .ROFS => return error.ReadOnlyFileSystem, - .XDEV => return error.NotSameFileSystem, + .XDEV => return error.CrossDevice, .INVAL => |err| return errnoBug(err), .ILSEQ => return error.BadPathName, else => |err| return posix.unexpectedErrno(err), diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig index d7d592ee36..a584fa3477 100644 --- a/lib/std/fs/test.zig +++ b/lib/std/fs/test.zig @@ -1022,8 +1022,7 @@ test "Dir.rename directory onto non-empty dir" { file.close(io); target_dir.close(io); - // Rename should fail with PathAlreadyExists if target_dir is non-empty - try expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, ctx.dir, target_dir_path, io)); + try expectError(error.DirNotEmpty, ctx.dir.rename(test_dir_path, ctx.dir, target_dir_path, io)); // Ensure the directory was not renamed var dir = try ctx.dir.openDir(io, test_dir_path, .{}); diff --git a/lib/std/os/linux.zig b/lib/std/os/linux.zig index 812e07f7e8..92ea207fd0 100644 --- a/lib/std/os/linux.zig +++ b/lib/std/os/linux.zig @@ -501,6 +501,15 @@ pub const O = switch (native_arch) { else => @compileError("missing std.os.linux.O constants for this architecture"), }; +pub const RENAME = packed struct(u32) { + /// Cannot be set together with `EXCHANGE`. + NOREPLACE: bool = false, + /// Cannot be set together with `NOREPLACE`. + EXCHANGE: bool = false, + WHITEOUT: bool = false, + _: u29 = 0, +}; + /// Set by startup code, used by `getauxval`. pub var elf_aux_maybe: ?[*]std.elf.Auxv = null; @@ -1346,9 +1355,22 @@ pub fn rename(old: [*:0]const u8, new: [*:0]const u8) usize { if (@hasField(SYS, "rename")) { return syscall2(.rename, @intFromPtr(old), @intFromPtr(new)); } else if (@hasField(SYS, "renameat")) { - return syscall4(.renameat, @as(usize, @bitCast(@as(isize, AT.FDCWD))), @intFromPtr(old), @as(usize, @bitCast(@as(isize, AT.FDCWD))), @intFromPtr(new)); + return syscall4( + .renameat, + @as(usize, @bitCast(@as(isize, AT.FDCWD))), + @intFromPtr(old), + @as(usize, @bitCast(@as(isize, AT.FDCWD))), + @intFromPtr(new), + ); } else { - return syscall5(.renameat2, @as(usize, @bitCast(@as(isize, AT.FDCWD))), @intFromPtr(old), @as(usize, @bitCast(@as(isize, AT.FDCWD))), @intFromPtr(new), 0); + return syscall5( + .renameat2, + @as(usize, @bitCast(@as(isize, AT.FDCWD))), + @intFromPtr(old), + @as(usize, @bitCast(@as(isize, AT.FDCWD))), + @intFromPtr(new), + 0, + ); } } @@ -1373,14 +1395,14 @@ pub fn renameat(oldfd: i32, oldpath: [*:0]const u8, newfd: i32, newpath: [*:0]co } } -pub fn renameat2(oldfd: i32, oldpath: [*:0]const u8, newfd: i32, newpath: [*:0]const u8, flags: u32) usize { +pub fn renameat2(oldfd: i32, oldpath: [*:0]const u8, newfd: i32, newpath: [*:0]const u8, flags: RENAME) usize { return syscall5( .renameat2, @as(usize, @bitCast(@as(isize, oldfd))), @intFromPtr(oldpath), @as(usize, @bitCast(@as(isize, newfd))), @intFromPtr(newpath), - flags, + @as(u32, @bitCast(flags)), ); } diff --git a/lib/std/os/windows.zig b/lib/std/os/windows.zig index f9768cf94c..52e4bd387d 100644 --- a/lib/std/os/windows.zig +++ b/lib/std/os/windows.zig @@ -3194,7 +3194,7 @@ pub const RenameError = error{ NetworkNotFound, AntivirusInterference, BadPathName, - RenameAcrossMountPoints, + CrossDevice, } || UnexpectedError; pub fn RenameFile( @@ -3295,7 +3295,7 @@ pub fn RenameFile( .ACCESS_DENIED => return error.AccessDenied, .OBJECT_NAME_NOT_FOUND => return error.FileNotFound, .OBJECT_PATH_NOT_FOUND => return error.FileNotFound, - .NOT_SAME_DEVICE => return error.RenameAcrossMountPoints, + .NOT_SAME_DEVICE => return error.CrossDevice, .OBJECT_NAME_COLLISION => return error.PathAlreadyExists, .DIRECTORY_NOT_EMPTY => return error.PathAlreadyExists, .FILE_IS_A_DIRECTORY => return error.IsDir, diff --git a/lib/std/posix.zig b/lib/std/posix.zig index fc59701225..60938818df 100644 --- a/lib/std/posix.zig +++ b/lib/std/posix.zig @@ -1594,7 +1594,7 @@ pub const FanotifyMarkError = error{ NotDir, OperationUnsupported, PermissionDenied, - NotSameFileSystem, + CrossDevice, NameTooLong, } || UnexpectedError; @@ -1634,7 +1634,7 @@ pub fn fanotify_markZ( .NOTDIR => return error.NotDir, .OPNOTSUPP => return error.OperationUnsupported, .PERM => return error.PermissionDenied, - .XDEV => return error.NotSameFileSystem, + .XDEV => return error.CrossDevice, else => |err| return unexpectedErrno(err), } } diff --git a/src/Compilation.zig b/src/Compilation.zig index bfd0193a26..5d369594c8 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -3460,7 +3460,7 @@ fn renameTmpIntoCache( }, else => return error.AccessDenied, }, - error.PathAlreadyExists => { + error.DirNotEmpty => { try cache_directory.handle.deleteTree(io, o_sub_path); continue; }, diff --git a/src/Package/Fetch.zig b/src/Package/Fetch.zig index 5a7948f8e8..d42e441d77 100644 --- a/src/Package/Fetch.zig +++ b/src/Package/Fetch.zig @@ -1476,7 +1476,7 @@ pub fn renameTmpIntoCache(io: Io, cache_dir: Io.Dir, tmp_dir_sub_path: []const u }; continue; }, - error.PathAlreadyExists, error.AccessDenied => { + error.DirNotEmpty, error.AccessDenied => { // Package has been already downloaded and may already be in use on the system. cache_dir.deleteTree(io, tmp_dir_sub_path) catch { // Garbage files leftover in zig-cache/tmp/ is, as they say