use std::{
ffi::{
OsStr,
OsString,
},
fmt,
path::{
Path,
PathBuf,
},
};
use qubit_sanitize::{
ArgvSanitizer,
EnvSanitizer,
FieldSanitizer,
NameMatchMode,
};
use crate::command_env::env_key_eq;
use crate::command_stdin::CommandStdin;
const COMMAND_LOG_MATCH_MODE: NameMatchMode = NameMatchMode::ExactOrSuffix;
const SHELL_COMMAND_REPLACEMENT: &str = "<shell command>";
#[derive(Clone, PartialEq, Eq)]
pub struct Command {
program: OsString,
args: Vec<OsString>,
working_directory: Option<PathBuf>,
clear_environment: bool,
envs: Vec<(OsString, OsString)>,
removed_envs: Vec<OsString>,
stdin: CommandStdin,
}
impl fmt::Debug for Command {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let field_sanitizer = FieldSanitizer::default();
formatter
.debug_struct("Command")
.field("argv", &self.sanitized_argv(&field_sanitizer))
.field("working_directory", &self.working_directory)
.field("clear_environment", &self.clear_environment)
.field(
"env",
&self.sanitized_environment_assignments(&field_sanitizer),
)
.field("unset", &self.removed_environment_names())
.field("stdin", &StdinDisplay(&self.stdin))
.finish()
}
}
impl Command {
#[inline]
pub fn new(program: &str) -> Self {
Self::new_os(program)
}
#[inline]
pub fn new_os<S>(program: S) -> Self
where
S: AsRef<OsStr>,
{
Self {
program: program.as_ref().to_owned(),
args: Vec::new(),
working_directory: None,
clear_environment: false,
envs: Vec::new(),
removed_envs: Vec::new(),
stdin: CommandStdin::Null,
}
}
#[cfg(not(windows))]
#[inline]
pub fn shell(command_line: &str) -> Self {
Self::new("sh").arg("-c").arg(command_line)
}
#[cfg(windows)]
#[inline]
pub fn shell(command_line: &str) -> Self {
Self::new("cmd").arg("/C").arg(command_line)
}
#[inline]
pub fn arg(mut self, arg: &str) -> Self {
self.args.push(OsString::from(arg));
self
}
#[inline]
pub fn arg_os<S>(mut self, arg: S) -> Self
where
S: AsRef<OsStr>,
{
self.args.push(arg.as_ref().to_owned());
self
}
#[inline]
pub fn args(mut self, args: &[&str]) -> Self {
self.args.extend(args.iter().map(OsString::from));
self
}
pub fn args_os<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.args
.extend(args.into_iter().map(|arg| arg.as_ref().to_owned()));
self
}
#[inline]
pub fn working_directory<P>(mut self, working_directory: P) -> Self
where
P: Into<PathBuf>,
{
self.working_directory = Some(working_directory.into());
self
}
#[inline]
pub fn env(mut self, key: &str, value: &str) -> Self {
self = self.env_os(key, value);
self
}
pub fn env_os<K, V>(mut self, key: K, value: V) -> Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
let key = key.as_ref().to_owned();
let value = value.as_ref().to_owned();
self.removed_envs
.retain(|removed| !env_key_eq(removed, &key));
self.envs
.retain(|(existing_key, _)| !env_key_eq(existing_key, &key));
self.envs.push((key, value));
self
}
#[inline]
pub fn env_remove(mut self, key: &str) -> Self {
self = self.env_remove_os(key);
self
}
pub fn env_remove_os<S>(mut self, key: S) -> Self
where
S: AsRef<OsStr>,
{
let key = key.as_ref().to_owned();
self.envs
.retain(|(existing_key, _)| !env_key_eq(existing_key, &key));
self.removed_envs
.retain(|removed| !env_key_eq(removed, &key));
self.removed_envs.push(key);
self
}
pub fn env_clear(mut self) -> Self {
self.clear_environment = true;
self.envs.clear();
self.removed_envs.clear();
self
}
pub fn stdin_null(mut self) -> Self {
self.stdin = CommandStdin::Null;
self
}
pub fn stdin_inherit(mut self) -> Self {
self.stdin = CommandStdin::Inherit;
self
}
pub fn stdin_bytes<B>(mut self, bytes: B) -> Self
where
B: Into<Vec<u8>>,
{
self.stdin = CommandStdin::Bytes(bytes.into());
self
}
pub fn stdin_file<P>(mut self, path: P) -> Self
where
P: Into<PathBuf>,
{
self.stdin = CommandStdin::File(path.into());
self
}
#[inline]
pub fn program(&self) -> &OsStr {
&self.program
}
#[inline]
pub fn arguments(&self) -> &[OsString] {
&self.args
}
#[inline]
pub fn working_directory_override(&self) -> Option<&Path> {
self.working_directory.as_deref()
}
#[inline]
pub fn environment(&self) -> &[(OsString, OsString)] {
&self.envs
}
#[inline]
pub fn removed_environment(&self) -> &[OsString] {
&self.removed_envs
}
#[inline]
pub const fn clears_environment(&self) -> bool {
self.clear_environment
}
#[inline]
pub(crate) fn into_stdin_configuration(self) -> CommandStdin {
self.stdin
}
pub(crate) fn display_command(&self, field_sanitizer: &FieldSanitizer) -> String {
let argv = self.sanitized_argv(field_sanitizer);
if self.envs.is_empty() && self.removed_envs.is_empty() {
return format!("{argv:?}");
}
let env = self.sanitized_environment_assignments(field_sanitizer);
let unset = self.removed_environment_names();
format!("Command {{ env: {env:?}, unset: {unset:?}, argv: {argv:?} }}")
}
fn sanitized_argv(&self, field_sanitizer: &FieldSanitizer) -> Vec<String> {
ArgvSanitizer::new(field_sanitizer.clone())
.sanitize_argv(self.argv_for_display(), COMMAND_LOG_MATCH_MODE)
}
fn argv_for_display(&self) -> Vec<OsString> {
let shell_payload_index = self.shell_payload_arg_index();
let mut argv = Vec::with_capacity(self.args.len() + 1);
argv.push(self.program.clone());
for (index, arg) in self.args.iter().enumerate() {
if Some(index) == shell_payload_index {
argv.push(OsString::from(SHELL_COMMAND_REPLACEMENT));
} else {
argv.push(arg.clone());
}
}
argv
}
fn shell_payload_arg_index(&self) -> Option<usize> {
if self.args.len() < 2 {
return None;
}
let first_arg = self.args.first()?;
if self.program.as_os_str() == OsStr::new("sh") && first_arg == OsStr::new("-c") {
return Some(1);
}
let program = self.program.to_string_lossy();
let first_arg = first_arg.to_string_lossy();
if (program.eq_ignore_ascii_case("cmd") || program.eq_ignore_ascii_case("cmd.exe"))
&& first_arg.eq_ignore_ascii_case("/C")
{
return Some(1);
}
None
}
fn sanitized_environment_assignments(&self, field_sanitizer: &FieldSanitizer) -> Vec<String> {
let sanitizer = EnvSanitizer::new(field_sanitizer.clone());
self.envs
.iter()
.map(|(key, value)| {
let (key, value) = sanitizer.sanitize_os_pair(key, value, COMMAND_LOG_MATCH_MODE);
format!("{key}={value}")
})
.collect()
}
fn removed_environment_names(&self) -> Vec<String> {
self.removed_envs
.iter()
.map(|key| key.to_string_lossy().into_owned())
.collect()
}
}
struct StdinDisplay<'a>(&'a CommandStdin);
impl fmt::Debug for StdinDisplay<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
CommandStdin::Null => formatter.write_str("Null"),
CommandStdin::Inherit => formatter.write_str("Inherit"),
CommandStdin::Bytes(bytes) => write!(formatter, "Bytes({} bytes)", bytes.len()),
CommandStdin::File(path) => formatter.debug_tuple("File").field(path).finish(),
}
}
}