use std::io;
#[cfg(unix)]
use std::os::fd::RawFd;
#[cfg(not(unix))]
type RawFd = i32;
#[cfg(target_os = "linux")]
const SPLICE_CHUNK: usize = 64 * 1024;
#[cfg(target_os = "linux")]
const SPLICE_F_MOVE: u32 = libc::SPLICE_F_MOVE;
#[cfg(target_os = "linux")]
pub fn splice_transfer(
fd_in: RawFd,
off_in: Option<i64>,
fd_out: RawFd,
off_out: Option<i64>,
len: usize,
) -> io::Result<usize> {
let mut pipe_fds = [0i32; 2];
if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } != 0 {
return Err(io::Error::last_os_error());
}
let guard = PipeGuard {
read: pipe_fds[0],
write: pipe_fds[1],
};
let mut in_offset = off_in;
let mut out_offset = off_out;
let mut total_transferred = 0usize;
while total_transferred < len {
let remaining = len - total_transferred;
let chunk = remaining.min(SPLICE_CHUNK);
let n_read = do_splice(fd_in, in_offset.as_mut(), guard.write, None, chunk)?;
if n_read == 0 {
break;
}
let n_read = n_read as usize;
let mut drained = 0usize;
while drained < n_read {
let to_write = n_read - drained;
let n_written = do_splice(guard.read, None, fd_out, out_offset.as_mut(), to_write)?;
if n_written == 0 {
return Err(io::Error::new(
io::ErrorKind::WriteZero,
"splice: destination accepted zero bytes",
));
}
drained += n_written as usize;
}
total_transferred += drained;
}
Ok(total_transferred)
}
#[cfg(not(target_os = "linux"))]
pub fn splice_transfer(
_fd_in: RawFd,
_off_in: Option<i64>,
_fd_out: RawFd,
_off_out: Option<i64>,
_len: usize,
) -> io::Result<usize> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"splice is only supported on Linux",
))
}
pub fn is_splice_supported() -> bool {
cfg!(target_os = "linux")
}
#[cfg(target_os = "linux")]
fn do_splice(
fd_in: RawFd,
off_in: Option<&mut i64>,
fd_out: RawFd,
off_out: Option<&mut i64>,
len: usize,
) -> io::Result<isize> {
let in_ptr = off_in
.map(std::ptr::from_mut)
.unwrap_or(std::ptr::null_mut());
let out_ptr = off_out
.map(std::ptr::from_mut)
.unwrap_or(std::ptr::null_mut());
loop {
let r = unsafe { libc::splice(fd_in, in_ptr, fd_out, out_ptr, len, SPLICE_F_MOVE) };
if r < 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(err);
}
return Ok(r);
}
}
#[cfg(target_os = "linux")]
struct PipeGuard {
read: i32,
write: i32,
}
#[cfg(target_os = "linux")]
impl Drop for PipeGuard {
fn drop(&mut self) {
unsafe {
libc::close(self.read);
libc::close(self.write);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_splice_supported() {
assert_eq!(is_splice_supported(), cfg!(target_os = "linux"));
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_splice_unsupported_on_non_linux() {
let result = splice_transfer(0, None, 1, None, 1024);
assert!(result.is_err());
assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::Unsupported);
}
#[cfg(target_os = "linux")]
#[test]
fn test_splice_transfer_between_files() {
use std::os::fd::AsRawFd;
let dir = tempfile::tempdir().expect("tempdir");
let src_path = dir.path().join("src.bin");
let dst_path = dir.path().join("dst.bin");
let payload = b"splice test data";
std::fs::write(&src_path, payload).expect("write src");
let src_file = std::fs::File::open(&src_path).expect("open src");
let dst_file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&dst_path)
.expect("open dst");
let n = splice_transfer(
src_file.as_raw_fd(),
None,
dst_file.as_raw_fd(),
None,
payload.len(),
)
.expect("splice_transfer");
assert_eq!(n, payload.len());
drop(src_file);
drop(dst_file);
let content = std::fs::read(&dst_path).expect("read dst");
assert_eq!(content, payload);
}
}