arcbox-agent 0.6.4

Guest agent for ArcBox VMs
use std::path::Path;

use crate::rpc::ErrorResponse;

pub(super) fn spawn_error(
    command: &str,
    working_dir: &str,
    path: Option<&str>,
    error: std::io::Error,
) -> ErrorResponse {
    if error.kind() == std::io::ErrorKind::NotFound
        && requested_executable_is_missing(command, working_dir, path)
    {
        ErrorResponse::new(404, format!("command not found: '{command}': {error}"))
    } else {
        ErrorResponse::new(500, format!("failed to spawn command '{command}': {error}"))
    }
}

fn requested_executable_is_missing(command: &str, working_dir: &str, path: Option<&str>) -> bool {
    let working_dir = (!working_dir.is_empty()).then(|| Path::new(working_dir));
    if working_dir.is_some_and(|dir| !dir.is_dir()) {
        return false;
    }

    let command_path = Path::new(command);
    if command_path.is_absolute() || command.contains(std::path::MAIN_SEPARATOR) {
        let resolved =
            working_dir.map_or_else(|| command_path.to_path_buf(), |dir| dir.join(command_path));
        return !resolved.is_file();
    }

    let Some(path) = path.map(Into::into).or_else(|| std::env::var_os("PATH")) else {
        return false;
    };
    !std::env::split_paths(&path).any(|dir| {
        let candidate = if dir.is_absolute() {
            dir.join(command)
        } else if let Some(working_dir) = working_dir {
            working_dir.join(dir).join(command)
        } else {
            dir.join(command)
        };
        candidate.is_file()
    })
}

#[cfg(test)]
mod tests {
    use std::os::unix::fs::PermissionsExt as _;

    use super::*;

    #[test]
    fn spawn_error_classifies_only_a_missing_executable_as_not_found() {
        let search_dir = tempfile::tempdir().unwrap();
        let path = search_dir.path().to_str().unwrap();
        let missing = spawn_failure("arcbox-command-that-does-not-exist", "", Some(path));
        assert_eq!(missing.code, 404);
        assert!(
            missing
                .message
                .contains("arcbox-command-that-does-not-exist")
        );

        let missing_working_dir = search_dir.path().join("missing-working-directory");
        let cwd_error = spawn_failure(
            "./arcbox-command-that-does-not-exist",
            missing_working_dir.to_str().unwrap(),
            None,
        );
        assert_eq!(cwd_error.code, 500);

        let script = search_dir.path().join("missing-interpreter");
        std::fs::write(&script, "#!/arcbox/no-such-interpreter\n").unwrap();
        let mut permissions = std::fs::metadata(&script).unwrap().permissions();
        permissions.set_mode(0o755);
        std::fs::set_permissions(&script, permissions).unwrap();
        let interpreter_error = spawn_failure(script.to_str().unwrap(), "", None);
        assert_eq!(interpreter_error.code, 500);
    }

    fn spawn_failure(command: &str, working_dir: &str, path: Option<&str>) -> ErrorResponse {
        let mut process = std::process::Command::new(command);
        if !working_dir.is_empty() {
            process.current_dir(working_dir);
        }
        if let Some(path) = path {
            process.env("PATH", path);
        }
        let error = process
            .spawn()
            .expect_err("test command must fail to spawn");
        assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
        spawn_error(command, working_dir, path, error)
    }
}