Skip to main content

discovery/
ignore.rs

1pub struct IgnoreList {
2    rules: Vec<IgnoreRule>,
3}
4
5struct IgnoreRule {
6    standard: Option<String>,
7    pattern: String,
8}
9
10impl IgnoreList {
11    #[must_use]
12    pub fn new() -> Self {
13        Self { rules: Vec::new() }
14    }
15
16    pub fn parse(text: &str) -> Self {
17        let mut rules = Vec::new();
18        for line in text.lines() {
19            let trimmed = line.trim();
20            if trimmed.is_empty() || trimmed.starts_with('#') {
21                continue;
22            }
23            let parts: Vec<&str> = trimmed.splitn(2, char::is_whitespace).collect();
24            if parts.len() == 2 {
25                let standard = parts[0].trim();
26                let glob = parts[1].trim().to_ascii_lowercase();
27                if standard == "*" {
28                    rules.push(IgnoreRule {
29                        standard: None,
30                        pattern: glob,
31                    });
32                } else {
33                    rules.push(IgnoreRule {
34                        standard: Some(standard.to_ascii_lowercase()),
35                        pattern: glob,
36                    });
37                }
38            } else {
39                rules.push(IgnoreRule {
40                    standard: None,
41                    pattern: trimmed.to_ascii_lowercase(),
42                });
43            }
44        }
45        Self { rules }
46    }
47
48    #[must_use]
49    pub fn is_ignored(&self, hostname: &str, standard: Option<&str>) -> bool {
50        let host_lower = hostname.to_ascii_lowercase();
51        for rule in &self.rules {
52            if !glob_match(&rule.pattern, &host_lower) {
53                continue;
54            }
55            match (&rule.standard, standard) {
56                (None, _) => return true,
57                (Some(rs), Some(s)) if rs == &s.to_ascii_lowercase() => return true,
58                _ => {}
59            }
60        }
61        false
62    }
63}
64
65impl Default for IgnoreList {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71fn glob_match(pattern: &str, text: &str) -> bool {
72    let pat: Vec<char> = pattern.chars().collect();
73    let txt: Vec<char> = text.chars().collect();
74    glob_match_inner(&pat, &txt)
75}
76
77fn glob_match_inner(pat: &[char], txt: &[char]) -> bool {
78    let mut pi = 0;
79    let mut ti = 0;
80    let mut star_pi = usize::MAX;
81    let mut star_ti = 0;
82
83    while ti < txt.len() {
84        if pi < pat.len() && (pat[pi] == '?' || pat[pi] == txt[ti]) {
85            pi += 1;
86            ti += 1;
87        } else if pi < pat.len() && pat[pi] == '*' {
88            star_pi = pi;
89            star_ti = ti;
90            pi += 1;
91        } else if star_pi != usize::MAX {
92            pi = star_pi + 1;
93            star_ti += 1;
94            ti = star_ti;
95        } else {
96            return false;
97        }
98    }
99
100    while pi < pat.len() && pat[pi] == '*' {
101        pi += 1;
102    }
103
104    pi == pat.len()
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn empty_list_ignores_nothing() {
113        let list = IgnoreList::new();
114        assert!(!list.is_ignored("example.com", None));
115    }
116
117    #[test]
118    fn exact_hostname() {
119        let list = IgnoreList::parse("* example.com");
120        assert!(list.is_ignored("example.com", None));
121        assert!(!list.is_ignored("other.com", None));
122    }
123
124    #[test]
125    fn wildcard_subdomain() {
126        let list = IgnoreList::parse("* *.example.com");
127        assert!(list.is_ignored("sub.example.com", None));
128        assert!(!list.is_ignored("example.com", None));
129    }
130
131    #[test]
132    fn case_insensitive() {
133        let list = IgnoreList::parse("* Example.COM");
134        assert!(list.is_ignored("EXAMPLE.com", None));
135    }
136
137    #[test]
138    fn comments_and_blanks() {
139        let list = IgnoreList::parse("# a comment\n\n* example.com\n");
140        assert!(list.is_ignored("example.com", None));
141    }
142
143    #[test]
144    fn standard_specific_rule() {
145        let list = IgnoreList::parse("ard example.com");
146        assert!(list.is_ignored("example.com", Some("ard")));
147        assert!(!list.is_ignored("example.com", Some("other")));
148        assert!(!list.is_ignored("example.com", None));
149    }
150
151    #[test]
152    fn bare_hostname_ignores_all_standards() {
153        let list = IgnoreList::parse("example.com");
154        assert!(list.is_ignored("example.com", None));
155        assert!(list.is_ignored("example.com", Some("ard")));
156    }
157
158    #[test]
159    fn glob_match_star() {
160        assert!(glob_match("*", "anything"));
161        assert!(glob_match("*.com", "example.com"));
162        assert!(!glob_match("*.com", "example.org"));
163    }
164
165    #[test]
166    fn glob_match_question() {
167        assert!(glob_match("a?c", "abc"));
168        assert!(!glob_match("a?c", "abbc"));
169    }
170}