diff --git a/lib/std/c/darwin.zig b/lib/std/c/darwin.zig index ed1ddb7d91b7..93efdd061cfb 100644 --- a/lib/std/c/darwin.zig +++ b/lib/std/c/darwin.zig @@ -18,6 +18,14 @@ pub extern "c" fn _dyld_get_image_header(image_index: u32) ?*mach_header; pub extern "c" fn _dyld_get_image_vmaddr_slide(image_index: u32) usize; pub extern "c" fn _dyld_get_image_name(image_index: u32) [*:0]const u8; +pub const COPYFILE_ACL = 1 << 0; +pub const COPYFILE_STAT = 1 << 1; +pub const COPYFILE_XATTR = 1 << 2; +pub const COPYFILE_DATA = 1 << 3; + +pub const copyfile_state_t = *@Type(.Opaque); +pub extern "c" fn fcopyfile(from: fd_t, to: fd_t, state: ?copyfile_state_t, flags: u32) c_int; + pub extern "c" fn @"realpath$DARWIN_EXTSN"(noalias file_name: [*:0]const u8, noalias resolved_name: [*]u8) ?[*:0]u8; pub extern "c" fn __getdirentries64(fd: c_int, buf_ptr: [*]u8, buf_len: usize, basep: *i64) isize; diff --git a/lib/std/fs.zig b/lib/std/fs.zig index 1890d7e1360c..5f49bb376682 100644 --- a/lib/std/fs.zig +++ b/lib/std/fs.zig @@ -1823,7 +1823,7 @@ pub const Dir = struct { var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode }); defer atomic_file.deinit(); - try atomic_file.file.writeFileAll(in_file, .{ .in_len = size }); + try copy_file(in_file.handle, atomic_file.file.handle); return atomic_file.finish(); } @@ -2263,6 +2263,53 @@ pub fn realpathAlloc(allocator: *Allocator, pathname: []const u8) ![]u8 { return allocator.dupe(u8, try os.realpath(pathname, &buf)); } +const CopyFileError = error{SystemResources} || os.CopyFileRangeError || os.SendFileError; + +// Transfer all the data between two file descriptors in the most efficient way. +// The copy starts at offset 0, the initial offsets are preserved. +// No metadata is transferred over. +fn copy_file(fd_in: os.fd_t, fd_out: os.fd_t) CopyFileError!void { + if (comptime std.Target.current.isDarwin()) { + const rc = os.system.fcopyfile(fd_in, fd_out, null, os.system.COPYFILE_DATA); + switch (os.errno(rc)) { + 0 => return, + os.EINVAL => unreachable, + os.ENOMEM => return error.SystemResources, + // The source file is not a directory, symbolic link, or regular file. + // Try with the fallback path before giving up. + os.ENOTSUP => {}, + else => |err| return os.unexpectedErrno(err), + } + } + + if (std.Target.current.os.tag == .linux) { + // Try copy_file_range first as that works at the FS level and is the + // most efficient method (if available). + var offset: u64 = 0; + cfr_loop: while (true) { + // The kernel checks the u64 value `offset+count` for overflow, use + // a 32 bit value so that the syscall won't return EINVAL except for + // impossibly large files (> 2^64-1 - 2^32-1). + const amt = try os.copy_file_range(fd_in, offset, fd_out, offset, math.maxInt(u32), 0); + // Terminate when no data was copied + if (amt == 0) break :cfr_loop; + offset += amt; + } + return; + } + + // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the + // fallback code will copy the contents chunk by chunk. + const empty_iovec = [0]os.iovec_const{}; + var offset: u64 = 0; + sendfile_loop: while (true) { + const amt = try os.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0); + // Terminate when no data was copied + if (amt == 0) break :sendfile_loop; + offset += amt; + } +} + test "" { if (builtin.os.tag != .wasi) { _ = makeDirAbsolute; diff --git a/lib/std/os.zig b/lib/std/os.zig index c06ce4ed005b..6e254518798d 100644 --- a/lib/std/os.zig +++ b/lib/std/os.zig @@ -4945,6 +4945,9 @@ pub fn sendfile( pub const CopyFileRangeError = error{ FileTooBig, InputOutput, + /// `fd_in` is not open for reading; or `fd_out` is not open for writing; + /// or the `O_APPEND` flag is set for `fd_out`. + FilesOpenedWithWrongFlags, IsDir, OutOfMemory, NoSpaceLeft, @@ -4953,6 +4956,11 @@ pub const CopyFileRangeError = error{ FileBusy, } || PReadError || PWriteError || UnexpectedError; +var has_copy_file_range_syscall = init: { + const kernel_has_syscall = std.Target.current.os.isAtLeast(.linux, .{ .major = 4, .minor = 5 }) orelse true; + break :init std.atomic.Int(bool).init(kernel_has_syscall); +}; + /// Transfer data between file descriptors at specified offsets. /// Returns the number of bytes written, which can less than requested. /// @@ -4981,22 +4989,18 @@ pub const CopyFileRangeError = error{ pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len: usize, flags: u32) CopyFileRangeError!usize { const use_c = std.c.versionCheck(.{ .major = 2, .minor = 27, .patch = 0 }).ok; - // TODO support for other systems than linux - const try_syscall = comptime std.Target.current.os.isAtLeast(.linux, .{ .major = 4, .minor = 5 }) != false; - - if (use_c or try_syscall) { + if (std.Target.current.os.tag == .linux and + (use_c or has_copy_file_range_syscall.get())) + { const sys = if (use_c) std.c else linux; var off_in_copy = @bitCast(i64, off_in); var off_out_copy = @bitCast(i64, off_out); const rc = sys.copy_file_range(fd_in, &off_in_copy, fd_out, &off_out_copy, len, flags); - - // TODO avoid wasting a syscall every time if kernel is too old and returns ENOSYS https://github.com/ziglang/zig/issues/1018 - switch (sys.getErrno(rc)) { 0 => return @intCast(usize, rc), - EBADF => unreachable, + EBADF => return error.FilesOpenedWithWrongFlags, EFBIG => return error.FileTooBig, EIO => return error.InputOutput, EISDIR => return error.IsDir, @@ -5005,9 +5009,14 @@ pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len EOVERFLOW => return error.Unseekable, EPERM => return error.PermissionDenied, ETXTBSY => return error.FileBusy, - EINVAL => {}, // these may not be regular files, try fallback - EXDEV => {}, // support for cross-filesystem copy added in Linux 5.3, use fallback - ENOSYS => {}, // syscall added in Linux 4.5, use fallback + // these may not be regular files, try fallback + EINVAL => {}, + // support for cross-filesystem copy added in Linux 5.3, use fallback + EXDEV => {}, + // syscall added in Linux 4.5, use fallback + ENOSYS => { + has_copy_file_range_syscall.set(false); + }, else => |err| return unexpectedErrno(err), } }