mxsh 0.1.0

Embeddable POSIX-style shell parser and runtime
Documentation
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

fn cstring_arg(arg: &str) -> Result<CString, std::io::Error> {
    CString::new(arg)
        .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "argument contains NUL"))
}

fn cstring_env(key: &str, value: &str) -> Result<CString, std::io::Error> {
    CString::new(format!("{key}={value}")).map_err(|_| {
        std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "environment entry contains NUL",
        )
    })
}

fn cstring_path(path: &str) -> Result<CString, std::io::Error> {
    CString::new(path)
        .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL"))
}

pub(crate) fn exec_replace(
    program: &str,
    argv: &[String],
    env: &[(String, String)],
    cwd: &Path,
) -> Result<(), std::io::Error> {
    let c_args: Vec<CString> = argv
        .iter()
        .map(|a| cstring_arg(a))
        .collect::<Result<_, _>>()?;
    let c_ptrs: Vec<*const libc::c_char> = c_args
        .iter()
        .map(|a| a.as_ptr())
        .chain(std::iter::once(std::ptr::null()))
        .collect();
    let c_env: Vec<CString> = env
        .iter()
        .map(|(k, v)| cstring_env(k, v))
        .collect::<Result<_, _>>()?;
    let c_env_ptrs: Vec<*const libc::c_char> = c_env
        .iter()
        .map(|e| e.as_ptr())
        .chain(std::iter::once(std::ptr::null()))
        .collect();
    let c_cwd = CString::new(cwd.as_os_str().as_bytes())
        .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "cwd contains NUL"))?;

    if unsafe { libc::chdir(c_cwd.as_ptr()) } != 0 {
        return Err(std::io::Error::last_os_error());
    }

    let mut candidates: Vec<String> = Vec::new();
    if program.contains('/') {
        candidates.push(program.to_string());
    } else {
        let path = env
            .iter()
            .find_map(|(k, v)| if k == "PATH" { Some(v.as_str()) } else { None })
            .unwrap_or("/usr/bin:/bin");
        for dir in path.split(':') {
            candidates.push(format!("{dir}/{program}"));
        }
    }

    let mut last_err = std::io::Error::from_raw_os_error(libc::ENOENT);
    for candidate in &candidates {
        let c_prog = cstring_path(candidate)?;
        unsafe { libc::execve(c_prog.as_ptr(), c_ptrs.as_ptr(), c_env_ptrs.as_ptr()) };
        let err = std::io::Error::last_os_error();
        if err.raw_os_error() != Some(libc::ENOENT) {
            last_err = err;
            break;
        }
        last_err = err;
    }

    Err(last_err)
}