1use std::ffi::OsStr;
17use std::io;
18
19use prick_core::keyname::KeyNameError;
20
21use crate::guard::GuardError;
22
23pub const EXIT_NOT_EXECUTABLE: i32 = 126;
25
26pub const EXIT_NOT_FOUND: i32 = 127;
28
29#[derive(Debug, thiserror::Error)]
31#[non_exhaustive]
32pub enum LaunchError {
33 #[error("no command was given to run")]
35 NoProgram,
36
37 #[error("command not found: {program}")]
39 NotFound {
40 program: String,
42 },
43
44 #[error("permission denied: {program}")]
46 PermissionDenied {
47 program: String,
49 },
50
51 #[error("{program} is not an executable format")]
56 NoExecFormat {
57 program: String,
59 },
60
61 #[error("cannot inject `{key}` into the child environment: {source}")]
63 InvalidKey {
64 key: String,
66 source: KeyNameError,
68 },
69
70 #[error(transparent)]
72 Guard(#[from] GuardError),
73
74 #[error(transparent)]
76 CommandLine(#[from] crate::cmdline::CmdLineError),
77
78 #[error("could not run {program}: {source}")]
80 Io {
81 program: String,
83 source: io::Error,
85 },
86}
87
88impl LaunchError {
89 pub fn exit_code(&self) -> i32 {
91 match self {
92 Self::NotFound { .. } => EXIT_NOT_FOUND,
93 Self::PermissionDenied { .. } | Self::NoExecFormat { .. } => EXIT_NOT_EXECUTABLE,
94 Self::NoProgram
97 | Self::InvalidKey { .. }
98 | Self::Guard(_)
99 | Self::CommandLine(_)
100 | Self::Io { .. } => i32::from(prick_core::classify::EXIT_FAILURE),
101 }
102 }
103
104 pub fn hint(&self) -> Option<&'static str> {
106 match self {
107 Self::NotFound { .. } => Some(
108 "Check the spelling and that the program is on PATH. `prk run` never invokes a \
109 shell, so shell builtins and aliases are not available; write `prk run -- sh -c \
110 '...'` if you need one.",
111 ),
112 Self::PermissionDenied { .. } => {
113 Some("The file exists but is not executable. On Unix, check its mode bits.")
114 }
115 Self::NoExecFormat { .. } => Some(
118 "The file is not a program image. If it is a script, check that its first line is \
119 a shebang such as `#!/usr/bin/env node` and that the file has Unix line endings \
120 -- a CRLF makes the interpreter path unresolvable.",
121 ),
122 Self::Guard(_) => Some(
123 "Rename the secret, or pass --allow-unsafe-env if the child really is meant to be \
124 configured this way.",
125 ),
126 Self::CommandLine(_) => Some(
127 "The program is a .cmd or .bat shim, so its arguments pass through cmd.exe, which \
128 cannot carry a line break. Pass the value through a file or an environment \
129 variable instead.",
130 ),
131 Self::NoProgram | Self::InvalidKey { .. } | Self::Io { .. } => None,
132 }
133 }
134
135 pub fn from_io(program: &OsStr, source: io::Error) -> Self {
140 let program = program.to_string_lossy().into_owned();
141 match source.kind() {
142 io::ErrorKind::NotFound => Self::NotFound { program },
143 io::ErrorKind::PermissionDenied => Self::PermissionDenied { program },
144 _ => {
145 if is_exec_format_error(&source) {
146 Self::NoExecFormat { program }
147 } else {
148 Self::Io { program, source }
149 }
150 }
151 }
152 }
153}
154
155#[cfg(unix)]
157fn is_exec_format_error(source: &io::Error) -> bool {
158 source.raw_os_error() == Some(libc::ENOEXEC)
159}
160
161#[cfg(windows)]
163fn is_exec_format_error(source: &io::Error) -> bool {
164 const ERROR_BAD_EXE_FORMAT: i32 = 193;
166 source.raw_os_error() == Some(ERROR_BAD_EXE_FORMAT)
167}
168
169#[cfg(not(any(unix, windows)))]
170fn is_exec_format_error(_source: &io::Error) -> bool {
171 false
172}
173
174#[cfg(test)]
175mod tests {
176 use std::ffi::OsString;
177
178 use super::*;
179
180 #[test]
181 fn a_missing_command_exits_127_like_a_shell() {
182 let err = LaunchError::from_io(
183 &OsString::from("nosuchprogram"),
184 io::Error::new(io::ErrorKind::NotFound, "not found"),
185 );
186 assert!(matches!(err, LaunchError::NotFound { .. }));
187 assert_eq!(err.exit_code(), 127);
188 }
189
190 #[test]
191 fn an_unexecutable_command_exits_126_like_a_shell() {
192 let err = LaunchError::from_io(
193 &OsString::from("/etc/hosts"),
194 io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
195 );
196 assert!(matches!(err, LaunchError::PermissionDenied { .. }));
197 assert_eq!(err.exit_code(), 126);
198 }
199
200 #[cfg(unix)]
201 #[test]
202 fn enoexec_is_recognised_and_points_at_the_shebang() {
203 let err = LaunchError::from_io(
204 &OsString::from("./script"),
205 io::Error::from_raw_os_error(libc::ENOEXEC),
206 );
207 assert!(matches!(err, LaunchError::NoExecFormat { .. }));
208 assert_eq!(err.exit_code(), 126);
209 assert!(err.hint().is_some_and(|h| h.contains("shebang")));
210 }
211
212 #[cfg(windows)]
213 #[test]
214 fn a_non_image_file_is_recognised_and_points_at_the_shebang() {
215 let err =
216 LaunchError::from_io(&OsString::from("script.txt"), io::Error::from_raw_os_error(193));
217 assert!(matches!(err, LaunchError::NoExecFormat { .. }));
218 assert_eq!(err.exit_code(), 126);
219 assert!(err.hint().is_some_and(|h| h.contains("shebang")));
220 }
221
222 #[test]
223 fn the_message_names_the_program_but_never_a_value() {
224 let err = LaunchError::NotFound { program: "npm".to_owned() };
225 assert!(err.to_string().contains("npm"));
226 }
227
228 #[test]
229 fn a_guard_refusal_keeps_its_own_message_and_hint() {
230 let err = LaunchError::from(GuardError::LoaderControlled { name: "LD_PRELOAD".to_owned() });
231 assert!(err.to_string().contains("LD_PRELOAD"));
232 assert!(err.hint().is_some_and(|h| h.contains("--allow-unsafe-env")));
233 assert_eq!(err.exit_code(), 1);
234 }
235
236 #[test]
237 fn an_unrepresentable_argument_reports_which_one() {
238 let err = LaunchError::from(crate::cmdline::CmdLineError::LineBreak { index: 2 });
239 assert!(err.to_string().contains("argument 2"));
240 assert!(err.hint().is_some());
241 }
242
243 #[test]
244 fn an_invalid_key_names_the_key_and_the_reason() {
245 let err = LaunchError::InvalidKey {
246 key: "A-B".to_owned(),
247 source: KeyNameError::InvalidCharacter { name: "A-B".to_owned(), ch: '-' },
248 };
249 let message = err.to_string();
250 assert!(message.contains("A-B"));
251 assert!(message.contains('-'));
252 }
253}