use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HostConfig {
pub hostname: Option<String>,
pub user: Option<String>,
pub port: Option<u16>,
pub identity_file: Option<String>,
}
pub fn resolve(config: &str, alias: &str) -> HostConfig {
let mut out = HostConfig::default();
let mut current: Option<Vec<String>> = None;
for raw in config.lines() {
let line = strip_comment(raw).trim();
if line.is_empty() {
continue;
}
let (key, value) = match split_kv(line) {
Some(kv) => kv,
None => continue,
};
if key.eq_ignore_ascii_case("Host") {
current = Some(
value
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect(),
);
continue;
}
let matches = current
.as_ref()
.is_some_and(|pats| host_matches(pats, alias));
if !matches {
continue;
}
if key.eq_ignore_ascii_case("HostName") {
out.hostname.get_or_insert_with(|| value.to_string());
} else if key.eq_ignore_ascii_case("User") {
out.user.get_or_insert_with(|| value.to_string());
} else if key.eq_ignore_ascii_case("Port") {
if out.port.is_none()
&& let Ok(p) = value.trim().parse()
{
out.port = Some(p);
}
} else if key.eq_ignore_ascii_case("IdentityFile") {
out.identity_file.get_or_insert_with(|| value.to_string());
}
}
out
}
fn strip_comment(line: &str) -> &str {
match line.find('#') {
Some(i) => &line[..i],
None => line,
}
}
fn split_kv(line: &str) -> Option<(&str, &str)> {
let sep = line.find(|c: char| c.is_whitespace() || c == '=')?;
let key = line[..sep].trim();
let value = line[sep..].trim_start_matches(['=', ' ', '\t']).trim();
if key.is_empty() || value.is_empty() {
return None;
}
Some((key, value))
}
fn host_matches(patterns: &[String], alias: &str) -> bool {
let mut matched = false;
for pat in patterns {
if let Some(neg) = pat.strip_prefix('!') {
if glob_match(neg, alias) {
return false;
}
} else if glob_match(pat, alias) {
matched = true;
}
}
matched
}
fn glob_match(pattern: &str, text: &str) -> bool {
let (p, t) = (pattern.as_bytes(), text.as_bytes());
let (mut pi, mut ti) = (0, 0);
let (mut star, mut mark) = (None, 0);
while ti < t.len() {
if pi < p.len() && (p[pi] == b'?' || p[pi] == t[ti]) {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == b'*' {
star = Some(pi);
mark = ti;
pi += 1;
} else if let Some(s) = star {
pi = s + 1;
mark += 1;
ti = mark;
} else {
return false;
}
}
while pi < p.len() && p[pi] == b'*' {
pi += 1;
}
pi == p.len()
}
#[cfg(test)]
mod tests {
use super::*;
const CONFIG: &str = "\
# a comment
Host gh github
HostName github.com
User git
Port 2222
IdentityFile ~/.ssh/id_gh
Host *.internal
User deploy
Host *
User fallback
Port 22
";
#[test]
fn resolves_an_exact_alias() {
let c = resolve(CONFIG, "gh");
assert_eq!(c.hostname.as_deref(), Some("github.com"));
assert_eq!(c.user.as_deref(), Some("git"));
assert_eq!(c.port, Some(2222));
assert_eq!(c.identity_file.as_deref(), Some("~/.ssh/id_gh"));
}
#[test]
fn second_alias_on_the_same_host_line() {
assert_eq!(resolve(CONFIG, "github").user.as_deref(), Some("git"));
}
#[test]
fn first_value_wins_across_blocks() {
let c = resolve(CONFIG, "box.internal");
assert_eq!(c.user.as_deref(), Some("deploy"));
assert_eq!(c.port, Some(22));
}
#[test]
fn wildcard_only_match() {
let c = resolve(CONFIG, "random");
assert_eq!(c.user.as_deref(), Some("fallback"));
assert!(c.hostname.is_none());
}
#[test]
fn glob_and_negation() {
assert!(glob_match("*.example.com", "a.example.com"));
assert!(glob_match("h?st", "host"));
assert!(!glob_match("h?st", "haste"));
let cfg = "Host *,!secret\n User general\n";
assert_eq!(resolve(cfg, "public").user.as_deref(), Some("general"));
assert!(resolve(cfg, "secret").user.is_none());
}
}