use std::fs::File;
use std::io;
pub(crate) trait SendfileSocket {
fn sendfile_socket(&self) -> Option<&tokio::net::TcpStream> {
None
}
}
impl SendfileSocket for tokio::net::TcpStream {
fn sendfile_socket(&self) -> Option<&tokio::net::TcpStream> {
Some(self)
}
}
impl SendfileSocket for tokio_rustls::server::TlsStream<tokio::net::TcpStream> {}
impl SendfileSocket for tokio::io::DuplexStream {}
pub(crate) const SUPPORTED: bool = cfg!(any(target_os = "linux", target_os = "macos"));
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) async fn send_file(socket: &tokio::net::TcpStream, file: &File, len: u64) -> io::Result<()> {
use std::os::unix::io::AsRawFd;
let (file_fd, socket_fd) = (file.as_raw_fd(), socket.as_raw_fd());
let mut offset: u64 = 0;
while offset < len {
socket.writable().await?;
let remaining = len - offset;
match socket.try_io(tokio::io::Interest::WRITABLE, || sendfile_once(file_fd, socket_fd, offset, remaining)) {
Ok(0) => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "file shorter than its Content-Length")),
Ok(sent) => offset += sent as u64,
Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue,
Err(e) => return Err(e),
}
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub(crate) async fn send_file(_socket: &tokio::net::TcpStream, _file: &File, _len: u64) -> io::Result<()> {
Err(io::Error::new(io::ErrorKind::Unsupported, "sendfile is not available on this platform"))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
const MAX_CHUNK: u64 = 1 << 30;
#[cfg(target_os = "linux")]
fn sendfile_once(file_fd: i32, socket_fd: i32, offset: u64, count: u64) -> io::Result<usize> {
let mut off = offset as libc::off_t;
let sent = unsafe { libc::sendfile(socket_fd, file_fd, &mut off, count.min(MAX_CHUNK) as usize) };
if sent < 0 {
Err(io::Error::last_os_error())
} else {
Ok(sent as usize)
}
}
#[cfg(target_os = "macos")]
fn sendfile_once(file_fd: i32, socket_fd: i32, offset: u64, count: u64) -> io::Result<usize> {
let mut len = count.min(MAX_CHUNK) as libc::off_t;
let rc = unsafe { libc::sendfile(file_fd, socket_fd, offset as libc::off_t, &mut len, std::ptr::null_mut(), 0) };
if rc == -1 {
let err = io::Error::last_os_error();
if len > 0 && matches!(err.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted) {
return Ok(len as usize);
}
return Err(err);
}
Ok(len as usize)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::AsyncReadExt;
#[tokio::test]
async fn sends_a_whole_file_over_tcp() {
if !SUPPORTED {
return;
}
let path = crate::test_env::TempPath::new("sendfile_src.bin");
let content: Vec<u8> = (0..3 * 1024 * 1024 + 17).map(|i| (i % 253) as u8).collect();
std::fs::write(path.as_str(), &content).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let reader = tokio::spawn(async move {
let mut client = tokio::net::TcpStream::connect(addr).await.unwrap();
let mut received = Vec::new();
client.read_to_end(&mut received).await.unwrap();
received
});
let (server, _) = listener.accept().await.unwrap();
let file = File::open(path.as_str()).unwrap();
send_file(&server, &file, content.len() as u64).await.unwrap();
drop(server);
assert!(reader.await.unwrap() == content, "received bytes differ from the file");
}
#[tokio::test]
async fn a_file_shorter_than_promised_is_an_error() {
if !SUPPORTED {
return;
}
let path = crate::test_env::TempPath::new("sendfile_short.bin");
std::fs::write(path.as_str(), b"short").unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let _client = tokio::net::TcpStream::connect(addr).await.unwrap();
let (server, _) = listener.accept().await.unwrap();
let file = File::open(path.as_str()).unwrap();
let err = send_file(&server, &file, 100).await.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
}
#[test]
fn only_plain_tcp_offers_its_socket() {
fn offers<S: SendfileSocket>(stream: &S) -> bool {
stream.sendfile_socket().is_some()
}
let (a, _b) = tokio::io::duplex(8);
assert!(!offers(&a));
}
}