use std::io::Write;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use super::*;
use crate::compat::host::{TtyMode, restore_saved_tty};
static LOCK: Mutex<()> = Mutex::new(());
fn lock() -> std::sync::MutexGuard<'static, ()> {
LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
fn pty() -> (OwnedFd, OwnedFd) {
let (mut m, mut s) = (-1, -1);
let rc = unsafe {
libc::openpty(
&mut m,
&mut s,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
assert_eq!(rc, 0, "openpty: {}", std::io::Error::last_os_error());
unsafe { (OwnedFd::from_raw_fd(m), OwnedFd::from_raw_fd(s)) }
}
fn lflag(fd: &OwnedFd) -> libc::tcflag_t {
let mut t: libc::termios = unsafe { std::mem::zeroed() };
assert_eq!(unsafe { libc::tcgetattr(fd.as_raw_fd(), &mut t) }, 0);
t.c_lflag
}
#[derive(Clone, Default)]
struct Shared(Arc<Mutex<Vec<u8>>>);
impl Write for Shared {
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(b);
Ok(b.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Shared {
fn text(&self) -> Vec<u8> {
self.0.lock().unwrap().clone()
}
}
fn type_at_prompts(master: &OwnedFd, slave: &OwnedFd, err: &Shared, lines: &[&[u8]]) {
let deadline = Instant::now() + Duration::from_secs(10);
for (k, line) in lines.iter().enumerate() {
loop {
let prompts = err.text().windows(4).filter(|w| w == b"pw: ").count();
if prompts > k && lflag(slave) & libc::ECHO == 0 {
break;
}
assert!(Instant::now() < deadline, "prompt {k} never waited");
std::thread::sleep(Duration::from_millis(1));
}
let mut f = std::fs::File::from(master.try_clone().unwrap());
f.write_all(line).unwrap();
}
}
fn getp_typed(
master: &OwnedFd,
slave: &OwnedFd,
lines: &'static [&'static [u8]],
) -> (Result<Password, PwError>, Vec<u8>) {
let err = Shared::default();
let typer = {
let (m, s, e) = (
master.try_clone().unwrap(),
slave.try_clone().unwrap(),
err.clone(),
);
std::thread::spawn(move || type_at_prompts(&m, &s, &e, lines))
};
let mut w = err.clone();
let r = getp_on(&mut w, slave.as_raw_fd(), b"pw: ", IZ_PWLEN + 1);
typer.join().unwrap();
(r, err.text())
}
#[test]
fn a_typed_password_is_returned_and_echo_comes_back() {
let _l = lock();
let (master, slave) = pty();
let before = lflag(&slave);
assert_ne!(before & libc::ECHO, 0);
let (r, err) = getp_typed(&master, &slave, &[b"secret\n"]);
assert_eq!(r.unwrap().key(), b"secret");
assert_eq!(err, b"pw: \n");
assert_eq!(lflag(&slave), before);
}
#[test]
fn a_line_longer_than_the_buffer_is_asked_for_again() {
let _l = lock();
let (master, slave) = pty();
const LONG: &[u8] =
b"ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp\n";
const FITS: &[u8] =
b"qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n";
assert_eq!((LONG.len(), FITS.len()), (82, 81));
let (r, err) = getp_typed(&master, &slave, &[LONG, FITS]);
assert_eq!(r.unwrap().key(), &FITS[..80]);
assert_eq!(err, b"pw: \n(line too long--try again)\npw: \n");
}
#[test]
fn the_terminal_mode_is_restored_after_an_error() {
let _l = lock();
let (_master, slave) = pty();
let before = lflag(&slave);
let path = {
let mut buf = [0 as libc::c_char; 256];
let rc = unsafe { libc::ttyname_r(slave.as_raw_fd(), buf.as_mut_ptr(), buf.len()) };
assert_eq!(rc, 0);
unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }.to_owned()
};
let wo = unsafe { libc::open(path.as_ptr(), libc::O_WRONLY | libc::O_NOCTTY) };
assert!(wo >= 0, "{}", std::io::Error::last_os_error());
let wo = unsafe { OwnedFd::from_raw_fd(wo) };
let mut err = Vec::new();
let r = getp_on(&mut err, wo.as_raw_fd(), b"pw: ", IZ_PWLEN + 1);
assert_eq!(r.unwrap_err(), PwError::HungUp);
assert_eq!(lflag(&slave), before, "echo left off after the error");
}
#[test]
fn the_terminal_mode_is_restored_when_a_panic_unwinds() {
let _l = lock();
let (_master, slave) = pty();
let before = lflag(&slave);
let fd = slave.as_raw_fd();
let r = std::panic::catch_unwind(|| {
let _m = TtyMode::set(fd, |t| t.c_lflag &= !(libc::ECHO | libc::ICANON)).unwrap();
assert_eq!(
lflag(&slave) & (libc::ECHO | libc::ICANON),
0,
"the mode was not changed"
);
panic!("inside the read");
});
assert!(r.is_err());
assert_eq!(lflag(&slave), before);
}
#[test]
fn the_signal_path_restores_the_saved_mode() {
let _l = lock();
let (_master, slave) = pty();
let before = lflag(&slave);
let m = TtyMode::set(slave.as_raw_fd(), |t| t.c_lflag &= !libc::ECHO).unwrap();
assert_eq!(lflag(&slave) & libc::ECHO, 0);
restore_saved_tty();
assert_eq!(lflag(&slave), before);
drop(m);
let mut t: libc::termios = unsafe { std::mem::zeroed() };
unsafe {
libc::tcgetattr(slave.as_raw_fd(), &mut t);
t.c_lflag &= !libc::ECHO;
libc::tcsetattr(slave.as_raw_fd(), libc::TCSANOW, &t);
}
restore_saved_tty(); assert_eq!(lflag(&slave) & libc::ECHO, 0);
}
#[cfg(target_os = "macos")]
#[test]
fn an_end_of_file_read_repeats_the_last_byte() {
let _l = lock();
let (master, slave) = pty();
let (r, _) = getp_typed(&master, &slave, &[b"abc\x04d\n"]);
assert_eq!(r.unwrap().key(), b"abccd");
let (r, _) = getp_typed(&master, &slave, &[b"\x04"]);
assert!(r.unwrap().is_empty_answer());
}
#[test]
fn a_password_shows_nothing_and_is_zeroed() {
let p = Password::new(b"hunter2\0tail");
assert_eq!(p.key(), b"hunter2");
assert_eq!(format!("{p:?}"), "Password(..)");
let mut q = Password::new(b"abc");
q.clear();
assert!(q.is_empty_answer());
}
#[test]
fn a_terminal_that_hung_up_ends_the_prompt() {
let _l = lock();
let (master, slave) = pty();
drop(master);
let fd = slave.as_raw_fd();
let mut b = 0u8;
let n = unsafe { libc::read(fd, (&mut b as *mut u8).cast(), 1) };
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
unsafe { libc::poll(&mut pfd, 1, 0) };
if cfg!(target_os = "macos") {
assert_eq!(n, 0, "macOS reads 0 from a hung-up terminal");
assert_ne!(pfd.revents & libc::POLLHUP, 0, "and reports POLLHUP");
} else {
assert!(n == 0 || n == -1, "{n}");
}
let mut err = Vec::new();
let r = getp_on(&mut err, fd, b"pw: ", IZ_PWLEN + 1);
assert_eq!(r.unwrap_err(), PwError::HungUp);
assert_eq!(err, b"pw: ");
}