comparison src/conv.rs @ 74:c7c596e6388f

Make conversations type-safe (last big reorg) (REAL) (NOT CLICKBAIT) In previous versions of Conversation, you could send messages and then return messages of the wrong type or in the wrong order or whatever. The receiver would then have to make sure that there were the right number of messages and that each message was the right type. That's annoying. This change makes the `Message` enum a two-way channel, where the asker puts their question into it, and then the answerer (the conversation) puts the answer in and returns control to the asker. The asker then only has to pull the Answer of the type they wanted out of the message.
author Paul Fisher <paul@pfish.zone>
date Fri, 06 Jun 2025 22:21:17 -0400
parents ac6881304c78
children e58d24849e82
comparison
equal deleted inserted replaced
73:ac6881304c78 74:c7c596e6388f
4 #![allow(dead_code)] 4 #![allow(dead_code)]
5 5
6 use crate::constants::Result; 6 use crate::constants::Result;
7 use crate::ErrorCode; 7 use crate::ErrorCode;
8 use secure_string::SecureString; 8 use secure_string::SecureString;
9 // TODO: In most cases, we should be passing around references to strings 9 use std::cell::Cell;
10 // or binary data. Right now we don't because that turns type inference and
11 // trait definitions/implementations into a HUGE MESS.
12 //
13 // Ideally, we would be using some kind of `TD: TextData` and `BD: BinaryData`
14 // associated types in the various Conversation traits to avoid copying
15 // when unnecessary.
16 10
17 /// The types of message and request that can be sent to a user. 11 /// The types of message and request that can be sent to a user.
18 /// 12 ///
19 /// The data within each enum value is the prompt (or other information) 13 /// The data within each enum value is the prompt (or other information)
20 /// that will be presented to the user. 14 /// that will be presented to the user.
21 #[derive(Clone, Copy, Debug)]
22 pub enum Message<'a> { 15 pub enum Message<'a> {
23 /// Requests information from the user; will be masked when typing. 16 MaskedPrompt(&'a MaskedPrompt<'a>),
17 Prompt(&'a Prompt<'a>),
18 RadioPrompt(&'a RadioPrompt<'a>),
19 BinaryPrompt(&'a BinaryPrompt<'a>),
20 InfoMsg(&'a InfoMsg<'a>),
21 ErrorMsg(&'a ErrorMsg<'a>),
22 }
23
24 /// A question-and-answer pair that can be communicated in a [`Conversation`].
25 ///
26 /// The asking side creates a `QAndA`, then converts it to a [`Message`]
27 /// and sends it via a [`Conversation`]. The Conversation then retrieves
28 /// the answer to the question (if needed) and sets the response.
29 /// Once control returns to the asker, the asker gets the answer from this
30 /// `QAndA` and uses it however it wants.
31 ///
32 /// For a more detailed explanation of how this works,
33 /// see [`Conversation::communicate`].
34 pub trait QAndA<'a> {
35 /// The type of the content of the question.
36 type Question: Copy;
37 /// The type of the answer to the question.
38 type Answer;
39
40 /// Converts this Q-and-A pair into a [`Message`] for the [`Conversation`].
41 fn message(&self) -> Message;
42
43 /// The contents of the question being asked.
24 /// 44 ///
25 /// Response: [`MaskedText`](Response::MaskedText) 45 /// For instance, this might say `"Username:"` to prompt the user
26 MaskedPrompt(&'a str), 46 /// for their name.
27 /// Requests information from the user; will not be masked. 47 fn question(&self) -> Self::Question;
48
49 /// Sets the answer to the question.
28 /// 50 ///
29 /// Response: [`Text`](Response::Text) 51 /// The [`Conversation`] implementation calls this to set the answer.
30 Prompt(&'a str), 52 /// The conversation should *always call this function*, even for messages
31 /// "Yes/No/Maybe conditionals" (a Linux-PAM extension). 53 /// that don't have "an answer" (like error or info messages).
54 fn set_answer(&self, answer: Result<Self::Answer>);
55
56 /// Gets the answer to the question.
57 fn answer(self) -> Result<Self::Answer>;
58 }
59
60 macro_rules! q_and_a {
61 ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $($doc:literal)*) => {
62 $(
63 #[doc = $doc]
64 )*
65 pub struct $name<'a> {
66 q: $qt,
67 a: Cell<Result<$at>>,
68 }
69
70 impl<'a> QAndA<'a> for $name<'a> {
71 type Question = $qt;
72 type Answer = $at;
73
74 fn question(&self) -> Self::Question {
75 self.q
76 }
77
78 fn set_answer(&self, answer: Result<Self::Answer>) {
79 self.a.set(answer)
80 }
81
82 fn answer(self) -> Result<Self::Answer> {
83 self.a.into_inner()
84 }
85
86 fn message(&self) -> Message {
87 Message::$name(self)
88 }
89 }
90 };
91 }
92
93 macro_rules! ask {
94 ($t:ident) => {
95 impl<'a> $t<'a> {
96 #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
97 fn ask(question: &'a str) -> Self {
98 Self {
99 q: question,
100 a: Cell::new(Err(ErrorCode::ConversationError)),
101 }
102 }
103 }
104 };
105 }
106
107 q_and_a!(
108 MaskedPrompt<'a, Q=&'a str, A=SecureString>,
109 "Asks the user for data and does not echo it back while being entered."
110 ""
111 "In other words, a password entry prompt."
112 );
113 ask!(MaskedPrompt);
114
115 q_and_a!(
116 Prompt<'a, Q=&'a str, A=String>,
117 "Asks the user for data."
118 ""
119 "This is the normal \"ask a person a question\" prompt."
120 "When the user types, their input will be shown to them."
121 "It can be used for things like usernames."
122 );
123 ask!(Prompt);
124
125 q_and_a!(
126 RadioPrompt<'a, Q=&'a str, A=String>,
127 "Asks the user for \"radio button\"–style data. (Linux-PAM extension)"
128 ""
129 "This message type is theoretically useful for \"yes/no/maybe\""
130 "questions, but nowhere in the documentation is it specified"
131 "what the format of the answer will be, or how this should be shown."
132 );
133 ask!(RadioPrompt);
134
135 q_and_a!(
136 BinaryPrompt<'a, Q=BinaryQuestion<'a>, A=BinaryData>,
137 "Asks for binary data. (Linux-PAM extension)"
138 ""
139 "This sends a binary message to the client application."
140 "It can be used to communicate with non-human logins,"
141 "or to enable things like security keys."
142 ""
143 "The `data_type` tag is a value that is simply passed through"
144 "to the application. PAM does not define any meaning for it."
145 );
146 impl<'a> BinaryPrompt<'a> {
147 /// Creates a prompt for the given binary data.
32 /// 148 ///
33 /// Response: [`Text`](Response::Text) 149 /// The `data_type` is a tag you can use for communication between
34 /// (Linux-PAM documentation doesn't define its contents.) 150 /// the module and the application. Its meaning is undefined by PAM.
35 RadioPrompt(&'a str), 151 fn ask(data: &'a [u8], data_type: u8) -> Self {
36 /// Raises an error message to the user. 152 Self {
37 /// 153 q: BinaryQuestion { data, data_type },
38 /// Response: [`NoResponse`](Response::NoResponse) 154 a: Cell::new(Err(ErrorCode::ConversationError)),
39 Error(&'a str), 155 }
40 /// Sends an informational message to the user. 156 }
41 /// 157 }
42 /// Response: [`NoResponse`](Response::NoResponse) 158
43 Info(&'a str), 159 /// The contents of a question requesting binary data.
44 /// Requests binary data from the client (a Linux-PAM extension). 160 ///
45 /// 161 /// A borrowed version of [`BinaryData`].
46 /// This is used for non-human or non-keyboard prompts (security key?). 162 #[derive(Copy, Clone, Debug)]
47 /// NOT part of the X/Open PAM specification. 163 pub struct BinaryQuestion<'a> {
48 /// 164 data: &'a [u8],
49 /// Response: [`Binary`](Response::Binary) 165 data_type: u8,
50 BinaryPrompt { 166 }
51 /// Some binary data. 167
52 data: &'a [u8], 168 impl BinaryQuestion<'_> {
53 /// A "type" that you can use for signalling. Has no strict definition in PAM. 169 /// Gets the data of this question.
54 data_type: u8, 170 pub fn data(&self) -> &[u8] {
55 }, 171 self.data
56 } 172 }
57 173
58 /// The responses that PAM will return from a request. 174 /// Gets the "type" of this data.
59 #[derive(Debug, PartialEq, derive_more::From)] 175 pub fn data_type(&self) -> u8 {
60 pub enum Response { 176 self.data_type
61 /// Used to fill in list entries where there is no response expected. 177 }
62 /// 178 }
63 /// Used in response to: 179
64 /// 180 /// Owned binary data.
65 /// - [`Error`](Message::Error) 181 ///
66 /// - [`Info`](Message::Info) 182 /// For borrowed data, see [`BinaryQuestion`].
67 NoResponse, 183 /// You can take ownership of the stored data with `.into::<Vec<u8>>()`.
68 /// A response with text data from the user. 184 #[derive(Debug, PartialEq)]
69 /// 185 pub struct BinaryData {
70 /// Used in response to: 186 data: Vec<u8>,
71 /// 187 data_type: u8,
72 /// - [`Prompt`](Message::Prompt) 188 }
73 /// - [`RadioPrompt`](Message::RadioPrompt) (a Linux-PAM extension) 189
74 Text(String), 190 impl BinaryData {
75 /// A response to a masked request with text data from the user. 191 /// Creates a `BinaryData` with the given contents and type.
76 /// 192 pub fn new(data: Vec<u8>, data_type: u8) -> Self {
77 /// Used in response to: 193 Self { data, data_type }
78 /// 194 }
79 /// - [`MaskedPrompt`](Message::MaskedPrompt) 195 /// A borrowed view of the data here.
80 MaskedText(SecureString), 196 pub fn data(&self) -> &[u8] {
81 /// A response to a binary request (a Linux-PAM extension). 197 &self.data
82 /// 198 }
83 /// Used in response to: 199 /// The type of the data stored in this.
84 /// 200 pub fn data_type(&self) -> u8 {
85 /// - [`BinaryPrompt`](Message::BinaryPrompt) 201 self.data_type
86 Binary(BinaryData), 202 }
87 } 203 }
88 204
89 /// The function type for a conversation. 205 impl From<BinaryData> for Vec<u8> {
90 /// 206 /// Takes ownership of the data stored herein.
91 /// A macro to save typing `FnMut(&[Message]) -> Result<Vec<Response>>`. 207 fn from(value: BinaryData) -> Self {
92 #[macro_export] 208 value.data
93 macro_rules! conv_type { 209 }
94 () => {FnMut(&[Message]) -> Result<Vec<Response>>}; 210 }
95 (impl) => { impl FnMut(&[Message]) -> Result<Vec<Response>> } 211
212 q_and_a!(
213 InfoMsg<'a, Q = &'a str, A = ()>,
214 "A message containing information to be passed to the user."
215 ""
216 "While this does not have an answer, [`Conversation`] implementations"
217 "should still call [`set_answer`][`QAndA::set_answer`] to verify that"
218 "the message has been displayed (or actively discarded)."
219 );
220 impl<'a> InfoMsg<'a> {
221 /// Creates an informational message to send to the user.
222 fn new(message: &'a str) -> Self {
223 Self {
224 q: message,
225 a: Cell::new(Err(ErrorCode::ConversationError)),
226 }
227 }
228 }
229
230 q_and_a!(
231 ErrorMsg<'a, Q = &'a str, A = ()>,
232 "An error message to be passed to the user."
233 ""
234 "While this does not have an answer, [`Conversation`] implementations"
235 "should still call [`set_answer`][`QAndA::set_answer`] to verify that"
236 "the message has been displayed (or actively discarded)."
237
238 );
239 impl<'a> ErrorMsg<'a> {
240 /// Creates an error message to send to the user.
241 fn new(message: &'a str) -> Self {
242 Self {
243 q: message,
244 a: Cell::new(Err(ErrorCode::ConversationError)),
245 }
246 }
96 } 247 }
97 248
98 /// A channel for PAM modules to request information from the user. 249 /// A channel for PAM modules to request information from the user.
99 /// 250 ///
100 /// This trait is used by both applications and PAM modules: 251 /// This trait is used by both applications and PAM modules:
106 pub trait Conversation { 257 pub trait Conversation {
107 /// Sends messages to the user. 258 /// Sends messages to the user.
108 /// 259 ///
109 /// The returned Vec of messages always contains exactly as many entries 260 /// The returned Vec of messages always contains exactly as many entries
110 /// as there were messages in the request; one corresponding to each. 261 /// as there were messages in the request; one corresponding to each.
111 fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>>; 262 ///
112 } 263 /// TODO: write detailed documentation about how to use this.
113 264 fn communicate(&mut self, messages: &[Message]);
114 fn conversation_func(func: conv_type!(impl)) -> impl Conversation { 265 }
266
267 /// Turns a simple function into a [`Conversation`].
268 ///
269 /// This can be used to wrap a free-floating function for use as a
270 /// Conversation:
271 ///
272 /// ```
273 /// use nonstick::conv::{Conversation, Message, conversation_func};
274 /// mod some_library {
275 /// # use nonstick::Conversation;
276 /// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ }
277 /// }
278 ///
279 /// fn my_terminal_prompt(messages: &[Message]) {
280 /// // ...
281 /// # todo!()
282 /// }
283 ///
284 /// fn main() {
285 /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt));
286 /// }
287 /// ```
288 pub fn conversation_func(func: impl FnMut(&[Message])) -> impl Conversation {
115 Convo(func) 289 Convo(func)
116 } 290 }
117 291
118 struct Convo<C: FnMut(&[Message]) -> Result<Vec<Response>>>(C); 292 struct Convo<C: FnMut(&[Message])>(C);
119 293
120 impl<C: FnMut(&[Message]) -> Result<Vec<Response>>> Conversation for Convo<C> { 294 impl<C: FnMut(&[Message])> Conversation for Convo<C> {
121 fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> { 295 fn communicate(&mut self, messages: &[Message]) {
122 self.0(messages) 296 self.0(messages)
123 } 297 }
124 } 298 }
125 299
126 /// Provides methods to make it easier to send exactly one message. 300 /// A conversation trait for asking or answering one question at a time.
127 /// 301 ///
128 /// This is primarily used by PAM modules, so that a module that only needs 302 /// An implementation of this is provided for any [`Conversation`],
129 /// one piece of information at a time doesn't have a ton of boilerplate. 303 /// or a PAM application can implement this trait and handle messages
130 /// You may also find it useful for testing PAM application libraries. 304 /// one at a time.
305 ///
306 /// For example, to use a `Conversation` as a `SimpleConversation`:
131 /// 307 ///
132 /// ``` 308 /// ```
133 /// # use nonstick::{PamHandleModule, Conversation, Result}; 309 /// # use nonstick::{Conversation, Result};
134 /// # fn _do_test(mut pam_handle: impl PamHandleModule) -> Result<()> { 310 /// # use secure_string::SecureString;
135 /// use nonstick::ConversationMux; 311 /// // Bring this trait into scope to get `masked_prompt`, among others.
136 /// 312 /// use nonstick::SimpleConversation;
137 /// let token = pam_handle.masked_prompt("enter your one-time token")?; 313 ///
138 /// # Ok(()) 314 /// fn ask_for_token(convo: &mut impl Conversation) -> Result<SecureString> {
315 /// convo.masked_prompt("enter your one-time token")
316 /// }
317 /// ```
318 ///
319 /// or to use a `SimpleConversation` as a `Conversation`:
320 ///
321 /// ```
322 /// use secure_string::SecureString;
323 /// use nonstick::{Conversation, SimpleConversation};
324 /// # use nonstick::{BinaryData, Result};
325 /// mod some_library {
326 /// # use nonstick::Conversation;
327 /// pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ }
328 /// }
329 ///
330 /// struct MySimpleConvo { /* ... */ }
331 /// # impl MySimpleConvo { fn new() -> Self { Self{} } }
332 ///
333 /// impl SimpleConversation for MySimpleConvo {
334 /// // ...
335 /// # fn prompt(&mut self, request: &str) -> Result<String> {
336 /// # todo!()
139 /// # } 337 /// # }
140 pub trait ConversationMux { 338 /// #
339 /// # fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
340 /// # todo!()
341 /// # }
342 /// #
343 /// # fn radio_prompt(&mut self, request: &str) -> Result<String> {
344 /// # todo!()
345 /// # }
346 /// #
347 /// # fn error_msg(&mut self, message: &str) {
348 /// # todo!()
349 /// # }
350 /// #
351 /// # fn info_msg(&mut self, message: &str) {
352 /// # todo!()
353 /// # }
354 /// #
355 /// # fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
356 /// # todo!()
357 /// # }
358 /// }
359 ///
360 /// fn main() {
361 /// let mut simple = MySimpleConvo::new();
362 /// some_library::get_auth_data(&mut simple.as_conversation())
363 /// }
364 /// ```
365 pub trait SimpleConversation {
366 /// Lets you use this simple conversation as a full [Conversation].
367 ///
368 /// The wrapper takes each message received in [`Conversation::communicate`]
369 /// and passes them one-by-one to the appropriate method,
370 /// then collects responses to return.
371 fn as_conversation(&mut self) -> Demux<Self>
372 where
373 Self: Sized,
374 {
375 Demux(self)
376 }
141 /// Prompts the user for something. 377 /// Prompts the user for something.
142 fn prompt(&mut self, request: &str) -> Result<String>; 378 fn prompt(&mut self, request: &str) -> Result<String>;
143 /// Prompts the user for something, but hides what the user types. 379 /// Prompts the user for something, but hides what the user types.
144 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>; 380 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
145 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). 381 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension).
146 /// 382 ///
147 /// PAM documentation doesn't define the format of the response. 383 /// PAM documentation doesn't define the format of the response.
148 fn radio_prompt(&mut self, request: &str) -> Result<String>; 384 fn radio_prompt(&mut self, request: &str) -> Result<String>;
149 /// Alerts the user to an error. 385 /// Alerts the user to an error.
150 fn error(&mut self, message: &str); 386 fn error_msg(&mut self, message: &str);
151 /// Sends an informational message to the user. 387 /// Sends an informational message to the user.
152 fn info(&mut self, message: &str); 388 fn info_msg(&mut self, message: &str);
153 /// Requests binary data from the user (a Linux-PAM extension). 389 /// Requests binary data from the user (a Linux-PAM extension).
154 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>; 390 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
155 } 391 }
156 392
157 impl<C: Conversation> ConversationMux for C { 393 macro_rules! conv_fn {
158 /// Prompts the user for something. 394 ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $ask:path }) => {
159 fn prompt(&mut self, request: &str) -> Result<String> { 395 fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
160 let resp = self.converse(&[Message::Prompt(request)])?.pop(); 396 let prompt = $ask($($param),*);
161 match resp { 397 self.communicate(&[prompt.message()]);
162 Some(Response::Text(s)) => Ok(s), 398 prompt.answer()
163 _ => Err(ErrorCode::ConversationError), 399 }
164 } 400 };
165 } 401 ($fn_name:ident($($param:ident: $pt:ty),+) { $ask:path }) => {
166 402 fn $fn_name(&mut self, $($param: $pt),*) {
167 /// Prompts the user for something, but hides what the user types. 403 self.communicate(&[$ask($($param),*).message()]);
168 fn masked_prompt(&mut self, request: &str) -> Result<SecureString> { 404 }
169 let resp = self.converse(&[Message::MaskedPrompt(request)])?.pop(); 405 };
170 match resp { 406 }
171 Some(Response::MaskedText(s)) => Ok(s), 407
172 _ => Err(ErrorCode::ConversationError), 408 impl<C: Conversation> SimpleConversation for C {
173 } 409 conv_fn!(prompt(message: &str) -> String { Prompt::ask });
174 } 410 conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedPrompt::ask });
175 411 conv_fn!(radio_prompt(message: &str) -> String { RadioPrompt::ask });
176 /// Prompts the user for a yes/no/maybe conditional (a Linux-PAM extension). 412 conv_fn!(error_msg(message: &str) { ErrorMsg::new });
177 /// 413 conv_fn!(info_msg(message: &str) { InfoMsg::new });
178 /// PAM documentation doesn't define the format of the response. 414 conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryPrompt::ask });
179 fn radio_prompt(&mut self, request: &str) -> Result<String> { 415 }
180 let resp = self.converse(&[Message::RadioPrompt(request)])?.pop(); 416
181 match resp { 417 /// A [`Conversation`] which asks the questions one at a time.
182 Some(Response::Text(s)) => Ok(s), 418 ///
183 _ => Err(ErrorCode::ConversationError), 419 /// This is automatically created by [`SimpleConversation::as_conversation`].
184 } 420 pub struct Demux<'a, SC: SimpleConversation>(&'a mut SC);
185 } 421
186 422 impl<SC: SimpleConversation> Conversation for Demux<'_, SC> {
187 /// Alerts the user to an error. 423 fn communicate(&mut self, messages: &[Message]) {
188 fn error(&mut self, message: &str) { 424 for msg in messages {
189 let _ = self.converse(&[Message::Error(message)]); 425 match msg {
190 } 426 Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())),
191 427 Message::MaskedPrompt(prompt) => {
192 /// Sends an informational message to the user. 428 prompt.set_answer(self.0.masked_prompt(prompt.question()))
193 fn info(&mut self, message: &str) { 429 }
194 let _ = self.converse(&[Message::Info(message)]); 430 Message::RadioPrompt(prompt) => {
195 } 431 prompt.set_answer(self.0.radio_prompt(prompt.question()))
196 432 }
197 /// Requests binary data from the user (a Linux-PAM extension). 433 Message::InfoMsg(prompt) => {
198 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> { 434 self.0.info_msg(prompt.question());
199 let resp = self 435 prompt.set_answer(Ok(()))
200 .converse(&[Message::BinaryPrompt { data, data_type }])? 436 }
201 .pop(); 437 Message::ErrorMsg(prompt) => {
202 match resp { 438 self.0.error_msg(prompt.question());
203 Some(Response::Binary(d)) => Ok(d), 439 prompt.set_answer(Ok(()))
204 _ => Err(ErrorCode::ConversationError), 440 }
205 } 441 Message::BinaryPrompt(prompt) => {
206 } 442 let q = prompt.question();
207 } 443 prompt.set_answer(self.0.binary_prompt(q.data, q.data_type))
208 444 }
209 /// Trait that an application can implement if they want to handle messages 445 }
210 /// one at a time. 446 }
211 pub trait DemuxedConversation {
212 /// Prompts the user for some text.
213 fn prompt(&mut self, request: &str) -> Result<String>;
214 /// Prompts the user for some text, but hides their typing.
215 fn masked_prompt(&mut self, request: &str) -> Result<SecureString>;
216 /// Prompts the user for a radio option (a Linux-PAM extension).
217 ///
218 /// The Linux-PAM documentation doesn't give the format of the response.
219 fn radio_prompt(&mut self, request: &str) -> Result<String>;
220 /// Alerts the user to an error.
221 fn error(&mut self, message: &str);
222 /// Sends an informational message to the user.
223 fn info(&mut self, message: &str);
224 /// Requests binary data from the user (a Linux-PAM extension).
225 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
226 }
227
228 impl<DM> Conversation for DM
229 where
230 DM: DemuxedConversation,
231 {
232 fn converse(&mut self, messages: &[Message]) -> Result<Vec<Response>> {
233 messages
234 .iter()
235 .map(|msg| match *msg {
236 Message::Prompt(prompt) => self.prompt(prompt).map(Response::from),
237 Message::MaskedPrompt(prompt) => self.masked_prompt(prompt).map(Response::from),
238 Message::RadioPrompt(prompt) => self.radio_prompt(prompt).map(Response::from),
239 Message::Info(message) => {
240 self.info(message);
241 Ok(Response::NoResponse)
242 }
243 Message::Error(message) => {
244 self.error(message);
245 Ok(Response::NoResponse)
246 }
247 Message::BinaryPrompt { data_type, data } => {
248 self.binary_prompt(data, data_type).map(Response::from)
249 }
250 })
251 .collect()
252 }
253 }
254
255 /// Owned binary data.
256 #[derive(Debug, PartialEq)]
257 pub struct BinaryData {
258 data: Vec<u8>,
259 data_type: u8,
260 }
261
262 impl BinaryData {
263 pub fn new(data: Vec<u8>, data_type: u8) -> Self {
264 Self { data, data_type }
265 }
266 pub fn data(&self) -> &[u8] {
267 &self.data
268 }
269 pub fn data_type(&self) -> u8 {
270 self.data_type
271 }
272 }
273
274 impl From<BinaryData> for Vec<u8> {
275 /// Extracts the inner vector from the BinaryData.
276 fn from(value: BinaryData) -> Self {
277 value.data
278 } 447 }
279 } 448 }
280 449
281 #[cfg(test)] 450 #[cfg(test)]
282 mod tests { 451 mod tests {
283 use super::{Conversation, DemuxedConversation, Message, Response, SecureString}; 452 use super::{
453 BinaryPrompt, Conversation, ErrorMsg, InfoMsg, MaskedPrompt, Message, Prompt, QAndA,
454 RadioPrompt, Result, SecureString, SimpleConversation,
455 };
284 use crate::constants::ErrorCode; 456 use crate::constants::ErrorCode;
457 use crate::BinaryData;
285 458
286 #[test] 459 #[test]
287 fn test_demux() { 460 fn test_demux() {
288 #[derive(Default)] 461 #[derive(Default)]
289 struct DemuxTester { 462 struct DemuxTester {
290 error_ran: bool, 463 error_ran: bool,
291 info_ran: bool, 464 info_ran: bool,
292 } 465 }
293 466
294 impl DemuxedConversation for DemuxTester { 467 impl SimpleConversation for DemuxTester {
295 fn prompt(&mut self, request: &str) -> crate::Result<String> { 468 fn prompt(&mut self, request: &str) -> Result<String> {
296 match request { 469 match request {
297 "what" => Ok("whatwhat".to_owned()), 470 "what" => Ok("whatwhat".to_owned()),
298 "give_err" => Err(ErrorCode::PermissionDenied), 471 "give_err" => Err(ErrorCode::PermissionDenied),
299 _ => panic!("unexpected prompt!"), 472 _ => panic!("unexpected prompt!"),
300 } 473 }
301 } 474 }
302 fn masked_prompt(&mut self, request: &str) -> crate::Result<SecureString> { 475 fn masked_prompt(&mut self, request: &str) -> Result<SecureString> {
303 assert_eq!("reveal", request); 476 assert_eq!("reveal", request);
304 Ok(SecureString::from("my secrets")) 477 Ok(SecureString::from("my secrets"))
305 } 478 }
306 fn radio_prompt(&mut self, request: &str) -> crate::Result<String> { 479 fn radio_prompt(&mut self, request: &str) -> Result<String> {
307 assert_eq!("channel?", request); 480 assert_eq!("channel?", request);
308 Ok("zero".to_owned()) 481 Ok("zero".to_owned())
309 } 482 }
310 fn error(&mut self, message: &str) { 483 fn error_msg(&mut self, message: &str) {
311 self.error_ran = true; 484 self.error_ran = true;
312 assert_eq!("whoopsie", message); 485 assert_eq!("whoopsie", message);
313 } 486 }
314 fn info(&mut self, message: &str) { 487 fn info_msg(&mut self, message: &str) {
315 self.info_ran = true; 488 self.info_ran = true;
316 assert_eq!("did you know", message); 489 assert_eq!("did you know", message);
317 } 490 }
318 fn binary_prompt( 491 fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
319 &mut self,
320 data: &[u8],
321 data_type: u8,
322 ) -> crate::Result<super::BinaryData> {
323 assert_eq!(&[10, 9, 8], data); 492 assert_eq!(&[10, 9, 8], data);
324 assert_eq!(66, data_type); 493 assert_eq!(66, data_type);
325 Ok(super::BinaryData::new(vec![5, 5, 5], 5)) 494 Ok(BinaryData::new(vec![5, 5, 5], 5))
326 } 495 }
327 } 496 }
328 497
329 let mut tester = DemuxTester::default(); 498 let mut tester = DemuxTester::default();
330 499
331 assert_eq!( 500 let what = Prompt::ask("what");
332 vec![ 501 let pass = MaskedPrompt::ask("reveal");
333 Response::Text("whatwhat".to_owned()), 502 let err = ErrorMsg::new("whoopsie");
334 Response::MaskedText("my secrets".into()), 503 let info = InfoMsg::new("did you know");
335 Response::NoResponse, 504 let has_err = Prompt::ask("give_err");
336 Response::NoResponse, 505
337 ], 506 let mut conv = tester.as_conversation();
338 tester 507
339 .converse(&[ 508 // Basic tests.
340 Message::Prompt("what"), 509
341 Message::MaskedPrompt("reveal"), 510 conv.communicate(&[
342 Message::Error("whoopsie"), 511 what.message(),
343 Message::Info("did you know"), 512 pass.message(),
344 ]) 513 err.message(),
345 .unwrap() 514 info.message(),
346 ); 515 has_err.message(),
516 ]);
517
518 assert_eq!("whatwhat", what.answer().unwrap());
519 assert_eq!(SecureString::from("my secrets"), pass.answer().unwrap());
520 assert_eq!(Ok(()), err.answer());
521 assert_eq!(Ok(()), info.answer());
522 assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err());
347 assert!(tester.error_ran); 523 assert!(tester.error_ran);
348 assert!(tester.info_ran); 524 assert!(tester.info_ran);
349 525
350 assert_eq!( 526 // Test the Linux extensions separately.
351 ErrorCode::PermissionDenied, 527
352 tester.converse(&[Message::Prompt("give_err")]).unwrap_err(), 528 let mut conv = tester.as_conversation();
353 ); 529
354 530 let radio = RadioPrompt::ask("channel?");
355 // Test the Linux-PAM extensions separately. 531 let bin = BinaryPrompt::ask(&[10, 9, 8], 66);
356 532 conv.communicate(&[radio.message(), bin.message()]);
357 assert_eq!( 533
358 vec![ 534 assert_eq!("zero", radio.answer().unwrap());
359 Response::Text("zero".to_owned()), 535 assert_eq!(BinaryData::new(vec![5, 5, 5], 5), bin.answer().unwrap());
360 Response::Binary(super::BinaryData::new(vec![5, 5, 5], 5)), 536 }
361 ], 537
362 tester
363 .converse(&[
364 Message::RadioPrompt("channel?"),
365 Message::BinaryPrompt {
366 data: &[10, 9, 8],
367 data_type: 66
368 },
369 ])
370 .unwrap()
371 );
372 }
373
374 #[test]
375 fn test_mux() { 538 fn test_mux() {
376 use super::ConversationMux;
377 struct MuxTester; 539 struct MuxTester;
378 540
379 impl Conversation for MuxTester { 541 impl Conversation for MuxTester {
380 fn converse(&mut self, messages: &[Message]) -> crate::Result<Vec<Response>> { 542 fn communicate(&mut self, messages: &[Message]) {
381 if let [msg] = messages { 543 if let [msg] = messages {
382 match msg { 544 match *msg {
383 Message::Info(info) => { 545 Message::InfoMsg(info) => {
384 assert_eq!("let me tell you", *info); 546 assert_eq!("let me tell you", info.question());
385 Ok(vec![Response::NoResponse]) 547 info.set_answer(Ok(()))
386 } 548 }
387 Message::Error(error) => { 549 Message::ErrorMsg(error) => {
388 assert_eq!("oh no", *error); 550 assert_eq!("oh no", error.question());
389 Ok(vec![Response::NoResponse]) 551 error.set_answer(Ok(()))
390 } 552 }
391 Message::Prompt("should_error") => Err(ErrorCode::BufferError), 553 Message::Prompt(prompt) => prompt.set_answer(match prompt.question() {
392 Message::Prompt(ask) => { 554 "should_err" => Err(ErrorCode::PermissionDenied),
393 assert_eq!("question", *ask); 555 "question" => Ok("answer".to_owned()),
394 Ok(vec![Response::Text("answer".to_owned())]) 556 other => panic!("unexpected question {other:?}"),
557 }),
558 Message::MaskedPrompt(ask) => {
559 assert_eq!("password!", ask.question());
560 ask.set_answer(Ok("open sesame".into()))
395 } 561 }
396 Message::MaskedPrompt("return_wrong_type") => { 562 Message::BinaryPrompt(prompt) => {
397 Ok(vec![Response::NoResponse]) 563 assert_eq!(&[1, 2, 3], prompt.question().data);
398 } 564 assert_eq!(69, prompt.question().data_type);
399 Message::MaskedPrompt(ask) => { 565 prompt.set_answer(Ok(BinaryData::new(vec![3, 2, 1], 42)))
400 assert_eq!("password!", *ask);
401 Ok(vec![Response::MaskedText(SecureString::from(
402 "open sesame",
403 ))])
404 }
405 Message::BinaryPrompt { data, data_type } => {
406 assert_eq!(&[1, 2, 3], data);
407 assert_eq!(69, *data_type);
408 Ok(vec![Response::Binary(super::BinaryData::new(
409 vec![3, 2, 1],
410 42,
411 ))])
412 } 566 }
413 Message::RadioPrompt(ask) => { 567 Message::RadioPrompt(ask) => {
414 assert_eq!("radio?", *ask); 568 assert_eq!("radio?", ask.question());
415 Ok(vec![Response::Text("yes".to_owned())]) 569 ask.set_answer(Ok("yes".to_owned()))
416 } 570 }
417 } 571 }
418 } else { 572 } else {
419 panic!("messages is the wrong size ({len})", len = messages.len()) 573 panic!(
574 "there should only be one message, not {len}",
575 len = messages.len()
576 )
420 } 577 }
421 } 578 }
422 } 579 }
423 580
424 let mut tester = MuxTester; 581 let mut tester = MuxTester;
426 assert_eq!("answer", tester.prompt("question").unwrap()); 583 assert_eq!("answer", tester.prompt("question").unwrap());
427 assert_eq!( 584 assert_eq!(
428 SecureString::from("open sesame"), 585 SecureString::from("open sesame"),
429 tester.masked_prompt("password!").unwrap() 586 tester.masked_prompt("password!").unwrap()
430 ); 587 );
431 tester.error("oh no"); 588 tester.error_msg("oh no");
432 tester.info("let me tell you"); 589 tester.info_msg("let me tell you");
433 { 590 {
434 assert_eq!("yes", tester.radio_prompt("radio?").unwrap()); 591 assert_eq!("yes", tester.radio_prompt("radio?").unwrap());
435 assert_eq!( 592 assert_eq!(
436 super::BinaryData::new(vec![3, 2, 1], 42), 593 BinaryData::new(vec![3, 2, 1], 42),
437 tester.binary_prompt(&[1, 2, 3], 69).unwrap(), 594 tester.binary_prompt(&[1, 2, 3], 69).unwrap(),
438 ) 595 )
439 } 596 }
440 assert_eq!( 597 assert_eq!(
441 ErrorCode::BufferError, 598 ErrorCode::BufferError,