use std::{
collections::BTreeMap,
ffi::{OsStr, OsString},
io,
path::Path,
process::ExitStatus,
};
#[cfg(not(windows))]
pub fn run(
program: &OsStr,
args: &[OsString],
env: &BTreeMap<OsString, OsString>,
cwd: &Path,
) -> io::Result<ExitStatus> {
use std::process::{Command, Stdio};
Command::new(program)
.args(args)
.env_clear()
.envs(env)
.current_dir(cwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
}
#[cfg(windows)]
pub fn run(
program: &OsStr,
args: &[OsString],
env: &BTreeMap<OsString, OsString>,
cwd: &Path,
) -> io::Result<ExitStatus> {
use std::os::windows::process::ExitStatusExt as _;
use windows_sys::Win32::{
Foundation::{CloseHandle, FALSE, WAIT_OBJECT_0},
System::Threading::{
CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, CreateProcessW, GetExitCodeProcess,
INFINITE, PROCESS_INFORMATION, STARTUPINFOW, WaitForSingleObject,
},
};
let mut command_line = command_line(program, args);
let environment = environment_block(env);
let directory = wide(cwd.as_os_str());
let mut startup: STARTUPINFOW = unsafe { std::mem::zeroed() };
startup.cb = u32::try_from(std::mem::size_of::<STARTUPINFOW>())
.expect("STARTUPINFOW is far smaller than u32::MAX bytes");
let mut process: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let created = unsafe {
CreateProcessW(
std::ptr::null(),
command_line.as_mut_ptr(),
std::ptr::null(),
std::ptr::null(),
FALSE,
CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
environment.as_ptr().cast(),
directory.as_ptr(),
&raw const startup,
&raw mut process,
)
};
if created == 0 {
return Err(io::Error::last_os_error());
}
unsafe {
CloseHandle(process.hThread);
let waited = WaitForSingleObject(process.hProcess, INFINITE);
let mut code = 0u32;
let read = if waited == WAIT_OBJECT_0 {
GetExitCodeProcess(process.hProcess, &raw mut code)
} else {
0
};
let error = io::Error::last_os_error();
CloseHandle(process.hProcess);
if waited != WAIT_OBJECT_0 || read == 0 {
return Err(error);
}
Ok(ExitStatus::from_raw(code))
}
}
#[cfg(windows)]
fn wide(value: &OsStr) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt as _;
value.encode_wide().chain(std::iter::once(0)).collect()
}
#[cfg(windows)]
fn command_line(program: &OsStr, args: &[OsString]) -> Vec<u16> {
use std::{iter, os::windows::ffi::OsStrExt as _};
let mut line = Vec::new();
for (index, argument) in iter::once(program)
.chain(args.iter().map(OsString::as_os_str))
.enumerate()
{
if index > 0 {
line.push(u16::from(b' '));
}
let units = argument.encode_wide().collect::<Vec<_>>();
let needs_quotes = units.is_empty()
|| units
.iter()
.any(|unit| [b' ', b'\t', b'"'].map(u16::from).contains(unit));
if !needs_quotes {
line.extend(units);
continue;
}
line.push(u16::from(b'"'));
let mut backslashes = 0usize;
for unit in units {
if unit == u16::from(b'\\') {
backslashes += 1;
continue;
}
if unit == u16::from(b'"') {
line.extend(iter::repeat_n(u16::from(b'\\'), backslashes * 2 + 1));
} else {
line.extend(iter::repeat_n(u16::from(b'\\'), backslashes));
}
backslashes = 0;
line.push(unit);
}
line.extend(iter::repeat_n(u16::from(b'\\'), backslashes * 2));
line.push(u16::from(b'"'));
}
line.push(0);
line
}
#[cfg(windows)]
fn environment_block(env: &BTreeMap<OsString, OsString>) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt as _;
let mut block = Vec::new();
for (key, value) in env {
block.extend(key.encode_wide());
block.push(u16::from(b'='));
block.extend(value.encode_wide());
block.push(0);
}
block.push(0);
block
}
#[cfg(all(test, windows))]
mod tests {
use std::ffi::{OsStr, OsString};
use super::command_line;
fn line(program: &str, args: &[&str]) -> String {
let args = args.iter().map(OsString::from).collect::<Vec<_>>();
let units = command_line(OsStr::new(program), &args);
String::from_utf16(&units[..units.len() - 1]).expect("ASCII input")
}
#[test]
fn quotes_only_what_the_c_runtime_would_split() {
assert_eq!(line("adb", &["start-server"]), "adb start-server");
assert_eq!(
line(r"C:\Program Files\adb.exe", &["", "a b"]),
r#""C:\Program Files\adb.exe" "" "a b""#
);
}
#[test]
fn escapes_quotes_and_the_backslashes_before_them() {
assert_eq!(line("x", &[r#"say "hi""#]), r#"x "say \"hi\"""#);
assert_eq!(line("x", &[r#"dir\"#]), r#"x dir\"#);
assert_eq!(line("x", &[r#"a dir\"#]), r#"x "a dir\\""#);
assert_eq!(line("x", &[r#"\\"q"#]), r#"x "\\\\\"q""#);
}
}