Skip to main content

ssh2_config/
host.rs

1//! # host
2//!
3//! Ssh host type
4
5use std::fmt;
6
7use wildmatch::WildMatch;
8
9use super::HostParams;
10
11/// Describes the rules to be used for a certain host
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Host {
14    /// List of hosts for which params are valid. String is string pattern, bool is whether condition is negated
15    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    /// Returns whether `host` argument intersects the host clauses
25    pub fn intersects(&self, host: &str) -> bool {
26        clauses_intersect(&self.pattern, host)
27    }
28}
29
30/// Returns whether a host name intersects a list of host clauses.
31pub(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 the entry is negated and it matches we can stop searching
36        if matches && entry.negated {
37            return false;
38        }
39        has_matched |= matches;
40    }
41    has_matched
42}
43
44/// Describes a single clause to match host
45#[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    /// Creates a new `HostClause` from arguments
63    pub fn new(pattern: String, negated: bool) -> Self {
64        Self { pattern, negated }
65    }
66
67    /// Returns whether `host` argument intersects the clause
68    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        // A host with positive and negated clauses where negated comes last
171        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        // Test that a negated clause still "intersects" (matches the pattern)
218        let clause = HostClause::new("192.168.*.*".to_string(), true);
219        assert!(clause.intersects("192.168.1.1")); // intersects returns true for pattern match
220    }
221}