Mercurial > crates > nonstick
view src/libpam/question.rs @ 132:0b6a17f8c894 default tip
Get constant test working again with OpenPAM.
author | Paul Fisher <paul@pfish.zone> |
---|---|
date | Wed, 02 Jul 2025 02:34:29 -0400 |
parents | 80c07e5ab22f |
children |
line wrap: on
line source
//! Data and types dealing with PAM messages. #[cfg(feature = "linux-pam-ext")] use crate::conv::{BinaryQAndA, RadioQAndA}; 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::ffi::{c_int, c_void, CStr}; 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. #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] #[repr(i32)] enum Style { /// Requests information from the user; will be masked when typing. PromptEchoOff = style_const::PAM_PROMPT_ECHO_OFF, /// Requests information from the user; will not be masked. PromptEchoOn = style_const::PAM_PROMPT_ECHO_ON, /// An error message. ErrorMsg = style_const::PAM_ERROR_MSG, /// An informational message. TextInfo = style_const::PAM_TEXT_INFO, /// Yes/No/Maybe conditionals. A Linux-PAM extension. #[cfg(feature = "linux-pam-ext")] 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 = 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 { /// 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> { match self.data.as_ref() { None => Ok(""), Some(data) => CStr::from_ptr(CHeapBox::as_ptr(data).cast().as_ptr()) .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 .as_ref() .map(|data| CBinaryData::data(CHeapBox::as_ptr(data).cast())) .unwrap_or_default() } } impl TryFrom<&Exchange<'_>> for Question { type Error = ErrorCode; fn try_from(msg: &Exchange) -> Result<Self> { let alloc = |style, text| -> Result<_> { Ok((style, unsafe { CHeapBox::cast(CHeapString::new(text)?.into_box()) })) }; // We will only allocate heap data if we have a valid input. let (style, data): (_, CHeapBox<c_void>) = match *msg { 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")] Exchange::RadioPrompt(p) => alloc(Style::RadioType, p.question()), #[cfg(feature = "linux-pam-ext")] Exchange::BinaryPrompt(p) => Ok((Style::BinaryPrompt, unsafe { CHeapBox::cast(CBinaryData::alloc(p.question())?) })), #[cfg(not(feature = "linux-pam-ext"))] Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => { Err(ErrorCode::ConversationError) } }?; Ok(Self { style: style.into(), data: Some(data), }) } } impl Drop for Question { fn drop(&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 { // This is nice-to-have. We'll try to zero out the data // in the Question. If it's not a supported format, we skip it. if let Ok(style) = Style::try_from(self.style) { let _ = match style { #[cfg(feature = "linux-pam-ext")] Style::BinaryPrompt => self .data .as_ref() .map(|p| CBinaryData::zero_contents(CHeapBox::as_ptr(p).cast())), #[cfg(feature = "linux-pam-ext")] Style::RadioType => self .data .as_ref() .map(|p| CHeapString::zero(CHeapBox::as_ptr(p).cast())), Style::TextInfo | Style::ErrorMsg | Style::PromptEchoOff | Style::PromptEchoOn => self .data .as_ref() .map(|p| CHeapString::zero(CHeapBox::as_ptr(p).cast())), }; }; } } } impl<'a> TryFrom<&'a Question> for OwnedExchange<'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 creating questions based on // known types that we get from PAM and the inner types it should have. 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()?)), #[cfg(feature = "linux-pam-ext")] Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), #[cfg(feature = "linux-pam-ext")] Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())), } }; Ok(prompt) } } #[cfg(test)] mod tests { use super::*; macro_rules! assert_matches { (($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!( "want enum variant {v}, got {exc:?}", v = stringify!($variant) ); } }; } // TODO: Test TryFrom<Exchange> for Question/OwnedQuestion. #[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(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") ); } }