commit 4365b0df88a300a48abdfcd5534acdb048b465ba (tree)
parent 81bfd289745c661ff5b96b42470f47492091c389
Author: Andrew Kelley <andrew@ziglang.org>
Date: Sun, 4 Jan 2026 21:35:41 -0800
std.Io.Threaded: add File.hardLink
Diffstat:
2 files changed, 66 insertions(+), 45 deletions(-)
diff --git a/lib/std/Io/Threaded.zig b/lib/std/Io/Threaded.zig
@@ -1446,6 +1446,7 @@ pub fn io(t: *Threaded) Io {
.fileUnlock = fileUnlock,
.fileDowngradeLock = fileDowngradeLock,
.fileRealPath = fileRealPath,
+ .fileHardLink = fileHardLink,
.processExecutableOpen = processExecutableOpen,
.processExecutablePath = processExecutablePath,
@@ -1593,6 +1594,7 @@ pub fn ioBasic(t: *Threaded) Io {
.fileUnlock = fileUnlock,
.fileDowngradeLock = fileDowngradeLock,
.fileRealPath = fileRealPath,
+ .fileHardLink = fileHardLink,
.processExecutableOpen = processExecutableOpen,
.processExecutablePath = processExecutablePath,
@@ -5144,6 +5146,65 @@ 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 (is_windows) return error.OperationUnsupported;
+ if (native_os == .wasi and !builtin.link_libc) @panic("TODO");
+
+ 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,
@@ -7560,46 +7621,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 {
diff --git 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);