par_term_ssh/
known_hosts.rs1use super::types::{SshHost, SshHostSource};
7use std::collections::HashSet;
8use std::path::Path;
9
10pub fn parse_known_hosts(path: &Path) -> Vec<SshHost> {
12 let content = match std::fs::read_to_string(path) {
13 Ok(c) => c,
14 Err(_) => return Vec::new(),
15 };
16 parse_known_hosts_str(&content)
17}
18
19pub fn parse_known_hosts_str(content: &str) -> Vec<SshHost> {
21 let mut seen = HashSet::new();
22 let mut hosts = Vec::new();
23
24 for line in content.lines() {
25 let line = line.trim();
26
27 if line.is_empty() || line.starts_with('#') || line.starts_with('@') {
28 continue;
29 }
30
31 let host_field = match line.split_whitespace().next() {
32 Some(f) => f,
33 None => continue,
34 };
35
36 if host_field.starts_with("|1|") {
37 continue;
38 }
39
40 for entry in host_field.split(',') {
41 let (hostname, port) = parse_host_entry(entry);
42
43 if hostname.is_empty() {
44 continue;
45 }
46
47 let key = format!("{}:{}", hostname, port.unwrap_or(22));
48 if seen.contains(&key) {
49 continue;
50 }
51 seen.insert(key);
52
53 hosts.push(SshHost {
54 alias: hostname.clone(),
55 hostname: Some(hostname),
56 user: None,
57 port,
58 identity_file: None,
59 proxy_jump: None,
60 source: SshHostSource::KnownHosts,
61 });
62 }
63 }
64
65 hosts
66}
67
68fn parse_host_entry(entry: &str) -> (String, Option<u16>) {
69 if entry.starts_with('[') {
70 if let Some(bracket_end) = entry.find(']') {
71 let hostname = entry[1..bracket_end].to_string();
72 let port = entry[bracket_end + 1..]
73 .strip_prefix(':')
74 .and_then(|p| p.parse().ok());
75 (hostname, port)
76 } else {
77 (entry.to_string(), None)
78 }
79 } else {
80 (entry.to_string(), None)
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn test_parse_basic_entries() {
90 let content = "github.com ssh-ed25519 AAAAC3...\nbitbucket.org ssh-rsa AAAAB3...\n";
91 let hosts = parse_known_hosts_str(content);
92 assert_eq!(hosts.len(), 2);
93 assert_eq!(hosts[0].alias, "github.com");
94 assert_eq!(hosts[1].alias, "bitbucket.org");
95 }
96
97 #[test]
98 fn test_skip_hashed_entries() {
99 let content = "|1|abc123|def456 ssh-rsa AAAAB3...\ngithub.com ssh-ed25519 AAAA...\n";
100 let hosts = parse_known_hosts_str(content);
101 assert_eq!(hosts.len(), 1);
102 assert_eq!(hosts[0].alias, "github.com");
103 }
104
105 #[test]
106 fn test_bracketed_port() {
107 let content = "[myhost.example.com]:2222 ssh-rsa AAAAB3...\n";
108 let hosts = parse_known_hosts_str(content);
109 assert_eq!(hosts.len(), 1);
110 assert_eq!(hosts[0].alias, "myhost.example.com");
111 assert_eq!(hosts[0].port, Some(2222));
112 }
113
114 #[test]
115 fn test_comma_separated_hostnames() {
116 let content = "host1.example.com,192.168.1.1 ssh-rsa AAAAB3...\n";
117 let hosts = parse_known_hosts_str(content);
118 assert_eq!(hosts.len(), 2);
119 assert_eq!(hosts[0].alias, "host1.example.com");
120 assert_eq!(hosts[1].alias, "192.168.1.1");
121 }
122
123 #[test]
124 fn test_dedup() {
125 let content = "host.com ssh-rsa AAAA...\nhost.com ssh-ed25519 AAAA...\n";
126 let hosts = parse_known_hosts_str(content);
127 assert_eq!(hosts.len(), 1);
128 }
129
130 #[test]
131 fn test_skip_comments_and_markers() {
132 let content =
133 "# comment\n@cert-authority *.example.com ssh-rsa AAAA...\nreal.host ssh-rsa AAAA...\n";
134 let hosts = parse_known_hosts_str(content);
135 assert_eq!(hosts.len(), 1);
136 assert_eq!(hosts[0].alias, "real.host");
137 }
138
139 #[test]
140 fn test_empty() {
141 let hosts = parse_known_hosts_str("");
142 assert!(hosts.is_empty());
143 }
144}