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}