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