use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub const DEFAULT_BINARIES: &[&str] = &[
"bash", "zsh", "sh", "git", "node", "nodejs", "npm", "npx", "pnpm", "yarn", "bun", "deno",
"python", "python3", "pip", "pip3", "cargo", "rustc", "go", "docker", "podman",
];
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ShimInfo {
pub name: String,
pub path: PathBuf,
pub is_symlink: bool,
pub is_executable: bool,
pub target: Option<String>,
}
pub struct ShimRegistry;
impl ShimRegistry {
pub fn default_binaries() -> Vec<&'static str> {
DEFAULT_BINARIES.to_vec()
}
pub fn detect_for_project(project_root: &Path) -> Vec<String> {
let mut binaries = Vec::new();
binaries.push("sh".to_string());
binaries.push("bash".to_string());
if project_root.join(".git").exists() {
binaries.push("git".to_string());
}
if project_root.join("Cargo.toml").exists() {
binaries.push("cargo".to_string());
binaries.push("rustc".to_string());
}
let is_node = project_root.join("package.json").exists()
|| project_root.join("pnpm-lock.yaml").exists()
|| project_root.join("yarn.lock").exists()
|| project_root.join("bun.lockb").exists();
if is_node {
binaries.push("node".to_string());
binaries.push("nodejs".to_string());
binaries.push("npm".to_string());
binaries.push("npx".to_string());
if project_root.join("pnpm-lock.yaml").exists() {
binaries.push("pnpm".to_string());
}
if project_root.join("yarn.lock").exists() {
binaries.push("yarn".to_string());
}
if project_root.join("bun.lockb").exists() {
binaries.push("bun".to_string());
}
if project_root.join("deno.json").exists() || project_root.join("deno.jsonc").exists() {
binaries.push("deno".to_string());
}
}
let is_python = project_root.join("pyproject.toml").exists()
|| project_root.join("requirements.txt").exists()
|| project_root.join("Pipfile").exists()
|| project_root.join("poetry.lock").exists()
|| project_root.join("setup.py").exists();
if is_python {
binaries.push("python".to_string());
binaries.push("python3".to_string());
binaries.push("pip".to_string());
binaries.push("pip3".to_string());
}
if project_root.join("go.mod").exists() {
binaries.push("go".to_string());
}
let is_docker = project_root.join("Dockerfile").exists()
|| project_root.join("docker-compose.yml").exists()
|| project_root.join("docker-compose.yaml").exists()
|| project_root.join("compose.yaml").exists()
|| project_root.join("compose.yml").exists();
if is_docker {
binaries.push("docker".to_string());
binaries.push("podman".to_string());
}
binaries.sort();
binaries.dedup();
binaries
}
pub fn generate_unix_shim_script(binary_name: &str, vetto_bin_hint: Option<&Path>) -> String {
let vetto_bin = vetto_bin_hint.and_then(|p| p.to_str()).unwrap_or("vetto");
format!(
r#"#!/bin/sh
# Vetto transparent binary shim for: {binary_name}
# Automatically generated by `vetto enable` / `vetto hook install`. Do not edit.
if [ -n "$VETTO_SANDBOXED" ] || [ -n "$VETTO_SHIM_ACTIVE" ] || [ -n "$VETTO_WRAPPED" ]; then
# Recursion barrier active — resolve real host binary outside vetto shims
REAL_BIN=""
_OLD_IFS="$IFS"
IFS=:
for _dir in $PATH; do
case "$_dir" in
*".vetto/shims"*|*"/vetto/shims"*) continue ;;
"") continue ;;
*)
if [ -f "$_dir/{binary_name}" ] && [ -x "$_dir/{binary_name}" ]; then
REAL_BIN="$_dir/{binary_name}"
break
fi
;;
esac
done
IFS="$_OLD_IFS"
unset _OLD_IFS
if [ -n "$REAL_BIN" ]; then
exec "$REAL_BIN" "$@"
fi
fi
# Not sandboxed yet: invoke Vetto native shim dispatcher
export VETTO_WRAPPED=1
VETTO_EXE="{vetto_bin}"
if command -v "$VETTO_EXE" >/dev/null 2>&1; then
exec "$VETTO_EXE" shim "{binary_name}" -- "$@"
elif command -v vetto >/dev/null 2>&1; then
exec vetto shim "{binary_name}" -- "$@"
else
echo "vetto: error: could not locate vetto binary to execute shim for {binary_name}" >&2
exit 127
fi
"#
)
}
pub fn generate_windows_cmd_shim(binary_name: &str, vetto_bin_hint: Option<&Path>) -> String {
let vetto_bin = vetto_bin_hint.and_then(|p| p.to_str()).unwrap_or("vetto");
format!(
r#"@echo off
rem Vetto transparent binary shim for: {binary_name}
rem Automatically generated by `vetto enable` / `vetto hook install`. Do not edit.
if "%VETTO_SANDBOXED%"=="1" goto passthrough
if "%VETTO_SHIM_ACTIVE%"=="1" goto passthrough
if "%VETTO_WRAPPED%"=="1" goto passthrough
set "VETTO_WRAPPED=1"
"{vetto_bin}" shim "{binary_name}" -- %*
exit /b %ERRORLEVEL%
:passthrough
rem Fall back to real binary
setlocal enabledelayedexpansion
for %%i in ({binary_name}.exe {binary_name}.cmd {binary_name}.bat {binary_name}) do (
set "REAL_BIN=%%~$PATH:i"
if defined REAL_BIN (
echo !REAL_BIN! | findstr /i /c:".vetto\shims" >nul
if errorlevel 1 (
endlocal
"%%REAL_BIN%%" %*
exit /b !ERRORLEVEL!
)
)
)
endlocal
"{binary_name}" %*
"#
)
}
pub fn create_shims(
target_dir: &Path,
binaries: &[String],
vetto_bin_hint: Option<&Path>,
) -> Result<Vec<PathBuf>> {
fs::create_dir_all(target_dir)
.with_context(|| format!("failed to create shims dir: {}", target_dir.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(target_dir)?.permissions();
perms.set_mode(0o755);
let _ = fs::set_permissions(target_dir, perms);
}
let mut created = Vec::new();
for name in binaries {
let shim_path = target_dir.join(name);
let content = Self::generate_unix_shim_script(name, vetto_bin_hint);
fs::write(&shim_path, content)
.with_context(|| format!("failed to write shim: {}", shim_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&shim_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&shim_path, perms).with_context(|| {
format!("failed to make shim executable: {}", shim_path.display())
})?;
}
#[cfg(windows)]
{
let cmd_path = target_dir.join(format!("{name}.cmd"));
let cmd_content = Self::generate_windows_cmd_shim(name, vetto_bin_hint);
fs::write(&cmd_path, cmd_content).with_context(|| {
format!("failed to write windows shim: {}", cmd_path.display())
})?;
created.push(cmd_path);
}
created.push(shim_path);
}
Ok(created)
}
pub fn remove_shims(target_dir: &Path, binaries: Option<&[String]>) -> Result<Vec<PathBuf>> {
if !target_dir.exists() {
return Ok(Vec::new());
}
let mut removed = Vec::new();
if let Some(list) = binaries {
for name in list {
let shim_path = target_dir.join(name);
if shim_path.exists() {
let _ = fs::remove_file(&shim_path);
removed.push(shim_path);
}
let cmd_path = target_dir.join(format!("{name}.cmd"));
if cmd_path.exists() {
let _ = fs::remove_file(&cmd_path);
removed.push(cmd_path);
}
}
} else {
for entry in fs::read_dir(target_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() || path.is_symlink() {
let _ = fs::remove_file(&path);
removed.push(path);
}
}
}
Ok(removed)
}
pub fn list_active_shims(target_dir: &Path) -> Result<Vec<ShimInfo>> {
if !target_dir.exists() {
return Ok(Vec::new());
}
let mut shims = Vec::new();
for entry in fs::read_dir(target_dir)? {
let entry = entry?;
let path = entry.path();
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
if name.starts_with('.') {
continue;
}
let symlink_meta = fs::symlink_metadata(&path)?;
let is_symlink = symlink_meta.file_type().is_symlink();
let is_executable = {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
symlink_meta.permissions().mode() & 0o111 != 0
}
#[cfg(windows)]
{
true
}
};
shims.push(ShimInfo {
name,
path,
is_symlink,
is_executable,
target: None,
});
}
shims.sort_by(|a, b| a.name.cmp(&b.name));
Ok(shims)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_test_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"vetto-shim-reg-{name}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn default_binaries_contain_essential_tools() {
let list = ShimRegistry::default_binaries();
assert!(list.contains(&"cargo"));
assert!(list.contains(&"node"));
assert!(list.contains(&"git"));
assert!(list.contains(&"python"));
assert!(list.contains(&"bash"));
}
#[test]
fn detects_rust_and_node_manifests() {
let dir = temp_test_dir("detect-stack");
fs::write(dir.join("Cargo.toml"), "[package]\nname = \"foo\"").unwrap();
fs::write(dir.join("package.json"), "{}").unwrap();
fs::write(dir.join("pnpm-lock.yaml"), "lockfileVersion: '9.0'").unwrap();
let detected = ShimRegistry::detect_for_project(&dir);
assert!(detected.contains(&"cargo".to_string()));
assert!(detected.contains(&"rustc".to_string()));
assert!(detected.contains(&"node".to_string()));
assert!(detected.contains(&"pnpm".to_string()));
assert!(detected.contains(&"bash".to_string()));
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn creates_and_removes_shims_correctly() {
let dir = temp_test_dir("create-remove");
let shims_dir = dir.join("shims");
let bins = vec!["node".to_string(), "git".to_string()];
let created = ShimRegistry::create_shims(&shims_dir, &bins, None).unwrap();
assert!(created.len() >= 2);
assert!(shims_dir.join("node").exists());
assert!(shims_dir.join("git").exists());
let active = ShimRegistry::list_active_shims(&shims_dir).unwrap();
assert!(active.iter().any(|s| s.name.starts_with("git")));
assert!(active.iter().any(|s| s.name.starts_with("node")));
let removed = ShimRegistry::remove_shims(&shims_dir, Some(&bins)).unwrap();
assert!(removed.len() >= 2);
assert!(!shims_dir.join("node").exists());
let _ = fs::remove_dir_all(&dir);
}
}