authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-06-24 07:48:52-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:30-07:00
log25a0648176ce0e0061a035fdf602795368a3b40e
treee5562574fda02d17c3d095baf6b58232d819f381
parent7926081ec35761028c87cdb82204f177311866be

std: move copy_file_range, fcopyfile impls and usage


6 files changed, 229 insertions(+), 166 deletions(-)

lib/std/fs/Dir.zig+31-71
......@@ -2609,7 +2609,7 @@ pub fn updateFile(
26092609 try dest_dir.makePath(dirname);
26102610 }
26112611
2612 var buffer: [2000]u8 = undefined;
2612 var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
26132613 var atomic_file = try dest_dir.atomicFile(dest_path, .{
26142614 .mode = actual_mode,
26152615 .write_buffer = &buffer,
......@@ -2619,7 +2619,7 @@ pub fn updateFile(
26192619 var src_reader: File.Reader = .initSize(src_file, &.{}, src_stat.size);
26202620 const dest_writer = &atomic_file.file_writer.interface;
26212621
2622 dest_writer.writeFileAll(&src_reader, .{}) catch |err| switch (err) {
2622 _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {
26232623 error.ReadFailed => return src_reader.err.?,
26242624 error.WriteFailed => return atomic_file.file_writer.err.?,
26252625 };
......@@ -2628,16 +2628,22 @@ pub fn updateFile(
26282628 return .stale;
26292629}
26302630
2631pub const CopyFileError = File.OpenError || File.StatError ||
2632 AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError;
2631pub const CopyFileError = File.OpenError || File.StatError || File.ReadError || File.WriteError ||
2632 AtomicFile.InitError || AtomicFile.FinishError;
26332633
2634/// Guaranteed to be atomic.
2635/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available,
2636/// there is a possibility of power loss or application termination leaving temporary files present
2637/// in the same directory as dest_path.
2638/// On Windows, both paths should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
2639/// On WASI, both paths should be encoded as valid UTF-8.
2640/// On other platforms, both paths are an opaque sequence of bytes with no particular encoding.
2634/// Atomically creates a new file at `dest_path` within `dest_dir` with the
2635/// same contents as `source_path` within `source_dir`, overwriting any already
2636/// existing file.
2637///
2638/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and
2639/// readily available, there is a possibility of power loss or application
2640/// termination leaving temporary files present in the same directory as
2641/// dest_path.
2642///
2643/// On Windows, both paths should be encoded as
2644/// [WTF-8](https://simonsapin.github.io/wtf-8/). On WASI, both paths should be
2645/// encoded as valid UTF-8. On other platforms, both paths are an opaque
2646/// sequence of bytes with no particular encoding.
26412647pub fn copyFile(
26422648 source_dir: Dir,
26432649 source_path: []const u8,
......@@ -2645,74 +2651,28 @@ pub fn copyFile(
26452651 dest_path: []const u8,
26462652 options: CopyFileOptions,
26472653) CopyFileError!void {
2648 var in_file = try source_dir.openFile(source_path, .{});
2649 defer in_file.close();
2654 var file_reader: File.Reader = .init(try source_dir.openFile(source_path, .{}), &.{});
2655 defer file_reader.file.close();
26502656
2651 var size: ?u64 = null;
26522657 const mode = options.override_mode orelse blk: {
2653 const st = try in_file.stat();
2654 size = st.size;
2658 const st = try file_reader.file.stat();
2659 file_reader.size = st.size;
26552660 break :blk st.mode;
26562661 };
26572662
2658 var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode });
2663 var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
2664 var atomic_file = try dest_dir.atomicFile(dest_path, .{
2665 .mode = mode,
2666 .write_buffer = &buffer,
2667 });
26592668 defer atomic_file.deinit();
26602669
2661 try copy_file(in_file.handle, atomic_file.file_writer.file.handle, size);
2670 const size = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
2671 error.ReadFailed => return file_reader.err.?,
2672 error.WriteFailed => return atomic_file.file_writer.err.?,
2673 };
26622674 try atomic_file.finish();
2663}
2664
2665const CopyFileRawError = error{SystemResources} || posix.CopyFileRangeError || posix.SendFileError;
2666
2667// Transfer all the data between two file descriptors in the most efficient way.
2668// The copy starts at offset 0, the initial offsets are preserved.
2669// No metadata is transferred over.
2670fn copy_file(fd_in: posix.fd_t, fd_out: posix.fd_t, maybe_size: ?u64) CopyFileRawError!void {
2671 if (builtin.target.os.tag.isDarwin()) {
2672 const rc = posix.system.fcopyfile(fd_in, fd_out, null, .{ .DATA = true });
2673 switch (posix.errno(rc)) {
2674 .SUCCESS => return,
2675 .INVAL => unreachable,
2676 .NOMEM => return error.SystemResources,
2677 // The source file is not a directory, symbolic link, or regular file.
2678 // Try with the fallback path before giving up.
2679 .OPNOTSUPP => {},
2680 else => |err| return posix.unexpectedErrno(err),
2681 }
2682 }
2683
2684 if (native_os == .linux) {
2685 // Try copy_file_range first as that works at the FS level and is the
2686 // most efficient method (if available).
2687 var offset: u64 = 0;
2688 cfr_loop: while (true) {
2689 // The kernel checks the u64 value `offset+count` for overflow, use
2690 // a 32 bit value so that the syscall won't return EINVAL except for
2691 // impossibly large files (> 2^64-1 - 2^32-1).
2692 const amt = try posix.copy_file_range(fd_in, offset, fd_out, offset, std.math.maxInt(u32), 0);
2693 // Terminate as soon as we have copied size bytes or no bytes
2694 if (maybe_size) |s| {
2695 if (s == amt) break :cfr_loop;
2696 }
2697 if (amt == 0) break :cfr_loop;
2698 offset += amt;
2699 }
2700 return;
2701 }
2702
2703 // Sendfile is a zero-copy mechanism iff the OS supports it, otherwise the
2704 // fallback code will copy the contents chunk by chunk.
2705 const empty_iovec = [0]posix.iovec_const{};
2706 var offset: u64 = 0;
2707 sendfile_loop: while (true) {
2708 const amt = try posix.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0);
2709 // Terminate as soon as we have copied size bytes or no bytes
2710 if (maybe_size) |s| {
2711 if (s == amt) break :sendfile_loop;
2712 }
2713 if (amt == 0) break :sendfile_loop;
2714 offset += amt;
2715 }
2675 _ = size;
27162676}
27172677
27182678pub const AtomicFileOptions = struct {
lib/std/fs/File.zig+82-4
......@@ -961,7 +961,7 @@ pub const Reader = struct {
961961 };
962962 }
963963
964 pub fn initSize(file: File, buffer: []u8, size: u64) Reader {
964 pub fn initSize(file: File, buffer: []u8, size: ?u64) Reader {
965965 return .{
966966 .file = file,
967967 .interface = initInterface(buffer),
......@@ -1099,7 +1099,6 @@ pub const Reader = struct {
10991099 var iovecs_buffer: [max_buffers_len]posix.iovec = undefined;
11001100 const dest = try w.writableVectorPosix(&iovecs_buffer, limit);
11011101 assert(dest[0].len > 0);
1102 // TODO also add buffer at the end
11031102 const n = posix.readv(r.file.handle, dest) catch |err| {
11041103 r.err = err;
11051104 return error.ReadFailed;
......@@ -1251,6 +1250,8 @@ pub const Writer = struct {
12511250 mode: Writer.Mode = .positional,
12521251 pos: u64 = 0,
12531252 sendfile_err: ?SendfileError = null,
1253 copy_file_range_err: ?CopyFileRangeError = null,
1254 fcopyfile_err: ?FcopyfileError = null,
12541255 seek_err: ?SeekError = null,
12551256 interface: std.io.Writer,
12561257
......@@ -1265,6 +1266,14 @@ pub const Writer = struct {
12651266 Unexpected,
12661267 };
12671268
1269 pub const CopyFileRangeError = std.os.freebsd.CopyFileRangeError || std.os.linux.wrapped.CopyFileRangeError;
1270
1271 pub const FcopyfileError = error{
1272 OperationNotSupported,
1273 OutOfMemory,
1274 Unexpected,
1275 };
1276
12681277 /// Number of slices to store on the stack, when trying to send as many byte
12691278 /// vectors through the underlying write calls as possible.
12701279 const max_buffers_len = 16;
......@@ -1408,7 +1417,6 @@ pub const Writer = struct {
14081417 const w: *Writer = @fieldParentPtr("interface", io_writer);
14091418 const out_fd = w.file.handle;
14101419 const in_fd = file_reader.file.handle;
1411 // TODO try using copy_file_range on Linux
14121420 // TODO try using copy_file_range on FreeBSD
14131421 // TODO try using sendfile on macOS
14141422 // TODO try using sendfile on FreeBSD
......@@ -1416,7 +1424,8 @@ pub const Writer = struct {
14161424 // Try using sendfile on Linux.
14171425 if (w.sendfile_err != null) break :sf;
14181426 // Linux sendfile does not support headers.
1419 if (io_writer.end != 0) return drain(io_writer, &.{""}, 1);
1427 const buffered = limit.slice(file_reader.interface.buffer);
1428 if (io_writer.end != 0 or buffered.len != 0) return drain(io_writer, &.{buffered}, 1);
14201429 const max_count = 0x7ffff000; // Avoid EINVAL.
14211430 var off: std.os.linux.off_t = undefined;
14221431 const off_ptr: ?*std.os.linux.off_t, const count: usize = switch (file_reader.mode) {
......@@ -1455,6 +1464,75 @@ pub const Writer = struct {
14551464 w.pos += n;
14561465 return n;
14571466 }
1467 const copy_file_range_fn = switch (native_os) {
1468 .freebsd => std.os.freebsd.copy_file_range,
1469 .linux => if (std.c.versionCheck(.{ .major = 2, .minor = 27, .patch = 0 })) std.os.linux.wrapped.copy_file_range else null,
1470 else => null,
1471 };
1472 if (copy_file_range_fn) |copy_file_range| cfr: {
1473 if (w.copy_file_range_err != null) break :cfr;
1474 const buffered = limit.slice(file_reader.interface.buffer);
1475 if (io_writer.end != 0 or buffered.len != 0) return drain(io_writer, &.{buffered}, 1);
1476 var off_in: i64 = undefined;
1477 var off_out: i64 = undefined;
1478 const off_in_ptr: ?*i64 = switch (file_reader.mode) {
1479 .positional_reading, .streaming_reading => return error.Unimplemented,
1480 .positional => p: {
1481 off_in = file_reader.pos;
1482 break :p &off_in;
1483 },
1484 .streaming => null,
1485 .failure => return error.WriteFailed,
1486 };
1487 const off_out_ptr: ?*i64 = switch (w.mode) {
1488 .positional_reading, .streaming_reading => return error.Unimplemented,
1489 .positional => p: {
1490 off_out = w.pos;
1491 break :p &off_out;
1492 },
1493 .streaming => null,
1494 .failure => return error.WriteFailed,
1495 };
1496 const n = copy_file_range(in_fd, off_in_ptr, out_fd, off_out_ptr, @intFromEnum(limit), 0) catch |err| {
1497 w.copy_file_range_err = err;
1498 return 0;
1499 };
1500 file_reader.pos += n;
1501 w.pos += n;
1502 return n;
1503 }
1504
1505 if (builtin.os.tag.isDarwin()) fcf: {
1506 if (w.fcopyfile_err != null) break :fcf;
1507 if (file_reader.pos != 0) break :fcf;
1508 if (w.pos != 0) break :fcf;
1509 if (limit != .unlimited) break :fcf;
1510 const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
1511 switch (posix.errno(rc)) {
1512 .SUCCESS => {},
1513 .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
1514 w.fcopyfile_err = error.Unexpected;
1515 return 0;
1516 },
1517 .NOMEM => {
1518 w.fcopyfile_err = error.OutOfMemory;
1519 return 0;
1520 },
1521 .OPNOTSUPP => {
1522 w.fcopyfile_err = error.OperationNotSupported;
1523 return 0;
1524 },
1525 else => |err| {
1526 w.fcopyfile_err = posix.unexpectedErrno(err);
1527 return 0;
1528 },
1529 }
1530 const n = if (file_reader.size) |size| size else @panic("TODO figure out how much copied");
1531 file_reader.pos = n;
1532 w.pos = n;
1533 return n;
1534 }
1535
14581536 return error.Unimplemented;
14591537 }
14601538
lib/std/os.zig+1
......@@ -31,6 +31,7 @@ pub const uefi = @import("os/uefi.zig");
3131pub const wasi = @import("os/wasi.zig");
3232pub const emscripten = @import("os/emscripten.zig");
3333pub const windows = @import("os/windows.zig");
34pub const freebsd = @import("os/freebsd.zig");
3435
3536test {
3637 _ = linux;
lib/std/os/freebsd.zig created+49
......@@ -0,0 +1,49 @@
1const std = @import("../std.zig");
2const fd_t = std.c.fd_t;
3const off_t = std.c.off_t;
4const unexpectedErrno = std.posix.unexpectedErrno;
5const errno = std.posix.errno;
6
7pub const CopyFileRangeError = error{
8 /// If infd is not open for reading or outfd is not open for writing, or
9 /// opened for writing with O_APPEND, or if infd and outfd refer to the
10 /// same file.
11 BadFileFlags,
12 /// If the copy exceeds the process's file size limit or the maximum
13 /// file size for the file system outfd re- sides on.
14 FileTooBig,
15 /// A signal interrupted the system call before it could be completed.
16 /// This may happen for files on some NFS mounts. When this happens,
17 /// the values pointed to by inoffp and outoffp are reset to the
18 /// initial values for the system call.
19 Interrupted,
20 /// One of:
21 /// * infd and outfd refer to the same file and the byte ranges overlap.
22 /// * The flags argument is not zero.
23 /// * Either infd or outfd refers to a file object that is not a regular file.
24 InvalidArguments,
25 /// An I/O error occurred while reading/writing the files.
26 InputOutput,
27 /// Corrupted data was detected while reading from a file system.
28 CorruptedData,
29 /// Either infd or outfd refers to a directory.
30 IsDir,
31 /// File system that stores outfd is full.
32 NoSpaceLeft,
33};
34
35pub fn copy_file_range(fd_in: fd_t, off_in: ?*i64, fd_out: fd_t, off_out: ?*i64, len: usize, flags: u32) CopyFileRangeError!usize {
36 const rc = std.c.copy_file_range(fd_in, off_in, fd_out, off_out, len, flags);
37 switch (errno(rc)) {
38 .SUCCESS => return @intCast(rc),
39 .BADF => return error.BadFileFlags,
40 .FBIG => return error.FileTooBig,
41 .INTR => return error.Interrupted,
42 .INVAL => return error.InvalidArguments,
43 .IO => return error.InputOutput,
44 .INTEGRITY => return error.CorruptedData,
45 .ISDIR => return error.IsDir,
46 .NOSPC => return error.NoSpaceLeft,
47 else => |err| return unexpectedErrno(err),
48 }
49}
lib/std/os/linux.zig+66-1
......@@ -9453,7 +9453,7 @@ pub const wrapped = struct {
94539453 const sendfileSymbol = if (lfs64_abi) system.sendfile64 else system.sendfile;
94549454 const rc = sendfileSymbol(out_fd, in_fd, in_offset, adjusted_len);
94559455 switch (errno(rc)) {
9456 .SUCCESS => return @bitCast(rc),
9456 .SUCCESS => return @intCast(rc),
94579457 .BADF => return invalidApiUsage(), // Always a race condition.
94589458 .FAULT => return invalidApiUsage(), // Segmentation fault.
94599459 .OVERFLOW => return unexpectedErrno(.OVERFLOW), // We avoid passing too large of a `count`.
......@@ -9469,6 +9469,71 @@ pub const wrapped = struct {
94699469 }
94709470 }
94719471
9472 pub const CopyFileRangeError = std.posix.UnexpectedError || error{
9473 /// One of:
9474 /// * One or more file descriptors are not valid.
9475 /// * fd_in is not open for reading; or fd_out is not open for writing.
9476 /// * The O_APPEND flag is set for the open file description referred
9477 /// to by the file descriptor fd_out.
9478 BadFileFlags,
9479 /// One of:
9480 /// * An attempt was made to write at a position past the maximum file
9481 /// offset the kernel supports.
9482 /// * An attempt was made to write a range that exceeds the allowed
9483 /// maximum file size. The maximum file size differs between
9484 /// filesystem implementations and can be different from the maximum
9485 /// allowed file offset.
9486 /// * An attempt was made to write beyond the process's file size
9487 /// resource limit. This may also result in the process receiving a
9488 /// SIGXFSZ signal.
9489 FileTooBig,
9490 /// One of:
9491 /// * either fd_in or fd_out is not a regular file
9492 /// * flags argument is not zero
9493 /// * fd_in and fd_out refer to the same file and the source and target ranges overlap.
9494 InvalidArguments,
9495 /// A low-level I/O error occurred while copying.
9496 InputOutput,
9497 /// Either fd_in or fd_out refers to a directory.
9498 IsDir,
9499 OutOfMemory,
9500 /// There is not enough space on the target filesystem to complete the copy.
9501 NoSpaceLeft,
9502 /// (since Linux 5.19) the filesystem does not support this operation.
9503 OperationNotSupported,
9504 /// The requested source or destination range is too large to represent
9505 /// in the specified data types.
9506 Overflow,
9507 /// fd_out refers to an immutable file.
9508 PermissionDenied,
9509 /// Either fd_in or fd_out refers to an active swap file.
9510 SwapFile,
9511 /// The files referred to by fd_in and fd_out are not on the same
9512 /// filesystem, and the source and target filesystems are not of the
9513 /// same type, or do not support cross-filesystem copy.
9514 NotSameFileSystem,
9515 };
9516
9517 pub fn copy_file_range(fd_in: fd_t, off_in: ?*i64, fd_out: fd_t, off_out: ?*i64, len: usize, flags: u32) CopyFileRangeError!usize {
9518 const rc = system.copy_file_range(fd_in, off_in, fd_out, off_out, len, flags);
9519 switch (errno(rc)) {
9520 .SUCCESS => return @intCast(rc),
9521 .BADF => return error.BadFileFlags,
9522 .FBIG => return error.FileTooBig,
9523 .INVAL => return error.InvalidArguments,
9524 .IO => return error.InputOutput,
9525 .ISDIR => return error.IsDir,
9526 .NOMEM => return error.OutOfMemory,
9527 .NOSPC => return error.NoSpaceLeft,
9528 .OPNOTSUPP => return error.OperationNotSupported,
9529 .OVERFLOW => return error.Overflow,
9530 .PERM => return error.PermissionDenied,
9531 .TXTBSY => return error.SwapFile,
9532 .XDEV => return error.NotSameFileSystem,
9533 else => |err| return unexpectedErrno(err),
9534 }
9535 }
9536
94729537 const unexpectedErrno = std.posix.unexpectedErrno;
94739538
94749539 fn invalidApiUsage() error{Unexpected} {
lib/std/posix.zig-90
......@@ -6601,96 +6601,6 @@ fn count_iovec_bytes(iovs: []const iovec_const) usize {
66016601 return count;
66026602}
66036603
6604pub const CopyFileRangeError = error{
6605 FileTooBig,
6606 InputOutput,
6607 /// `fd_in` is not open for reading; or `fd_out` is not open for writing;
6608 /// or the `APPEND` flag is set for `fd_out`.
6609 FilesOpenedWithWrongFlags,
6610 IsDir,
6611 OutOfMemory,
6612 NoSpaceLeft,
6613 Unseekable,
6614 PermissionDenied,
6615 SwapFile,
6616 CorruptedData,
6617} || PReadError || PWriteError || UnexpectedError;
6618
6619/// Transfer data between file descriptors at specified offsets.
6620///
6621/// Returns the number of bytes written, which can less than requested.
6622///
6623/// The `copy_file_range` call copies `len` bytes from one file descriptor to another. When possible,
6624/// this is done within the operating system kernel, which can provide better performance
6625/// characteristics than transferring data from kernel to user space and back, such as with
6626/// `pread` and `pwrite` calls.
6627///
6628/// `fd_in` must be a file descriptor opened for reading, and `fd_out` must be a file descriptor
6629/// opened for writing. They may be any kind of file descriptor; however, if `fd_in` is not a regular
6630/// file system file, it may cause this function to fall back to calling `pread` and `pwrite`, in which case
6631/// atomicity guarantees no longer apply.
6632///
6633/// If `fd_in` and `fd_out` are the same, source and target ranges must not overlap.
6634/// The file descriptor seek positions are ignored and not updated.
6635/// When `off_in` is past the end of the input file, it successfully reads 0 bytes.
6636///
6637/// `flags` has different meanings per operating system; refer to the respective man pages.
6638///
6639/// These systems support in-kernel data copying:
6640/// * Linux (cross-filesystem from version 5.3)
6641/// * FreeBSD 13.0
6642///
6643/// Other systems fall back to calling `pread` / `pwrite`.
6644///
6645/// Maximum offsets on Linux and FreeBSD are `maxInt(i64)`.
6646pub fn copy_file_range(fd_in: fd_t, off_in: u64, fd_out: fd_t, off_out: u64, len: usize, flags: u32) CopyFileRangeError!usize {
6647 if (builtin.os.tag == .freebsd or
6648 (comptime builtin.os.tag == .linux and std.c.versionCheck(.{ .major = 2, .minor = 27, .patch = 0 })))
6649 {
6650 var off_in_copy: i64 = @bitCast(off_in);
6651 var off_out_copy: i64 = @bitCast(off_out);
6652
6653 while (true) {
6654 const rc = system.copy_file_range(fd_in, &off_in_copy, fd_out, &off_out_copy, len, flags);
6655 if (native_os == .freebsd) {
6656 switch (errno(rc)) {
6657 .SUCCESS => return @intCast(rc),
6658 .BADF => return error.FilesOpenedWithWrongFlags,
6659 .FBIG => return error.FileTooBig,
6660 .IO => return error.InputOutput,
6661 .ISDIR => return error.IsDir,
6662 .NOSPC => return error.NoSpaceLeft,
6663 .INVAL => break, // these may not be regular files, try fallback
6664 .INTEGRITY => return error.CorruptedData,
6665 .INTR => continue,
6666 else => |err| return unexpectedErrno(err),
6667 }
6668 } else { // assume linux
6669 switch (errno(rc)) {
6670 .SUCCESS => return @intCast(rc),
6671 .BADF => return error.FilesOpenedWithWrongFlags,
6672 .FBIG => return error.FileTooBig,
6673 .IO => return error.InputOutput,
6674 .ISDIR => return error.IsDir,
6675 .NOSPC => return error.NoSpaceLeft,
6676 .INVAL => break, // these may not be regular files, try fallback
6677 .NOMEM => return error.OutOfMemory,
6678 .OVERFLOW => return error.Unseekable,
6679 .PERM => return error.PermissionDenied,
6680 .TXTBSY => return error.SwapFile,
6681 .XDEV => break, // support for cross-filesystem copy added in Linux 5.3, use fallback
6682 else => |err| return unexpectedErrno(err),
6683 }
6684 }
6685 }
6686 }
6687
6688 var buf: [8 * 4096]u8 = undefined;
6689 const amt_read = try pread(fd_in, buf[0..@min(buf.len, len)], off_in);
6690 if (amt_read == 0) return 0;
6691 return pwrite(fd_out, buf[0..amt_read], off_out);
6692}
6693
66946604pub const PollError = error{
66956605 /// The network subsystem has failed.
66966606 NetworkSubsystemFailed,