From cdd6e545cc7e4c6cb32d1a87695f215ce9d5f753 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Mon, 12 Oct 2026 13:30:02 -0700 Subject: [PATCH] std.Io.Threaded: handle unsupported renamePreserveLinux with link/unlink fallback. closes #31960 --- lib/std/Io/Dir.zig | 9 +++++++-- lib/std/Io/Threaded.zig | 13 +++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) 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 79cc01c21d16bdbd6308b74740227731733cd3c3..c777c258127e4d72f38ff461f24c415d74b6e99b 100644 --- a/lib/std/Io/Threaded.zig +++ b/lib/std/Io/Threaded.zig @@ -7756,7 +7756,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); @@ -8084,8 +8091,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), }; } -- 2.54.0