diff src/conv.rs @ 143:ebb71a412b58

Turn everything into OsString and Just Walk Out! for strings with nul. To reduce the hazard surface of the API, this replaces most uses of &str with &OsStr (and likewise with String/OsString). Also, I've decided that instead of dealing with callers putting `\0` in their parameters, I'm going to follow the example of std::env and Just Walk Out! (i.e., panic!()). This makes things a lot less annoying for both me and (hopefully) users.
author Paul Fisher <paul@pfish.zone>
date Sat, 05 Jul 2025 22:12:46 -0400
parents 80c07e5ab22f
children 1bc52025156b
line wrap: on
line diff
--- a/src/conv.rs	Sat Jul 05 21:49:27 2025 -0400
+++ b/src/conv.rs	Sat Jul 05 22:12:46 2025 -0400
@@ -5,6 +5,7 @@
 
 use crate::constants::{ErrorCode, Result};
 use std::cell::Cell;
+use std::ffi::{OsStr, OsString};
 use std::fmt;
 use std::fmt::Debug;
 use std::result::Result as StdResult;
@@ -31,13 +32,15 @@
     /// use nonstick::ErrorCode;
     ///
     /// fn cant_respond(message: Exchange) {
+    ///     // "question" is kind of a bad name in the context of
+    ///     // a one-way message, but it's for consistency.
     ///     match message {
     ///         Exchange::Info(i) => {
-    ///             eprintln!("fyi, {}", i.question());
+    ///             eprintln!("fyi, {:?}", i.question());
     ///             i.set_answer(Ok(()))
     ///         }
     ///         Exchange::Error(e) => {
-    ///             eprintln!("ERROR: {}", e.question());
+    ///             eprintln!("ERROR: {:?}", e.question());
     ///             e.set_answer(Ok(()))
     ///         }
     ///         // We can't answer any questions.
@@ -118,7 +121,7 @@
     /// A Q&A that asks the user for text and does not show it while typing.
     ///
     /// In other words, a password entry prompt.
-    MaskedQAndA<'a, Q=&'a str, A=String>,
+    MaskedQAndA<'a, Q=&'a OsStr, A=OsString>,
     Exchange::MaskedPrompt
 );
 
@@ -128,7 +131,7 @@
     /// 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.
-    QAndA<'a, Q=&'a str, A=String>,
+    QAndA<'a, Q=&'a OsStr, A=OsString>,
     Exchange::Prompt
 );
 
@@ -138,7 +141,7 @@
     /// 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.
-    RadioQAndA<'a, Q=&'a str, A=String>,
+    RadioQAndA<'a, Q=&'a OsStr, A=OsString>,
     Exchange::RadioPrompt
 );
 
@@ -203,7 +206,7 @@
     /// While this does not have an answer, [`Conversation`] implementations
     /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
     /// the message has been displayed (or actively discarded).
-    InfoMsg<'a, Q = &'a str, A = ()>,
+    InfoMsg<'a, Q = &'a OsStr, A = ()>,
     Exchange::Info
 );
 
@@ -213,7 +216,7 @@
     /// While this does not have an answer, [`Conversation`] implementations
     /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
     /// the message has been displayed (or actively discarded).
-    ErrorMsg<'a, Q = &'a str, A = ()>,
+    ErrorMsg<'a, Q = &'a OsStr, A = ()>,
     Exchange::Error
 );
 
@@ -286,10 +289,11 @@
 ///
 /// ```
 /// # use nonstick::{Conversation, Result};
+/// # use std::ffi::OsString;
 /// // Bring this trait into scope to get `masked_prompt`, among others.
 /// use nonstick::ConversationAdapter;
 ///
-/// fn ask_for_token(convo: &impl Conversation) -> Result<String> {
+/// fn ask_for_token(convo: &impl Conversation) -> Result<OsString> {
 ///     convo.masked_prompt("enter your one-time token")
 /// }
 /// ```
@@ -299,6 +303,7 @@
 /// ```
 /// use nonstick::{Conversation, ConversationAdapter};
 /// # use nonstick::{BinaryData, Result};
+/// # use std::ffi::{OsStr, OsString};
 /// mod some_library {
 /// #    use nonstick::Conversation;
 ///     pub fn get_auth_data(conv: &impl Conversation) { /* ... */
@@ -310,23 +315,23 @@
 ///
 /// impl ConversationAdapter for MySimpleConvo {
 ///     // ...
-/// # fn prompt(&self, request: &str) -> Result<String> {
+/// # fn prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn masked_prompt(&self, request: &str) -> Result<String> {
+/// # fn masked_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn error_msg(&self, message: &str) {
+/// # fn error_msg(&self, message: impl AsRef<OsStr>) {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn info_msg(&self, message: &str) {
+/// # fn info_msg(&self, message: impl AsRef<OsStr>) {
 /// #     unimplemented!()
 /// # }
 /// #
-/// # fn radio_prompt(&self, request: &str) -> Result<String> {
+/// # fn radio_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
 /// #     unimplemented!()
 /// # }
 /// #
@@ -353,13 +358,13 @@
         Demux(self)
     }
     /// Prompts the user for something.
-    fn prompt(&self, request: &str) -> Result<String>;
+    fn prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString>;
     /// Prompts the user for something, but hides what the user types.
-    fn masked_prompt(&self, request: &str) -> Result<String>;
+    fn masked_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString>;
     /// Alerts the user to an error.
-    fn error_msg(&self, message: &str);
+    fn error_msg(&self, message: impl AsRef<OsStr>);
     /// Sends an informational message to the user.
-    fn info_msg(&self, message: &str);
+    fn info_msg(&self, message: impl AsRef<OsStr>);
     /// \[Linux extension] Prompts the user for a yes/no/maybe conditional.
     ///
     /// PAM documentation doesn't define the format of the response.
@@ -368,7 +373,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(&self, request: &str) -> Result<String> {
+    fn radio_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
         let _ = request;
         Err(ErrorCode::ConversationError)
     }
@@ -391,29 +396,33 @@
 }
 
 macro_rules! conv_fn {
-    ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
+    ($(#[$m:meta])* $fn_name:ident($param:tt: $pt:ty) -> $resp_type:ty { $msg:ty }) => {
         $(#[$m])*
-        fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> {
-            let prompt = <$msg>::new($($param),*);
+        fn $fn_name(&self, $param: impl AsRef<$pt>) -> Result<$resp_type> {
+            let prompt = <$msg>::new($param.as_ref());
             self.communicate(&[prompt.exchange()]);
             prompt.answer()
         }
     };
-    ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => {
+    ($(#[$m:meta])*$fn_name:ident($param:tt: $pt:ty) { $msg:ty }) => {
         $(#[$m])*
-        fn $fn_name(&self, $($param: $pt),*) {
-            self.communicate(&[<$msg>::new($($param),*).exchange()]);
+        fn $fn_name(&self, $param: impl AsRef<$pt>) {
+            self.communicate(&[<$msg>::new($param.as_ref()).exchange()]);
         }
     };
 }
 
 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 });
-    conv_fn!(info_msg(message: &str) { InfoMsg });
-    conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA });
-    conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA });
+    conv_fn!(prompt(message: OsStr) -> OsString { QAndA });
+    conv_fn!(masked_prompt(message: OsStr) -> OsString { MaskedQAndA } );
+    conv_fn!(error_msg(message: OsStr) { ErrorMsg });
+    conv_fn!(info_msg(message: OsStr) { InfoMsg });
+    conv_fn!(radio_prompt(message: OsStr) -> OsString { RadioQAndA });
+    fn binary_prompt(&self, (data, typ): (&[u8], u8)) -> Result<BinaryData> {
+        let prompt = BinaryQAndA::new((data, typ));
+        self.communicate(&[prompt.exchange()]);
+        prompt.answer()
+    }
 }
 
 /// A [`Conversation`] which asks the questions one at a time.
@@ -459,7 +468,6 @@
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::constants::ErrorCode;
 
     #[test]
     fn test_demux() {
@@ -470,28 +478,28 @@
         }
 
         impl ConversationAdapter for DemuxTester {
-            fn prompt(&self, request: &str) -> Result<String> {
-                match request {
-                    "what" => Ok("whatwhat".to_owned()),
+            fn prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
+                match request.as_ref().to_str().unwrap() {
+                    "what" => Ok("whatwhat".into()),
                     "give_err" => Err(ErrorCode::PermissionDenied),
                     _ => panic!("unexpected prompt!"),
                 }
             }
-            fn masked_prompt(&self, request: &str) -> Result<String> {
-                assert_eq!("reveal", request);
-                Ok("my secrets".to_owned())
+            fn masked_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
+                assert_eq!("reveal", request.as_ref());
+                Ok("my secrets".into())
             }
-            fn error_msg(&self, message: &str) {
+            fn error_msg(&self, message: impl AsRef<OsStr>) {
                 self.error_ran.set(true);
-                assert_eq!("whoopsie", message);
+                assert_eq!("whoopsie", message.as_ref());
             }
-            fn info_msg(&self, message: &str) {
+            fn info_msg(&self, message: impl AsRef<OsStr>) {
                 self.info_ran.set(true);
-                assert_eq!("did you know", message);
+                assert_eq!("did you know", message.as_ref());
             }
-            fn radio_prompt(&self, request: &str) -> Result<String> {
-                assert_eq!("channel?", request);
-                Ok("zero".to_owned())
+            fn radio_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
+                assert_eq!("channel?", request.as_ref());
+                Ok("zero".into())
             }
             fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
                 assert_eq!((&[10, 9, 8][..], 66), data_and_type);
@@ -501,11 +509,11 @@
 
         let tester = DemuxTester::default();
 
-        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 = QAndA::new("give_err");
+        let what = QAndA::new("what".as_ref());
+        let pass = MaskedQAndA::new("reveal".as_ref());
+        let err = ErrorMsg::new("whoopsie".as_ref());
+        let info = InfoMsg::new("did you know".as_ref());
+        let has_err = QAndA::new("give_err".as_ref());
 
         let conv = tester.into_conversation();
 
@@ -532,7 +540,7 @@
         {
             let conv = tester.into_conversation();
 
-            let radio = RadioQAndA::new("channel?");
+            let radio = RadioQAndA::new("channel?".as_ref());
             let bin = BinaryQAndA::new((&[10, 9, 8], 66));
             conv.communicate(&[radio.exchange(), bin.exchange()]);
 
@@ -556,11 +564,13 @@
                             assert_eq!("oh no", error.question());
                             error.set_answer(Ok(()))
                         }
-                        Exchange::Prompt(prompt) => prompt.set_answer(match prompt.question() {
-                            "should_err" => Err(ErrorCode::PermissionDenied),
-                            "question" => Ok("answer".to_owned()),
-                            other => panic!("unexpected question {other:?}"),
-                        }),
+                        Exchange::Prompt(prompt) => {
+                            prompt.set_answer(match prompt.question().to_str().unwrap() {
+                                "should_err" => Err(ErrorCode::PermissionDenied),
+                                "question" => Ok("answer".into()),
+                                other => panic!("unexpected question {other:?}"),
+                            })
+                        }
                         Exchange::MaskedPrompt(ask) => {
                             assert_eq!("password!", ask.question());
                             ask.set_answer(Ok("open sesame".into()))
@@ -571,7 +581,7 @@
                         }
                         Exchange::RadioPrompt(ask) => {
                             assert_eq!("radio?", ask.question());
-                            ask.set_answer(Ok("yes".to_owned()))
+                            ask.set_answer(Ok("yes".into()))
                         }
                     }
                 } else {