changeset 96:f3e260f9ddcb

Make conversation trait use immutable references. Since sending a conversation a message doesn't really "mutate" it, it shouldn't really be considered "mutable" for that purpose.
author Paul Fisher <paul@pfish.zone>
date Mon, 23 Jun 2025 14:26:34 -0400
parents 51c9d7e8261a
children efe2f5f8b5b2
files src/conv.rs src/lib.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/module.rs
diffstat 5 files changed, 78 insertions(+), 58 deletions(-) [+]
line wrap: on
line diff
--- a/src/conv.rs	Mon Jun 23 14:03:44 2025 -0400
+++ b/src/conv.rs	Mon Jun 23 14:26:34 2025 -0400
@@ -236,7 +236,7 @@
     /// as there were messages in the request; one corresponding to each.
     ///
     /// TODO: write detailed documentation about how to use this.
-    fn communicate(&mut self, messages: &[Message]);
+    fn communicate(&self, messages: &[Message]);
 }
 
 /// Turns a simple function into a [`Conversation`].
@@ -262,102 +262,108 @@
 ///     some_library::get_auth_data(&mut conversation_func(my_terminal_prompt));
 /// }
 /// ```
-pub fn conversation_func(func: impl FnMut(&[Message])) -> impl Conversation {
-    Convo(func)
+pub fn conversation_func(func: impl Fn(&[Message])) -> impl Conversation {
+    FunctionConvo(func)
 }
 
-struct Convo<C: FnMut(&[Message])>(C);
+struct FunctionConvo<C: Fn(&[Message])>(C);
 
-impl<C: FnMut(&[Message])> Conversation for Convo<C> {
-    fn communicate(&mut self, messages: &[Message]) {
+impl<C: Fn(&[Message])> Conversation for FunctionConvo<C> {
+    fn communicate(&self, messages: &[Message]) {
         self.0(messages)
     }
 }
 
+/// A Conversation
+struct UsernamePasswordConvo {
+    username: String,
+    password: String,
+}
+
 /// A conversation trait for asking or answering one question at a time.
 ///
 /// An implementation of this is provided for any [`Conversation`],
 /// or a PAM application can implement this trait and handle messages
 /// one at a time.
 ///
-/// For example, to use a `Conversation` as a `SimpleConversation`:
+/// For example, to use a `Conversation` as a `ConversationAdapter`:
 ///
 /// ```
 /// # use nonstick::{Conversation, Result};
 /// // Bring this trait into scope to get `masked_prompt`, among others.
-/// use nonstick::SimpleConversation;
+/// use nonstick::ConversationAdapter;
 ///
-/// fn ask_for_token(convo: &mut impl Conversation) -> Result<String> {
+/// fn ask_for_token(convo: &impl Conversation) -> Result<String> {
 ///     convo.masked_prompt("enter your one-time token")
 /// }
 /// ```
 ///
-/// or to use a `SimpleConversation` as a `Conversation`:
+/// or to use a `ConversationAdapter` as a `Conversation`:
 ///
 /// ```
-/// use nonstick::{Conversation, SimpleConversation};
+/// use nonstick::{Conversation, ConversationAdapter};
 /// # use nonstick::{BinaryData, Result};
 /// mod some_library {
 /// #    use nonstick::Conversation;
-///     pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */
+///     pub fn get_auth_data(conv: &impl Conversation) { /* ... */
 ///     }
 /// }
 ///
 /// struct MySimpleConvo {/* ... */}
 /// # impl MySimpleConvo { fn new() -> Self { Self{} } }
 ///
-/// impl SimpleConversation for MySimpleConvo {
+/// impl ConversationAdapter for MySimpleConvo {
 ///     // ...
-/// # fn prompt(&mut self, request: &str) -> Result<String> {
+/// # fn prompt(&self, request: &str) -> Result<String> {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn masked_prompt(&mut self, request: &str) -> Result<String> {
+/// # fn masked_prompt(&self, request: &str) -> Result<String> {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn error_msg(&mut self, message: &str) {
+/// # fn error_msg(&self, message: &str) {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn info_msg(&mut self, message: &str) {
+/// # fn info_msg(&self, message: &str) {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn radio_prompt(&mut self, request: &str) -> Result<String> {
+/// # fn radio_prompt(&self, request: &str) -> Result<String> {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> {
+/// # fn binary_prompt(&self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> {
 /// #     unimplemented!()
 /// # }
 /// }
 ///
 /// fn main() {
 ///     let mut simple = MySimpleConvo::new();
-///     some_library::get_auth_data(&mut simple.as_conversation())
+///     some_library::get_auth_data(&mut simple.into_conversation())
 /// }
 /// ```
-pub trait SimpleConversation {
+pub trait ConversationAdapter {
     /// Lets you use this simple conversation as a full [Conversation].
     ///
     /// The wrapper takes each message received in [`Conversation::communicate`]
     /// and passes them one-by-one to the appropriate method,
     /// then collects responses to return.
-    fn as_conversation(&mut self) -> Demux<'_, Self>
+    fn into_conversation(self) -> Demux<Self>
     where
         Self: Sized,
     {
         Demux(self)
     }
     /// Prompts the user for something.
-    fn prompt(&mut self, request: &str) -> Result<String>;
+    fn prompt(&self, request: &str) -> Result<String>;
     /// Prompts the user for something, but hides what the user types.
-    fn masked_prompt(&mut self, request: &str) -> Result<String>;
+    fn masked_prompt(&self, request: &str) -> Result<String>;
     /// Alerts the user to an error.
-    fn error_msg(&mut self, message: &str);
+    fn error_msg(&self, message: &str);
     /// Sends an informational message to the user.
-    fn info_msg(&mut self, message: &str);
+    fn info_msg(&self, message: &str);
     /// \[Linux extension] Prompts the user for a yes/no/maybe conditional.
     ///
     /// PAM documentation doesn't define the format of the response.
@@ -366,7 +372,7 @@
     /// this will return [`ErrorCode::ConversationError`].
     /// If implemented on an implementation that doesn't support radio prompts,
     /// this will never be called.
-    fn radio_prompt(&mut self, request: &str) -> Result<String> {
+    fn radio_prompt(&self, request: &str) -> Result<String> {
         let _ = request;
         Err(ErrorCode::ConversationError)
     }
@@ -376,16 +382,22 @@
     /// this will return [`ErrorCode::ConversationError`].
     /// If implemented on an implementation that doesn't support radio prompts,
     /// this will never be called.
-    fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
+    fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
         let _ = data_and_type;
         Err(ErrorCode::ConversationError)
     }
 }
 
+impl<CA: ConversationAdapter> From<CA> for Demux<CA> {
+    fn from(value: CA) -> Self {
+        Demux(value)
+    }
+}
+
 macro_rules! conv_fn {
     ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
         $(#[$m])*
-        fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
+        fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> {
             let prompt = <$msg>::new($($param),*);
             self.communicate(&[prompt.message()]);
             prompt.answer()
@@ -393,13 +405,13 @@
     };
     ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => {
         $(#[$m])*
-        fn $fn_name(&mut self, $($param: $pt),*) {
+        fn $fn_name(&self, $($param: $pt),*) {
             self.communicate(&[<$msg>::new($($param),*).message()]);
         }
     };
 }
 
-impl<C: Conversation> SimpleConversation for C {
+impl<C: Conversation> ConversationAdapter for C {
     conv_fn!(prompt(message: &str) -> String { QAndA });
     conv_fn!(masked_prompt(message: &str) -> String { MaskedQAndA } );
     conv_fn!(error_msg(message: &str) { ErrorMsg });
@@ -410,11 +422,18 @@
 
 /// A [`Conversation`] which asks the questions one at a time.
 ///
-/// This is automatically created by [`SimpleConversation::as_conversation`].
-pub struct Demux<'a, SC: SimpleConversation>(&'a mut SC);
+/// This is automatically created by [`ConversationAdapter::into_conversation`].
+pub struct Demux<CA: ConversationAdapter>(CA);
 
-impl<SC: SimpleConversation> Conversation for Demux<'_, SC> {
-    fn communicate(&mut self, messages: &[Message]) {
+impl<CA: ConversationAdapter> Demux<CA> {
+    /// Gets the original Conversation out of this wrapper.
+    fn into_inner(self) -> CA {
+        self.0
+    }
+}
+
+impl<CA: ConversationAdapter> Conversation for Demux<CA> {
+    fn communicate(&self, messages: &[Message]) {
         for msg in messages {
             match msg {
                 Message::Prompt(prompt) => prompt.set_answer(self.0.prompt(prompt.question())),
@@ -450,41 +469,41 @@
     fn test_demux() {
         #[derive(Default)]
         struct DemuxTester {
-            error_ran: bool,
-            info_ran: bool,
+            error_ran: Cell<bool>,
+            info_ran: Cell<bool>,
         }
 
-        impl SimpleConversation for DemuxTester {
-            fn prompt(&mut self, request: &str) -> Result<String> {
+        impl ConversationAdapter for DemuxTester {
+            fn prompt(&self, request: &str) -> Result<String> {
                 match request {
                     "what" => Ok("whatwhat".to_owned()),
                     "give_err" => Err(ErrorCode::PermissionDenied),
                     _ => panic!("unexpected prompt!"),
                 }
             }
-            fn masked_prompt(&mut self, request: &str) -> Result<String> {
+            fn masked_prompt(&self, request: &str) -> Result<String> {
                 assert_eq!("reveal", request);
                 Ok("my secrets".to_owned())
             }
-            fn error_msg(&mut self, message: &str) {
-                self.error_ran = true;
+            fn error_msg(&self, message: &str) {
+                self.error_ran.set(true);
                 assert_eq!("whoopsie", message);
             }
-            fn info_msg(&mut self, message: &str) {
-                self.info_ran = true;
+            fn info_msg(&self, message: &str) {
+                self.info_ran.set(true);
                 assert_eq!("did you know", message);
             }
-            fn radio_prompt(&mut self, request: &str) -> Result<String> {
+            fn radio_prompt(&self, request: &str) -> Result<String> {
                 assert_eq!("channel?", request);
                 Ok("zero".to_owned())
             }
-            fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
+            fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
                 assert_eq!((&[10, 9, 8][..], 66), data_and_type);
                 Ok(BinaryData::new(vec![5, 5, 5], 5))
             }
         }
 
-        let mut tester = DemuxTester::default();
+        let tester = DemuxTester::default();
 
         let what = QAndA::new("what");
         let pass = MaskedQAndA::new("reveal");
@@ -492,7 +511,7 @@
         let info = InfoMsg::new("did you know");
         let has_err = QAndA::new("give_err");
 
-        let mut conv = tester.as_conversation();
+        let conv = tester.into_conversation();
 
         // Basic tests.
 
@@ -509,12 +528,13 @@
         assert_eq!(Ok(()), err.answer());
         assert_eq!(Ok(()), info.answer());
         assert_eq!(ErrorCode::PermissionDenied, has_err.answer().unwrap_err());
-        assert!(tester.error_ran);
-        assert!(tester.info_ran);
+        let tester = conv.into_inner();
+        assert!(tester.error_ran.get());
+        assert!(tester.info_ran.get());
 
         // Test the Linux extensions separately.
         {
-            let mut conv = tester.as_conversation();
+            let conv = tester.into_conversation();
 
             let radio = RadioQAndA::new("channel?");
             let bin = BinaryQAndA::new((&[10, 9, 8], 66));
@@ -529,7 +549,7 @@
         struct MuxTester;
 
         impl Conversation for MuxTester {
-            fn communicate(&mut self, messages: &[Message]) {
+            fn communicate(&self, messages: &[Message]) {
                 if let [msg] = messages {
                     match *msg {
                         Message::Info(info) => {
@@ -567,7 +587,7 @@
             }
         }
 
-        let mut tester = MuxTester;
+        let tester = MuxTester;
 
         assert_eq!("answer", tester.prompt("question").unwrap());
         assert_eq!("open sesame", tester.masked_prompt("password!").unwrap());
--- a/src/lib.rs	Mon Jun 23 14:03:44 2025 -0400
+++ b/src/lib.rs	Mon Jun 23 14:26:34 2025 -0400
@@ -40,7 +40,7 @@
 #[doc(inline)]
 pub use crate::{
     constants::{ErrorCode, Flags, Result},
-    conv::{BinaryData, Conversation, SimpleConversation},
+    conv::{BinaryData, Conversation, ConversationAdapter},
     handle::{PamHandleApplication, PamHandleModule, PamShared},
     module::PamModule,
 };
--- a/src/libpam/conversation.rs	Mon Jun 23 14:03:44 2025 -0400
+++ b/src/libpam/conversation.rs	Mon Jun 23 14:26:34 2025 -0400
@@ -63,7 +63,7 @@
 }
 
 impl Conversation for LibPamConversation<'_> {
-    fn communicate(&mut self, messages: &[Message]) {
+    fn communicate(&self, messages: &[Message]) {
         let internal = || {
             let questions = Questions::new(messages)?;
             let mut response_pointer = std::ptr::null_mut();
--- a/src/libpam/handle.rs	Mon Jun 23 14:03:44 2025 -0400
+++ b/src/libpam/handle.rs	Mon Jun 23 14:26:34 2025 -0400
@@ -127,7 +127,7 @@
 }
 
 impl Conversation for LibPamHandle {
-    fn communicate(&mut self, messages: &[Message]) {
+    fn communicate(&self, messages: &[Message]) {
         match self.conversation_item() {
             Ok(conv) => conv.communicate(messages),
             Err(e) => {
@@ -204,7 +204,7 @@
     }
 
     /// Gets the `PAM_CONV` item from the handle.
-    fn conversation_item(&mut self) -> Result<&mut LibPamConversation<'_>> {
+    fn conversation_item(&self) -> Result<&mut LibPamConversation<'_>> {
         let output: *mut LibPamConversation = ptr::null_mut();
         let result = unsafe {
             pam_ffi::pam_get_item(
--- a/src/libpam/module.rs	Mon Jun 23 14:03:44 2025 -0400
+++ b/src/libpam/module.rs	Mon Jun 23 14:26:34 2025 -0400
@@ -12,7 +12,7 @@
 /// ```no_run
 /// use nonstick::{
 ///     pam_hooks, Flags, OwnedLibPamHandle, PamHandleModule, PamModule, Result as PamResult,
-///     SimpleConversation,
+///     ConversationAdapter,
 /// };
 /// use std::ffi::CStr;
 /// # fn main() {}