use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::fs::{File, OpenOptions};
use std::io;
use std::io::ErrorKind;
use std::sync::{Arc, OnceLock};
use crate::exec::Redirection;
#[cfg(windows)]
use crate::process::ExtProcessState;
use crate::process::Process;
pub(crate) use os::OsOptions;
pub(crate) use os::env_keys_cmp;
pub use os::make_pipe;
#[derive(Clone, Debug)]
pub(crate) enum Arg {
Regular(OsString),
#[cfg(windows)]
Raw(OsString),
}
impl Arg {
pub fn display_escaped(&self) -> String {
match self {
Arg::Regular(s) => display_escape(&s.to_string_lossy()).into_owned(),
#[cfg(windows)]
Arg::Raw(s) => s.to_string_lossy().into_owned(),
}
}
}
pub(crate) fn display_escape(s: &str) -> Cow<'_, str> {
fn nice_char(c: char) -> bool {
match c {
'-' | '_' | '.' | ',' | '/' => true,
c if c.is_ascii_alphanumeric() => true,
_ => false,
}
}
if !s.chars().all(nice_char) {
Cow::Owned(format!("'{}'", s.replace("'", r#"'\''"#)))
} else {
Cow::Borrowed(s)
}
}
pub(crate) struct SpawnResult {
pub process: Process,
pub stdin: Option<File>,
pub stdout: Option<File>,
pub stderr: Option<File>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn(
argv: Vec<Arg>,
stdin: Arc<Redirection>,
stdout: Arc<Redirection>,
stderr: Arc<Redirection>,
detached: bool,
executable: Option<&OsStr>,
env: Option<&[(OsString, OsString)]>,
cwd: Option<&OsStr>,
os_options: OsOptions,
) -> io::Result<SpawnResult> {
if argv.is_empty() {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"argv must not be empty",
));
}
let (parent_ends, child_ends) = setup_streams(stdin, stdout, stderr)?;
let process = os::os_start(argv, child_ends, detached, executable, env, cwd, os_options)?;
Ok(SpawnResult {
process,
stdin: parent_ends.0,
stdout: parent_ends.1,
stderr: parent_ends.2,
})
}
fn child_file(r: &Redirection) -> &File {
match r {
Redirection::File(f) => f,
_ => unreachable!(),
}
}
fn prepare_child_stream(
redir: Arc<Redirection>,
is_input: bool,
) -> io::Result<(Option<File>, Option<Arc<Redirection>>)> {
match &*redir {
Redirection::File(_) => Ok((None, Some(os::prepare_file(redir)?))),
Redirection::Pipe => {
let (parent, child) = prepare_pipe(is_input)?;
Ok((Some(parent), Some(child)))
}
Redirection::Null => Ok((None, Some(prepare_null_file(is_input)?))),
Redirection::None => Ok((None, None)),
Redirection::Merge => unreachable!(),
}
}
fn wrap_file(file: File) -> Arc<Redirection> {
Arc::new(Redirection::File(file))
}
fn prepare_pipe(parent_writes: bool) -> io::Result<(File, Arc<Redirection>)> {
let (read, write) = os::make_pipe()?;
let (parent_end, child_end) = if parent_writes {
(write, read)
} else {
(read, write)
};
Ok((parent_end, os::prepare_file(wrap_file(child_end))?))
}
fn prepare_null_file(for_read: bool) -> io::Result<Arc<Redirection>> {
let file = if for_read {
OpenOptions::new().read(true).open(os::NULL_DEVICE)?
} else {
OpenOptions::new().write(true).open(os::NULL_DEVICE)?
};
os::prepare_file(wrap_file(file))
}
fn reuse_stream(
dest: &mut Option<Arc<Redirection>>,
src: &mut Option<Arc<Redirection>>,
src_id: StandardStream,
) -> io::Result<()> {
if src.is_none() {
*src = Some(get_redirection_to_standard_stream(src_id)?);
}
*dest = src.clone();
Ok(())
}
fn setup_streams(
stdin: Arc<Redirection>,
stdout: Arc<Redirection>,
stderr: Arc<Redirection>,
) -> io::Result<(
(Option<File>, Option<File>, Option<File>),
(
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
)> {
#[derive(PartialEq, Eq, Copy, Clone)]
enum MergeKind {
ErrToOut, OutToErr, None,
}
if matches!(&*stdin, Redirection::Merge) {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"Redirection::Merge not valid for stdin",
));
}
let merge = match (
matches!(&*stdout, Redirection::Merge),
matches!(&*stderr, Redirection::Merge),
) {
(false, false) => MergeKind::None,
(false, true) => MergeKind::ErrToOut,
(true, false) => MergeKind::OutToErr,
(true, true) => {
return Err(io::Error::new(
ErrorKind::InvalidInput,
"Redirection::Merge not valid for both stdout and stderr",
));
}
};
let (parent_stdin, child_stdin) = prepare_child_stream(stdin, true)?;
let (parent_stdout, mut child_stdout) = if merge == MergeKind::OutToErr {
(None, None)
} else {
prepare_child_stream(stdout, false)?
};
let (parent_stderr, mut child_stderr) = if merge == MergeKind::ErrToOut {
(None, None)
} else {
prepare_child_stream(stderr, false)?
};
match merge {
MergeKind::ErrToOut => {
reuse_stream(&mut child_stderr, &mut child_stdout, StandardStream::Output)?
}
MergeKind::OutToErr => {
reuse_stream(&mut child_stdout, &mut child_stderr, StandardStream::Error)?
}
MergeKind::None => (),
}
Ok((
(parent_stdin, parent_stdout, parent_stderr),
(child_stdin, child_stdout, child_stderr),
))
}
#[derive(Debug, Copy, Clone)]
#[allow(dead_code)]
pub(crate) enum StandardStream {
Input = 0,
Output = 1,
Error = 2,
}
fn get_redirection_to_standard_stream(which: StandardStream) -> io::Result<Arc<Redirection>> {
static STREAMS: [OnceLock<Arc<Redirection>>; 3] =
[OnceLock::new(), OnceLock::new(), OnceLock::new()];
let lock = &STREAMS[which as usize];
if let Some(stream) = lock.get() {
return Ok(Arc::clone(stream));
}
let stream = os::make_redirection_to_standard_stream(which)?;
Ok(Arc::clone(lock.get_or_init(|| stream)))
}
#[cfg(unix)]
pub(crate) mod os {
use super::*;
#[derive(Default)]
pub struct OsOptions {
pub setuid: Option<u32>,
pub setgid: Option<u32>,
pub setpgid: Option<u32>,
pub pre_exec_fns: Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>>,
}
impl OsOptions {
pub fn setpgid_is_set(&self) -> bool {
self.setpgid.is_some()
}
pub fn set_pgid_value(&mut self, pgid: u32) {
self.setpgid = Some(pgid);
}
}
pub const NULL_DEVICE: &str = "/dev/null";
pub(crate) fn env_keys_cmp(a: &OsStr, b: &OsStr) -> std::cmp::Ordering {
a.cmp(b)
}
use crate::posix;
use std::collections::HashSet;
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::fd::{AsRawFd, RawFd};
pub use crate::posix::make_redirection_to_standard_stream;
fn read_exact_or_eof<const N: usize>(source: &mut File) -> io::Result<Option<[u8; N]>> {
let mut buf = [0u8; N];
let mut total_read = 0;
while total_read < N {
let n = source.read(&mut buf[total_read..])?;
if n == 0 {
break;
}
total_read += n;
}
match total_read {
0 => Ok(None),
n if n == N => Ok(Some(buf)),
_ => Err(io::ErrorKind::UnexpectedEof.into()),
}
}
pub(crate) fn os_start(
argv: Vec<Arg>,
child_ends: (
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
detached: bool,
executable: Option<&OsStr>,
env: Option<&[(OsString, OsString)]>,
cwd: Option<&OsStr>,
os_options: OsOptions,
) -> io::Result<Process> {
let argv: Vec<OsString> = argv.into_iter().map(|Arg::Regular(s)| s).collect();
let mut exec_fail_pipe = posix::pipe()?;
let child_env = env.map(format_env);
let cmd_to_exec = executable.unwrap_or(&argv[0]);
let just_exec = posix::prep_exec(cmd_to_exec, &argv, child_env.as_deref())?;
let do_chdir = cwd.map(posix::prep_chdir).transpose()?;
let pid;
unsafe {
match posix::fork()? {
Some(child_pid) => {
pid = child_pid;
}
None => {
drop(exec_fail_pipe.0);
let result = do_exec(just_exec, child_ends, do_chdir, os_options);
let error_code = match result {
Ok(()) => unreachable!(),
Err(e) => e.raw_os_error().unwrap_or(-1),
} as u32;
exec_fail_pipe.1.write_all(&error_code.to_le_bytes()).ok();
posix::_exit(127);
}
}
}
drop(child_ends);
drop(exec_fail_pipe.1);
match read_exact_or_eof::<4>(&mut exec_fail_pipe.0)? {
None => Ok(Process::new(pid, (), detached)),
Some(error_buf) => {
let error_code = u32::from_le_bytes(error_buf);
Err(io::Error::from_raw_os_error(error_code as i32))
}
}
}
fn format_env(env: &[(OsString, OsString)]) -> Vec<OsString> {
let mut seen = HashSet::<&OsStr>::new();
let mut formatted: Vec<_> = env
.iter()
.rev()
.filter(|&(k, _)| seen.insert(k))
.map(|(k, v)| {
let mut fmt = k.clone();
fmt.push("=");
fmt.push(v);
fmt
})
.collect();
formatted.reverse();
formatted
}
fn redirect_streams(slots: &[Option<Arc<Redirection>>; 3]) -> io::Result<()> {
let mut sources: [Option<i32>; 3] = [
slots[0].as_ref().map(|a| child_file(a).as_raw_fd()),
slots[1].as_ref().map(|a| child_file(a).as_raw_fd()),
slots[2].as_ref().map(|a| child_file(a).as_raw_fd()),
];
for i in 0..3 {
if let Some(fd) = sources[i] {
let will_be_overwritten = (0..=2).contains(&fd)
&& fd != i as i32
&& sources[fd as usize].is_some_and(|s| s != fd);
if will_be_overwritten {
sources[i] = Some(posix::fcntl(fd, posix::F_DUPFD_CLOEXEC, Some(3))?);
}
}
}
for (i, &source) in sources.iter().enumerate() {
let target = i as i32;
let Some(fd) = source else { continue };
if fd == target {
set_inheritable(fd, true)?;
} else {
posix::dup2(fd, target)?;
if fd >= 3 {
let _ = set_inheritable(fd, false);
}
}
}
Ok(())
}
fn do_exec(
just_exec: impl FnOnce() -> io::Result<()>,
child_ends: (
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
chdir: Option<impl FnOnce() -> io::Result<()>>,
os_options: OsOptions,
) -> io::Result<()> {
let (stdin, stdout, stderr) = child_ends;
let slots = std::mem::ManuallyDrop::new([stdin, stdout, stderr]);
let mut just_exec = std::mem::ManuallyDrop::new(just_exec);
let mut os_options = std::mem::ManuallyDrop::new(os_options);
if let Some(chdir) = chdir {
chdir()?;
}
redirect_streams(&slots)?;
posix::reset_sigpipe()?;
if let Some(gid) = os_options.setgid {
posix::setgid(gid)?;
}
if let Some(uid) = os_options.setuid {
posix::setuid(uid)?;
}
if let Some(pgid) = os_options.setpgid {
posix::setpgid(0, pgid)?;
}
for f in &mut os_options.pre_exec_fns {
f()?;
}
let just_exec = unsafe { std::mem::ManuallyDrop::take(&mut just_exec) };
just_exec()?;
unreachable!();
}
pub fn set_inheritable(fd: RawFd, inheritable: bool) -> io::Result<()> {
let old = posix::fcntl(fd, posix::F_GETFD, None)?;
let new = if inheritable {
old & !posix::FD_CLOEXEC
} else {
old | posix::FD_CLOEXEC
};
if new != old {
posix::fcntl(fd, posix::F_SETFD, Some(new))?;
}
Ok(())
}
pub fn prepare_file(redir: Arc<Redirection>) -> io::Result<Arc<Redirection>> {
Ok(redir)
}
pub fn make_pipe() -> io::Result<(File, File)> {
posix::pipe()
}
}
#[cfg(windows)]
pub(crate) mod os {
use super::*;
#[derive(Clone, Default)]
pub struct OsOptions {
pub creation_flags: u32,
}
pub const NULL_DEVICE: &str = "nul";
pub(crate) fn env_keys_cmp(a: &OsStr, b: &OsStr) -> std::cmp::Ordering {
let a: Vec<u16> = a.encode_wide().collect();
let b: Vec<u16> = b.encode_wide().collect();
win32::compare_string_ordinal(&a, &b, true)
}
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::io;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::os::windows::io::{AsRawHandle, RawHandle};
use crate::win32;
pub use crate::win32::make_redirection_to_standard_stream;
pub(crate) fn os_start(
argv: Vec<Arg>,
child_ends: (
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
Option<Arc<Redirection>>,
),
detached: bool,
executable: Option<&OsStr>,
env: Option<&[(OsString, OsString)]>,
cwd: Option<&OsStr>,
os_options: OsOptions,
) -> io::Result<Process> {
fn raw(opt: Option<&Arc<Redirection>>) -> Option<RawHandle> {
opt.map(|r| child_file(r).as_raw_handle())
}
let (mut child_stdin, mut child_stdout, mut child_stderr) = child_ends;
ensure_child_stream(&mut child_stdin, StandardStream::Input)?;
ensure_child_stream(&mut child_stdout, StandardStream::Output)?;
ensure_child_stream(&mut child_stderr, StandardStream::Error)?;
let cmdline = assemble_cmdline(argv)?;
let env_block = env.map(format_env_block);
let executable_located = executable.map(|e| locate_in_path(e.to_owned()));
let (handle, pid) = win32::CreateProcess(
executable_located.as_ref().map(OsString::as_ref),
&cmdline,
env_block.as_deref(),
cwd,
true,
os_options.creation_flags,
raw(child_stdin.as_ref()),
raw(child_stdout.as_ref()),
raw(child_stderr.as_ref()),
win32::STARTF_USESTDHANDLES,
)?;
Ok(Process::new(pid as u32, ExtProcessState(handle), detached))
}
fn format_env_block(env: &[(OsString, OsString)]) -> Vec<u16> {
let mut sorted = std::collections::BTreeMap::<EnvKey, &OsStr>::new();
for (k, v) in env.iter().rev() {
sorted.entry(EnvKey::new(k)).or_insert(v);
}
let mut block = vec![];
for (k, v) in sorted {
block.extend(k.0);
block.push('=' as u16);
block.extend(v.encode_wide());
block.push(0);
}
block.push(0);
block
}
struct EnvKey(Vec<u16>);
impl EnvKey {
fn new(s: &OsStr) -> Self {
EnvKey(s.encode_wide().collect())
}
}
impl Ord for EnvKey {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
win32::compare_string_ordinal(&self.0, &other.0, true)
}
}
impl PartialOrd for EnvKey {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for EnvKey {
fn eq(&self, other: &Self) -> bool {
self.cmp(other).is_eq()
}
}
impl Eq for EnvKey {}
fn ensure_child_stream(
stream: &mut Option<Arc<Redirection>>,
which: StandardStream,
) -> io::Result<()> {
if stream.is_none() {
*stream = Some(get_redirection_to_standard_stream(which)?);
}
Ok(())
}
pub fn set_inheritable(f: &File, inheritable: bool) -> io::Result<()> {
win32::SetHandleInformation(
f.as_raw_handle(),
win32::HANDLE_FLAG_INHERIT,
if inheritable { 1 } else { 0 },
)?;
Ok(())
}
pub fn prepare_file(redir: Arc<Redirection>) -> io::Result<Arc<Redirection>> {
set_inheritable(child_file(&redir), true)?;
Ok(redir)
}
pub fn make_pipe() -> io::Result<(File, File)> {
win32::make_pipe()
}
fn locate_in_path(executable: OsString) -> OsString {
let Some(path_var) = env::var_os("PATH") else {
return executable;
};
for dir in env::split_paths(&path_var) {
let candidate = dir
.join(&executable)
.with_extension(std::env::consts::EXE_EXTENSION);
if candidate.exists() {
return candidate.into_os_string();
}
}
executable
}
fn assemble_cmdline(argv: Vec<Arg>) -> io::Result<OsString> {
let mut cmdline = vec![];
for (i, arg) in argv.iter().enumerate() {
if i > 0 {
cmdline.push(' ' as u16);
}
let s = match arg {
Arg::Regular(s) | Arg::Raw(s) => s,
};
if s.encode_wide().any(|c| c == 0) {
return Err(io::Error::from_raw_os_error(win32::ERROR_BAD_PATHNAME as _));
}
match arg {
Arg::Regular(s) => append_quoted(s, &mut cmdline),
Arg::Raw(s) => cmdline.extend(s.encode_wide()),
}
}
Ok(OsString::from_wide(&cmdline))
}
fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>) {
if !arg.is_empty()
&& !arg.encode_wide().any(|c| {
c == ' ' as u16
|| c == '\t' as u16
|| c == '\n' as u16
|| c == '\x0b' as u16
|| c == '\"' as u16
})
{
cmdline.extend(arg.encode_wide());
return;
}
cmdline.push('"' as u16);
let arg: Vec<_> = arg.encode_wide().collect();
let mut i = 0;
while i < arg.len() {
let mut num_backslashes = 0;
while i < arg.len() && arg[i] == '\\' as u16 {
i += 1;
num_backslashes += 1;
}
if i == arg.len() {
for _ in 0..num_backslashes * 2 {
cmdline.push('\\' as u16);
}
break;
} else if arg[i] == b'"' as u16 {
for _ in 0..num_backslashes * 2 + 1 {
cmdline.push('\\' as u16);
}
cmdline.push(arg[i]);
} else {
for _ in 0..num_backslashes {
cmdline.push('\\' as u16);
}
cmdline.push(arg[i]);
}
i += 1;
}
cmdline.push('"' as u16);
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_block(block: &[u16]) -> Vec<(OsString, OsString)> {
let mut entries = Vec::new();
let mut start = 0;
for (i, &u) in block.iter().enumerate() {
if u == 0 {
if i == start {
break;
}
let chunk = &block[start..i];
let eq = chunk.iter().position(|&u| u == b'=' as u16).unwrap();
entries.push((
OsString::from_wide(&chunk[..eq]),
OsString::from_wide(&chunk[eq + 1..]),
));
start = i + 1;
}
}
entries
}
fn pair(k: &str, v: &str) -> (OsString, OsString) {
(OsString::from(k), OsString::from(v))
}
#[test]
fn format_env_block_dedup_keeps_last_occurrence() {
let env = vec![pair("A", "1"), pair("B", "x"), pair("A", "2")];
assert_eq!(
parse_block(&format_env_block(&env)),
vec![pair("A", "2"), pair("B", "x")]
);
}
#[test]
fn format_env_block_is_sorted() {
let env = vec![pair("Z", "1"), pair("a", "2"), pair("M", "3")];
assert_eq!(
parse_block(&format_env_block(&env)),
vec![pair("a", "2"), pair("M", "3"), pair("Z", "1")]
);
}
#[test]
fn format_env_block_dedup_is_case_insensitive() {
let env = vec![pair("Path", "old"), pair("FOO", "y"), pair("PATH", "new")];
assert_eq!(
parse_block(&format_env_block(&env)),
vec![pair("FOO", "y"), pair("PATH", "new")]
);
}
#[test]
fn format_env_block_dedup_folds_non_ascii_case() {
let env = vec![pair("Ä", "old"), pair("ä", "new")];
assert_eq!(parse_block(&format_env_block(&env)), vec![pair("ä", "new")]);
}
}
}