1#[derive(thiserror::Error, Debug)]
3pub enum Error {
4 #[error("IO error: {0}")]
5 Io(#[from] std::io::Error),
6}
7
8pub fn read_password() -> Result<String, Error> {
13 platform::read_password()
14}
15
16pub fn read_line() -> Result<String, Error> {
20 platform::read_line()
21}
22
23#[cfg(unix)]
26mod platform {
27 use std::io::Read;
28
29 use libc::{ECHO, ICANON, ICRNL, ISIG, STDIN_FILENO, TCSANOW, tcgetattr, tcsetattr, termios};
30
31 struct TermiosGuard {
33 saved: termios,
34 }
35
36 impl Drop for TermiosGuard {
37 fn drop(&mut self) {
38 unsafe { tcsetattr(STDIN_FILENO, TCSANOW, &self.saved) };
40 }
41 }
42
43 pub(super) fn read_password() -> Result<String, super::Error> {
44 let mut saved: termios = unsafe { std::mem::zeroed() };
46 let is_tty = unsafe { tcgetattr(STDIN_FILENO, &mut saved) } == 0;
47
48 let _guard = if is_tty {
50 let mut raw = saved;
51 raw.c_lflag &= !(ECHO as libc::tcflag_t);
55 raw.c_lflag |= (ICANON | ISIG) as libc::tcflag_t;
56 raw.c_iflag |= ICRNL as libc::tcflag_t;
57 unsafe { tcsetattr(STDIN_FILENO, TCSANOW, &raw) };
59 Some(TermiosGuard {
60 saved,
61 })
62 } else {
63 None
64 };
65
66 let buf = read_line_inner()?;
67 String::from_utf8(buf).map_err(|e| super::Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
68 }
69
70 fn read_line_inner() -> Result<Vec<u8>, super::Error> {
71 let mut buf = Vec::new();
72 let stdin = std::io::stdin();
73 for byte in stdin.lock().bytes() {
74 let b = byte?;
75 if b == b'\n' || b == b'\r' {
76 break;
77 }
78 buf.push(b);
79 }
80 Ok(buf)
81 }
82
83 pub(super) fn read_line() -> Result<String, super::Error> {
84 let buf = read_line_inner()?;
85 String::from_utf8(buf).map_err(|e| super::Error::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))
86 }
87}
88
89#[cfg(not(unix))]
92mod platform {
93 use std::io::BufRead;
94
95 pub(super) fn read_password() -> Result<String, super::Error> {
96 let mut line = String::new();
97 std::io::stdin().lock().read_line(&mut line)?;
98 if line.ends_with('\n') {
99 line.pop();
100 if line.ends_with('\r') {
101 line.pop();
102 }
103 }
104 Ok(line)
105 }
106
107 pub(super) fn read_line() -> Result<String, super::Error> {
108 let mut line = String::new();
109 std::io::stdin().lock().read_line(&mut line)?;
110 if line.ends_with('\n') {
111 line.pop();
112 if line.ends_with('\r') {
113 line.pop();
114 }
115 }
116 Ok(line)
117 }
118}
119
120#[cfg(test)]
123mod tests {
124 #[test]
131 fn password_bytes_strip_newline() {
132 let mut raw = b"secret\n".to_vec();
133 if raw.ends_with(b"\n") {
134 raw.pop();
135 if raw.ends_with(b"\r") {
136 raw.pop();
137 }
138 }
139 assert_eq!(raw, b"secret");
140 }
141
142 #[test]
143 fn password_bytes_strip_crlf() {
144 let mut raw = b"secret\r\n".to_vec();
145 if raw.ends_with(b"\n") {
146 raw.pop();
147 if raw.ends_with(b"\r") {
148 raw.pop();
149 }
150 }
151 assert_eq!(raw, b"secret");
152 }
153
154 #[test]
155 fn password_bytes_no_newline() {
156 let raw = b"secret".to_vec();
157 assert_eq!(raw, b"secret");
158 }
159
160 #[test]
162 fn read_line_non_utf8() {
163 let raw = b"\xff\xfe".to_vec();
164 match String::from_utf8(raw) {
165 Ok(_) => panic!("expected 'string from bytes' to be invalid"),
166 Err(_) => {} }
168 }
169
170 #[test]
171 fn read_line_strip_newline() {
172 let mut s = "hello\n".to_string();
173 if s.ends_with('\n') {
174 s.pop();
175 if s.ends_with('\r') {
176 s.pop();
177 }
178 }
179 assert_eq!(s, "hello");
180 }
181
182 #[test]
183 fn read_line_strip_crlf() {
184 let mut s = "hello\r\n".to_string();
185 if s.ends_with('\n') {
186 s.pop();
187 if s.ends_with('\r') {
188 s.pop();
189 }
190 }
191 assert_eq!(s, "hello");
192 }
193
194 #[test]
195 fn read_line_no_newline() {
196 let s = "hello".to_string();
197 assert_eq!(s, "hello");
198 }
199}