#[cfg(unix)]
pub(crate) fn stdin_would_lose_data() -> std::io::Result<bool> {
use std::os::unix::io::AsRawFd;
fd_would_lose_data(std::io::stdin().as_raw_fd())
}
#[cfg(unix)]
pub(crate) fn fd_would_lose_data(fd: std::os::unix::io::RawFd) -> std::io::Result<bool> {
if unsafe { libc::isatty(fd) } == 1 {
return Ok(false);
}
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } != 0 {
return Err(std::io::Error::last_os_error());
}
match st.st_mode & libc::S_IFMT {
libc::S_IFREG => Ok(st.st_size > 0),
libc::S_IFIFO => fifo_has_pending_data(fd),
_ => Ok(false),
}
}
#[cfg(unix)]
fn fifo_has_pending_data(fd: std::os::unix::io::RawFd) -> std::io::Result<bool> {
if !fifo_poll_readable(fd)? {
return Ok(true);
}
let mut byte = [0u8; 1];
let r = unsafe { libc::read(fd, byte.as_mut_ptr() as *mut libc::c_void, 1) };
if r > 0 {
Ok(true) } else if r == 0 {
Ok(false) } else {
let err = std::io::Error::last_os_error();
match err.raw_os_error() {
Some(e) if e == libc::EAGAIN || e == libc::EWOULDBLOCK => Ok(false),
_ => Err(err),
}
}
}
#[cfg(unix)]
fn fifo_poll_readable(fd: std::os::unix::io::RawFd) -> std::io::Result<bool> {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let n = unsafe { libc::poll(&mut pfd, 1, 0) };
if n < 0 {
return Err(std::io::Error::last_os_error());
}
Ok(n > 0)
}
#[cfg(unix)]
pub(crate) fn stdin_is_readable() -> std::io::Result<bool> {
use std::os::unix::io::AsRawFd;
fd_is_readable(std::io::stdin().as_raw_fd())
}
#[cfg(unix)]
pub(crate) fn fd_is_readable(fd: std::os::unix::io::RawFd) -> std::io::Result<bool> {
if unsafe { libc::isatty(fd) } == 1 {
return Ok(false);
}
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } != 0 {
return Err(std::io::Error::last_os_error());
}
match st.st_mode & libc::S_IFMT {
libc::S_IFREG => Ok(st.st_size > 0),
libc::S_IFIFO => fifo_poll_readable(fd),
_ => Ok(false),
}
}
#[cfg(not(unix))]
pub(crate) fn stdin_would_lose_data() -> std::io::Result<bool> {
use std::os::windows::io::AsRawHandle;
let handle = std::io::stdin().as_raw_handle();
if !handle_is_pipe(handle) {
return Ok(false);
}
Ok(pipe_has_data(handle))
}
#[cfg(not(unix))]
pub(crate) fn stdin_is_readable() -> std::io::Result<bool> {
use std::io::IsTerminal;
use std::os::windows::io::AsRawHandle;
if std::io::stdin().is_terminal() {
return Ok(false);
}
let handle = std::io::stdin().as_raw_handle();
if !handle_is_pipe(handle) {
return Ok(true);
}
Ok(pipe_has_data(handle))
}
#[cfg(not(unix))]
fn pipe_has_data(handle: std::os::windows::io::RawHandle) -> bool {
const ERROR_BROKEN_PIPE: u32 = 109;
unsafe extern "system" {
fn PeekNamedPipe(
handle: *mut core::ffi::c_void,
buffer: *mut core::ffi::c_void,
buffer_size: u32,
bytes_read: *mut u32,
total_bytes_avail: *mut u32,
bytes_left_this_message: *mut u32,
) -> i32;
fn GetLastError() -> u32;
}
let mut avail: u32 = 0;
let ok = unsafe {
PeekNamedPipe(
handle,
core::ptr::null_mut(),
0,
core::ptr::null_mut(),
&mut avail,
core::ptr::null_mut(),
)
};
if ok == 0 {
return unsafe { GetLastError() } != ERROR_BROKEN_PIPE;
}
avail > 0
}
#[cfg(not(unix))]
fn handle_is_pipe(handle: std::os::windows::io::RawHandle) -> bool {
const FILE_TYPE_PIPE: u32 = 0x0003;
unsafe extern "system" {
fn GetFileType(handle: *mut core::ffi::c_void) -> u32;
}
unsafe { GetFileType(handle) == FILE_TYPE_PIPE }
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::{fd_is_readable, fd_would_lose_data};
use std::io::Write;
use std::os::unix::io::AsRawFd;
struct Pipe {
read: libc::c_int,
write: libc::c_int,
}
impl Pipe {
fn new() -> Self {
let mut fds = [0 as libc::c_int; 2];
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
assert_eq!(rc, 0, "pipe() failed");
Pipe {
read: fds[0],
write: fds[1],
}
}
fn close_write(&mut self) {
if self.write >= 0 {
unsafe { libc::close(self.write) };
self.write = -1;
}
}
}
impl Drop for Pipe {
fn drop(&mut self) {
self.close_write();
if self.read >= 0 {
unsafe { libc::close(self.read) };
}
}
}
#[test]
fn pipe_with_bytes_would_lose_data() {
let p = Pipe::new();
let wrote = unsafe { libc::write(p.write, b"x".as_ptr() as *const libc::c_void, 1) };
assert_eq!(wrote, 1);
assert!(
fd_would_lose_data(p.read).unwrap(),
"a pipe carrying a byte is data we'd lose"
);
}
#[test]
fn closed_pipe_eof_is_safe() {
let mut p = Pipe::new();
p.close_write();
assert!(
!fd_would_lose_data(p.read).unwrap(),
"a closed/EOF pipe has nothing to lose"
);
}
#[test]
fn open_but_silent_pipe_is_data_intent() {
let p = Pipe::new();
assert!(
fd_would_lose_data(p.read).unwrap(),
"an open pipe with a live writer is treated as data intent"
);
}
#[test]
fn empty_file_redirect_is_safe() {
let f = tempfile::NamedTempFile::new().unwrap();
let fd = f.as_file().as_raw_fd();
assert!(
!fd_would_lose_data(fd).unwrap(),
"an empty `< file` redirect has nothing to lose"
);
}
#[test]
fn nonempty_file_redirect_would_lose_data() {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(b"piped context\n").unwrap();
f.flush().unwrap();
let fd = f.as_file().as_raw_fd();
assert!(
fd_would_lose_data(fd).unwrap(),
"a non-empty `< file` redirect is data the child would lose"
);
}
#[test]
fn dev_null_is_safe() {
let f = std::fs::File::open("/dev/null").unwrap();
assert!(
!fd_would_lose_data(f.as_raw_fd()).unwrap(),
"/dev/null is a char device with nothing to lose"
);
}
#[test]
fn pipe_with_bytes_is_readable() {
let p = Pipe::new();
let wrote = unsafe { libc::write(p.write, b"log\n".as_ptr() as *const libc::c_void, 4) };
assert_eq!(wrote, 4);
assert!(
fd_is_readable(p.read).unwrap(),
"a pipe with bytes is readable"
);
}
#[test]
fn open_but_idle_pipe_is_not_readable() {
let p = Pipe::new();
assert!(
!fd_is_readable(p.read).unwrap(),
"an open-but-idle pipe is not readable -- the no-hang guard"
);
}
#[test]
fn closed_pipe_eof_is_readable() {
let mut p = Pipe::new();
p.close_write();
assert!(
fd_is_readable(p.read).unwrap(),
"a clean-EOF pipe is readable (read returns empty without blocking)"
);
}
#[test]
fn nonempty_file_is_readable() {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(b"context\n").unwrap();
f.flush().unwrap();
assert!(
fd_is_readable(f.as_file().as_raw_fd()).unwrap(),
"a non-empty `< file` redirect is readable"
);
}
#[test]
fn empty_file_is_not_readable() {
let f = tempfile::NamedTempFile::new().unwrap();
assert!(
!fd_is_readable(f.as_file().as_raw_fd()).unwrap(),
"an empty file adds nothing -- skip it"
);
}
#[test]
fn dev_null_is_not_readable() {
let f = std::fs::File::open("/dev/null").unwrap();
assert!(
!fd_is_readable(f.as_raw_fd()).unwrap(),
"/dev/null carries no context"
);
}
}