Mercurial > crates > nonstick
comparison src/conv.rs @ 130:80c07e5ab22f
Transfer over (almost) completely to using libpam-sys.
This reimplements everything in nonstick on top of the new -sys crate.
We don't yet use libpam-sys's helpers for binary message payloads. Soon.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Tue, 01 Jul 2025 06:11:43 -0400 |
parents | f3e260f9ddcb |
children |
comparison
equal
deleted
inserted
replaced
129:5b2de52dd8b2 | 130:80c07e5ab22f |
---|---|
7 use std::cell::Cell; | 7 use std::cell::Cell; |
8 use std::fmt; | 8 use std::fmt; |
9 use std::fmt::Debug; | 9 use std::fmt::Debug; |
10 use std::result::Result as StdResult; | 10 use std::result::Result as StdResult; |
11 | 11 |
12 /// The types of message and request that can be sent to a user. | 12 /// An individual pair of request/response to be sent to the user. |
13 /// | 13 #[derive(Debug)] |
14 /// The data within each enum value is the prompt (or other information) | |
15 /// that will be presented to the user. | |
16 #[non_exhaustive] | 14 #[non_exhaustive] |
17 pub enum Message<'a> { | 15 pub enum Exchange<'a> { |
18 Prompt(&'a QAndA<'a>), | 16 Prompt(&'a QAndA<'a>), |
19 MaskedPrompt(&'a MaskedQAndA<'a>), | 17 MaskedPrompt(&'a MaskedQAndA<'a>), |
20 Error(&'a ErrorMsg<'a>), | 18 Error(&'a ErrorMsg<'a>), |
21 Info(&'a InfoMsg<'a>), | 19 Info(&'a InfoMsg<'a>), |
22 RadioPrompt(&'a RadioQAndA<'a>), | 20 RadioPrompt(&'a RadioQAndA<'a>), |
23 BinaryPrompt(&'a BinaryQAndA<'a>), | 21 BinaryPrompt(&'a BinaryQAndA<'a>), |
24 } | 22 } |
25 | 23 |
26 impl Message<'_> { | 24 impl Exchange<'_> { |
27 /// Sets an error answer on this question, without having to inspect it. | 25 /// Sets an error answer on this question, without having to inspect it. |
28 /// | 26 /// |
29 /// Use this as a default match case: | 27 /// Use this as a default match case: |
30 /// | 28 /// |
31 /// ``` | 29 /// ``` |
32 /// use nonstick::conv::{Message, QAndA}; | 30 /// use nonstick::conv::{Exchange, QAndA}; |
33 /// use nonstick::ErrorCode; | 31 /// use nonstick::ErrorCode; |
34 /// | 32 /// |
35 /// fn cant_respond(message: Message) { | 33 /// fn cant_respond(message: Exchange) { |
36 /// match message { | 34 /// match message { |
37 /// Message::Info(i) => { | 35 /// Exchange::Info(i) => { |
38 /// eprintln!("fyi, {}", i.question()); | 36 /// eprintln!("fyi, {}", i.question()); |
39 /// i.set_answer(Ok(())) | 37 /// i.set_answer(Ok(())) |
40 /// } | 38 /// } |
41 /// Message::Error(e) => { | 39 /// Exchange::Error(e) => { |
42 /// eprintln!("ERROR: {}", e.question()); | 40 /// eprintln!("ERROR: {}", e.question()); |
43 /// e.set_answer(Ok(())) | 41 /// e.set_answer(Ok(())) |
44 /// } | 42 /// } |
45 /// // We can't answer any questions. | 43 /// // We can't answer any questions. |
46 /// other => other.set_error(ErrorCode::ConversationError), | 44 /// other => other.set_error(ErrorCode::ConversationError), |
47 /// } | 45 /// } |
48 /// } | 46 /// } |
49 pub fn set_error(&self, err: ErrorCode) { | 47 pub fn set_error(&self, err: ErrorCode) { |
50 match *self { | 48 match *self { |
51 Message::Prompt(m) => m.set_answer(Err(err)), | 49 Exchange::Prompt(m) => m.set_answer(Err(err)), |
52 Message::MaskedPrompt(m) => m.set_answer(Err(err)), | 50 Exchange::MaskedPrompt(m) => m.set_answer(Err(err)), |
53 Message::Error(m) => m.set_answer(Err(err)), | 51 Exchange::Error(m) => m.set_answer(Err(err)), |
54 Message::Info(m) => m.set_answer(Err(err)), | 52 Exchange::Info(m) => m.set_answer(Err(err)), |
55 Message::RadioPrompt(m) => m.set_answer(Err(err)), | 53 Exchange::RadioPrompt(m) => m.set_answer(Err(err)), |
56 Message::BinaryPrompt(m) => m.set_answer(Err(err)), | 54 Exchange::BinaryPrompt(m) => m.set_answer(Err(err)), |
57 } | 55 } |
58 } | 56 } |
59 } | 57 } |
60 | 58 |
61 macro_rules! q_and_a { | 59 macro_rules! q_and_a { |
74 q: question, | 72 q: question, |
75 a: Cell::new(Err(ErrorCode::ConversationError)), | 73 a: Cell::new(Err(ErrorCode::ConversationError)), |
76 } | 74 } |
77 } | 75 } |
78 | 76 |
79 /// Converts this Q&A into a [`Message`] for the [`Conversation`]. | 77 /// Converts this Q&A into a [`Exchange`] for the [`Conversation`]. |
80 pub fn message(&self) -> Message<'_> { | 78 pub fn exchange(&self) -> Exchange<'_> { |
81 $val(self) | 79 $val(self) |
82 } | 80 } |
83 | 81 |
84 /// The contents of the question being asked. | 82 /// The contents of the question being asked. |
85 /// | 83 /// |
108 // shout out to stackoverflow user ballpointben for this lazy impl: | 106 // shout out to stackoverflow user ballpointben for this lazy impl: |
109 // https://stackoverflow.com/a/78871280/39808 | 107 // https://stackoverflow.com/a/78871280/39808 |
110 $(#[$m])* | 108 $(#[$m])* |
111 impl fmt::Debug for $name<'_> { | 109 impl fmt::Debug for $name<'_> { |
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { | 110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> { |
113 #[derive(Debug)] | 111 f.debug_struct(stringify!($name)).field("q", &self.q).finish_non_exhaustive() |
114 struct $name<'a> { q: $qt } | |
115 fmt::Debug::fmt(&$name { q: self.q }, f) | |
116 } | 112 } |
117 } | 113 } |
118 }; | 114 }; |
119 } | 115 } |
120 | 116 |
121 q_and_a!( | 117 q_and_a!( |
122 /// A Q&A that asks the user for text and does not show it while typing. | 118 /// A Q&A that asks the user for text and does not show it while typing. |
123 /// | 119 /// |
124 /// In other words, a password entry prompt. | 120 /// In other words, a password entry prompt. |
125 MaskedQAndA<'a, Q=&'a str, A=String>, | 121 MaskedQAndA<'a, Q=&'a str, A=String>, |
126 Message::MaskedPrompt | 122 Exchange::MaskedPrompt |
127 ); | 123 ); |
128 | 124 |
129 q_and_a!( | 125 q_and_a!( |
130 /// A standard Q&A prompt that asks the user for text. | 126 /// A standard Q&A prompt that asks the user for text. |
131 /// | 127 /// |
132 /// This is the normal "ask a person a question" prompt. | 128 /// This is the normal "ask a person a question" prompt. |
133 /// When the user types, their input will be shown to them. | 129 /// When the user types, their input will be shown to them. |
134 /// It can be used for things like usernames. | 130 /// It can be used for things like usernames. |
135 QAndA<'a, Q=&'a str, A=String>, | 131 QAndA<'a, Q=&'a str, A=String>, |
136 Message::Prompt | 132 Exchange::Prompt |
137 ); | 133 ); |
138 | 134 |
139 q_and_a!( | 135 q_and_a!( |
140 /// A Q&A for "radio button"–style data. (Linux-PAM extension) | 136 /// A Q&A for "radio button"–style data. (Linux-PAM extension) |
141 /// | 137 /// |
142 /// This message type is theoretically useful for "yes/no/maybe" | 138 /// This message type is theoretically useful for "yes/no/maybe" |
143 /// questions, but nowhere in the documentation is it specified | 139 /// questions, but nowhere in the documentation is it specified |
144 /// what the format of the answer will be, or how this should be shown. | 140 /// what the format of the answer will be, or how this should be shown. |
145 RadioQAndA<'a, Q=&'a str, A=String>, | 141 RadioQAndA<'a, Q=&'a str, A=String>, |
146 Message::RadioPrompt | 142 Exchange::RadioPrompt |
147 ); | 143 ); |
148 | 144 |
149 q_and_a!( | 145 q_and_a!( |
150 /// Asks for binary data. (Linux-PAM extension) | 146 /// Asks for binary data. (Linux-PAM extension) |
151 /// | 147 /// |
154 /// or to enable things like security keys. | 150 /// or to enable things like security keys. |
155 /// | 151 /// |
156 /// The `data_type` tag is a value that is simply passed through | 152 /// The `data_type` tag is a value that is simply passed through |
157 /// to the application. PAM does not define any meaning for it. | 153 /// to the application. PAM does not define any meaning for it. |
158 BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>, | 154 BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>, |
159 Message::BinaryPrompt | 155 Exchange::BinaryPrompt |
160 ); | 156 ); |
161 | 157 |
162 /// Owned binary data. | 158 /// Owned binary data. |
163 #[derive(Debug, Default, PartialEq)] | 159 #[derive(Debug, Default, PartialEq)] |
164 pub struct BinaryData { | 160 pub struct BinaryData { |
206 /// | 202 /// |
207 /// While this does not have an answer, [`Conversation`] implementations | 203 /// While this does not have an answer, [`Conversation`] implementations |
208 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that | 204 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that |
209 /// the message has been displayed (or actively discarded). | 205 /// the message has been displayed (or actively discarded). |
210 InfoMsg<'a, Q = &'a str, A = ()>, | 206 InfoMsg<'a, Q = &'a str, A = ()>, |
211 Message::Info | 207 Exchange::Info |
212 ); | 208 ); |
213 | 209 |
214 q_and_a!( | 210 q_and_a!( |
215 /// An error message to be passed to the user. | 211 /// An error message to be passed to the user. |
216 /// | 212 /// |
217 /// While this does not have an answer, [`Conversation`] implementations | 213 /// While this does not have an answer, [`Conversation`] implementations |
218 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that | 214 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that |
219 /// the message has been displayed (or actively discarded). | 215 /// the message has been displayed (or actively discarded). |
220 ErrorMsg<'a, Q = &'a str, A = ()>, | 216 ErrorMsg<'a, Q = &'a str, A = ()>, |
221 Message::Error | 217 Exchange::Error |
222 ); | 218 ); |
223 | 219 |
224 /// A channel for PAM modules to request information from the user. | 220 /// A channel for PAM modules to request information from the user. |
225 /// | 221 /// |
226 /// This trait is used by both applications and PAM modules: | 222 /// This trait is used by both applications and PAM modules: |
234 /// | 230 /// |
235 /// The returned Vec of messages always contains exactly as many entries | 231 /// The returned Vec of messages always contains exactly as many entries |
236 /// as there were messages in the request; one corresponding to each. | 232 /// as there were messages in the request; one corresponding to each. |
237 /// | 233 /// |
238 /// TODO: write detailed documentation about how to use this. | 234 /// TODO: write detailed documentation about how to use this. |
239 fn communicate(&self, messages: &[Message]); | 235 fn communicate(&self, messages: &[Exchange]); |
240 } | 236 } |
241 | 237 |
242 /// Turns a simple function into a [`Conversation`]. | 238 /// Turns a simple function into a [`Conversation`]. |
243 /// | 239 /// |
244 /// This can be used to wrap a free-floating function for use as a | 240 /// This can be used to wrap a free-floating function for use as a |
245 /// Conversation: | 241 /// Conversation: |
246 /// | 242 /// |
247 /// ``` | 243 /// ``` |
248 /// use nonstick::conv::{conversation_func, Conversation, Message}; | 244 /// use nonstick::conv::{conversation_func, Conversation, Exchange}; |
249 /// mod some_library { | 245 /// mod some_library { |
250 /// # use nonstick::Conversation; | 246 /// # use nonstick::Conversation; |
251 /// pub fn get_auth_data(conv: &mut impl Conversation) { | 247 /// pub fn get_auth_data(conv: &mut impl Conversation) { |
252 /// /* ... */ | 248 /// /* ... */ |
253 /// } | 249 /// } |
254 /// } | 250 /// } |
255 /// | 251 /// |
256 /// fn my_terminal_prompt(messages: &[Message]) { | 252 /// fn my_terminal_prompt(messages: &[Exchange]) { |
257 /// // ... | 253 /// // ... |
258 /// # unimplemented!() | 254 /// # unimplemented!() |
259 /// } | 255 /// } |
260 /// | 256 /// |
261 /// fn main() { | 257 /// fn main() { |
262 /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); | 258 /// some_library::get_auth_data(&mut conversation_func(my_terminal_prompt)); |
263 /// } | 259 /// } |
264 /// ``` | 260 /// ``` |
265 pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation { | 261 pub fn conversation_func(func: impl Fn(&[Exchange])) -> impl Conversation { |
266 FunctionConvo(func) | 262 FunctionConvo(func) |
267 } | 263 } |
268 | 264 |
269 struct FunctionConvo<C: Fn(&[Message])>(C); | 265 struct FunctionConvo<C: Fn(&[Exchange])>(C); |
270 | 266 |
271 impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> { | 267 impl<C: Fn(&[Exchange])> Conversation for FunctionConvo<C> { |
272 fn communicate(&self, messages: &[Message]) { | 268 fn communicate(&self, messages: &[Exchange]) { |
273 self.0(messages) | 269 self.0(messages) |
274 } | 270 } |
275 } | 271 } |
276 | 272 |
277 /// A Conversation | 273 /// A Conversation |
397 macro_rules! conv_fn { | 393 macro_rules! conv_fn { |
398 ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { | 394 ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { |
399 $(#[$m])* | 395 $(#[$m])* |
400 fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { | 396 fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { |
401 let prompt = <$msg>::new($($param),*); | 397 let prompt = <$msg>::new($($param),*); |
402 self.communicate(&[prompt.message()]); | 398 self.communicate(&[prompt.exchange()]); |
403 prompt.answer() | 399 prompt.answer() |
404 } | 400 } |
405 }; | 401 }; |
406 ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { | 402 ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { |
407 $(#[$m])* | 403 $(#[$m])* |
408 fn $fn_name(&self, $($param: $pt),*) { | 404 fn $fn_name(&self, $($param: $pt),*) { |
409 self.communicate(&[<$msg>::new($($param),*).message()]); | 405 self.communicate(&[<$msg>::new($($param),*).exchange()]); |
410 } | 406 } |
411 }; | 407 }; |
412 } | 408 } |
413 | 409 |
414 impl<C: Conversation> ConversationAdapter for C { | 410 impl<C: Conversation> ConversationAdapter for C { |
431 self.0 | 427 self.0 |
432 } | 428 } |
433 } | 429 } |
434 | 430 |
435 impl<CA: ConversationAdapter> Conversation for Demux<CA> { | 431 impl<CA: ConversationAdapter> Conversation for Demux<CA> { |
436 fn communicate(&self, messages: &[Message]) { | 432 fn communicate(&self, messages: &[Exchange]) { |
437 for msg in messages { | 433 for msg in messages { |
438 match msg { | 434 match msg { |
439 Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), | 435 Exchange::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())), |
440 Message::MaskedPrompt(prompt) => { | 436 Exchange::MaskedPrompt(prompt) => { |
441 prompt.set_answer(self.0.masked_prompt(prompt.question())) | 437 prompt.set_answer(self.0.masked_prompt(prompt.question())) |
442 } | 438 } |
443 Message::RadioPrompt(prompt) => { | 439 Exchange::RadioPrompt(prompt) => { |
444 prompt.set_answer(self.0.radio_prompt(prompt.question())) | 440 prompt.set_answer(self.0.radio_prompt(prompt.question())) |
445 } | 441 } |
446 Message::Info(prompt) => { | 442 Exchange::Info(prompt) => { |
447 self.0.info_msg(prompt.question()); | 443 self.0.info_msg(prompt.question()); |
448 prompt.set_answer(Ok(())) | 444 prompt.set_answer(Ok(())) |
449 } | 445 } |
450 Message::Error(prompt) => { | 446 Exchange::Error(prompt) => { |
451 self.0.error_msg(prompt.question()); | 447 self.0.error_msg(prompt.question()); |
452 prompt.set_answer(Ok(())) | 448 prompt.set_answer(Ok(())) |
453 } | 449 } |
454 Message::BinaryPrompt(prompt) => { | 450 Exchange::BinaryPrompt(prompt) => { |
455 let q = prompt.question(); | 451 let q = prompt.question(); |
456 prompt.set_answer(self.0.binary_prompt(q)) | 452 prompt.set_answer(self.0.binary_prompt(q)) |
457 } | 453 } |
458 } | 454 } |
459 } | 455 } |
514 let conv = tester.into_conversation(); | 510 let conv = tester.into_conversation(); |
515 | 511 |
516 // Basic tests. | 512 // Basic tests. |
517 | 513 |
518 conv.communicate(&[ | 514 conv.communicate(&[ |
519 what.message(), | 515 what.exchange(), |
520 pass.message(), | 516 pass.exchange(), |
521 err.message(), | 517 err.exchange(), |
522 info.message(), | 518 info.exchange(), |
523 has_err.message(), | 519 has_err.exchange(), |
524 ]); | 520 ]); |
525 | 521 |
526 assert_eq!("whatwhat", what.answer().unwrap()); | 522 assert_eq!("whatwhat", what.answer().unwrap()); |
527 assert_eq!("my secrets", pass.answer().unwrap()); | 523 assert_eq!("my secrets", pass.answer().unwrap()); |
528 assert_eq!(Ok(()), err.answer()); | 524 assert_eq!(Ok(()), err.answer()); |
536 { | 532 { |
537 let conv = tester.into_conversation(); | 533 let conv = tester.into_conversation(); |
538 | 534 |
539 let radio = RadioQAndA::new("channel?"); | 535 let radio = RadioQAndA::new("channel?"); |
540 let bin = BinaryQAndA::new((&[10, 9, 8], 66)); | 536 let bin = BinaryQAndA::new((&[10, 9, 8], 66)); |
541 conv.communicate(&[radio.message(), bin.message()]); | 537 conv.communicate(&[radio.exchange(), bin.exchange()]); |
542 | 538 |
543 assert_eq!("zero", radio.answer().unwrap()); | 539 assert_eq!("zero", radio.answer().unwrap()); |
544 assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap()); | 540 assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap()); |
545 } | 541 } |
546 } | 542 } |
547 | 543 |
548 fn test_mux() { | 544 fn test_mux() { |
549 struct MuxTester; | 545 struct MuxTester; |
550 | 546 |
551 impl Conversation for MuxTester { | 547 impl Conversation for MuxTester { |
552 fn communicate(&self, messages: &[Message]) { | 548 fn communicate(&self, messages: &[Exchange]) { |
553 if let [msg] = messages { | 549 if let [msg] = messages { |
554 match *msg { | 550 match *msg { |
555 Message::Info(info) => { | 551 Exchange::Info(info) => { |
556 assert_eq!("let me tell you", info.question()); | 552 assert_eq!("let me tell you", info.question()); |
557 info.set_answer(Ok(())) | 553 info.set_answer(Ok(())) |
558 } | 554 } |
559 Message::Error(error) => { | 555 Exchange::Error(error) => { |
560 assert_eq!("oh no", error.question()); | 556 assert_eq!("oh no", error.question()); |
561 error.set_answer(Ok(())) | 557 error.set_answer(Ok(())) |
562 } | 558 } |
563 Message::Prompt(prompt) => prompt.set_answer(match prompt.question() { | 559 Exchange::Prompt(prompt) => prompt.set_answer(match prompt.question() { |
564 "should_err" => Err(ErrorCode::PermissionDenied), | 560 "should_err" => Err(ErrorCode::PermissionDenied), |
565 "question" => Ok("answer".to_owned()), | 561 "question" => Ok("answer".to_owned()), |
566 other => panic!("unexpected question {other:?}"), | 562 other => panic!("unexpected question {other:?}"), |
567 }), | 563 }), |
568 Message::MaskedPrompt(ask) => { | 564 Exchange::MaskedPrompt(ask) => { |
569 assert_eq!("password!", ask.question()); | 565 assert_eq!("password!", ask.question()); |
570 ask.set_answer(Ok("open sesame".into())) | 566 ask.set_answer(Ok("open sesame".into())) |
571 } | 567 } |
572 Message::BinaryPrompt(prompt) => { | 568 Exchange::BinaryPrompt(prompt) => { |
573 assert_eq!((&[1, 2, 3][..], 69), prompt.question()); | 569 assert_eq!((&[1, 2, 3][..], 69), prompt.question()); |
574 prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42)))) | 570 prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42)))) |
575 } | 571 } |
576 Message::RadioPrompt(ask) => { | 572 Exchange::RadioPrompt(ask) => { |
577 assert_eq!("radio?", ask.question()); | 573 assert_eq!("radio?", ask.question()); |
578 ask.set_answer(Ok("yes".to_owned())) | 574 ask.set_answer(Ok("yes".to_owned())) |
579 } | 575 } |
580 } | 576 } |
581 } else { | 577 } else { |