Mercurial > crates > nonstick
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") + ); + } }