Skip to main content

vanta_shim/
lib.rs

1//! `vanta-shim` — the per-tool dispatcher (see `docs/10-environments.md`).
2//!
3//! Installed under each tool's name in `~/.vanta/bin`. When invoked it finds the
4//! tool that its `argv[0]` names in the active generation, locates the real
5//! binary in the store, and `exec`s it. (Per-directory switching via a
6//! resolution cache is a later refinement; this cut dispatches from the active
7//! generation.)
8#![forbid(unsafe_code)]
9
10use std::path::{Path, PathBuf};
11use std::process::{Command, ExitCode};
12use vanta_core::StoreKey;
13use vanta_state::State;
14
15/// Entry point: derive the tool name from `argv[0]`, dispatch, and map failures
16/// to an error exit code. The binary's `main` is a thin wrapper over this.
17#[must_use]
18pub fn run() -> ExitCode {
19    let invoked = std::env::args()
20        .next()
21        .and_then(|p| {
22            Path::new(&p)
23                .file_name()
24                .map(|s| s.to_string_lossy().into_owned())
25        })
26        .unwrap_or_default();
27    let name = invoked.strip_suffix(".exe").unwrap_or(&invoked).to_string();
28    let args: Vec<String> = std::env::args().skip(1).collect();
29
30    match dispatch(&name, &args) {
31        Ok(code) => code,
32        Err(msg) => {
33            eprintln!("vanta-shim: {msg}");
34            ExitCode::from(1)
35        }
36    }
37}
38
39/// Resolve `name` in the active generation, locate its real binary in the
40/// store, and `exec` it with `args`. Returns the child exit code (non-unix) or
41/// only returns on failure (unix `exec` replaces the process).
42pub fn dispatch(name: &str, args: &[String]) -> Result<ExitCode, String> {
43    let home = home().ok_or("cannot determine VANTA_HOME")?;
44    let state = State::open(&home.join("state.db")).map_err(|e| e.to_string())?;
45    let id = state
46        .current()
47        .map_err(|e| e.to_string())?
48        .ok_or("no active generation")?;
49    let generation = state
50        .get_generation(id)
51        .map_err(|e| e.to_string())?
52        .ok_or("active generation is missing")?;
53    let (_, key) = generation
54        .tools
55        .iter()
56        .find(|(tool, _)| tool == name)
57        .ok_or_else(|| format!("`{name}` is not managed by vanta"))?;
58    // L12/M7: validate the key shape before joining it onto the store path so a
59    // malformed generation record cannot traverse out of the store.
60    let key = StoreKey::new(key.clone()).map_err(|e| e.to_string())?;
61    let entry = home.join("store").join(key.as_str());
62    let bin = find_bin(&entry, name)
63        .ok_or_else(|| format!("executable for `{name}` not found in {}", entry.display()))?;
64    exec(&bin, args)
65}
66
67fn find_bin(entry: &Path, name: &str) -> Option<PathBuf> {
68    let candidates = [
69        entry.join("bin").join(name),
70        entry.join(name),
71        entry.join("bin").join(format!("{name}.exe")),
72        entry.join(format!("{name}.exe")),
73    ];
74    candidates.into_iter().find(|c| c.is_file())
75}
76
77#[cfg(unix)]
78fn exec(bin: &Path, args: &[String]) -> Result<ExitCode, String> {
79    use std::os::unix::process::CommandExt;
80    // `exec` replaces this process and only returns on failure.
81    let err = Command::new(bin).args(args).exec();
82    Err(format!("exec {}: {err}", bin.display()))
83}
84
85#[cfg(not(unix))]
86fn exec(bin: &Path, args: &[String]) -> Result<ExitCode, String> {
87    let status = Command::new(bin)
88        .args(args)
89        .status()
90        .map_err(|e| format!("running {}: {e}", bin.display()))?;
91    Ok(ExitCode::from(status.code().unwrap_or(1) as u8))
92}
93
94fn home() -> Option<PathBuf> {
95    if let Ok(h) = std::env::var("VANTA_HOME") {
96        return Some(PathBuf::from(h));
97    }
98    std::env::var("HOME")
99        .or_else(|_| std::env::var("USERPROFILE"))
100        .ok()
101        .map(|base| PathBuf::from(base).join(".vanta"))
102}