comparison src/conv.rs @ 143:ebb71a412b58

Turn everything into OsString and Just Walk Out! for strings with nul. To reduce the hazard surface of the API, this replaces most uses of &str with &OsStr (and likewise with String/OsString). Also, I've decided that instead of dealing with callers putting `\0` in their parameters, I'm going to follow the example of std::env and Just Walk Out! (i.e., panic!()). This makes things a lot less annoying for both me and (hopefully) users.
author Paul Fisher <paul@pfish.zone>
date Sat, 05 Jul 2025 22:12:46 -0400
parents 80c07e5ab22f
children 1bc52025156b
comparison
equal deleted inserted replaced
142:5c1e315c18ff 143:ebb71a412b58
3 // Temporarily allowed until we get the actual conversation functions hooked up. 3 // Temporarily allowed until we get the actual conversation functions hooked up.
4 #![allow(dead_code)] 4 #![allow(dead_code)]
5 5
6 use crate::constants::{ErrorCode, Result}; 6 use crate::constants::{ErrorCode, Result};
7 use std::cell::Cell; 7 use std::cell::Cell;
8 use std::ffi::{OsStr, OsString};
8 use std::fmt; 9 use std::fmt;
9 use std::fmt::Debug; 10 use std::fmt::Debug;
10 use std::result::Result as StdResult; 11 use std::result::Result as StdResult;
11 12
12 /// An individual pair of request/response to be sent to the user. 13 /// An individual pair of request/response to be sent to the user.
29 /// ``` 30 /// ```
30 /// use nonstick::conv::{Exchange, QAndA}; 31 /// use nonstick::conv::{Exchange, QAndA};
31 /// use nonstick::ErrorCode; 32 /// use nonstick::ErrorCode;
32 /// 33 ///
33 /// fn cant_respond(message: Exchange) { 34 /// fn cant_respond(message: Exchange) {
35 /// // "question" is kind of a bad name in the context of
36 /// // a one-way message, but it's for consistency.
34 /// match message { 37 /// match message {
35 /// Exchange::Info(i) => { 38 /// Exchange::Info(i) => {
36 /// eprintln!("fyi, {}", i.question()); 39 /// eprintln!("fyi, {:?}", i.question());
37 /// i.set_answer(Ok(())) 40 /// i.set_answer(Ok(()))
38 /// } 41 /// }
39 /// Exchange::Error(e) => { 42 /// Exchange::Error(e) => {
40 /// eprintln!("ERROR: {}", e.question()); 43 /// eprintln!("ERROR: {:?}", e.question());
41 /// e.set_answer(Ok(())) 44 /// e.set_answer(Ok(()))
42 /// } 45 /// }
43 /// // We can't answer any questions. 46 /// // We can't answer any questions.
44 /// other => other.set_error(ErrorCode::ConversationError), 47 /// other => other.set_error(ErrorCode::ConversationError),
45 /// } 48 /// }
116 119
117 q_and_a!( 120 q_and_a!(
118 /// A Q&A that asks the user for text and does not show it while typing. 121 /// A Q&A that asks the user for text and does not show it while typing.
119 /// 122 ///
120 /// In other words, a password entry prompt. 123 /// In other words, a password entry prompt.
121 MaskedQAndA<'a, Q=&'a str, A=String>, 124 MaskedQAndA<'a, Q=&'a OsStr, A=OsString>,
122 Exchange::MaskedPrompt 125 Exchange::MaskedPrompt
123 ); 126 );
124 127
125 q_and_a!( 128 q_and_a!(
126 /// A standard Q&A prompt that asks the user for text. 129 /// A standard Q&A prompt that asks the user for text.
127 /// 130 ///
128 /// This is the normal "ask a person a question" prompt. 131 /// This is the normal "ask a person a question" prompt.
129 /// When the user types, their input will be shown to them. 132 /// When the user types, their input will be shown to them.
130 /// It can be used for things like usernames. 133 /// It can be used for things like usernames.
131 QAndA<'a, Q=&'a str, A=String>, 134 QAndA<'a, Q=&'a OsStr, A=OsString>,
132 Exchange::Prompt 135 Exchange::Prompt
133 ); 136 );
134 137
135 q_and_a!( 138 q_and_a!(
136 /// A Q&A for "radio button"–style data. (Linux-PAM extension) 139 /// A Q&A for "radio button"–style data. (Linux-PAM extension)
137 /// 140 ///
138 /// This message type is theoretically useful for "yes/no/maybe" 141 /// This message type is theoretically useful for "yes/no/maybe"
139 /// questions, but nowhere in the documentation is it specified 142 /// questions, but nowhere in the documentation is it specified
140 /// what the format of the answer will be, or how this should be shown. 143 /// what the format of the answer will be, or how this should be shown.
141 RadioQAndA<'a, Q=&'a str, A=String>, 144 RadioQAndA<'a, Q=&'a OsStr, A=OsString>,
142 Exchange::RadioPrompt 145 Exchange::RadioPrompt
143 ); 146 );
144 147
145 q_and_a!( 148 q_and_a!(
146 /// Asks for binary data. (Linux-PAM extension) 149 /// Asks for binary data. (Linux-PAM extension)
201 /// A message containing information to be passed to the user. 204 /// A message containing information to be passed to the user.
202 /// 205 ///
203 /// While this does not have an answer, [`Conversation`] implementations 206 /// While this does not have an answer, [`Conversation`] implementations
204 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that 207 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
205 /// the message has been displayed (or actively discarded). 208 /// the message has been displayed (or actively discarded).
206 InfoMsg<'a, Q = &'a str, A = ()>, 209 InfoMsg<'a, Q = &'a OsStr, A = ()>,
207 Exchange::Info 210 Exchange::Info
208 ); 211 );
209 212
210 q_and_a!( 213 q_and_a!(
211 /// An error message to be passed to the user. 214 /// An error message to be passed to the user.
212 /// 215 ///
213 /// While this does not have an answer, [`Conversation`] implementations 216 /// While this does not have an answer, [`Conversation`] implementations
214 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that 217 /// should still call [`set_answer`][`QAndA::set_answer`] to verify that
215 /// the message has been displayed (or actively discarded). 218 /// the message has been displayed (or actively discarded).
216 ErrorMsg<'a, Q = &'a str, A = ()>, 219 ErrorMsg<'a, Q = &'a OsStr, A = ()>,
217 Exchange::Error 220 Exchange::Error
218 ); 221 );
219 222
220 /// A channel for PAM modules to request information from the user. 223 /// A channel for PAM modules to request information from the user.
221 /// 224 ///
284 /// 287 ///
285 /// For example, to use a `Conversation` as a `ConversationAdapter`: 288 /// For example, to use a `Conversation` as a `ConversationAdapter`:
286 /// 289 ///
287 /// ``` 290 /// ```
288 /// # use nonstick::{Conversation, Result}; 291 /// # use nonstick::{Conversation, Result};
292 /// # use std::ffi::OsString;
289 /// // Bring this trait into scope to get `masked_prompt`, among others. 293 /// // Bring this trait into scope to get `masked_prompt`, among others.
290 /// use nonstick::ConversationAdapter; 294 /// use nonstick::ConversationAdapter;
291 /// 295 ///
292 /// fn ask_for_token(convo: &impl Conversation) -> Result<String> { 296 /// fn ask_for_token(convo: &impl Conversation) -> Result<OsString> {
293 /// convo.masked_prompt("enter your one-time token") 297 /// convo.masked_prompt("enter your one-time token")
294 /// } 298 /// }
295 /// ``` 299 /// ```
296 /// 300 ///
297 /// or to use a `ConversationAdapter` as a `Conversation`: 301 /// or to use a `ConversationAdapter` as a `Conversation`:
298 /// 302 ///
299 /// ``` 303 /// ```
300 /// use nonstick::{Conversation, ConversationAdapter}; 304 /// use nonstick::{Conversation, ConversationAdapter};
301 /// # use nonstick::{BinaryData, Result}; 305 /// # use nonstick::{BinaryData, Result};
306 /// # use std::ffi::{OsStr, OsString};
302 /// mod some_library { 307 /// mod some_library {
303 /// # use nonstick::Conversation; 308 /// # use nonstick::Conversation;
304 /// pub fn get_auth_data(conv: &impl Conversation) { /* ... */ 309 /// pub fn get_auth_data(conv: &impl Conversation) { /* ... */
305 /// } 310 /// }
306 /// } 311 /// }
308 /// struct MySimpleConvo {/* ... */} 313 /// struct MySimpleConvo {/* ... */}
309 /// # impl MySimpleConvo { fn new() -> Self { Self{} } } 314 /// # impl MySimpleConvo { fn new() -> Self { Self{} } }
310 /// 315 ///
311 /// impl ConversationAdapter for MySimpleConvo { 316 /// impl ConversationAdapter for MySimpleConvo {
312 /// // ... 317 /// // ...
313 /// # fn prompt(&self, request: &str) -> Result<String> { 318 /// # fn prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
314 /// # unimplemented!() 319 /// # unimplemented!()
315 /// # } 320 /// # }
316 /// # 321 /// #
317 /// # fn masked_prompt(&self, request: &str) -> Result<String> { 322 /// # fn masked_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
318 /// # unimplemented!() 323 /// # unimplemented!()
319 /// # } 324 /// # }
320 /// # 325 /// #
321 /// # fn error_msg(&self, message: &str) { 326 /// # fn error_msg(&self, message: impl AsRef<OsStr>) {
322 /// # unimplemented!() 327 /// # unimplemented!()
323 /// # } 328 /// # }
324 /// # 329 /// #
325 /// # fn info_msg(&self, message: &str) { 330 /// # fn info_msg(&self, message: impl AsRef<OsStr>) {
326 /// # unimplemented!() 331 /// # unimplemented!()
327 /// # } 332 /// # }
328 /// # 333 /// #
329 /// # fn radio_prompt(&self, request: &str) -> Result<String> { 334 /// # fn radio_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
330 /// # unimplemented!() 335 /// # unimplemented!()
331 /// # } 336 /// # }
332 /// # 337 /// #
333 /// # fn binary_prompt(&self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> { 338 /// # fn binary_prompt(&self, (data, data_type): (&[u8], u8)) -> Result<BinaryData> {
334 /// # unimplemented!() 339 /// # unimplemented!()
351 Self: Sized, 356 Self: Sized,
352 { 357 {
353 Demux(self) 358 Demux(self)
354 } 359 }
355 /// Prompts the user for something. 360 /// Prompts the user for something.
356 fn prompt(&self, request: &str) -> Result<String>; 361 fn prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString>;
357 /// Prompts the user for something, but hides what the user types. 362 /// Prompts the user for something, but hides what the user types.
358 fn masked_prompt(&self, request: &str) -> Result<String>; 363 fn masked_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString>;
359 /// Alerts the user to an error. 364 /// Alerts the user to an error.
360 fn error_msg(&self, message: &str); 365 fn error_msg(&self, message: impl AsRef<OsStr>);
361 /// Sends an informational message to the user. 366 /// Sends an informational message to the user.
362 fn info_msg(&self, message: &str); 367 fn info_msg(&self, message: impl AsRef<OsStr>);
363 /// \[Linux extension] Prompts the user for a yes/no/maybe conditional. 368 /// \[Linux extension] Prompts the user for a yes/no/maybe conditional.
364 /// 369 ///
365 /// PAM documentation doesn't define the format of the response. 370 /// PAM documentation doesn't define the format of the response.
366 /// 371 ///
367 /// When called on an implementation that doesn't support radio prompts, 372 /// When called on an implementation that doesn't support radio prompts,
368 /// this will return [`ErrorCode::ConversationError`]. 373 /// this will return [`ErrorCode::ConversationError`].
369 /// If implemented on an implementation that doesn't support radio prompts, 374 /// If implemented on an implementation that doesn't support radio prompts,
370 /// this will never be called. 375 /// this will never be called.
371 fn radio_prompt(&self, request: &str) -> Result<String> { 376 fn radio_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
372 let _ = request; 377 let _ = request;
373 Err(ErrorCode::ConversationError) 378 Err(ErrorCode::ConversationError)
374 } 379 }
375 /// \[Linux extension] Requests binary data from the user. 380 /// \[Linux extension] Requests binary data from the user.
376 /// 381 ///
389 Demux(value) 394 Demux(value)
390 } 395 }
391 } 396 }
392 397
393 macro_rules! conv_fn { 398 macro_rules! conv_fn {
394 ($(#[$m:meta])* $fn_name:ident($($param:tt: $pt:ty),+) -> $resp_type:ty { $msg:ty }) => { 399 ($(#[$m:meta])* $fn_name:ident($param:tt: $pt:ty) -> $resp_type:ty { $msg:ty }) => {
395 $(#[$m])* 400 $(#[$m])*
396 fn $fn_name(&self, $($param: $pt),*) -> Result<$resp_type> { 401 fn $fn_name(&self, $param: impl AsRef<$pt>) -> Result<$resp_type> {
397 let prompt = <$msg>::new($($param),*); 402 let prompt = <$msg>::new($param.as_ref());
398 self.communicate(&[prompt.exchange()]); 403 self.communicate(&[prompt.exchange()]);
399 prompt.answer() 404 prompt.answer()
400 } 405 }
401 }; 406 };
402 ($(#[$m:meta])*$fn_name:ident($($param:tt: $pt:ty),+) { $msg:ty }) => { 407 ($(#[$m:meta])*$fn_name:ident($param:tt: $pt:ty) { $msg:ty }) => {
403 $(#[$m])* 408 $(#[$m])*
404 fn $fn_name(&self, $($param: $pt),*) { 409 fn $fn_name(&self, $param: impl AsRef<$pt>) {
405 self.communicate(&[<$msg>::new($($param),*).exchange()]); 410 self.communicate(&[<$msg>::new($param.as_ref()).exchange()]);
406 } 411 }
407 }; 412 };
408 } 413 }
409 414
410 impl<C: Conversation> ConversationAdapter for C { 415 impl<C: Conversation> ConversationAdapter for C {
411 conv_fn!(prompt(message: &str) -> String { QAndA }); 416 conv_fn!(prompt(message: OsStr) -> OsString { QAndA });
412 conv_fn!(masked_prompt(message: &str) -> String { MaskedQAndA } ); 417 conv_fn!(masked_prompt(message: OsStr) -> OsString { MaskedQAndA } );
413 conv_fn!(error_msg(message: &str) { ErrorMsg }); 418 conv_fn!(error_msg(message: OsStr) { ErrorMsg });
414 conv_fn!(info_msg(message: &str) { InfoMsg }); 419 conv_fn!(info_msg(message: OsStr) { InfoMsg });
415 conv_fn!(radio_prompt(message: &str) -> String { RadioQAndA }); 420 conv_fn!(radio_prompt(message: OsStr) -> OsString { RadioQAndA });
416 conv_fn!(binary_prompt((data, data_type): (&[u8], u8)) -> BinaryData { BinaryQAndA }); 421 fn binary_prompt(&self, (data, typ): (&[u8], u8)) -> Result<BinaryData> {
422 let prompt = BinaryQAndA::new((data, typ));
423 self.communicate(&[prompt.exchange()]);
424 prompt.answer()
425 }
417 } 426 }
418 427
419 /// A [`Conversation`] which asks the questions one at a time. 428 /// A [`Conversation`] which asks the questions one at a time.
420 /// 429 ///
421 /// This is automatically created by [`ConversationAdapter::into_conversation`]. 430 /// This is automatically created by [`ConversationAdapter::into_conversation`].
457 } 466 }
458 467
459 #[cfg(test)] 468 #[cfg(test)]
460 mod tests { 469 mod tests {
461 use super::*; 470 use super::*;
462 use crate::constants::ErrorCode;
463 471
464 #[test] 472 #[test]
465 fn test_demux() { 473 fn test_demux() {
466 #[derive(Default)] 474 #[derive(Default)]
467 struct DemuxTester { 475 struct DemuxTester {
468 error_ran: Cell<bool>, 476 error_ran: Cell<bool>,
469 info_ran: Cell<bool>, 477 info_ran: Cell<bool>,
470 } 478 }
471 479
472 impl ConversationAdapter for DemuxTester { 480 impl ConversationAdapter for DemuxTester {
473 fn prompt(&self, request: &str) -> Result<String> { 481 fn prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
474 match request { 482 match request.as_ref().to_str().unwrap() {
475 "what" => Ok("whatwhat".to_owned()), 483 "what" => Ok("whatwhat".into()),
476 "give_err" => Err(ErrorCode::PermissionDenied), 484 "give_err" => Err(ErrorCode::PermissionDenied),
477 _ => panic!("unexpected prompt!"), 485 _ => panic!("unexpected prompt!"),
478 } 486 }
479 } 487 }
480 fn masked_prompt(&self, request: &str) -> Result<String> { 488 fn masked_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
481 assert_eq!("reveal", request); 489 assert_eq!("reveal", request.as_ref());
482 Ok("my secrets".to_owned()) 490 Ok("my secrets".into())
483 } 491 }
484 fn error_msg(&self, message: &str) { 492 fn error_msg(&self, message: impl AsRef<OsStr>) {
485 self.error_ran.set(true); 493 self.error_ran.set(true);
486 assert_eq!("whoopsie", message); 494 assert_eq!("whoopsie", message.as_ref());
487 } 495 }
488 fn info_msg(&self, message: &str) { 496 fn info_msg(&self, message: impl AsRef<OsStr>) {
489 self.info_ran.set(true); 497 self.info_ran.set(true);
490 assert_eq!("did you know", message); 498 assert_eq!("did you know", message.as_ref());
491 } 499 }
492 fn radio_prompt(&self, request: &str) -> Result<String> { 500 fn radio_prompt(&self, request: impl AsRef<OsStr>) -> Result<OsString> {
493 assert_eq!("channel?", request); 501 assert_eq!("channel?", request.as_ref());
494 Ok("zero".to_owned()) 502 Ok("zero".into())
495 } 503 }
496 fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> { 504 fn binary_prompt(&self, data_and_type: (&[u8], u8)) -> Result<BinaryData> {
497 assert_eq!((&[10, 9, 8][..], 66), data_and_type); 505 assert_eq!((&[10, 9, 8][..], 66), data_and_type);
498 Ok(BinaryData::new(vec![5, 5, 5], 5)) 506 Ok(BinaryData::new(vec![5, 5, 5], 5))
499 } 507 }
500 } 508 }
501 509
502 let tester = DemuxTester::default(); 510 let tester = DemuxTester::default();
503 511
504 let what = QAndA::new("what"); 512 let what = QAndA::new("what".as_ref());
505 let pass = MaskedQAndA::new("reveal"); 513 let pass = MaskedQAndA::new("reveal".as_ref());
506 let err = ErrorMsg::new("whoopsie"); 514 let err = ErrorMsg::new("whoopsie".as_ref());
507 let info = InfoMsg::new("did you know"); 515 let info = InfoMsg::new("did you know".as_ref());
508 let has_err = QAndA::new("give_err"); 516 let has_err = QAndA::new("give_err".as_ref());
509 517
510 let conv = tester.into_conversation(); 518 let conv = tester.into_conversation();
511 519
512 // Basic tests. 520 // Basic tests.
513 521
530 538
531 // Test the Linux extensions separately. 539 // Test the Linux extensions separately.
532 { 540 {
533 let conv = tester.into_conversation(); 541 let conv = tester.into_conversation();
534 542
535 let radio = RadioQAndA::new("channel?"); 543 let radio = RadioQAndA::new("channel?".as_ref());
536 let bin = BinaryQAndA::new((&[10, 9, 8], 66)); 544 let bin = BinaryQAndA::new((&[10, 9, 8], 66));
537 conv.communicate(&[radio.exchange(), bin.exchange()]); 545 conv.communicate(&[radio.exchange(), bin.exchange()]);
538 546
539 assert_eq!("zero", radio.answer().unwrap()); 547 assert_eq!("zero", radio.answer().unwrap());
540 assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap()); 548 assert_eq!(BinaryData::from(([5, 5, 5], 5)), bin.answer().unwrap());
554 } 562 }
555 Exchange::Error(error) => { 563 Exchange::Error(error) => {
556 assert_eq!("oh no", error.question()); 564 assert_eq!("oh no", error.question());
557 error.set_answer(Ok(())) 565 error.set_answer(Ok(()))
558 } 566 }
559 Exchange::Prompt(prompt) => prompt.set_answer(match prompt.question() { 567 Exchange::Prompt(prompt) => {
560 "should_err" => Err(ErrorCode::PermissionDenied), 568 prompt.set_answer(match prompt.question().to_str().unwrap() {
561 "question" => Ok("answer".to_owned()), 569 "should_err" => Err(ErrorCode::PermissionDenied),
562 other => panic!("unexpected question {other:?}"), 570 "question" => Ok("answer".into()),
563 }), 571 other => panic!("unexpected question {other:?}"),
572 })
573 }
564 Exchange::MaskedPrompt(ask) => { 574 Exchange::MaskedPrompt(ask) => {
565 assert_eq!("password!", ask.question()); 575 assert_eq!("password!", ask.question());
566 ask.set_answer(Ok("open sesame".into())) 576 ask.set_answer(Ok("open sesame".into()))
567 } 577 }
568 Exchange::BinaryPrompt(prompt) => { 578 Exchange::BinaryPrompt(prompt) => {
569 assert_eq!((&[1, 2, 3][..], 69), prompt.question()); 579 assert_eq!((&[1, 2, 3][..], 69), prompt.question());
570 prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42)))) 580 prompt.set_answer(Ok(BinaryData::from((&[3, 2, 1], 42))))
571 } 581 }
572 Exchange::RadioPrompt(ask) => { 582 Exchange::RadioPrompt(ask) => {
573 assert_eq!("radio?", ask.question()); 583 assert_eq!("radio?", ask.question());
574 ask.set_answer(Ok("yes".to_owned())) 584 ask.set_answer(Ok("yes".into()))
575 } 585 }
576 } 586 }
577 } else { 587 } else {
578 panic!( 588 panic!(
579 "there should only be one message, not {len}", 589 "there should only be one message, not {len}",