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 {