use anyhow::{Context, Result};
#[cfg(unix)]
pub fn read_masked() -> Result<String> {
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
let stdin = std::io::stdin();
let fd = stdin.as_raw_fd();
let original = unsafe {
let mut termios: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut termios) != 0 {
return rpassword::read_password().context("reading the value");
}
termios
};
let _restore = Restore { fd, original };
unsafe {
let mut raw = original;
raw.c_lflag &= !(libc::ECHO | libc::ICANON | libc::ISIG);
if libc::tcsetattr(fd, libc::TCSANOW, &raw) != 0 {
return rpassword::read_password().context("reading the value");
}
}
let mut value: Vec<u8> = Vec::new();
let mut byte = [0u8; 1];
let mut stderr = std::io::stderr();
loop {
if stdin.lock().read(&mut byte).context("reading the value")? == 0 {
break; }
match byte[0] {
b'\n' | b'\r' => break,
0x03 => {
drop(_restore);
let _ = writeln!(stderr, "^C");
std::process::exit(130);
}
0x04 => break, 0x08 | 0x7f => {
if pop_last_char(&mut value) {
let _ = write!(stderr, "\u{8} \u{8}");
let _ = stderr.flush();
}
}
c if c < 0x20 => {}
c => {
value.push(c);
if !is_continuation(c) {
let _ = write!(stderr, "*");
let _ = stderr.flush();
}
}
}
}
let _ = writeln!(stderr);
String::from_utf8(value).context("the value was not valid UTF-8")
}
#[cfg(unix)]
fn is_continuation(byte: u8) -> bool {
byte & 0xC0 == 0x80
}
#[cfg(unix)]
fn pop_last_char(bytes: &mut Vec<u8>) -> bool {
let removed = !bytes.is_empty();
while let Some(byte) = bytes.pop() {
if !is_continuation(byte) {
break;
}
}
removed
}
#[cfg(not(unix))]
pub fn read_masked() -> Result<String> {
rpassword::read_password().context("reading the value")
}
#[cfg(unix)]
struct Restore {
fd: std::os::fd::RawFd,
original: libc::termios,
}
#[cfg(unix)]
impl Drop for Restore {
fn drop(&mut self) {
unsafe {
libc::tcsetattr(self.fd, libc::TCSANOW, &self.original);
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
#[test]
fn backspace_removes_a_whole_character() {
let mut bytes = "aé€".as_bytes().to_vec(); assert!(pop_last_char(&mut bytes));
assert_eq!(bytes, "aé".as_bytes());
assert!(pop_last_char(&mut bytes));
assert_eq!(bytes, b"a");
assert!(pop_last_char(&mut bytes));
assert!(bytes.is_empty());
assert!(!pop_last_char(&mut bytes), "an empty value has nothing to erase");
}
#[test]
fn asterisks_are_counted_per_character_not_per_byte() {
let printed = "aé€".bytes().filter(|b| !is_continuation(*b)).count();
assert_eq!(printed, "aé€".chars().count());
}
}