1use std::fmt;
6
7use wildmatch::WildMatch;
8
9use super::HostParams;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Host {
14 pub pattern: Vec<HostClause>,
16 pub params: HostParams,
17}
18
19impl Host {
20 pub fn new(pattern: Vec<HostClause>, params: HostParams) -> Self {
21 Self { pattern, params }
22 }
23
24 pub fn intersects(&self, host: &str) -> bool {
26 clauses_intersect(&self.pattern, host)
27 }
28}
29
30pub(crate) fn clauses_intersect(clauses: &[HostClause], host: &str) -> bool {
32 let mut has_matched = false;
33 for entry in clauses {
34 let matches = entry.intersects(host);
35 if matches && entry.negated {
37 return false;
38 }
39 has_matched |= matches;
40 }
41 has_matched
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct HostClause {
47 pub pattern: String,
48 pub negated: bool,
49}
50
51impl fmt::Display for HostClause {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 if self.negated {
54 write!(f, "!{}", self.pattern)
55 } else {
56 write!(f, "{}", self.pattern)
57 }
58 }
59}
60
61impl HostClause {
62 pub fn new(pattern: String, negated: bool) -> Self {
64 Self { pattern, negated }
65 }
66
67 pub fn intersects(&self, host: &str) -> bool {
69 WildMatch::new(self.pattern.as_str()).matches(host)
70 }
71}
72
73#[cfg(test)]
74mod tests {
75
76 use pretty_assertions::assert_eq;
77
78 use super::*;
79 use crate::DefaultAlgorithms;
80
81 #[test]
82 fn should_build_host_clause() {
83 let clause = HostClause::new("192.168.1.1".to_string(), false);
84 assert_eq!(clause.pattern.as_str(), "192.168.1.1");
85 assert_eq!(clause.negated, false);
86 }
87
88 #[test]
89 fn should_intersect_host_clause() {
90 let clause = HostClause::new("192.168.*.*".to_string(), false);
91 assert!(clause.intersects("192.168.2.30"));
92 let clause = HostClause::new("192.168.?0.*".to_string(), false);
93 assert!(clause.intersects("192.168.40.28"));
94 }
95
96 #[test]
97 fn should_not_intersect_host_clause() {
98 let clause = HostClause::new("192.168.*.*".to_string(), false);
99 assert_eq!(clause.intersects("172.26.104.4"), false);
100 }
101
102 #[test]
103 fn should_init_host() {
104 let host = Host::new(
105 vec![HostClause::new("192.168.*.*".to_string(), false)],
106 HostParams::new(&DefaultAlgorithms::default()),
107 );
108 assert_eq!(host.pattern.len(), 1);
109 }
110
111 #[test]
112 fn should_intersect_clause() {
113 let host = Host::new(
114 vec![
115 HostClause::new("192.168.*.*".to_string(), false),
116 HostClause::new("172.26.*.*".to_string(), false),
117 HostClause::new("10.8.*.*".to_string(), false),
118 HostClause::new("10.8.0.8".to_string(), true),
119 ],
120 HostParams::new(&DefaultAlgorithms::default()),
121 );
122 assert!(host.intersects("192.168.1.32"));
123 assert!(host.intersects("172.26.104.4"));
124 assert!(host.intersects("10.8.0.10"));
125 }
126
127 #[test]
128 fn should_not_intersect_clause() {
129 let host = Host::new(
130 vec![
131 HostClause::new("192.168.*.*".to_string(), false),
132 HostClause::new("172.26.*.*".to_string(), false),
133 HostClause::new("10.8.*.*".to_string(), false),
134 HostClause::new("10.8.0.8".to_string(), true),
135 ],
136 HostParams::new(&DefaultAlgorithms::default()),
137 );
138 assert_eq!(host.intersects("192.169.1.32"), false);
139 assert_eq!(host.intersects("172.28.104.4"), false);
140 assert_eq!(host.intersects("10.9.0.8"), false);
141 assert_eq!(host.intersects("10.8.0.8"), false);
142 }
143
144 #[test]
145 fn should_display_host_clause() {
146 let clause = HostClause::new("192.168.*.*".to_string(), false);
147 assert_eq!(clause.to_string(), "192.168.*.*");
148
149 let negated_clause = HostClause::new("192.168.1.1".to_string(), true);
150 assert_eq!(negated_clause.to_string(), "!192.168.1.1");
151 }
152
153 #[test]
154 fn should_not_intersect_with_empty_pattern() {
155 let host = Host::new(vec![], HostParams::new(&DefaultAlgorithms::default()));
156 assert_eq!(host.intersects("any-host"), false);
157 }
158
159 #[test]
160 fn should_intersect_with_single_char_wildcard() {
161 let clause = HostClause::new("server?".to_string(), false);
162 assert!(clause.intersects("server1"));
163 assert!(clause.intersects("serverA"));
164 assert!(!clause.intersects("server12"));
165 assert!(!clause.intersects("server"));
166 }
167
168 #[test]
169 fn should_intersect_with_only_negated_clauses_after_positive() {
170 let host = Host::new(
172 vec![
173 HostClause::new("*.example.com".to_string(), false),
174 HostClause::new("secret.example.com".to_string(), true),
175 ],
176 HostParams::new(&DefaultAlgorithms::default()),
177 );
178 assert!(host.intersects("www.example.com"));
179 assert!(!host.intersects("secret.example.com"));
180 assert!(!host.intersects("other.net"));
181 }
182
183 #[test]
184 fn should_handle_wildcard_at_start() {
185 let clause = HostClause::new("*-server".to_string(), false);
186 assert!(clause.intersects("prod-server"));
187 assert!(clause.intersects("dev-server"));
188 assert!(!clause.intersects("server-prod"));
189 }
190
191 #[test]
192 fn should_handle_wildcard_at_end() {
193 let clause = HostClause::new("server-*".to_string(), false);
194 assert!(clause.intersects("server-prod"));
195 assert!(clause.intersects("server-dev"));
196 assert!(!clause.intersects("prod-server"));
197 }
198
199 #[test]
200 fn should_match_exact_pattern() {
201 let clause = HostClause::new("exact-host".to_string(), false);
202 assert!(clause.intersects("exact-host"));
203 assert!(!clause.intersects("exact-host-extra"));
204 assert!(!clause.intersects("prefix-exact-host"));
205 }
206
207 #[test]
208 fn should_match_universal_wildcard() {
209 let clause = HostClause::new("*".to_string(), false);
210 assert!(clause.intersects("any-host"));
211 assert!(clause.intersects("192.168.1.1"));
212 assert!(clause.intersects(""));
213 }
214
215 #[test]
216 fn should_intersect_negated_clause_returns_true_for_matching_negated() {
217 let clause = HostClause::new("192.168.*.*".to_string(), true);
219 assert!(clause.intersects("192.168.1.1")); }
221}