portable_pty/
cmdbuilder.rs

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/// Used to deal with Windows having case-insensitive environment variables.
11#[derive(Clone, Debug, PartialEq, PartialOrd)]
12#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
13struct EnvEntry {
14  /// Whether or not this environment variable came from the base environment,
15  /// as opposed to having been explicitly set by the caller.
16  is_from_base_env: bool,
17
18  /// For case-insensitive platforms, the environment variable key in its preferred casing.
19  preferred_key: OsString,
20
21  /// The environment variable value.
22  value: OsString,
23}
24
25impl EnvEntry {
26  fn map_key(k: OsString) -> OsString {
27    if cfg!(windows) {
28      // Best-effort lowercase transformation of an os string
29      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            // Merge the system and user paths together
123            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/// `CommandBuilder` is used to prepare a command to be spawned into a pty.
157/// The interface is intentionally similar to that of `std::process::Command`.
158#[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  /// Create a new builder instance with argv[0] set to the specified
172  /// program.
173  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  /// Create a new builder instance from a pre-built argument vector
186  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  /// Set whether we should set the pty as the controlling terminal.
217  /// The default is true, which is usually what you want, but you
218  /// may need to set this to false if you are crossing container
219  /// boundaries (eg: flatpak) to workaround issues like:
220  /// <https://github.com/flatpak/flatpak/issues/3697>
221  /// <https://github.com/flatpak/flatpak/issues/3285>
222  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  /// Create a new builder instance that will run some idea of a default
231  /// program.  Such a builder will panic if `arg` is called on it.
232  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  /// Returns true if this builder was created via `new_default_prog`
245  pub fn is_default_prog(&self) -> bool {
246    self.args.is_empty()
247  }
248
249  /// Append an argument to the current command line.
250  /// Will panic if called on a builder created via `new_default_prog`.
251  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  /// Append a sequence of arguments to the current command line
259  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  /// Override the value of an environmental variable
278  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  /// Iterate over the configured environment. Only includes environment
337  /// variables set by the caller via `env`, not variables set in the base
338  /// environment.
339  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  /// Return the configured command and arguments as a single string,
372  /// quoted per the unix shell conventions.
373  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  /// Convert the CommandBuilder to a `std::process::Command` instance.
434  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      // Run the shell as a login shell by prefixing the shell's
451      // basename with `-` and setting that as argv0
452      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  /// Determine which shell to run.
478  /// We take the contents of the $SHELL env var first, then
479  /// fall back to looking it up from the password database.
480  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        // Check for exactly the user's string in this path dir
541        let candidate = path.join(&exe);
542        if candidate.exists() {
543          return candidate.into_os_string();
544        }
545
546        // otherwise try tacking on some extensions.
547        // Note that this really replaces the extension in the
548        // user specified path, so this is potentially wrong.
549        for ext in std::env::split_paths(&extensions) {
550          // PATHEXT includes the leading `.`, but `with_extension`
551          // doesn't want that
552          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  /// Constructs an environment block for this spawn attempt.
593  /// Uses the current process environment as the base and then
594  /// adds/replaces the environment that was specified via the
595  /// `env` methods.
596  pub(crate) fn environment_block(&self) -> Vec<u16> {
597    // encode the environment as wide characters
598    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    // and a final terminator for CreateProcessW
612    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    // Ensure that we nul terminate the module name, otherwise we'll
644    // ask CreateProcessW to start something random!
645    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    // Ensure that the command line is nul terminated too!
662    cmdline.push(0);
663    Ok((exe, cmdline))
664  }
665
666  // Borrowed from https://github.com/hniksic/rust-subprocess/blob/873dfed165173e52907beb87118b2c0c05d8b8a1/src/popen.rs#L1117
667  // which in turn was translated from ArgvQuote at http://tinyurl.com/zmgtnls
668  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}