1#[derive(Debug, Clone, PartialEq)]
3pub struct ParsedTarget {
4 pub user: String,
5 pub hostname: String,
6 pub port: u16,
7}
8
9pub fn parse_target(target: &str) -> Result<ParsedTarget, String> {
11 if target.is_empty() {
12 return Err("Target can't be empty.".to_string());
13 }
14
15 let (user, rest) = if let Some(at_pos) = target.find('@') {
16 let user = &target[..at_pos];
17 if user.is_empty() {
18 return Err("User part before @ can't be empty.".to_string());
19 }
20 (user.to_string(), &target[at_pos + 1..])
21 } else {
22 (String::new(), target)
23 };
24
25 let (hostname, port) = if rest.starts_with('[') {
26 if let Some(bracket_end) = rest.find(']') {
28 let host = &rest[1..bracket_end];
29 let after = &rest[bracket_end + 1..];
30 if let Some(port_str) = after.strip_prefix(':') {
31 if let Ok(port) = port_str.parse::<u16>() {
32 if port == 0 {
33 return Err("Port 0? Bold choice, but no. Try 1-65535.".to_string());
34 }
35 (host.to_string(), port)
36 } else {
37 return Err("Invalid port after bracketed host.".to_string());
38 }
39 } else if after.is_empty() {
40 (host.to_string(), 22)
41 } else {
42 return Err("Unexpected text after closing bracket.".to_string());
43 }
44 } else {
45 return Err("Missing closing bracket for IPv6 address.".to_string());
46 }
47 } else if let Some(colon_pos) = rest.rfind(':') {
48 let port_str = &rest[colon_pos + 1..];
49 let host_part = &rest[..colon_pos];
50 if !host_part.contains(':') {
52 if port_str.is_empty() {
53 return Err("Trailing colon with no port. Try host:22 or just host.".to_string());
54 }
55 if let Ok(port) = port_str.parse::<u16>() {
56 if port == 0 {
57 return Err("Port 0? Bold choice, but no. Try 1-65535.".to_string());
58 }
59 (host_part.to_string(), port)
60 } else {
61 return Err(format!(
62 "'{}' is not a valid port number. Ports are 1-65535.",
63 port_str
64 ));
65 }
66 } else {
67 (rest.to_string(), 22)
69 }
70 } else {
71 (rest.to_string(), 22)
72 };
73
74 if hostname.is_empty() {
75 return Err("Hostname can't be empty.".to_string());
76 }
77
78 if hostname.chars().any(|c| c.is_control() || c == ' ') {
79 return Err("Hostname contains invalid characters.".to_string());
80 }
81 if !user.is_empty() && user.chars().any(|c| c.is_control() || c == ' ') {
82 return Err("User contains invalid characters.".to_string());
83 }
84
85 Ok(ParsedTarget {
86 user,
87 hostname,
88 port,
89 })
90}
91
92pub fn looks_like_target(s: &str) -> bool {
94 if s.contains('@') {
95 return true;
96 }
97 if s.starts_with('[') {
99 return true;
100 }
101 if let Some(colon_pos) = s.rfind(':') {
102 let before = &s[..colon_pos];
103 let after = &s[colon_pos + 1..];
104 return !before.contains(':')
106 && !after.is_empty()
107 && after.chars().all(|c| c.is_ascii_digit());
108 }
109 false
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn test_full_target() {
118 let result = parse_target("admin@example.com:2222").unwrap();
119 assert_eq!(result.user, "admin");
120 assert_eq!(result.hostname, "example.com");
121 assert_eq!(result.port, 2222);
122 }
123
124 #[test]
125 fn test_user_and_host() {
126 let result = parse_target("root@10.0.0.1").unwrap();
127 assert_eq!(result.user, "root");
128 assert_eq!(result.hostname, "10.0.0.1");
129 assert_eq!(result.port, 22);
130 }
131
132 #[test]
133 fn test_host_and_port() {
134 let result = parse_target("example.com:8022").unwrap();
135 assert_eq!(result.user, "");
136 assert_eq!(result.hostname, "example.com");
137 assert_eq!(result.port, 8022);
138 }
139
140 #[test]
141 fn test_host_only() {
142 let result = parse_target("example.com").unwrap();
143 assert_eq!(result.user, "");
144 assert_eq!(result.hostname, "example.com");
145 assert_eq!(result.port, 22);
146 }
147
148 #[test]
149 fn test_empty_target() {
150 assert!(parse_target("").is_err());
151 }
152
153 #[test]
154 fn test_empty_user() {
155 assert!(parse_target("@example.com").is_err());
156 }
157
158 #[test]
159 fn test_empty_hostname() {
160 assert!(parse_target("user@").is_err());
161 }
162
163 #[test]
164 fn test_port_zero() {
165 assert!(parse_target("example.com:0").is_err());
166 }
167
168 #[test]
169 fn test_invalid_port_text() {
170 assert!(parse_target("example.com:abc").is_err());
171 }
172
173 #[test]
174 fn test_trailing_colon() {
175 assert!(parse_target("example.com:").is_err());
176 }
177
178 #[test]
179 fn test_port_overflow() {
180 assert!(parse_target("example.com:99999").is_err());
181 }
182
183 #[test]
184 fn test_looks_like_target_with_at() {
185 assert!(looks_like_target("user@host"));
186 }
187
188 #[test]
189 fn test_looks_like_target_with_port() {
190 assert!(looks_like_target("host:22"));
191 }
192
193 #[test]
194 fn test_looks_like_target_plain_host() {
195 assert!(!looks_like_target("myserver"));
196 }
197
198 #[test]
199 fn test_bare_ipv6() {
200 let result = parse_target("2001:db8::1").unwrap();
201 assert_eq!(result.hostname, "2001:db8::1");
202 assert_eq!(result.port, 22);
203 }
204
205 #[test]
206 fn test_bracketed_ipv6_with_port() {
207 let result = parse_target("[2001:db8::1]:2222").unwrap();
208 assert_eq!(result.hostname, "2001:db8::1");
209 assert_eq!(result.port, 2222);
210 }
211
212 #[test]
213 fn test_bracketed_ipv6_no_port() {
214 let result = parse_target("[::1]").unwrap();
215 assert_eq!(result.hostname, "::1");
216 assert_eq!(result.port, 22);
217 }
218
219 #[test]
220 fn test_user_at_ipv6() {
221 let result = parse_target("root@2001:db8::1").unwrap();
222 assert_eq!(result.user, "root");
223 assert_eq!(result.hostname, "2001:db8::1");
224 assert_eq!(result.port, 22);
225 }
226
227 #[test]
228 fn test_looks_like_target_bare_ipv6() {
229 assert!(!looks_like_target("2001:db8::1"));
231 }
232
233 #[test]
234 fn test_looks_like_target_bracketed_ipv6() {
235 assert!(looks_like_target("[::1]:22"));
236 }
237
238 #[test]
239 fn test_hostname_with_space() {
240 assert!(parse_target("bad host").is_err());
241 }
242
243 #[test]
244 fn test_hostname_with_control_char() {
245 assert!(parse_target("bad\x00host").is_err());
246 assert!(parse_target("bad\nhost").is_err());
247 }
248
249 #[test]
250 fn test_user_with_space() {
251 assert!(parse_target("bad user@host").is_err());
252 }
253
254 #[test]
255 fn test_user_with_control_char() {
256 assert!(parse_target("bad\x01user@host").is_err());
257 }
258}