changeset 78:002adfb98c5c

Rename files, reorder structs, remove annoying BorrowedBinaryData type. This is basically a cleanup change. Also it adds tests. - Renames the files with Questions and Answers to question and answer. - Reorders the structs in those files to put the important ones first. - Removes the BorrowedBinaryData type. It was a bad idea all along. Instead, we just use (&[u8], u8). - Adds some tests because I just can't help myself.
author Paul Fisher <paul@pfish.zone>
date Sun, 08 Jun 2025 03:48:40 -0400
parents 351bdc13005e
children 2128123b9406
files src/conv.rs src/handle.rs src/libpam/answer.rs src/libpam/conversation.rs src/libpam/handle.rs src/libpam/memory.rs src/libpam/message.rs src/libpam/mod.rs src/libpam/module.rs src/libpam/question.rs src/libpam/response.rs src/module.rs
diffstat 12 files changed, 858 insertions(+), 863 deletions(-) [+]
line wrap: on
line diff
--- a/src/conv.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/conv.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -3,10 +3,11 @@
 // Temporarily allowed until we get the actual conversation functions hooked up.
 #![allow(dead_code)]
 
-use crate::constants::Result;
-use crate::ErrorCode;
+use crate::constants::{ErrorCode, Result};
 use secure_string::SecureString;
 use std::cell::Cell;
+use std::fmt;
+use std::result::Result as StdResult;
 
 /// The types of message and request that can be sent to a user.
 ///
@@ -68,8 +69,16 @@
         }
 
         impl<'a> $name<'a> {
+            #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
+            pub fn new(question: $qt) -> Self {
+                Self {
+                    q: question,
+                    a: Cell::new(Err(ErrorCode::ConversationError)),
+                }
+            }
+
             /// Converts this Q&A into a [`Message`] for the [`Conversation`].
-            pub fn message(&self) -> Message {
+            pub fn message(&self) -> Message<'_> {
                 $val(self)
             }
 
@@ -96,18 +105,14 @@
                 self.a.into_inner()
             }
         }
-    };
-}
 
-macro_rules! ask {
-    ($t:ident) => {
-        impl<'a> $t<'a> {
-            #[doc = concat!("Creates a `", stringify!($t), "` to be sent to the user.")]
-            pub fn new(question: &'a str) -> Self {
-                Self {
-                    q: question,
-                    a: Cell::new(Err(ErrorCode::ConversationError)),
-                }
+        // shout out to stackoverflow user ballpointben for this lazy impl:
+        // https://stackoverflow.com/a/78871280/39808
+        impl fmt::Debug for $name<'_> {
+            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> StdResult<(), fmt::Error> {
+                #[derive(Debug)]
+                struct $name<'a> { q: $qt }
+                fmt::Debug::fmt(&$name { q: self.q }, f)
             }
         }
     };
@@ -120,7 +125,6 @@
     ""
     "In other words, a password entry prompt."
 );
-ask!(MaskedQAndA);
 
 q_and_a!(
     QAndA<'a, Q=&'a str, A=String>,
@@ -131,7 +135,6 @@
     "When the user types, their input will be shown to them."
     "It can be used for things like usernames."
 );
-ask!(QAndA);
 
 q_and_a!(
     RadioQAndA<'a, Q=&'a str, A=String>,
@@ -142,10 +145,9 @@
     "questions, but nowhere in the documentation is it specified"
     "what the format of the answer will be, or how this should be shown."
 );
-ask!(RadioQAndA);
 
 q_and_a!(
-    BinaryQAndA<'a, Q=BorrowedBinaryData<'a>, A=BinaryData>,
+    BinaryQAndA<'a, Q=(&'a [u8], u8), A=BinaryData>,
     Message::BinaryPrompt,
     "Asks for binary data. (Linux-PAM extension)"
     ""
@@ -156,65 +158,16 @@
     "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> 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.
-    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,
-            a: Cell::new(Err(ErrorCode::ConversationError)),
-        }
-    }
-}
-
-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<'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
-    }
-
-    /// Gets the "type" of this data.
-    pub fn data_type(&self) -> u8 {
-        self.data_type
-    }
-}
-
-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 [`BorrowedBinaryData`].
-/// You can take ownership of the stored data with `.into::<Vec<u8>>()`.
+/// You can take ownership of the stored data by destructuring it:
+/// 
+/// ```
+/// # use nonstick::BinaryData;
+/// # let binary_data = BinaryData::new(vec![99, 88, 77], 66);
+/// let (data, data_type) = binary_data.into();
+/// ```
 #[derive(Debug, Default, PartialEq)]
 pub struct BinaryData {
     data: Vec<u8>,
@@ -223,28 +176,32 @@
 
 impl BinaryData {
     /// Creates a `BinaryData` with the given contents and type.
-    pub fn new(data: Vec<u8>, data_type: u8) -> Self {
-        Self { data, data_type }
+    pub fn new(data: impl Into<Vec<u8>>, data_type: u8) -> Self {
+        Self { data: data.into(), data_type }
     }
-    /// A borrowed view of the data here.
-    pub fn data(&self) -> &[u8] {
-        &self.data
-    }
-    /// The type of the data stored in this.
-    pub fn data_type(&self) -> u8 {
-        self.data_type
+}
+
+impl From<BinaryData> for (Vec<u8>, u8) {
+    fn from(value: BinaryData) -> Self {
+        (value.data, value.data_type)
     }
 }
 
-impl<'a> From<BorrowedBinaryData<'a>> for BinaryData {
-    fn from(value: BorrowedBinaryData) -> Self {
+impl From<(&'_[u8], u8)> for BinaryData {
+    fn from((data, data_type): (&'_[u8], u8)) -> Self {
         Self {
-            data: value.data.to_vec(),
-            data_type: value.data_type,
+            data: data.to_vec(),
+            data_type,
         }
     }
 }
 
+impl<'a> From<&'a BinaryData> for (&'a[u8], u8) {
+    fn from(value: &'a BinaryData) -> Self {
+        (&value.data, value.data_type)
+    }
+}
+
 impl From<BinaryData> for Vec<u8> {
     /// Takes ownership of the data stored herein.
     fn from(value: BinaryData) -> Self {
@@ -261,15 +218,6 @@
     "should still call [`set_answer`][`QAndA::set_answer`] to verify that"
     "the message has been displayed (or actively discarded)."
 );
-impl<'a> InfoMsg<'a> {
-    /// Creates an informational message to send to the user.
-    pub fn new(message: &'a str) -> Self {
-        Self {
-            q: message,
-            a: Cell::new(Err(ErrorCode::ConversationError)),
-        }
-    }
-}
 
 q_and_a!(
     ErrorMsg<'a, Q = &'a str, A = ()>,
@@ -279,17 +227,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)."
-
 );
-impl<'a> ErrorMsg<'a> {
-    /// Creates an error message to send to the user.
-    pub fn new(message: &'a str) -> Self {
-        Self {
-            q: message,
-            a: Cell::new(Err(ErrorCode::ConversationError)),
-        }
-    }
-}
 
 /// A channel for PAM modules to request information from the user.
 ///
@@ -315,10 +253,12 @@
 /// Conversation:
 ///
 /// ```
-/// use nonstick::conv::{Conversation, Message, conversation_func};
+/// use nonstick::conv::{conversation_func, Conversation, Message};
 /// mod some_library {
 /// #    use nonstick::Conversation;
-///     pub fn get_auth_data(conv: &mut impl Conversation) { /* ... */ }
+///     pub fn get_auth_data(conv: &mut impl Conversation) {
+///         /* ... */
+///     }
 /// }
 ///
 /// fn my_terminal_prompt(messages: &[Message]) {
@@ -364,15 +304,16 @@
 /// or to use a `SimpleConversation` as a `Conversation`:
 ///
 /// ```
+/// use nonstick::{Conversation, SimpleConversation};
 /// use secure_string::SecureString;
-/// use nonstick::{Conversation, SimpleConversation};
 /// # 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: &mut impl Conversation) { /* ... */
+///     }
 /// }
 ///
-/// struct MySimpleConvo { /* ... */ }
+/// struct MySimpleConvo {/* ... */}
 /// # impl MySimpleConvo { fn new() -> Self { Self{} } }
 ///
 /// impl SimpleConversation for MySimpleConvo {
@@ -397,7 +338,7 @@
 /// #     todo!()
 /// # }
 /// #
-/// # fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
+/// # fn binary_prompt(&mut self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> {
 /// #     todo!()
 /// # }
 /// }
@@ -413,7 +354,7 @@
     /// 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 as_conversation(&mut self) -> Demux<'_, Self>
     where
         Self: Sized,
     {
@@ -432,18 +373,18 @@
     /// Sends an informational message to the user.
     fn info_msg(&mut self, message: &str);
     /// Requests binary data from the user (a Linux-PAM extension).
-    fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData>;
+    fn binary_prompt(&mut self, data_and_type: (&[u8], u8)) -> Result<BinaryData>;
 }
 
 macro_rules! conv_fn {
-    ($fn_name:ident($($param:ident: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
+    ($fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => {
         fn $fn_name(&mut self, $($param: $pt),*) -> Result<$resp_type> {
             let prompt = <$msg>::new($($param),*);
             self.communicate(&[prompt.message()]);
             prompt.answer()
         }
     };
-    ($fn_name:ident($($param:ident: $pt:ty),+) { $msg:ty }) => {
+    ($fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => {
         fn $fn_name(&mut self, $($param: $pt),*) {
             self.communicate(&[<$msg>::new($($param),*).message()]);
         }
@@ -456,7 +397,7 @@
     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 });
+    conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA });
 }
 
 /// A [`Conversation`] which asks the questions one at a time.
@@ -485,7 +426,7 @@
                 }
                 Message::BinaryPrompt(prompt) => {
                     let q = prompt.question();
-                    prompt.set_answer(self.0.binary_prompt(q.data, q.data_type))
+                    prompt.set_answer(self.0.binary_prompt(q))
                 }
             }
         }
@@ -495,8 +436,8 @@
 #[cfg(test)]
 mod tests {
     use super::{
-        BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA,
-        RadioQAndA, Result, SecureString, SimpleConversation,
+        BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA,
+        Result, SecureString, SimpleConversation,
     };
     use crate::constants::ErrorCode;
     use crate::BinaryData;
@@ -533,9 +474,8 @@
                 self.info_ran = true;
                 assert_eq!("did you know", message);
             }
-            fn binary_prompt(&mut self, data: &[u8], data_type: u8) -> Result<BinaryData> {
-                assert_eq!(&[10, 9, 8], data);
-                assert_eq!(66, data_type);
+            fn binary_prompt(&mut 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))
             }
         }
@@ -573,7 +513,7 @@
         let mut conv = tester.as_conversation();
 
         let radio = RadioQAndA::new("channel?");
-        let bin = BinaryQAndA::new(&[10, 9, 8], 66);
+        let bin = BinaryQAndA::new((&[10, 9, 8], 66));
         conv.communicate(&[radio.message(), bin.message()]);
 
         assert_eq!("zero", radio.answer().unwrap());
@@ -605,8 +545,7 @@
                             ask.set_answer(Ok("open sesame".into()))
                         }
                         Message::BinaryPrompt(prompt) => {
-                            assert_eq!(&[1, 2, 3], prompt.question().data);
-                            assert_eq!(69, prompt.question().data_type);
+                            assert_eq!((&[1, 2, 3][..], 69), prompt.question());
                             prompt.set_answer(Ok(BinaryData::new(vec![3, 2, 1], 42)))
                         }
                         Message::RadioPrompt(ask) => {
@@ -636,7 +575,7 @@
             assert_eq!("yes", tester.radio_prompt("radio?").unwrap());
             assert_eq!(
                 BinaryData::new(vec![3, 2, 1], 42),
-                tester.binary_prompt(&[1, 2, 3], 69).unwrap(),
+                tester.binary_prompt((&[1, 2, 3], 69)).unwrap(),
             )
         }
         assert_eq!(
--- a/src/handle.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/handle.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -209,18 +209,18 @@
     trait_item!(
         set = set_authtok_item,
         item = "PAM_AUTHTOK",
-        see = PamModuleHandle::authtok_item,
-        "Sets the user's authentication token (e.g., password)."
+        see = PamModuleOnly::authtok_item,
+        "Gets the user's authentication token (e.g., password)."
         ""
         "This is usually set automatically when "
-        "[`get_authtok`](PamModuleHandle::get_authtok) is called, "
+        "[`get_authtok`](PamModuleOnly::get_authtok) is called, "
         "but can be manually set."
     );
 
     trait_item!(
         set = set_old_authtok_item,
         item = "PAM_OLDAUTHTOK",
-        see = PamModuleHandle::old_authtok_item,
+        see = PamModuleOnly::old_authtok_item,
         "Sets the user's \"old authentication token\" when changing passwords."
         ""
         "This is usually set automatically by PAM."
@@ -308,7 +308,7 @@
     trait_item!(
         get = old_authtok_item,
         item = "PAM_OLDAUTHTOK",
-        see = PamHandle::set_old_authtok_item,
+        see = PamShared::set_old_authtok_item,
         "Gets the user's old authentication token when changing passwords."
         ""
         "This should only ever be called by *password-change* PAM modules."
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/answer.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -0,0 +1,333 @@
+//! Types used to communicate data from the application to the module.
+
+use crate::libpam::conversation::OwnedMessage;
+use crate::libpam::memory;
+use crate::libpam::memory::{CBinaryData, Immovable};
+use crate::{ErrorCode, Result};
+use std::ffi::{c_int, c_void, CStr};
+use std::ops::{Deref, DerefMut};
+use std::{iter, mem, ptr, slice};
+
+/// The corridor via which the answer to Messages navigate through PAM.
+#[derive(Debug)]
+pub struct Answers {
+    base: *mut Answer,
+    count: usize,
+}
+
+impl Answers {
+    /// Builds an Answers out of the given answered Message Q&As.
+    pub fn build(value: Vec<OwnedMessage>) -> Result<Self> {
+        let mut outputs = Self {
+            base: memory::calloc(value.len()),
+            count: value.len(),
+        };
+        // Even if we fail during this process, we still end up freeing
+        // all allocated answer memory.
+        for (input, output) in iter::zip(value, outputs.iter_mut()) {
+            match input {
+                OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.unsecure())?,
+                OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
+                OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?,
+                OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
+                OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
+                OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
+            }
+        }
+        Ok(outputs)
+    }
+
+    /// Converts this into a `*Answer` for passing to PAM.
+    ///
+    /// This object is consumed and the `Answer` pointer now owns its data.
+    /// It can be recreated with [`Self::from_c_heap`].
+    pub fn into_ptr(self) -> *mut Answer {
+        let ret = self.base;
+        mem::forget(self);
+        ret
+    }
+
+    /// Takes ownership of a list of answers allocated on the C heap.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to make sure you pass a valid pointer,
+    /// like one that you got from PAM, or maybe [`Self::into_ptr`].
+    pub unsafe fn from_c_heap(base: *mut Answer, count: usize) -> Self {
+        Answers { base, count }
+    }
+}
+
+impl Deref for Answers {
+    type Target = [Answer];
+    fn deref(&self) -> &Self::Target {
+        // SAFETY: This is the memory we manage ourselves.
+        unsafe { slice::from_raw_parts(self.base, self.count) }
+    }
+}
+
+impl DerefMut for Answers {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        // SAFETY: This is the memory we manage ourselves.
+        unsafe { slice::from_raw_parts_mut(self.base, self.count) }
+    }
+}
+
+impl Drop for Answers {
+    fn drop(&mut self) {
+        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
+        unsafe {
+            for answer in self.iter_mut() {
+                answer.free_contents()
+            }
+            memory::free(self.base)
+        }
+    }
+}
+
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct TextAnswer(Answer);
+
+impl TextAnswer {
+    /// Interprets the provided `Answer` as a text answer.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to provide an answer that is a `TextAnswer`.
+    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
+        // SAFETY: We're provided a valid reference.
+        &mut *(from as *mut Answer).cast::<Self>()
+    }
+
+    /// Converts the `Answer` to a `TextAnswer` with the given text.
+    fn fill(dest: &mut Answer, text: &str) -> Result<()> {
+        let allocated = memory::malloc_str(text)?;
+        dest.free_contents();
+        dest.data = allocated.cast();
+        Ok(())
+    }
+
+    /// Gets the string stored in this answer.
+    pub fn contents(&self) -> Result<&str> {
+        if self.0.data.is_null() {
+            Ok("")
+        } else {
+            // SAFETY: This data is either passed from PAM (so we are forced
+            // to trust it) or was created by us in TextAnswerInner::alloc.
+            // In either case, it's going to be a valid null-terminated string.
+            unsafe { CStr::from_ptr(self.0.data.cast()) }
+                .to_str()
+                .map_err(|_| ErrorCode::ConversationError)
+        }
+    }
+
+    /// Zeroes out the answer data, frees it, and points our data to `null`.
+    ///
+    /// When this `TextAnswer` is part of an [`Answers`],
+    /// this is optional (since that will perform the `free`),
+    /// but it will clear potentially sensitive data.
+    pub fn free_contents(&mut self) {
+        // SAFETY: We own this data and know it's valid.
+        // If it's null, this is a no-op.
+        // After we're done, it will be null.
+        unsafe {
+            memory::zero_c_string(self.0.data.cast());
+            memory::free(self.0.data);
+            self.0.data = ptr::null_mut()
+        }
+    }
+}
+
+/// A [`Answer`] with [`CBinaryData`] in it.
+#[repr(transparent)]
+#[derive(Debug)]
+pub struct BinaryAnswer(Answer);
+
+impl BinaryAnswer {
+    /// Interprets the provided [`Answer`] as a binary answer.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to provide an answer that is a `BinaryAnswer`.
+    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
+        // SAFETY: We're provided a valid reference.
+        &mut *(from as *mut Answer).cast::<Self>()
+    }
+
+    /// Fills in a [`Answer`] with the provided binary data.
+    ///
+    /// The `data_type` is a tag you can use for whatever.
+    /// It is passed through PAM unchanged.
+    ///
+    /// The referenced data is copied to the C heap.
+    /// We do not take ownership of the original data.
+    pub fn fill(dest: &mut Answer, data_and_type: (&[u8], u8)) -> Result<()> {
+        let allocated = CBinaryData::alloc(data_and_type)?;
+        dest.free_contents();
+        dest.data = allocated.cast();
+        Ok(())
+    }
+
+    /// Gets the binary data in this answer.
+    pub fn data(&self) -> Option<&CBinaryData> {
+        // SAFETY: We either got this data from PAM or allocated it ourselves.
+        // Either way, we trust that it is either valid data or null.
+        unsafe { self.0.data.cast::<CBinaryData>().as_ref() }
+    }
+
+    /// Zeroes out the answer data, frees it, and points our data to `null`.
+    ///
+    /// When this `TextAnswer` is part of an [`Answers`],
+    /// this is optional (since that will perform the `free`),
+    /// but it will clear potentially sensitive data.
+    pub fn zero_contents(&mut self) {
+        // SAFETY: We know that our data pointer is either valid or null.
+        // Once we're done, it's null and the answer is safe.
+        unsafe {
+            let data_ref = self.0.data.cast::<CBinaryData>().as_mut();
+            if let Some(d) = data_ref {
+                d.zero_contents()
+            }
+            memory::free(self.0.data);
+            self.0.data = ptr::null_mut()
+        }
+    }
+}
+
+/// Generic version of answer data.
+///
+/// This has the same structure as [`BinaryAnswer`]
+/// and [`TextAnswer`].
+#[repr(C)]
+#[derive(Debug)]
+pub struct Answer {
+    /// Pointer to the data returned in an answer.
+    /// For most answers, this will be a [`CStr`], but for answers to
+    /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
+    /// (a Linux-PAM extension).
+    data: *mut c_void,
+    /// Unused.
+    return_code: c_int,
+    _marker: Immovable,
+}
+
+impl Answer {
+    /// Frees the contents of this answer.
+    ///
+    /// After this is done, this answer's `data` will be `null`,
+    /// which is a valid (empty) state.
+    fn free_contents(&mut self) {
+        // SAFETY: We have either an owned valid pointer, or null.
+        // We can free our owned pointer, and `free(null)` is a no-op.
+        unsafe {
+            memory::free(self.data);
+            self.data = ptr::null_mut();
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{Answer, Answers, BinaryAnswer, TextAnswer};
+    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA};
+    use crate::libpam::conversation::OwnedMessage;
+    use crate::BinaryData;
+    use crate::libpam::memory;
+
+    #[test]
+    fn test_round_trip() {
+        let binary_msg = {
+            let qa = BinaryQAndA::new((&[][..], 0));
+            qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99)));
+            OwnedMessage::BinaryPrompt(qa)
+        };
+
+        macro_rules! answered {
+            ($typ:ty, $msg:path, $data:expr) => {{
+                let qa = <$typ>::new("");
+                qa.set_answer(Ok($data));
+                $msg(qa)
+            }};
+        }
+
+        let answers = vec![
+            binary_msg,
+            answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()),
+            answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()),
+            answered!(ErrorMsg, OwnedMessage::Error, ()),
+            answered!(InfoMsg, OwnedMessage::Info, ()),
+            answered!(
+                RadioQAndA,
+                OwnedMessage::RadioPrompt,
+                "beep boop".to_owned()
+            ),
+        ];
+        let n = answers.len();
+        let sent = Answers::build(answers).unwrap();
+        let heap_answers = sent.into_ptr();
+        let mut received = unsafe { Answers::from_c_heap(heap_answers, n) };
+
+        let assert_text = |want, raw| {
+            let up = unsafe { TextAnswer::upcast(raw) };
+            assert_eq!(want, up.contents().unwrap());
+            up.free_contents();
+            assert_eq!("", up.contents().unwrap());
+        };
+        let assert_bin = |want, raw| {
+            let up = unsafe { BinaryAnswer::upcast(raw) };
+            assert_eq!(BinaryData::from(want), up.data().into());
+            up.zero_contents();
+            assert_eq!(BinaryData::default(), up.data().into());
+        };
+        if let [zero, one, two, three, four, five] = &mut received[..] {
+            assert_bin((&[1, 2, 3][..], 99), zero);
+            assert_text("whats going on", one);
+            assert_text("well then", two);
+            assert_text("", three);
+            assert_text("", four);
+            assert_text("beep boop", five);
+        } else {
+            panic!("received wrong size {len}!", len = received.len())
+        }
+    }
+
+    #[test]
+    fn test_text_answer() {
+        let answer_ptr: *mut Answer = memory::calloc(1);
+        let answer = unsafe {&mut *answer_ptr};
+        TextAnswer::fill(answer, "hello").unwrap();
+        let zeroth_text = unsafe { TextAnswer::upcast(answer) };
+        let data = zeroth_text.contents().expect("valid");
+        assert_eq!("hello", data);
+        zeroth_text.free_contents();
+        zeroth_text.free_contents();
+        TextAnswer::fill(answer, "hell\0").expect_err("should error; contains nul");
+        unsafe { memory::free(answer_ptr) }
+    }
+
+    #[test]
+    fn test_binary_answer() {
+        let answer_ptr: *mut Answer = memory::calloc(1);
+        let answer = unsafe { &mut *answer_ptr };
+        let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9);
+        BinaryAnswer::fill(answer, (&real_data).into()).expect("alloc should succeed");
+        let bin_answer = unsafe { BinaryAnswer::upcast(answer) };
+        assert_eq!(real_data, bin_answer.data().into());
+        answer.free_contents();
+        answer.free_contents();
+        unsafe { memory::free(answer_ptr) }
+    }
+
+    #[test]
+    #[ignore]
+    fn test_binary_answer_too_big() {
+        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
+        let answer_ptr: *mut Answer = memory::calloc(1);
+        let answer = unsafe {&mut*answer_ptr};
+        BinaryAnswer::fill(answer, (&big_data, 100))
+            .expect_err("this is too big!");
+        answer.free_contents();
+        unsafe { memory::free(answer) }
+    }
+}
--- a/src/libpam/conversation.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/libpam/conversation.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -1,10 +1,9 @@
 use crate::conv::{
-    BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA,
-    RadioQAndA,
+    BinaryQAndA, Conversation, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA,
 };
+use crate::libpam::answer::{Answer, Answers, BinaryAnswer, TextAnswer};
 use crate::libpam::memory::Immovable;
-use crate::libpam::message::{Indirect, Questions};
-use crate::libpam::response::{Answer, Answers, BinaryAnswer, TextAnswer};
+use crate::libpam::question::{Indirect, Questions};
 use crate::ErrorCode;
 use crate::Result;
 use std::ffi::c_int;
@@ -76,7 +75,7 @@
             // Build our owned list of Q&As from the questions we've been asked
             let messages: Vec<OwnedMessage> = indirect
                 .iter(count as usize)
-                .map(OwnedMessage::try_from)
+                .map(TryInto::try_into)
                 .collect::<Result<_>>()
                 .map_err(|_| ErrorCode::ConversationError)?;
             // Borrow all those Q&As and ask them
@@ -95,16 +94,13 @@
 impl Conversation for LibPamConversation<'_> {
     fn communicate(&mut self, messages: &[Message]) {
         let internal = || {
-            let mut msgs_to_send = Questions::alloc(messages.len());
-            for (dst, src) in iter::zip(msgs_to_send.iter_mut(), messages.iter()) {
-                dst.fill(src).map_err(|_| ErrorCode::ConversationError)?
-            }
+            let questions = Questions::new(messages)?;
             let mut response_pointer = std::ptr::null_mut();
             // SAFETY: We're calling into PAM with valid everything.
             let result = unsafe {
                 (self.callback)(
                     messages.len() as c_int,
-                    msgs_to_send.indirect(),
+                    questions.indirect(),
                     &mut response_pointer,
                     self.appdata,
                 )
@@ -128,6 +124,7 @@
 }
 
 /// Like [`Message`], but this time we own the contents.
+#[derive(Debug)]
 pub enum OwnedMessage<'a> {
     MaskedPrompt(MaskedQAndA<'a>),
     Prompt(QAndA<'a>),
@@ -157,9 +154,11 @@
 /// You are responsible for ensuring that the src-dst pair matches.
 unsafe fn convert(msg: &Message, resp: &mut Answer) {
     macro_rules! fill_text {
-    ($dst:ident, $src:ident) => {{let text_resp = unsafe {TextAnswer::upcast($src)};
-    $dst.set_answer(text_resp.contents().map(Into::into));}}
-}
+        ($dst:ident, $src:ident) => {{
+            let text_resp = unsafe { TextAnswer::upcast($src) };
+            $dst.set_answer(text_resp.contents().map(Into::into));
+        }};
+    }
     match *msg {
         Message::MaskedPrompt(qa) => fill_text!(qa, resp),
         Message::Prompt(qa) => fill_text!(qa, resp),
--- a/src/libpam/handle.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/libpam/handle.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -54,7 +54,7 @@
     }
 
     /// Gets the `PAM_CONV` item from the handle.
-    fn conversation_item(&mut self) -> Result<&mut LibPamConversation> {
+    fn conversation_item(&mut self) -> Result<&mut LibPamConversation<'_>> {
         let output: *mut LibPamConversation = ptr::null_mut();
         let result = unsafe {
             super::pam_get_item(
--- a/src/libpam/memory.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/libpam/memory.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -1,12 +1,28 @@
 //! Things for dealing with memory.
 
-use crate::conv::BorrowedBinaryData;
 use crate::Result;
 use crate::{BinaryData, ErrorCode};
-use std::ffi::{c_char, c_void, CStr, CString};
+use std::ffi::{c_char, CStr, CString};
 use std::marker::{PhantomData, PhantomPinned};
 use std::{ptr, slice};
 
+/// Allocates `count` elements to hold `T`.
+#[inline]
+pub fn calloc<T>(count: usize) -> *mut T {
+    // SAFETY: it's always safe to allocate! Leaking memory is fun!
+    unsafe { libc::calloc(count, size_of::<T>()) }.cast()
+}
+
+/// Wrapper for [`libc::free`] to make debugging calls/frees easier.
+///
+/// # Safety
+///
+/// If you double-free, it's all your fault.
+#[inline]
+pub unsafe fn free<T>(p: *mut T) {
+    libc::free(p.cast())
+}
+
 /// Makes whatever it's in not [`Send`], [`Sync`], or [`Unpin`].
 #[repr(C)]
 #[derive(Debug)]
@@ -62,7 +78,7 @@
 
 /// Allocates a string with the given contents on the C heap.
 ///
-/// This is like [`CString::new`](std::ffi::CString::new), but:
+/// This is like [`CString::new`], but:
 ///
 /// - it allocates data on the C heap with [`libc::malloc`].
 /// - it doesn't take ownership of the data passed in.
@@ -71,11 +87,12 @@
     if data.contains(&0) {
         return Err(ErrorCode::ConversationError);
     }
+    let data_alloc: *mut c_char = calloc(data.len() + 1);
+    // SAFETY: we just allocated this and we have enough room.
     unsafe {
-        let data_alloc = libc::calloc(data.len() + 1, 1);
-        libc::memcpy(data_alloc, data.as_ptr().cast(), data.len());
-        Ok(data_alloc.cast())
+        libc::memcpy(data_alloc.cast(), data.as_ptr().cast(), data.len());
     }
+    Ok(data_alloc)
 }
 
 /// Writes zeroes over the contents of a C string.
@@ -85,9 +102,9 @@
 /// # Safety
 ///
 /// It's up to you to provide a valid C string.
-pub unsafe fn zero_c_string(cstr: *mut c_void) {
+pub unsafe fn zero_c_string(cstr: *mut c_char) {
     if !cstr.is_null() {
-        libc::memset(cstr, 0, libc::strlen(cstr.cast()));
+        libc::memset(cstr.cast(), 0, libc::strlen(cstr.cast()));
     }
 }
 
@@ -110,20 +127,20 @@
 
 impl CBinaryData {
     /// Copies the given data to a new BinaryData on the heap.
-    pub fn alloc(source: &[u8], data_type: u8) -> Result<*mut CBinaryData> {
+    pub fn alloc((data, data_type): (&[u8], u8)) -> Result<*mut CBinaryData> {
         let buffer_size =
-            u32::try_from(source.len() + 5).map_err(|_| ErrorCode::ConversationError)?;
+            u32::try_from(data.len() + 5).map_err(|_| ErrorCode::ConversationError)?;
         // SAFETY: We're only allocating here.
-        let data = unsafe {
-            let dest_buffer: *mut CBinaryData = libc::malloc(buffer_size as usize).cast();
-            let data = &mut *dest_buffer;
-            data.total_length = buffer_size.to_be_bytes();
-            data.data_type = data_type;
-            let dest = data.data.as_mut_ptr();
-            libc::memcpy(dest.cast(), source.as_ptr().cast(), source.len());
+        let dest = unsafe {
+            let dest_buffer: *mut CBinaryData = calloc::<u8>(buffer_size as usize).cast();
+            let dest = &mut *dest_buffer;
+            dest.total_length = buffer_size.to_be_bytes();
+            dest.data_type = data_type;
+            let dest = dest.data.as_mut_ptr();
+            libc::memcpy(dest.cast(), data.as_ptr().cast(), data.len());
             dest_buffer
         };
-        Ok(data)
+        Ok(dest)
     }
 
     fn length(&self) -> usize {
@@ -141,39 +158,42 @@
     }
 }
 
-impl<'a> From<&'a CBinaryData> for BorrowedBinaryData<'a> {
+impl<'a> From<&'a CBinaryData> for (&'a[u8], u8) {
     fn from(value: &'a CBinaryData) -> Self {
-        BorrowedBinaryData::new(
-            unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) },
-            value.data_type,
-        )
+        (unsafe { slice::from_raw_parts(value.data.as_ptr(), value.length()) },
+            value.data_type        )
     }
 }
 
 impl From<Option<&'_ CBinaryData>> for BinaryData {
     fn from(value: Option<&CBinaryData>) -> Self {
-        value.map(BorrowedBinaryData::from).map(Into::into).unwrap_or_default()
+        // This is a dumb trick but I like it because it is simply the presence
+        // of `.map(|(x, y)| (x, y))` in the middle of this that gives
+        // type inference the hint it needs to make this work.
+        value
+            .map(Into::into)
+            .map(|(data, data_type)| (data, data_type))
+            .map(Into::into)
+            .unwrap_or_default()
     }
 }
 
 #[cfg(test)]
 mod tests {
-    use super::{copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string};
-    use crate::ErrorCode;
-    use std::ffi::CString;
+    use super::{free, ErrorCode, CString, copy_pam_string, malloc_str, option_cstr, prompt_ptr, zero_c_string};
     #[test]
     fn test_strings() {
         let str = malloc_str("hello there").unwrap();
         malloc_str("hell\0 there").unwrap_err();
         unsafe {
-            let copied = copy_pam_string(str.cast()).unwrap();
+            let copied = copy_pam_string(str).unwrap();
             assert_eq!("hello there", copied);
-            zero_c_string(str.cast());
+            zero_c_string(str);
             let idx_three = str.add(3).as_mut().unwrap();
             *idx_three = 0x80u8 as i8;
-            let zeroed = copy_pam_string(str.cast()).unwrap();
+            let zeroed = copy_pam_string(str).unwrap();
             assert!(zeroed.is_empty());
-            libc::free(str.cast());
+            free(str);
         }
     }
 
--- a/src/libpam/message.rs	Sun Jun 08 01:03:46 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,348 +0,0 @@
-//! Data and types dealing with PAM messages.
-
-use crate::constants::InvalidEnum;
-use crate::libpam::conversation::OwnedMessage;
-use crate::libpam::memory;
-use crate::libpam::memory::{CBinaryData, Immovable};
-use crate::ErrorCode;
-use crate::Result;
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use std::ffi::{c_int, c_void, CStr};
-use std::result::Result as StdResult;
-use std::{ptr, slice};
-use crate::conv::{BorrowedBinaryData, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA};
-
-/// The C enum values for messages shown to the user.
-#[derive(Debug, PartialEq, FromPrimitive)]
-pub enum Style {
-    /// Requests information from the user; will be masked when typing.
-    PromptEchoOff = 1,
-    /// Requests information from the user; will not be masked.
-    PromptEchoOn = 2,
-    /// An error message.
-    ErrorMsg = 3,
-    /// An informational message.
-    TextInfo = 4,
-    /// Yes/No/Maybe conditionals. A Linux-PAM extension.
-    RadioType = 5,
-    /// For server–client non-human interaction.
-    ///
-    /// NOT part of the X/Open PAM specification.
-    /// A Linux-PAM extension.
-    BinaryPrompt = 7,
-}
-
-impl TryFrom<c_int> for Style {
-    type Error = InvalidEnum<Self>;
-    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
-        Self::from_i32(value).ok_or(value.into())
-    }
-}
-
-impl From<Style> for c_int {
-    fn from(val: Style) -> Self {
-        val as Self
-    }
-}
-
-/// A question sent by PAM or a module to an application.
-///
-/// PAM refers to this as a "message", but we call it a question
-/// to avoid confusion with [`Message`].
-///
-/// This question, and its internal data, is owned by its creator
-/// (either the module or PAM itself).
-#[repr(C)]
-pub struct Question {
-    /// The style of message to request.
-    style: c_int,
-    /// A description of the data requested.
-    ///
-    /// For most requests, this will be an owned [`CStr`], but for requests
-    /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`]
-    /// (a Linux-PAM extension).
-    data: *mut c_void,
-    _marker: Immovable,
-}
-
-impl Question {
-    pub fn fill(&mut self, msg: &Message) -> Result<()> {
-        let (style, data) = copy_to_heap(msg)?;
-        self.clear();
-        // SAFETY: We allocated this ourselves or were given it by PAM.
-        // Otherwise, it's null, but free(null) is fine.
-        unsafe { libc::free(self.data) };
-        self.style = style as c_int;
-        self.data = data;
-        Ok(())
-    }
-
-    /// Gets this message's data pointer as a string.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to pass this only on types with a string value.
-    unsafe fn string_data(&self) -> Result<&str> {
-        if self.data.is_null() {
-            Ok("")
-        } else {
-            CStr::from_ptr(self.data.cast())
-                .to_str()
-                .map_err(|_| ErrorCode::ConversationError)
-        }
-    }
-
-    /// Gets this message's data pointer as borrowed binary data.
-    unsafe fn binary_data(&self) -> BorrowedBinaryData {
-        self.data
-            .cast::<CBinaryData>()
-            .as_ref()
-            .map(Into::into)
-            .unwrap_or_default()
-    }
-
-    /// Zeroes out the data stored here.
-    fn clear(&mut self) {
-        // SAFETY: We either created this data or we got it from PAM.
-        // After this function is done, it will be zeroed out.
-        unsafe {
-            if let Ok(style) = Style::try_from(self.style) {
-                match style {
-                    Style::BinaryPrompt => {
-                        if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
-                            d.zero_contents()
-                        }
-                    }
-                    Style::TextInfo
-                    | Style::RadioType
-                    | Style::ErrorMsg
-                    | Style::PromptEchoOff
-                    | Style::PromptEchoOn => memory::zero_c_string(self.data),
-                }
-            };
-            libc::free(self.data);
-            self.data = ptr::null_mut();
-        }
-    }
-}
-
-impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> {
-    type Error = ErrorCode;
-    fn try_from(question: &'a Question) -> Result<Self> {
-        let style: Style = question
-            .style
-            .try_into()
-            .map_err(|_| ErrorCode::ConversationError)?;
-        // SAFETY: In all cases below, we're matching the
-        let prompt = unsafe {
-            match style {
-                Style::PromptEchoOff => {
-                    Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?))
-                }
-                Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)),
-                Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
-                Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
-                Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
-                Style::BinaryPrompt => Self::BinaryPrompt(question.binary_data().into()),
-            }
-        };
-        Ok(prompt)
-    }
-}
-
-/// Copies the contents of this message to the C heap.
-fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> {
-    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
-    match *msg {
-        Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
-        Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
-        Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
-        Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
-        Message::Info(p) => alloc(Style::TextInfo, p.question()),
-        Message::BinaryPrompt(p) => {
-            let q = p.question();
-            Ok((
-                Style::BinaryPrompt,
-                CBinaryData::alloc(q.data(), q.data_type())?.cast(),
-            ))
-        }
-    }
-}
-
-/// Abstraction of a collection of questions to be sent in a PAM conversation.
-///
-/// The PAM C API conversation function looks like this:
-///
-/// ```c
-/// int pam_conv(
-///     int count,
-///     const struct pam_message **questions,
-///     struct pam_response **answers,
-///     void *appdata_ptr,
-/// )
-/// ```
-///
-/// On Linux-PAM and other compatible implementations, `messages`
-/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
-/// (In this situation, the value of `OwnedMessages.indirect` is
-/// the pointer passed to `pam_conv`.)
-///
-/// ```text
-/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message ═╗
-/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style     ║
-/// ║ count       ║             │ base[1] ┄┄┄┼┄┄┄╮   ║ data      ║
-/// ╚═════════════╝             │ ...        │   ┆   ╚═══════════╝
-///                                              ┆
-///                                              ┆    ╔═ Message ═╗
-///                                              ╰┄┄> ║ style     ║
-///                                                   ║ data      ║
-///                                                   ╚═══════════╝
-/// ```
-///
-/// On OpenPAM and other compatible implementations (like Solaris),
-/// `messages` is a pointer-to-pointer-to-array.  This appears to be
-/// the correct implementation as required by the XSSO specification.
-///
-/// ```text
-/// ╔═ OwnedMsgs ═╗  points to  ┌─ Indirect ─┐       ╔═ Message[] ═╗
-/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style       ║
-/// ║ count       ║             └────────────┘       ║ data        ║
-/// ╚═════════════╝                                  ╟─────────────╢
-///                                                  ║ style       ║
-///                                                  ║ data        ║
-///                                                  ╟─────────────╢
-///                                                  ║ ...         ║
-/// ```
-///
-/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
-pub struct Questions {
-    /// An indirection to the questions themselves, stored on the C heap.
-    indirect: *mut Indirect,
-    /// The number of questions.
-    count: usize,
-}
-
-impl Questions {
-    /// Allocates data to store questions on the C heap.
-    pub fn alloc(count: usize) -> Self {
-        Self {
-            indirect: Indirect::alloc(count),
-            count,
-        }
-    }
-
-    /// The pointer to the thing with the actual list.
-    pub fn indirect(&self) -> *const Indirect {
-        self.indirect
-    }
-
-    pub fn iter(&self) -> impl Iterator<Item = &Question> {
-        // SAFETY: we're iterating over an amount we know.
-        unsafe { (*self.indirect).iter(self.count) }
-    }
-
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
-        // SAFETY: we're iterating over an amount we know.
-        unsafe { (*self.indirect).iter_mut(self.count) }
-    }
-}
-
-impl Drop for Questions {
-    fn drop(&mut self) {
-        // SAFETY: We are valid and have a valid pointer.
-        // Once we're done, everything will be safe.
-        unsafe {
-            if let Some(indirect) = self.indirect.as_mut() {
-                indirect.free(self.count)
-            }
-            libc::free(self.indirect.cast());
-            self.indirect = ptr::null_mut();
-        }
-    }
-}
-
-/// An indirect reference to messages.
-///
-/// This is kept separate to provide a place where we can separate
-/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
-#[repr(transparent)]
-pub struct Indirect {
-    base: [*mut Question; 0],
-    _marker: Immovable,
-}
-
-impl Indirect {
-    /// Allocates memory for this indirector and all its members.
-    fn alloc(count: usize) -> *mut Self {
-        // SAFETY: We're only allocating, and when we're done,
-        // everything will be in a known-good state.
-        unsafe {
-            let me_ptr: *mut Indirect = libc::calloc(count, size_of::<*mut Question>()).cast();
-            let me = &mut *me_ptr;
-            let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count);
-            for entry in ptr_list {
-                *entry = libc::calloc(1, size_of::<Question>()).cast();
-            }
-            me
-        }
-    }
-
-    /// Returns an iterator yielding the given number of messages.
-    ///
-    /// # Safety
-    ///
-    /// You have to provide the right count.
-    pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
-        (0..count).map(|idx| &**self.base.as_ptr().add(idx))
-    }
-
-    /// Returns a mutable iterator yielding the given number of messages.
-    ///
-    /// # Safety
-    ///
-    /// You have to provide the right count.
-    pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
-        (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
-    }
-
-    /// Frees this and everything it points to.
-    ///
-    /// # Safety
-    ///
-    /// You have to pass the right size.
-    unsafe fn free(&mut self, count: usize) {
-        let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count);
-        for msg in msgs {
-            if let Some(msg) = msg.as_mut() {
-                msg.clear();
-            }
-            libc::free(msg.cast());
-            *msg = ptr::null_mut();
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{MaskedQAndA, Questions};
-    use crate::conv::{BinaryQAndA, QAndA};
-
-    #[test]
-    fn test_owned_messages() {
-        let mut tons_of_messages = Questions::alloc(10);
-        let mut msgs: Vec<_> = tons_of_messages.iter_mut().collect();
-        assert!(msgs.get(10).is_none());
-        let last_msg = &mut msgs[9];
-        last_msg
-            .fill(&MaskedQAndA::new("hocus pocus").message())
-            .unwrap();
-        let another_msg = &mut msgs[0];
-        another_msg
-            .fill(&BinaryQAndA::new(&[5, 4, 3, 2, 1], 66).message())
-            .unwrap();
-        let overwrite = &mut msgs[3];
-        overwrite.fill(&QAndA::new("what").message()).unwrap();
-        overwrite.fill(&QAndA::new("who").message()).unwrap();
-    }
-}
--- a/src/libpam/mod.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/libpam/mod.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -5,16 +5,15 @@
 //! those data structures.
 //!
 //! Everything in here is hazmat.
-//!
 
 #![allow(dead_code)]
 
+mod answer;
 mod conversation;
 mod handle;
 mod memory;
-mod message;
 mod module;
-mod response;
+mod question;
 
 pub use handle::{LibPamHandle, OwnedLibPamHandle};
 use std::ffi::{c_char, c_int, c_void};
--- a/src/libpam/module.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/libpam/module.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -1,15 +1,19 @@
-/// Generates the dynamic library entry points for a [PamModule] implementation.
+/// Generates the dynamic library entry points for a PAM module
 ///
-/// Calling `pam_hooks!(SomeType)` on a type that implements [PamModule] will
-/// generate the exported `extern "C"` functions that PAM uses to call into
-/// your module.
+/// Calling `pam_hooks!(SomeType)` on a type that implements
+/// [`PamModule`](crate::PamModule) will generate the exported
+/// `extern "C"` functions that PAM uses to call into your module.
 ///
 /// ## Examples:
 ///
-/// Here is full example of a PAM module that would authenticate and authorize everybody:
+/// Here is full example of a PAM module that would authenticate
+/// and authorize everybody:
 ///
 /// ```no_run
-/// use nonstick::{Flags, SimpleConversation, OwnedLibPamHandle, PamModule, PamHandleModule, Result as PamResult, pam_hooks};
+/// use nonstick::{
+///     pam_hooks, Flags, OwnedLibPamHandle, PamHandleModule, PamModule, Result as PamResult,
+///     SimpleConversation,
+/// };
 /// use std::ffi::CStr;
 /// # fn main() {}
 ///
@@ -19,7 +23,8 @@
 /// impl<T: PamHandleModule> PamModule<T> for MyPamModule {
 ///     fn authenticate(handle: &mut T, args: Vec<&CStr>, flags: Flags) -> PamResult<()> {
 ///         let password = handle.get_authtok(Some("what's your password?"))?;
-///         let response = format!("If you say your password is {password:?}, who am I to disagree?");
+///         let response =
+///             format!("If you say your password is {password:?}, who am I to disagree?");
 ///         handle.info_msg(&response);
 ///         Ok(())
 ///     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/libpam/question.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -0,0 +1,374 @@
+//! Data and types dealing with PAM messages.
+
+use crate::constants::InvalidEnum;
+use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA};
+use crate::libpam::conversation::OwnedMessage;
+use crate::libpam::memory;
+use crate::libpam::memory::{CBinaryData, Immovable};
+use crate::ErrorCode;
+use crate::Result;
+use num_derive::FromPrimitive;
+use num_traits::FromPrimitive;
+use std::ffi::{c_int, c_void, CStr};
+use std::result::Result as StdResult;
+use std::{iter, ptr, slice};
+
+/// Abstraction of a collection of questions to be sent in a PAM conversation.
+///
+/// The PAM C API conversation function looks like this:
+///
+/// ```c
+/// int pam_conv(
+///     int count,
+///     const struct pam_message **questions,
+///     struct pam_response **answers,
+///     void *appdata_ptr,
+/// )
+/// ```
+///
+/// On Linux-PAM and other compatible implementations, `questions`
+/// is treated as a pointer-to-pointers, like `int argc, char **argv`.
+/// (In this situation, the value of `Questions.indirect` is
+/// the pointer passed to `pam_conv`.)
+///
+/// ```text
+/// ╔═ Questions ═╗  points to  ┌─ Indirect ─┐       ╔═ Question ═╗
+/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style      ║
+/// ║ count       ║             │ base[1] ┄┄┄┼┄┄┄╮   ║ data ┄┄┄┄┄┄╫┄┄> ...
+/// ╚═════════════╝             │ ...        │   ┆   ╚════════════╝
+///                                              ┆
+///                                              ┆    ╔═ Question ═╗
+///                                              ╰┄┄> ║ style      ║
+///                                                   ║ data ┄┄┄┄┄┄╫┄┄> ...
+///                                                   ╚════════════╝
+/// ```
+///
+/// On OpenPAM and other compatible implementations (like Solaris),
+/// `messages` is a pointer-to-pointer-to-array.  This appears to be
+/// the correct implementation as required by the XSSO specification.
+///
+/// ```text
+/// ╔═ Questions ═╗  points to  ┌─ Indirect ─┐       ╔═ Question[] ═╗
+/// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style        ║
+/// ║ count       ║             └────────────┘       ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
+/// ╚═════════════╝                                  ╟──────────────╢
+///                                                  ║ style        ║
+///                                                  ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
+///                                                  ╟──────────────╢
+///                                                  ║ ...          ║
+/// ```
+///
+/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
+pub struct Questions {
+    /// An indirection to the questions themselves, stored on the C heap.
+    indirect: *mut Indirect,
+    /// The number of questions.
+    count: usize,
+}
+
+impl Questions {
+    /// Stores the provided questions on the C heap.
+    pub fn new(messages: &[Message]) -> Result<Self> {
+        let count = messages.len();
+        let mut ret = Self {
+            indirect: Indirect::alloc(count),
+            count,
+        };
+        // Even if we fail partway through this, all our memory will be freed.
+        for (question, message) in iter::zip(ret.iter_mut(), messages) {
+            question.fill(message)?
+        }
+        Ok(ret)
+    }
+
+    /// The pointer to the thing with the actual list.
+    pub fn indirect(&self) -> *const Indirect {
+        self.indirect
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = &Question> {
+        // SAFETY: we're iterating over an amount we know.
+        unsafe { (*self.indirect).iter(self.count) }
+    }
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
+        // SAFETY: we're iterating over an amount we know.
+        unsafe { (*self.indirect).iter_mut(self.count) }
+    }
+}
+
+impl Drop for Questions {
+    fn drop(&mut self) {
+        // SAFETY: We are valid and have a valid pointer.
+        // Once we're done, everything will be safe.
+        unsafe {
+            if let Some(indirect) = self.indirect.as_mut() {
+                indirect.free(self.count)
+            }
+            memory::free(self.indirect);
+            self.indirect = ptr::null_mut();
+        }
+    }
+}
+
+/// An indirect reference to messages.
+///
+/// This is kept separate to provide a place where we can separate
+/// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
+#[repr(transparent)]
+pub struct Indirect {
+    base: [*mut Question; 0],
+    _marker: Immovable,
+}
+
+impl Indirect {
+    /// Allocates memory for this indirector and all its members.
+    fn alloc(count: usize) -> *mut Self {
+        // SAFETY: We're only allocating, and when we're done,
+        // everything will be in a known-good state.
+        let me_ptr: *mut Indirect = memory::calloc::<Question>(count).cast();
+        unsafe {
+            let me = &mut *me_ptr;
+            let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count);
+            for entry in ptr_list {
+                *entry = memory::calloc(1);
+            }
+            me
+        }
+    }
+
+    /// Returns an iterator yielding the given number of messages.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide the right count.
+    pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
+        (0..count).map(|idx| &**self.base.as_ptr().add(idx))
+    }
+
+    /// Returns a mutable iterator yielding the given number of messages.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide the right count.
+    pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
+        (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx))
+    }
+
+    /// Frees everything this points to.
+    ///
+    /// # Safety
+    ///
+    /// You have to pass the right size.
+    unsafe fn free(&mut self, count: usize) {
+        let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count);
+        for msg in msgs {
+            if let Some(msg) = msg.as_mut() {
+                msg.clear();
+            }
+            memory::free(*msg);
+            *msg = ptr::null_mut();
+        }
+    }
+}
+
+/// The C enum values for messages shown to the user.
+#[derive(Debug, PartialEq, FromPrimitive)]
+pub enum Style {
+    /// Requests information from the user; will be masked when typing.
+    PromptEchoOff = 1,
+    /// Requests information from the user; will not be masked.
+    PromptEchoOn = 2,
+    /// An error message.
+    ErrorMsg = 3,
+    /// An informational message.
+    TextInfo = 4,
+    /// Yes/No/Maybe conditionals. A Linux-PAM extension.
+    RadioType = 5,
+    /// For server–client non-human interaction.
+    ///
+    /// NOT part of the X/Open PAM specification.
+    /// A Linux-PAM extension.
+    BinaryPrompt = 7,
+}
+
+impl TryFrom<c_int> for Style {
+    type Error = InvalidEnum<Self>;
+    fn try_from(value: c_int) -> StdResult<Self, Self::Error> {
+        Self::from_i32(value).ok_or(value.into())
+    }
+}
+
+impl From<Style> for c_int {
+    fn from(val: Style) -> Self {
+        val as Self
+    }
+}
+
+/// A question sent by PAM or a module to an application.
+///
+/// PAM refers to this as a "message", but we call it a question
+/// to avoid confusion with [`Message`].
+///
+/// This question, and its internal data, is owned by its creator
+/// (either the module or PAM itself).
+#[repr(C)]
+pub struct Question {
+    /// The style of message to request.
+    style: c_int,
+    /// A description of the data requested.
+    ///
+    /// For most requests, this will be an owned [`CStr`], but for requests
+    /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`]
+    /// (a Linux-PAM extension).
+    data: *mut c_void,
+    _marker: Immovable,
+}
+
+impl Question {
+    /// Replaces the contents of this question with the question
+    /// from the message.
+    pub fn fill(&mut self, msg: &Message) -> Result<()> {
+        let (style, data) = copy_to_heap(msg)?;
+        self.clear();
+        self.style = style as c_int;
+        self.data = data;
+        Ok(())
+    }
+
+    /// Gets this message's data pointer as a string.
+    ///
+    /// # Safety
+    ///
+    /// It's up to you to pass this only on types with a string value.
+    unsafe fn string_data(&self) -> Result<&str> {
+        if self.data.is_null() {
+            Ok("")
+        } else {
+            CStr::from_ptr(self.data.cast())
+                .to_str()
+                .map_err(|_| ErrorCode::ConversationError)
+        }
+    }
+
+    /// Gets this message's data pointer as borrowed binary data.
+    unsafe fn binary_data(&self) -> (&[u8], u8) {
+        self.data
+            .cast::<CBinaryData>()
+            .as_ref()
+            .map(Into::into)
+            .unwrap_or_default()
+    }
+
+    /// Zeroes out the data stored here.
+    fn clear(&mut self) {
+        // SAFETY: We either created this data or we got it from PAM.
+        // After this function is done, it will be zeroed out.
+        unsafe {
+            if let Ok(style) = Style::try_from(self.style) {
+                match style {
+                    Style::BinaryPrompt => {
+                        if let Some(d) = self.data.cast::<CBinaryData>().as_mut() {
+                            d.zero_contents()
+                        }
+                    }
+                    Style::TextInfo
+                    | Style::RadioType
+                    | Style::ErrorMsg
+                    | Style::PromptEchoOff
+                    | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()),
+                }
+            };
+            memory::free(self.data);
+            self.data = ptr::null_mut();
+        }
+    }
+}
+
+impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> {
+    type Error = ErrorCode;
+    fn try_from(question: &'a Question) -> Result<Self> {
+        let style: Style = question
+            .style
+            .try_into()
+            .map_err(|_| ErrorCode::ConversationError)?;
+        // SAFETY: In all cases below, we're matching the
+        let prompt = unsafe {
+            match style {
+                Style::PromptEchoOff => {
+                    Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?))
+                }
+                Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)),
+                Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)),
+                Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)),
+                Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)),
+                Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())),
+            }
+        };
+        Ok(prompt)
+    }
+}
+
+/// Copies the contents of this message to the C heap.
+fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> {
+    let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
+    match *msg {
+        Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
+        Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
+        Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
+        Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
+        Message::Info(p) => alloc(Style::TextInfo, p.question()),
+        Message::BinaryPrompt(p) => {
+            let q = p.question();
+            Ok((
+                Style::BinaryPrompt,
+                CBinaryData::alloc(q)?.cast(),
+            ))
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+
+    use super::{MaskedQAndA, Questions, Result};
+    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA};
+    use crate::libpam::conversation::OwnedMessage;
+
+    #[test]
+    fn test_round_trip() {
+        let interrogation = Questions::new(&[
+            MaskedQAndA::new("hocus pocus").message(),
+            BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
+            QAndA::new("what").message(),
+            QAndA::new("who").message(),
+            InfoMsg::new("hey").message(),
+            ErrorMsg::new("gasp").message(),
+            RadioQAndA::new("you must choose").message(),
+        ])
+        .unwrap();
+        let indirect = interrogation.indirect();
+
+        let remade = unsafe { indirect.as_ref() }.unwrap();
+        let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
+            .map(TryInto::try_into)
+            .collect::<Result<_>>()
+            .unwrap();
+        let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap();
+        macro_rules! assert_matches {
+            ($id:ident => $variant:path, $q:expr) => {
+                if let $variant($id) = $id {
+                    assert_eq!($q, $id.question());
+                } else {
+                    panic!("mismatched enum variant {x:?}", x = $id);
+                }
+            };
+        }
+        assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
+        assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
+        assert_matches!(what => OwnedMessage::Prompt, "what");
+        assert_matches!(who => OwnedMessage::Prompt, "who");
+        assert_matches!(hey => OwnedMessage::Info, "hey");
+        assert_matches!(gasp => OwnedMessage::Error, "gasp");
+        assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
+    }
+}
--- a/src/libpam/response.rs	Sun Jun 08 01:03:46 2025 -0400
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,327 +0,0 @@
-//! Types used to communicate data from the application to the module.
-
-use crate::conv::BorrowedBinaryData;
-use crate::libpam::conversation::OwnedMessage;
-use crate::libpam::memory;
-use crate::libpam::memory::{CBinaryData, Immovable};
-use crate::{ErrorCode, Result};
-use std::ffi::{c_int, c_void, CStr};
-use std::ops::{Deref, DerefMut};
-use std::{iter, mem, ptr, slice};
-
-#[repr(transparent)]
-#[derive(Debug)]
-pub struct TextAnswer(Answer);
-
-impl TextAnswer {
-    /// Interprets the provided `Answer` as a text answer.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to provide an answer that is a `TextAnswer`.
-    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
-        // SAFETY: We're provided a valid reference.
-        &mut *(from as *mut Answer).cast::<Self>()
-    }
-
-    /// Converts the `Answer` to a `TextAnswer` with the given text.
-    fn fill(dest: &mut Answer, text: &str) -> Result<()> {
-        let allocated = memory::malloc_str(text)?;
-        dest.free_contents();
-        dest.data = allocated.cast();
-        Ok(())
-    }
-
-    /// Gets the string stored in this answer.
-    pub fn contents(&self) -> Result<&str> {
-        if self.0.data.is_null() {
-            Ok("")
-        } else {
-            // SAFETY: This data is either passed from PAM (so we are forced
-            // to trust it) or was created by us in TextAnswerInner::alloc.
-            // In either case, it's going to be a valid null-terminated string.
-            unsafe { CStr::from_ptr(self.0.data.cast()) }
-                .to_str()
-                .map_err(|_| ErrorCode::ConversationError)
-        }
-    }
-
-    /// Zeroes out the answer data, frees it, and points our data to `null`.
-    ///
-    /// When this `TextAnswer` is part of an [`Answers`],
-    /// this is optional (since that will perform the `free`),
-    /// but it will clear potentially sensitive data.
-    pub fn free_contents(&mut self) {
-        // SAFETY: We own this data and know it's valid.
-        // If it's null, this is a no-op.
-        // After we're done, it will be null.
-        unsafe {
-            memory::zero_c_string(self.0.data);
-            libc::free(self.0.data);
-            self.0.data = ptr::null_mut()
-        }
-    }
-}
-
-/// A [`Answer`] with [`CBinaryData`] in it.
-#[repr(transparent)]
-#[derive(Debug)]
-pub struct BinaryAnswer(Answer);
-
-impl BinaryAnswer {
-    /// Interprets the provided [`Answer`] as a binary answer.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to provide an answer that is a `BinaryAnswer`.
-    pub unsafe fn upcast(from: &mut Answer) -> &mut Self {
-        // SAFETY: We're provided a valid reference.
-        &mut *(from as *mut Answer).cast::<Self>()
-    }
-
-    /// Fills in a [`Answer`] with the provided binary data.
-    ///
-    /// The `data_type` is a tag you can use for whatever.
-    /// It is passed through PAM unchanged.
-    ///
-    /// The referenced data is copied to the C heap.
-    /// We do not take ownership of the original data.
-    pub fn fill(dest: &mut Answer, data: BorrowedBinaryData) -> Result<()> {
-        let allocated = CBinaryData::alloc(data.data(), data.data_type())?;
-        dest.free_contents();
-        dest.data = allocated.cast();
-        Ok(())
-    }
-
-    /// Gets the binary data in this answer.
-    pub fn data(&self) -> Option<&CBinaryData> {
-        // SAFETY: We either got this data from PAM or allocated it ourselves.
-        // Either way, we trust that it is either valid data or null.
-        unsafe { self.0.data.cast::<CBinaryData>().as_ref() }
-    }
-
-    /// Zeroes out the answer data, frees it, and points our data to `null`.
-    ///
-    /// When this `TextAnswer` is part of an [`Answers`],
-    /// this is optional (since that will perform the `free`),
-    /// but it will clear potentially sensitive data.
-    pub fn zero_contents(&mut self) {
-        // SAFETY: We know that our data pointer is either valid or null.
-        // Once we're done, it's null and the answer is safe.
-        unsafe {
-            let data_ref = self.0.data.cast::<CBinaryData>().as_mut();
-            if let Some(d) = data_ref {
-                d.zero_contents()
-            }
-            libc::free(self.0.data);
-            self.0.data = ptr::null_mut()
-        }
-    }
-}
-
-/// Generic version of answer data.
-///
-/// This has the same structure as [`BinaryAnswer`]
-/// and [`TextAnswer`].
-#[repr(C)]
-#[derive(Debug)]
-pub struct Answer {
-    /// Pointer to the data returned in an answer.
-    /// For most answers, this will be a [`CStr`], but for answers to
-    /// [`MessageStyle::BinaryPrompt`]s, this will be [`CBinaryData`]
-    /// (a Linux-PAM extension).
-    data: *mut c_void,
-    /// Unused.
-    return_code: c_int,
-    _marker: Immovable,
-}
-
-impl Answer {
-    /// Frees the contents of this answer.
-    ///
-    /// After this is done, this answer's `data` will be `null`,
-    /// which is a valid (empty) state.
-    fn free_contents(&mut self) {
-        // SAFETY: We have either an owned valid pointer, or null.
-        // We can free our owned pointer, and `free(null)` is a no-op.
-        unsafe {
-            libc::free(self.data);
-            self.data = ptr::null_mut();
-        }
-    }
-}
-
-/// An owned, contiguous block of [`Answer`]s.
-#[derive(Debug)]
-pub struct Answers {
-    base: *mut Answer,
-    count: usize,
-}
-
-impl Answers {
-    /// Allocates an owned list of answers on the C heap.
-    fn alloc(count: usize) -> Self {
-        Answers {
-            // SAFETY: We are doing allocation here.
-            base: unsafe { libc::calloc(count, size_of::<Answer>()) }.cast(),
-            count,
-        }
-    }
-
-    pub fn build(value: Vec<OwnedMessage>) -> Result<Self> {
-        let mut outputs = Answers::alloc(value.len());
-        // Even if we fail during this process, we still end up freeing
-        // all allocated answer memory.
-        for (input, output) in iter::zip(value, outputs.iter_mut()) {
-            match input {
-                OwnedMessage::MaskedPrompt(p) => TextAnswer::fill(output, p.answer()?.unsecure())?,
-                OwnedMessage::Prompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
-                OwnedMessage::BinaryPrompt(p) => BinaryAnswer::fill(output, (&p.answer()?).into())?,
-                OwnedMessage::Error(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
-                OwnedMessage::Info(p) => TextAnswer::fill(output, p.answer().map(|_| "")?)?,
-                OwnedMessage::RadioPrompt(p) => TextAnswer::fill(output, &(p.answer()?))?,
-            }
-        }
-        Ok(outputs)
-    }
-
-    /// Converts this into a `*Answer` for passing to PAM.
-    ///
-    /// The pointer "owns" its own data (i.e., this will not be dropped).
-    pub fn into_ptr(self) -> *mut Answer {
-        let ret = self.base;
-        mem::forget(self);
-        ret
-    }
-
-    /// Takes ownership of a list of answers allocated on the C heap.
-    ///
-    /// # Safety
-    ///
-    /// It's up to you to make sure you pass a valid pointer.
-    pub unsafe fn from_c_heap(base: *mut Answer, count: usize) -> Self {
-        Answers { base, count }
-    }
-}
-
-impl Deref for Answers {
-    type Target = [Answer];
-    fn deref(&self) -> &Self::Target {
-        // SAFETY: This is the memory we manage ourselves.
-        unsafe { slice::from_raw_parts(self.base, self.count) }
-    }
-}
-
-impl DerefMut for Answers {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        // SAFETY: This is the memory we manage ourselves.
-        unsafe { slice::from_raw_parts_mut(self.base, self.count) }
-    }
-}
-
-impl Drop for Answers {
-    fn drop(&mut self) {
-        // SAFETY: We allocated this ourselves, or it was provided to us by PAM.
-        unsafe {
-            for answer in self.iter_mut() {
-                answer.free_contents()
-            }
-            libc::free(self.base.cast())
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::{Answers, BinaryAnswer, TextAnswer, BorrowedBinaryData};
-    use crate::BinaryData;
-    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, QAndA, RadioQAndA};
-    use crate::libpam::conversation::OwnedMessage;
-
-    #[test]
-    fn test_round_trip() {
-        let binary_msg = {
-            let qa = BinaryQAndA::new(&[], 0);
-            qa.set_answer(Ok(BinaryData::new(vec![1, 2, 3], 99)));
-            OwnedMessage::BinaryPrompt(qa)
-        };
-
-        macro_rules! answered {
-        ($typ:ty, $msg:path, $data:expr) => {
-            {let qa = <$typ>::new("");
-            qa.set_answer(Ok($data)); $msg(qa)}
-        }
-    }
-
-
-        let answers = vec![
-            binary_msg,
-            answered!(QAndA, OwnedMessage::Prompt, "whats going on".to_owned()),
-            answered!(MaskedQAndA, OwnedMessage::MaskedPrompt, "well then".into()),
-            answered!(ErrorMsg, OwnedMessage::Error, ()),
-            answered!(InfoMsg, OwnedMessage::Info, ()),
-            answered!(RadioQAndA, OwnedMessage::RadioPrompt, "beep boop".to_owned()),
-        ];
-        let n = answers.len();
-        let sent = Answers::build(answers).unwrap();
-        let heap_answers = sent.into_ptr();
-        let mut received = unsafe { Answers::from_c_heap(heap_answers, n) };
-
-        let assert_text = |want, raw| {
-            let up = unsafe { TextAnswer::upcast(raw) };
-            assert_eq!(want, up.contents().unwrap());
-            up.free_contents();
-            assert_eq!("", up.contents().unwrap());
-        };
-        let assert_bin = |want_data: &[u8], want_type, raw| {
-            let up = unsafe { BinaryAnswer::upcast(raw) };
-            assert_eq!(BinaryData::new(want_data.into(), want_type), up.data().into());
-            up.zero_contents();
-            assert_eq!(BinaryData::default(), up.data().into());
-        };
-        if let [zero, one, two, three, four, five] = &mut received[..] {
-            assert_bin(&[1, 2, 3], 99, zero);
-            assert_text("whats going on", one);
-            assert_text("well then", two);
-            assert_text("", three);
-            assert_text("", four);
-            assert_text("beep boop", five);
-        } else {
-            panic!("received wrong size {len}!", len = received.len())
-        }
-    }
-
-    #[test]
-    fn test_text_answer() {
-        let mut answers = Answers::alloc(2);
-        let zeroth = &mut answers[0];
-        TextAnswer::fill(zeroth, "hello").unwrap();
-        let zeroth_text = unsafe { TextAnswer::upcast(zeroth) };
-        let data = zeroth_text.contents().expect("valid");
-        assert_eq!("hello", data);
-        zeroth_text.free_contents();
-        zeroth_text.free_contents();
-        TextAnswer::fill(&mut answers[1], "hell\0").expect_err("should error; contains nul");
-    }
-
-    #[test]
-    fn test_binary_answer() {
-        let mut answers = Answers::alloc(1);
-        let real_data = BinaryData::new(vec![1, 2, 3, 4, 5, 6, 7, 8], 9);
-        let answer = &mut answers[0];
-        BinaryAnswer::fill(answer, (&real_data).into()).expect("alloc should succeed");
-        let bin_answer = unsafe { BinaryAnswer::upcast(answer) };
-        assert_eq!(real_data, bin_answer.data().into());
-        answer.free_contents();
-        answer.free_contents();
-    }
-
-    #[test]
-    #[ignore]
-    fn test_binary_answer_too_big() {
-        let big_data: Vec<u8> = vec![0xFFu8; 10_000_000_000];
-        let mut answers = Answers::alloc(1);
-        BinaryAnswer::fill(&mut answers[0], BorrowedBinaryData::new(&big_data, 100))
-            .expect_err("this is too big!");
-    }
-}
--- a/src/module.rs	Sun Jun 08 01:03:46 2025 -0400
+++ b/src/module.rs	Sun Jun 08 03:48:40 2025 -0400
@@ -28,7 +28,8 @@
     ///
     /// This is probably the first thing you want to implement.
     /// In most cases, you will want to get the user and password,
-    /// using [`PamHandle::get_user`] and [`PamModuleOnly::get_authtok`],
+    /// using [`PamShared::get_user`](crate::PamShared::get_user)
+    /// and [`PamModuleOnly::get_authtok`](crate::handle::PamModuleOnly::get_authtok),
     /// and verify them against something.
     ///
     /// See [the Module Writer's Guide entry for `pam_sm_authenticate`][mwg]