diff src/conv.rs @ 77:351bdc13005e

Update the libpam module to work with the new structure.
author Paul Fisher <paul@pfish.zone>
date Sun, 08 Jun 2025 01:03:46 -0400
parents e58d24849e82
children 002adfb98c5c
line wrap: on
line diff
--- a/src/conv.rs	Sat Jun 07 18:55:27 2025 -0400
+++ b/src/conv.rs	Sun Jun 08 01:03:46 2025 -0400
@@ -14,12 +14,12 @@
 /// that will be presented to the user.
 #[non_exhaustive]
 pub enum Message<'a> {
-    MaskedPrompt(&'a MaskedPrompt<'a>),
-    Prompt(&'a Prompt<'a>),
-    RadioPrompt(&'a RadioPrompt<'a>),
-    BinaryPrompt(&'a BinaryPrompt<'a>),
-    InfoMsg(&'a InfoMsg<'a>),
-    ErrorMsg(&'a ErrorMsg<'a>),
+    Prompt(&'a QAndA<'a>),
+    MaskedPrompt(&'a MaskedQAndA<'a>),
+    RadioPrompt(&'a RadioQAndA<'a>),
+    BinaryPrompt(&'a BinaryQAndA<'a>),
+    Error(&'a ErrorMsg<'a>),
+    Info(&'a InfoMsg<'a>),
 }
 
 impl Message<'_> {
@@ -33,11 +33,11 @@
     ///
     /// fn cant_respond(message: Message) {
     ///     match message {
-    ///         Message::InfoMsg(i) => {
+    ///         Message::Info(i) => {
     ///             eprintln!("fyi, {}", i.question());
     ///             i.set_answer(Ok(()))
     ///         }
-    ///         Message::ErrorMsg(e) => {
+    ///         Message::Error(e) => {
     ///             eprintln!("ERROR: {}", e.question());
     ///             e.set_answer(Ok(()))
     ///         }
@@ -46,55 +46,19 @@
     ///     }
     /// }
     pub fn set_error(&self, err: ErrorCode) {
-        match self {
+        match *self {
+            Message::Prompt(m) => m.set_answer(Err(err)),
             Message::MaskedPrompt(m) => m.set_answer(Err(err)),
-            Message::Prompt(m) => m.set_answer(Err(err)),
             Message::RadioPrompt(m) => m.set_answer(Err(err)),
             Message::BinaryPrompt(m) => m.set_answer(Err(err)),
-            Message::InfoMsg(m) => m.set_answer(Err(err)),
-            Message::ErrorMsg(m) => m.set_answer(Err(err)),
+            Message::Error(m) => m.set_answer(Err(err)),
+            Message::Info(m) => m.set_answer(Err(err)),
         }
     }
 }
 
-/// A question-and-answer pair that can be communicated in a [`Conversation`].
-///
-/// The asking side creates a `QAndA`, then converts it to a [`Message`]
-/// and sends it via a [`Conversation`]. The Conversation then retrieves
-/// the answer to the question (if needed) and sets the response.
-/// Once control returns to the asker, the asker gets the answer from this
-/// `QAndA` and uses it however it wants.
-///
-/// For a more detailed explanation of how this works,
-/// see [`Conversation::communicate`].
-pub trait QAndA<'a> {
-    /// The type of the content of the question.
-    type Question: Copy;
-    /// The type of the answer to the question.
-    type Answer;
-
-    /// Converts this Q-and-A pair into a [`Message`] for the [`Conversation`].
-    fn message(&self) -> Message;
-
-    /// The contents of the question being asked.
-    ///
-    /// For instance, this might say `"Username:"` to prompt the user
-    /// for their name.
-    fn question(&self) -> Self::Question;
-
-    /// Sets the answer to the question.
-    ///
-    /// The [`Conversation`] implementation calls this to set the answer.
-    /// The conversation should *always call this function*, even for messages
-    /// that don't have "an answer" (like error or info messages).
-    fn set_answer(&self, answer: Result<Self::Answer>);
-
-    /// Gets the answer to the question.
-    fn answer(self) -> Result<Self::Answer>;
-}
-
 macro_rules! q_and_a {
-    ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $($doc:literal)*) => {
+    ($name:ident<'a, Q=$qt:ty, A=$at:ty>, $val:path, $($doc:literal)*) => {
         $(
             #[doc = $doc]
         )*
@@ -103,25 +67,34 @@
             a: Cell<Result<$at>>,
         }
 
-        impl<'a> QAndA<'a> for $name<'a> {
-            type Question = $qt;
-            type Answer = $at;
+        impl<'a> $name<'a> {
+            /// Converts this Q&A into a [`Message`] for the [`Conversation`].
+            pub fn message(&self) -> Message {
+                $val(self)
+            }
 
-            fn question(&self) -> Self::Question {
+            /// The contents of the question being asked.
+            ///
+            /// For instance, this might say `"Username:"` to prompt the user
+            /// for their name, or the text of an error message.
+            pub fn question(&self) -> $qt {
                 self.q
             }
 
-            fn set_answer(&self, answer: Result<Self::Answer>) {
+            /// Sets the answer to the question.
+            ///
+            /// The [`Conversation`] implementation calls this to set the answer.
+            /// The conversation should *always call this function*,
+            /// even for Q&A messages that don't have "an answer"
+            /// (like error or info messages).
+            pub fn set_answer(&self, answer: Result<$at>) {
                 self.a.set(answer)
             }
 
-            fn answer(self) -> Result<Self::Answer> {
+            /// Gets the answer to the question.
+            pub fn answer(self) -> Result<$at> {
                 self.a.into_inner()
             }
-
-            fn message(&self) -> Message {
-                Message::$name(self)
-            }
         }
     };
 }
@@ -130,7 +103,7 @@
     ($t:ident) => {
         impl<'a> $t<'a> {
             #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
-            fn ask(question: &'a str) -> Self {
+            pub fn new(question: &'a str) -> Self {
                 Self {
                     q: question,
                     a: Cell::new(Err(ErrorCode::ConversationError)),
@@ -141,35 +114,39 @@
 }
 
 q_and_a!(
-    MaskedPrompt<'a, Q=&'a str, A=SecureString>,
-    "Asks the user for data and does not echo it back while being entered."
+    MaskedQAndA<'a, Q=&'a str, A=SecureString>,
+    Message::MaskedPrompt,
+    "A Q&A that asks the user for text and does not show it while typing."
     ""
     "In other words, a password entry prompt."
 );
-ask!(MaskedPrompt);
+ask!(MaskedQAndA);
 
 q_and_a!(
-    Prompt<'a, Q=&'a str, A=String>,
-    "Asks the user for data."
+    QAndA<'a, Q=&'a str, A=String>,
+    Message::Prompt,
+    "A standard Q&A prompt that asks the user for text."
     ""
     "This is the normal \"ask a person a question\" prompt."
     "When the user types, their input will be shown to them."
     "It can be used for things like usernames."
 );
-ask!(Prompt);
+ask!(QAndA);
 
 q_and_a!(
-    RadioPrompt<'a, Q=&'a str, A=String>,
-    "Asks the user for \"radio button\"–style data. (Linux-PAM extension)"
+    RadioQAndA<'a, Q=&'a str, A=String>,
+    Message::RadioPrompt,
+    "A Q&A for \"radio button\"–style data. (Linux-PAM extension)"
     ""
     "This message type is theoretically useful for \"yes/no/maybe\""
     "questions, but nowhere in the documentation is it specified"
     "what the format of the answer will be, or how this should be shown."
 );
-ask!(RadioPrompt);
+ask!(RadioQAndA);
 
 q_and_a!(
-    BinaryPrompt<'a, Q=BinaryQuestion<'a>, A=BinaryData>,
+    BinaryQAndA<'a, Q=BorrowedBinaryData<'a>, A=BinaryData>,
+    Message::BinaryPrompt,
     "Asks for binary data. (Linux-PAM extension)"
     ""
     "This sends a binary message to the client application."
@@ -179,29 +156,44 @@
     "The `data_type` tag is a value that is simply passed through"
     "to the application. PAM does not define any meaning for it."
 );
-impl<'a> BinaryPrompt<'a> {
+impl<'a> BinaryQAndA<'a> {
     /// Creates a prompt for the given binary data.
     ///
     /// The `data_type` is a tag you can use for communication between
     /// the module and the application. Its meaning is undefined by PAM.
-    fn ask(data: &'a [u8], data_type: u8) -> Self {
+    pub fn new(data: &'a [u8], data_type: u8) -> Self {
+        BorrowedBinaryData { data, data_type }.into()
+    }
+}
+
+impl<'a> From<BorrowedBinaryData<'a>> for BinaryQAndA<'a> {
+    fn from(q: BorrowedBinaryData<'a>) -> Self {
         Self {
-            q: BinaryQuestion { data, data_type },
+            q,
             a: Cell::new(Err(ErrorCode::ConversationError)),
         }
     }
 }
 
-/// The contents of a question requesting binary data.
-///
-/// A borrowed version of [`BinaryData`].
-#[derive(Copy, Clone, Debug)]
-pub struct BinaryQuestion<'a> {
+impl<'a> From<&'a BinaryData> for BinaryQAndA<'a> {
+    fn from(src: &'a BinaryData) -> Self {
+        BorrowedBinaryData::from(src).into()
+    }
+}
+
+/// A version of [`BinaryData`] where the `data` is borrowed.
+#[derive(Copy, Clone, Debug, Default)]
+pub struct BorrowedBinaryData<'a> {
     data: &'a [u8],
     data_type: u8,
 }
 
-impl BinaryQuestion<'_> {
+impl<'a> BorrowedBinaryData<'a> {
+    /// Creates a new BinaryQuestion as a view over the given data.
+    pub fn new(data: &'a [u8], data_type: u8) -> Self {
+        Self { data, data_type }
+    }
+
     /// Gets the data of this question.
     pub fn data(&self) -> &[u8] {
         self.data
@@ -213,11 +205,17 @@
     }
 }
 
+impl<'a> From<&'a BinaryData> for BorrowedBinaryData<'a> {
+    fn from(value: &'a BinaryData) -> Self {
+        Self::new(&value.data, value.data_type)
+    }
+}
+
 /// Owned binary data.
 ///
-/// For borrowed data, see [`BinaryQuestion`].
+/// For borrowed data, see [`BorrowedBinaryData`].
 /// You can take ownership of the stored data with `.into::<Vec<u8>>()`.
-#[derive(Debug, PartialEq)]
+#[derive(Debug, Default, PartialEq)]
 pub struct BinaryData {
     data: Vec<u8>,
     data_type: u8,
@@ -238,6 +236,15 @@
     }
 }
 
+impl<'a> From<BorrowedBinaryData<'a>> for BinaryData {
+    fn from(value: BorrowedBinaryData) -> Self {
+        Self {
+            data: value.data.to_vec(),
+            data_type: value.data_type,
+        }
+    }
+}
+
 impl From<BinaryData> for Vec<u8> {
     /// Takes ownership of the data stored herein.
     fn from(value: BinaryData) -> Self {
@@ -247,6 +254,7 @@
 
 q_and_a!(
     InfoMsg<'a, Q = &'a str, A = ()>,
+    Message::Info,
     "A message containing information to be passed to the user."
     ""
     "While this does not have an answer, [`Conversation`] implementations"
@@ -255,7 +263,7 @@
 );
 impl<'a> InfoMsg<'a> {
     /// Creates an informational message to send to the user.
-    fn new(message: &'a str) -> Self {
+    pub fn new(message: &'a str) -> Self {
         Self {
             q: message,
             a: Cell::new(Err(ErrorCode::ConversationError)),
@@ -265,6 +273,7 @@
 
 q_and_a!(
     ErrorMsg<'a, Q = &'a str, A = ()>,
+    Message::Error,
     "An error message to be passed to the user."
     ""
     "While this does not have an answer, [`Conversation`] implementations"
@@ -274,7 +283,7 @@
 );
 impl<'a> ErrorMsg<'a> {
     /// Creates an error message to send to the user.
-    fn new(message: &'a str) -> Self {
+    pub fn new(message: &'a str) -> Self {
         Self {
             q: message,
             a: Cell::new(Err(ErrorCode::ConversationError)),
@@ -427,27 +436,27 @@
 }
 
 macro_rules! conv_fn {
-    ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $ask:path }) => {
+    ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
         fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
-            let prompt = $ask($($param),*);
+            let prompt = <$msg>::new($($param),*);
             self.communicate(&[prompt.message()]);
             prompt.answer()
         }
     };
-    ($fn_name:ident($($param:ident: $pt:ty),+) { $ask:path }) => {
+    ($fn_name:ident($($param:ident: $pt:ty),+) { $msg:ty }) => {
         fn $fn_name(&mut self, $($param: $pt),*) {
-            self.communicate(&[$ask($($param),*).message()]);
+            self.communicate(&[<$msg>::new($($param),*).message()]);
         }
     };
 }
 
 impl<C: Conversation> SimpleConversation for C {
-    conv_fn!(prompt(message: &str) -> String { Prompt::ask });
-    conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedPrompt::ask });
-    conv_fn!(radio_prompt(message: &str) -> String { RadioPrompt::ask });
-    conv_fn!(error_msg(message: &str) { ErrorMsg::new });
-    conv_fn!(info_msg(message: &str) { InfoMsg::new });
-    conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryPrompt::ask });
+    conv_fn!(prompt(message: &str) -> String { QAndA });
+    conv_fn!(masked_prompt(message: &str) -> SecureString { MaskedQAndA } );
+    conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA });
+    conv_fn!(error_msg(message: &str) { ErrorMsg });
+    conv_fn!(info_msg(message: &str) { InfoMsg });
+    conv_fn!(binary_prompt(data: &[u8], data_type: u8) -> BinaryData { BinaryQAndA });
 }
 
 /// A [`Conversation`] which asks the questions one at a time.
@@ -466,11 +475,11 @@
                 Message::RadioPrompt(prompt) => {
                     prompt.set_answer(self.0.radio_prompt(prompt.question()))
                 }
-                Message::InfoMsg(prompt) => {
+                Message::Info(prompt) => {
                     self.0.info_msg(prompt.question());
                     prompt.set_answer(Ok(()))
                 }
-                Message::ErrorMsg(prompt) => {
+                Message::Error(prompt) => {
                     self.0.error_msg(prompt.question());
                     prompt.set_answer(Ok(()))
                 }
@@ -486,8 +495,8 @@
 #[cfg(test)]
 mod tests {
     use super::{
-        BinaryPrompt, Conversation, ErrorMsg, InfoMsg, MaskedPrompt, Message, Prompt, QAndA,
-        RadioPrompt, Result, SecureString, SimpleConversation,
+        BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA,
+        RadioQAndA, Result, SecureString, SimpleConversation,
     };
     use crate::constants::ErrorCode;
     use crate::BinaryData;
@@ -533,11 +542,11 @@
 
         let mut tester = DemuxTester::default();
 
-        let what = Prompt::ask("what");
-        let pass = MaskedPrompt::ask("reveal");
+        let what = QAndA::new("what");
+        let pass = MaskedQAndA::new("reveal");
         let err = ErrorMsg::new("whoopsie");
         let info = InfoMsg::new("did you know");
-        let has_err = Prompt::ask("give_err");
+        let has_err = QAndA::new("give_err");
 
         let mut conv = tester.as_conversation();
 
@@ -563,8 +572,8 @@
 
         let mut conv = tester.as_conversation();
 
-        let radio = RadioPrompt::ask("channel?");
-        let bin = BinaryPrompt::ask(&[10, 9, 8], 66);
+        let radio = RadioQAndA::new("channel?");
+        let bin = BinaryQAndA::new(&[10, 9, 8], 66);
         conv.communicate(&[radio.message(), bin.message()]);
 
         assert_eq!("zero", radio.answer().unwrap());
@@ -578,11 +587,11 @@
             fn communicate(&mut self, messages: &[Message]) {
                 if let [msg] = messages {
                     match *msg {
-                        Message::InfoMsg(info) => {
+                        Message::Info(info) => {
                             assert_eq!("let me tell you", info.question());
                             info.set_answer(Ok(()))
                         }
-                        Message::ErrorMsg(error) => {
+                        Message::Error(error) => {
                             assert_eq!("oh no", error.question());
                             error.set_answer(Ok(()))
                         }