cradle/
error.rs

1//! The [`Error`] type used in the return type of [`run_result!`].
2
3use crate::config::Config;
4use std::{ffi::OsString, fmt::Display, io, process::ExitStatus, string::FromUtf8Error};
5
6/// Error type returned when an error occurs while using [`run_result!`]
7/// or [`crate::input::Input::run_result`].
8///
9/// [`run!`], [`crate::input::Input::run`], [`run_output!`],
10/// and [`crate::input::Input::run_output`] will turn these errors
11/// into panics.
12#[derive(Debug)]
13pub enum Error {
14    /// The [`Input`](crate::Input)s to a command must produce
15    /// at least one argument: the executable to run.
16    ///
17    /// ```
18    /// use cradle::prelude::*;
19    ///
20    /// let result: Result<(), cradle::Error> = run_result!(());
21    /// match result {
22    ///   Err(Error::NoExecutableGiven) => {}
23    ///   _ => panic!(),
24    /// }
25    /// ```
26    NoExecutableGiven,
27    /// A `file not found` error occurred while trying to spawn
28    /// the child process:
29    ///
30    /// ```
31    /// use cradle::prelude::*;
32    ///
33    /// let result: Result<(), Error> = run_result!("does-not-exist");
34    /// match result {
35    ///   Err(Error::FileNotFound { .. }) => {}
36    ///   _ => panic!(),
37    /// }
38    /// ```
39    ///
40    /// Note that this error doesn't necessarily mean that the executable file
41    /// could not be found.
42    /// A few other circumstances in which this can occur are:
43    ///
44    /// - a binary is dynamically linked against a library,
45    ///   but that library cannot be found, or
46    /// - the executable starts with a
47    ///   [shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)),
48    ///   but the interpreter specified in the shebang cannot be found.
49    FileNotFound {
50        executable: OsString,
51        source: io::Error,
52    },
53    /// An IO error during execution. A few circumstances in which this can occur are:
54    ///
55    /// - spawning the child process fails (for another reason than
56    ///   [`FileNotFound`](Error::FileNotFound)),
57    /// - writing to `stdin` of the child process fails,
58    /// - reading from `stdout` or `stderr` of the child process fails,
59    /// - writing to the parent's `stdout` or `stderr` fails,
60    /// - the given executable doesn't have the executable flag set.
61    CommandIoError { message: String, source: io::Error },
62    /// The child process exited with a non-zero exit code.
63    ///
64    /// ```
65    /// use cradle::prelude::*;
66    ///
67    /// let result: Result<(), cradle::Error> = run_result!("false");
68    /// match result {
69    ///   Err(Error::NonZeroExitCode { .. }) => {}
70    ///   _ => panic!(),
71    /// }
72    /// ```
73    ///
74    /// This error will be suppressed when [`Status`](crate::Status) is used.
75    NonZeroExitCode {
76        full_command: String,
77        exit_status: ExitStatus,
78    },
79    /// The child process's `stdout` is being captured,
80    /// (e.g. with [`StdoutUntrimmed`](crate::StdoutUntrimmed)),
81    /// but the process wrote bytes to its `stdout` that are not
82    /// valid utf-8.
83    InvalidUtf8ToStdout {
84        full_command: String,
85        source: FromUtf8Error,
86    },
87    /// The child process's `stderr` is being captured,
88    /// (with [`Stderr`](crate::Stderr)),
89    /// but the process wrote bytes to its `stderr` that are not
90    /// valid utf-8.
91    InvalidUtf8ToStderr {
92        full_command: String,
93        source: FromUtf8Error,
94    },
95    /// This error is raised when an internal invariant of `cradle` is broken,
96    /// and likely indicates a bug.
97    Internal {
98        message: String,
99        full_command: String,
100        config: Config,
101    },
102}
103
104impl Error {
105    pub(crate) fn command_io_error(config: &Config, source: io::Error) -> Error {
106        Error::CommandIoError {
107            message: format!("{}:\n  {}", config.full_command(), source),
108            source,
109        }
110    }
111
112    pub(crate) fn internal(message: &str, config: &Config) -> Error {
113        Error::Internal {
114            message: message.to_string(),
115            full_command: config.full_command(),
116            config: config.clone(),
117        }
118    }
119}
120
121#[doc(hidden)]
122#[rustversion::attr(since(1.46), track_caller)]
123pub fn panic_on_error<T>(result: Result<T, Error>) -> T {
124    match result {
125        Ok(t) => t,
126        Err(error) => panic!("cradle error: {}", error),
127    }
128}
129
130fn english_list(list: &[&str]) -> String {
131    let mut result = String::new();
132    for (i, word) in list.iter().enumerate() {
133        let is_first = i == 0;
134        let is_last = i == list.len() - 1;
135        if !is_first {
136            result.push_str(if is_last { " and " } else { ", " });
137        }
138        result.push('\'');
139        result.push_str(word);
140        result.push('\'');
141    }
142    result
143}
144
145fn executable_with_whitespace_note(executable: &str) -> Option<String> {
146    let words = executable.split_whitespace().collect::<Vec<&str>>();
147    if words.len() >= 2 {
148        let intended_executable = words[0];
149        let intended_arguments = &words[1..];
150        let mut result = format!(
151            "note: Given executable name '{}' contains whitespace.\n",
152            executable
153        );
154        result.push_str(&format!(
155            "  Did you mean to run '{}', with {} as {}?\n",
156            intended_executable,
157            english_list(intended_arguments),
158            if intended_arguments.len() == 1 {
159                "the argument"
160            } else {
161                "arguments"
162            },
163        ));
164        result.push_str(concat!(
165            "  Consider using Split: ",
166            "https://docs.rs/cradle/latest/cradle/input/struct.Split.html"
167        ));
168        Some(result)
169    } else {
170        None
171    }
172}
173
174impl Display for Error {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        use Error::*;
177        match self {
178            NoExecutableGiven => write!(f, "no arguments given"),
179            FileNotFound { executable, .. } => {
180                let executable = executable.to_string_lossy();
181                write!(f, "File not found error when executing '{}'", executable)?;
182                if let Some(whitespace_note) = executable_with_whitespace_note(executable.as_ref())
183                {
184                    write!(f, "\n{}", whitespace_note)?;
185                }
186                Ok(())
187            }
188            CommandIoError { message, .. } => write!(f, "{}", message),
189            NonZeroExitCode {
190                full_command,
191                exit_status,
192            } => {
193                if let Some(exit_code) = exit_status.code() {
194                    write!(
195                        f,
196                        "{}:\n  exited with exit code: {}",
197                        full_command, exit_code
198                    )
199                } else {
200                    write!(f, "{}:\n  exited with {}", full_command, exit_status)
201                }
202            }
203            InvalidUtf8ToStdout { full_command, .. } => {
204                write!(f, "{}:\n  invalid utf-8 written to stdout", full_command)
205            }
206            InvalidUtf8ToStderr { full_command, .. } => {
207                write!(f, "{}:\n  invalid utf-8 written to stderr", full_command)
208            }
209            Internal { .. } => {
210                let snippets = vec![
211                    "Congratulations, you've found a bug in cradle! :/",
212                    "Please, open an issue on https://github.com/soenkehahn/cradle/issues",
213                    "with the following information:",
214                ];
215                writeln!(f, "{}\n{:#?}", snippets.join(" "), self)
216            }
217        }
218    }
219}
220
221impl std::error::Error for Error {
222    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
223        use Error::*;
224        match self {
225            FileNotFound { source, .. } | CommandIoError { source, .. } => Some(source),
226            InvalidUtf8ToStdout { source, .. } | InvalidUtf8ToStderr { source, .. } => Some(source),
227            NoExecutableGiven | NonZeroExitCode { .. } | Internal { .. } => None,
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235    use crate::prelude::*;
236    use executable_path::executable_path;
237
238    #[test]
239    fn invalid_utf8_to_stdout_has_source() {
240        let result: Result<StdoutUntrimmed, crate::Error> = run_result!(
241            executable_path("cradle_test_helper").to_str().unwrap(),
242            "invalid utf-8 stdout"
243        );
244        assert!(std::error::Error::source(&result.unwrap_err()).is_some());
245    }
246
247    #[test]
248    fn invalid_utf8_to_stderr_has_source() {
249        let result: Result<Stderr, crate::Error> = run_result!(
250            executable_path("cradle_test_helper").to_str().unwrap(),
251            "invalid utf-8 stderr"
252        );
253        assert!(std::error::Error::source(&result.unwrap_err()).is_some());
254    }
255
256    mod english_list {
257        use super::*;
258        use pretty_assertions::assert_eq;
259
260        macro_rules! test {
261            ($name:ident, $input:expr, $expected:expr) => {
262                #[test]
263                fn $name() {
264                    assert_eq!(english_list($input), $expected);
265                }
266            };
267        }
268
269        test!(one, &["foo"], "'foo'");
270        test!(two, &["foo", "bar"], "'foo' and 'bar'");
271        test!(three, &["foo", "bar", "baz"], "'foo', 'bar' and 'baz'");
272        test!(
273            four,
274            &["foo", "bar", "baz", "boo"],
275            "'foo', 'bar', 'baz' and 'boo'"
276        );
277    }
278}