diff src/libpam/question.rs @ 80:5aa1a010f1e8

Start using PAM headers; improve owned/borrowed distinction. - Uses bindgen to generate bindings (only if needed). - Gets the story together on owned vs. borrowed handles. - Reduces number of mutable borrows in handle operation (since `PamHandle` is neither `Send` nor `Sync`, we never have to worry about thread safety. - Improves a bunch of macros so we don't have our own special syntax for docs. - Implement question indirection for standard XSSO PAM implementations.
author Paul Fisher <paul@pfish.zone>
date Tue, 10 Jun 2025 01:09:30 -0400
parents 2128123b9406
children 5e14bb093851
line wrap: on
line diff
--- a/src/libpam/question.rs	Sun Jun 08 04:21:58 2025 -0400
+++ b/src/libpam/question.rs	Tue Jun 10 01:09:30 2025 -0400
@@ -1,16 +1,13 @@
 //! 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};
+pub use crate::libpam::pam_ffi::{Question, Style};
 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::ffi::{c_void, CStr};
 use std::{iter, ptr, slice};
 
 /// Abstraction of a collection of questions to be sent in a PAM conversation.
@@ -57,21 +54,19 @@
 ///                                                  ╟──────────────╢
 ///                                                  ║ ...          ║
 /// ```
-///
-/// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.***
-pub struct Questions {
+pub struct GenericQuestions<I: IndirectTrait> {
     /// An indirection to the questions themselves, stored on the C heap.
-    indirect: *mut Indirect,
+    indirect: *mut I,
     /// The number of questions.
     count: usize,
 }
 
-impl Questions {
+impl<I: IndirectTrait> GenericQuestions<I> {
     /// 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),
+            indirect: I::alloc(count),
             count,
         };
         // Even if we fail partway through this, all our memory will be freed.
@@ -82,8 +77,8 @@
     }
 
     /// The pointer to the thing with the actual list.
-    pub fn indirect(&self) -> *const Indirect {
-        self.indirect
+    pub fn indirect(&self) -> *const *const Question {
+        self.indirect.cast()
     }
 
     pub fn iter(&self) -> impl Iterator<Item = &Question> {
@@ -96,13 +91,13 @@
     }
 }
 
-impl Drop for Questions {
+impl<I: IndirectTrait> Drop for GenericQuestions<I> {
     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)
+                indirect.free_contents(self.count)
             }
             memory::free(self.indirect);
             self.indirect = ptr::null_mut();
@@ -110,56 +105,131 @@
     }
 }
 
+/// The trait that each of the `Indirect` implementations implement.
+///
+/// Basically a slice but with more meat.
+pub trait IndirectTrait {
+    /// Converts a pointer into a borrowed `Self`.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide a valid pointer.
+    unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self>
+    where
+        Self: Sized,
+    {
+        ptr.cast::<Self>().as_ref()
+    }
+
+    /// Allocates memory for this indirector and all its members.
+    fn alloc(count: usize) -> *mut Self;
+
+    /// Returns an iterator yielding the given number of messages.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide the right count.
+    unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>;
+
+    /// Returns a mutable iterator yielding the given number of messages.
+    ///
+    /// # Safety
+    ///
+    /// You have to provide the right count.
+    unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>;
+
+    /// Frees everything this points to.
+    ///
+    /// # Safety
+    ///
+    /// You have to pass the right size.
+    unsafe fn free_contents(&mut self, count: usize);
+}
+
+/// 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.
+#[cfg(pam_impl = "linux-pam")]
+pub type Indirect = LinuxPamIndirect;
+
 /// 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.
+#[cfg(not(pam_impl = "linux-pam"))]
+pub type Indirect = XssoIndirect;
+
+pub type Questions = GenericQuestions<Indirect>;
+
+/// The XSSO standard version of the indirection layer between Question and Questions.
 #[repr(transparent)]
-pub struct Indirect {
+pub struct StandardIndirect {
+    base: *mut Question,
+    _marker: Immovable,
+}
+
+impl IndirectTrait for StandardIndirect {
+    fn alloc(count: usize) -> *mut Self {
+        let questions = memory::calloc(count);
+        let me_ptr: *mut Self = memory::calloc(1);
+        // SAFETY: We just allocated this, and we're putting a valid pointer in.
+        unsafe {
+            let me = &mut *me_ptr;
+            me.base = questions;
+        }
+        me_ptr
+    }
+
+    unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> {
+        (0..count).map(|idx| &*self.base.add(idx))
+    }
+
+    unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> {
+        (0..count).map(|idx| &mut *self.base.add(idx))
+    }
+
+    unsafe fn free_contents(&mut self, count: usize) {
+        let msgs = slice::from_raw_parts_mut(self.base, count);
+        for msg in msgs {
+            msg.clear()
+        }
+        memory::free(self.base);
+        self.base = ptr::null_mut()
+    }
+}
+
+/// The Linux version of the indirection layer between Question and Questions.
+#[repr(transparent)]
+pub struct LinuxPamIndirect {
     base: [*mut Question; 0],
     _marker: Immovable,
 }
 
-impl Indirect {
-    /// Allocates memory for this indirector and all its members.
+impl IndirectTrait for LinuxPamIndirect {
     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();
+        let me_ptr: *mut Self = memory::calloc::<*mut 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
         }
+        me_ptr
     }
 
-    /// 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> {
+    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> {
+    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) {
+    unsafe fn free_contents(&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() {
@@ -171,66 +241,23 @@
     }
 }
 
-/// 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 Default for Question {
+    fn default() -> Self {
+        Self {
+            style: Default::default(),
+            data: ptr::null_mut(),
+            _marker: Default::default(),
+        }
     }
 }
 
-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.style = style.into();
         self.data = data;
         Ok(())
     }
@@ -327,45 +354,52 @@
 #[cfg(test)]
 mod tests {
 
-    use super::{MaskedQAndA, Questions, Result};
-    use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA};
-    use crate::libpam::conversation::OwnedMessage;
+    use super::{
+        BinaryQAndA, ErrorMsg, GenericQuestions, IndirectTrait, InfoMsg, LinuxPamIndirect,
+        MaskedQAndA, OwnedMessage, QAndA, RadioQAndA, Result, StandardIndirect,
+    };
 
-    #[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();
+    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);
+            }
+        };
+    }
 
-        let remade = unsafe { indirect.as_ref() }.unwrap();
-        let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) }
-            .map(TryInto::try_into)
-            .collect::<Result<_>>()
+    macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
+        #[test]
+        fn $fn_name() {
+            let interrogation = GenericQuestions::<$typ>::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 [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);
-                }
-            };
+            let indirect = interrogation.indirect();
+
+            let remade = unsafe { $typ::borrow_ptr(indirect) }.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();
+            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");
         }
-        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");
-    }
+    }}
+
+    tests!(test_xsso<StandardIndirect>);
+    tests!(test_linux<LinuxPamIndirect>);
 }