Skip to main content

evalbox_sandbox/
resolve.rs

1//! Binary path resolution and mount detection.
2
3use std::path::{Path, PathBuf};
4
5use thiserror::Error;
6
7use crate::plan::Mount;
8use crate::sysinfo::{SYSTEM_PATHS, SystemPaths, SystemType};
9
10#[derive(Debug, Clone)]
11pub struct ResolvedBinary {
12    pub path: PathBuf,
13    pub required_mounts: Vec<Mount>,
14}
15
16#[derive(Debug, Error)]
17pub enum ResolveError {
18    #[error("command not found: {0}")]
19    NotFound(String),
20}
21
22/// Resolve command to absolute path and detect required mounts.
23pub fn resolve_binary(cmd: &str) -> Result<ResolvedBinary, ResolveError> {
24    let path = if cmd.starts_with('/') {
25        let p = PathBuf::from(cmd);
26        if !p.exists() && !cmd.starts_with("/work/") {
27            return Err(ResolveError::NotFound(cmd.to_string()));
28        }
29        p
30    } else {
31        which::which(cmd).map_err(|_| ResolveError::NotFound(cmd.to_string()))?
32    };
33
34    let sys_paths = &*SYSTEM_PATHS;
35    let required_mounts = detect_mounts(&path, sys_paths);
36
37    Ok(ResolvedBinary {
38        path,
39        required_mounts,
40    })
41}
42
43fn detect_mounts(binary: &Path, sys_paths: &SystemPaths) -> Vec<Mount> {
44    let path_str = binary.to_string_lossy();
45    let mut mounts = Vec::new();
46
47    for mount_path in &sys_paths.readonly_mounts {
48        mounts.push(Mount::ro(mount_path));
49    }
50
51    if sys_paths.system_type == SystemType::Fhs {
52        if path_str.starts_with("/usr") {
53            add_if_missing(&mut mounts, "/usr");
54        } else if path_str.starts_with("/bin") || path_str.starts_with("/sbin") {
55            if Path::new("/bin").is_symlink() {
56                add_if_missing(&mut mounts, "/usr");
57            } else {
58                add_if_missing(&mut mounts, "/bin");
59            }
60        }
61    }
62
63    mounts
64}
65
66fn add_if_missing(mounts: &mut Vec<Mount>, path: &str) {
67    let path_buf = PathBuf::from(path);
68    if !mounts.iter().any(|m| m.source == path_buf) && path_buf.exists() {
69        mounts.push(Mount::ro(path_buf));
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn resolve_echo() {
79        let resolved = resolve_binary("echo").unwrap();
80        assert!(resolved.path.exists());
81        assert!(resolved.path.is_absolute());
82    }
83
84    #[test]
85    fn resolve_nonexistent() {
86        assert!(resolve_binary("nonexistent_binary_12345").is_err());
87    }
88
89    #[test]
90    fn detect_nix_mounts() {
91        let sys_paths = &*SYSTEM_PATHS;
92        let mounts = detect_mounts(Path::new("/nix/store/abc123/bin/echo"), sys_paths);
93
94        if sys_paths.system_type == SystemType::NixOS {
95            assert!(mounts.iter().any(|m| m.source == Path::new("/nix/store")));
96        }
97    }
98
99    #[test]
100    fn detect_fhs_mounts() {
101        let sys_paths = &*SYSTEM_PATHS;
102        let mounts = detect_mounts(Path::new("/usr/bin/echo"), sys_paths);
103
104        // Only check for /usr mount if we're on an actual FHS system with /usr
105        if sys_paths.system_type == SystemType::Fhs && Path::new("/usr").exists() {
106            assert!(mounts.iter().any(|m| m.source == Path::new("/usr")));
107        }
108    }
109
110    #[test]
111    fn resolve_has_system_mounts() {
112        let resolved = resolve_binary("sh").unwrap();
113        assert!(!resolved.required_mounts.is_empty());
114    }
115}