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 } |