diff src/libpam/question.rs @ 130:80c07e5ab22f

Transfer over (almost) completely to using libpam-sys. This reimplements everything in nonstick on top of the new -sys crate. We don't yet use libpam-sys's helpers for binary message payloads. Soon.
author Paul Fisher <paul@pfish.zone>
date Tue, 01 Jul 2025 06:11:43 -0400
parents 178310336596
children
line wrap: on
line diff
--- a/src/libpam/question.rs	Mon Jun 30 23:49:54 2025 -0400
+++ b/src/libpam/question.rs	Tue Jul 01 06:11:43 2025 -0400
@@ -2,177 +2,22 @@
 
 #[cfg(feature = "linux-pam-ext")]
 use crate::conv::{BinaryQAndA, RadioQAndA};
-use crate::conv::{ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA};
-use crate::libpam::conversation::OwnedMessage;
-use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString, Immovable};
-use crate::libpam::pam_ffi;
-pub use crate::libpam::pam_ffi::Question;
+use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA};
+use crate::libpam::conversation::OwnedExchange;
+use crate::libpam::memory::{CBinaryData, CHeapBox, CHeapString};
 use crate::ErrorCode;
 use crate::Result;
 use num_enum::{IntoPrimitive, TryFromPrimitive};
-use std::cell::Cell;
-use std::ffi::{c_void, CStr};
-use std::pin::Pin;
-use std::{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
-///            points to  ┌───────────────┐      ╔═ Question ═╗
-/// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style      ║
-///                       │ questions[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
-///            points to  ┌─────────────┐       ╔═ Question[] ═╗
-/// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style        ║
-///                       └─────────────┘       ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
-///                                             ╟──────────────╢
-///                                             ║ style        ║
-///                                             ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
-///                                             ╟──────────────╢
-///                                             ║ ...          ║
-/// ```
-pub trait QuestionsTrait {
-    /// Allocates memory for this indirector and all its members.
-    fn new(messages: &[Message]) -> Result<Self>
-    where
-        Self: Sized;
-
-    /// Gets the pointer that is passed .
-    fn ptr(self: Pin<&Self>) -> *const *const Question;
-
-    /// Converts a pointer into a borrowed list of Questions.
-    ///
-    /// # Safety
-    ///
-    /// You have to provide a valid pointer.
-    unsafe fn borrow_ptr<'a>(
-        ptr: *const *const Question,
-        count: usize,
-    ) -> impl Iterator<Item = &'a Question>;
-}
-
-#[cfg(pam_impl = "linux-pam")]
-pub type Questions = LinuxPamQuestions;
-
-#[cfg(not(pam_impl = "linux-pam"))]
-pub type Questions = XSsoQuestions;
+use std::ffi::{c_int, c_void, CStr};
 
-/// The XSSO standard version of the pointer train to questions.
-#[derive(Debug)]
-#[repr(C)]
-pub struct XSsoQuestions {
-    /// Points to the memory address where the meat of `questions` is.
-    /// **The memory layout of Vec is not specified**, and we need to return
-    /// a pointer to the pointer, hence we have to store it here.
-    pointer: Cell<*const Question>,
-    questions: Vec<Question>,
-    _marker: Immovable,
-}
-
-impl XSsoQuestions {
-    fn len(&self) -> usize {
-        self.questions.len()
-    }
-    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
-        self.questions.iter_mut()
-    }
-}
-
-impl QuestionsTrait for XSsoQuestions {
-    fn new(messages: &[Message]) -> Result<Self> {
-        let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect();
-        let questions = questions?;
-        Ok(Self {
-            pointer: Cell::new(ptr::null()),
-            questions,
-            _marker: Default::default(),
-        })
-    }
-
-    fn ptr(self: Pin<&Self>) -> *const *const Question {
-        let me = self.get_ref();
-        me.pointer.set(self.questions.as_ptr());
-        me.pointer.as_ptr()
-    }
-
-    unsafe fn borrow_ptr<'a>(
-        ptr: *const *const Question,
-        count: usize,
-    ) -> impl Iterator<Item = &'a Question> {
-        slice::from_raw_parts(*ptr, count).iter()
-    }
-}
-
-/// The Linux version of the pointer train to questions.
-#[derive(Debug)]
-#[repr(C)]
-pub struct LinuxPamQuestions {
-    #[allow(clippy::vec_box)] // we need to box vec items.
-    /// The place where the questions are.
-    questions: Vec<Box<Question>>,
-}
-
-impl LinuxPamQuestions {
-    fn len(&self) -> usize {
-        self.questions.len()
-    }
-
-    fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
-        self.questions.iter_mut().map(AsMut::as_mut)
-    }
-}
-
-impl QuestionsTrait for LinuxPamQuestions {
-    fn new(messages: &[Message]) -> Result<Self> {
-        let questions: Result<_> = messages
-            .iter()
-            .map(|msg| Question::try_from(msg).map(Box::new))
-            .collect();
-        Ok(Self {
-            questions: questions?,
-        })
-    }
-
-    fn ptr(self: Pin<&Self>) -> *const *const Question {
-        self.questions.as_ptr().cast()
-    }
-
-    unsafe fn borrow_ptr<'a>(
-        ptr: *const *const Question,
-        count: usize,
-    ) -> impl Iterator<Item = &'a Question> {
-        slice::from_raw_parts(ptr.cast::<&Question>(), count)
-            .iter()
-            .copied()
-    }
+mod style_const {
+    pub use libpam_sys::*;
+    #[cfg(not(feature = "link"))]
+    #[cfg_pam_impl(not("LinuxPam"))]
+    pub const PAM_RADIO_TYPE: i32 = 897;
+    #[cfg(not(feature = "link"))]
+    #[cfg_pam_impl(not("LinuxPam"))]
+    pub const PAM_BINARY_PROMPT: i32 = 10010101;
 }
 
 /// The C enum values for messages shown to the user.
@@ -180,22 +25,42 @@
 #[repr(i32)]
 enum Style {
     /// Requests information from the user; will be masked when typing.
-    PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF,
+    PromptEchoOff = style_const::PAM_PROMPT_ECHO_OFF,
     /// Requests information from the user; will not be masked.
-    PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON,
+    PromptEchoOn = style_const::PAM_PROMPT_ECHO_ON,
     /// An error message.
-    ErrorMsg = pam_ffi::PAM_ERROR_MSG,
+    ErrorMsg = style_const::PAM_ERROR_MSG,
     /// An informational message.
-    TextInfo = pam_ffi::PAM_TEXT_INFO,
+    TextInfo = style_const::PAM_TEXT_INFO,
     /// Yes/No/Maybe conditionals. A Linux-PAM extension.
     #[cfg(feature = "linux-pam-ext")]
-    RadioType = pam_ffi::PAM_RADIO_TYPE,
+    RadioType = style_const::PAM_RADIO_TYPE,
     /// For server–client non-human interaction.
     ///
     /// NOT part of the X/Open PAM specification.
     /// A Linux-PAM extension.
     #[cfg(feature = "linux-pam-ext")]
-    BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT,
+    BinaryPrompt = style_const::PAM_BINARY_PROMPT,
+}
+
+/// 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`](crate::conv::Exchange).
+///
+/// This question, and its internal data, is owned by its creator
+/// (either the module or PAM itself).
+#[repr(C)]
+#[derive(Debug)]
+pub struct Question {
+    /// The style of message to request.
+    pub style: c_int,
+    /// A description of the data requested.
+    ///
+    /// For most requests, this will be an owned [`CStr`],
+    /// but for requests with style `PAM_BINARY_PROMPT`,
+    /// this will be `CBinaryData` (a Linux-PAM extension).
+    pub data: Option<CHeapBox<c_void>>,
 }
 
 impl Question {
@@ -222,9 +87,9 @@
     }
 }
 
-impl TryFrom<&Message<'_>> for Question {
+impl TryFrom<&Exchange<'_>> for Question {
     type Error = ErrorCode;
-    fn try_from(msg: &Message) -> Result<Self> {
+    fn try_from(msg: &Exchange) -> Result<Self> {
         let alloc = |style, text| -> Result<_> {
             Ok((style, unsafe {
                 CHeapBox::cast(CHeapString::new(text)?.into_box())
@@ -232,18 +97,20 @@
         };
         // We will only allocate heap data if we have a valid input.
         let (style, data): (_, CHeapBox<c_void>) = match *msg {
-            Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
-            Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
-            Message::Error(p) => alloc(Style::ErrorMsg, p.question()),
-            Message::Info(p) => alloc(Style::TextInfo, p.question()),
+            Exchange::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
+            Exchange::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
+            Exchange::Error(p) => alloc(Style::ErrorMsg, p.question()),
+            Exchange::Info(p) => alloc(Style::TextInfo, p.question()),
             #[cfg(feature = "linux-pam-ext")]
-            Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
+            Exchange::RadioPrompt(p) => alloc(Style::RadioType, p.question()),
             #[cfg(feature = "linux-pam-ext")]
-            Message::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe {
+            Exchange::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe {
                 CHeapBox::cast(CBinaryData::alloc(p.question())?)
             })),
             #[cfg(not(feature = "linux-pam-ext"))]
-            Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError),
+            Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => {
+                Err(ErrorCode::ConversationError)
+            }
         }?;
         Ok(Self {
             style: style.into(),
@@ -284,7 +151,7 @@
     }
 }
 
-impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> {
+impl<'a> TryFrom<&'a Question> for OwnedExchange<'a> {
     type Error = ErrorCode;
     fn try_from(question: &'a Question) -> Result<Self> {
         let style: Style = question
@@ -313,76 +180,47 @@
 
 #[cfg(test)]
 mod tests {
+    use super::*;
 
     macro_rules! assert_matches {
-        ($id:ident => $variant:path, $q:expr) => {
-            if let $variant($id) = $id {
-                assert_eq!($q, $id.question());
+        (($variant:path, $q:expr), $input:expr) => {
+            let input = $input;
+            let exc = input.exchange();
+            if let $variant(msg) = exc {
+                assert_eq!($q, msg.question());
             } else {
-                panic!("mismatched enum variant {x:?}", x = $id);
+                panic!(
+                    "want enum variant {v}, got {exc:?}",
+                    v = stringify!($variant)
+                );
             }
         };
     }
 
-    macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
-        mod $fn_name {
-            use super::super::*;
-            #[test]
-            fn standard() {
-                let interrogation = Box::pin(<$typ>::new(&[
-                    MaskedQAndA::new("hocus pocus").message(),
-                    QAndA::new("what").message(),
-                    QAndA::new("who").message(),
-                    InfoMsg::new("hey").message(),
-                    ErrorMsg::new("gasp").message(),
-                ])
-                .unwrap());
-                let indirect = interrogation.as_ref().ptr();
+    // TODO: Test TryFrom<Exchange> for Question/OwnedQuestion.
 
-                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
-                let messages: Vec<OwnedMessage> = remade
-                    .map(TryInto::try_into)
-                    .collect::<Result<_>>()
-                    .unwrap();
-                let [masked, what, who, hey, gasp] = messages.try_into().unwrap();
-                assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
-                assert_matches!(what => OwnedMessage::Prompt, "what");
-                assert_matches!(who => OwnedMessage::Prompt, "who");
-                assert_matches!(hey => OwnedMessage::Info, "hey");
-                assert_matches!(gasp => OwnedMessage::Error, "gasp");
-            }
+    #[test]
+    fn standard() {
+        assert_matches!(
+            (Exchange::MaskedPrompt, "hocus pocus"),
+            MaskedQAndA::new("hocus pocus")
+        );
+        assert_matches!((Exchange::Prompt, "what"), QAndA::new("what"));
+        assert_matches!((Exchange::Prompt, "who"), QAndA::new("who"));
+        assert_matches!((Exchange::Info, "hey"), InfoMsg::new("hey"));
+        assert_matches!((Exchange::Error, "gasp"), ErrorMsg::new("gasp"));
+    }
 
-            #[test]
-            #[cfg(not(feature = "linux-pam-ext"))]
-            fn no_linux_extensions() {
-                use crate::conv::{BinaryQAndA, RadioQAndA};
-                <$typ>::new(&[
-                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
-                    RadioQAndA::new("you must choose").message(),
-                ]).unwrap_err();
-            }
-
-            #[test]
-            #[cfg(feature = "linux-pam-ext")]
-            fn linux_extensions() {
-                let interrogation = Box::pin(<$typ>::new(&[
-                    BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
-                    RadioQAndA::new("you must choose").message(),
-                ]).unwrap());
-                let indirect = interrogation.as_ref().ptr();
-
-                let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
-                let messages: Vec<OwnedMessage> = remade
-                    .map(TryInto::try_into)
-                    .collect::<Result<_>>()
-                    .unwrap();
-                let [bin, choose] = messages.try_into().unwrap();
-                assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
-                assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
-            }
-        }
-    }}
-
-    tests!(test_xsso<XSsoQuestions>);
-    tests!(test_linux<LinuxPamQuestions>);
+    #[test]
+    #[cfg(feature = "linux-pam-ext")]
+    fn linux_extensions() {
+        assert_matches!(
+            (Exchange::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)),
+            BinaryQAndA::new((&[5, 4, 3, 2, 1], 66))
+        );
+        assert_matches!(
+            (Exchange::RadioPrompt, "you must choose"),
+            RadioQAndA::new("you must choose")
+        );
+    }
 }