1use rtoolbox::fix_line_issues::fix_line_issues;
34use rtoolbox::print_tty::print_writer;
35use rtoolbox::safe_string::SafeString;
36use std::fs::OpenOptions;
37use std::io;
38use std::io::{BufRead, Cursor, Write};
39
40mod config;
41mod feedback;
42
43#[cfg(all(target_family = "unix", not(target_family = "wasm")))]
44mod unix;
45#[cfg(all(target_family = "unix", not(target_family = "wasm")))]
46use unix::*;
47
48#[cfg(target_family = "windows")]
49mod windows;
50#[cfg(target_family = "windows")]
51use windows::*;
52
53mod utf8;
54#[cfg(target_family = "wasm")]
55mod wasm;
56
57#[cfg(target_family = "wasm")]
58use wasm::*;
59
60use crate::config::{OutputTarget, PasswordFeedback};
61use crate::feedback::FeedbackState;
62pub use config::{Config, ConfigBuilder};
63
64const BACKSPACE: char = '\x08';
65const DEL: char = '\x7F';
66const CTRL_C: char = '\x03';
67const CTRL_D: char = '\x04';
68const CTRL_U: char = '\x15';
69const CTRL_W: char = '\x17';
70const ESC: char = '\x1B';
71
72trait RawPasswordInput {
73 fn new(config: Config) -> io::Result<impl RawPasswordInput>;
74 fn needs_terminal_configuration(&self) -> bool;
75 fn apply_terminal_configuration(&mut self) -> io::Result<()>;
76 fn read_char(&mut self) -> std::io::Result<char>;
77 fn write_output(&mut self, output: &str) -> std::io::Result<()>;
78 fn send_signal_sigint(&mut self) -> std::io::Result<()>;
79
80 fn read_password(&mut self, password_feedback: PasswordFeedback) -> std::io::Result<String> {
82 if self.needs_terminal_configuration() {
83 self.apply_terminal_configuration()?;
84 }
85
86 let mut state = FeedbackState::new(password_feedback, self.needs_terminal_configuration());
87
88 loop {
89 let c = match self.read_char() {
90 Ok(c) => c,
91 Err(e) => {
92 if e.kind() == std::io::ErrorKind::UnexpectedEof {
93 break;
94 }
95 return Err(e);
96 }
97 };
98 match c {
99 '\n' | '\r' => {
101 let output = state.finish();
102 if !output.is_empty() {
103 self.write_output(output.as_str())?;
104 }
105 break;
106 }
107 DEL | BACKSPACE => {
109 let output = state.pop_char();
110 if !output.is_empty() {
111 self.write_output(output.as_str())?;
112 }
113 }
114 CTRL_U => {
116 let output = state.clear();
117 if !output.is_empty() {
118 self.write_output(output.as_str())?;
119 }
120 }
121 CTRL_W => {
123 let output = state.clear_til_last_space();
124 if !output.is_empty() {
125 self.write_output(output.as_str())?;
126 }
127 }
128 CTRL_C => {
130 let output = state.abort();
131 if !output.is_empty() {
132 self.write_output(output.as_str())?;
133 }
134 self.send_signal_sigint()?;
135 return Err(std::io::Error::new(
136 std::io::ErrorKind::Interrupted,
137 "interrupted",
138 ));
139 }
140 CTRL_D => {
142 if state.is_empty() {
143 return Err(std::io::Error::new(
144 std::io::ErrorKind::UnexpectedEof,
145 "unexpected end of file",
146 ));
147 }
148 }
149 ESC => {
151 let c = match self.read_char() {
152 Ok(c) => c,
153 Err(e) => {
154 if e.kind() == std::io::ErrorKind::UnexpectedEof {
155 break;
156 }
157 return Err(e);
158 }
159 };
160
161 if c == '[' || c == 'O' {
162 loop {
164 let c = match self.read_char() {
165 Ok(c) => c,
166 Err(e) => {
167 if e.kind() == std::io::ErrorKind::UnexpectedEof {
168 break;
169 }
170 return Err(e);
171 }
172 };
173 if ('\x40'..='\x7E').contains(&c) {
174 break;
175 }
176 }
177 }
178 }
179 c if !c.is_control() => {
180 let output = state.push_char(c);
181 if !output.is_empty() {
182 self.write_output(output.as_str())?;
183 }
184 }
185 _ => {}
187 }
188 }
189
190 Ok(state.into_password())
191 }
192}
193
194#[deprecated(
213 since = "7.5.0",
214 note = "Use `read_password_with_config` with `ConfigBuilder::input_reader` instead."
215)]
216pub fn read_password_from_bufread(reader: &mut impl BufRead) -> std::io::Result<String> {
217 let mut password = SafeString::new();
218 reader.read_line(&mut password)?;
219
220 fix_line_issues(password.into_inner())
221}
222
223#[deprecated(
244 since = "7.5.0",
245 note = "Use `prompt_password_with_config` with `ConfigBuilder::input_reader` and `ConfigBuilder::output_writer()` instead."
246)]
247#[allow(deprecated)]
248pub fn prompt_password_from_bufread(
249 reader: &mut impl BufRead,
250 writer: &mut impl Write,
251 prompt: impl ToString,
252) -> std::io::Result<String> {
253 print_writer(writer, prompt.to_string().as_str())
254 .and_then(|_| read_password_from_bufread(reader))
255}
256
257pub fn read_password_with_config(config: Config) -> std::io::Result<String> {
259 let password_feedback = config.password_feedback;
260 let mut raw_mode_input = RawModeInput::new(config)?;
261 raw_mode_input.read_password(password_feedback)
262}
263
264pub fn read_password() -> std::io::Result<String> {
266 read_password_with_config(ConfigBuilder::default().build())
267}
268
269pub fn prompt_password(prompt: impl ToString) -> std::io::Result<String> {
271 prompt_password_with_config(prompt, ConfigBuilder::new().build())
272}
273
274pub fn prompt_password_with_config(
276 prompt: impl ToString,
277 mut config: Config,
278) -> std::io::Result<String> {
279 {
280 let mut output: Box<dyn Write> = match &mut config.output {
283 OutputTarget::FilePath(path) => Box::new(OpenOptions::new().write(true).open(path)?),
284 OutputTarget::Writer(writer) => Box::new(writer),
285 OutputTarget::Void => Box::new(Cursor::new(Vec::<u8>::new())), };
287 output.write_all(prompt.to_string().as_bytes())?;
288 output.flush()?;
289 }
290
291 read_password_with_config(config)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use std::io::Cursor;
298
299 fn mock_input_crlf() -> Cursor<&'static [u8]> {
300 Cursor::new(&b"A mocked response.\r\nAnother mocked response.\r\n"[..])
301 }
302
303 fn mock_input_lf() -> Cursor<&'static [u8]> {
304 Cursor::new(&b"A mocked response.\nAnother mocked response.\n"[..])
305 }
306
307 #[test]
308 #[allow(deprecated)]
309 fn can_read_from_redirected_input_many_times() {
310 let mut reader_crlf = mock_input_crlf();
311
312 let response = read_password_from_bufread(&mut reader_crlf).unwrap();
313 assert_eq!(response, "A mocked response.");
314 let response = read_password_from_bufread(&mut reader_crlf).unwrap();
315 assert_eq!(response, "Another mocked response.");
316
317 let mut reader_lf = mock_input_lf();
318 let response = read_password_from_bufread(&mut reader_lf).unwrap();
319 assert_eq!(response, "A mocked response.");
320 let response = read_password_from_bufread(&mut reader_lf).unwrap();
321 assert_eq!(response, "Another mocked response.");
322 }
323
324 #[test]
325 fn test_read_password_with_config_with_input_file() {
326 let mut temp_file = tempfile::NamedTempFile::new().unwrap();
327 temp_file.write_all(b"password\n").unwrap();
328 let path = temp_file.path().to_str().unwrap().to_string();
329
330 let config = ConfigBuilder::new()
331 .input_file_path(path.as_str())
332 .output_discard()
333 .build();
334
335 let result = read_password_with_config(config);
336 assert_eq!("password", result.unwrap());
337 }
338
339 #[test]
340 fn test_read_password_with_config_with_input_cursor() {
341 let config = ConfigBuilder::new()
342 .input_data("hello world\x7F\x7F\x7F\n")
343 .output_discard()
344 .build();
345
346 let result = read_password_with_config(config);
347 assert_eq!("hello wo", result.unwrap());
348 }
349}