comparison src/libpam/question.rs @ 89:dd3e9c4bcde3

Simplify memory management in Questions. When we're sending Questions to the client, we don't need them to be C-managed, we just need the pointers going to the right place. This replaces a bunch of Question management cruft with Vecs and Boxes.
author Paul Fisher <paul@pfish.zone>
date Fri, 13 Jun 2025 05:22:48 -0400
parents 05291b601f0a
children efc2b56c8928
comparison
equal deleted inserted replaced
88:c9fc7e6257d3 89:dd3e9c4bcde3
9 use crate::libpam::{memory, pam_ffi}; 9 use crate::libpam::{memory, pam_ffi};
10 use crate::ErrorCode; 10 use crate::ErrorCode;
11 use crate::Result; 11 use crate::Result;
12 use num_enum::{IntoPrimitive, TryFromPrimitive}; 12 use num_enum::{IntoPrimitive, TryFromPrimitive};
13 use std::ffi::{c_void, CStr}; 13 use std::ffi::{c_void, CStr};
14 use std::{iter, ptr, slice}; 14 use std::{ptr, slice};
15 15
16 /// Abstraction of a collection of questions to be sent in a PAM conversation. 16 /// Abstraction of a collection of questions to be sent in a PAM conversation.
17 /// 17 ///
18 /// The PAM C API conversation function looks like this: 18 /// The PAM C API conversation function looks like this:
19 /// 19 ///
30 /// is treated as a pointer-to-pointers, like `int argc, char **argv`. 30 /// is treated as a pointer-to-pointers, like `int argc, char **argv`.
31 /// (In this situation, the value of `Questions.indirect` is 31 /// (In this situation, the value of `Questions.indirect` is
32 /// the pointer passed to `pam_conv`.) 32 /// the pointer passed to `pam_conv`.)
33 /// 33 ///
34 /// ```text 34 /// ```text
35 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question ═╗ 35 /// points to ┌───────────────┐ ╔═ Question ═╗
36 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base[0] ┄┄┄┼┄┄┄┄┄> ║ style ║ 36 /// questions ┄┄┄┄┄┄┄┄┄┄> │ questions[0] ┄┼┄┄┄┄> ║ style ║
37 /// ║ count ║ │ base[1] ┄┄┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ... 37 /// │ questions[1] ┄┼┄┄┄╮ ║ data ┄┄┄┄┄┄╫┄┄> ...
38 /// ╚═════════════╝ │ ... │ ┆ ╚════════════╝ 38 /// │ ... │ ┆ ╚════════════╝
39 /// ┆ 39 /// ┆
40 /// ┆ ╔═ Question ═╗ 40 /// ┆ ╔═ Question ═╗
41 /// ╰┄┄> ║ style ║ 41 /// ╰┄┄> ║ style ║
42 /// ║ data ┄┄┄┄┄┄╫┄┄> ... 42 /// ║ data ┄┄┄┄┄┄╫┄┄> ...
43 /// ╚════════════╝ 43 /// ╚════════════╝
44 /// ``` 44 /// ```
45 /// 45 ///
46 /// On OpenPAM and other compatible implementations (like Solaris), 46 /// On OpenPAM and other compatible implementations (like Solaris),
47 /// `messages` is a pointer-to-pointer-to-array. This appears to be 47 /// `messages` is a pointer-to-pointer-to-array. This appears to be
48 /// the correct implementation as required by the XSSO specification. 48 /// the correct implementation as required by the XSSO specification.
49 /// 49 ///
50 /// ```text 50 /// ```text
51 /// ╔═ Questions ═╗ points to ┌─ Indirect ─┐ ╔═ Question[] ═╗ 51 /// points to ┌─────────────┐ ╔═ Question[] ═╗
52 /// ║ indirect ┄┄┄╫┄┄┄┄┄┄┄┄┄┄┄> │ base ┄┄┄┄┄┄┼┄┄┄┄┄> ║ style ║ 52 /// questions ┄┄┄┄┄┄┄┄┄┄> │ *questions ┄┼┄┄┄┄┄> ║ style ║
53 /// ║ count ║ └────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... 53 /// └─────────────┘ ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
54 /// ╚═════════════╝ ╟──────────────╢ 54 /// ╟──────────────╢
55 /// ║ style ║ 55 /// ║ style ║
56 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ... 56 /// ║ data ┄┄┄┄┄┄┄┄╫┄┄> ...
57 /// ╟──────────────╢ 57 /// ╟──────────────╢
58 /// ║ ... ║ 58 /// ║ ... ║
59 /// ``` 59 /// ```
60 #[derive(Debug)] 60 pub trait QuestionsTrait {
61 pub struct GenericQuestions<I: IndirectTrait> { 61 /// Allocates memory for this indirector and all its members.
62 /// An indirection to the questions themselves, stored on the C heap. 62 fn new(messages: &[Message]) -> Result<Self>
63 indirect: *mut I, 63 where
64 /// The number of questions. 64 Self: Sized;
65 count: usize, 65
66 } 66 /// Gets the pointer that is passed .
67 67 fn ptr(&self) -> *const *const Question;
68 impl<I: IndirectTrait> GenericQuestions<I> { 68
69 /// Stores the provided questions on the C heap. 69 /// Converts a pointer into a borrowed list of Questions.
70 pub fn new(messages: &[Message]) -> Result<Self> {
71 let count = messages.len();
72 let mut ret = Self {
73 indirect: I::alloc(count),
74 count,
75 };
76 // Even if we fail partway through this, all our memory will be freed.
77 for (question, message) in iter::zip(ret.iter_mut(), messages) {
78 question.try_fill(message)?
79 }
80 Ok(ret)
81 }
82
83 /// The pointer to the thing with the actual list.
84 pub fn indirect(&self) -> *const *const Question {
85 self.indirect.cast()
86 }
87
88 pub fn iter(&self) -> impl Iterator<Item = &Question> {
89 // SAFETY: we're iterating over an amount we know.
90 unsafe { (*self.indirect).iter(self.count) }
91 }
92 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
93 // SAFETY: we're iterating over an amount we know.
94 unsafe { (*self.indirect).iter_mut(self.count) }
95 }
96 }
97
98 impl<I: IndirectTrait> Drop for GenericQuestions<I> {
99 fn drop(&mut self) {
100 // SAFETY: We are valid and have a valid pointer.
101 // Once we're done, everything will be safe.
102 unsafe {
103 if let Some(indirect) = self.indirect.as_mut() {
104 indirect.free_contents(self.count)
105 }
106 memory::free(self.indirect);
107 self.indirect = ptr::null_mut();
108 }
109 }
110 }
111
112 /// The trait that each of the `Indirect` implementations implement.
113 ///
114 /// Basically a slice but with more meat.
115 pub trait IndirectTrait {
116 /// Converts a pointer into a borrowed `Self`.
117 /// 70 ///
118 /// # Safety 71 /// # Safety
119 /// 72 ///
120 /// You have to provide a valid pointer. 73 /// You have to provide a valid pointer.
121 unsafe fn borrow_ptr<'a>(ptr: *const *const Question) -> Option<&'a Self> 74 unsafe fn borrow_ptr<'a>(
122 where 75 ptr: *const *const Question,
123 Self: Sized, 76 count: usize,
124 { 77 ) -> impl Iterator<Item = &'a Question>;
125 ptr.cast::<Self>().as_ref() 78 }
126 } 79
127
128 /// Allocates memory for this indirector and all its members.
129 fn alloc(count: usize) -> *mut Self;
130
131 /// Returns an iterator yielding the given number of messages.
132 ///
133 /// # Safety
134 ///
135 /// You have to provide the right count.
136 unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question>;
137
138 /// Returns a mutable iterator yielding the given number of messages.
139 ///
140 /// # Safety
141 ///
142 /// You have to provide the right count.
143 unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question>;
144
145 /// Frees everything this points to.
146 ///
147 /// # Safety
148 ///
149 /// You have to pass the right size.
150 unsafe fn free_contents(&mut self, count: usize);
151 }
152
153 /// An indirect reference to messages.
154 ///
155 /// This is kept separate to provide a place where we can separate
156 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
157 #[cfg(pam_impl = "linux-pam")] 80 #[cfg(pam_impl = "linux-pam")]
158 pub type Indirect = LinuxPamIndirect; 81 pub type Questions = LinuxPamQuestions;
159 82
160 /// An indirect reference to messages.
161 ///
162 /// This is kept separate to provide a place where we can separate
163 /// the pointer-to-pointer-to-list from pointer-to-list-of-pointers.
164 #[cfg(not(pam_impl = "linux-pam"))] 83 #[cfg(not(pam_impl = "linux-pam"))]
165 pub type Indirect = StandardIndirect; 84 pub type Questions = XSsoQuestions;
166 85
167 pub type Questions = GenericQuestions<Indirect>; 86 /// The XSSO standard version of the pointer train to questions.
168
169 /// The XSSO standard version of the indirection layer between Question and Questions.
170 #[derive(Debug)] 87 #[derive(Debug)]
171 #[repr(C)] 88 #[repr(C)]
172 pub struct StandardIndirect { 89 pub struct XSsoQuestions {
173 base: *mut Question, 90 /// Points to the memory address where the meat of `questions` is.
91 /// **The memory layout of Vec is not specified**, and we need to return
92 /// a pointer to the pointer, hence we have to store it here.
93 pointer: *const Question,
94 questions: Vec<Question>,
174 _marker: Immovable, 95 _marker: Immovable,
175 } 96 }
176 97
177 impl IndirectTrait for StandardIndirect { 98 impl XSsoQuestions {
178 fn alloc(count: usize) -> *mut Self { 99 fn len(&self) -> usize {
179 let questions = memory::calloc(count); 100 self.questions.len()
180 let me_ptr: *mut Self = memory::calloc(1); 101 }
181 // SAFETY: We just allocated this, and we're putting a valid pointer in. 102 fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
182 unsafe { 103 self.questions.iter_mut()
183 let me = &mut *me_ptr; 104 }
184 me.base = questions; 105 }
185 } 106
186 me_ptr 107 impl QuestionsTrait for XSsoQuestions {
187 } 108 fn new(messages: &[Message]) -> Result<Self> {
188 109 let questions: Result<Vec<_>> = messages.iter().map(Question::try_from).collect();
189 unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { 110 let questions = questions?;
190 (0..count).map(|idx| &*self.base.add(idx)) 111 Ok(Self {
191 } 112 pointer: questions.as_ptr(),
192 113 questions,
193 unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { 114 _marker: Default::default(),
194 (0..count).map(|idx| &mut *self.base.add(idx)) 115 })
195 } 116 }
196 117
197 unsafe fn free_contents(&mut self, count: usize) { 118 fn ptr(&self) -> *const *const Question {
198 let msgs = slice::from_raw_parts_mut(self.base, count); 119 &self.pointer as *const *const Question
199 for msg in msgs { 120 }
200 msg.clear() 121
201 } 122 unsafe fn borrow_ptr<'a>(
202 memory::free(self.base); 123 ptr: *const *const Question,
203 self.base = ptr::null_mut() 124 count: usize,
204 } 125 ) -> impl Iterator<Item = &'a Question> {
205 } 126 slice::from_raw_parts(*ptr, count).iter()
206 127 }
207 /// The Linux version of the indirection layer between Question and Questions. 128 }
129
130 /// The Linux version of the pointer train to questions.
208 #[derive(Debug)] 131 #[derive(Debug)]
209 #[repr(C)] 132 #[repr(C)]
210 pub struct LinuxPamIndirect { 133 pub struct LinuxPamQuestions {
211 base: [*mut Question; 0], 134 #[allow(clippy::vec_box)] // we need to do this.
135 /// The place where the questions are.
136 questions: Vec<Box<Question>>,
212 _marker: Immovable, 137 _marker: Immovable,
213 } 138 }
214 139
215 impl IndirectTrait for LinuxPamIndirect { 140 impl LinuxPamQuestions {
216 fn alloc(count: usize) -> *mut Self { 141 fn len(&self) -> usize {
217 // SAFETY: We're only allocating, and when we're done, 142 self.questions.len()
218 // everything will be in a known-good state. 143 }
219 let me_ptr: *mut Self = memory::calloc::<*mut Question>(count).cast(); 144
220 unsafe { 145 fn iter_mut(&mut self) -> impl Iterator<Item = &mut Question> {
221 let me = &mut *me_ptr; 146 self.questions.iter_mut().map(AsMut::as_mut)
222 let ptr_list = slice::from_raw_parts_mut(me.base.as_mut_ptr(), count); 147 }
223 for entry in ptr_list { 148 }
224 *entry = memory::calloc(1); 149
225 } 150 impl QuestionsTrait for LinuxPamQuestions {
226 } 151 fn new(messages: &[Message]) -> Result<Self> {
227 me_ptr 152 let questions: Result<_> = messages
228 } 153 .iter()
229 154 .map(|msg| Question::try_from(msg).map(Box::new))
230 unsafe fn iter(&self, count: usize) -> impl Iterator<Item = &Question> { 155 .collect();
231 (0..count).map(|idx| &**self.base.as_ptr().add(idx)) 156 Ok(Self {
232 } 157 questions: questions?,
233 158 _marker: Default::default(),
234 unsafe fn iter_mut(&mut self, count: usize) -> impl Iterator<Item = &mut Question> { 159 })
235 (0..count).map(|idx| &mut **self.base.as_mut_ptr().add(idx)) 160 }
236 } 161
237 162 fn ptr(&self) -> *const *const Question {
238 unsafe fn free_contents(&mut self, count: usize) { 163 self.questions.as_ptr().cast()
239 let msgs = slice::from_raw_parts_mut(self.base.as_mut_ptr(), count); 164 }
240 for msg in msgs { 165
241 if let Some(msg) = msg.as_mut() { 166 unsafe fn borrow_ptr<'a>(
242 msg.clear(); 167 ptr: *const *const Question,
243 } 168 count: usize,
244 memory::free(*msg); 169 ) -> impl Iterator<Item = &'a Question> {
245 *msg = ptr::null_mut(); 170 slice::from_raw_parts(ptr.cast::<&Question>(), count)
246 } 171 .iter()
172 .copied()
247 } 173 }
248 } 174 }
249 175
250 /// The C enum values for messages shown to the user. 176 /// The C enum values for messages shown to the user.
251 #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)] 177 #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive)]
252 #[repr(u32)] 178 #[repr(u32)]
253 pub enum Style { 179 enum Style {
254 /// Requests information from the user; will be masked when typing. 180 /// Requests information from the user; will be masked when typing.
255 PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF, 181 PromptEchoOff = pam_ffi::PAM_PROMPT_ECHO_OFF,
256 /// Requests information from the user; will not be masked. 182 /// Requests information from the user; will not be masked.
257 PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON, 183 PromptEchoOn = pam_ffi::PAM_PROMPT_ECHO_ON,
258 /// An error message. 184 /// An error message.
268 /// A Linux-PAM extension. 194 /// A Linux-PAM extension.
269 #[cfg(feature = "linux-pam-extensions")] 195 #[cfg(feature = "linux-pam-extensions")]
270 BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT, 196 BinaryPrompt = pam_ffi::PAM_BINARY_PROMPT,
271 } 197 }
272 198
273 impl Default for Question { 199 impl Question {
274 fn default() -> Self { 200 /// Gets this message's data pointer as a string.
275 Self { 201 ///
276 style: Default::default(), 202 /// # Safety
277 data: ptr::null_mut(), 203 ///
278 _marker: Default::default(), 204 /// It's up to you to pass this only on types with a string value.
205 unsafe fn string_data(&self) -> Result<&str> {
206 if self.data.is_null() {
207 Ok("")
208 } else {
209 CStr::from_ptr(self.data.cast())
210 .to_str()
211 .map_err(|_| ErrorCode::ConversationError)
279 } 212 }
280 } 213 }
281 } 214
282 215 /// Gets this message's data pointer as borrowed binary data.
283 impl Question { 216 unsafe fn binary_data(&self) -> (&[u8], u8) {
284 /// Replaces the contents of this question with the question 217 self.data
285 /// from the message. 218 .cast::<CBinaryData>()
286 /// 219 .as_ref()
287 /// If the message is not valid (invalid message type, bad contents, etc.), 220 .map(Into::into)
288 /// this will fail. 221 .unwrap_or_default()
289 pub fn try_fill(&mut self, msg: &Message) -> Result<()> { 222 }
223 }
224
225 impl TryFrom<&Message<'_>> for Question {
226 type Error = ErrorCode;
227 fn try_from(msg: &Message) -> Result<Self> {
290 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast())); 228 let alloc = |style, text| Ok((style, memory::malloc_str(text)?.cast()));
291 // We will only allocate heap data if we have a valid input. 229 // We will only allocate heap data if we have a valid input.
292 let (style, data): (_, *mut c_void) = match *msg { 230 let (style, data): (_, *mut c_void) = match *msg {
293 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()), 231 Message::MaskedPrompt(p) => alloc(Style::PromptEchoOff, p.question()),
294 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()), 232 Message::Prompt(p) => alloc(Style::PromptEchoOn, p.question()),
302 CBinaryData::alloc(p.question())?.cast(), 240 CBinaryData::alloc(p.question())?.cast(),
303 )), 241 )),
304 #[cfg(not(feature = "linux-pam-extensions"))] 242 #[cfg(not(feature = "linux-pam-extensions"))]
305 Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError), 243 Message::RadioPrompt(_) | Message::BinaryPrompt(_) => Err(ErrorCode::ConversationError),
306 }?; 244 }?;
307 // Now that we know everything is valid, fill ourselves in. 245 Ok(Self {
308 self.clear(); 246 style: style.into(),
309 self.style = style.into(); 247 data,
310 self.data = data; 248 _marker: Default::default(),
311 Ok(()) 249 })
312 } 250 }
313 251 }
314 /// Gets this message's data pointer as a string. 252
315 /// 253 impl Drop for Question {
316 /// # Safety 254 fn drop(&mut self) {
317 ///
318 /// It's up to you to pass this only on types with a string value.
319 unsafe fn string_data(&self) -> Result<&str> {
320 if self.data.is_null() {
321 Ok("")
322 } else {
323 CStr::from_ptr(self.data.cast())
324 .to_str()
325 .map_err(|_| ErrorCode::ConversationError)
326 }
327 }
328
329 /// Gets this message's data pointer as borrowed binary data.
330 unsafe fn binary_data(&self) -> (&[u8], u8) {
331 self.data
332 .cast::<CBinaryData>()
333 .as_ref()
334 .map(Into::into)
335 .unwrap_or_default()
336 }
337
338 /// Zeroes out the data stored here.
339 fn clear(&mut self) {
340 // SAFETY: We either created this data or we got it from PAM. 255 // SAFETY: We either created this data or we got it from PAM.
341 // After this function is done, it will be zeroed out. 256 // After this function is done, it will be zeroed out.
342 unsafe { 257 unsafe {
343 // This is nice-to-have. We'll try to zero out the data 258 // This is nice-to-have. We'll try to zero out the data
344 // in the Question. If it's not a supported format, we skip it. 259 // in the Question. If it's not a supported format, we skip it.
407 macro_rules! tests { ($fn_name:ident<$typ:ident>) => { 322 macro_rules! tests { ($fn_name:ident<$typ:ident>) => {
408 mod $fn_name { 323 mod $fn_name {
409 use super::super::*; 324 use super::super::*;
410 #[test] 325 #[test]
411 fn standard() { 326 fn standard() {
412 let interrogation = GenericQuestions::<$typ>::new(&[ 327 let interrogation = <$typ>::new(&[
413 MaskedQAndA::new("hocus pocus").message(), 328 MaskedQAndA::new("hocus pocus").message(),
414 QAndA::new("what").message(), 329 QAndA::new("what").message(),
415 QAndA::new("who").message(), 330 QAndA::new("who").message(),
416 InfoMsg::new("hey").message(), 331 InfoMsg::new("hey").message(),
417 ErrorMsg::new("gasp").message(), 332 ErrorMsg::new("gasp").message(),
418 ]) 333 ])
419 .unwrap(); 334 .unwrap();
420 let indirect = interrogation.indirect(); 335 let indirect = interrogation.ptr();
421 336
422 let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); 337 let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
423 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } 338 let messages: Vec<OwnedMessage> = remade
424 .map(TryInto::try_into) 339 .map(TryInto::try_into)
425 .collect::<Result<_>>() 340 .collect::<Result<_>>()
426 .unwrap(); 341 .unwrap();
427 let [masked, what, who, hey, gasp] = messages.try_into().unwrap(); 342 let [masked, what, who, hey, gasp] = messages.try_into().unwrap();
428 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus"); 343 assert_matches!(masked => OwnedMessage::MaskedPrompt, "hocus pocus");
434 349
435 #[test] 350 #[test]
436 #[cfg(not(feature = "linux-pam-extensions"))] 351 #[cfg(not(feature = "linux-pam-extensions"))]
437 fn no_linux_extensions() { 352 fn no_linux_extensions() {
438 use crate::conv::{BinaryQAndA, RadioQAndA}; 353 use crate::conv::{BinaryQAndA, RadioQAndA};
439 GenericQuestions::<$typ>::new(&[ 354 <$typ>::new(&[
440 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), 355 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
441 RadioQAndA::new("you must choose").message(), 356 RadioQAndA::new("you must choose").message(),
442 ]).unwrap_err(); 357 ]).unwrap_err();
443 } 358 }
444 359
445 #[test] 360 #[test]
446 #[cfg(feature = "linux-pam-extensions")] 361 #[cfg(feature = "linux-pam-extensions")]
447 fn linux_extensions() { 362 fn linux_extensions() {
448 let interrogation = GenericQuestions::<$typ>::new(&[ 363 let interrogation = <$typ>::new(&[
449 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(), 364 BinaryQAndA::new((&[5, 4, 3, 2, 1], 66)).message(),
450 RadioQAndA::new("you must choose").message(), 365 RadioQAndA::new("you must choose").message(),
451 ]).unwrap(); 366 ]).unwrap();
452 let indirect = interrogation.indirect(); 367 let indirect = interrogation.ptr();
453 368
454 let remade = unsafe { $typ::borrow_ptr(indirect) }.unwrap(); 369 let remade = unsafe { $typ::borrow_ptr(indirect, interrogation.len()) };
455 let messages: Vec<OwnedMessage> = unsafe { remade.iter(interrogation.count) } 370 let messages: Vec<OwnedMessage> = unsafe { remade }
456 .map(TryInto::try_into) 371 .map(TryInto::try_into)
457 .collect::<Result<_>>() 372 .collect::<Result<_>>()
458 .unwrap(); 373 .unwrap();
459 let [bin, choose] = messages.try_into().unwrap(); 374 let [bin, choose] = messages.try_into().unwrap();
460 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66)); 375 assert_matches!(bin => OwnedMessage::BinaryPrompt, (&[5, 4, 3, 2, 1][..], 66));
461 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose"); 376 assert_matches!(choose => OwnedMessage::RadioPrompt, "you must choose");
462 } 377 }
463 } 378 }
464 }} 379 }}
465 380
466 tests!(test_xsso<StandardIndirect>); 381 tests!(test_xsso<XSsoQuestions>);
467 tests!(test_linux<LinuxPamIndirect>); 382 tests!(test_linux<LinuxPamQuestions>);
468 } 383 }