use std::fs::File;
use std::io;
use std::mem::ManuallyDrop;
use std::ops::{Deref, DerefMut};
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
pub const FIRST_HIGH_FD: RawFd = 10;
pub struct AutoClosePipes {
pub read: OwnedFd,
pub write: OwnedFd,
}
pub fn make_autoclose_pipes() -> io::Result<AutoClosePipes> {
let (read_fd, write_fd) =
nix::unistd::pipe().map_err(|e| io::Error::from_raw_os_error(e as i32))?;
let read_fd = heightenize_fd(read_fd)?;
let write_fd = heightenize_fd(write_fd)?;
Ok(AutoClosePipes {
read: read_fd,
write: write_fd,
})
}
fn heightenize_fd(fd: OwnedFd) -> io::Result<OwnedFd> {
let raw_fd = fd.as_raw_fd();
if raw_fd >= FIRST_HIGH_FD {
set_cloexec(raw_fd, true)?;
return Ok(fd);
}
let new_fd = nix::fcntl::fcntl(raw_fd, nix::fcntl::FcntlArg::F_DUPFD_CLOEXEC(FIRST_HIGH_FD))
.map_err(|e| io::Error::from_raw_os_error(e as i32))?;
Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
}
pub fn set_cloexec(fd: RawFd, should_set: bool) -> io::Result<()> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
let new_flags = if should_set {
flags | libc::FD_CLOEXEC
} else {
flags & !libc::FD_CLOEXEC
};
if flags != new_flags {
let result = unsafe { libc::fcntl(fd, libc::F_SETFD, new_flags) };
if result < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn make_fd_nonblocking(fd: RawFd) -> io::Result<()> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
if (flags & libc::O_NONBLOCK) == 0 {
let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
if result < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn make_fd_blocking(fd: RawFd) -> io::Result<()> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
if (flags & libc::O_NONBLOCK) != 0 {
let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) };
if result < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
pub fn close_fd(fd: RawFd) {
if fd < 0 {
return;
}
loop {
let result = unsafe { libc::close(fd) };
if result == 0 {
break;
}
let err = io::Error::last_os_error();
if err.raw_os_error() != Some(libc::EINTR) {
break;
}
}
}
pub fn dup_fd(fd: RawFd) -> io::Result<RawFd> {
let new_fd = unsafe { libc::dup(fd) };
if new_fd < 0 {
Err(io::Error::last_os_error())
} else {
Ok(new_fd)
}
}
pub fn dup2_fd(src: RawFd, dst: RawFd) -> io::Result<()> {
if src == dst {
return Ok(());
}
let result = unsafe { libc::dup2(src, dst) };
if result < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
pub struct BorrowedFdFile(ManuallyDrop<File>);
impl Deref for BorrowedFdFile {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BorrowedFdFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromRawFd for BorrowedFdFile {
unsafe fn from_raw_fd(fd: RawFd) -> Self {
Self(ManuallyDrop::new(unsafe { File::from_raw_fd(fd) }))
}
}
impl AsRawFd for BorrowedFdFile {
fn as_raw_fd(&self) -> RawFd {
self.0.as_raw_fd()
}
}
impl IntoRawFd for BorrowedFdFile {
fn into_raw_fd(self) -> RawFd {
ManuallyDrop::into_inner(self.0).into_raw_fd()
}
}
impl Clone for BorrowedFdFile {
fn clone(&self) -> Self {
unsafe { Self::from_raw_fd(self.as_raw_fd()) }
}
}
impl BorrowedFdFile {
pub fn stdin() -> Self {
unsafe { Self::from_raw_fd(libc::STDIN_FILENO) }
}
pub fn stdout() -> Self {
unsafe { Self::from_raw_fd(libc::STDOUT_FILENO) }
}
pub fn stderr() -> Self {
unsafe { Self::from_raw_fd(libc::STDERR_FILENO) }
}
}
impl io::Read for BorrowedFdFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.deref_mut().read(buf)
}
}
impl io::Write for BorrowedFdFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.deref_mut().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.deref_mut().flush()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FdType {
Unused = 0,
External = 1,
Internal = 2,
Module = 3,
Flock = 4,
FlockExec = 5,
Proc = 6,
}
pub fn movefd(fd: RawFd) -> RawFd {
if !(0..FIRST_HIGH_FD).contains(&fd) {
return fd;
}
unsafe {
let new_fd = libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, FIRST_HIGH_FD);
if new_fd != -1 {
libc::close(fd);
return new_fd;
}
}
fd
}
pub fn redup(x: RawFd, y: RawFd) -> RawFd {
if x < 0 {
unsafe { libc::close(y) };
return y;
}
if x == y {
return y;
}
let result = unsafe { libc::dup2(x, y) };
if result == -1 {
return -1;
}
unsafe { libc::close(x) };
y
}
pub fn zclose(fd: RawFd) -> i32 {
if fd >= 0 {
unsafe { libc::close(fd) }
} else {
-1
}
}
pub fn zdup(fd: RawFd) -> RawFd {
if fd < 0 {
return -1;
}
unsafe { libc::dup(fd) }
}
pub fn fd_is_open(fd: RawFd) -> bool {
if fd < 0 {
return false;
}
unsafe { libc::fcntl(fd, libc::F_GETFD, 0) >= 0 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_autoclose_pipes() {
let _g = crate::test_util::global_state_lock();
let pipes = make_autoclose_pipes().expect("Failed to create pipes");
assert!(pipes.read.as_raw_fd() >= FIRST_HIGH_FD);
assert!(pipes.write.as_raw_fd() >= FIRST_HIGH_FD);
let read_flags = unsafe { libc::fcntl(pipes.read.as_raw_fd(), libc::F_GETFD, 0) };
let write_flags = unsafe { libc::fcntl(pipes.write.as_raw_fd(), libc::F_GETFD, 0) };
assert!(read_flags >= 0);
assert!(write_flags >= 0);
assert_ne!(read_flags & libc::FD_CLOEXEC, 0);
assert_ne!(write_flags & libc::FD_CLOEXEC, 0);
}
#[test]
fn test_set_cloexec() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
set_cloexec(fd, true).unwrap();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
assert_ne!(flags & libc::FD_CLOEXEC, 0);
set_cloexec(fd, false).unwrap();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
assert_eq!(flags & libc::FD_CLOEXEC, 0);
}
#[test]
fn test_nonblocking() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
make_fd_nonblocking(fd).unwrap();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
assert_ne!(flags & libc::O_NONBLOCK, 0);
make_fd_blocking(fd).unwrap();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL, 0) };
assert_eq!(flags & libc::O_NONBLOCK, 0);
}
#[test]
fn test_borrowed_fd_file_does_not_close() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
{
let _borrowed = unsafe { BorrowedFdFile::from_raw_fd(fd) };
}
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
assert!(
flags >= 0,
"fd should still be valid after dropping BorrowedFdFile"
);
drop(file);
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD, 0) };
assert!(
flags < 0,
"fd should be invalid after dropping original File"
);
}
#[test]
fn fd_is_open_true_for_stdin_stdout_stderr() {
let _g = crate::test_util::global_state_lock();
assert!(fd_is_open(0));
assert!(fd_is_open(1));
assert!(fd_is_open(2));
}
#[test]
fn fd_is_open_false_for_huge_unused_fd() {
let _g = crate::test_util::global_state_lock();
assert!(!fd_is_open(99_999));
}
#[test]
fn fd_is_open_false_after_close() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
assert!(fd_is_open(fd));
drop(file);
assert!(!fd_is_open(fd));
}
#[test]
fn dup_fd_yields_independent_open_fd() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
let dup = dup_fd(fd).unwrap();
assert!(fd_is_open(dup));
assert_ne!(dup, fd, "dup must yield a fresh descriptor");
close_fd(dup);
assert!(fd_is_open(fd));
drop(file);
}
#[test]
fn dup_fd_on_invalid_fd_fails() {
let _g = crate::test_util::global_state_lock();
let r = dup_fd(99_998);
assert!(r.is_err(), "dup of invalid fd must error: {:?}", r);
}
#[test]
fn close_fd_on_invalid_fd_is_silent() {
let _g = crate::test_util::global_state_lock();
close_fd(99_997);
}
#[test]
fn movefd_returns_a_valid_high_fd() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
let dup = dup_fd(fd).unwrap();
let moved = movefd(dup);
if moved >= 0 {
assert!(fd_is_open(moved));
close_fd(moved);
}
drop(file);
}
#[test]
fn zclose_on_open_fd_returns_zero() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = dup_fd(file.as_raw_fd()).unwrap();
let r = zclose(fd);
assert_eq!(r, 0, "zclose should return 0 on successful close");
drop(file);
}
#[test]
fn zdup_returns_new_open_fd() {
let _g = crate::test_util::global_state_lock();
let file = std::fs::File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
let dup = zdup(fd);
if dup >= 0 {
assert!(fd_is_open(dup));
assert_ne!(dup, fd);
close_fd(dup);
}
drop(file);
}
#[test]
fn zdup_on_invalid_fd_returns_negative() {
let _g = crate::test_util::global_state_lock();
let r = zdup(99_996);
assert!(r < 0, "zdup of invalid fd should be negative: {}", r);
}
}