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
50fn get_shell() -> String {
51 use std::{
52 ffi::CStr,
53 path::Path,
54 str,
55 };
56
57 use nix::unistd::{
58 AccessFlags,
59 access,
60 };
61
62 let ent = unsafe { libc::getpwuid(libc::getuid()) };
63 if !ent.is_null() {
64 let shell = unsafe { CStr::from_ptr((*ent).pw_shell) };
65 match shell.to_str().map(str::to_owned) {
66 Err(err) => {
67 warn!(
68 "passwd database shell could not be \
69 represented as utf-8: {err:#}, \
70 falling back to /bin/sh"
71 );
72 }
73 Ok(shell) => {
74 if let Err(err) = access(Path::new(&shell), AccessFlags::X_OK) {
75 warn!(
76 "passwd database shell={shell:?} which is \
77 not executable ({err:#}), falling back to /bin/sh"
78 );
79 } else {
80 return shell;
81 }
82 }
83 }
84 }
85 "/bin/sh".into()
86}
87
88#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct CommandBuilder {
92 args: Vec<OsString>,
93 cwd: Option<PathBuf>,
94 pub(crate) umask: Option<libc::mode_t>,
95 controlling_tty: bool,
96}
97
98impl CommandBuilder {
99 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
102 Self {
103 args: vec![program.as_ref().to_owned()],
104 cwd: None,
105 umask: None,
106 controlling_tty: true,
107 }
108 }
109
110 pub fn from_argv(args: Vec<OsString>) -> Self {
112 Self {
113 args,
114 cwd: None,
115 umask: None,
116 controlling_tty: true,
117 }
118 }
119
120 pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
127 self.controlling_tty = controlling_tty;
128 }
129
130 pub fn get_controlling_tty(&self) -> bool {
131 self.controlling_tty
132 }
133
134 pub fn new_default_prog() -> Self {
137 Self {
138 args: vec![],
139 cwd: None,
140 umask: None,
141 controlling_tty: true,
142 }
143 }
144
145 pub fn is_default_prog(&self) -> bool {
147 self.args.is_empty()
148 }
149
150 pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
153 if self.is_default_prog() {
154 panic!("attempted to add args to a default_prog builder");
155 }
156 self.args.push(arg.as_ref().to_owned());
157 }
158
159 pub fn args<I, S>(&mut self, args: I)
161 where
162 I: IntoIterator<Item = S>,
163 S: AsRef<OsStr>,
164 {
165 for arg in args {
166 self.arg(arg);
167 }
168 }
169
170 pub fn get_argv(&self) -> &Vec<OsString> {
171 &self.args
172 }
173
174 pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
175 &mut self.args
176 }
177
178 pub fn cwd<D>(&mut self, dir: D)
179 where
180 D: AsRef<Path>,
181 {
182 self.cwd = Some(dir.as_ref().to_owned());
183 }
184
185 pub fn clear_cwd(&mut self) {
186 self.cwd.take();
187 }
188
189 pub fn get_cwd(&self) -> Option<&Path> {
190 self.cwd.as_deref()
191 }
192}
193
194impl CommandBuilder {
195 pub fn umask(&mut self, mask: Option<libc::mode_t>) {
196 self.umask = mask;
197 }
198
199 fn resolve_path(&self) -> Option<OsString> {
200 env::var_os("PATH")
201 }
202
203 fn search_path(&self, exe: &OsStr, cwd: &Path) -> color_eyre::Result<PathBuf> {
204 use std::path::Path;
205
206 use nix::unistd::{
207 AccessFlags,
208 access,
209 };
210
211 let exe_path: &Path = exe.as_ref();
212 if exe_path.is_relative() {
213 let abs_path = cwd.join(exe_path);
214 if abs_path.exists() {
215 return Ok(abs_path);
216 }
217
218 if let Some(path) = self.resolve_path() {
219 for path in std::env::split_paths(&path) {
220 let candidate = path.join(exe);
221 if access(&candidate, AccessFlags::X_OK).is_ok() {
222 return Ok(candidate);
223 }
224 }
225 }
226 bail!(
227 "Unable to spawn {} because it doesn't exist on the filesystem \
228 and was not found in PATH",
229 exe_path.display()
230 );
231 } else {
232 if let Err(err) = access(exe_path, AccessFlags::X_OK) {
233 bail!(
234 "Unable to spawn {} because it doesn't exist on the filesystem \
235 or is not executable ({err:#})",
236 exe_path.display()
237 );
238 }
239
240 Ok(PathBuf::from(exe))
241 }
242 }
243
244 pub(crate) fn build(self) -> color_eyre::Result<Command> {
246 use std::os::unix::process::CommandExt;
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 cwd: dir,
264 })
265 }
266}
267
268pub struct Command {
269 pub program: PathBuf,
270 pub args: Vec<CString>,
271 pub cwd: PathBuf,
272}
273
274#[cfg(test)]
275mod tests {
276 use std::{
277 ffi::{
278 OsStr,
279 OsString,
280 },
281 fs::{
282 self,
283 File,
284 },
285 os::unix::fs::PermissionsExt,
286 path::PathBuf,
287 };
288
289 use rusty_fork::rusty_fork_test;
290 use tempfile::TempDir;
291
292 use super::*;
293
294 fn make_executable(dir: &TempDir, name: &str) -> PathBuf {
295 let path = dir.path().join(name);
296 File::create(&path).unwrap();
297 let mut perms = fs::metadata(&path).unwrap().permissions();
298 perms.set_mode(0o755);
299 fs::set_permissions(&path, perms).unwrap();
300 path
301 }
302
303 #[test]
304 fn test_new_builder() {
305 let b = CommandBuilder::new("echo");
306 assert_eq!(b.get_argv(), &vec![OsString::from("echo")]);
307 assert!(b.get_cwd().is_none());
308 assert!(b.get_controlling_tty());
309 }
310
311 #[test]
312 fn test_from_argv() {
313 let argv = vec![OsString::from("ls"), OsString::from("-l")];
314 let b = CommandBuilder::from_argv(argv.clone());
315 assert_eq!(b.get_argv(), &argv);
316 }
317
318 #[test]
319 fn test_default_prog() {
320 let b = CommandBuilder::new_default_prog();
321 assert!(b.is_default_prog());
322 }
323
324 #[test]
325 #[should_panic(expected = "attempted to add args to a default_prog builder")]
326 fn test_default_prog_panics_on_arg() {
327 let mut b = CommandBuilder::new_default_prog();
328 b.arg("ls");
329 }
330
331 #[test]
332 fn test_arg_and_args() {
333 let mut b = CommandBuilder::new("cmd");
334 b.arg("a");
335 b.args(["b", "c"]);
336 let argv: Vec<&OsStr> = b.get_argv().iter().map(|s| s.as_os_str()).collect();
337 assert_eq!(argv, ["cmd", "a", "b", "c"]);
338 }
339
340 #[test]
341 fn test_cwd_set_and_clear() {
342 let mut b = CommandBuilder::new("cmd");
343 let tmp = TempDir::new().unwrap();
344
345 b.cwd(tmp.path());
346 assert_eq!(b.get_cwd(), Some(tmp.path()));
347
348 b.clear_cwd();
349 assert!(b.get_cwd().is_none());
350 }
351
352 #[test]
353 fn test_controlling_tty_flag() {
354 let mut b = CommandBuilder::new("cmd");
355 assert!(b.get_controlling_tty());
356
357 b.set_controlling_tty(false);
358 assert!(!b.get_controlling_tty());
359 }
360
361 rusty_fork_test! {
362 #[test]
363 fn test_search_path_finds_executable_in_path() {
364 let dir = TempDir::new().unwrap();
365 let exe = make_executable(&dir, "mycmd");
366
367 unsafe {
368 std::env::set_var("PATH", dir.path());
370 }
371
372 let b = CommandBuilder::new("mycmd");
373 let resolved = b.search_path(OsStr::new("mycmd"), dir.path()).unwrap();
374
375 assert_eq!(resolved, exe);
376 }
377 }
378
379 #[test]
380 fn test_search_path_relative_to_cwd() {
381 let dir = TempDir::new().unwrap();
382 let exe = make_executable(&dir, "tool");
383
384 let b = CommandBuilder::new("./tool");
385 let resolved = b.search_path(OsStr::new("./tool"), dir.path()).unwrap();
386
387 assert_eq!(resolved, exe);
388 }
389
390 #[test]
391 fn test_search_path_missing_binary_fails() {
392 let dir = TempDir::new().unwrap();
393 let b = CommandBuilder::new("does_not_exist");
394
395 let err = b.search_path(OsStr::new("does_not_exist"), dir.path());
396 assert!(err.is_err());
397 }
398
399 rusty_fork_test! {
400
401 #[test]
402 fn test_build_sets_program_args_and_cwd() {
403 let dir = TempDir::new().unwrap();
404 let exe = make_executable(&dir, "echo");
405
406 unsafe {
407 std::env::set_var("PATH", dir.path());
409 }
410
411 let mut b = CommandBuilder::new("echo");
412 b.arg("hello");
413 b.cwd(dir.path());
414
415 let cmd = b.build().unwrap();
416
417 assert_eq!(cmd.program, exe);
418 assert_eq!(cmd.cwd, dir.path());
419
420 let args: Vec<&str> = cmd.args.iter().map(|c| c.to_str().unwrap()).collect();
421
422 assert_eq!(args, ["echo", "hello"]);
423 dir.close().unwrap()
424 }
425
426 #[test]
427 fn test_build_uses_current_dir_when_cwd_not_set() {
428 let dir = TempDir::new().unwrap();
429 let exe = make_executable(&dir, "cmd");
430
431 unsafe { std::env::set_var("PATH", dir.path()); }
433
434 let b = CommandBuilder::new("cmd");
435 let cmd = b.build().unwrap();
436
437 assert_eq!(cmd.program, exe);
438 assert_eq!(cmd.cwd, std::env::current_dir().unwrap());
439 dir.close().unwrap()
440 }
441 }
442}