1#[cfg(unix)]
2use anyhow::Context;
3#[cfg(feature = "serde_support")]
4use serde_derive::*;
5use std::collections::BTreeMap;
6use std::ffi::{OsStr, OsString};
7#[cfg(windows)]
8use std::os::windows::ffi::OsStrExt;
9#[cfg(unix)]
10use std::path::Component;
11use std::path::Path;
12
13#[derive(Clone, Debug, PartialEq, PartialOrd)]
15#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
16struct EnvEntry {
17 is_from_base_env: bool,
20
21 preferred_key: OsString,
23
24 value: OsString,
26}
27
28impl EnvEntry {
29 fn map_key(k: OsString) -> OsString {
30 if cfg!(windows) {
31 match k.to_str() {
33 Some(s) => s.to_lowercase().into(),
34 None => k,
35 }
36 } else {
37 k
38 }
39 }
40}
41
42#[cfg(unix)]
43fn get_shell() -> String {
44 use nix::unistd::{access, AccessFlags};
45 use std::ffi::CStr;
46 use std::str;
47
48 let ent = unsafe { libc::getpwuid(libc::getuid()) };
49 if !ent.is_null() {
50 let shell = unsafe { CStr::from_ptr((*ent).pw_shell) };
51 match shell.to_str().map(str::to_owned) {
52 Err(err) => {
53 log::warn!(
54 "passwd database shell could not be \
55 represented as utf-8: {err:#}, \
56 falling back to /bin/sh"
57 );
58 }
59 Ok(shell) => {
60 if let Err(err) = access(Path::new(&shell), AccessFlags::X_OK) {
61 log::warn!(
62 "passwd database shell={shell:?} which is \
63 not executable ({err:#}), falling back to /bin/sh"
64 );
65 } else {
66 return shell;
67 }
68 }
69 }
70 }
71 "/bin/sh".into()
72}
73
74fn get_base_env() -> BTreeMap<OsString, EnvEntry> {
75 let mut env: BTreeMap<OsString, EnvEntry> = std::env::vars_os()
76 .map(|(key, value)| {
77 (
78 EnvEntry::map_key(key.clone()),
79 EnvEntry {
80 is_from_base_env: true,
81 preferred_key: key,
82 value,
83 },
84 )
85 })
86 .collect();
87
88 #[cfg(unix)]
89 {
90 let key = EnvEntry::map_key("SHELL".into());
91 if !env.contains_key(&key) {
93 env.insert(
94 EnvEntry::map_key("SHELL".into()),
95 EnvEntry {
96 is_from_base_env: true,
97 preferred_key: "SHELL".into(),
98 value: get_shell().into(),
99 },
100 );
101 }
102 }
103
104 #[cfg(windows)]
105 {
106 use std::os::windows::ffi::OsStringExt;
107 use winapi::um::processenv::ExpandEnvironmentStringsW;
108 use winreg::enums::{RegType, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
109 use winreg::types::FromRegValue;
110 use winreg::{RegKey, RegValue};
111
112 fn reg_value_to_string(value: &RegValue) -> anyhow::Result<OsString> {
113 match value.vtype {
114 RegType::REG_EXPAND_SZ => {
115 let src = unsafe {
116 std::slice::from_raw_parts(
117 value.bytes.as_ptr() as *const u16,
118 value.bytes.len() / 2,
119 )
120 };
121 let size =
122 unsafe { ExpandEnvironmentStringsW(src.as_ptr(), std::ptr::null_mut(), 0) };
123 let mut buf = vec![0u16; size as usize + 1];
124 unsafe {
125 ExpandEnvironmentStringsW(src.as_ptr(), buf.as_mut_ptr(), buf.len() as u32)
126 };
127
128 let mut buf = buf.as_slice();
129 while let Some(0) = buf.last() {
130 buf = &buf[0..buf.len() - 1];
131 }
132 Ok(OsString::from_wide(buf))
133 }
134 _ => Ok(OsString::from_reg_value(value)?),
135 }
136 }
137
138 if let Ok(sys_env) = RegKey::predef(HKEY_LOCAL_MACHINE)
139 .open_subkey("System\\CurrentControlSet\\Control\\Session Manager\\Environment")
140 {
141 for res in sys_env.enum_values() {
142 if let Ok((name, value)) = res {
143 if name.to_ascii_lowercase() == "username" {
144 continue;
145 }
146 if let Ok(value) = reg_value_to_string(&value) {
147 log::trace!("adding SYS env: {:?} {:?}", name, value);
148 env.insert(
149 EnvEntry::map_key(name.clone().into()),
150 EnvEntry {
151 is_from_base_env: true,
152 preferred_key: name.into(),
153 value,
154 },
155 );
156 }
157 }
158 }
159 }
160
161 if let Ok(sys_env) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
162 for res in sys_env.enum_values() {
163 if let Ok((name, value)) = res {
164 if let Ok(value) = reg_value_to_string(&value) {
165 let value = if name.to_ascii_lowercase() == "path" {
167 match env.get(&EnvEntry::map_key(name.clone().into())) {
168 Some(entry) => {
169 let mut result = OsString::new();
170 result.push(&entry.value);
171 result.push(";");
172 result.push(&value);
173 result
174 }
175 None => value,
176 }
177 } else {
178 value
179 };
180
181 log::trace!("adding USER env: {:?} {:?}", name, value);
182 env.insert(
183 EnvEntry::map_key(name.clone().into()),
184 EnvEntry {
185 is_from_base_env: true,
186 preferred_key: name.into(),
187 value,
188 },
189 );
190 }
191 }
192 }
193 }
194 }
195
196 env
197}
198
199#[derive(Clone, Debug, PartialEq)]
202#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
203pub struct CommandBuilder {
204 args: Vec<OsString>,
205 envs: BTreeMap<OsString, EnvEntry>,
206 cwd: Option<OsString>,
207 #[cfg(unix)]
208 pub(crate) umask: Option<libc::mode_t>,
209 controlling_tty: bool,
210}
211
212impl CommandBuilder {
213 pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
216 Self {
217 args: vec![program.as_ref().to_owned()],
218 envs: get_base_env(),
219 cwd: None,
220 #[cfg(unix)]
221 umask: None,
222 controlling_tty: true,
223 }
224 }
225
226 pub fn from_argv(args: Vec<OsString>) -> Self {
228 Self {
229 args,
230 envs: get_base_env(),
231 cwd: None,
232 #[cfg(unix)]
233 umask: None,
234 controlling_tty: true,
235 }
236 }
237
238 pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
245 self.controlling_tty = controlling_tty;
246 }
247
248 pub fn get_controlling_tty(&self) -> bool {
249 self.controlling_tty
250 }
251
252 pub fn new_default_prog() -> Self {
255 Self {
256 args: vec![],
257 envs: get_base_env(),
258 cwd: None,
259 #[cfg(unix)]
260 umask: None,
261 controlling_tty: true,
262 }
263 }
264
265 pub fn is_default_prog(&self) -> bool {
267 self.args.is_empty()
268 }
269
270 pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
273 if self.is_default_prog() {
274 panic!("attempted to add args to a default_prog builder");
275 }
276 self.args.push(arg.as_ref().to_owned());
277 }
278
279 pub fn args<I, S>(&mut self, args: I)
281 where
282 I: IntoIterator<Item = S>,
283 S: AsRef<OsStr>,
284 {
285 for arg in args {
286 self.arg(arg);
287 }
288 }
289
290 pub fn get_argv(&self) -> &Vec<OsString> {
291 &self.args
292 }
293
294 pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
295 &mut self.args
296 }
297
298 pub fn env<K, V>(&mut self, key: K, value: V)
300 where
301 K: AsRef<OsStr>,
302 V: AsRef<OsStr>,
303 {
304 let key: OsString = key.as_ref().into();
305 let value: OsString = value.as_ref().into();
306 self.envs.insert(
307 EnvEntry::map_key(key.clone()),
308 EnvEntry {
309 is_from_base_env: false,
310 preferred_key: key,
311 value: value,
312 },
313 );
314 }
315
316 pub fn env_remove<K>(&mut self, key: K)
317 where
318 K: AsRef<OsStr>,
319 {
320 let key = key.as_ref().into();
321 self.envs.remove(&EnvEntry::map_key(key));
322 }
323
324 pub fn env_clear(&mut self) {
325 self.envs.clear();
326 }
327
328 pub fn get_env<K>(&self, key: K) -> Option<&OsStr>
329 where
330 K: AsRef<OsStr>,
331 {
332 let key = key.as_ref().into();
333 self.envs.get(&EnvEntry::map_key(key)).map(
334 |EnvEntry {
335 is_from_base_env: _,
336 preferred_key: _,
337 value,
338 }| value.as_os_str(),
339 )
340 }
341
342 pub fn cwd<D>(&mut self, dir: D)
343 where
344 D: AsRef<OsStr>,
345 {
346 self.cwd = Some(dir.as_ref().to_owned());
347 }
348
349 pub fn clear_cwd(&mut self) {
350 self.cwd.take();
351 }
352
353 pub fn get_cwd(&self) -> Option<&OsString> {
354 self.cwd.as_ref()
355 }
356
357 pub fn iter_extra_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> {
361 self.envs.values().filter_map(
362 |EnvEntry {
363 is_from_base_env,
364 preferred_key,
365 value,
366 }| {
367 if *is_from_base_env {
368 None
369 } else {
370 let key = preferred_key.to_str()?;
371 let value = value.to_str()?;
372 Some((key, value))
373 }
374 },
375 )
376 }
377
378 pub fn iter_full_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> {
379 self.envs.values().filter_map(
380 |EnvEntry {
381 preferred_key,
382 value,
383 ..
384 }| {
385 let key = preferred_key.to_str()?;
386 let value = value.to_str()?;
387 Some((key, value))
388 },
389 )
390 }
391
392 pub fn as_unix_command_line(&self) -> anyhow::Result<String> {
395 let mut strs = vec![];
396 for arg in &self.args {
397 let s = arg
398 .to_str()
399 .ok_or_else(|| anyhow::anyhow!("argument cannot be represented as utf8"))?;
400 strs.push(s);
401 }
402 Ok(shell_words::join(strs))
403 }
404}
405
406#[cfg(unix)]
407impl CommandBuilder {
408 pub fn umask(&mut self, mask: Option<libc::mode_t>) {
409 self.umask = mask;
410 }
411
412 fn resolve_path(&self) -> Option<&OsStr> {
413 self.get_env("PATH")
414 }
415
416 fn search_path(&self, exe: &OsStr, cwd: &OsStr) -> anyhow::Result<OsString> {
417 use nix::unistd::{access, AccessFlags};
418
419 let exe_path: &Path = exe.as_ref();
420 if exe_path.is_relative() {
421 let cwd: &Path = cwd.as_ref();
422 let mut errors = vec![];
423
424 if is_cwd_relative_path(exe_path) {
427 let abs_path = cwd.join(exe_path);
428
429 if abs_path.is_dir() {
430 anyhow::bail!(
431 "Unable to spawn {} because it is a directory",
432 abs_path.display()
433 );
434 } else if access(&abs_path, AccessFlags::X_OK).is_ok() {
435 return Ok(abs_path.into_os_string());
436 } else if access(&abs_path, AccessFlags::F_OK).is_ok() {
437 anyhow::bail!(
438 "Unable to spawn {} because it is not executable",
439 abs_path.display()
440 );
441 }
442
443 anyhow::bail!(
444 "Unable to spawn {} because it does not exist",
445 abs_path.display()
446 );
447 }
448
449 if let Some(path) = self.resolve_path() {
450 for path in std::env::split_paths(&path) {
451 let candidate = cwd.join(&path).join(&exe);
452
453 if candidate.is_dir() {
454 errors.push(format!("{} exists but is a directory", candidate.display()));
455 } else if access(&candidate, AccessFlags::X_OK).is_ok() {
456 return Ok(candidate.into_os_string());
457 } else if access(&candidate, AccessFlags::F_OK).is_ok() {
458 errors.push(format!(
459 "{} exists but is not executable",
460 candidate.display()
461 ));
462 }
463 }
464 errors.push(format!("No viable candidates found in PATH {path:?}"));
465 } else {
466 errors.push("Unable to resolve the PATH".to_string());
467 }
468 anyhow::bail!(
469 "Unable to spawn {} because:\n{}",
470 exe_path.display(),
471 errors.join(".\n")
472 );
473 } else if exe_path.is_dir() {
474 anyhow::bail!(
475 "Unable to spawn {} because it is a directory",
476 exe_path.display()
477 );
478 } else {
479 if let Err(err) = access(exe_path, AccessFlags::X_OK) {
480 if access(exe_path, AccessFlags::F_OK).is_ok() {
481 anyhow::bail!(
482 "Unable to spawn {} because it is not executable ({err:#})",
483 exe_path.display()
484 );
485 } else {
486 anyhow::bail!(
487 "Unable to spawn {} because it doesn't exist on the filesystem ({err:#})",
488 exe_path.display()
489 );
490 }
491 }
492
493 Ok(exe.to_owned())
494 }
495 }
496
497 pub(crate) fn as_command(&self) -> anyhow::Result<std::process::Command> {
499 use std::os::unix::process::CommandExt;
500
501 let home = self.get_home_dir()?;
502 let dir: &OsStr = self
503 .cwd
504 .as_ref()
505 .map(|dir| dir.as_os_str())
506 .filter(|dir| std::path::Path::new(dir).is_dir())
507 .unwrap_or(home.as_ref());
508 let shell = self.get_shell();
509
510 let mut cmd = if self.is_default_prog() {
511 let mut cmd = std::process::Command::new(&shell);
512
513 let basename = shell.rsplit('/').next().unwrap_or(&shell);
516 cmd.arg0(&format!("-{}", basename));
517 cmd
518 } else {
519 let resolved = self.search_path(&self.args[0], dir)?;
520 let mut cmd = std::process::Command::new(&resolved);
521 cmd.arg0(&self.args[0]);
522 cmd.args(&self.args[1..]);
523 cmd
524 };
525
526 cmd.current_dir(dir);
527
528 cmd.env_clear();
529 cmd.env("SHELL", shell);
530 cmd.envs(self.envs.values().map(
531 |EnvEntry {
532 is_from_base_env: _,
533 preferred_key,
534 value,
535 }| (preferred_key.as_os_str(), value.as_os_str()),
536 ));
537
538 Ok(cmd)
539 }
540
541 pub fn get_shell(&self) -> String {
545 use nix::unistd::{access, AccessFlags};
546
547 if let Some(shell) = self.get_env("SHELL").and_then(OsStr::to_str) {
548 match access(shell, AccessFlags::X_OK) {
549 Ok(()) => return shell.into(),
550 Err(err) => log::warn!(
551 "$SHELL -> {shell:?} which is \
552 not executable ({err:#}), falling back to password db lookup"
553 ),
554 }
555 }
556
557 get_shell().into()
558 }
559
560 fn get_home_dir(&self) -> anyhow::Result<String> {
561 if let Some(home_dir) = self.get_env("HOME").and_then(OsStr::to_str) {
562 return Ok(home_dir.into());
563 }
564
565 let ent = unsafe { libc::getpwuid(libc::getuid()) };
566 if ent.is_null() {
567 Ok("/".into())
568 } else {
569 use std::ffi::CStr;
570 use std::str;
571 let home = unsafe { CStr::from_ptr((*ent).pw_dir) };
572 home.to_str()
573 .map(str::to_owned)
574 .context("failed to resolve home dir")
575 }
576 }
577}
578
579#[cfg(windows)]
580impl CommandBuilder {
581 fn search_path(&self, exe: &OsStr) -> OsString {
582 if let Some(path) = self.get_env("PATH") {
583 let extensions = self.get_env("PATHEXT").unwrap_or(OsStr::new(".EXE"));
584 for path in std::env::split_paths(&path) {
585 let candidate = path.join(&exe);
587 if candidate.exists() {
588 return candidate.into_os_string();
589 }
590
591 for ext in std::env::split_paths(&extensions) {
595 let ext = ext.to_str().expect("PATHEXT entries must be utf8");
598 let path = path.join(&exe).with_extension(&ext[1..]);
599 if path.exists() {
600 return path.into_os_string();
601 }
602 }
603 }
604 }
605
606 exe.to_owned()
607 }
608
609 pub(crate) fn current_directory(&self) -> Option<Vec<u16>> {
610 let home: Option<&OsStr> = self
611 .get_env("USERPROFILE")
612 .filter(|path| Path::new(path).is_dir());
613 let cwd: Option<&OsStr> = self.cwd.as_deref().filter(|path| Path::new(path).is_dir());
614 let dir: Option<&OsStr> = cwd.or(home);
615
616 dir.map(|dir| {
617 let mut wide = vec![];
618
619 if Path::new(dir).is_relative() {
620 if let Ok(ccwd) = std::env::current_dir() {
621 wide.extend(ccwd.join(dir).as_os_str().encode_wide());
622 } else {
623 wide.extend(dir.encode_wide());
624 }
625 } else {
626 wide.extend(dir.encode_wide());
627 }
628
629 wide.push(0);
630 wide
631 })
632 }
633
634 pub(crate) fn environment_block(&self) -> Vec<u16> {
639 let mut block = vec![];
641
642 for EnvEntry {
643 is_from_base_env: _,
644 preferred_key,
645 value,
646 } in self.envs.values()
647 {
648 block.extend(preferred_key.encode_wide());
649 block.push(b'=' as u16);
650 block.extend(value.encode_wide());
651 block.push(0);
652 }
653 block.push(0);
655
656 block
657 }
658
659 pub fn get_shell(&self) -> String {
660 let exe: OsString = self
661 .get_env("ComSpec")
662 .unwrap_or(OsStr::new("cmd.exe"))
663 .into();
664 exe.into_string()
665 .unwrap_or_else(|_| "%CompSpec%".to_string())
666 }
667
668 pub(crate) fn cmdline(&self) -> anyhow::Result<(Vec<u16>, Vec<u16>)> {
669 let mut cmdline = Vec::<u16>::new();
670
671 let exe: OsString = if self.is_default_prog() {
672 self.get_env("ComSpec")
673 .unwrap_or(OsStr::new("cmd.exe"))
674 .into()
675 } else {
676 self.search_path(&self.args[0])
677 };
678
679 Self::append_quoted(&exe, &mut cmdline);
680
681 let mut exe: Vec<u16> = exe.encode_wide().collect();
684 exe.push(0);
685
686 for arg in self.args.iter().skip(1) {
687 cmdline.push(' ' as u16);
688 anyhow::ensure!(
689 !arg.encode_wide().any(|c| c == 0),
690 "invalid encoding for command line argument {:?}",
691 arg
692 );
693 Self::append_quoted(arg, &mut cmdline);
694 }
695 cmdline.push(0);
697 Ok((exe, cmdline))
698 }
699
700 fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>) {
703 if !arg.is_empty()
704 && !arg.encode_wide().any(|c| {
705 c == ' ' as u16
706 || c == '\t' as u16
707 || c == '\n' as u16
708 || c == '\x0b' as u16
709 || c == '\"' as u16
710 })
711 {
712 cmdline.extend(arg.encode_wide());
713 return;
714 }
715 cmdline.push('"' as u16);
716
717 let arg: Vec<_> = arg.encode_wide().collect();
718 let mut i = 0;
719 while i < arg.len() {
720 let mut num_backslashes = 0;
721 while i < arg.len() && arg[i] == '\\' as u16 {
722 i += 1;
723 num_backslashes += 1;
724 }
725
726 if i == arg.len() {
727 for _ in 0..num_backslashes * 2 {
728 cmdline.push('\\' as u16);
729 }
730 break;
731 } else if arg[i] == b'"' as u16 {
732 for _ in 0..num_backslashes * 2 + 1 {
733 cmdline.push('\\' as u16);
734 }
735 cmdline.push(arg[i]);
736 } else {
737 for _ in 0..num_backslashes {
738 cmdline.push('\\' as u16);
739 }
740 cmdline.push(arg[i]);
741 }
742 i += 1;
743 }
744 cmdline.push('"' as u16);
745 }
746}
747
748#[cfg(unix)]
749fn is_cwd_relative_path<P: AsRef<Path>>(p: P) -> bool {
751 matches!(
752 p.as_ref().components().next(),
753 Some(Component::CurDir | Component::ParentDir)
754 )
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760
761 #[cfg(unix)]
762 #[test]
763 fn test_cwd_relative() {
764 assert!(is_cwd_relative_path("."));
765 assert!(is_cwd_relative_path("./foo"));
766 assert!(is_cwd_relative_path("../foo"));
767 assert!(!is_cwd_relative_path("foo"));
768 assert!(!is_cwd_relative_path("/foo"));
769 }
770
771 #[test]
772 fn test_env() {
773 let mut cmd = CommandBuilder::new("dummy");
774 let package_authors = cmd.get_env("CARGO_PKG_AUTHORS");
775 println!("package_authors: {:?}", package_authors);
776 assert!(package_authors == Some(OsStr::new("Wez Furlong")));
777
778 cmd.env("foo key", "foo value");
779 cmd.env("bar key", "bar value");
780
781 let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
782 println!("iterated_envs: {:?}", iterated_envs);
783 assert!(iterated_envs == vec![("bar key", "bar value"), ("foo key", "foo value")]);
784
785 {
786 let mut cmd = cmd.clone();
787 cmd.env_remove("foo key");
788
789 let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
790 println!("iterated_envs: {:?}", iterated_envs);
791 assert!(iterated_envs == vec![("bar key", "bar value")]);
792 }
793
794 {
795 let mut cmd = cmd.clone();
796 cmd.env_remove("bar key");
797
798 let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
799 println!("iterated_envs: {:?}", iterated_envs);
800 assert!(iterated_envs == vec![("foo key", "foo value")]);
801 }
802
803 {
804 let mut cmd = cmd.clone();
805 cmd.env_clear();
806
807 let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
808 println!("iterated_envs: {:?}", iterated_envs);
809 assert!(iterated_envs.is_empty());
810 }
811 }
812
813 #[cfg(windows)]
814 #[test]
815 fn test_env_case_insensitive_override() {
816 let mut cmd = CommandBuilder::new("dummy");
817 cmd.env("Cargo_Pkg_Authors", "Not Wez");
818 assert!(cmd.get_env("cargo_pkg_authors") == Some(OsStr::new("Not Wez")));
819
820 cmd.env_remove("cARGO_pKG_aUTHORS");
821 assert!(cmd.get_env("CARGO_PKG_AUTHORS").is_none());
822 }
823}