Skip to content

Commit

Permalink
Implement sendfile on FreeBSD and Darwin
Browse files Browse the repository at this point in the history
  • Loading branch information
morrowa committed May 29, 2018
1 parent b87d45b commit 325c43c
Show file tree
Hide file tree
Showing 5 changed files with 305 additions and 10 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

### Added
- Added `sendfile` on FreeBSD and Darwin.
([#901](https://github.com/nix-rust/nix/pull/901))
- Added `pselect`
([#894](https://github.com/nix-rust/nix/pull/894))
- Exposed `preadv` and `pwritev` on the BSDs.
Expand Down
7 changes: 5 additions & 2 deletions src/sys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ pub mod reboot;

pub mod select;

// TODO: Add support for dragonfly, freebsd, and ios/macos.
#[cfg(any(target_os = "android", target_os = "linux"))]
#[cfg(any(target_os = "android",
target_os = "freebsd",
target_os = "ios",
target_os = "linux",
target_os = "macos"))]
pub mod sendfile;

pub mod signal;
Expand Down
191 changes: 189 additions & 2 deletions src/sys/sendfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,195 @@ use libc::{self, off_t};
use Result;
use errno::Errno;

pub fn sendfile(out_fd: RawFd, in_fd: RawFd, offset: Option<&mut off_t>, count: usize) -> Result<usize> {
let offset = offset.map(|offset| offset as *mut _).unwrap_or(ptr::null_mut());
/// Copy up to `count` bytes to `out_fd` from `in_fd` starting at `offset`.
///
/// Returns a `Result` with the number of bytes written.
///
/// If `offset` is `None`, `sendfile` will begin reading at the current offset of `in_fd`and will
/// update the offset of `in_fd`. If `offset` is `Some`, `sendfile` will begin at the specified
/// offset and will not update the offset of `in_fd`. Instead, it will mutate `offset` to point to
/// the byte after the last byte copied.
///
/// `in_fd` must support `mmap`-like operations and therefore cannot be a socket.
///
/// For more information, see [the sendfile(2) man page.](http://man7.org/linux/man-pages/man2/sendfile.2.html)
#[cfg(any(target_os = "android", target_os = "linux"))]
pub fn sendfile(
out_fd: RawFd,
in_fd: RawFd,
offset: Option<&mut off_t>,
count: usize,
) -> Result<usize> {
let offset = offset
.map(|offset| offset as *mut _)
.unwrap_or(ptr::null_mut());
let ret = unsafe { libc::sendfile(out_fd, in_fd, offset, count) };
Errno::result(ret).map(|r| r as usize)
}

cfg_if! {
if #[cfg(any(target_os = "freebsd",
target_os = "ios",
target_os = "macos"))] {
use sys::uio::IoVec;

#[allow(missing_debug_implementations)]
struct SendfileHeaderTrailer<'a>(
libc::sf_hdtr,
Option<Vec<IoVec<&'a [u8]>>>,
Option<Vec<IoVec<&'a [u8]>>>,
);

impl<'a> SendfileHeaderTrailer<'a> {
fn new(
headers: Option<&'a [&'a [u8]]>,
trailers: Option<&'a [&'a [u8]]>
) -> SendfileHeaderTrailer<'a> {
let header_iovecs: Option<Vec<IoVec<&[u8]>>> =
headers.map(|s| s.iter().map(|b| IoVec::from_slice(b)).collect());
let trailer_iovecs: Option<Vec<IoVec<&[u8]>>> =
trailers.map(|s| s.iter().map(|b| IoVec::from_slice(b)).collect());
SendfileHeaderTrailer(
libc::sf_hdtr {
headers: {
header_iovecs
.as_ref()
.map_or(ptr::null(), |v| v.as_ptr()) as *mut libc::iovec
},
hdr_cnt: header_iovecs.as_ref().map(|v| v.len()).unwrap_or(0) as i32,
trailers: {
trailer_iovecs
.as_ref()
.map_or(ptr::null(), |v| v.as_ptr()) as *mut libc::iovec
},
trl_cnt: trailer_iovecs.as_ref().map(|v| v.len()).unwrap_or(0) as i32
},
header_iovecs,
trailer_iovecs,
)
}
}
}
}

cfg_if! {
if #[cfg(target_os = "freebsd")] {
use libc::c_int;

libc_bitflags!{
/// Configuration options for [`sendfile`.](fn.sendfile.html)
pub struct SfFlags: c_int {
/// Causes `sendfile` to return EBUSY instead of blocking when attempting to read a
/// busy page.
SF_NODISKIO;
/// Causes `sendfile` to sleep until the network stack releases its reference to the
/// VM pages read. When `sendfile` returns, the data is not guaranteed to have been
/// sent, but it is safe to modify the file.
SF_SYNC;
/// Causes `sendfile` to cache exactly the number of pages specified in the
/// `readahead` parameter, disabling caching heuristics.
SF_USER_READAHEAD;
/// Causes `sendfile` not to cache the data read.
SF_NOCACHE;
}
}

/// Read up to `count` bytes from `in_fd` starting at `offset` and write to `out_sock`.
///
/// Returns a `Result` and a count of bytes written. Bytes written may be non-zero even if
/// an error occurs.
///
/// `in_fd` must describe a regular file or shared memory object. `out_sock` must describe a
/// stream socket.
///
/// If `offset` falls past the end of the file, the function returns success and zero bytes
/// written.
///
/// If `count` is `None` or 0, bytes will be read from `in_fd` until reaching the end of
/// file (EOF).
///
/// `headers` and `trailers` specify optional slices of byte slices to be sent before and
/// after the data read from `in_fd`, respectively. The length of headers and trailers sent
/// is included in the returned count of bytes written. The values of `offset` and `count`
/// do not apply to headers or trailers.
///
/// `readahead` specifies the minimum number of pages to cache in memory ahead of the page
/// currently being sent.
///
/// For more information, see
/// [the sendfile(2) man page.](https://www.freebsd.org/cgi/man.cgi?query=sendfile&sektion=2)
pub fn sendfile(
in_fd: RawFd,
out_sock: RawFd,
offset: off_t,
count: Option<usize>,
headers: Option<&[&[u8]]>,
trailers: Option<&[&[u8]]>,
flags: SfFlags,
readahead: u16
) -> (Result<()>, off_t) {
// Readahead goes in upper 16 bits
// Flags goes in lower 16 bits
// see `man 2 sendfile`
let flags: u32 = ((readahead as u32) << 16) | (flags.bits() as u32);
let mut bytes_sent: off_t = 0;
let hdtr = headers.or(trailers).map(|_| SendfileHeaderTrailer::new(headers, trailers));
let hdtr_ptr = hdtr.as_ref().map_or(ptr::null(), |s| &s.0 as *const libc::sf_hdtr);
let return_code = unsafe {
libc::sendfile(in_fd,
out_sock,
offset,
count.unwrap_or(0),
hdtr_ptr as *mut libc::sf_hdtr,
&mut bytes_sent as *mut off_t,
flags as c_int)
};
(Errno::result(return_code).and(Ok(())), bytes_sent)
}
} else if #[cfg(any(target_os = "ios", target_os = "macos"))] {
/// Read bytes from `in_fd` starting at `offset` and write up to `count` bytes to
/// `out_sock`.
///
/// Returns a `Result` and a count of bytes written. Bytes written may be non-zero even if
/// an error occurs.
///
/// `in_fd` must describe a regular file. `out_sock` must describe a stream socket.
///
/// If `offset` falls past the end of the file, the function returns success and zero bytes
/// written.
///
/// If `count` is `None` or 0, bytes will be read from `in_fd` until reaching the end of
/// file (EOF).
///
/// `hdtr` specifies an optional list of headers and trailers to be sent before and after
/// the data read from `in_fd`, respectively. The length of headers and trailers sent is
/// included in the returned count of bytes written. If any headers are specified and
/// `count` is non-zero, the length of the headers will be counted in the limit of total
/// bytes sent. Trailers do not count toward the limit of bytes sent and will always be sent
/// regardless. The value of `offset` does not affect headers or trailers.
///
/// For more information, see
/// [the sendfile(2) man page.](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man2/sendfile.2.html)
pub fn sendfile(
in_fd: RawFd,
out_sock: RawFd,
offset: off_t,
count: Option<off_t>,
headers: Option<&[&[u8]]>,
trailers: Option<&[&[u8]]>
) -> (Result<()>, off_t) {
let mut len = count.unwrap_or(0);
let hdtr = headers.or(trailers).map(|_| SendfileHeaderTrailer::new(headers, trailers));
let hdtr_ptr = hdtr.as_ref().map_or(ptr::null(), |s| &s.0 as *const libc::sf_hdtr);
let return_code = unsafe {
libc::sendfile(in_fd,
out_sock,
offset,
&mut len as *mut off_t,
hdtr_ptr as *mut libc::sf_hdtr,
0)
};
(Errno::result(return_code).and(Ok(())), len)
}
}
}
6 changes: 5 additions & 1 deletion test/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ mod test_net;
mod test_nix_path;
mod test_poll;
mod test_pty;
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(any(target_os = "android",
target_os = "freebsd",
target_os = "ios",
target_os = "linux",
target_os = "macos"))]
mod test_sendfile;
mod test_stat;
mod test_unistd;
Expand Down
109 changes: 104 additions & 5 deletions test/test_sendfile.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use std::io::prelude::*;
use std::os::unix::prelude::*;

use tempfile::tempfile;

use libc::off_t;
use nix::sys::sendfile::*;
use tempfile::tempfile;

use nix::unistd::{close, pipe, read};
use nix::sys::sendfile::sendfile;
cfg_if! {
if #[cfg(any(target_os = "android", target_os = "linux"))] {
use nix::unistd::{close, pipe, read};
} else if #[cfg(any(target_os = "freebsd", target_os = "ios", target_os = "macos"))] {
use std::net::Shutdown;
use std::os::unix::net::UnixStream;
}
}

#[cfg(any(target_os = "android", target_os = "linux"))]
#[test]
fn test_sendfile() {
fn test_sendfile_linux() {
const CONTENTS: &[u8] = b"abcdef123456";
let mut tmp = tempfile().unwrap();
tmp.write_all(CONTENTS).unwrap();
Expand All @@ -28,3 +35,95 @@ fn test_sendfile() {
close(rd).unwrap();
close(wr).unwrap();
}

#[cfg(target_os = "freebsd")]
#[test]
fn test_sendfile_freebsd() {
// Declare the content
let header_strings = vec!["HTTP/1.1 200 OK\n", "Content-Type: text/plain\n", "\n"];
let body = "Xabcdef123456";
let body_offset = 1;
let trailer_strings = vec!["\n", "Served by Make Believe\n"];

// Write the body to a file
let mut tmp = tempfile().unwrap();
tmp.write_all(body.as_bytes()).unwrap();

// Prepare headers and trailers for sendfile
let headers: Vec<&[u8]> = header_strings.iter().map(|s| s.as_bytes()).collect();
let trailers: Vec<&[u8]> = trailer_strings.iter().map(|s| s.as_bytes()).collect();

// Prepare socket pair
let (mut rd, wr) = UnixStream::pair().unwrap();

// Call the test method
let (res, bytes_written) = sendfile(
tmp.as_raw_fd(),
wr.as_raw_fd(),
body_offset as off_t,
None,
Some(headers.as_slice()),
Some(trailers.as_slice()),
SfFlags::empty(),
0,
);
assert!(res.is_ok());
wr.shutdown(Shutdown::Both).unwrap();

// Prepare the expected result
let expected_string =
header_strings.concat() + &body[body_offset..] + &trailer_strings.concat();

// Verify the message that was sent
assert_eq!(bytes_written as usize, expected_string.as_bytes().len());

let mut read_string = String::new();
let bytes_read = rd.read_to_string(&mut read_string).unwrap();
assert_eq!(bytes_written as usize, bytes_read);
assert_eq!(expected_string, read_string);
}

#[cfg(any(target_os = "ios", target_os = "macos"))]
#[test]
fn test_sendfile_darwin() {
// Declare the content
let header_strings = vec!["HTTP/1.1 200 OK\n", "Content-Type: text/plain\n", "\n"];
let body = "Xabcdef123456";
let body_offset = 1;
let trailer_strings = vec!["\n", "Served by Make Believe\n"];

// Write the body to a file
let mut tmp = tempfile().unwrap();
tmp.write_all(body.as_bytes()).unwrap();

// Prepare headers and trailers for sendfile
let headers: Vec<&[u8]> = header_strings.iter().map(|s| s.as_bytes()).collect();
let trailers: Vec<&[u8]> = trailer_strings.iter().map(|s| s.as_bytes()).collect();

// Prepare socket pair
let (mut rd, wr) = UnixStream::pair().unwrap();

// Call the test method
let (res, bytes_written) = sendfile(
tmp.as_raw_fd(),
wr.as_raw_fd(),
body_offset as off_t,
None,
Some(headers.as_slice()),
Some(trailers.as_slice()),
);
assert!(res.is_ok());
wr.shutdown(Shutdown::Both).unwrap();

// Prepare the expected result
let expected_string =
header_strings.concat() + &body[body_offset..] + &trailer_strings.concat();

// Verify the message that was sent
assert_eq!(bytes_written as usize, expected_string.as_bytes().len());

let mut read_string = String::new();
let bytes_read = rd.read_to_string(&mut read_string).unwrap();
assert_eq!(bytes_written as usize, bytes_read);
assert_eq!(expected_string, read_string);
}

0 comments on commit 325c43c

Please sign in to comment.