runx-core 0.6.19

Pure Rust parity kernel for runx state-machine and policy decisions.
Documentation
use super::posix_basename::posix_basename;

pub(crate) struct InlineInterpreter {
    pub command: String,
    pub trigger: String,
}

pub(crate) fn detect_inline_interpreter(
    command: Option<&str>,
    args: &[String],
) -> Option<InlineInterpreter> {
    let command_name = normalize_executable_name(command?);
    if command_name.is_empty() {
        return None;
    }

    if command_name == "env" {
        let (forwarded_command, forwarded_args) = unwrap_env_command(args)?;
        return detect_inline_interpreter(Some(&forwarded_command), &forwarded_args);
    }

    let lowered_args = args
        .iter()
        .map(|arg| arg.trim().to_owned())
        .collect::<Vec<_>>();

    detect_inline_trigger(&command_name, &lowered_args).map(|trigger| InlineInterpreter {
        command: command_name,
        trigger,
    })
}

fn detect_inline_trigger(command_name: &str, args: &[String]) -> Option<String> {
    if matches!(command_name, "node" | "nodejs" | "bun") {
        return find_exact_arg(args, &["-e", "--eval", "-p", "--print"]);
    }
    if command_name == "deno" {
        return args
            .first()
            .filter(|arg| arg.eq_ignore_ascii_case("eval"))
            .cloned();
    }
    if is_python_like(command_name) {
        return find_exact_arg(args, &["-c"]);
    }
    if matches!(command_name, "ruby" | "perl" | "lua") {
        return find_exact_arg(args, &["-e"]);
    }
    if command_name == "php" {
        return find_exact_arg(args, &["-r"]);
    }
    if matches!(
        command_name,
        "sh" | "bash" | "zsh" | "dash" | "ksh" | "ash" | "fish"
    ) {
        return args.iter().find(|arg| is_shell_c_flag(arg)).cloned();
    }
    if matches!(command_name, "pwsh" | "powershell") {
        return find_exact_arg(args, &["-c", "-command", "-encodedcommand"]);
    }
    if command_name == "cmd" {
        return find_exact_arg(args, &["/c", "/k"]).map(|trigger| trigger.to_ascii_lowercase());
    }
    None
}

fn normalize_executable_name(command: &str) -> String {
    strip_windows_executable_suffix(&posix_basename(command).to_lowercase())
}

fn strip_windows_executable_suffix(command: &str) -> String {
    for suffix in [".exe", ".cmd", ".bat"] {
        if let Some(stripped) = command.strip_suffix(suffix) {
            return stripped.to_owned();
        }
    }
    command.to_owned()
}

fn unwrap_env_command(args: &[String]) -> Option<(String, Vec<String>)> {
    let trimmed_args = args
        .iter()
        .map(|arg| arg.trim())
        .filter(|arg| !arg.is_empty())
        .collect::<Vec<_>>();
    let mut index = 0;

    while trimmed_args
        .get(index)
        .is_some_and(|arg| is_env_assignment(arg))
    {
        index += 1;
    }

    let command = trimmed_args.get(index)?;
    let forwarded_args = trimmed_args[index + 1..]
        .iter()
        .map(|arg| (*arg).to_owned())
        .collect();
    Some(((*command).to_owned(), forwarded_args))
}

fn find_exact_arg(args: &[String], candidates: &[&str]) -> Option<String> {
    args.iter()
        .find(|arg| {
            candidates
                .iter()
                .any(|candidate| arg.eq_ignore_ascii_case(candidate))
        })
        .cloned()
}

fn is_env_assignment(value: &str) -> bool {
    let Some((name, _)) = value.split_once('=') else {
        return false;
    };
    let mut chars = name.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    (first == '_' || first.is_ascii_alphabetic())
        && chars.all(|char| char == '_' || char.is_ascii_alphanumeric())
}

fn is_python_like(command_name: &str) -> bool {
    command_name == "python"
        || command_name == "pypy"
        || command_name
            .strip_prefix("python")
            .is_some_and(is_python_version_suffix)
}

fn is_python_version_suffix(value: &str) -> bool {
    if value.is_empty() {
        return false;
    }
    let parts = value.split('.').collect::<Vec<_>>();
    matches!(parts.as_slice(), [major] if digits_only(major))
        || matches!(parts.as_slice(), [major, minor] if digits_only(major) && digits_only(minor))
}

fn digits_only(value: &str) -> bool {
    !value.is_empty() && value.chars().all(|char| char.is_ascii_digit())
}

fn is_shell_c_flag(value: &str) -> bool {
    let Some(flags) = value.strip_prefix('-') else {
        return false;
    };
    !flags.is_empty()
        && flags.chars().all(|char| char.is_ascii_alphabetic())
        && flags.chars().any(|char| char == 'c')
}

#[cfg(test)]
mod tests {
    use super::detect_inline_interpreter;

    #[test]
    fn unwraps_env_assignments_before_interpreter_detection() {
        let args = vec![
            "PYTHONPATH=.".to_owned(),
            "python3".to_owned(),
            "-c".to_owned(),
        ];

        let detected = detect_inline_interpreter(Some("/usr/bin/env"), &args);

        assert!(detected.is_some_and(|value| value.command == "python3" && value.trigger == "-c"));
    }

    #[test]
    fn strips_windows_executable_suffix_and_detects_node_eval() {
        let args = vec!["-e".to_owned(), "console.log('hi')".to_owned()];

        let detected = detect_inline_interpreter(Some(r"C:\Tools\node.exe"), &args);

        assert!(detected.is_some_and(|value| value.command == "node" && value.trigger == "-e"));
    }

    #[test]
    fn lowercases_cmd_trigger_to_match_typescript() {
        let args = vec!["/C".to_owned(), "echo hi".to_owned()];

        let detected = detect_inline_interpreter(Some("cmd"), &args);

        assert!(detected.is_some_and(|value| value.command == "cmd" && value.trigger == "/c"));
    }
}