Skip to main content

soma_infra/
host_exec_policy.rs

1use std::path::{Path, PathBuf};
2
3#[cfg(any(feature = "process-driver", test))]
4use crate::HostExecRequest;
5use crate::{FileReadPolicy, InfraError, InfraResult};
6
7const MAX_EXEC_ROOTS: usize = 32;
8
9/// Explicit read roots used by the typed host command launcher.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct HostExecPolicy {
12    files: FileReadPolicy,
13}
14
15impl HostExecPolicy {
16    /// Creates a host command policy with one to thirty-two absolute roots.
17    pub fn new<I, P>(roots: I) -> InfraResult<Self>
18    where
19        I: IntoIterator<Item = P>,
20        P: Into<PathBuf>,
21    {
22        let roots = roots.into_iter().map(Into::into).collect::<Vec<_>>();
23        if roots.is_empty() || roots.len() > MAX_EXEC_ROOTS {
24            return Err(invalid(format!(
25                "host execution requires 1-{MAX_EXEC_ROOTS} read roots"
26            )));
27        }
28        Ok(Self {
29            files: FileReadPolicy::new(roots)?,
30        })
31    }
32
33    /// Returns roots in deterministic order.
34    pub fn roots(&self) -> impl Iterator<Item = &Path> {
35        self.files.roots()
36    }
37
38    #[cfg(any(feature = "process-driver", test))]
39    pub(crate) fn launcher_plan(&self, request: &HostExecRequest) -> InfraResult<LauncherPlan> {
40        let path_indices =
41            crate::host_exec_argv::filesystem_operand_indices(request.command(), request.args())?;
42        for index in &path_indices {
43            let path = validate_operand_path(&request.args()[*index])?;
44            self.files.resolve(path)?;
45        }
46        if let Some(path) = request.working_dir() {
47            self.files.resolve(path)?;
48        }
49        let roots = self
50            .files
51            .roots()
52            .map(|root| root.to_string_lossy().into_owned())
53            .collect();
54        Ok(LauncherPlan {
55            path_indices,
56            roots,
57            working_dir: request
58                .working_dir()
59                .map(|path| path.to_string_lossy().into_owned()),
60        })
61    }
62}
63
64#[cfg(any(feature = "process-driver", test))]
65pub(crate) struct LauncherPlan {
66    pub(crate) path_indices: Vec<usize>,
67    pub(crate) roots: Vec<String>,
68    pub(crate) working_dir: Option<String>,
69}
70
71#[cfg(any(feature = "process-driver", test))]
72fn validate_operand_path(value: &str) -> InfraResult<&Path> {
73    let path = Path::new(value);
74    if !path.is_absolute()
75        || path.components().any(|part| {
76            matches!(
77                part,
78                std::path::Component::ParentDir | std::path::Component::CurDir
79            )
80        })
81        || value.chars().any(char::is_control)
82    {
83        Err(invalid(format!(
84            "filesystem command operands must be absolute and normalized: {value:?}"
85        )))
86    } else {
87        Ok(path)
88    }
89}
90
91fn invalid(message: impl Into<String>) -> InfraError {
92    InfraError::InvalidRequest {
93        domain: "host-exec",
94        message: message.into(),
95    }
96}
97
98#[cfg(test)]
99#[path = "host_exec_policy_tests.rs"]
100mod tests;