use crate::process_windows::os_error;
use std::{
ffi::{OsStr, OsString},
fs::OpenOptions,
io,
os::windows::{
ffi::OsStrExt,
io::{AsHandle, AsRawHandle, BorrowedHandle, FromRawHandle, OwnedHandle, RawHandle},
process::ExitStatusExt,
},
path::{Path, PathBuf},
process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, ExitStatus},
sync::{
atomic::{AtomicU64, Ordering},
Arc, Mutex,
},
time::Duration,
};
use windows::{
core::{PCWSTR, PWSTR},
Win32::{
Foundation::{
GetHandleInformation, SetHandleInformation, HANDLE, HANDLE_FLAGS, HANDLE_FLAG_INHERIT,
WAIT_OBJECT_0, WAIT_TIMEOUT,
},
System::Threading::{
CreateProcessW, DeleteProcThreadAttributeList, GetExitCodeProcess,
InitializeProcThreadAttributeList, ResumeThread, TerminateProcess,
UpdateProcThreadAttribute, WaitForSingleObject, CREATE_BREAKAWAY_FROM_JOB,
CREATE_NO_WINDOW, CREATE_SUSPENDED, CREATE_UNICODE_ENVIRONMENT,
EXTENDED_STARTUPINFO_PRESENT, LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST, STARTF_USESTDHANDLES, STARTUPINFOEXW,
},
},
};
#[derive(Debug, Default)]
pub enum WindowsStdio {
#[default]
Inherit,
Null,
Piped,
Handle(OwnedHandle),
}
#[derive(Debug)]
pub struct WindowsSpawnConfig {
pub stdin: WindowsStdio,
pub stdout: WindowsStdio,
pub stderr: WindowsStdio,
pub inherit_environment: bool,
pub raw_arguments: Vec<usize>,
}
impl Default for WindowsSpawnConfig {
fn default() -> Self {
Self {
stdin: WindowsStdio::Inherit,
stdout: WindowsStdio::Inherit,
stderr: WindowsStdio::Inherit,
inherit_environment: true,
raw_arguments: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct WindowsChild {
pub stdin: Option<ChildStdin>,
pub stdout: Option<ChildStdout>,
pub stderr: Option<ChildStderr>,
handle: OwnedHandle,
thread: Option<OwnedHandle>,
pid: u32,
}
impl From<Child> for WindowsChild {
fn from(mut child: Child) -> Self {
Self {
stdin: child.stdin.take(),
stdout: child.stdout.take(),
stderr: child.stderr.take(),
pid: child.id(),
handle: child.into(),
thread: None,
}
}
}
impl AsHandle for WindowsChild {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.handle.as_handle()
}
}
impl AsRawHandle for WindowsChild {
fn as_raw_handle(&self) -> RawHandle {
self.handle.as_raw_handle()
}
}
impl WindowsChild {
pub fn id(&self) -> u32 {
self.pid
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
self.wait_native(0)
}
pub fn wait(&mut self) -> io::Result<ExitStatus> {
drop(self.stdin.take());
self.wait_native(u32::MAX)?
.ok_or_else(|| io::Error::other("infinite wait timed out"))
}
pub fn kill(&mut self) -> io::Result<()> {
if self.try_wait()?.is_some() {
return Ok(());
}
match unsafe { TerminateProcess(HANDLE(self.handle.as_raw_handle()), 1) } {
Ok(()) => Ok(()),
Err(error) => {
let error = os_error(error);
if self.try_wait()?.is_some() {
Ok(())
} else {
Err(error)
}
}
}
}
fn wait_native(&self, timeout: u32) -> io::Result<Option<ExitStatus>> {
let raw = HANDLE(self.handle.as_raw_handle());
match unsafe { WaitForSingleObject(raw, timeout) } {
WAIT_TIMEOUT => Ok(None),
WAIT_OBJECT_0 => {
let mut code = 0;
unsafe { GetExitCodeProcess(raw, &mut code) }.map_err(os_error)?;
Ok(Some(ExitStatus::from_raw(code)))
}
_ => Err(io::Error::last_os_error()),
}
}
pub(crate) fn resume(&mut self, detached: bool) -> io::Result<()> {
if let Some(thread) = self.thread.take() {
if unsafe { ResumeThread(HANDLE(thread.as_raw_handle())) } == u32::MAX {
return Err(io::Error::last_os_error());
}
Ok(())
} else if !detached {
crate::process_windows::resume(self.pid)
} else {
Ok(())
}
}
}
static SPAWN_LOCK: Mutex<()> = Mutex::new(());
struct Inheritable(Vec<(HANDLE, u32)>);
impl Inheritable {
fn new(handles: &[HANDLE]) -> io::Result<Self> {
let mut guard = Self(Vec::with_capacity(handles.len()));
for &handle in handles {
let mut flags = 0;
unsafe { GetHandleInformation(handle, &mut flags) }.map_err(os_error)?;
if flags & HANDLE_FLAG_INHERIT.0 == 0 {
unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT.0, HANDLE_FLAG_INHERIT) }
.map_err(os_error)?;
guard.0.push((handle, flags));
}
}
Ok(guard)
}
fn restore(&mut self) -> io::Result<()> {
let mut error = None;
for &(handle, flags) in self.0.iter().rev() {
if let Err(failure) =
unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT.0, HANDLE_FLAGS(flags)) }
{
error.get_or_insert_with(|| os_error(failure));
}
}
if let Some(error) = error {
Err(error)
} else {
self.0.clear();
Ok(())
}
}
}
impl Drop for Inheritable {
fn drop(&mut self) {
let _ = self.restore();
}
}
struct Attributes {
_storage: Vec<AttributeStorage>,
list: LPPROC_THREAD_ATTRIBUTE_LIST,
}
#[repr(align(16))]
#[derive(Clone)]
struct AttributeStorage {
_bytes: [u8; 16],
}
impl Attributes {
fn new(handles: &[HANDLE]) -> io::Result<Self> {
let mut bytes = 0;
let _ = unsafe { InitializeProcThreadAttributeList(None, 1, None, &mut bytes) };
if bytes == 0 {
return Err(io::Error::last_os_error());
}
let mut storage = vec![AttributeStorage { _bytes: [0; 16] }; bytes.div_ceil(16)];
let list = LPPROC_THREAD_ATTRIBUTE_LIST(storage.as_mut_ptr().cast());
unsafe { InitializeProcThreadAttributeList(Some(list), 1, None, &mut bytes) }
.map_err(os_error)?;
let attributes = Self {
_storage: storage,
list,
};
unsafe {
UpdateProcThreadAttribute(
list,
0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize,
Some(handles.as_ptr().cast()),
std::mem::size_of_val(handles),
None,
None,
)
}
.map_err(os_error)?;
Ok(attributes)
}
}
impl Drop for Attributes {
fn drop(&mut self) {
unsafe {
DeleteProcThreadAttributeList(self.list);
}
}
}
fn stdio(config: WindowsStdio, index: u32) -> io::Result<(OwnedHandle, Option<OwnedHandle>)> {
match config {
WindowsStdio::Piped => {
let (parent, child) = child_pipe(index != 0)?;
Ok((child, Some(parent)))
}
WindowsStdio::Handle(handle) => Ok((handle, None)),
WindowsStdio::Inherit => {
let raw = unsafe { GetStdHandle((-10i32 - index as i32) as u32) };
if !raw.is_null() && raw as isize != -1 {
return Ok((
unsafe { BorrowedHandle::borrow_raw(raw) }.try_clone_to_owned()?,
None,
));
}
stdio(WindowsStdio::Null, index)
}
WindowsStdio::Null => {
let file = OpenOptions::new()
.read(index == 0)
.write(index != 0)
.open("NUL")?;
Ok((file.into(), None))
}
}
}
fn child_pipe(parent_reads: bool) -> io::Result<(OwnedHandle, OwnedHandle)> {
static NEXT_PIPE: AtomicU64 = AtomicU64::new(0);
let name = format!(
"\\\\.\\pipe\\rightkit-process-{}-{}",
std::process::id(),
NEXT_PIPE.fetch_add(1, Ordering::Relaxed)
);
let name = wide(OsStr::new(&name))?;
let access = if parent_reads { 1 } else { 2 }; let parent = unsafe {
CreateNamedPipeW(
name.as_ptr(),
access | 0x4000_0000 | 0x0008_0000, 8,
1,
65_536,
65_536,
0,
std::ptr::null(),
)
}; if parent as isize == -1 {
return Err(io::Error::last_os_error());
}
let parent = unsafe { OwnedHandle::from_raw_handle(parent) };
let child = unsafe {
CreateFileW(
name.as_ptr(),
if parent_reads {
0x4000_0000
} else {
0x8000_0000
}, 0,
std::ptr::null(),
3,
0x0010_0080,
std::ptr::null_mut(),
)
};
if child as isize == -1 {
return Err(io::Error::last_os_error());
}
Ok((parent, unsafe { OwnedHandle::from_raw_handle(child) }))
}
pub(crate) fn spawn(
command: &Command,
mut config: WindowsSpawnConfig,
handles: &[usize],
detached: bool,
hidden: bool,
env_cleared: bool,
cleanup_timeout: Option<Duration>,
) -> io::Result<WindowsChild> {
if env_cleared {
config.inherit_environment = false;
}
let environment = environment(command, config.inherit_environment)?;
let program = resolve_program(command, &environment)?;
let (program_wide, mut line) = if program.extension().is_some_and(|extension| {
extension.eq_ignore_ascii_case("bat") || extension.eq_ignore_ascii_case("cmd")
}) {
let mut buffer = vec![0; 32_768];
let length =
unsafe { GetSystemDirectoryW(buffer.as_mut_ptr(), buffer.len() as u32) } as usize;
if length == 0 || length >= buffer.len() {
return Err(io::Error::last_os_error());
}
buffer.truncate(length);
buffer.extend("\\cmd.exe\0".encode_utf16());
(
buffer,
batch_command_line(&program, command, &config.raw_arguments)?,
)
} else {
(
wide(program.as_os_str())?,
command_line(command, &config.raw_arguments)?,
)
};
let cwd = command
.get_current_dir()
.map(|path| wide(path.as_os_str()))
.transpose()?;
let env_block = environment_block(&environment)?;
let (stdin, parent_stdin) = stdio(config.stdin, 0)?;
let (stdout, parent_stdout) = stdio(config.stdout, 1)?;
let (stderr, parent_stderr) = stdio(config.stderr, 2)?;
let mut handles: Vec<_> = handles
.iter()
.map(|handle| HANDLE(*handle as RawHandle))
.collect();
handles.extend([
HANDLE(stdin.as_raw_handle()),
HANDLE(stdout.as_raw_handle()),
HANDLE(stderr.as_raw_handle()),
]);
handles.sort_unstable_by_key(|handle| handle.0 as usize);
handles.dedup();
let _lock = SPAWN_LOCK
.lock()
.unwrap_or_else(|poison| poison.into_inner());
let mut inheritable = Inheritable::new(&handles)?;
let attributes = Attributes::new(&handles)?;
let mut startup = STARTUPINFOEXW::default();
startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startup.StartupInfo.hStdInput = HANDLE(stdin.as_raw_handle());
startup.StartupInfo.hStdOutput = HANDLE(stdout.as_raw_handle());
startup.StartupInfo.hStdError = HANDLE(stderr.as_raw_handle());
startup.lpAttributeList = attributes.list;
let mut flags = CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
if hidden {
flags |= CREATE_NO_WINDOW;
}
if detached {
flags |= CREATE_BREAKAWAY_FROM_JOB;
}
let mut info = PROCESS_INFORMATION::default();
unsafe {
CreateProcessW(
PCWSTR(program_wide.as_ptr()),
Some(PWSTR(line.as_mut_ptr())),
None,
None,
true,
flags,
Some(env_block.as_ptr().cast()),
PCWSTR(cwd.as_ref().map_or(std::ptr::null(), |cwd| cwd.as_ptr())),
&startup.StartupInfo,
&mut info,
)
}
.map_err(os_error)?;
let child = WindowsChild {
stdin: parent_stdin.map(ChildStdin::from),
stdout: parent_stdout.map(ChildStdout::from),
stderr: parent_stderr.map(ChildStderr::from),
handle: unsafe { OwnedHandle::from_raw_handle(info.hProcess.0) },
thread: Some(unsafe { OwnedHandle::from_raw_handle(info.hThread.0) }),
pid: info.dwProcessId,
};
if let Err(error) = inheritable.restore() {
crate::owned::cleanup_direct(Arc::new(Mutex::new(child)), cleanup_timeout);
return Err(error);
}
Ok(child)
}
fn wide(value: &OsStr) -> io::Result<Vec<u16>> {
let mut value: Vec<_> = value.encode_wide().collect();
if value.contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "embedded NUL"));
}
value.push(0);
Ok(value)
}
fn command_line(command: &Command, raw_arguments: &[usize]) -> io::Result<Vec<u16>> {
let program = wide(command.get_program())?;
if program.contains(&(b'"' as u16)) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"program contains quotes",
));
}
let args: Vec<_> = command.get_args().collect();
if raw_arguments.iter().any(|index| *index >= args.len()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"raw argument index out of bounds",
));
}
let mut line = vec![b'"' as u16];
line.extend_from_slice(&program[..program.len() - 1]);
line.push(b'"' as u16);
for (index, arg) in args.into_iter().enumerate() {
line.push(b' ' as u16);
let mut arg = wide(arg)?;
arg.pop();
if raw_arguments.contains(&index) {
line.extend(arg);
continue;
}
append_argument(&mut line, &arg);
}
line.push(0);
if line.len() > 32_767 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"command line too long",
));
}
Ok(line)
}
fn append_argument(line: &mut Vec<u16>, arg: &[u16]) {
let quote = arg.is_empty() || arg.iter().any(|ch| *ch == 32 || *ch == 9);
if quote {
line.push(34);
}
let mut slashes = 0;
for &ch in arg {
if ch == 92 {
slashes += 1;
} else {
if ch == 34 {
line.extend(std::iter::repeat_n(92, slashes + 1));
}
slashes = 0;
}
line.push(ch);
}
if quote {
line.extend(std::iter::repeat_n(92, slashes));
line.push(34);
}
}
fn compare_keys(left: &OsStr, right: &OsStr) -> std::cmp::Ordering {
let left: Vec<_> = left.encode_wide().collect();
let right: Vec<_> = right.encode_wide().collect();
match unsafe {
CompareStringOrdinal(
left.as_ptr(),
left.len() as i32,
right.as_ptr(),
right.len() as i32,
1,
)
} {
1 => std::cmp::Ordering::Less,
3 => std::cmp::Ordering::Greater,
_ => std::cmp::Ordering::Equal,
}
}
fn environment(command: &Command, inherit: bool) -> io::Result<Vec<(OsString, OsString)>> {
let mut env: Vec<_> = if inherit {
std::env::vars_os().collect()
} else {
Vec::new()
};
for (key, value) in command.get_envs() {
let key_wide = wide(key)?;
if key_wide.len() == 1 || key_wide[..key_wide.len() - 1].contains(&61) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid environment key",
));
}
env.retain(|(existing, _)| !compare_keys(existing, key).is_eq());
if let Some(value) = value {
wide(value)?;
env.push((key.into(), value.into()));
}
}
env.sort_by(|(left, _), (right, _)| compare_keys(left, right));
Ok(env)
}
fn environment_block(env: &[(OsString, OsString)]) -> io::Result<Vec<u16>> {
let mut block = Vec::new();
for (key, value) in env {
let mut key = wide(key)?;
key.pop();
block.extend(key);
block.push(61);
block.extend(wide(value)?);
}
if block.is_empty() {
block.push(0);
}
block.push(0);
Ok(block)
}
fn resolve_program(command: &Command, env: &[(OsString, OsString)]) -> io::Result<PathBuf> {
let program = Path::new(command.get_program());
let program_wide = wide(program.as_os_str())?;
if program_wide.len() == 1
|| program_wide
.get(program_wide.len() - 2)
.is_some_and(|ch| *ch == 47 || *ch == 92)
{
return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty program"));
}
let mut file = program.to_path_buf();
let find = |path: PathBuf| -> Option<PathBuf> {
if path.is_file() {
Some(path)
} else {
None
}
};
if file.components().count() > 1 || file.is_absolute() {
if !file
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("exe"))
{
let mut appended = file.as_os_str().to_owned();
appended.push(".exe");
if let Some(path) = find(PathBuf::from(appended)) {
return std::path::absolute(path);
}
}
return std::path::absolute(file);
}
if !program_wide.contains(&46) {
file.set_extension("exe");
}
let child_path = env
.iter()
.find(|(key, _)| compare_keys(key, OsStr::new("PATH")).is_eq());
let explicitly_set_path = command
.get_envs()
.any(|(key, value)| compare_keys(key, OsStr::new("PATH")).is_eq() && value.is_some());
let mut directories = Vec::new();
if explicitly_set_path {
if let Some((_, path)) = child_path {
directories
.extend(std::env::split_paths(path).filter(|path| !path.as_os_str().is_empty()));
}
}
if let Some(parent) = std::env::current_exe()?.parent() {
directories.push(parent.into());
}
for system in [
GetSystemDirectoryW as unsafe extern "system" fn(*mut u16, u32) -> u32,
GetWindowsDirectoryW as unsafe extern "system" fn(*mut u16, u32) -> u32,
] {
let mut buffer = vec![0; 32_768];
let length = unsafe { system(buffer.as_mut_ptr(), buffer.len() as u32) } as usize;
if length == 0 {
return Err(io::Error::last_os_error());
}
if length >= buffer.len() {
return Err(io::Error::from_raw_os_error(206));
}
use std::os::windows::ffi::OsStringExt;
directories.push(OsString::from_wide(&buffer[..length]).into());
}
if let Some(path) = std::env::var_os("PATH") {
directories
.extend(std::env::split_paths(&path).filter(|path| !path.as_os_str().is_empty()));
}
for directory in directories {
if let Some(path) = find(directory.join(&file)) {
return std::path::absolute(path);
}
}
Err(io::Error::from_raw_os_error(2))
}
fn batch_command_line(
program: &Path,
command: &Command,
raw_arguments: &[usize],
) -> io::Result<Vec<u16>> {
let mut script = wide(program.as_os_str())?;
script.pop();
if script.len() < 260 && script.starts_with(&[92, 92, 63, 92]) {
if script.get(4..8) == Some(&[85, 78, 67, 92]) {
script.splice(..8, [92, 92]);
} else if script.get(5) == Some(&58) {
script.drain(..4);
}
}
if script.contains(&34) || script.last() == Some(&92) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid batch path",
));
}
let args: Vec<_> = command.get_args().collect();
if raw_arguments.iter().any(|index| *index >= args.len()) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"raw argument index out of bounds",
));
}
let mut line: Vec<_> = "cmd.exe /e:ON /v:OFF /d /c \"\"".encode_utf16().collect();
line.extend(script);
line.push(34);
for (index, value) in args.into_iter().enumerate() {
line.push(32);
let mut value = wide(value)?;
value.pop();
if raw_arguments.contains(&index) {
line.extend(value);
continue;
}
if value.iter().any(|ch| *ch == 10 || *ch == 13) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid batch argument",
));
}
let quote = value.is_empty()
|| value.last() == Some(&92)
|| char::decode_utf16(value.iter().copied())
.filter_map(Result::ok)
.any(|ch| {
ch.is_control()
|| (ch.is_ascii()
&& !ch.is_ascii_alphanumeric()
&& !r"#$*+-./:?@\_".contains(ch))
});
if quote {
line.push(34);
}
let mut slashes = 0;
for ch in value {
if ch == 92 {
slashes += 1;
} else {
if ch == 34 {
line.extend(std::iter::repeat_n(92, slashes));
line.push(34);
} else if ch == 37 {
line.extend("%%cd:~,".encode_utf16());
}
slashes = 0;
}
line.push(ch);
}
if quote {
line.extend(std::iter::repeat_n(92, slashes));
line.push(34);
}
}
line.extend([34, 0]);
if line.len() > 32_767 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"command line too long",
));
}
Ok(line)
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn GetStdHandle(index: u32) -> RawHandle;
fn CompareStringOrdinal(
left: *const u16,
left_len: i32,
right: *const u16,
right_len: i32,
ignore_case: i32,
) -> i32;
fn GetSystemDirectoryW(buffer: *mut u16, size: u32) -> u32;
fn GetWindowsDirectoryW(buffer: *mut u16, size: u32) -> u32;
fn CreateNamedPipeW(
name: *const u16,
open_mode: u32,
pipe_mode: u32,
instances: u32,
out_buffer: u32,
in_buffer: u32,
timeout: u32,
security: *const std::ffi::c_void,
) -> RawHandle;
fn CreateFileW(
name: *const u16,
access: u32,
sharing: u32,
security: *const std::ffi::c_void,
disposition: u32,
attributes: u32,
template: RawHandle,
) -> RawHandle;
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::windows::{ffi::OsStringExt, process::CommandExt};
#[link(name = "shell32")]
unsafe extern "system" {
fn CommandLineToArgvW(line: *const u16, argc: *mut i32) -> *mut *mut u16;
}
#[link(name = "kernel32")]
unsafe extern "system" {
fn LocalFree(memory: *mut std::ffi::c_void) -> *mut std::ffi::c_void;
}
#[test]
fn windows_parser_round_trips_regular_arguments() {
let args = [
OsString::from(""),
OsString::from("space tab\tend"),
OsString::from("embedded\"quote"),
OsString::from("slash\\\"quote"),
OsString::from(r"trailing space \\"),
OsString::from("plain\\"),
OsString::from("🦀"),
OsString::from_wide(&[0xd800, 32, 0xdc00]),
];
let mut command = Command::new("C:\\Program Files\\daemon.exe");
command.args(&args);
let line = command_line(&command, &[]).unwrap();
let mut argc = 0;
let argv = unsafe { CommandLineToArgvW(line.as_ptr(), &mut argc) };
assert!(!argv.is_null());
struct Arguments(*mut *mut u16);
impl Drop for Arguments {
fn drop(&mut self) {
unsafe {
LocalFree(self.0.cast());
}
}
}
let _guard = Arguments(argv);
assert_eq!(argc as usize, args.len() + 1);
for (index, expected) in std::iter::once(command.get_program())
.chain(args.iter().map(OsString::as_os_str))
.enumerate()
{
let pointer = unsafe { *argv.add(index) };
let mut length = 0;
while unsafe { *pointer.add(length) } != 0 {
length += 1;
}
let actual = unsafe { std::slice::from_raw_parts(pointer, length) };
assert_eq!(actual, expected.encode_wide().collect::<Vec<_>>());
}
}
#[test]
fn raw_arguments_are_appended_verbatim() {
let mut command = Command::new("cmd.exe");
command.arg("/c").raw_arg("echo first & echo second");
let line = command_line(&command, &[1]).unwrap();
assert_eq!(
String::from_utf16(&line[..line.len() - 1]).unwrap(),
"\"cmd.exe\" /c echo first & echo second"
);
}
#[test]
fn embedded_nul_is_rejected_before_spawn() {
let mut command = Command::new("cmd.exe");
command.arg("bad\0argument");
assert_eq!(
command_line(&command, &[]).unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn batch_arguments_reject_line_breaks_and_escape_expansions() {
let mut command = Command::new("script.cmd");
command.arg("%PATH%");
let line = batch_command_line(Path::new("C:\\script.cmd"), &command, &[]).unwrap();
assert!(String::from_utf16(&line[..line.len() - 1])
.unwrap()
.contains("%%cd:~,%PATH%%cd:~,%"));
command.arg("bad\nargument");
assert_eq!(
batch_command_line(Path::new("C:\\script.cmd"), &command, &[])
.unwrap_err()
.kind(),
io::ErrorKind::InvalidInput
);
}
}