diff --git a/lib/std/Io/Dir.zig b/lib/std/Io/Dir.zig index a5bce6c76fdcf848b4f63e0d9100454804f5bb3f..3aec198dd8bc0f02505a647e4cd17c895d90c6e9 100644 --- a/lib/std/Io/Dir.zig +++ b/lib/std/Io/Dir.zig @@ -1122,8 +1122,7 @@ pub const RenamePreserveError = error{ /// policy of the current thread or process disallows dataless directory materialization. AccessDenied, PathAlreadyExists, - /// Operating system or file system does not support atomic nonreplacing - /// rename. + /// Operating system or file system does not support atomic nonreplacing rename. OperationUnsupported, } || RenameError; @@ -1131,6 +1130,12 @@ pub const RenamePreserveError = error{ /// /// If `new_sub_path` already exists, `error.PathAlreadyExists` will be returned. /// +/// If the operating system supports it, the entire operation will be done atomically. If unsupported, will +/// fall back to hard link followed by unlink, which is atomic with respect to `new_sub_path` but leaves a +/// window where the cleanup of `old_sub_path may fail. Although the operation is fairly portable, technically +/// this fallback, too, may be unsupported in some environments, in which case `error.OperationUnsupported` is +/// returned. +/// /// * 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. diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig index 1e42814e0cd9d41afbb651cf6b1bc7623fd87163..5df6c2f58541eee78fccf5d14a749f3dd933daa8 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7734,7 +7734,14 @@ fn dirRenamePreserve( const t: *Threaded = @ptrCast(@alignCast(userdata)); if (is_windows) return dirRenameWindowsInner(old_dir, old_sub_path, new_dir, new_sub_path, false); if (is_darwin) return dirRenamePreserveDarwin(old_dir, old_sub_path, new_dir, new_sub_path); - if (native_os == .linux) return dirRenamePreserveLinux(old_dir, old_sub_path, new_dir, new_sub_path); + if (native_os == .linux) { + if (dirRenamePreserveLinux(old_dir, old_sub_path, new_dir, new_sub_path)) |_| { + return; + } else |err| switch (err) { + else => |e| return e, + error.OperationUnsupported => {}, // Fall back to link/unlink below. + } + } // 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); @@ -8062,8 +8069,10 @@ fn dirRenamePreserveLinux( .ROFS => return syscall.fail(error.ReadOnlyFileSystem), .XDEV => return syscall.fail(error.CrossDevice), .ILSEQ => return syscall.fail(error.BadPathName), + // EINVAL when passing NOREPLACE=true observed on ext4, 7.0.0-38-generic #38~24.04.4-Ubuntu. + // OPNOTSUPP handled defensively. + .INVAL, .OPNOTSUPP => return syscall.fail(error.OperationUnsupported), .FAULT => |err| return syscall.errnoBug(err), - .INVAL => return syscall.fail(error.OperationUnsupported), else => |err| return syscall.unexpectedErrno(err), }; }