1use rtoolbox::fix_line_issues::fix_line_issues;
32use rtoolbox::print_tty::{print_tty, print_writer};
33use rtoolbox::safe_string::SafeString;
34use std::io::{BufRead, Write};
35
36#[cfg(target_family = "wasm")]
37mod wasm {
38 use std::io::{self, BufRead};
39
40 pub fn read_password() -> std::io::Result<String> {
42 let tty = std::fs::File::open("/dev/tty")?;
43 let mut reader = io::BufReader::new(tty);
44
45 read_password_from_fd_with_hidden_input(&mut reader)
46 }
47
48 fn read_password_from_fd_with_hidden_input(
50 reader: &mut impl BufRead,
51 ) -> std::io::Result<String> {
52 let mut password = super::SafeString::new();
53
54 reader.read_line(&mut password)?;
55 super::fix_line_issues(password.into_inner())
56 }
57}
58
59#[cfg(target_family = "unix")]
60mod unix {
61 use libc::{c_int, tcsetattr, termios, ECHO, ECHONL, TCSANOW};
62 use std::io::{self, BufRead};
63 use std::mem;
64 use std::os::unix::io::AsRawFd;
65
66 struct HiddenInput {
67 fd: i32,
68 term_orig: termios,
69 }
70
71 impl HiddenInput {
72 fn new(fd: i32) -> io::Result<HiddenInput> {
73 let mut term = safe_tcgetattr(fd)?;
77 let term_orig = safe_tcgetattr(fd)?;
78
79 term.c_lflag &= !ECHO;
81
82 term.c_lflag |= ECHONL;
84
85 io_result(unsafe { tcsetattr(fd, TCSANOW, &term) })?;
87
88 Ok(HiddenInput { fd, term_orig })
89 }
90 }
91
92 impl Drop for HiddenInput {
93 fn drop(&mut self) {
94 unsafe {
96 tcsetattr(self.fd, TCSANOW, &self.term_orig);
97 }
98 }
99 }
100
101 fn io_result(ret: c_int) -> std::io::Result<()> {
103 match ret {
104 0 => Ok(()),
105 _ => Err(std::io::Error::last_os_error()),
106 }
107 }
108
109 fn safe_tcgetattr(fd: c_int) -> std::io::Result<termios> {
110 let mut term = mem::MaybeUninit::<termios>::uninit();
111 io_result(unsafe { ::libc::tcgetattr(fd, term.as_mut_ptr()) })?;
112 Ok(unsafe { term.assume_init() })
113 }
114
115 pub fn read_password() -> std::io::Result<String> {
117 let tty = std::fs::File::open("/dev/tty")?;
118 let fd = tty.as_raw_fd();
119 let mut reader = io::BufReader::new(tty);
120
121 read_password_from_fd_with_hidden_input(&mut reader, fd)
122 }
123
124 fn read_password_from_fd_with_hidden_input(
126 reader: &mut impl BufRead,
127 fd: i32,
128 ) -> std::io::Result<String> {
129 let mut password = super::SafeString::new();
130
131 let hidden_input = HiddenInput::new(fd)?;
132
133 reader.read_line(&mut password)?;
134
135 std::mem::drop(hidden_input);
136
137 super::fix_line_issues(password.into_inner())
138 }
139}
140
141#[cfg(target_family = "windows")]
142mod windows {
143 use std::io::BufRead;
144 use std::io::{self, BufReader};
145 use std::os::windows::io::FromRawHandle;
146 use windows_sys::core::PCSTR;
147 use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE};
148 use windows_sys::Win32::Storage::FileSystem::{
149 CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
150 };
151 use windows_sys::Win32::System::Console::{
152 GetConsoleMode, SetConsoleMode, CONSOLE_MODE, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT,
153 };
154
155 struct HiddenInput {
156 mode: u32,
157 handle: HANDLE,
158 }
159
160 impl HiddenInput {
161 fn new(handle: HANDLE) -> io::Result<HiddenInput> {
162 let mut mode = 0;
163
164 if unsafe { GetConsoleMode(handle, &mut mode as *mut CONSOLE_MODE) } == 0 {
166 return Err(std::io::Error::last_os_error());
167 }
168
169 let new_mode_flags = ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT;
171 if unsafe { SetConsoleMode(handle, new_mode_flags) } == 0 {
172 return Err(std::io::Error::last_os_error());
173 }
174
175 Ok(HiddenInput { mode, handle })
176 }
177 }
178
179 impl Drop for HiddenInput {
180 fn drop(&mut self) {
181 unsafe {
183 SetConsoleMode(self.handle, self.mode);
184 }
185 }
186 }
187
188 pub fn read_password() -> std::io::Result<String> {
190 let handle = unsafe {
191 CreateFileA(
192 b"CONIN$\x00".as_ptr() as PCSTR,
193 GENERIC_READ | GENERIC_WRITE,
194 FILE_SHARE_READ | FILE_SHARE_WRITE,
195 std::ptr::null(),
196 OPEN_EXISTING,
197 0,
198 INVALID_HANDLE_VALUE,
199 )
200 };
201
202 if handle == INVALID_HANDLE_VALUE {
203 return Err(std::io::Error::last_os_error());
204 }
205
206 let mut stream = BufReader::new(unsafe { std::fs::File::from_raw_handle(handle as _) });
207 read_password_from_handle_with_hidden_input(&mut stream, handle)
208 }
209
210 fn read_password_from_handle_with_hidden_input(
212 reader: &mut impl BufRead,
213 handle: HANDLE,
214 ) -> io::Result<String> {
215 let mut password = super::SafeString::new();
216
217 let hidden_input = HiddenInput::new(handle)?;
218
219 let reader_return = reader.read_line(&mut password);
220
221 println!();
223
224 if reader_return.is_err() {
225 return Err(reader_return.unwrap_err());
226 }
227
228 std::mem::drop(hidden_input);
229
230 super::fix_line_issues(password.into_inner())
231 }
232}
233
234#[cfg(target_family = "unix")]
235pub use unix::read_password;
236#[cfg(target_family = "wasm")]
237pub use wasm::read_password;
238#[cfg(target_family = "windows")]
239pub use windows::read_password;
240
241pub fn read_password_from_bufread(reader: &mut impl BufRead) -> std::io::Result<String> {
243 let mut password = SafeString::new();
244 reader.read_line(&mut password)?;
245
246 fix_line_issues(password.into_inner())
247}
248
249pub fn prompt_password_from_bufread(
251 reader: &mut impl BufRead,
252 writer: &mut impl Write,
253 prompt: impl ToString,
254) -> std::io::Result<String> {
255 print_writer(writer, prompt.to_string().as_str())
256 .and_then(|_| read_password_from_bufread(reader))
257}
258
259pub fn prompt_password(prompt: impl ToString) -> std::io::Result<String> {
261 print_tty(prompt.to_string().as_str()).and_then(|_| read_password())
262}
263
264#[cfg(test)]
265mod tests {
266 use std::io::Cursor;
267
268 fn mock_input_crlf() -> Cursor<&'static [u8]> {
269 Cursor::new(&b"A mocked response.\r\nAnother mocked response.\r\n"[..])
270 }
271
272 fn mock_input_lf() -> Cursor<&'static [u8]> {
273 Cursor::new(&b"A mocked response.\nAnother mocked response.\n"[..])
274 }
275
276 #[test]
277 fn can_read_from_redirected_input_many_times() {
278 let mut reader_crlf = mock_input_crlf();
279
280 let response = super::read_password_from_bufread(&mut reader_crlf).unwrap();
281 assert_eq!(response, "A mocked response.");
282 let response = super::read_password_from_bufread(&mut reader_crlf).unwrap();
283 assert_eq!(response, "Another mocked response.");
284
285 let mut reader_lf = mock_input_lf();
286 let response = super::read_password_from_bufread(&mut reader_lf).unwrap();
287 assert_eq!(response, "A mocked response.");
288 let response = super::read_password_from_bufread(&mut reader_lf).unwrap();
289 assert_eq!(response, "Another mocked response.");
290 }
291}