1#![forbid(unsafe_code)]
16
17use super::ConversationHandler;
18use crate::error::ErrorCode;
19use std::ffi::{CStr, CString};
20use std::io::{self, BufRead, Write};
21
22fn trim_newline(s: &mut String) {
24 if s.ends_with('\n') {
25 s.pop();
26 if s.ends_with('\r') {
27 s.pop();
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47pub struct Conversation {
48 info_prefix: String,
49 error_prefix: String,
50}
51
52impl Conversation {
53 #[must_use]
55 pub fn new() -> Self {
56 Self {
57 info_prefix: "[PAM INFO] ".to_string(),
58 error_prefix: "[PAM ERROR] ".to_string(),
59 }
60 }
61
62 #[inline]
64 #[must_use]
65 pub fn info_prefix(&self) -> &str {
66 &self.info_prefix
67 }
68
69 pub fn set_info_prefix(&mut self, prefix: impl Into<String>) {
71 self.info_prefix = prefix.into();
72 }
73
74 #[inline]
76 #[must_use]
77 pub fn error_prefix(&self) -> &str {
78 &self.error_prefix
79 }
80
81 pub fn set_error_prefix(&mut self, prefix: impl Into<String>) {
83 self.error_prefix = prefix.into();
84 }
85}
86
87impl Default for Conversation {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl ConversationHandler for Conversation {
94 fn prompt_echo_on(&mut self, msg: &CStr) -> Result<CString, ErrorCode> {
95 let mut line = String::new();
96 if io::stderr().lock().write_all(msg.to_bytes()).is_err() {
97 return Err(ErrorCode::CONV_ERR);
98 }
99 let result = io::stdin().lock().read_line(&mut line);
100 match result {
101 Err(_) | Ok(0) => Err(ErrorCode::CONV_ERR),
102 Ok(_) => {
103 trim_newline(&mut line);
104 CString::new(line).map_err(|_| ErrorCode::CONV_ERR)
105 }
106 }
107 }
108
109 fn prompt_echo_off(&mut self, msg: &CStr) -> Result<CString, ErrorCode> {
110 let prompt = msg.to_string_lossy();
111 match rpassword::prompt_password(&prompt) {
112 Err(_) => Err(ErrorCode::CONV_ERR),
113 Ok(password) => CString::new(password).map_err(|_| ErrorCode::CONV_ERR),
114 }
115 }
116
117 fn text_info(&mut self, msg: &CStr) {
118 eprintln!("{}{}", &self.info_prefix, msg.to_string_lossy());
119 }
120
121 fn error_msg(&mut self, msg: &CStr) {
122 eprintln!("{}{}", &self.error_prefix, msg.to_string_lossy());
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_trim() {
132 let mut value = "Test\r\n".to_string();
133 trim_newline(&mut value);
134 assert_eq!(value, "Test");
135
136 let mut value = "Test\n".to_string();
137 trim_newline(&mut value);
138 assert_eq!(value, "Test");
139 }
140
141 #[test]
142 fn test_output() {
143 let mut c = Conversation::default();
144 c.set_info_prefix("INFO: ");
145 c.set_error_prefix("ERROR: ");
146 assert_eq!(c.info_prefix(), "INFO: ");
147 assert_eq!(c.error_prefix(), "ERROR: ");
148 c.text_info(&CString::new("test").unwrap());
149 c.error_msg(&CString::new("test2").unwrap());
150
151 assert!(format!("{:?}", &c).contains("ERROR: "));
152 }
153}