1#[derive(Debug, Clone, PartialEq)]
11pub struct Query {
12 pub user: Option<String>,
14 pub hosts: Vec<String>,
17 pub long: bool,
19 pub port: u16,
21}
22
23pub const DEFAULT_PORT: u16 = 79;
25
26#[derive(Debug, Clone, PartialEq, thiserror::Error)]
28pub enum QueryError {
29 #[error("invalid query: empty hostname in '{input}'")]
31 EmptyHostname { input: String },
32}
33
34impl Query {
35 pub fn parse(input: Option<&str>, long: bool, port: u16) -> Result<Query, QueryError> {
53 let input = input.unwrap_or("");
54
55 if input.is_empty() {
56 return Ok(Query {
57 user: None,
58 hosts: vec!["localhost".to_string()],
59 long,
60 port,
61 });
62 }
63
64 let parts: Vec<&str> = input.splitn(2, '@').collect();
66
67 if parts.len() == 1 {
68 return Ok(Query {
70 user: Some(parts[0].to_string()),
71 hosts: vec!["localhost".to_string()],
72 long,
73 port,
74 });
75 }
76
77 let user = if parts[0].is_empty() {
79 None
80 } else {
81 Some(parts[0].to_string())
82 };
83
84 let hosts: Vec<String> = parts[1].split('@').map(|s| s.to_string()).collect();
85
86 if hosts.iter().any(|h| h.is_empty()) {
88 return Err(QueryError::EmptyHostname {
89 input: input.to_string(),
90 });
91 }
92
93 Ok(Query {
94 user,
95 hosts,
96 long,
97 port,
98 })
99 }
100
101 pub fn target_host(&self) -> &str {
103 self.hosts.last().expect("hosts must not be empty")
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn parse_user_at_host() {
113 let q = Query::parse(Some("user@host"), false, 79).unwrap();
114 assert_eq!(q.user, Some("user".to_string()));
115 assert_eq!(q.hosts, vec!["host".to_string()]);
116 assert!(!q.long);
117 assert_eq!(q.port, 79);
118 }
119
120 #[test]
121 fn parse_at_host_lists_users() {
122 let q = Query::parse(Some("@host"), false, 79).unwrap();
123 assert_eq!(q.user, None);
124 assert_eq!(q.hosts, vec!["host".to_string()]);
125 }
126
127 #[test]
128 fn parse_user_only_defaults_to_localhost() {
129 let q = Query::parse(Some("user"), false, 79).unwrap();
130 assert_eq!(q.user, Some("user".to_string()));
131 assert_eq!(q.hosts, vec!["localhost".to_string()]);
132 }
133
134 #[test]
135 fn parse_empty_string_defaults_to_localhost() {
136 let q = Query::parse(Some(""), false, 79).unwrap();
137 assert_eq!(q.user, None);
138 assert_eq!(q.hosts, vec!["localhost".to_string()]);
139 }
140
141 #[test]
142 fn parse_none_defaults_to_localhost() {
143 let q = Query::parse(None, false, 79).unwrap();
144 assert_eq!(q.user, None);
145 assert_eq!(q.hosts, vec!["localhost".to_string()]);
146 }
147
148 #[test]
149 fn parse_forwarding_chain() {
150 let q = Query::parse(Some("user@host1@host2"), false, 79).unwrap();
151 assert_eq!(q.user, Some("user".to_string()));
152 assert_eq!(q.hosts, vec!["host1".to_string(), "host2".to_string()]);
153 }
154
155 #[test]
156 fn parse_forwarding_chain_no_user() {
157 let q = Query::parse(Some("@host1@host2"), false, 79).unwrap();
158 assert_eq!(q.user, None);
159 assert_eq!(q.hosts, vec!["host1".to_string(), "host2".to_string()]);
160 }
161
162 #[test]
163 fn parse_long_flag_preserved() {
164 let q = Query::parse(Some("user@host"), true, 79).unwrap();
165 assert!(q.long);
166 }
167
168 #[test]
169 fn parse_custom_port_preserved() {
170 let q = Query::parse(Some("user@host"), false, 7979).unwrap();
171 assert_eq!(q.port, 7979);
172 }
173
174 #[test]
175 fn target_host_returns_last_host() {
176 let q = Query::parse(Some("user@host1@host2"), false, 79).unwrap();
177 assert_eq!(q.target_host(), "host2");
178 }
179
180 #[test]
181 fn target_host_single_host() {
182 let q = Query::parse(Some("user@host"), false, 79).unwrap();
183 assert_eq!(q.target_host(), "host");
184 }
185
186 #[test]
187 fn parse_three_host_chain() {
188 let q = Query::parse(Some("user@a@b@c"), false, 79).unwrap();
189 assert_eq!(q.user, Some("user".to_string()));
190 assert_eq!(
191 q.hosts,
192 vec!["a".to_string(), "b".to_string(), "c".to_string()]
193 );
194 assert_eq!(q.target_host(), "c");
195 }
196
197 #[test]
198 fn parse_trailing_at_is_error() {
199 let result = Query::parse(Some("user@"), false, 79);
200 assert_eq!(
201 result,
202 Err(QueryError::EmptyHostname {
203 input: "user@".to_string()
204 })
205 );
206 }
207
208 #[test]
209 fn parse_bare_at_is_error() {
210 let result = Query::parse(Some("@"), false, 79);
211 assert_eq!(
212 result,
213 Err(QueryError::EmptyHostname {
214 input: "@".to_string()
215 })
216 );
217 }
218
219 #[test]
220 fn parse_trailing_at_in_chain_is_error() {
221 let result = Query::parse(Some("user@host@"), false, 79);
222 assert_eq!(
223 result,
224 Err(QueryError::EmptyHostname {
225 input: "user@host@".to_string()
226 })
227 );
228 }
229
230 #[test]
231 fn parse_double_at_is_error() {
232 let result = Query::parse(Some("user@@host"), false, 79);
233 assert_eq!(
234 result,
235 Err(QueryError::EmptyHostname {
236 input: "user@@host".to_string()
237 })
238 );
239 }
240
241 #[test]
242 fn parse_at_host_trailing_at_is_error() {
243 let result = Query::parse(Some("@host@"), false, 79);
244 assert_eq!(
245 result,
246 Err(QueryError::EmptyHostname {
247 input: "@host@".to_string()
248 })
249 );
250 }
251}