1use {
2 indexmap::IndexMap,
3 std::{env, fs, io},
4};
5
6pub type HostMap = IndexMap<String, String>;
7
8pub fn load_ssh_config(path: &str) -> io::Result<HostMap> {
10 parse_ssh_config(&fs::read_to_string(
11 path.replace('~', &env::var("HOME").expect("$HOME must be set")),
12 )?)
13}
14
15pub fn parse_ssh_config<S: AsRef<str>>(config: S) -> io::Result<HostMap> {
17 let config = config.as_ref();
18 let mut map = HostMap::new();
19
20 let mut token = String::new(); let mut line = vec![]; let mut skip_line = false; let mut stanza = String::new(); let mut key = true; for c in config.chars() {
27 if skip_line {
28 if c == '\n' {
29 skip_line = false;
30 }
31 continue;
32 }
33
34 if c == '#' {
35 skip_line = true;
37 if !token.is_empty() {
38 line.push(token);
39 token = String::new();
40 }
41 } else if key && (c == ' ' || c == '=') {
42 if !token.is_empty() {
44 line.push(token);
45 token = String::new();
46 key = false;
47 }
48 } else if c == '\n' {
49 if !token.is_empty() {
50 line.push(token);
51 token = String::new();
52 key = true;
53 }
54
55 if line.is_empty() {
56 continue;
57 } else if line.len() != 2 {
58 return Err(io::Error::new(
59 io::ErrorKind::Other,
60 format!("can't parse line: {:?}", line),
61 ));
62 } else {
63 match line[0].to_lowercase().as_ref() {
64 "host" => {
65 let parsed = &line[1];
66 if parsed.contains('*')
68 || parsed.contains('!')
69 || parsed.contains('?')
70 || parsed.contains(',')
71 || parsed.contains(' ')
72 {
73 stanza.clear();
74 } else {
75 stanza = parsed.clone();
76 map.insert(stanza.clone(), stanza.clone());
79 }
80 }
81 "hostname" => {
82 if !stanza.is_empty() {
83 map.insert(stanza.clone(), line[1].clone());
84 stanza.clear();
85 }
86 }
87 _ => {}
88 }
89 line.clear();
90 }
91 } else if (c == ' ' || c == '=') && token.is_empty() {
92 continue;
94 } else {
95 token.push(c);
97 }
98 }
99
100 Ok(map)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_config() {
109 let config = load_ssh_config("./tests/test_config").expect("failed to parse config");
110 assert_eq!(11, config.len());
111
112 assert_eq!(
113 config.keys().cloned().collect::<Vec<_>>(),
114 vec![
115 "homework-server",
116 "nixcraft",
117 "docker1",
118 "nas01",
119 "docker2",
120 "docker3",
121 "devserver",
122 "ec2-some-long-name.amazon.probably.com",
123 "ec2-some-long-namer.amazon.probably.com",
124 "torrentz-server",
125 "midi-files.com",
126 ]
127 );
128 assert_eq!("torrentz-r-us.com", config.get("torrentz-server").unwrap());
129 assert_eq!("docker3.mycloud.net", config.get("docker3").unwrap());
130 assert_eq!("192.168.1.100", config.get("nas01").unwrap());
131 assert_eq!("midi-files.com", config.get("midi-files.com").unwrap());
132 }
133}