Skip to main content

openmoat_core/engine/
context.rs

1//! What an [`EvalContext`] checks and expands: `moved_dirs` validation and the
2//! spellings of one rule pattern (moved state and host directories, real paths).
3
4use super::EvalContext;
5use crate::paths;
6use crate::policy::PolicyError;
7
8impl EvalContext {
9    /// Reject [`Self::moved_dirs`] entries whose `default` could alias every
10    /// pattern body (empty-after-trim or a bare `/`) or does not look like a
11    /// directory (must start with `/` or `~/`). The engine fails closed on such
12    /// a context: without validation an empty `default` makes [`Self::spellings`]
13    /// emit a spelling under `moved` for every pattern (#399), so a `moved_dirs`
14    /// mistake in the caller would silently widen every rule. The CLI legitimately
15    /// lists the same `default` twice when the moved directory has a resolved
16    /// spelling or comes from both the environment and the recorded host dir,
17    /// so a duplicate `default` is kept (its spellings deduped in [`Self::spellings`]).
18    pub(crate) fn validate_moved_dirs(&self) -> Result<(), PolicyError> {
19        for (default, _) in &self.moved_dirs {
20            let trimmed = default.trim();
21            let valid = !trimmed.is_empty()
22                && trimmed != "/"
23                && (trimmed == "~" || trimmed.starts_with("~/") || paths::is_absolute(trimmed));
24            if !valid {
25                return Err(PolicyError::Rule {
26                    rule: "context.moved_dirs".to_owned(),
27                    problem: format!(
28                        "default directory `{default}` must be absolute (`/…`) or home-rooted (`~/…`), \
29                         not empty, whitespace or a bare `/`"
30                    ),
31                });
32            }
33        }
34        Ok(())
35    }
36
37    /// `raw` expanded once per spelling of the home directory and project root
38    /// it names (`paths::expand_pattern`), and once more per directory in
39    /// [`Self::moved_dirs`] it falls under; empty when it names no location.
40    /// The policy compiler (`ir`) lowers patterns through the same expansion,
41    /// and `openmoat run` finds where a policy pattern points with it.
42    #[must_use]
43    pub fn spellings(&self, raw: &str) -> Vec<String> {
44        let (negated, body) = crate::pattern::split_negation(raw);
45        let bang = if negated { "!" } else { "" };
46        let moved = self.moved_dirs.iter().filter_map(|(default, dir)| {
47            let rest = body.strip_prefix(default.as_str())?;
48            (rest.is_empty() || rest.starts_with('/')).then(|| format!("{bang}{dir}{rest}"))
49        });
50        let raws: Vec<String> = std::iter::once(raw.to_owned()).chain(moved).collect();
51        let projects: Vec<Option<&str>> = match &self.project {
52            Some(p) => [Some(p), self.real_project.as_ref()]
53                .into_iter()
54                .flatten()
55                .map(|p| Some(p.as_str()))
56                .collect(),
57            None => vec![None],
58        };
59        let mut out = Vec::new();
60        for home in [Some(&self.home), self.real_home.as_ref()]
61            .into_iter()
62            .flatten()
63        {
64            for project in &projects {
65                out.extend(
66                    raws.iter()
67                        .filter_map(|raw| paths::expand_pattern(raw, home, *project)),
68                );
69            }
70        }
71        out.sort();
72        out.dedup();
73        out
74    }
75}