Mercurial > crates > nonstick
comparison src/libpam/question.rs @ 78:002adfb98c5c
Rename files, reorder structs, remove annoying BorrowedBinaryData type.
This is basically a cleanup change. Also it adds tests.
- Renames the files with Questions and Answers to question and answer.
- Reorders the structs in those files to put the important ones first.
- Removes the BorrowedBinaryData type. It was a bad idea all along.
Instead, we just use (&[u8], u8).
- Adds some tests because I just can't help myself.
| author | Paul Fisher <paul@pfish.zone> |
|---|---|
| date | Sun, 08 Jun 2025 03:48:40 -0400 |
| parents | src/libpam/message.rs@351bdc13005e |
| children | 2128123b9406 |
comparison
equal
deleted
inserted
replaced
| 77:351bdc13005e | 78:002adfb98c5c |
|---|---|
| 1 //! Data and types dealing with PAM messages. | |
| 2 | |
| 3 use crate::constants::InvalidEnum; | |
| 4 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, MaskedQAndA, Message, QAndA, RadioQAndA}; | |
| 5 use crate::libpam::conversation::OwnedMessage; | |
| 6 use crate::libpam::memory; | |
| 7 use crate::libpam::memory::{CBinaryData, Immovable}; | |
| 8 use crate::ErrorCode; | |
| 9 use crate::Result; | |
| 10 use num_derive::FromPrimitive; | |
| 11 use num_traits::FromPrimitive; | |
| 12 use std::ffi::{c_int, c_void, CStr}; | |
| 13 use std::result::Result as StdResult; | |
| 14 use std::{iter, ptr, slice}; | |
| 15 | |
| 16 /// Abstraction of a collection of questions to be sent in a PAM conversation. | |
| 17 /// | |
| 18 /// The PAM C API conversation function looks like this: | |
| 19 /// | |
| 20 /// ```c | |
| 21 /// int pam_conv( | |
| 22 /// int count, | |
| 23 /// const struct pam_message **questions, | |
| 24 /// struct pam_response **answers, | |
| 25 /// void *appdata_ptr, | |
| 26 /// ) | |
| 27 /// ``` | |
| 28 /// | |
| 29 /// On Linux-PAM and other compatible implementations, `questions` | |
| 30 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. | |
| 31 /// (In this situation, the value of `Questions.indirect` is | |
| 32 /// the pointer passed to `pam_conv`.) | |
| 33 /// | |
| 34 /// ```text | |
| 35 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question ═╗ | |
| 36 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ | |
| 37 /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... | |
| 38 /// ╚═════════════╝ │ ... │ ┆ ╚════════════╝ | |
| 39 /// ┆ | |
| 40 /// ┆ ╔═ Question ═╗ | |
| 41 /// ╰┄┄> ║ style ║ | |
| 42 /// ║ data ┄┄┄┄┄┄╫┄┄> ... | |
| 43 /// ╚════════════╝ | |
| 44 /// ``` | |
| 45 /// | |
| 46 /// On OpenPAM and other compatible implementations (like Solaris), | |
| 47 /// `messages` is a pointer-to-pointer-to-array. This appears to be | |
| 48 /// the correct implementation as required by the XSSO specification. | |
| 49 /// | |
| 50 /// ```text | |
| 51 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question[] ═╗ | |
| 52 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ | |
| 53 /// ║ count ║ └────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... | |
| 54 /// ╚═════════════╝ ╟──────────────╢ | |
| 55 /// ║ style ║ | |
| 56 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... | |
| 57 /// ╟──────────────╢ | |
| 58 /// ║ ... ║ | |
| 59 /// ``` | |
| 60 /// | |
| 61 /// ***THIS LIBRARY CURRENTLY SUPPORTS ONLY LINUX-PAM.*** | |
| 62 pub struct Questions { | |
| 63 /// An indirection to the questions themselves, stored on the C heap. | |
| 64 indirect: *mut Indirect, | |
| 65 /// The number of questions. | |
| 66 count: usize, | |
| 67 } | |
| 68 | |
| 69 impl Questions { | |
| 70 /// Stores the provided questions on the C heap. | |
| 71 pub fn new(messages: &[Message]) -> Result<Self> { | |
| 72 let count = messages.len(); | |
| 73 let mut ret = Self { | |
| 74 indirect: Indirect::alloc(count), | |
| 75 count, | |
| 76 }; | |
| 77 // Even if we fail partway through this, all our memory will be freed. | |
| 78 for (question, message) in iter::zip(ret.iter_mut(), messages) { | |
| 79 question.fill(message)? | |
| 80 } | |
| 81 Ok(ret) | |
| 82 } | |
| 83 | |
| 84 /// The pointer to the thing with the actual list. | |
| 85 pub fn indirect(&self) -> *const Indirect { | |
| 86 self.indirect | |
| 87 } | |
| 88 | |
| 89 pub fn iter(&self) -> impl Iterator<Item = &Question> { | |
| 90 // SAFETY: we're iterating over an amount we know. | |
| 91 unsafe { (*self.indirect).iter(self.count) } | |
| 92 } | |
| 93 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> { | |
| 94 // SAFETY: we're iterating over an amount we know. | |
| 95 unsafe { (*self.indirect).iter_mut(self.count) } | |
| 96 } | |
| 97 } | |
| 98 | |
| 99 impl Drop for Questions { | |
| 100 fn drop(&mut self) { | |
| 101 // SAFETY: We are valid and have a valid pointer. | |
| 102 // Once we're done, everything will be safe. | |
| 103 unsafe { | |
| 104 if let Some(indirect) = self.indirect.as_mut() { | |
| 105 indirect.free(self.count) | |
| 106 } | |
| 107 memory::free(self.indirect); | |
| 108 self.indirect = ptr::null_mut(); | |
| 109 } | |
| 110 } | |
| 111 } | |
| 112 | |
| 113 /// An indirect reference to messages. | |
| 114 /// | |
| 115 /// This is kept separate to provide a place where we can separate | |
| 116 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers. | |
| 117 #[repr(transparent)] | |
| 118 pub struct Indirect { | |
| 119 base: [*mut Question; 0], | |
| 120 _marker: Immovable, | |
| 121 } | |
| 122 | |
| 123 impl Indirect { | |
| 124 /// Allocates memory for this indirector and all its members. | |
| 125 fn alloc(count: usize) -> *mut Self { | |
| 126 // SAFETY: We're only allocating, and when we're done, | |
| 127 // everything will be in a known-good state. | |
| 128 let me_ptr: *mut Indirect = memory::calloc::<Question>(count).cast(); | |
| 129 unsafe { | |
| 130 let me = &mut *me_ptr; | |
| 131 let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); | |
| 132 for entry in ptr_list { | |
| 133 *entry = memory::calloc(1); | |
| 134 } | |
| 135 me | |
| 136 } | |
| 137 } | |
| 138 | |
| 139 /// Returns an iterator yielding the given number of messages. | |
| 140 /// | |
| 141 /// # Safety | |
| 142 /// | |
| 143 /// You have to provide the right count. | |
| 144 pub unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { | |
| 145 (0..count).map(|idx| &**self.base.as_ptr().add(idx)) | |
| 146 } | |
| 147 | |
| 148 /// Returns a mutable iterator yielding the given number of messages. | |
| 149 /// | |
| 150 /// # Safety | |
| 151 /// | |
| 152 /// You have to provide the right count. | |
| 153 pub unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { | |
| 154 (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) | |
| 155 } | |
| 156 | |
| 157 /// Frees everything this points to. | |
| 158 /// | |
| 159 /// # Safety | |
| 160 /// | |
| 161 /// You have to pass the right size. | |
| 162 unsafe fn free(&mut self, count: usize) { | |
| 163 let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count); | |
| 164 for msg in msgs { | |
| 165 if let Some(msg) = msg.as_mut() { | |
| 166 msg.clear(); | |
| 167 } | |
| 168 memory::free(*msg); | |
| 169 *msg = ptr::null_mut(); | |
| 170 } | |
| 171 } | |
| 172 } | |
| 173 | |
| 174 /// The C enum values for messages shown to the user. | |
| 175 #[derive(Debug, PartialEq, FromPrimitive)] | |
| 176 pub enum Style { | |
| 177 /// Requests information from the user; will be masked when typing. | |
| 178 PromptEchoOff = 1, | |
| 179 /// Requests information from the user; will not be masked. | |
| 180 PromptEchoOn = 2, | |
| 181 /// An error message. | |
| 182 ErrorMsg = 3, | |
| 183 /// An informational message. | |
| 184 TextInfo = 4, | |
| 185 /// Yes/No/Maybe conditionals. A Linux-PAM extension. | |
| 186 RadioType = 5, | |
| 187 /// For server–client non-human interaction. | |
| 188 /// | |
| 189 /// NOT part of the X/Open PAM specification. | |
| 190 /// A Linux-PAM extension. | |
| 191 BinaryPrompt = 7, | |
| 192 } | |
| 193 | |
| 194 impl TryFrom<c_int> for Style { | |
| 195 type Error = InvalidEnum<Self>; | |
| 196 fn try_from(value: c_int) -> StdResult<Self, Self::Error> { | |
| 197 Self::from_i32(value).ok_or(value.into()) | |
| 198 } | |
| 199 } | |
| 200 | |
| 201 impl From<Style> for c_int { | |
| 202 fn from(val: Style) -> Self { | |
| 203 val as Self | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 /// A question sent by PAM or a module to an application. | |
| 208 /// | |
| 209 /// PAM refers to this as a "message", but we call it a question | |
| 210 /// to avoid confusion with [`Message`]. | |
| 211 /// | |
| 212 /// This question, and its internal data, is owned by its creator | |
| 213 /// (either the module or PAM itself). | |
| 214 #[repr(C)] | |
| 215 pub struct Question { | |
| 216 /// The style of message to request. | |
| 217 style: c_int, | |
| 218 /// A description of the data requested. | |
| 219 /// | |
| 220 /// For most requests, this will be an owned [`CStr`], but for requests | |
| 221 /// with [`Style::BinaryPrompt`], this will be [`CBinaryData`] | |
| 222 /// (a Linux-PAM extension). | |
| 223 data: *mut c_void, | |
| 224 _marker: Immovable, | |
| 225 } | |
| 226 | |
| 227 impl Question { | |
| 228 /// Replaces the contents of this question with the question | |
| 229 /// from the message. | |
| 230 pub fn fill(&mut self, msg: &Message) -> Result<()> { | |
| 231 let (style, data) = copy_to_heap(msg)?; | |
| 232 self.clear(); | |
| 233 self.style = style as c_int; | |
| 234 self.data = data; | |
| 235 Ok(()) | |
| 236 } | |
| 237 | |
| 238 /// Gets this message's data pointer as a string. | |
| 239 /// | |
| 240 /// # Safety | |
| 241 /// | |
| 242 /// It's up to you to pass this only on types with a string value. | |
| 243 unsafe fn string_data(&self) -> Result<&str> { | |
| 244 if self.data.is_null() { | |
| 245 Ok("") | |
| 246 } else { | |
| 247 CStr::from_ptr(self.data.cast()) | |
| 248 .to_str() | |
| 249 .map_err(|_| ErrorCode::ConversationError) | |
| 250 } | |
| 251 } | |
| 252 | |
| 253 /// Gets this message's data pointer as borrowed binary data. | |
| 254 unsafe fn binary_data(&self) -> (&[u8], u8) { | |
| 255 self.data | |
| 256 .cast::<CBinaryData>() | |
| 257 .as_ref() | |
| 258 .map(Into::into) | |
| 259 .unwrap_or_default() | |
| 260 } | |
| 261 | |
| 262 /// Zeroes out the data stored here. | |
| 263 fn clear(&mut self) { | |
| 264 // SAFETY: We either created this data or we got it from PAM. | |
| 265 // After this function is done, it will be zeroed out. | |
| 266 unsafe { | |
| 267 if let Ok(style) = Style::try_from(self.style) { | |
| 268 match style { | |
| 269 Style::BinaryPrompt => { | |
| 270 if let Some(d) = self.data.cast::<CBinaryData>().as_mut() { | |
| 271 d.zero_contents() | |
| 272 } | |
| 273 } | |
| 274 Style::TextInfo | |
| 275 | Style::RadioType | |
| 276 | Style::ErrorMsg | |
| 277 | Style::PromptEchoOff | |
| 278 | Style::PromptEchoOn => memory::zero_c_string(self.data.cast()), | |
| 279 } | |
| 280 }; | |
| 281 memory::free(self.data); | |
| 282 self.data = ptr::null_mut(); | |
| 283 } | |
| 284 } | |
| 285 } | |
| 286 | |
| 287 impl<'a> TryFrom<&'a Question> for OwnedMessage<'a> { | |
| 288 type Error = ErrorCode; | |
| 289 fn try_from(question: &'a Question) -> Result<Self> { | |
| 290 let style: Style = question | |
| 291 .style | |
| 292 .try_into() | |
| 293 .map_err(|_| ErrorCode::ConversationError)?; | |
| 294 // SAFETY: In all cases below, we're matching the | |
| 295 let prompt = unsafe { | |
| 296 match style { | |
| 297 Style::PromptEchoOff => { | |
| 298 Self::MaskedPrompt(MaskedQAndA::new(question.string_data()?)) | |
| 299 } | |
| 300 Style::PromptEchoOn => Self::Prompt(QAndA::new(question.string_data()?)), | |
| 301 Style::ErrorMsg => Self::Error(ErrorMsg::new(question.string_data()?)), | |
| 302 Style::TextInfo => Self::Info(InfoMsg::new(question.string_data()?)), | |
| 303 Style::RadioType => Self::RadioPrompt(RadioQAndA::new(question.string_data()?)), | |
| 304 Style::BinaryPrompt => Self::BinaryPrompt(BinaryQAndA::new(question.binary_data())), | |
| 305 } | |
| 306 }; | |
| 307 Ok(prompt) | |
| 308 } | |
| 309 } | |
| 310 | |
| 311 /// Copies the contents of this message to the C heap. | |
| 312 fn copy_to_heap(msg: &Message) -> Result<(Style, *mut c_void)> { | |
| 313 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); | |
| 314 match *msg { | |
| 315 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), | |
| 316 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), | |
| 317 Message::RadioPrompt(p) => alloc(Style::RadioType, p.question()), | |
| 318 Message::Error(p) => alloc(Style::ErrorMsg, p.question()), | |
| 319 Message::Info(p) => alloc(Style::TextInfo, p.question()), | |
| 320 Message::BinaryPrompt(p) => { | |
| 321 let q = p.question(); | |
| 322 Ok(( | |
| 323 Style::BinaryPrompt, | |
| 324 CBinaryData::alloc(q)?.cast(), | |
| 325 )) | |
| 326 } | |
| 327 } | |
| 328 } | |
| 329 | |
| 330 #[cfg(test)] | |
| 331 mod tests { | |
| 332 | |
| 333 use super::{MaskedQAndA, Questions, Result}; | |
| 334 use crate::conv::{BinaryQAndA, ErrorMsg, InfoMsg, QAndA, RadioQAndA}; | |
| 335 use crate::libpam::conversation::OwnedMessage; | |
| 336 | |
| 337 #[test] | |
| 338 fn test_round_trip() { | |
| 339 let interrogation = Questions::new(&[ | |
| 340 MaskedQAndA::new("hocus pocus").message(), | |
| 341 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), | |
| 342 QAndA::new("what").message(), | |
| 343 QAndA::new("who").message(), | |
| 344 InfoMsg::new("hey").message(), | |
| 345 ErrorMsg::new("gasp").message(), | |
| 346 RadioQAndA::new("you must choose").message(), | |
| 347 ]) | |
| 348 .unwrap(); | |
| 349 let indirect = interrogation.indirect(); | |
| 350 | |
| 351 let remade = unsafe { indirect.as_ref() }.unwrap(); | |
| 352 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } | |
| 353 .map(TryInto::try_into) | |
| 354 .collect::<Result<_>>() | |
| 355 .unwrap(); | |
| 356 let [masked, bin, what, who, hey, gasp, choose] = messages.try_into().unwrap(); | |
| 357 macro_rules! assert_matches { | |
| 358 ($id:ident => $variant:path, $q:expr) => { | |
| 359 if let $variant($id) = $id { | |
| 360 assert_eq!($q, $id.question()); | |
| 361 } else { | |
| 362 panic!("mismatched enum variant {x:?}", x = $id); | |
| 363 } | |
| 364 }; | |
| 365 } | |
| 366 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); | |
| 367 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); | |
| 368 assert_matches!(what => OwnedMessage::Prompt, "what"); | |
| 369 assert_matches!(who => OwnedMessage::Prompt, "who"); | |
| 370 assert_matches!(hey => OwnedMessage::Info, "hey"); | |
| 371 assert_matches!(gasp => OwnedMessage::Error, "gasp"); | |
| 372 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); | |
| 373 } | |
| 374 } |
