1#![allow(unused)]
27
28use std::{
29 collections::BTreeMap,
30 env,
31 ffi::{
32 CString,
33 OsStr,
34 OsString,
35 },
36 os::unix::ffi::OsStringExt,
37 path::{
38 Path,
39 PathBuf,
40 },
41};
42
43use color_eyre::eyre::{
44 Context,
45 bail,
46};
47use nix::libc;
48use tracing::warn;
49
50#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct CommandBuilder {
54 args: Vec<OsString>,
55 env: Option<Vec<(OsString, OsString)>>,
56 cwd: Option<PathBuf>,
57 pub(crate) umask: Option<libc::mode_t>,
58 controlling_tty: bool,
59}
60
61impl CommandBuilder {
62 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
65 Self {
66 args: vec![program.as_ref().to_owned()],
67 env: None,
68 cwd: None,
69 umask: None,
70 controlling_tty: true,
71 }
72 }
73
74 pub fn from_argv(args: Vec<OsString>) -> Self {
76 Self {
77 args,
78 env: None,
79 cwd: None,
80 umask: None,
81 controlling_tty: true,
82 }
83 }
84
85 pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
92 self.controlling_tty = controlling_tty;
93 }
94
95 pub fn get_controlling_tty(&self) -> bool {
96 self.controlling_tty
97 }
98
99 pub fn new_default_prog() -> Self {
102 Self {
103 args: vec![],
104 env: None,
105 cwd: None,
106 umask: None,
107 controlling_tty: true,
108 }
109 }
110
111 pub fn is_default_prog(&self) -> bool {
113 self.args.is_empty()
114 }
115
116 pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
119 if self.is_default_prog() {
120 panic!("attempted to add args to a default_prog builder");
121 }
122 self.args.push(arg.as_ref().to_owned());
123 }
124
125 pub fn args<I, S>(&mut self, args: I)
127 where
128 I: IntoIterator<Item = S>,
129 S: AsRef<OsStr>,
130 {
131 for arg in args {
132 self.arg(arg);
133 }
134 }
135
136 pub fn get_argv(&self) -> &Vec<OsString> {
137 &self.args
138 }
139
140 pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
141 &mut self.args
142 }
143
144 pub fn env<K, V>(&mut self, key: K, value: V)
145 where
146 K: AsRef<OsStr>,
147 V: AsRef<OsStr>,
148 {
149 self
150 .env
151 .get_or_insert_with(Vec::new)
152 .push((key.as_ref().to_owned(), value.as_ref().to_owned()));
153 }
154
155 pub fn envs<I, K, V>(&mut self, envs: I)
156 where
157 I: IntoIterator<Item = (K, V)>,
158 K: Into<OsString>,
159 V: Into<OsString>,
160 {
161 self.env = Some(
162 envs
163 .into_iter()
164 .map(|(key, value)| (key.into(), value.into()))
165 .collect(),
166 );
167 }
168
169 pub fn get_env(&self) -> Option<&[(OsString, OsString)]> {
170 self.env.as_deref()
171 }
172
173 pub fn cwd<D>(&mut self, dir: D)
174 where
175 D: AsRef<Path>,
176 {
177 self.cwd = Some(dir.as_ref().to_owned());
178 }
179
180 pub fn clear_cwd(&mut self) {
181 self.cwd.take();
182 }
183
184 pub fn get_cwd(&self) -> Option<&Path> {
185 self.cwd.as_deref()
186 }
187}
188
189impl CommandBuilder {
190 pub fn umask(&mut self, mask: Option<libc::mode_t>) {
191 self.umask = mask;
192 }
193
194 fn resolve_path(&self) -> Option<OsString> {
195 match &self.env {
196 Some(env) => env
197 .iter()
198 .rev()
199 .find_map(|(key, value)| (key == OsStr::new("PATH")).then_some(value.clone())),
200 None => env::var_os("PATH"),
201 }
202 }
203
204 fn search_path(&self, exe: &OsStr, cwd: &Path) -> color_eyre::Result<PathBuf> {
205 use std::path::Path;
206
207 use nix::unistd::{
208 AccessFlags,
209 access,
210 };
211
212 let exe_path: &Path = exe.as_ref();
213 if exe_path.is_relative() {
214 let abs_path = cwd.join(exe_path);
215 if abs_path.exists() {
216 return Ok(abs_path);
217 }
218
219 if let Some(path) = self.resolve_path() {
220 for path in std::env::split_paths(&path) {
221 let candidate = path.join(exe);
222 if access(&candidate, AccessFlags::X_OK).is_ok() {
223 return Ok(candidate);
224 }
225 }
226 }
227 bail!(
228 "Unable to spawn {} because it doesn't exist on the filesystem \
229 and was not found in PATH",
230 exe_path.display()
231 );
232 } else {
233 if let Err(err) = access(exe_path, AccessFlags::X_OK) {
234 bail!(
235 "Unable to spawn {} because it doesn't exist on the filesystem \
236 or is not executable ({err:#})",
237 exe_path.display()
238 );
239 }
240
241 Ok(PathBuf::from(exe))
242 }
243 }
244
245 pub(crate) fn build(self) -> color_eyre::Result<Command> {
247 let cwd = env::current_dir()?;
248 let dir = if let Some(dir) = self.cwd.as_deref() {
249 dir.to_owned()
250 } else {
251 cwd
252 };
253 let resolved = self.search_path(&self.args[0], &dir)?;
254 tracing::trace!("resolved path to {:?}", resolved);
255
256 Ok(Command {
257 program: resolved,
258 args: self
259 .args
260 .into_iter()
261 .map(|a| CString::new(a.into_vec()))
262 .collect::<Result<_, _>>()?,
263 env: self
264 .env
265 .map(|env| {
266 env
267 .into_iter()
268 .map(|(key, value)| {
269 let mut bytes = key.into_vec();
270 bytes.push(b'=');
271 bytes.extend_from_slice(&value.into_vec());
272 CString::new(bytes)
273 })
274 .collect::<Result<Vec<_>, _>>()
275 })
276 .transpose()?,
277 cwd: dir,
278 })
279 }
280}
281
282pub struct Command {
283 pub program: PathBuf,
284 pub args: Vec<CString>,
285 pub env: Option<Vec<CString>>,
286 pub cwd: PathBuf,
287}
288
289#[cfg(test)]
290mod tests {
291 use std::{
292 ffi::{
293 OsStr,
294 OsString,
295 },
296 fs::{
297 self,
298 File,
299 },
300 os::unix::fs::PermissionsExt,
301 path::PathBuf,
302 };
303
304 use rusty_fork::rusty_fork_test;
305 use tempfile::TempDir;
306 use test_that::prelude::*;
307
308 use super::*;
309
310 fn make_executable(dir: &TempDir, name: &str) -> PathBuf {
311 let path = dir.path().join(name);
312 File::create(&path).unwrap();
313 let mut perms = fs::metadata(&path).unwrap().permissions();
314 perms.set_mode(0o755);
315 fs::set_permissions(&path, perms).unwrap();
316 path
317 }
318
319 #[test]
320 fn test_new_builder() {
321 let b = CommandBuilder::new("echo");
322 assert_eq!(b.get_argv(), &vec![OsString::from("echo")]);
323 assert_that!(b.get_cwd(), none());
324 assert!(b.get_controlling_tty());
325 }
326
327 #[test]
328 fn test_from_argv() {
329 let argv = vec![OsString::from("ls"), OsString::from("-l")];
330 let b = CommandBuilder::from_argv(argv.clone());
331 assert_eq!(b.get_argv(), &argv);
332 }
333
334 #[test]
335 fn test_default_prog() {
336 let b = CommandBuilder::new_default_prog();
337 assert!(b.is_default_prog());
338 }
339
340 #[test]
341 #[should_panic(expected = "attempted to add args to a default_prog builder")]
342 fn test_default_prog_panics_on_arg() {
343 let mut b = CommandBuilder::new_default_prog();
344 b.arg("ls");
345 }
346
347 #[test]
348 fn test_arg_and_args() {
349 let mut b = CommandBuilder::new("cmd");
350 b.arg("a");
351 b.args(["b", "c"]);
352 let argv: Vec<&OsStr> = b.get_argv().iter().map(|s| s.as_os_str()).collect();
353 assert_eq!(argv, ["cmd", "a", "b", "c"]);
354 }
355
356 #[test]
357 fn test_cwd_set_and_clear() {
358 let mut b = CommandBuilder::new("cmd");
359 let tmp = TempDir::new().unwrap();
360
361 b.cwd(tmp.path());
362 assert_eq!(b.get_cwd(), Some(tmp.path()));
363
364 b.clear_cwd();
365 assert_that!(b.get_cwd(), none());
366 }
367
368 #[test]
369 fn test_controlling_tty_flag() {
370 let mut b = CommandBuilder::new("cmd");
371 assert!(b.get_controlling_tty());
372
373 b.set_controlling_tty(false);
374 assert!(!b.get_controlling_tty());
375 }
376
377 rusty_fork_test! {
378 #[test]
379 fn test_search_path_finds_executable_in_path() {
380 let dir = TempDir::new().unwrap();
381 let exe = make_executable(&dir, "mycmd");
382
383 unsafe {
384 std::env::set_var("PATH", dir.path());
386 }
387
388 let b = CommandBuilder::new("mycmd");
389 let resolved = b.search_path(OsStr::new("mycmd"), dir.path()).unwrap();
390
391 assert_eq!(resolved, exe);
392 }
393 }
394
395 #[test]
396 fn test_search_path_relative_to_cwd() {
397 let dir = TempDir::new().unwrap();
398 let exe = make_executable(&dir, "tool");
399
400 let b = CommandBuilder::new("./tool");
401 let resolved = b.search_path(OsStr::new("./tool"), dir.path()).unwrap();
402
403 assert_eq!(resolved, exe);
404 }
405
406 #[test]
407 fn test_search_path_missing_binary_fails() {
408 let dir = TempDir::new().unwrap();
409 let b = CommandBuilder::new("does_not_exist");
410
411 let result = b.search_path(OsStr::new("does_not_exist"), dir.path());
412 assert_that!(result, err(anything()));
413 }
414
415 rusty_fork_test! {
416
417 #[test]
418 fn test_build_sets_program_args_and_cwd() {
419 let dir = TempDir::new().unwrap();
420 let exe = make_executable(&dir, "echo");
421
422 unsafe {
423 std::env::set_var("PATH", dir.path());
425 }
426
427 let mut b = CommandBuilder::new("echo");
428 b.arg("hello");
429 b.cwd(dir.path());
430
431 let cmd = b.build().unwrap();
432
433 assert_eq!(cmd.program, exe);
434 assert_eq!(cmd.cwd, dir.path());
435
436 let args: Vec<&str> = cmd.args.iter().map(|c| c.to_str().unwrap()).collect();
437
438 assert_eq!(args, ["echo", "hello"]);
439 assert_that!(cmd.env, none());
440 dir.close().unwrap()
441 }
442
443 #[test]
444 fn test_build_sets_explicit_env() {
445 let dir = TempDir::new().unwrap();
446 let exe = make_executable(&dir, "cmd");
447
448 let mut b = CommandBuilder::new("cmd");
449 b.envs([
450 (OsString::from("PATH"), dir.path().as_os_str().to_owned()),
451 (OsString::from("TRACEXEC_TEST"), OsString::from("value")),
452 ]);
453
454 let cmd = b.build().unwrap();
455
456 assert_eq!(cmd.program, exe);
457 let env: Vec<String> = cmd
458 .env
459 .as_ref()
460 .unwrap()
461 .iter()
462 .map(|c| c.to_str().unwrap().to_owned())
463 .collect();
464 assert_eq!(
465 env,
466 vec![
467 "PATH=".to_string() + dir.path().to_str().unwrap(),
468 "TRACEXEC_TEST=value".to_string(),
469 ]
470 );
471 dir.close().unwrap()
472 }
473
474 #[test]
475 fn test_build_uses_current_dir_when_cwd_not_set() {
476 let dir = TempDir::new().unwrap();
477 let exe = make_executable(&dir, "cmd");
478
479 unsafe { std::env::set_var("PATH", dir.path()); }
481
482 let b = CommandBuilder::new("cmd");
483 let cmd = b.build().unwrap();
484
485 assert_eq!(cmd.program, exe);
486 assert_eq!(cmd.cwd, std::env::current_dir().unwrap());
487 dir.close().unwrap()
488 }
489 }
490}