std: extract sendfile/copy_file_range from Io.File.Writer

and move it into std.Io.Threaded (below the VTable)
This commit is contained in:
Andrew Kelley 2025-12-05 17:12:57 -08:00
parent 4f6658a67b
commit fc8b792251
11 changed files with 1015 additions and 834 deletions

View file

@ -662,8 +662,8 @@ pub const VTable = struct {
conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void, conditionWaitUncancelable: *const fn (?*anyopaque, cond: *Condition, mutex: *Mutex) void,
conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void, conditionWake: *const fn (?*anyopaque, cond: *Condition, wake: Condition.Wake) void,
dirMake: *const fn (?*anyopaque, Dir, []const u8, Dir.Mode) Dir.MakeError!void, dirMake: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.MakeError!void,
dirMakePath: *const fn (?*anyopaque, Dir, []const u8, Dir.Mode) Dir.MakePathError!Dir.MakePathStatus, dirMakePath: *const fn (?*anyopaque, Dir, []const u8, Dir.Permissions) Dir.MakePathError!Dir.MakePathStatus,
dirMakeOpenPath: *const fn (?*anyopaque, Dir, []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir, dirMakeOpenPath: *const fn (?*anyopaque, Dir, []const u8, Dir.OpenOptions) Dir.MakeOpenPathError!Dir,
dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat, dirStat: *const fn (?*anyopaque, Dir) Dir.StatError!Dir.Stat,
dirStatPath: *const fn (?*anyopaque, Dir, []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat, dirStatPath: *const fn (?*anyopaque, Dir, []const u8, Dir.StatPathOptions) Dir.StatPathError!File.Stat,
@ -687,8 +687,10 @@ pub const VTable = struct {
fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat, fileStat: *const fn (?*anyopaque, File) File.StatError!File.Stat,
fileLength: *const fn (?*anyopaque, File) File.LengthError!u64, fileLength: *const fn (?*anyopaque, File) File.LengthError!u64,
fileClose: *const fn (?*anyopaque, File) void, fileClose: *const fn (?*anyopaque, File) void,
fileWriteStreaming: *const fn (?*anyopaque, File, buffer: [][]const u8) File.WriteStreamingError!usize, fileWriteStreaming: *const fn (?*anyopaque, File, header: []const u8, data: []const []const u8, splat: usize) File.Writer.Error!usize,
fileWritePositional: *const fn (?*anyopaque, File, buffer: [][]const u8, offset: u64) File.WritePositionalError!usize, fileWritePositional: *const fn (?*anyopaque, File, header: []const u8, data: []const []const u8, splat: usize, offset: u64) File.WritePositionalError!usize,
fileWriteFileStreaming: *const fn (?*anyopaque, File, header: []const u8, *Io.File.Reader, Io.Limit) File.Writer.WriteFileError!usize,
fileWriteFilePositional: *const fn (?*anyopaque, File, header: []const u8, *Io.File.Reader, Io.Limit, offset: u64) File.WriteFilePositionalError!usize,
/// Returns 0 on end of stream. /// Returns 0 on end of stream.
fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.Reader.Error!usize, fileReadStreaming: *const fn (?*anyopaque, File, data: [][]u8) File.Reader.Error!usize,
/// Returns 0 on end of stream. /// Returns 0 on end of stream.
@ -724,6 +726,7 @@ pub const VTable = struct {
/// Returns 0 on end of stream. /// Returns 0 on end of stream.
netRead: *const fn (?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize, netRead: *const fn (?*anyopaque, src: net.Socket.Handle, data: [][]u8) net.Stream.Reader.Error!usize,
netWrite: *const fn (?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize, netWrite: *const fn (?*anyopaque, dest: net.Socket.Handle, header: []const u8, data: []const []const u8, splat: usize) net.Stream.Writer.Error!usize,
netWriteFile: *const fn (?*anyopaque, net.Socket.Handle, header: []const u8, *Io.File.Reader, Io.Limit) net.Stream.WriteFileError!usize,
netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void, netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void,
netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface, netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface,
netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name, netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name,

View file

@ -652,7 +652,6 @@ pub const RealPathError = error{
NoSpaceLeft, NoSpaceLeft,
FileSystem, FileSystem,
DeviceBusy, DeviceBusy,
ProcessNotFound,
SharingViolation, SharingViolation,
PipeBusy, PipeBusy,
/// Windows: file paths provided by the user must be valid WTF-8. /// Windows: file paths provided by the user must be valid WTF-8.
@ -1038,7 +1037,6 @@ pub const DeleteTreeError = error{
FileSystem, FileSystem,
FileBusy, FileBusy,
DeviceBusy, DeviceBusy,
ProcessNotFound,
/// One of the path components was not a directory. /// One of the path components was not a directory.
/// This error is unreachable if `sub_path` does not contain a path separator. /// This error is unreachable if `sub_path` does not contain a path separator.
NotDir, NotDir,

View file

@ -198,7 +198,6 @@ pub const OpenError = error{
NoDevice, NoDevice,
/// On Windows, `\\server` or `\\server\share` was not found. /// On Windows, `\\server` or `\\server\share` was not found.
NetworkNotFound, NetworkNotFound,
ProcessNotFound,
/// On Windows, antivirus software is enabled by default. It can be /// On Windows, antivirus software is enabled by default. It can be
/// disabled, but Windows Update sometimes ignores the user's preference /// disabled, but Windows Update sometimes ignores the user's preference
/// and re-enables it. When enabled, antivirus software on Windows /// and re-enables it. When enabled, antivirus software on Windows
@ -477,18 +476,25 @@ pub fn readPositional(file: File, io: Io, buffer: [][]u8, offset: u64) ReadPosit
return io.vtable.fileReadPositional(io.userdata, file, buffer, offset); return io.vtable.fileReadPositional(io.userdata, file, buffer, offset);
} }
pub const WriteStreamingError = error{} || Io.UnexpectedError || Io.Cancelable; pub const WritePositionalError = Writer.Error || error{Unseekable};
pub fn writeStreaming(file: File, io: Io, buffer: [][]const u8) WriteStreamingError!usize {
return file.fileWriteStreaming(io, buffer);
}
pub const WritePositionalError = WriteStreamingError || error{Unseekable};
pub fn writePositional(file: File, io: Io, buffer: [][]const u8, offset: u64) WritePositionalError!usize { pub fn writePositional(file: File, io: Io, buffer: [][]const u8, offset: u64) WritePositionalError!usize {
return io.vtable.fileWritePositional(io.userdata, file, buffer, offset); return io.vtable.fileWritePositional(io.userdata, file, buffer, offset);
} }
pub const WriteFileStreamingError = error{
/// `out_fd` is an unconnected socket, or out_fd closed its read end.
BrokenPipe,
/// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd.
UnsupportedOperation,
/// Nonblocking I/O has been selected but the write would block.
WouldBlock,
/// Unspecified error while reading from in_fd.
InputOutput,
/// Insufficient kernel memory to read from in_fd.
SystemResources,
} || Io.Cancelable || Io.UnexpectedError;
/// Opens a file for reading or writing, without attempting to create a new /// Opens a file for reading or writing, without attempting to create a new
/// file, based on an absolute path. /// file, based on an absolute path.
/// ///
@ -511,6 +517,8 @@ pub const SeekError = error{
AccessDenied, AccessDenied,
} || Io.Cancelable || Io.UnexpectedError; } || Io.Cancelable || Io.UnexpectedError;
pub const WriteFilePositionalError = Writer.WriteFileError || error{Unseekable};
/// Defaults to positional reading; falls back to streaming. /// Defaults to positional reading; falls back to streaming.
/// ///
/// Positional is more threadsafe, since the global seek position is not /// Positional is more threadsafe, since the global seek position is not

View file

@ -17,13 +17,13 @@ const assert = std.debug.assert;
io: Io, io: Io,
file: File, file: File,
err: ?Error = null, err: ?Error = null,
mode: Reader.Mode = .positional, mode: Mode = .positional,
/// Tracks the true seek position in the file. To obtain the logical /// Tracks the true seek position in the file. To obtain the logical
/// position, use `logicalPos`. /// position, use `logicalPos`.
pos: u64 = 0, pos: u64 = 0,
size: ?u64 = null, size: ?u64 = null,
size_err: ?SizeError = null, size_err: ?SizeError = null,
seek_err: ?Reader.SeekError = null, seek_err: ?SeekError = null,
interface: Io.Reader, interface: Io.Reader,
pub const Error = error{ pub const Error = error{
@ -37,15 +37,12 @@ pub const Error = error{
/// trying to read a directory file descriptor as if it were a file. /// trying to read a directory file descriptor as if it were a file.
NotOpenForReading, NotOpenForReading,
SocketUnconnected, SocketUnconnected,
/// This error occurs when no global event loop is configured, /// Non-blocking has been enabled, and reading from the file descriptor
/// and reading from the file descriptor would block. /// would block.
WouldBlock, WouldBlock,
/// In WASI, this error occurs when the file descriptor does /// In WASI, this error occurs when the file descriptor does
/// not hold the required rights to read from it. /// not hold the required rights to read from it.
AccessDenied, AccessDenied,
/// This error occurs in Linux if the process to be read from
/// no longer exists.
ProcessNotFound,
/// Unable to read file due to lock. /// Unable to read file due to lock.
LockViolation, LockViolation,
} || Io.Cancelable || Io.UnexpectedError; } || Io.Cancelable || Io.UnexpectedError;
@ -93,9 +90,9 @@ pub const Mode = enum {
pub fn initInterface(buffer: []u8) Io.Reader { pub fn initInterface(buffer: []u8) Io.Reader {
return .{ return .{
.vtable = &.{ .vtable = &.{
.stream = Reader.stream, .stream = stream,
.discard = Reader.discard, .discard = discard,
.readVec = Reader.readVec, .readVec = readVec,
}, },
.buffer = buffer, .buffer = buffer,
.seek = 0, .seek = 0,
@ -153,7 +150,7 @@ pub fn getSize(r: *Reader) SizeError!u64 {
}; };
} }
pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void { pub fn seekBy(r: *Reader, offset: i64) SeekError!void {
const io = r.io; const io = r.io;
switch (r.mode) { switch (r.mode) {
.positional, .positional_reading => { .positional, .positional_reading => {
@ -183,7 +180,7 @@ pub fn seekBy(r: *Reader, offset: i64) Reader.SeekError!void {
} }
/// Repositions logical read offset relative to the beginning of the file. /// Repositions logical read offset relative to the beginning of the file.
pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void { pub fn seekTo(r: *Reader, offset: u64) SeekError!void {
const io = r.io; const io = r.io;
switch (r.mode) { switch (r.mode) {
.positional, .positional_reading => { .positional, .positional_reading => {
@ -191,7 +188,7 @@ pub fn seekTo(r: *Reader, offset: u64) Reader.SeekError!void {
}, },
.streaming, .streaming_reading => { .streaming, .streaming_reading => {
const logical_pos = logicalPos(r); const logical_pos = logicalPos(r);
if (offset >= logical_pos) return Reader.seekBy(r, @intCast(offset - logical_pos)); if (offset >= logical_pos) return seekBy(r, @intCast(offset - logical_pos));
if (r.seek_err) |err| return err; if (r.seek_err) |err| return err;
io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| { io.vtable.fileSeekTo(io.userdata, r.file, offset) catch |err| {
r.seek_err = err; r.seek_err = err;
@ -224,7 +221,7 @@ fn stream(io_reader: *Io.Reader, w: *Io.Writer, limit: Io.Limit) Io.Reader.Strea
return streamMode(r, w, limit, r.mode); return streamMode(r, w, limit, r.mode);
} }
pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Reader.Mode) Io.Reader.StreamError!usize { pub fn streamMode(r: *Reader, w: *Io.Writer, limit: Io.Limit, mode: Mode) Io.Reader.StreamError!usize {
switch (mode) { switch (mode) {
.positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) { .positional, .streaming => return w.sendFile(r, limit) catch |write_err| switch (write_err) {
error.Unimplemented => { error.Unimplemented => {

View file

@ -1,53 +1,38 @@
const Writer = @This(); const Writer = @This();
const builtin = @import("builtin");
const native_os = builtin.os.tag;
const is_windows = native_os == .windows;
const std = @import("../../std.zig"); const std = @import("../../std.zig");
const Io = std.Io; const Io = std.Io;
const File = std.Io.File; const File = std.Io.File;
const assert = std.debug.assert; const assert = std.debug.assert;
const windows = std.os.windows;
const posix = std.posix;
io: Io,
file: File, file: File,
err: ?File.WriteError = null, err: ?File.WriteError = null,
mode: Writer.Mode = .positional, mode: Mode = .positional,
/// Tracks the true seek position in the file. To obtain the logical /// Tracks the true seek position in the file. To obtain the logical
/// position, add the buffer size to this value. /// position, add the buffer size to this value.
pos: u64 = 0, pos: u64 = 0,
sendfile_err: ?SendfileError = null, write_file_err: ?WriteFileError = null,
copy_file_range_err: ?CopyFileRangeError = null, seek_err: ?SeekError = null,
fcopyfile_err: ?FcopyfileError = null,
seek_err: ?Writer.SeekError = null,
interface: Io.Writer, interface: Io.Writer,
pub const Mode = File.Reader.Mode; pub const Mode = File.Reader.Mode;
pub const SendfileError = error{ pub const WriteFileError = error{
UnsupportedOperation, /// `out_fd` is an unconnected socket, or out_fd closed its read end.
SystemResources,
InputOutput,
BrokenPipe, BrokenPipe,
/// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd.
UnsupportedOperation,
/// Nonblocking I/O has been selected but the write would block.
WouldBlock, WouldBlock,
Unexpected, /// Unspecified error while reading from in_fd.
}; InputOutput,
/// Insufficient kernel memory to read from in_fd.
pub const CopyFileRangeError = std.os.freebsd.CopyFileRangeError || std.os.linux.wrapped.CopyFileRangeError; SystemResources,
} || Io.Cancelable || Io.UnexpectedError;
pub const FcopyfileError = error{
OperationNotSupported,
OutOfMemory,
Unexpected,
};
pub const SeekError = Io.File.SeekError; pub const SeekError = Io.File.SeekError;
/// Number of slices to store on the stack, when trying to send as many byte
/// vectors through the underlying write calls as possible.
const max_buffers_len = 16;
pub fn init(file: File, buffer: []u8) Writer { pub fn init(file: File, buffer: []u8) Writer {
return .{ return .{
.file = file, .file = file,
@ -91,121 +76,17 @@ pub fn moveToReader(w: *Writer) File.Reader {
pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize { pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w)); const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
const handle = w.file.handle;
const buffered = io_w.buffered();
if (is_windows) switch (w.mode) {
.positional, .positional_reading => {
if (buffered.len != 0) {
const n = windows.WriteFile(handle, buffered, w.pos) catch |err| {
w.err = err;
return error.WriteFailed;
};
w.pos += n;
return io_w.consume(n);
}
for (data[0 .. data.len - 1]) |buf| {
if (buf.len == 0) continue;
const n = windows.WriteFile(handle, buf, w.pos) catch |err| {
w.err = err;
return error.WriteFailed;
};
w.pos += n;
return io_w.consume(n);
}
const pattern = data[data.len - 1];
if (pattern.len == 0 or splat == 0) return 0;
const n = windows.WriteFile(handle, pattern, w.pos) catch |err| {
w.err = err;
return error.WriteFailed;
};
w.pos += n;
return io_w.consume(n);
},
.streaming, .streaming_reading => {
if (buffered.len != 0) {
const n = windows.WriteFile(handle, buffered, null) catch |err| {
w.err = err;
return error.WriteFailed;
};
w.pos += n;
return io_w.consume(n);
}
for (data[0 .. data.len - 1]) |buf| {
if (buf.len == 0) continue;
const n = windows.WriteFile(handle, buf, null) catch |err| {
w.err = err;
return error.WriteFailed;
};
w.pos += n;
return io_w.consume(n);
}
const pattern = data[data.len - 1];
if (pattern.len == 0 or splat == 0) return 0;
const n = windows.WriteFile(handle, pattern, null) catch |err| {
w.err = err;
return error.WriteFailed;
};
w.pos += n;
return io_w.consume(n);
},
.failure => return error.WriteFailed,
};
var iovecs: [max_buffers_len]posix.iovec_const = undefined;
var len: usize = 0;
if (buffered.len > 0) {
iovecs[len] = .{ .base = buffered.ptr, .len = buffered.len };
len += 1;
}
for (data[0 .. data.len - 1]) |d| {
if (d.len == 0) continue;
iovecs[len] = .{ .base = d.ptr, .len = d.len };
len += 1;
if (iovecs.len - len == 0) break;
}
const pattern = data[data.len - 1];
if (iovecs.len - len != 0) switch (splat) {
0 => {},
1 => if (pattern.len != 0) {
iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
len += 1;
},
else => switch (pattern.len) {
0 => {},
1 => {
const splat_buffer_candidate = io_w.buffer[io_w.end..];
var backup_buffer: [64]u8 = undefined;
const splat_buffer = if (splat_buffer_candidate.len >= backup_buffer.len)
splat_buffer_candidate
else
&backup_buffer;
const memset_len = @min(splat_buffer.len, splat);
const buf = splat_buffer[0..memset_len];
@memset(buf, pattern[0]);
iovecs[len] = .{ .base = buf.ptr, .len = buf.len };
len += 1;
var remaining_splat = splat - buf.len;
while (remaining_splat > splat_buffer.len and iovecs.len - len != 0) {
assert(buf.len == splat_buffer.len);
iovecs[len] = .{ .base = splat_buffer.ptr, .len = splat_buffer.len };
len += 1;
remaining_splat -= splat_buffer.len;
}
if (remaining_splat > 0 and iovecs.len - len != 0) {
iovecs[len] = .{ .base = splat_buffer.ptr, .len = remaining_splat };
len += 1;
}
},
else => for (0..splat) |_| {
iovecs[len] = .{ .base = pattern.ptr, .len = pattern.len };
len += 1;
if (iovecs.len - len == 0) break;
},
},
};
if (len == 0) return 0;
switch (w.mode) { switch (w.mode) {
.positional, .positional_reading => { .positional, .positional_reading => return drainPositional(w, data, splat),
const n = posix.pwritev(handle, iovecs[0..len], w.pos) catch |err| switch (err) { .streaming, .streaming_reading => return drainStreaming(w, data, splat),
.failure => return error.WriteFailed,
}
}
fn drainPositional(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const io = w.io;
const header = w.interface.buffered();
const n = io.vtable.fileWritePositional(io.userdata, w.file, header, data, splat, w.pos) catch |err| switch (err) {
error.Unseekable => { error.Unseekable => {
w.mode = w.mode.toStreaming(); w.mode = w.mode.toStreaming();
const pos = w.pos; const pos = w.pos;
@ -224,312 +105,93 @@ pub fn drain(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer
}, },
}; };
w.pos += n; w.pos += n;
return io_w.consume(n); return w.interface.consume(n);
}, }
.streaming, .streaming_reading => {
const n = posix.writev(handle, iovecs[0..len]) catch |err| { fn drainStreaming(w: *Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
const io = w.io;
const header = w.interface.buffered();
const n = io.vtable.fileWriteStreaming(io.userdata, w.file, header, data, splat) catch |err| {
w.err = err; w.err = err;
return error.WriteFailed; return error.WriteFailed;
}; };
w.pos += n; w.pos += n;
return io_w.consume(n); return w.interface.consume(n);
}, }
pub fn sendFile(io_w: *Io.Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
const w: *Writer = @alignCast(@fieldParentPtr("interface", io_w));
switch (w.mode) {
.positional => return sendFilePositional(w, file_reader, limit),
.positional_reading => return error.Unimplemented,
.streaming => return sendFileStreaming(w, file_reader, limit),
.streaming_reading => return error.Unimplemented,
.failure => return error.WriteFailed, .failure => return error.WriteFailed,
} }
} }
pub fn sendFile( fn sendFilePositional(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
io_w: *Io.Writer, const io = w.io;
file_reader: *Io.File.Reader, const header = w.interface.buffered();
limit: Io.Limit, const n = io.vtable.fileSendFilePositional(io.userdata, w.file, header, file_reader, limit, w.pos) catch |err| switch (err) {
) Io.Writer.FileError!usize {
const reader_buffered = file_reader.interface.buffered();
if (reader_buffered.len >= @intFromEnum(limit))
return sendFileBuffered(io_w, file_reader, limit.slice(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;
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, std.math.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 (w.sendfile_err != null) {
// Give calling code chance to observe the error before trying
// something else.
return 0;
}
if (sbytes == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
const consumed = io_w.consume(@intCast(sbytes));
file_reader.seekBy(@intCast(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 = std.math.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 (w.sendfile_err != null) {
// Give calling code chance to observe the error before trying
// something else.
return 0;
}
if (len == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
const consumed = io_w.consume(@bitCast(len));
file_reader.seekBy(@intCast(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.
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) {
.positional => o: {
const size = file_reader.getSize() catch return 0;
off = std.math.cast(std.os.linux.off_t, file_reader.pos) orelse return error.ReadFailed;
break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) };
},
.streaming => .{ null, limit.minInt(max_count) },
.streaming_reading, .positional_reading => break :sf,
.failure => return error.ReadFailed,
};
const n = std.os.linux.wrapped.sendfile(out_fd, in_fd, off_ptr, count) catch |err| switch (err) {
error.Unseekable => { error.Unseekable => {
file_reader.mode = file_reader.mode.toStreaming(); w.mode = w.mode.toStreaming();
const pos = file_reader.pos; const pos = w.pos;
if (pos != 0) { if (pos != 0) {
file_reader.pos = 0; w.pos = 0;
file_reader.seekBy(@intCast(pos)) catch { w.seekTo(@intCast(pos)) catch {
file_reader.mode = .failure; w.mode = .failure;
return error.ReadFailed; return error.WriteFailed;
}; };
} }
return 0; return 0;
}, },
error.Canceled => {
w.err = error.Canceled;
return error.WriteFailed;
},
else => |e| { else => |e| {
w.sendfile_err = e; w.write_file_err = e;
return 0; return error.WriteFailed;
}, },
}; };
if (n == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
file_reader.pos += n;
w.pos += n; w.pos += n;
return n; return w.interface.consume(n);
} }
const copy_file_range = switch (native_os) { fn sendFileStreaming(w: *Writer, file_reader: *Io.File.Reader, limit: Io.Limit) Io.Writer.FileError!usize {
.freebsd => std.os.freebsd.copy_file_range, const io = w.io;
.linux => std.os.linux.wrapped.copy_file_range, const header = w.interface.buffered();
else => {}, const n = io.vtable.fileSendFileStreaming(io.userdata, w.file, header, file_reader, limit) catch |err| switch (err) {
}; error.Canceled => {
if (@TypeOf(copy_file_range) != void) cfr: { w.err = error.Canceled;
if (w.copy_file_range_err != null) break :cfr; return error.WriteFailed;
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) {
.positional_reading, .streaming_reading => return error.Unimplemented,
.positional => p: {
off_in = @intCast(file_reader.pos);
break :p &off_in;
}, },
.streaming => null, else => |e| {
.failure => return error.WriteFailed, w.write_file_err = e;
}; return error.WriteFailed;
const off_out_ptr: ?*i64 = switch (w.mode) {
.positional_reading, .streaming_reading => return error.Unimplemented,
.positional => p: {
off_out = @intCast(w.pos);
break :p &off_out;
}, },
.streaming => null,
.failure => return error.WriteFailed,
}; };
const n = copy_file_range(in_fd, off_in_ptr, out_fd, off_out_ptr, @intFromEnum(limit), 0) catch |err| {
w.copy_file_range_err = err;
return 0;
};
if (n == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
file_reader.pos += n;
w.pos += n; w.pos += n;
return n; return w.interface.consume(n);
} }
if (builtin.os.tag.isDarwin()) fcf: { pub fn seekTo(w: *Writer, offset: u64) (SeekError || Io.Writer.Error)!void {
if (w.fcopyfile_err != null) break :fcf;
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 => {},
.INVAL => if (builtin.mode == .Debug) @panic("invalid API usage") else {
w.fcopyfile_err = error.Unexpected;
return 0;
},
.NOMEM => {
w.fcopyfile_err = error.OutOfMemory;
return 0;
},
.OPNOTSUPP => {
w.fcopyfile_err = error.OperationNotSupported;
return 0;
},
else => |err| {
w.fcopyfile_err = posix.unexpectedErrno(err);
return 0;
},
}
file_reader.pos = size;
w.pos = size;
return size;
}
return error.Unimplemented;
}
fn sendFileBuffered(
io_w: *Io.Writer,
file_reader: *Io.File.Reader,
reader_buffered: []const u8,
) Io.Writer.FileError!usize {
const n = try drain(io_w, &.{reader_buffered}, 1);
file_reader.seekBy(@intCast(n)) catch return error.ReadFailed;
return n;
}
pub fn seekTo(w: *Writer, offset: u64) (Writer.SeekError || Io.Writer.Error)!void {
try w.interface.flush(); try w.interface.flush();
try seekToUnbuffered(w, offset); try seekToUnbuffered(w, offset);
} }
/// Asserts that no data is currently buffered. /// Asserts that no data is currently buffered.
pub fn seekToUnbuffered(w: *Writer, offset: u64) Writer.SeekError!void { pub fn seekToUnbuffered(w: *Writer, offset: u64) SeekError!void {
assert(w.interface.buffered().len == 0); assert(w.interface.buffered().len == 0);
const io = w.io;
switch (w.mode) { switch (w.mode) {
.positional, .positional_reading => { .positional, .positional_reading => {
w.pos = offset; w.pos = offset;
}, },
.streaming, .streaming_reading => { .streaming, .streaming_reading => {
if (w.seek_err) |err| return err; if (w.seek_err) |err| return err;
posix.lseek_SET(w.file.handle, offset) catch |err| { io.vtable.fileSeekTo(io.userdata, w.file, offset) catch |err| {
w.seek_err = err; w.seek_err = err;
return err; return err;
}; };
@ -553,7 +215,7 @@ pub fn end(w: *Writer) EndError!void {
switch (w.mode) { switch (w.mode) {
.positional, .positional,
.positional_reading, .positional_reading,
=> w.file.setEndPos(w.pos) catch |err| switch (err) { => w.file.setLength(w.pos) catch |err| switch (err) {
error.NonResizable => return, error.NonResizable => return,
else => |e| return e, else => |e| return e,
}, },

View file

@ -3,6 +3,7 @@ const Threaded = @This();
const builtin = @import("builtin"); const builtin = @import("builtin");
const native_os = builtin.os.tag; const native_os = builtin.os.tag;
const is_windows = native_os == .windows; const is_windows = native_os == .windows;
const is_darwin = native_os.isDarwin();
const windows = std.os.windows; const windows = std.os.windows;
const ws2_32 = std.os.windows.ws2_32; const ws2_32 = std.os.windows.ws2_32;
const is_debug = builtin.mode == .Debug; const is_debug = builtin.mode == .Debug;
@ -70,6 +71,10 @@ have_signal_handler: bool,
old_sig_io: if (have_sig_io) posix.Sigaction else void, old_sig_io: if (have_sig_io) posix.Sigaction else void,
old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void, old_sig_pipe: if (have_sig_pipe) posix.Sigaction else void,
use_sendfile: UseSendfile = .default,
use_copy_file_range: UseCopyFileRange = .default,
use_fcopyfile: UseFcopyfile = .default,
pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum { pub const RobustCancel = if (std.Thread.use_pthreads or native_os == .linux) enum {
enabled, enabled,
disabled, disabled,
@ -82,6 +87,33 @@ pub const Pid = if (native_os == .linux) enum(posix.pid_t) {
_, _,
} else enum(u0) { unknown = 0 }; } else enum(u0) { unknown = 0 };
pub const UseSendfile = if (have_sendfile) enum {
enabled,
disabled,
pub const default: UseSendfile = .enabled;
} else enum {
disabled,
pub const default: UseSendfile = .disabled;
};
pub const UseCopyFileRange = if (have_copy_file_range) enum {
enabled,
disabled,
pub const default: UseCopyFileRange = .enabled;
} else enum {
disabled,
pub const default: UseCopyFileRange = .disabled;
};
pub const UseFcopyfile = if (have_fcopyfile) enum {
enabled,
disabled,
pub const default: UseFcopyfile = .enabled;
} else enum {
disabled,
pub const default: UseFcopyfile = .disabled;
};
const Thread = struct { const Thread = struct {
/// The value that needs to be passed to pthread_kill or tgkill in order to /// The value that needs to be passed to pthread_kill or tgkill in order to
/// send a signal. /// send a signal.
@ -436,6 +468,8 @@ pub fn io(t: *Threaded) Io {
.fileClose = fileClose, .fileClose = fileClose,
.fileWriteStreaming = fileWriteStreaming, .fileWriteStreaming = fileWriteStreaming,
.fileWritePositional = fileWritePositional, .fileWritePositional = fileWritePositional,
.fileWriteFileStreaming = fileWriteFileStreaming,
.fileWriteFilePositional = fileWriteFilePositional,
.fileReadStreaming = fileReadStreaming, .fileReadStreaming = fileReadStreaming,
.fileReadPositional = fileReadPositional, .fileReadPositional = fileReadPositional,
.fileSeekBy = fileSeekBy, .fileSeekBy = fileSeekBy,
@ -556,6 +590,8 @@ pub fn ioBasic(t: *Threaded) Io {
.fileClose = fileClose, .fileClose = fileClose,
.fileWriteStreaming = fileWriteStreaming, .fileWriteStreaming = fileWriteStreaming,
.fileWritePositional = fileWritePositional, .fileWritePositional = fileWritePositional,
.fileWriteFileStreaming = fileWriteFileStreaming,
.fileWriteFilePositional = fileWriteFilePositional,
.fileReadStreaming = fileReadStreaming, .fileReadStreaming = fileReadStreaming,
.fileReadPositional = fileReadPositional, .fileReadPositional = fileReadPositional,
.fileSeekBy = fileSeekBy, .fileSeekBy = fileSeekBy,
@ -596,7 +632,7 @@ pub fn ioBasic(t: *Threaded) Io {
}; };
} }
pub const socket_flags_unsupported = native_os.isDarwin() or native_os == .haiku; pub const socket_flags_unsupported = is_darwin or native_os == .haiku;
const have_accept4 = !socket_flags_unsupported; const have_accept4 = !socket_flags_unsupported;
const have_flock_open_flags = @hasField(posix.O, "EXLOCK"); const have_flock_open_flags = @hasField(posix.O, "EXLOCK");
const have_networking = native_os != .wasi; const have_networking = native_os != .wasi;
@ -612,6 +648,12 @@ const have_preadv = switch (native_os) {
}; };
const have_sig_io = posix.SIG != void and @hasField(posix.SIG, "IO"); const have_sig_io = posix.SIG != void and @hasField(posix.SIG, "IO");
const have_sig_pipe = posix.SIG != void and @hasField(posix.SIG, "PIPE"); const have_sig_pipe = posix.SIG != void and @hasField(posix.SIG, "PIPE");
const have_sendfile = if (builtin.link_libc) @TypeOf(std.c.sendfile) != void else native_os == .linux;
const have_copy_file_range = switch (native_os) {
.linux, .freebsd => true,
else => false,
};
const have_fcopyfile = is_darwin;
const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat; const openat_sym = if (posix.lfs64_abi) posix.system.openat64 else posix.system.openat;
const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat; const fstat_sym = if (posix.lfs64_abi) posix.system.fstat64 else posix.system.fstat;
@ -619,6 +661,18 @@ const fstatat_sym = if (posix.lfs64_abi) posix.system.fstatat64 else posix.syste
const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek; const lseek_sym = if (posix.lfs64_abi) posix.system.lseek64 else posix.system.lseek;
const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv; const preadv_sym = if (posix.lfs64_abi) posix.system.preadv64 else posix.system.preadv;
const ftruncate_sym = if (posix.lfs64_abi) posix.system.ftruncate64 else posix.system.ftruncate; const ftruncate_sym = if (posix.lfs64_abi) posix.system.ftruncate64 else posix.system.ftruncate;
const pwritev_sym = if (posix.lfs64_abi) posix.system.pwritev64 else posix.system.pwritev;
const sendfile_sym = if (posix.lfs64_abi) posix.system.sendfile64 else posix.system.sendfile;
const linux_copy_file_range_use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) .{
.major = 34,
.minor = 0,
.patch = 0,
} else .{
.major = 2,
.minor = 27,
.patch = 0,
});
const linux_copy_file_range_sys = if (linux_copy_file_range_use_c) std.c else std.os.linux;
/// Trailing data: /// Trailing data:
/// 1. context /// 1. context
@ -5715,27 +5769,828 @@ fn openSelfExe(userdata: ?*anyopaque, flags: Io.File.OpenFlags) Io.File.OpenSelf
fn fileWritePositional( fn fileWritePositional(
userdata: ?*anyopaque, userdata: ?*anyopaque,
file: Io.File, file: Io.File,
buffer: [][]const u8, header: []const u8,
data: []const []const u8,
splat: usize,
offset: u64, offset: u64,
) Io.File.WritePositionalError!usize { ) Io.File.WritePositionalError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata)); const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t; const current_thread = Thread.getCurrent(t);
if (is_windows) @panic("TODO");
var iovecs: [max_iovecs_len]posix.iovec_const = undefined;
var iovlen: iovlen_t = 0;
addBuf(&iovecs, &iovlen, header);
for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &iovlen, bytes);
const pattern = data[data.len - 1];
if (iovecs.len - iovlen != 0) switch (splat) {
0 => {},
1 => addBuf(&iovecs, &iovlen, pattern),
else => switch (pattern.len) {
0 => {},
1 => {
var backup_buffer: [splat_buffer_size]u8 = undefined;
const splat_buffer = &backup_buffer;
const memset_len = @min(splat_buffer.len, splat);
const buf = splat_buffer[0..memset_len];
@memset(buf, pattern[0]);
addBuf(&iovecs, &iovlen, buf);
var remaining_splat = splat - buf.len;
while (remaining_splat > splat_buffer.len and iovecs.len - iovlen != 0) {
assert(buf.len == splat_buffer.len);
addBuf(&iovecs, &iovlen, splat_buffer);
remaining_splat -= splat_buffer.len;
}
addBuf(&iovecs, &iovlen, splat_buffer[0..remaining_splat]);
},
else => for (0..@min(splat, iovecs.len - iovlen)) |_| {
addBuf(&iovecs, &iovlen, pattern);
},
},
};
if (native_os == .wasi and !builtin.link_libc) {
var n_written: usize = undefined;
try current_thread.beginSyscall();
while (true) { while (true) {
_ = file; switch (std.os.wasi.fd_pwrite(file.handle, &iovecs, iovlen, offset, &n_written)) {
_ = buffer; .SUCCESS => {
_ = offset; current_thread.endSyscall();
@panic("TODO implement fileWritePositional"); return n_written;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.AGAIN => |err| return errnoBug(err),
.BADF => return error.NotOpenForWriting, // can be a race condition.
.DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.NOTCAPABLE => return error.AccessDenied,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
}
},
}
} }
} }
fn fileWriteStreaming(userdata: ?*anyopaque, file: Io.File, buffer: [][]const u8) Io.File.WriteStreamingError!usize { try current_thread.beginSyscall();
const t: *Threaded = @ptrCast(@alignCast(userdata));
_ = t;
while (true) { while (true) {
_ = file; const rc = pwritev_sym(file.handle, &iovecs, iovlen, @bitCast(offset));
_ = buffer; switch (posix.errno(rc)) {
@panic("TODO implement fileWriteStreaming"); .SUCCESS => {
current_thread.endSyscall();
return @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => return error.InvalidArgument,
.FAULT => |err| return errnoBug(err),
.AGAIN => return error.WouldBlock,
.BADF => return error.NotOpenForWriting, // Usually a race condition.
.DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.CONNRESET => return error.ConnectionResetByPeer,
.BUSY => return error.DeviceBusy,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
.OVERFLOW => return error.Unseekable,
else => |err| return posix.unexpectedErrno(err),
} }
},
}
}
}
fn fileWriteStreaming(
userdata: ?*anyopaque,
file: Io.File,
header: []const u8,
data: []const []const u8,
splat: usize,
) Io.File.WriteStreamingError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const current_thread = Thread.getCurrent(t);
if (is_windows) @panic("TODO");
var iovecs: [max_iovecs_len]posix.iovec_const = undefined;
var iovlen: iovlen_t = 0;
addBuf(&iovecs, &iovlen, header);
for (data[0 .. data.len - 1]) |bytes| addBuf(&iovecs, &iovlen, bytes);
const pattern = data[data.len - 1];
if (iovecs.len - iovlen != 0) switch (splat) {
0 => {},
1 => addBuf(&iovecs, &iovlen, pattern),
else => switch (pattern.len) {
0 => {},
1 => {
var backup_buffer: [splat_buffer_size]u8 = undefined;
const splat_buffer = &backup_buffer;
const memset_len = @min(splat_buffer.len, splat);
const buf = splat_buffer[0..memset_len];
@memset(buf, pattern[0]);
addBuf(&iovecs, &iovlen, buf);
var remaining_splat = splat - buf.len;
while (remaining_splat > splat_buffer.len and iovecs.len - iovlen != 0) {
assert(buf.len == splat_buffer.len);
addBuf(&iovecs, &iovlen, splat_buffer);
remaining_splat -= splat_buffer.len;
}
addBuf(&iovecs, &iovlen, splat_buffer[0..remaining_splat]);
},
else => for (0..@min(splat, iovecs.len - iovlen)) |_| {
addBuf(&iovecs, &iovlen, pattern);
},
},
};
if (native_os == .wasi and !builtin.link_libc) {
var n_written: usize = undefined;
try current_thread.beginSyscall();
while (true) {
switch (std.os.wasi.fd_write(file.handle, &iovecs, iovlen, &n_written)) {
.SUCCESS => {
current_thread.endSyscall();
return n_written;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => |err| return errnoBug(err),
.FAULT => |err| return errnoBug(err),
.AGAIN => |err| return errnoBug(err),
.BADF => return error.NotOpenForWriting, // can be a race condition.
.DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
try current_thread.beginSyscall();
while (true) {
const rc = posix.system.writev(file.handle, &iovecs, iovlen);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
return @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
switch (e) {
.INVAL => return error.InvalidArgument,
.FAULT => |err| return errnoBug(err),
.SRCH => return error.ProcessNotFound,
.AGAIN => return error.WouldBlock,
.BADF => return error.NotOpenForWriting, // Can be a race condition.
.DESTADDRREQ => |err| return errnoBug(err), // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.CONNRESET => return error.ConnectionResetByPeer,
.BUSY => return error.DeviceBusy,
else => |err| return posix.unexpectedErrno(err),
}
},
}
}
}
fn fileWriteFileStreaming(
userdata: ?*anyopaque,
file: Io.File,
header: []const u8,
file_reader: *Io.File.Reader,
limit: Io.Limit,
) Io.File.WriteFileStreamingError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const reader_buffered = file_reader.interface.buffered();
if (reader_buffered.len >= @intFromEnum(limit)) {
const n = try fileWriteStreaming(t, file, header, &.{limit.slice(reader_buffered)}, 1);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
}
const file_limit = @intFromEnum(limit) - reader_buffered.len;
const out_fd = file.handle;
const in_fd = file_reader.file.handle;
if (file_reader.size) |size| {
if (size - file_reader.pos == 0) {
if (reader_buffered.len != 0) {
const n = try fileWriteStreaming(t, file, header, &.{limit.slice(reader_buffered)}, 1);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
} else {
return error.EndOfStream;
}
}
}
if (native_os == .freebsd) sf: {
// Try using sendfile on FreeBSD.
if (@atomicLoad(UseSendfile, &t.use_sendfile, .monotonic) == .disabled) 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 (header.len != 0) {
headers[headers_i] = .{ .base = header.ptr, .len = header.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 = 0;
const nbytes: usize = @min(file_limit, std.math.maxInt(usize));
const flags = 0;
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, nbytes, hdtr, &sbytes, flags))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INVAL, .OPNOTSUPP, .NOTSOCK, .NOSYS => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseSendfile, &t.use_sendfile, .disabled, .monotonic);
return 0;
},
.INTR, .BUSY => {
if (sbytes == 0) {
try current_thread.checkCancel();
continue;
} else {
// Even if we are being canceled, there have been side
// effects, so it is better to report those side
// effects to the caller.
current_thread.endSyscall();
break;
}
},
.AGAIN => {
current_thread.endSyscall();
if (sbytes == 0) return error.WouldBlock;
break;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.NOTCONN => return error.BrokenPipe,
.IO => return error.InputOutput,
.PIPE => return error.BrokenPipe,
.NOBUFS => return error.SystemResources,
.BADF => |err| errnoBug(err),
.FAULT => |err| errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
// Give calling code chance to observe the error before trying
// something else.
@atomicStore(UseSendfile, &t.use_sendfile, .disabled, .monotonic);
return 0;
},
}
}
if (sbytes == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
const ubytes: usize = @intCast(sbytes);
file_reader.seekBy(ubytes -| header.len) catch return error.ReadFailed;
return ubytes;
}
if (is_darwin) sf: {
// Try using sendfile on macOS.
if (@atomicLoad(UseSendfile, &t.use_sendfile, .monotonic) == .disabled) 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 (header.len != 0) {
headers[headers_i] = .{ .base = header.ptr, .len = header.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 = std.math.maxInt(i32); // Avoid EINVAL.
var len: std.c.off_t = @min(file_limit, max_count);
const flags = 0;
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
switch (posix.errno(std.c.sendfile(in_fd, out_fd, offset, &len, hdtr, flags))) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.OPNOTSUPP, .NOTSOCK, .NOSYS => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseSendfile, &t.use_sendfile, .disabled, .monotonic);
return 0;
},
.INTR => {
if (len == 0) {
try current_thread.checkCancel();
continue;
} else {
// Even if we are being canceled, there have been side
// effects, so it is better to report those side
// effects to the caller.
current_thread.endSyscall();
break;
}
},
.AGAIN => {
current_thread.endSyscall();
if (len == 0) return error.WouldBlock;
break;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.NOTCONN => return error.BrokenPipe,
.IO => return error.InputOutput,
.PIPE => return error.BrokenPipe,
.BADF => |err| errnoBug(err),
.FAULT => |err| errnoBug(err),
.INVAL => |err| errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
// Give calling code chance to observe the error before trying
// something else.
@atomicStore(UseSendfile, &t.use_sendfile, .disabled, .monotonic);
return 0;
},
}
}
if (len == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
const u_len: usize = @bitCast(len);
file_reader.seekBy(u_len -| header.len) catch return error.ReadFailed;
return u_len;
}
if (native_os == .linux) sf: {
// Try using sendfile on Linux.
if (@atomicLoad(UseSendfile, &t.use_sendfile, .monotonic) == .disabled) break :sf;
// Linux sendfile does not support headers.
if (header.len != 0 or reader_buffered.len != 0) {
const n = try fileWriteStreaming(t, file, header, &.{limit.slice(reader_buffered)}, 1);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
}
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) {
.positional => o: {
const size = file_reader.getSize() catch return 0;
off = std.math.cast(std.os.linux.off_t, file_reader.pos) orelse return error.ReadFailed;
break :o .{ &off, @min(@intFromEnum(limit), size - file_reader.pos, max_count) };
},
.streaming => .{ null, limit.minInt(max_count) },
.streaming_reading, .positional_reading => break :sf,
.failure => return error.ReadFailed,
};
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
const n: usize = while (true) {
const rc = sendfile_sym(out_fd, in_fd, off_ptr, count);
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break @intCast(rc);
},
.NOSYS, .INVAL => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseSendfile, &t.use_sendfile, .disabled, .monotonic);
return 0;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.NOTCONN => return error.BrokenPipe, // `out_fd` is an unconnected socket
.AGAIN => return error.WouldBlock,
.IO => return error.InputOutput,
.PIPE => return error.BrokenPipe,
.NOMEM => return error.SystemResources,
.NXIO, .SPIPE => {
file_reader.mode = file_reader.mode.toStreaming();
const pos = file_reader.pos;
if (pos != 0) {
file_reader.pos = 0;
file_reader.seekBy(@intCast(pos)) catch {
file_reader.mode = .failure;
return error.ReadFailed;
};
}
return 0;
},
.BADF => |err| errnoBug(err), // Always a race condition.
.FAULT => |err| errnoBug(err), // Segmentation fault.
.OVERFLOW => |err| errnoBug(err), // We avoid passing too large of a `count`.
else => |err| posix.unexpectedErrno(err),
});
// Give calling code chance to observe the error before trying
// something else.
@atomicStore(UseSendfile, &t.use_sendfile, .disabled, .monotonic);
return 0;
},
}
};
if (n == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
file_reader.pos += n;
return n;
}
if (have_copy_file_range) cfr: {
if (@atomicLoad(UseCopyFileRange, &t.use_copy_file_range, .monotonic) == .disabled) break :cfr;
if (header.len != 0 or reader_buffered.len != 0) {
const n = try fileWriteStreaming(t, file, header, &.{limit.slice(reader_buffered)}, 1);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
}
var off_in: i64 = undefined;
const off_in_ptr: ?*i64 = switch (file_reader.mode) {
.positional_reading, .streaming_reading => return error.Unimplemented,
.positional => p: {
off_in = @intCast(file_reader.pos);
break :p &off_in;
},
.streaming => null,
.failure => return error.WriteFailed,
};
const current_thread = Thread.getCurrent(t);
const n: usize = switch (native_os) {
.linux => n: {
try current_thread.beginSyscall();
while (true) {
const rc = linux_copy_file_range_sys.copy_file_range(in_fd, off_in_ptr, out_fd, null, @intFromEnum(limit), 0);
switch (linux_copy_file_range_sys.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break :n @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.OPNOTSUPP, .INVAL, .NOSYS => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.OVERFLOW => return error.Overflow,
.PERM => return error.PermissionDenied,
.TXTBSY => return error.SwapFile,
.XDEV => return error.NotSameFileSystem,
.BADF => |err| errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
}
}
},
.freebsd => n: {
try current_thread.beginSyscall();
while (true) {
const rc = std.c.copy_file_range(in_fd, off_in_ptr, out_fd, null, @intFromEnum(limit), 0);
switch (std.c.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break :n @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.OPNOTSUPP, .INVAL, .NOSYS => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.INTEGRITY => return error.CorruptedData,
.ISDIR => return error.IsDir,
.NOSPC => return error.NoSpaceLeft,
.BADF => |err| errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
}
}
},
else => comptime unreachable,
};
if (n == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
file_reader.pos += n;
return n;
}
return error.Unimplemented;
}
fn fileWriteFilePositional(
userdata: ?*anyopaque,
file: Io.File,
header: []const u8,
file_reader: *Io.File.Reader,
limit: Io.Limit,
offset: u64,
) Io.File.WriteFilePositionalError!usize {
const t: *Threaded = @ptrCast(@alignCast(userdata));
const reader_buffered = file_reader.interface.buffered();
if (reader_buffered.len >= @intFromEnum(limit)) {
const n = try fileWritePositional(t, file, header, &.{limit.slice(reader_buffered)}, 1, offset);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
}
const out_fd = file.handle;
const in_fd = file_reader.file.handle;
if (file_reader.size) |size| {
if (size - file_reader.pos == 0) {
if (reader_buffered.len != 0) {
const n = try fileWritePositional(t, file, header, &.{limit.slice(reader_buffered)}, 1, offset);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
} else {
return error.EndOfStream;
}
}
}
if (have_copy_file_range) cfr: {
if (@atomicLoad(UseCopyFileRange, &t.use_copy_file_range, .monotonic) == .disabled) break :cfr;
if (header.len != 0 or reader_buffered.len != 0) {
const n = try fileWritePositional(t, file, header, &.{limit.slice(reader_buffered)}, 1, offset);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
}
var off_in: i64 = undefined;
const off_in_ptr: ?*i64 = switch (file_reader.mode) {
.positional_reading, .streaming_reading => return error.Unimplemented,
.positional => p: {
off_in = @intCast(file_reader.pos);
break :p &off_in;
},
.streaming => null,
.failure => return error.WriteFailed,
};
var off_out: i64 = @intCast(offset);
const current_thread = Thread.getCurrent(t);
const n: usize = switch (native_os) {
.linux => n: {
try current_thread.beginSyscall();
while (true) {
const rc = linux_copy_file_range_sys.copy_file_range(in_fd, off_in_ptr, out_fd, &off_out, @intFromEnum(limit), 0);
switch (linux_copy_file_range_sys.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break :n @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.CANCELED => return current_thread.endSyscallCanceled(),
.OPNOTSUPP, .INVAL, .NOSYS => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOMEM => return error.SystemResources,
.NOSPC => return error.NoSpaceLeft,
.OVERFLOW => return error.Unseekable,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
.PERM => return error.PermissionDenied,
.TXTBSY => return error.SwapFile,
.XDEV => return error.NotSameFileSystem,
.BADF => |err| errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
}
}
},
.freebsd => n: {
try current_thread.beginSyscall();
while (true) {
const rc = std.c.copy_file_range(in_fd, off_in_ptr, out_fd, &off_out, @intFromEnum(limit), 0);
switch (std.c.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break :n @intCast(rc);
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.OPNOTSUPP, .INVAL, .NOSYS => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.INTEGRITY => return error.CorruptedData,
.ISDIR => return error.IsDir,
.NOSPC => return error.NoSpaceLeft,
.OVERFLOW => return error.Unseekable,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
.BADF => |err| errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
@atomicStore(UseCopyFileRange, &t.use_copy_file_range, .disabled, .monotonic);
return 0;
},
}
}
},
else => comptime unreachable,
};
if (n == 0) {
file_reader.size = file_reader.pos;
return error.EndOfStream;
}
file_reader.pos += n;
return n;
}
if (is_darwin) fcf: {
if (@atomicLoad(UseFcopyfile, &t.use_fcopyfile, .monotonic) == .disabled) break :fcf;
if (file_reader.pos != 0) break :fcf;
if (offset != 0) break :fcf;
if (limit != .unlimited) break :fcf;
const size = file_reader.getSize() catch break :fcf;
if (header.len != 0 or reader_buffered.len != 0) {
const n = try fileWritePositional(t, file, header, &.{limit.slice(reader_buffered)}, 1, offset);
file_reader.seekBy(n -| header.len) catch return error.ReadFailed;
return n;
}
const current_thread = Thread.getCurrent(t);
try current_thread.beginSyscall();
while (true) {
const rc = std.c.fcopyfile(in_fd, out_fd, null, .{ .DATA = true });
switch (posix.errno(rc)) {
.SUCCESS => {
current_thread.endSyscall();
break;
},
.INTR => {
try current_thread.checkCancel();
continue;
},
.OPNOTSUPP => {
// Give calling code chance to observe before trying
// something else.
current_thread.endSyscall();
@atomicStore(UseFcopyfile, &t.use_fcopyfile, .disabled, .monotonic);
return 0;
},
else => |e| {
current_thread.endSyscall();
assert(error.Unexpected == switch (e) {
.NOMEM => return error.SystemResources,
.INVAL => |err| posix.errnoBug(err),
else => |err| posix.unexpectedErrno(err),
});
return 0;
},
}
}
file_reader.pos = size;
return size;
}
return error.Unimplemented;
} }
fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp { fn nowPosix(userdata: ?*anyopaque, clock: Io.Clock) Io.Clock.Error!Io.Timestamp {
@ -7666,6 +8521,7 @@ fn netWritePosix(
}, },
}; };
const flags = posix.MSG.NOSIGNAL; const flags = posix.MSG.NOSIGNAL;
try current_thread.beginSyscall(); try current_thread.beginSyscall();
while (true) { while (true) {
const rc = posix.system.sendmsg(fd, &msg, flags); const rc = posix.system.sendmsg(fd, &msg, flags);
@ -7829,7 +8685,11 @@ fn netWriteUnavailable(
return error.NetworkDown; return error.NetworkDown;
} }
fn addBuf(v: []posix.iovec_const, i: *@FieldType(posix.msghdr_const, "iovlen"), bytes: []const u8) void { /// This is either usize or u32. Since, either is fine, let's use the same
/// `addBuf` function for both writing to a file and sending network messages.
const iovlen_t = @FieldType(posix.msghdr_const, "iovlen");
fn addBuf(v: []posix.iovec_const, i: *iovlen_t, bytes: []const u8) void {
// OS checks ptr addr before length so zero length vectors must be omitted. // OS checks ptr addr before length so zero length vectors must be omitted.
if (bytes.len == 0) return; if (bytes.len == 0) return;
if (v.len - i.* == 0) return; if (v.len - i.* == 0) return;
@ -8152,7 +9012,7 @@ fn netLookupFallible(
// TODO use dnsres_getaddrinfo // TODO use dnsres_getaddrinfo
} }
if (native_os.isDarwin()) { if (is_darwin) {
// TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution // TODO use CFHostStartInfoResolution / CFHostCancelInfoResolution
} }

View file

@ -30,7 +30,6 @@ pub const uefi = @import("os/uefi.zig");
pub const wasi = @import("os/wasi.zig"); pub const wasi = @import("os/wasi.zig");
pub const emscripten = @import("os/emscripten.zig"); pub const emscripten = @import("os/emscripten.zig");
pub const windows = @import("os/windows.zig"); pub const windows = @import("os/windows.zig");
pub const freebsd = @import("os/freebsd.zig");
test { test {
_ = linux; _ = linux;

View file

@ -1,50 +0,0 @@
const std = @import("../std.zig");
const fd_t = std.c.fd_t;
const off_t = std.c.off_t;
const unexpectedErrno = std.posix.unexpectedErrno;
const errno = std.posix.errno;
const builtin = @import("builtin");
pub const CopyFileRangeError = std.posix.UnexpectedError || error{
/// If infd is not open for reading or outfd is not open for writing, or
/// opened for writing with O_APPEND, or if infd and outfd refer to the
/// same file.
BadFileFlags,
/// If the copy exceeds the process's file size limit or the maximum
/// file size for the file system outfd re- sides on.
FileTooBig,
/// A signal interrupted the system call before it could be completed.
/// This may happen for files on some NFS mounts. When this happens,
/// the values pointed to by inoffp and outoffp are reset to the
/// initial values for the system call.
Interrupted,
/// One of:
/// * infd and outfd refer to the same file and the byte ranges overlap.
/// * The flags argument is not zero.
/// * Either infd or outfd refers to a file object that is not a regular file.
InvalidArguments,
/// An I/O error occurred while reading/writing the files.
InputOutput,
/// Corrupted data was detected while reading from a file system.
CorruptedData,
/// Either infd or outfd refers to a directory.
IsDir,
/// File system that stores outfd is full.
NoSpaceLeft,
};
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 {
const rc = std.c.copy_file_range(fd_in, off_in, fd_out, off_out, len, flags);
switch (errno(rc)) {
.SUCCESS => return @intCast(rc),
.BADF => return error.BadFileFlags,
.FBIG => return error.FileTooBig,
.INTR => return error.Interrupted,
.INVAL => return error.InvalidArguments,
.IO => return error.InputOutput,
.INTEGRITY => return error.CorruptedData,
.ISDIR => return error.IsDir,
.NOSPC => return error.NoSpaceLeft,
else => |err| return unexpectedErrno(err),
}
}

View file

@ -9855,125 +9855,3 @@ pub const cmsghdr = extern struct {
level: i32, level: i32,
type: i32, type: i32,
}; };
/// The syscalls, but with Zig error sets, going through libc if linking libc,
/// and with some footguns eliminated.
pub const wrapped = struct {
pub const lfs64_abi = builtin.link_libc and (builtin.abi.isGnu() or builtin.abi.isAndroid());
const system = if (builtin.link_libc) std.c else std.os.linux;
pub const SendfileError = std.posix.UnexpectedError || error{
/// `out_fd` is an unconnected socket, or out_fd closed its read end.
BrokenPipe,
/// Descriptor is not valid or locked, or an mmap(2)-like operation is not available for in_fd.
UnsupportedOperation,
/// Nonblocking I/O has been selected but the write would block.
WouldBlock,
/// Unspecified error while reading from in_fd.
InputOutput,
/// Insufficient kernel memory to read from in_fd.
SystemResources,
/// `offset` is not `null` but the input file is not seekable.
Unseekable,
};
pub fn sendfile(
out_fd: fd_t,
in_fd: fd_t,
in_offset: ?*off_t,
in_len: usize,
) SendfileError!usize {
const adjusted_len = @min(in_len, 0x7ffff000); // Prevents EOVERFLOW.
const sendfileSymbol = if (lfs64_abi) system.sendfile64 else system.sendfile;
const rc = sendfileSymbol(out_fd, in_fd, in_offset, adjusted_len);
switch (system.errno(rc)) {
.SUCCESS => return @intCast(rc),
.BADF => return invalidApiUsage(), // Always a race condition.
.FAULT => return invalidApiUsage(), // Segmentation fault.
.OVERFLOW => return unexpectedErrno(.OVERFLOW), // We avoid passing too large of a `count`.
.NOTCONN => return error.BrokenPipe, // `out_fd` is an unconnected socket
.INVAL => return error.UnsupportedOperation,
.AGAIN => return error.WouldBlock,
.IO => return error.InputOutput,
.PIPE => return error.BrokenPipe,
.NOMEM => return error.SystemResources,
.NXIO => return error.Unseekable,
.SPIPE => return error.Unseekable,
else => |err| return unexpectedErrno(err),
}
}
pub const CopyFileRangeError = std.posix.UnexpectedError || error{
/// One of:
/// * One or more file descriptors are not valid.
/// * fd_in is not open for reading; or fd_out is not open for writing.
/// * The O_APPEND flag is set for the open file description referred
/// to by the file descriptor fd_out.
BadFileFlags,
/// One of:
/// * An attempt was made to write at a position past the maximum file
/// offset the kernel supports.
/// * An attempt was made to write a range that exceeds the allowed
/// maximum file size. The maximum file size differs between
/// filesystem implementations and can be different from the maximum
/// allowed file offset.
/// * An attempt was made to write beyond the process's file size
/// resource limit. This may also result in the process receiving a
/// SIGXFSZ signal.
FileTooBig,
/// One of:
/// * either fd_in or fd_out is not a regular file
/// * flags argument is not zero
/// * fd_in and fd_out refer to the same file and the source and target ranges overlap.
InvalidArguments,
/// A low-level I/O error occurred while copying.
InputOutput,
/// Either fd_in or fd_out refers to a directory.
IsDir,
OutOfMemory,
/// There is not enough space on the target filesystem to complete the copy.
NoSpaceLeft,
/// (since Linux 5.19) the filesystem does not support this operation.
OperationNotSupported,
/// The requested source or destination range is too large to represent
/// in the specified data types.
Overflow,
/// fd_out refers to an immutable file.
PermissionDenied,
/// Either fd_in or fd_out refers to an active swap file.
SwapFile,
/// The files referred to by fd_in and fd_out are not on the same
/// filesystem, and the source and target filesystems are not of the
/// same type, or do not support cross-filesystem copy.
NotSameFileSystem,
};
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 {
const use_c = std.c.versionCheck(if (builtin.abi.isAndroid()) .{ .major = 34, .minor = 0, .patch = 0 } else .{ .major = 2, .minor = 27, .patch = 0 });
const sys = if (use_c) std.c else std.os.linux;
const rc = sys.copy_file_range(fd_in, off_in, fd_out, off_out, len, flags);
switch (sys.errno(rc)) {
.SUCCESS => return @intCast(rc),
.BADF => return error.BadFileFlags,
.FBIG => return error.FileTooBig,
.INVAL => return error.InvalidArguments,
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOMEM => return error.OutOfMemory,
.NOSPC => return error.NoSpaceLeft,
.OPNOTSUPP => return error.OperationNotSupported,
.OVERFLOW => return error.Overflow,
.PERM => return error.PermissionDenied,
.TXTBSY => return error.SwapFile,
.XDEV => return error.NotSameFileSystem,
else => |err| return unexpectedErrno(err),
}
}
const unexpectedErrno = std.posix.unexpectedErrno;
fn invalidApiUsage() error{Unexpected} {
if (builtin.mode == .Debug) @panic("invalid API usage");
return error.Unexpected;
}
};

View file

@ -405,14 +405,6 @@ test "futex2_requeue" {
try expectEqual(0, rc); try expectEqual(0, rc);
} }
test "copy_file_range error" {
const fds = try std.posix.pipe();
defer std.posix.close(fds[0]);
defer std.posix.close(fds[1]);
try std.testing.expectError(error.InvalidArguments, linux.wrapped.copy_file_range(fds[0], null, fds[1], null, 1, 0));
}
test { test {
_ = linux.IoUring; _ = linux.IoUring;
} }

View file

@ -1140,80 +1140,6 @@ pub fn write(fd: fd_t, bytes: []const u8) WriteError!usize {
} }
} }
/// Write multiple buffers to a file descriptor.
/// Retries when interrupted by a signal.
/// Returns the number of bytes written. If nonzero bytes were supplied, this will be nonzero.
///
/// Note that a successful write() may transfer fewer bytes than supplied. Such partial writes can
/// occur for various reasons; for example, because there was insufficient space on the disk
/// device to write all of the requested bytes, or because a blocked write() to a socket, pipe, or
/// similar was interrupted by a signal handler after it had transferred some, but before it had
/// transferred all of the requested bytes. In the event of a partial write, the caller can make
/// another write() call to transfer the remaining bytes. The subsequent call will either
/// transfer further bytes or may result in an error (e.g., if the disk is now full).
///
/// For POSIX systems, if `fd` is opened in non blocking mode, the function will
/// return error.WouldBlock when EAGAIN is received.
/// On Windows, if the application has a global event loop enabled, I/O Completion Ports are
/// used to perform the I/O. `error.WouldBlock` is not possible on Windows.
///
/// If `iov.len` is larger than `IOV_MAX`, a partial write will occur.
///
/// This function assumes that all vectors, including zero-length vectors, have
/// a pointer within the address space of the application.
pub fn writev(fd: fd_t, iov: []const iovec_const) WriteError!usize {
if (native_os == .windows) {
// TODO improve this to use WriteFileScatter
if (iov.len == 0) return 0;
const first = iov[0];
return write(fd, first.base[0..first.len]);
}
if (native_os == .wasi and !builtin.link_libc) {
var nwritten: usize = undefined;
switch (wasi.fd_write(fd, iov.ptr, iov.len, &nwritten)) {
.SUCCESS => return nwritten,
.INTR => unreachable,
.INVAL => unreachable,
.FAULT => unreachable,
.AGAIN => unreachable,
.BADF => return error.NotOpenForWriting, // can be a race condition.
.DESTADDRREQ => unreachable, // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.NOTCAPABLE => return error.AccessDenied,
else => |err| return unexpectedErrno(err),
}
}
while (true) {
const rc = system.writev(fd, iov.ptr, @min(iov.len, IOV_MAX));
switch (errno(rc)) {
.SUCCESS => return @intCast(rc),
.INTR => continue,
.INVAL => return error.InvalidArgument,
.FAULT => unreachable,
.SRCH => return error.ProcessNotFound,
.AGAIN => return error.WouldBlock,
.BADF => return error.NotOpenForWriting, // Can be a race condition.
.DESTADDRREQ => unreachable, // `connect` was never called.
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.PermissionDenied,
.PIPE => return error.BrokenPipe,
.CONNRESET => return error.ConnectionResetByPeer,
.BUSY => return error.DeviceBusy,
.CANCELED => return error.Canceled,
else => |err| return unexpectedErrno(err),
}
}
}
pub const PWriteError = WriteError || error{Unseekable}; pub const PWriteError = WriteError || error{Unseekable};
/// Write to a file descriptor, with a position offset. /// Write to a file descriptor, with a position offset.
@ -4369,98 +4295,6 @@ pub fn send(
}; };
} }
pub const CopyFileRangeError = error{
FileTooBig,
InputOutput,
/// `fd_in` is not open for reading; or `fd_out` is not open for writing;
/// or the `APPEND` flag is set for `fd_out`.
FilesOpenedWithWrongFlags,
IsDir,
OutOfMemory,
NoSpaceLeft,
Unseekable,
PermissionDenied,
SwapFile,
CorruptedData,
} || PReadError || PWriteError || UnexpectedError;
/// Transfer data between file descriptors at specified offsets.
///
/// Returns the number of bytes written, which can less than requested.
///
/// The `copy_file_range` call copies `len` bytes from one file descriptor to another. When possible,
/// this is done within the operating system kernel, which can provide better performance
/// characteristics than transferring data from kernel to user space and back, such as with
/// `pread` and `pwrite` calls.
///
/// `fd_in` must be a file descriptor opened for reading, and `fd_out` must be a file descriptor
/// opened for writing. They may be any kind of file descriptor; however, if `fd_in` is not a regular
/// file system file, it may cause this function to fall back to calling `pread` and `pwrite`, in which case
/// atomicity guarantees no longer apply.
///
/// If `fd_in` and `fd_out` are the same, source and target ranges must not overlap.
/// The file descriptor seek positions are ignored and not updated.
/// When `off_in` is past the end of the input file, it successfully reads 0 bytes.
///
/// `flags` has different meanings per operating system; refer to the respective man pages.
///
/// These systems support in-kernel data copying:
/// * Linux (cross-filesystem from version 5.3)
/// * FreeBSD 13.0
///
/// Other systems fall back to calling `pread` / `pwrite`.
///
/// Maximum offsets on Linux and FreeBSD are `maxInt(i64)`.
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 {
if (builtin.os.tag == .freebsd or builtin.os.tag == .linux) {
const use_c = native_os != .linux or
std.c.versionCheck(if (builtin.abi.isAndroid()) .{ .major = 34, .minor = 0, .patch = 0 } else .{ .major = 2, .minor = 27, .patch = 0 });
const sys = if (use_c) std.c else linux;
var off_in_copy: i64 = @bitCast(off_in);
var off_out_copy: i64 = @bitCast(off_out);
while (true) {
const rc = sys.copy_file_range(fd_in, &off_in_copy, fd_out, &off_out_copy, len, flags);
if (native_os == .freebsd) {
switch (sys.errno(rc)) {
.SUCCESS => return @intCast(rc),
.BADF => return error.FilesOpenedWithWrongFlags,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOSPC => return error.NoSpaceLeft,
.INVAL => break, // these may not be regular files, try fallback
.INTEGRITY => return error.CorruptedData,
.INTR => continue,
else => |err| return unexpectedErrno(err),
}
} else { // assume linux
switch (sys.errno(rc)) {
.SUCCESS => return @intCast(rc),
.BADF => return error.FilesOpenedWithWrongFlags,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.ISDIR => return error.IsDir,
.NOSPC => return error.NoSpaceLeft,
.INVAL => break, // these may not be regular files, try fallback
.NOMEM => return error.OutOfMemory,
.OVERFLOW => return error.Unseekable,
.PERM => return error.PermissionDenied,
.TXTBSY => return error.SwapFile,
.XDEV => break, // support for cross-filesystem copy added in Linux 5.3, use fallback
else => |err| return unexpectedErrno(err),
}
}
}
}
var buf: [8 * 4096]u8 = undefined;
const amt_read = try pread(fd_in, buf[0..@min(buf.len, len)], off_in);
if (amt_read == 0) return 0;
return pwrite(fd_out, buf[0..amt_read], off_out);
}
pub const PollError = error{ pub const PollError = error{
/// The network subsystem has failed. /// The network subsystem has failed.
NetworkDown, NetworkDown,