aboutsummaryrefslogtreecommitdiff
path: root/lib/std/fs
diff options
context:
space:
mode:
authorAndrew Kelley <andrew@ziglang.org>2025-07-22 18:40:11 +0200
committerGitHub <noreply@github.com>2025-07-22 18:40:11 +0200
commitf34b4780b7bd52d14df253d0762d9c73db8eb226 (patch)
tree87b5fc511384829f0f504ef8c649cf9ae5bf1556 /lib/std/fs
parentc41ac8f19ec96ed854d9ac31a2015006ba3e4657 (diff)
parent34d2778239b7eea854385354fd956358ae7cf5a0 (diff)
downloadzig-f34b4780b7bd52d14df253d0762d9c73db8eb226.tar.gz
zig-f34b4780b7bd52d14df253d0762d9c73db8eb226.zip
Merge pull request #24521 from ziglang/fs-streaming
std.fs.File: delete writeFileAll and friends
Diffstat (limited to 'lib/std/fs')
-rw-r--r--lib/std/fs/AtomicFile.zig98
-rw-r--r--lib/std/fs/Dir.zig180
-rw-r--r--lib/std/fs/File.zig298
-rw-r--r--lib/std/fs/test.zig86
4 files changed, 351 insertions, 311 deletions
diff --git a/lib/std/fs/AtomicFile.zig b/lib/std/fs/AtomicFile.zig
index 17a17f8993..96793aec72 100644
--- a/lib/std/fs/AtomicFile.zig
+++ b/lib/std/fs/AtomicFile.zig
@@ -1,6 +1,13 @@
-file: File,
-// TODO either replace this with rand_buf or use []u16 on Windows
-tmp_path_buf: [tmp_path_len:0]u8,
+const AtomicFile = @This();
+const std = @import("../std.zig");
+const File = std.fs.File;
+const Dir = std.fs.Dir;
+const fs = std.fs;
+const assert = std.debug.assert;
+const posix = std.posix;
+
+file_writer: File.Writer,
+random_integer: u64,
dest_basename: []const u8,
file_open: bool,
file_exists: bool,
@@ -9,35 +16,24 @@ dir: Dir,
pub const InitError = File.OpenError;
-pub const random_bytes_len = 12;
-const tmp_path_len = fs.base64_encoder.calcSize(random_bytes_len);
-
/// Note that the `Dir.atomicFile` API may be more handy than this lower-level function.
pub fn init(
dest_basename: []const u8,
mode: File.Mode,
dir: Dir,
close_dir_on_deinit: bool,
+ write_buffer: []u8,
) InitError!AtomicFile {
- var rand_buf: [random_bytes_len]u8 = undefined;
- var tmp_path_buf: [tmp_path_len:0]u8 = undefined;
-
while (true) {
- std.crypto.random.bytes(rand_buf[0..]);
- const tmp_path = fs.base64_encoder.encode(&tmp_path_buf, &rand_buf);
- tmp_path_buf[tmp_path.len] = 0;
-
- const file = dir.createFile(
- tmp_path,
- .{ .mode = mode, .exclusive = true },
- ) catch |err| switch (err) {
+ const random_integer = std.crypto.random.int(u64);
+ const tmp_sub_path = std.fmt.hex(random_integer);
+ const file = dir.createFile(&tmp_sub_path, .{ .mode = mode, .exclusive = true }) catch |err| switch (err) {
error.PathAlreadyExists => continue,
else => |e| return e,
};
-
- return AtomicFile{
- .file = file,
- .tmp_path_buf = tmp_path_buf,
+ return .{
+ .file_writer = file.writer(write_buffer),
+ .random_integer = random_integer,
.dest_basename = dest_basename,
.file_open = true,
.file_exists = true,
@@ -48,41 +44,51 @@ pub fn init(
}
/// Always call deinit, even after a successful finish().
-pub fn deinit(self: *AtomicFile) void {
- if (self.file_open) {
- self.file.close();
- self.file_open = false;
+pub fn deinit(af: *AtomicFile) void {
+ if (af.file_open) {
+ af.file_writer.file.close();
+ af.file_open = false;
}
- if (self.file_exists) {
- self.dir.deleteFile(&self.tmp_path_buf) catch {};
- self.file_exists = false;
+ if (af.file_exists) {
+ const tmp_sub_path = std.fmt.hex(af.random_integer);
+ af.dir.deleteFile(&tmp_sub_path) catch {};
+ af.file_exists = false;
}
- if (self.close_dir_on_deinit) {
- self.dir.close();
+ if (af.close_dir_on_deinit) {
+ af.dir.close();
}
- self.* = undefined;
+ af.* = undefined;
}
-pub const FinishError = posix.RenameError;
+pub const FlushError = File.WriteError;
+
+pub fn flush(af: *AtomicFile) FlushError!void {
+ af.file_writer.interface.flush() catch |err| switch (err) {
+ error.WriteFailed => return af.file_writer.err.?,
+ };
+}
+
+pub const RenameIntoPlaceError = posix.RenameError;
/// On Windows, this function introduces a period of time where some file
/// system operations on the destination file will result in
/// `error.AccessDenied`, including rename operations (such as the one used in
/// this function).
-pub fn finish(self: *AtomicFile) FinishError!void {
- assert(self.file_exists);
- if (self.file_open) {
- self.file.close();
- self.file_open = false;
+pub fn renameIntoPlace(af: *AtomicFile) RenameIntoPlaceError!void {
+ assert(af.file_exists);
+ if (af.file_open) {
+ af.file_writer.file.close();
+ af.file_open = false;
}
- try posix.renameat(self.dir.fd, self.tmp_path_buf[0..], self.dir.fd, self.dest_basename);
- self.file_exists = false;
+ const tmp_sub_path = std.fmt.hex(af.random_integer);
+ try posix.renameat(af.dir.fd, &tmp_sub_path, af.dir.fd, af.dest_basename);
+ af.file_exists = false;
}
-const AtomicFile = @This();
-const std = @import("../std.zig");
-const File = std.fs.File;
-const Dir = std.fs.Dir;
-const fs = std.fs;
-const assert = std.debug.assert;
-const posix = std.posix;
+pub const FinishError = FlushError || RenameIntoPlaceError;
+
+/// Combination of `flush` followed by `renameIntoPlace`.
+pub fn finish(af: *AtomicFile) FinishError!void {
+ try af.flush();
+ try af.renameIntoPlace();
+}
diff --git a/lib/std/fs/Dir.zig b/lib/std/fs/Dir.zig
index 27d97a00cb..16418d216f 100644
--- a/lib/std/fs/Dir.zig
+++ b/lib/std/fs/Dir.zig
@@ -1,3 +1,20 @@
+const Dir = @This();
+const builtin = @import("builtin");
+const std = @import("../std.zig");
+const File = std.fs.File;
+const AtomicFile = std.fs.AtomicFile;
+const base64_encoder = fs.base64_encoder;
+const posix = std.posix;
+const mem = std.mem;
+const path = fs.path;
+const fs = std.fs;
+const Allocator = std.mem.Allocator;
+const assert = std.debug.assert;
+const linux = std.os.linux;
+const windows = std.os.windows;
+const native_os = builtin.os.tag;
+const have_flock = @TypeOf(posix.system.flock) != void;
+
fd: Handle,
pub const Handle = posix.fd_t;
@@ -1862,9 +1879,10 @@ pub fn symLinkW(
/// Same as `symLink`, except tries to create the symbolic link until it
/// succeeds or encounters an error other than `error.PathAlreadyExists`.
-/// On Windows, both paths should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
-/// 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.
+///
+/// * On Windows, both paths should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
+/// * 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.
pub fn atomicSymLink(
dir: Dir,
target_path: []const u8,
@@ -1880,9 +1898,8 @@ pub fn atomicSymLink(
const dirname = path.dirname(sym_link_path) orelse ".";
- var rand_buf: [AtomicFile.random_bytes_len]u8 = undefined;
-
- const temp_path_len = dirname.len + 1 + base64_encoder.calcSize(rand_buf.len);
+ const rand_len = @sizeOf(u64) * 2;
+ const temp_path_len = dirname.len + 1 + rand_len;
var temp_path_buf: [fs.max_path_bytes]u8 = undefined;
if (temp_path_len > temp_path_buf.len) return error.NameTooLong;
@@ -1892,8 +1909,8 @@ pub fn atomicSymLink(
const temp_path = temp_path_buf[0..temp_path_len];
while (true) {
- crypto.random.bytes(rand_buf[0..]);
- _ = base64_encoder.encode(temp_path[dirname.len + 1 ..], rand_buf[0..]);
+ const random_integer = std.crypto.random.int(u64);
+ temp_path[dirname.len + 1 ..][0..rand_len].* = std.fmt.hex(random_integer);
if (dir.symLink(target_path, temp_path, flags)) {
return dir.rename(temp_path, sym_link_path);
@@ -2552,25 +2569,42 @@ pub fn updateFile(
try dest_dir.makePath(dirname);
}
- var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = actual_mode });
+ var buffer: [1000]u8 = undefined; // Used only when direct fd-to-fd is not available.
+ var atomic_file = try dest_dir.atomicFile(dest_path, .{
+ .mode = actual_mode,
+ .write_buffer = &buffer,
+ });
defer atomic_file.deinit();
- try atomic_file.file.writeFileAll(src_file, .{ .in_len = src_stat.size });
- try atomic_file.file.updateTimes(src_stat.atime, src_stat.mtime);
+ var src_reader: File.Reader = .initSize(src_file, &.{}, src_stat.size);
+ const dest_writer = &atomic_file.file_writer.interface;
+
+ _ = dest_writer.sendFileAll(&src_reader, .unlimited) catch |err| switch (err) {
+ error.ReadFailed => return src_reader.err.?,
+ error.WriteFailed => return atomic_file.file_writer.err.?,
+ };
+ try atomic_file.file_writer.file.updateTimes(src_stat.atime, src_stat.mtime);
try atomic_file.finish();
- return PrevStatus.stale;
+ return .stale;
}
pub const CopyFileError = File.OpenError || File.StatError ||
- AtomicFile.InitError || CopyFileRawError || AtomicFile.FinishError;
+ AtomicFile.InitError || AtomicFile.FinishError ||
+ File.ReadError || File.WriteError;
-/// Guaranteed to be atomic.
-/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and readily available,
-/// there is a possibility of power loss or application termination leaving temporary files present
-/// in the same directory as dest_path.
-/// On Windows, both paths should be encoded as [WTF-8](https://simonsapin.github.io/wtf-8/).
-/// 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.
+/// Atomically creates a new file at `dest_path` within `dest_dir` with the
+/// same contents as `source_path` within `source_dir`, overwriting any already
+/// existing file.
+///
+/// On Linux, until https://patchwork.kernel.org/patch/9636735/ is merged and
+/// readily available, there is a possibility of power loss or application
+/// termination leaving temporary files present in the same directory as
+/// dest_path.
+///
+/// On Windows, both paths should be encoded as
+/// [WTF-8](https://simonsapin.github.io/wtf-8/). 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.
pub fn copyFile(
source_dir: Dir,
source_path: []const u8,
@@ -2578,79 +2612,34 @@ pub fn copyFile(
dest_path: []const u8,
options: CopyFileOptions,
) CopyFileError!void {
- var in_file = try source_dir.openFile(source_path, .{});
- defer in_file.close();
+ var file_reader: File.Reader = .init(try source_dir.openFile(source_path, .{}), &.{});
+ defer file_reader.file.close();
- var size: ?u64 = null;
const mode = options.override_mode orelse blk: {
- const st = try in_file.stat();
- size = st.size;
+ const st = try file_reader.file.stat();
+ file_reader.size = st.size;
break :blk st.mode;
};
- var atomic_file = try dest_dir.atomicFile(dest_path, .{ .mode = mode });
+ var buffer: [1024]u8 = undefined; // Used only when direct fd-to-fd is not available.
+ var atomic_file = try dest_dir.atomicFile(dest_path, .{
+ .mode = mode,
+ .write_buffer = &buffer,
+ });
defer atomic_file.deinit();
- try copy_file(in_file.handle, atomic_file.file.handle, size);
- try atomic_file.finish();
-}
-
-const CopyFileRawError = error{SystemResources} || posix.CopyFileRangeError || posix.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: posix.fd_t, fd_out: posix.fd_t, maybe_size: ?u64) CopyFileRawError!void {
- if (builtin.target.os.tag.isDarwin()) {
- const rc = posix.system.fcopyfile(fd_in, fd_out, null, .{ .DATA = true });
- switch (posix.errno(rc)) {
- .SUCCESS => return,
- .INVAL => unreachable,
- .NOMEM => return error.SystemResources,
- // The source file is not a directory, symbolic link, or regular file.
- // Try with the fallback path before giving up.
- .OPNOTSUPP => {},
- else => |err| return posix.unexpectedErrno(err),
- }
- }
-
- if (native_os == .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 posix.copy_file_range(fd_in, offset, fd_out, offset, std.math.maxInt(u32), 0);
- // Terminate as soon as we have copied size bytes or no bytes
- if (maybe_size) |s| {
- if (s == amt) break :cfr_loop;
- }
- if (amt == 0) break :cfr_loop;
- offset += amt;
- }
- return;
- }
+ _ = atomic_file.file_writer.interface.sendFileAll(&file_reader, .unlimited) catch |err| switch (err) {
+ error.ReadFailed => return file_reader.err.?,
+ error.WriteFailed => return atomic_file.file_writer.err.?,
+ };
- // 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]posix.iovec_const{};
- var offset: u64 = 0;
- sendfile_loop: while (true) {
- const amt = try posix.sendfile(fd_out, fd_in, offset, 0, &empty_iovec, &empty_iovec, 0);
- // Terminate as soon as we have copied size bytes or no bytes
- if (maybe_size) |s| {
- if (s == amt) break :sendfile_loop;
- }
- if (amt == 0) break :sendfile_loop;
- offset += amt;
- }
+ try atomic_file.finish();
}
pub const AtomicFileOptions = struct {
mode: File.Mode = File.default_mode,
make_path: bool = false,
+ write_buffer: []u8,
};
/// Directly access the `.file` field, and then call `AtomicFile.finish` to
@@ -2668,9 +2657,9 @@ pub fn atomicFile(self: Dir, dest_path: []const u8, options: AtomicFileOptions)
else
try self.openDir(dirname, .{});
- return AtomicFile.init(fs.path.basename(dest_path), options.mode, dir, true);
+ return .init(fs.path.basename(dest_path), options.mode, dir, true, options.write_buffer);
} else {
- return AtomicFile.init(dest_path, options.mode, self, false);
+ return .init(dest_path, options.mode, self, false, options.write_buffer);
}
}
@@ -2768,30 +2757,3 @@ pub fn setPermissions(self: Dir, permissions: Permissions) SetPermissionsError!v
const file: File = .{ .handle = self.fd };
try file.setPermissions(permissions);
}
-
-const Metadata = File.Metadata;
-pub const MetadataError = File.MetadataError;
-
-/// Returns a `Metadata` struct, representing the permissions on the directory
-pub fn metadata(self: Dir) MetadataError!Metadata {
- const file: File = .{ .handle = self.fd };
- return try file.metadata();
-}
-
-const Dir = @This();
-const builtin = @import("builtin");
-const std = @import("../std.zig");
-const File = std.fs.File;
-const AtomicFile = std.fs.AtomicFile;
-const base64_encoder = fs.base64_encoder;
-const crypto = std.crypto;
-const posix = std.posix;
-const mem = std.mem;
-const path = fs.path;
-const fs = std.fs;
-const Allocator = std.mem.Allocator;
-const assert = std.debug.assert;
-const linux = std.os.linux;
-const windows = std.os.windows;
-const native_os = builtin.os.tag;
-const have_flock = @TypeOf(posix.system.flock) != void;
diff --git a/lib/std/fs/File.zig b/lib/std/fs/File.zig
index 5b7e0aa570..138807972e 100644
--- a/lib/std/fs/File.zig
+++ b/lib/std/fs/File.zig
@@ -1089,113 +1089,6 @@ pub fn copyRangeAll(in: File, in_offset: u64, out: File, out_offset: u64, len: u
return total_bytes_copied;
}
-/// Deprecated in favor of `Writer`.
-pub const WriteFileOptions = struct {
- in_offset: u64 = 0,
- in_len: ?u64 = null,
- headers_and_trailers: []posix.iovec_const = &[0]posix.iovec_const{},
- header_count: usize = 0,
-};
-
-/// Deprecated in favor of `Writer`.
-pub const WriteFileError = ReadError || error{EndOfStream} || WriteError;
-
-/// Deprecated in favor of `Writer`.
-pub fn writeFileAll(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void {
- return self.writeFileAllSendfile(in_file, args) catch |err| switch (err) {
- error.Unseekable,
- error.FastOpenAlreadyInProgress,
- error.MessageTooBig,
- error.FileDescriptorNotASocket,
- error.NetworkUnreachable,
- error.NetworkSubsystemFailed,
- error.ConnectionRefused,
- => return self.writeFileAllUnseekable(in_file, args),
- else => |e| return e,
- };
-}
-
-/// Deprecated in favor of `Writer`.
-pub fn writeFileAllUnseekable(self: File, in_file: File, args: WriteFileOptions) WriteFileError!void {
- const headers = args.headers_and_trailers[0..args.header_count];
- const trailers = args.headers_and_trailers[args.header_count..];
- try self.writevAll(headers);
- try in_file.deprecatedReader().skipBytes(args.in_offset, .{ .buf_size = 4096 });
- var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init();
- if (args.in_len) |len| {
- var stream = std.io.limitedReader(in_file.deprecatedReader(), len);
- try fifo.pump(stream.reader(), self.deprecatedWriter());
- } else {
- try fifo.pump(in_file.deprecatedReader(), self.deprecatedWriter());
- }
- try self.writevAll(trailers);
-}
-
-/// Deprecated in favor of `Writer`.
-fn writeFileAllSendfile(self: File, in_file: File, args: WriteFileOptions) posix.SendFileError!void {
- const count = blk: {
- if (args.in_len) |l| {
- if (l == 0) {
- return self.writevAll(args.headers_and_trailers);
- } else {
- break :blk l;
- }
- } else {
- break :blk 0;
- }
- };
- const headers = args.headers_and_trailers[0..args.header_count];
- const trailers = args.headers_and_trailers[args.header_count..];
- const zero_iovec = &[0]posix.iovec_const{};
- // When reading the whole file, we cannot put the trailers in the sendfile() syscall,
- // because we have no way to determine whether a partial write is past the end of the file or not.
- const trls = if (count == 0) zero_iovec else trailers;
- const offset = args.in_offset;
- const out_fd = self.handle;
- const in_fd = in_file.handle;
- const flags = 0;
- var amt: usize = 0;
- hdrs: {
- var i: usize = 0;
- while (i < headers.len) {
- amt = try posix.sendfile(out_fd, in_fd, offset, count, headers[i..], trls, flags);
- while (amt >= headers[i].len) {
- amt -= headers[i].len;
- i += 1;
- if (i >= headers.len) break :hdrs;
- }
- headers[i].base += amt;
- headers[i].len -= amt;
- }
- }
- if (count == 0) {
- var off: u64 = amt;
- while (true) {
- amt = try posix.sendfile(out_fd, in_fd, offset + off, 0, zero_iovec, zero_iovec, flags);
- if (amt == 0) break;
- off += amt;
- }
- } else {
- var off: u64 = amt;
- while (off < count) {
- amt = try posix.sendfile(out_fd, in_fd, offset + off, count - off, zero_iovec, trailers, flags);
- off += amt;
- }
- amt = @as(usize, @intCast(off - count));
- }
- var i: usize = 0;
- while (i < trailers.len) {
- while (amt >= trailers[i].len) {
- amt -= trailers[i].len;
- i += 1;
- if (i >= trailers.len) return;
- }
- trailers[i].base += amt;
- trailers[i].len -= amt;
- amt = try posix.writev(self.handle, trailers[i..]);
- }
-}
-
/// Deprecated in favor of `Reader`.
pub const DeprecatedReader = io.GenericReader(File, ReadError, read);
@@ -1242,7 +1135,7 @@ pub const Reader = struct {
err: ?ReadError = null,
mode: Reader.Mode = .positional,
/// Tracks the true seek position in the file. To obtain the logical
- /// position, subtract the buffer size from this value.
+ /// position, use `logicalPos`.
pos: u64 = 0,
size: ?u64 = null,
size_err: ?GetEndPosError = null,
@@ -1335,14 +1228,12 @@ pub const Reader = struct {
pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void {
switch (r.mode) {
.positional, .positional_reading => {
- // TODO: make += operator allow any integer types
- r.pos = @intCast(@as(i64, @intCast(r.pos)) + offset);
+ setPosAdjustingBuffer(r, @intCast(@as(i64, @intCast(r.pos)) + offset));
},
.streaming, .streaming_reading => {
const seek_err = r.seek_err orelse e: {
if (posix.lseek_CUR(r.file.handle, offset)) |_| {
- // TODO: make += operator allow any integer types
- r.pos = @intCast(@as(i64, @intCast(r.pos)) + offset);
+ setPosAdjustingBuffer(r, @intCast(@as(i64, @intCast(r.pos)) + offset));
return;
} else |err| {
r.seek_err = err;
@@ -1358,6 +1249,8 @@ pub const Reader = struct {
r.pos += n;
remaining -= n;
}
+ r.interface.seek = 0;
+ r.interface.end = 0;
},
.failure => return r.seek_err.?,
}
@@ -1366,7 +1259,7 @@ pub const Reader = struct {
pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void {
switch (r.mode) {
.positional, .positional_reading => {
- r.pos = offset;
+ setPosAdjustingBuffer(r, offset);
},
.streaming, .streaming_reading => {
if (offset >= r.pos) return Reader.seekBy(r, @intCast(offset - r.pos));
@@ -1375,12 +1268,28 @@ pub const Reader = struct {
r.seek_err = err;
return err;
};
- r.pos = offset;
+ setPosAdjustingBuffer(r, offset);
},
.failure => return r.seek_err.?,
}
}
+ pub fn logicalPos(r: *const Reader) u64 {
+ return r.pos - r.interface.bufferedLen();
+ }
+
+ fn setPosAdjustingBuffer(r: *Reader, offset: u64) void {
+ const logical_pos = logicalPos(r);
+ if (offset < logical_pos or offset >= r.pos) {
+ r.interface.seek = 0;
+ r.interface.end = 0;
+ r.pos = offset;
+ } else {
+ const logical_delta: usize = @intCast(offset - logical_pos);
+ r.interface.seek += logical_delta;
+ }
+ }
+
/// Number of slices to store on the stack, when trying to send as many byte
/// vectors through the underlying read calls as possible.
const max_buffers_len = 16;
@@ -1526,7 +1435,7 @@ pub const Reader = struct {
}
return 0;
};
- const n = @min(size - pos, std.math.maxInt(i64), @intFromEnum(limit));
+ const n = @min(size - pos, maxInt(i64), @intFromEnum(limit));
file.seekBy(n) catch |err| {
r.seek_err = err;
return 0;
@@ -1715,7 +1624,6 @@ pub const Writer = struct {
const pattern = data[data.len - 1];
if (pattern.len == 0 or splat == 0) return 0;
const n = windows.WriteFile(handle, pattern, null) catch |err| {
- std.debug.print("windows write file failed3: {t}\n", .{err});
w.err = err;
return error.WriteFailed;
};
@@ -1817,18 +1725,141 @@ pub const Writer = struct {
file_reader: *Reader,
limit: std.io.Limit,
) std.io.Writer.FileError!usize {
+ const reader_buffered = file_reader.interface.buffered();
+ if (reader_buffered.len >= @intFromEnum(limit))
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
+ const writer_buffered = io_w.buffered();
+ const file_limit = @intFromEnum(limit) - reader_buffered.len;
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
const out_fd = w.file.handle;
const in_fd = file_reader.file.handle;
- // TODO try using copy_file_range on FreeBSD
- // TODO try using sendfile on macOS
- // TODO try using sendfile on FreeBSD
+
+ if (file_reader.size) |size| {
+ if (size - file_reader.pos == 0) {
+ if (reader_buffered.len != 0) {
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
+ } else {
+ return error.EndOfStream;
+ }
+ }
+ }
+
+ if (native_os == .freebsd and w.mode == .streaming) sf: {
+ // Try using sendfile on FreeBSD.
+ if (w.sendfile_err != null) break :sf;
+ const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
+ var hdtr_data: std.c.sf_hdtr = undefined;
+ var headers: [2]posix.iovec_const = undefined;
+ var headers_i: u8 = 0;
+ if (writer_buffered.len != 0) {
+ headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
+ headers_i += 1;
+ }
+ if (reader_buffered.len != 0) {
+ headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
+ headers_i += 1;
+ }
+ const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
+ hdtr_data = .{
+ .headers = &headers,
+ .hdr_cnt = headers_i,
+ .trailers = null,
+ .trl_cnt = 0,
+ };
+ break :b &hdtr_data;
+ };
+ var sbytes: std.c.off_t = undefined;
+ const nbytes: usize = @min(file_limit, maxInt(usize));
+ const flags = 0;
+ switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, nbytes, hdtr, &sbytes, flags))) {
+ .SUCCESS, .INTR => {},
+ .INVAL, .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
+ .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .NOTCONN => w.sendfile_err = error.BrokenPipe,
+ .AGAIN, .BUSY => if (sbytes == 0) {
+ w.sendfile_err = error.WouldBlock;
+ },
+ .IO => w.sendfile_err = error.InputOutput,
+ .PIPE => w.sendfile_err = error.BrokenPipe,
+ .NOBUFS => w.sendfile_err = error.SystemResources,
+ else => |err| w.sendfile_err = posix.unexpectedErrno(err),
+ }
+ if (sbytes == 0) {
+ file_reader.size = file_reader.pos;
+ return error.EndOfStream;
+ }
+ const consumed = io_w.consume(@intCast(sbytes));
+ file_reader.seekTo(file_reader.pos + consumed) catch return error.ReadFailed;
+ return consumed;
+ }
+
+ if (native_os.isDarwin() and w.mode == .streaming) sf: {
+ // Try using sendfile on macOS.
+ if (w.sendfile_err != null) break :sf;
+ const offset = std.math.cast(std.c.off_t, file_reader.pos) orelse break :sf;
+ var hdtr_data: std.c.sf_hdtr = undefined;
+ var headers: [2]posix.iovec_const = undefined;
+ var headers_i: u8 = 0;
+ if (writer_buffered.len != 0) {
+ headers[headers_i] = .{ .base = writer_buffered.ptr, .len = writer_buffered.len };
+ headers_i += 1;
+ }
+ if (reader_buffered.len != 0) {
+ headers[headers_i] = .{ .base = reader_buffered.ptr, .len = reader_buffered.len };
+ headers_i += 1;
+ }
+ const hdtr: ?*std.c.sf_hdtr = if (headers_i == 0) null else b: {
+ hdtr_data = .{
+ .headers = &headers,
+ .hdr_cnt = headers_i,
+ .trailers = null,
+ .trl_cnt = 0,
+ };
+ break :b &hdtr_data;
+ };
+ const max_count = maxInt(i32); // Avoid EINVAL.
+ var len: std.c.off_t = @min(file_limit, max_count);
+ const flags = 0;
+ switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, &len, hdtr, flags))) {
+ .SUCCESS, .INTR => {},
+ .OPNOTSUPP, .NOTSOCK, .NOSYS => w.sendfile_err = error.UnsupportedOperation,
+ .BADF => if (builtin.mode == .Debug) @panic("race condition") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .FAULT => if (builtin.mode == .Debug) @panic("segmentation fault") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
+ w.sendfile_err = error.Unexpected;
+ },
+ .NOTCONN => w.sendfile_err = error.BrokenPipe,
+ .AGAIN => if (len == 0) {
+ w.sendfile_err = error.WouldBlock;
+ },
+ .IO => w.sendfile_err = error.InputOutput,
+ .PIPE => w.sendfile_err = error.BrokenPipe,
+ else => |err| w.sendfile_err = posix.unexpectedErrno(err),
+ }
+ if (len == 0) {
+ file_reader.size = file_reader.pos;
+ return error.EndOfStream;
+ }
+ const consumed = io_w.consume(@bitCast(len));
+ file_reader.seekTo(file_reader.pos + consumed) catch return error.ReadFailed;
+ return consumed;
+ }
+
if (native_os == .linux and w.mode == .streaming) sf: {
// Try using sendfile on Linux.
if (w.sendfile_err != null) break :sf;
// Linux sendfile does not support headers.
- const buffered = limit.slice(file_reader.interface.buffer);
- if (io_w.end != 0 or buffered.len != 0) return drain(io_w, &.{buffered}, 1);
+ if (writer_buffered.len != 0 or reader_buffered.len != 0)
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
const max_count = 0x7ffff000; // Avoid EINVAL.
var off: std.os.linux.off_t = undefined;
const off_ptr: ?*std.os.linux.off_t, const count: usize = switch (file_reader.mode) {
@@ -1875,6 +1906,7 @@ pub const Writer = struct {
w.pos += n;
return n;
}
+
const copy_file_range = switch (native_os) {
.freebsd => std.os.freebsd.copy_file_range,
.linux => if (std.c.versionCheck(.{ .major = 2, .minor = 27, .patch = 0 })) std.os.linux.wrapped.copy_file_range else {},
@@ -1882,8 +1914,8 @@ pub const Writer = struct {
};
if (@TypeOf(copy_file_range) != void) cfr: {
if (w.copy_file_range_err != null) break :cfr;
- const buffered = limit.slice(file_reader.interface.buffer);
- if (io_w.end != 0 or buffered.len != 0) return drain(io_w, &.{buffered}, 1);
+ if (writer_buffered.len != 0 or reader_buffered.len != 0)
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
var off_in: i64 = undefined;
var off_out: i64 = undefined;
const off_in_ptr: ?*i64 = switch (file_reader.mode) {
@@ -1922,6 +1954,9 @@ pub const Writer = struct {
if (file_reader.pos != 0) break :fcf;
if (w.pos != 0) break :fcf;
if (limit != .unlimited) break :fcf;
+ const size = file_reader.getSize() catch break :fcf;
+ if (writer_buffered.len != 0 or reader_buffered.len != 0)
+ return sendFileBuffered(io_w, file_reader, reader_buffered);
const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
switch (posix.errno(rc)) {
.SUCCESS => {},
@@ -1942,15 +1977,24 @@ pub const Writer = struct {
return 0;
},
}
- const n = if (file_reader.size) |size| size else @panic("TODO figure out how much copied");
- file_reader.pos = n;
- w.pos = n;
- return n;
+ file_reader.pos = size;
+ w.pos = size;
+ return size;
}
return error.Unimplemented;
}
+ fn sendFileBuffered(
+ io_w: *std.io.Writer,
+ file_reader: *Reader,
+ reader_buffered: []const u8,
+ ) std.io.Writer.FileError!usize {
+ const n = try drain(io_w, &.{reader_buffered}, 1);
+ file_reader.seekTo(file_reader.pos + n) catch return error.ReadFailed;
+ return n;
+ }
+
pub fn seekTo(w: *Writer, offset: u64) SeekError!void {
switch (w.mode) {
.positional, .positional_reading => {
diff --git a/lib/std/fs/test.zig b/lib/std/fs/test.zig
index 50cbccf270..4b63873af5 100644
--- a/lib/std/fs/test.zig
+++ b/lib/std/fs/test.zig
@@ -1499,32 +1499,18 @@ test "sendfile" {
const header2 = "second header\n";
const trailer1 = "trailer1\n";
const trailer2 = "second trailer\n";
- var hdtr = [_]posix.iovec_const{
- .{
- .base = header1,
- .len = header1.len,
- },
- .{
- .base = header2,
- .len = header2.len,
- },
- .{
- .base = trailer1,
- .len = trailer1.len,
- },
- .{
- .base = trailer2,
- .len = trailer2.len,
- },
- };
+ var headers: [2][]const u8 = .{ header1, header2 };
+ var trailers: [2][]const u8 = .{ trailer1, trailer2 };
var written_buf: [100]u8 = undefined;
- try dest_file.writeFileAll(src_file, .{
- .in_offset = 1,
- .in_len = 10,
- .headers_and_trailers = &hdtr,
- .header_count = 2,
- });
+ var file_reader = src_file.reader(&.{});
+ var fallback_buffer: [50]u8 = undefined;
+ var file_writer = dest_file.writer(&fallback_buffer);
+ try file_writer.interface.writeVecAll(&headers);
+ try file_reader.seekTo(1);
+ try testing.expectEqual(10, try file_writer.interface.sendFileAll(&file_reader, .limited(10)));
+ try file_writer.interface.writeVecAll(&trailers);
+ try file_writer.interface.flush();
const amt = try dest_file.preadAll(&written_buf, 0);
try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]);
}
@@ -1595,9 +1581,10 @@ test "AtomicFile" {
;
{
- var af = try ctx.dir.atomicFile(test_out_file, .{});
+ var buffer: [100]u8 = undefined;
+ var af = try ctx.dir.atomicFile(test_out_file, .{ .write_buffer = &buffer });
defer af.deinit();
- try af.file.writeAll(test_content);
+ try af.file_writer.interface.writeAll(test_content);
try af.finish();
}
const content = try ctx.dir.readFileAlloc(allocator, test_out_file, 9999);
@@ -2073,7 +2060,7 @@ test "invalid UTF-8/WTF-8 paths" {
}
test "read file non vectored" {
- var tmp_dir = std.testing.tmpDir(.{});
+ var tmp_dir = testing.tmpDir(.{});
defer tmp_dir.cleanup();
const contents = "hello, world!\n";
@@ -2098,6 +2085,47 @@ test "read file non vectored" {
else => |e| return e,
};
}
- try std.testing.expectEqualStrings(contents, w.buffered());
- try std.testing.expectEqual(contents.len, i);
+ try testing.expectEqualStrings(contents, w.buffered());
+ try testing.expectEqual(contents.len, i);
+}
+
+test "seek keeping partial buffer" {
+ var tmp_dir = testing.tmpDir(.{});
+ defer tmp_dir.cleanup();
+
+ const contents = "0123456789";
+
+ const file = try tmp_dir.dir.createFile("input.txt", .{ .read = true });
+ defer file.close();
+ {
+ var file_writer: std.fs.File.Writer = .init(file, &.{});
+ try file_writer.interface.writeAll(contents);
+ try file_writer.interface.flush();
+ }
+
+ var read_buffer: [3]u8 = undefined;
+ var file_reader: std.fs.File.Reader = .init(file, &read_buffer);
+
+ try testing.expectEqual(0, file_reader.logicalPos());
+
+ var buf: [4]u8 = undefined;
+ try file_reader.interface.readSliceAll(&buf);
+
+ if (file_reader.interface.bufferedLen() != 3) {
+ // Pass the test if the OS doesn't give us vectored reads.
+ return;
+ }
+
+ try testing.expectEqual(4, file_reader.logicalPos());
+ try testing.expectEqual(7, file_reader.pos);
+ try file_reader.seekTo(6);
+ try testing.expectEqual(6, file_reader.logicalPos());
+ try testing.expectEqual(7, file_reader.pos);
+
+ try testing.expectEqualStrings("0123", &buf);
+
+ const n = try file_reader.interface.readSliceShort(&buf);
+ try testing.expectEqual(4, n);
+
+ try testing.expectEqualStrings("6789", &buf);
}