Mercurial > crates > nonstick
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}", |