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