shell_sanitize_rules/glob.rs
1use shell_sanitize::{Rule, RuleResult, RuleViolation};
2
3use crate::charset::CharSet;
4
5/// Rejects input containing shell glob characters.
6///
7/// Default denied: `*`, `?`, `[`, `]`, `{`, `}`.
8///
9/// # Rationale
10///
11/// Glob characters cause unintended filename expansion in shell contexts:
12/// ```text
13/// rm *.log → deletes all .log files
14/// cat file[0-9] → expands to matching filenames
15/// echo {a,b,c} → brace expansion
16/// ```
17///
18/// Note: `{` and `}` overlap with [`ShellMetaRule`](crate::ShellMetaRule).
19/// When both rules are active, each reports its own violation independently.
20/// This is intentional — overlapping reports provide richer context about
21/// why an input was rejected.
22pub struct GlobRule {
23 denied: CharSet,
24}
25
26impl Default for GlobRule {
27 fn default() -> Self {
28 Self {
29 denied: CharSet::from_chars(&['*', '?', '[', ']', '{', '}']),
30 }
31 }
32}
33
34impl GlobRule {
35 /// Create a rule with a custom set of denied glob characters.
36 pub fn with_denied(denied: Vec<char>) -> Self {
37 Self {
38 denied: CharSet::from_chars(&denied),
39 }
40 }
41}
42
43impl Rule for GlobRule {
44 fn name(&self) -> &'static str {
45 "glob"
46 }
47
48 fn check(&self, input: &str) -> RuleResult {
49 let violations: Vec<_> = input
50 .char_indices()
51 .filter(|(_, c)| self.denied.contains(*c))
52 .map(|(i, c)| {
53 RuleViolation::new(self.name(), format!("glob character {:?} found", c))
54 .at(i)
55 .with_fragment(c.to_string())
56 })
57 .collect();
58
59 if violations.is_empty() {
60 Ok(())
61 } else {
62 Err(violations)
63 }
64 }
65}