#[derive(Debug, Clone, PartialEq)]
pub struct ParsedTarget {
pub user: String,
pub hostname: String,
pub port: u16,
}
pub fn parse_target(target: &str) -> Result<ParsedTarget, String> {
if target.is_empty() {
return Err("Target can't be empty.".to_string());
}
let (user, rest) = if let Some(at_pos) = target.find('@') {
let user = &target[..at_pos];
if user.is_empty() {
return Err("User part before @ can't be empty.".to_string());
}
(user.to_string(), &target[at_pos + 1..])
} else {
(String::new(), target)
};
let (hostname, port) = if rest.starts_with('[') {
if let Some(bracket_end) = rest.find(']') {
let host = &rest[1..bracket_end];
let after = &rest[bracket_end + 1..];
if let Some(port_str) = after.strip_prefix(':') {
if let Ok(port) = port_str.parse::<u16>() {
if port == 0 {
return Err("Port 0? Bold choice, but no. Try 1-65535.".to_string());
}
(host.to_string(), port)
} else {
return Err("Invalid port after bracketed host.".to_string());
}
} else if after.is_empty() {
(host.to_string(), 22)
} else {
return Err("Unexpected text after closing bracket.".to_string());
}
} else {
return Err("Missing closing bracket for IPv6 address.".to_string());
}
} else if let Some(colon_pos) = rest.rfind(':') {
let port_str = &rest[colon_pos + 1..];
let host_part = &rest[..colon_pos];
if !host_part.contains(':') {
if port_str.is_empty() {
return Err("Trailing colon with no port. Try host:22 or just host.".to_string());
}
if let Ok(port) = port_str.parse::<u16>() {
if port == 0 {
return Err("Port 0? Bold choice, but no. Try 1-65535.".to_string());
}
(host_part.to_string(), port)
} else {
return Err(format!("'{}' is not a valid port number. Ports are 1-65535.", port_str));
}
} else {
(rest.to_string(), 22)
}
} else {
(rest.to_string(), 22)
};
if hostname.is_empty() {
return Err("Hostname can't be empty.".to_string());
}
if hostname.chars().any(|c| c.is_control() || c == ' ') {
return Err("Hostname contains invalid characters.".to_string());
}
if !user.is_empty() && user.chars().any(|c| c.is_control() || c == ' ') {
return Err("User contains invalid characters.".to_string());
}
Ok(ParsedTarget {
user,
hostname,
port,
})
}
pub fn looks_like_target(s: &str) -> bool {
if s.contains('@') {
return true;
}
if s.starts_with('[') {
return true;
}
if let Some(colon_pos) = s.rfind(':') {
let before = &s[..colon_pos];
let after = &s[colon_pos + 1..];
return !before.contains(':')
&& !after.is_empty()
&& after.chars().all(|c| c.is_ascii_digit());
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_full_target() {
let result = parse_target("admin@example.com:2222").unwrap();
assert_eq!(result.user, "admin");
assert_eq!(result.hostname, "example.com");
assert_eq!(result.port, 2222);
}
#[test]
fn test_user_and_host() {
let result = parse_target("root@10.0.0.1").unwrap();
assert_eq!(result.user, "root");
assert_eq!(result.hostname, "10.0.0.1");
assert_eq!(result.port, 22);
}
#[test]
fn test_host_and_port() {
let result = parse_target("example.com:8022").unwrap();
assert_eq!(result.user, "");
assert_eq!(result.hostname, "example.com");
assert_eq!(result.port, 8022);
}
#[test]
fn test_host_only() {
let result = parse_target("example.com").unwrap();
assert_eq!(result.user, "");
assert_eq!(result.hostname, "example.com");
assert_eq!(result.port, 22);
}
#[test]
fn test_empty_target() {
assert!(parse_target("").is_err());
}
#[test]
fn test_empty_user() {
assert!(parse_target("@example.com").is_err());
}
#[test]
fn test_empty_hostname() {
assert!(parse_target("user@").is_err());
}
#[test]
fn test_port_zero() {
assert!(parse_target("example.com:0").is_err());
}
#[test]
fn test_invalid_port_text() {
assert!(parse_target("example.com:abc").is_err());
}
#[test]
fn test_trailing_colon() {
assert!(parse_target("example.com:").is_err());
}
#[test]
fn test_port_overflow() {
assert!(parse_target("example.com:99999").is_err());
}
#[test]
fn test_looks_like_target_with_at() {
assert!(looks_like_target("user@host"));
}
#[test]
fn test_looks_like_target_with_port() {
assert!(looks_like_target("host:22"));
}
#[test]
fn test_looks_like_target_plain_host() {
assert!(!looks_like_target("myserver"));
}
#[test]
fn test_bare_ipv6() {
let result = parse_target("2001:db8::1").unwrap();
assert_eq!(result.hostname, "2001:db8::1");
assert_eq!(result.port, 22);
}
#[test]
fn test_bracketed_ipv6_with_port() {
let result = parse_target("[2001:db8::1]:2222").unwrap();
assert_eq!(result.hostname, "2001:db8::1");
assert_eq!(result.port, 2222);
}
#[test]
fn test_bracketed_ipv6_no_port() {
let result = parse_target("[::1]").unwrap();
assert_eq!(result.hostname, "::1");
assert_eq!(result.port, 22);
}
#[test]
fn test_user_at_ipv6() {
let result = parse_target("root@2001:db8::1").unwrap();
assert_eq!(result.user, "root");
assert_eq!(result.hostname, "2001:db8::1");
assert_eq!(result.port, 22);
}
#[test]
fn test_looks_like_target_bare_ipv6() {
assert!(!looks_like_target("2001:db8::1"));
}
#[test]
fn test_looks_like_target_bracketed_ipv6() {
assert!(looks_like_target("[::1]:22"));
}
#[test]
fn test_hostname_with_space() {
assert!(parse_target("bad host").is_err());
}
#[test]
fn test_hostname_with_control_char() {
assert!(parse_target("bad\x00host").is_err());
assert!(parse_target("bad\nhost").is_err());
}
#[test]
fn test_user_with_space() {
assert!(parse_target("bad user@host").is_err());
}
#[test]
fn test_user_with_control_char() {
assert!(parse_target("bad\x01user@host").is_err());
}
}