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