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
130
131
132
use std::path::Path;
use plain_path::PlainPathExt;
use crate::{ConfigError, Error, SshConfig, SshHostConfig, SshOptionKey};
pub struct SshConfigParser;
impl SshConfigParser {
pub async fn parse(path: &Path) -> Result<SshConfig, Error> {
let path = path.plain()?;
let contents = Self::ssh_config_contents(&path).await?;
let ssh_config = Self::parse_config_contents(&contents)?;
Ok(ssh_config)
}
pub async fn parse_home() -> Result<SshConfig, Error> {
if let Some(mut ssh_path) = dirs::home_dir() {
ssh_path.push(".ssh");
ssh_path.push("config");
Self::parse(&ssh_path).await
} else {
Err(Error::HomeDirNotFound)
}
}
async fn ssh_config_contents(path: &Path) -> Result<String, Error> {
async_fs::read_to_string(path)
.await
.map_err(|error| Error::SshConfigRead {
path: path.to_path_buf(),
error,
})
}
pub fn parse_config_contents(ssh_config_contents: &str) -> Result<SshConfig, Error> {
let mut errors = Vec::new();
let kv_pairs = Self::kv_pairs(ssh_config_contents, &mut errors).into_iter();
let mut ssh_config = SshConfig::default();
let mut current_host = None;
let mut ssh_host_config = SshHostConfig::default();
for (key, value) in kv_pairs {
let ssh_option_key = match key.parse::<SshOptionKey>() {
Ok(ssh_option_key) => ssh_option_key,
Err(error) => {
errors.push(error);
continue;
}
};
if let SshOptionKey::Host = ssh_option_key {
if let Some(current_host) = current_host.take() {
ssh_config.insert(current_host, ssh_host_config);
ssh_host_config = SshHostConfig::default();
}
current_host = Some(value.to_string());
} else if current_host.is_none() {
errors.push(ConfigError::SshOptionBeforeHost {
option: ssh_option_key,
value: value.to_string(),
});
} else {
ssh_host_config.insert(ssh_option_key, value.to_string());
}
}
if let Some(current_host) = current_host.take() {
ssh_config.insert(current_host, ssh_host_config);
}
if errors.is_empty() {
Ok(ssh_config)
} else {
Err(Error::ConfigErrors { errors })
}
}
fn kv_pairs<'f>(
ssh_config_contents: &'f str,
config_errors: &mut Vec<ConfigError>,
) -> Vec<(&'f str, &'f str)> {
ssh_config_contents
.lines()
.map(|line| line.split_once('#').map_or(line, |split| split.0))
.map(str::trim)
.filter(|line| !line.is_empty())
.filter_map(|line| {
let kv_pair = Self::kv_split_by(line, '=').or_else(|| Self::kv_split_by(line, ' '));
if kv_pair.is_none() {
config_errors.push(ConfigError::KeyValueNotFound {
line: line.to_string(),
});
}
kv_pair
})
.collect::<Vec<_>>()
}
fn kv_split_by(line: &str, separator: char) -> Option<(&str, &str)> {
let mut kv_split = line.splitn(2, separator);
let key = kv_split.next();
let value = kv_split.next();
match (key, value) {
(Some(key), Some(value)) => Some((key.trim(), value.trim())),
(Some(_), None) => None,
_ => unreachable!("Empty lines are filtered."),
}
}
}