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