use crate::conv::{ErrorMsg, Exchange, InfoMsg, MaskedQAndA, QAndA};
use crate::libpam::conversation::OwnedExchange;
use crate::libpam::memory;
use crate::ErrorCode;
use crate::Result;
use libpam_sys_helpers;
use std::ffi::{c_int, c_void, CStr, OsStr};
use std::os::unix::ffi::OsStrExt;
use std::ptr::NonNull;
memory::num_enum! {
enum Style {
PromptEchoOff = libpam_sys::PAM_PROMPT_ECHO_OFF,
PromptEchoOn = libpam_sys::PAM_PROMPT_ECHO_ON,
ErrorMsg = libpam_sys::PAM_ERROR_MSG,
TextInfo = libpam_sys::PAM_TEXT_INFO,
#[cfg(feature = "linux-pam-ext")]
RadioType = libpam_sys::PAM_RADIO_TYPE,
#[cfg(feature = "linux-pam-ext")]
BinaryPrompt = libpam_sys::PAM_BINARY_PROMPT,
}
}
#[repr(C)]
#[derive(Debug)]
pub struct Question {
pub style: c_int,
pub data: Option<NonNull<c_void>>,
}
impl Question {
unsafe fn string_data(&self) -> &OsStr {
match self.data.as_ref() {
None => "".as_ref(),
Some(data) => OsStr::from_bytes(CStr::from_ptr(data.as_ptr().cast()).to_bytes()),
}
}
unsafe fn binary_data(&self) -> (&[u8], u8) {
self.data
.as_ref()
.map(|data| libpam_sys_helpers::BinaryPayload::contents(data.as_ptr().cast()))
.unwrap_or_default()
}
}
impl TryFrom<&Exchange<'_>> for Question {
type Error = ErrorCode;
fn try_from(msg: &Exchange) -> Result<Self> {
let alloc = |style, text: &OsStr| -> Result<_> {
Ok((style, unsafe {
memory::CHeapBox::cast(memory::CHeapString::new(text.as_bytes()).into_box())
}))
};
let (style, data): (_, memory::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) => {
let (data, typ) = p.question();
let payload = memory::CHeapPayload::new(data, typ)?.into_inner();
Ok((Style::BinaryPrompt, unsafe {
memory::CHeapBox::cast(payload)
}))
}
#[cfg(not(feature = "linux-pam-ext"))]
Exchange::RadioPrompt(_) | Exchange::BinaryPrompt(_) => {
Err(ErrorCode::ConversationError)
}
}?;
Ok(Self {
style: style.into(),
data: Some(memory::CHeapBox::into_ptr(data)),
})
}
}
impl Drop for Question {
fn drop(&mut self) {
unsafe {
if let Ok(style) = Style::try_from(self.style) {
let _ = match style {
#[cfg(feature = "linux-pam-ext")]
Style::BinaryPrompt => self
.data
.as_mut()
.map(|p| libpam_sys_helpers::BinaryPayload::zero(p.as_ptr().cast())),
#[cfg(feature = "linux-pam-ext")]
Style::RadioType => self
.data
.as_mut()
.map(|p| memory::CHeapString::zero(p.cast())),
Style::TextInfo
| Style::ErrorMsg
| Style::PromptEchoOff
| Style::PromptEchoOn => self
.data
.as_mut()
.map(|p| memory::CHeapString::zero(p.cast())),
};
};
let _ = self.data.map(|p| memory::CHeapBox::from_ptr(p));
}
}
}
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)?;
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(crate::conv::RadioQAndA::new(question.string_data()))
}
#[cfg(feature = "linux-pam-ext")]
Style::BinaryPrompt => {
Self::BinaryPrompt(crate::conv::BinaryQAndA::new(question.binary_data()))
}
}
};
Ok(prompt)
}
}
#[cfg(feature = "linux-pam-ext")]
impl From<libpam_sys_helpers::TooBigError> for ErrorCode {
fn from(_: libpam_sys_helpers::TooBigError) -> Self {
ErrorCode::BufferError
}
}
#[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)
);
}
};
}
#[test]
fn standard() {
assert_matches!(
(Exchange::MaskedPrompt, "hocus pocus"),
MaskedQAndA::new("hocus pocus".as_ref())
);
assert_matches!((Exchange::Prompt, "what"), QAndA::new("what".as_ref()));
assert_matches!((Exchange::Prompt, "who"), QAndA::new("who".as_ref()));
assert_matches!((Exchange::Info, "hey"), InfoMsg::new("hey".as_ref()));
assert_matches!((Exchange::Error, "gasp"), ErrorMsg::new("gasp".as_ref()));
}
#[test]
#[cfg(feature = "linux-pam-ext")]
fn linux_extensions() {
use crate::conv::{BinaryQAndA, RadioQAndA};
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".as_ref())
);
}
}