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 | ebb71a412b58 |
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 { |
