Report errors for non-UTF-8 entries in Zip and 7z
[ouch.git] / src / utils / question.rs
blob713931e39fe295f4afd6bad6177ca2294bc858a5
1 //! Utils related to asking [Y/n] questions to the user.
2 //!
3 //! Example:
4 //!   "Do you want to overwrite 'archive.tar.gz'? [Y/n]"
6 use std::{
7     borrow::Cow,
8     io::{self, Write},
9     path::Path,
12 use fs_err as fs;
14 use super::{strip_cur_dir, to_utf};
15 use crate::{
16     accessible::is_running_in_accessible_mode,
17     error::{Error, FinalError, Result},
18     utils::{self, colors},
21 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
22 /// Determines if overwrite questions should be skipped or asked to the user
23 pub enum QuestionPolicy {
24     /// Ask the user every time
25     Ask,
26     /// Set by `--yes`, will say 'Y' to all overwrite questions
27     AlwaysYes,
28     /// Set by `--no`, will say 'N' to all overwrite questions
29     AlwaysNo,
32 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
33 /// Determines which action is being questioned
34 pub enum QuestionAction {
35     /// question called from a compression function
36     Compression,
37     /// question called from a decompression function
38     Decompression,
41 /// Check if QuestionPolicy flags were set, otherwise, ask user if they want to overwrite.
42 pub fn user_wants_to_overwrite(path: &Path, question_policy: QuestionPolicy) -> crate::Result<bool> {
43     match question_policy {
44         QuestionPolicy::AlwaysYes => Ok(true),
45         QuestionPolicy::AlwaysNo => Ok(false),
46         QuestionPolicy::Ask => {
47             let path = to_utf(strip_cur_dir(path));
48             let path = Some(&*path);
49             let placeholder = Some("FILE");
50             Confirmation::new("Do you want to overwrite 'FILE'?", placeholder).ask(path)
51         }
52     }
55 /// Create the file if it doesn't exist and if it does then ask to overwrite it.
56 /// If the user doesn't want to overwrite then we return [`Ok(None)`]
57 pub fn ask_to_create_file(path: &Path, question_policy: QuestionPolicy) -> Result<Option<fs::File>> {
58     match fs::OpenOptions::new().write(true).create_new(true).open(path) {
59         Ok(w) => Ok(Some(w)),
60         Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
61             if user_wants_to_overwrite(path, question_policy)? {
62                 utils::remove_file_or_dir(path)?;
63                 Ok(Some(fs::File::create(path)?))
64             } else {
65                 Ok(None)
66             }
67         }
68         Err(e) => Err(Error::from(e)),
69     }
72 /// Check if QuestionPolicy flags were set, otherwise, ask the user if they want to continue.
73 pub fn user_wants_to_continue(
74     path: &Path,
75     question_policy: QuestionPolicy,
76     question_action: QuestionAction,
77 ) -> crate::Result<bool> {
78     match question_policy {
79         QuestionPolicy::AlwaysYes => Ok(true),
80         QuestionPolicy::AlwaysNo => Ok(false),
81         QuestionPolicy::Ask => {
82             let action = match question_action {
83                 QuestionAction::Compression => "compress",
84                 QuestionAction::Decompression => "decompress",
85             };
86             let path = to_utf(strip_cur_dir(path));
87             let path = Some(&*path);
88             let placeholder = Some("FILE");
89             Confirmation::new(&format!("Do you want to {action} 'FILE'?"), placeholder).ask(path)
90         }
91     }
94 /// Confirmation dialog for end user with [Y/n] question.
95 ///
96 /// If the placeholder is found in the prompt text, it will be replaced to form the final message.
97 pub struct Confirmation<'a> {
98     /// The message to be displayed with the placeholder text in it.
99     /// e.g.: "Do you want to overwrite 'FILE'?"
100     pub prompt: &'a str,
102     /// The placeholder text that will be replaced in the `ask` function:
103     /// e.g.: Some("FILE")
104     pub placeholder: Option<&'a str>,
107 impl<'a> Confirmation<'a> {
108     /// Creates a new Confirmation.
109     pub const fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
110         Self {
111             prompt,
112             placeholder: pattern,
113         }
114     }
116     /// Creates user message and receives a boolean input to be used on the program
117     pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
118         let message = match (self.placeholder, substitute) {
119             (None, _) => Cow::Borrowed(self.prompt),
120             (Some(_), None) => unreachable!("dev error, should be reported, we checked this won't happen"),
121             (Some(placeholder), Some(subs)) => Cow::Owned(self.prompt.replace(placeholder, subs)),
122         };
124         // Ask the same question to end while no valid answers are given
125         loop {
126             if is_running_in_accessible_mode() {
127                 print!(
128                     "{} {}yes{}/{}no{}: ",
129                     message,
130                     *colors::GREEN,
131                     *colors::RESET,
132                     *colors::RED,
133                     *colors::RESET
134                 );
135             } else {
136                 print!(
137                     "{} [{}Y{}/{}n{}] ",
138                     message,
139                     *colors::GREEN,
140                     *colors::RESET,
141                     *colors::RED,
142                     *colors::RESET
143                 );
144             }
145             io::stdout().flush()?;
147             let mut answer = String::new();
148             let bytes_read = io::stdin().read_line(&mut answer)?;
150             if bytes_read == 0 {
151                 let error = FinalError::with_title("Unexpected EOF when asking question.")
152                     .detail("When asking the user:")
153                     .detail(format!("  \"{message}\""))
154                     .detail("Expected 'y' or 'n' as answer, but found EOF instead.")
155                     .hint("If using Ouch in scripting, consider using `--yes` and `--no`.");
157                 return Err(error.into());
158             }
160             answer.make_ascii_lowercase();
161             match answer.trim() {
162                 "" | "y" | "yes" => return Ok(true),
163                 "n" | "no" => return Ok(false),
164                 _ => continue, // Try again
165             }
166         }
167     }