use rtoolbox::fix_line_issues::fix_line_issues;
use rtoolbox::print_tty::{print_tty, print_writer};
use rtoolbox::safe_string::SafeString;
use std::io::{BufRead, Write};
#[cfg(target_family = "wasm")]
mod wasm {
use std::io::{self, BufRead};
pub fn read_password() -> std::io::Result<String> {
let tty = std::fs::File::open("/dev/tty")?;
let mut reader = io::BufReader::new(tty);
read_password_from_fd_with_hidden_input(&mut reader)
}
fn read_password_from_fd_with_hidden_input(
reader: &mut impl BufRead,
) -> std::io::Result<String> {
let mut password = super::SafeString::new();
reader.read_line(&mut password)?;
super::fix_line_issues(password.into_inner())
}
}
#[cfg(target_family = "unix")]
mod unix {
use libc::{c_int, tcsetattr, termios, ECHO, ECHONL, TCSANOW};
use std::io::{self, BufRead};
use std::mem;
use std::os::unix::io::AsRawFd;
struct HiddenInput {
fd: i32,
term_orig: termios,
}
impl HiddenInput {
fn new(fd: i32) -> io::Result<HiddenInput> {
let mut term = safe_tcgetattr(fd)?;
let term_orig = safe_tcgetattr(fd)?;
term.c_lflag &= !ECHO;
term.c_lflag |= ECHONL;
io_result(unsafe { tcsetattr(fd, TCSANOW, &term) })?;
Ok(HiddenInput { fd, term_orig })
}
}
impl Drop for HiddenInput {
fn drop(&mut self) {
unsafe {
tcsetattr(self.fd, TCSANOW, &self.term_orig);
}
}
}
fn io_result(ret: c_int) -> std::io::Result<()> {
match ret {
0 => Ok(()),
_ => Err(std::io::Error::last_os_error()),
}
}
fn safe_tcgetattr(fd: c_int) -> std::io::Result<termios> {
let mut term = mem::MaybeUninit::<termios>::uninit();
io_result(unsafe { ::libc::tcgetattr(fd, term.as_mut_ptr()) })?;
Ok(unsafe { term.assume_init() })
}
pub fn read_password() -> std::io::Result<String> {
let tty = std::fs::File::open("/dev/tty")?;
let fd = tty.as_raw_fd();
let mut reader = io::BufReader::new(tty);
read_password_from_fd_with_hidden_input(&mut reader, fd)
}
fn read_password_from_fd_with_hidden_input(
reader: &mut impl BufRead,
fd: i32,
) -> std::io::Result<String> {
let mut password = super::SafeString::new();
let hidden_input = HiddenInput::new(fd)?;
reader.read_line(&mut password)?;
std::mem::drop(hidden_input);
super::fix_line_issues(password.into_inner())
}
}
#[cfg(target_family = "windows")]
mod windows {
use std::io::BufRead;
use std::io::{self, BufReader};
use std::os::windows::io::FromRawHandle;
use windows_sys::core::PCSTR;
use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
};
use windows_sys::Win32::System::Console::{
GetConsoleMode, SetConsoleMode, CONSOLE_MODE, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
};
struct HiddenInput {
mode: u32,
handle: HANDLE,
}
impl HiddenInput {
fn new(handle: HANDLE) -> io::Result<HiddenInput> {
let mut mode = 0;
if unsafe { GetConsoleMode(handle, &mut mode as *mut CONSOLE_MODE) } == 0 {
return Err(std::io::Error::last_os_error());
}
let new_mode_flags = ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
if unsafe { SetConsoleMode(handle, new_mode_flags) } == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(HiddenInput { mode, handle })
}
}
impl Drop for HiddenInput {
fn drop(&mut self) {
unsafe {
SetConsoleMode(self.handle, self.mode);
}
}
}
pub fn read_password() -> std::io::Result<String> {
let handle = unsafe {
CreateFileA(
b"CONIN$\x00".as_ptr() as PCSTR,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
INVALID_HANDLE_VALUE,
)
};
if handle == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error());
}
let mut stream = BufReader::new(unsafe { std::fs::File::from_raw_handle(handle as _) });
read_password_from_handle_with_hidden_input(&mut stream, handle)
}
fn read_password_from_handle_with_hidden_input(
reader: &mut impl BufRead,
handle: HANDLE,
) -> io::Result<String> {
let mut password = super::SafeString::new();
let hidden_input = HiddenInput::new(handle)?;
let reader_return = reader.read_line(&mut password);
println!();
if reader_return.is_err() {
return Err(reader_return.unwrap_err());
}
std::mem::drop(hidden_input);
super::fix_line_issues(password.into_inner())
}
}
#[cfg(target_family = "unix")]
pub use unix::read_password;
#[cfg(target_family = "wasm")]
pub use wasm::read_password;
#[cfg(target_family = "windows")]
pub use windows::read_password;
pub fn read_password_from_bufread(reader: &mut impl BufRead) -> std::io::Result<String> {
let mut password = SafeString::new();
reader.read_line(&mut password)?;
fix_line_issues(password.into_inner())
}
pub fn prompt_password_from_bufread(
reader: &mut impl BufRead,
writer: &mut impl Write,
prompt: impl ToString,
) -> std::io::Result<String> {
print_writer(writer, prompt.to_string().as_str())
.and_then(|_| read_password_from_bufread(reader))
}
pub fn prompt_password(prompt: impl ToString) -> std::io::Result<String> {
print_tty(prompt.to_string().as_str()).and_then(|_| read_password())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
fn mock_input_crlf() -> Cursor<&'static [u8]> {
Cursor::new(&b"A mocked response.\r\nAnother mocked response.\r\n"[..])
}
fn mock_input_lf() -> Cursor<&'static [u8]> {
Cursor::new(&b"A mocked response.\nAnother mocked response.\n"[..])
}
#[test]
fn can_read_from_redirected_input_many_times() {
let mut reader_crlf = mock_input_crlf();
let response = super::read_password_from_bufread(&mut reader_crlf).unwrap();
assert_eq!(response, "A mocked response.");
let response = super::read_password_from_bufread(&mut reader_crlf).unwrap();
assert_eq!(response, "Another mocked response.");
let mut reader_lf = mock_input_lf();
let response = super::read_password_from_bufread(&mut reader_lf).unwrap();
assert_eq!(response, "A mocked response.");
let response = super::read_password_from_bufread(&mut reader_lf).unwrap();
assert_eq!(response, "Another mocked response.");
}
}