Mercurial > crates > nonstick
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, |