use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{Context, Result, bail};
use sshenv_cli_models::RunArgs;
use sshenv_shims::{default_bindings_path, load_bindings, resolve_shim_dir};
use crate::commands::{Context as CmdContext, load_and_unlock};
pub fn run(ctx: &CmdContext, args: RunArgs) -> Result<()> {
if args.command.is_empty() {
bail!("no command provided; usage: sshenv run <profile> -- <cmd> [args...]");
}
let (vault, _key) = load_and_unlock(&ctx.vault_path)?;
let Some(vars) = vault.profiles.get(&args.profile) else {
bail!("no such profile: {}", args.profile);
};
let (cmd_name, cmd_args) = args
.command
.split_first()
.expect("not empty, checked above");
let shim_dir = current_shim_dir();
let target = resolve_command_skipping_shim_dir(cmd_name, &shim_dir)?;
let mut child = Command::new(&target);
child.args(cmd_args);
for (k, v) in vars {
child.env(k, v);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
let err = child.exec();
Err(err).with_context(|| format!("failed to exec {}", target.display()))?;
unreachable!()
}
#[cfg(not(unix))]
{
let status = child
.status()
.with_context(|| format!("failed to spawn {}", target.display()))?;
std::process::exit(status.code().unwrap_or(1));
}
}
fn current_shim_dir() -> PathBuf {
let bindings = load_bindings(&default_bindings_path()).unwrap_or_default();
resolve_shim_dir(&bindings)
}
fn resolve_command_skipping_shim_dir(cmd: &str, shim_dir: &Path) -> Result<PathBuf> {
let path_env = std::env::var_os("PATH").unwrap_or_default();
resolve_command_skipping_shim_dir_in(cmd, shim_dir, path_env.as_os_str())
}
fn resolve_command_skipping_shim_dir_in(
cmd: &str,
shim_dir: &Path,
path_env: &OsStr,
) -> Result<PathBuf> {
if cmd.contains('/') {
return Ok(PathBuf::from(cmd));
}
let canon_shim = shim_dir.canonicalize().ok();
let mut found_outside_shim_dir = false;
for dir in std::env::split_paths(path_env) {
let is_shim_dir = matches!(
(&canon_shim, dir.canonicalize().ok()),
(Some(a), Some(b)) if a == &b
);
if is_shim_dir {
continue;
}
found_outside_shim_dir = true;
let candidate = dir.join(cmd);
if is_executable_file(&candidate) {
return Ok(candidate);
}
}
if found_outside_shim_dir {
bail!("command '{cmd}' not found on PATH");
}
bail!(
"command '{cmd}' not found on PATH outside the shim directory ({}). \
Aborting to prevent infinite shim recursion.",
shim_dir.display()
);
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|m| m.is_file() && (m.permissions().mode() & 0o111 != 0))
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable_file(path: &Path) -> bool {
path.is_file()
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsString;
use std::fs;
#[cfg(unix)]
fn mk_executable(path: &Path) {
use std::os::unix::fs::PermissionsExt;
fs::write(path, "#!/bin/sh\nexit 0\n").unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).unwrap();
}
#[cfg(not(unix))]
fn mk_executable(path: &Path) {
fs::write(path, "").unwrap();
}
fn path_os(parts: &[&Path]) -> OsString {
std::env::join_paths(parts.iter().map(|p| p.as_os_str())).unwrap()
}
#[test]
fn resolve_absolute_path_passes_through() {
let path = path_os(&[]);
let r =
resolve_command_skipping_shim_dir_in("/usr/bin/env", Path::new("/nonexistent"), &path)
.unwrap();
assert_eq!(r, PathBuf::from("/usr/bin/env"));
}
#[test]
fn resolve_relative_with_slash_passes_through() {
let path = path_os(&[]);
let r = resolve_command_skipping_shim_dir_in("./foo/bar", Path::new("/nonexistent"), &path)
.unwrap();
assert_eq!(r, PathBuf::from("./foo/bar"));
}
#[test]
fn resolve_finds_command_outside_shim_dir() {
let tmp = tempfile::tempdir().unwrap();
let shim_dir = tmp.path().join("shim");
let real_dir = tmp.path().join("real");
fs::create_dir_all(&shim_dir).unwrap();
fs::create_dir_all(&real_dir).unwrap();
mk_executable(&shim_dir.join("mycmd"));
mk_executable(&real_dir.join("mycmd"));
let path = path_os(&[shim_dir.as_path(), real_dir.as_path()]);
let r = resolve_command_skipping_shim_dir_in("mycmd", &shim_dir, &path).unwrap();
assert_eq!(r, real_dir.join("mycmd"));
}
#[test]
fn resolve_errors_when_only_in_shim_dir() {
let tmp = tempfile::tempdir().unwrap();
let shim_dir = tmp.path().join("shim");
fs::create_dir_all(&shim_dir).unwrap();
mk_executable(&shim_dir.join("mycmd"));
let path = path_os(&[shim_dir.as_path()]);
let err = resolve_command_skipping_shim_dir_in("mycmd", &shim_dir, &path).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("infinite shim recursion"),
"expected loop-prevention message, got: {msg}"
);
assert!(
msg.contains("not found on PATH outside the shim directory"),
"unexpected error: {msg}"
);
}
#[test]
fn resolve_errors_when_command_missing_entirely() {
let tmp = tempfile::tempdir().unwrap();
let shim_dir = tmp.path().join("shim");
let other_dir = tmp.path().join("other");
fs::create_dir_all(&shim_dir).unwrap();
fs::create_dir_all(&other_dir).unwrap();
let path = path_os(&[shim_dir.as_path(), other_dir.as_path()]);
let err =
resolve_command_skipping_shim_dir_in("no-such-cmd", &shim_dir, &path).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("not found on PATH"),
"expected 'not found on PATH' message, got: {msg}"
);
assert!(
!msg.contains("infinite shim recursion"),
"should not claim recursion when the command is simply missing: {msg}"
);
}
#[test]
fn resolve_canonicalizes_shim_dir_for_comparison() {
let tmp = tempfile::tempdir().unwrap();
let shim_dir = tmp.path().join("shim");
let real_dir = tmp.path().join("real");
fs::create_dir_all(&shim_dir).unwrap();
fs::create_dir_all(&real_dir).unwrap();
mk_executable(&shim_dir.join("mycmd"));
mk_executable(&real_dir.join("mycmd"));
let shim_with_slash: PathBuf = format!("{}/", shim_dir.display()).into();
let path = path_os(&[shim_with_slash.as_path(), real_dir.as_path()]);
let r = resolve_command_skipping_shim_dir_in("mycmd", &shim_dir, &path).unwrap();
assert_eq!(r, real_dir.join("mycmd"));
}
#[test]
fn resolve_ignores_nonexistent_path_entries() {
let tmp = tempfile::tempdir().unwrap();
let shim_dir = tmp.path().join("shim");
let real_dir = tmp.path().join("real");
let nope = tmp.path().join("nonexistent");
fs::create_dir_all(&shim_dir).unwrap();
fs::create_dir_all(&real_dir).unwrap();
mk_executable(&real_dir.join("mycmd"));
let path = path_os(&[nope.as_path(), shim_dir.as_path(), real_dir.as_path()]);
let r = resolve_command_skipping_shim_dir_in("mycmd", &shim_dir, &path).unwrap();
assert_eq!(r, real_dir.join("mycmd"));
}
}