1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use indexmap::IndexMap;
use std::{fs, io};

pub type HostMap = IndexMap<String, String>;

/// For now just load the hostnames and their labels.
pub fn load_ssh_config(path: &str) -> Result<HostMap, io::Error> {
    parse_ssh_config(&fs::read_to_string(path.replace('~', env!("HOME")))?)
}

/// Parse .ssh/config to a (sorted) map.
pub fn parse_ssh_config<S: AsRef<str>>(config: S) -> Result<HostMap, io::Error> {
    let config = config.as_ref();
    let mut map = HostMap::new();

    let mut token = String::new(); // the token we're parsing
    let mut line = vec![]; // current line
    let mut skip_line = false; // skip until EOL for comments
    let mut stanza = String::new(); // ssh config is broken into stanzas
    let mut key = true; // parsing the key or the value?

    for c in config.chars() {
        if skip_line {
            if c == '\n' {
                skip_line = false;
            }
            continue;
        }

        if c == '#' {
            // skip comments
            skip_line = true;
            if !token.is_empty() {
                line.push(token);
                token = String::new();
            }
        } else if key && (c == ' ' || c == '=') {
            // "key = value" OR "key value" separator
            if !token.is_empty() {
                line.push(token);
                token = String::new();
                key = false;
            }
        } else if c == '\n' {
            if !token.is_empty() {
                line.push(token);
                token = String::new();
                key = true;
            }

            if line.is_empty() {
                continue;
            } else if line.len() != 2 {
                return Err(io::Error::new(
                    io::ErrorKind::Other,
                    format!("can't parse line: {:?}", line),
                ));
            } else {
                match line[0].to_lowercase().as_ref() {
                    "host" => {
                        let parsed = &line[1];
                        // skip any Host patterns
                        if parsed.contains('*')
                            || parsed.contains('!')
                            || parsed.contains('?')
                            || parsed.contains(',')
                            || parsed.contains(' ')
                        {
                            stanza.clear();
                        } else {
                            stanza = parsed.clone();
                            // by default we assume host patterns are
                            // actual hostnames
                            map.insert(stanza.clone(), stanza.clone());
                        }
                    }
                    "hostname" => {
                        if !stanza.is_empty() {
                            map.insert(stanza.clone(), line[1].clone());
                            stanza.clear();
                        }
                    }
                    _ => {}
                }
                line.clear();
            }
        } else if (c == ' ' || c == '=') && token.is_empty() {
            // skip = and whitespace at start of value, key = value format
            continue;
        } else {
            // regular char
            token.push(c);
        }
    }

    Ok(map)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_config() {
        let config = load_ssh_config("./tests/test_config").expect("failed to parse config");
        assert_eq!(11, config.len());

        assert_eq!(
            config.keys().cloned().collect::<Vec<_>>(),
            vec![
                "homework-server",
                "nixcraft",
                "docker1",
                "nas01",
                "docker2",
                "docker3",
                "devserver",
                "ec2-some-long-name.amazon.probably.com",
                "ec2-some-long-namer.amazon.probably.com",
                "torrentz-server",
                "midi-files.com",
            ]
        );
        assert_eq!("torrentz-r-us.com", config.get("torrentz-server").unwrap());
        assert_eq!("docker3.mycloud.net", config.get("docker3").unwrap());
        assert_eq!("192.168.1.100", config.get("nas01").unwrap());
        assert_eq!("midi-files.com", config.get("midi-files.com").unwrap());
    }
}