1use crate::config::Config;
4use std::{ffi::OsString, fmt::Display, io, process::ExitStatus, string::FromUtf8Error};
5
6#[derive(Debug)]
13pub enum Error {
14 NoExecutableGiven,
27 FileNotFound {
50 executable: OsString,
51 source: io::Error,
52 },
53 CommandIoError { message: String, source: io::Error },
62 NonZeroExitCode {
76 full_command: String,
77 exit_status: ExitStatus,
78 },
79 InvalidUtf8ToStdout {
84 full_command: String,
85 source: FromUtf8Error,
86 },
87 InvalidUtf8ToStderr {
92 full_command: String,
93 source: FromUtf8Error,
94 },
95 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}