Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ result


# Added by cargo

.idea/
.zed/
/target
100 changes: 81 additions & 19 deletions src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,34 +66,96 @@ impl From<ParseError> for ParseConfigError {
}
}

/// # Errors
///
/// Will return `Err` if the SSH configuration file cannot be parsed.
pub fn parse_config(raw_path: &String) -> Result<Vec<Host>, ParseConfigError> {
let normalized_path = shellexpand::tilde(&raw_path).to_string();
let path = std::fs::canonicalize(normalized_path)?;

let hosts = ssh_config::Parser::new()
.parse_file(path)?
.apply_patterns()
/// Process raw host configurations, apply empty hostname logic and convert to Host structs
///
/// # Arguments
/// * `raw_hosts` - List of raw host configurations parsed from SSH config file
///
/// # Returns
/// List of processed Host structs
pub fn process_hosts(raw_hosts: Vec<ssh_config::Host>) -> Vec<Host> {
// Apply configuration processing in optimal order and convert to Host structs
raw_hosts
.apply_name_to_empty_hostname()
.apply_patterns()
.merge_same_hosts()
.iter()
.map(|host| Host {
name: host
.get_patterns()
.first()
.unwrap_or(&String::new())
.clone(),
name: host.get_patterns().first().unwrap_or(&String::new()).clone(),
aliases: host.get_patterns().iter().skip(1).join(", "),
user: host.get(&ssh_config::EntryType::User),
destination: host
.get(&ssh_config::EntryType::Hostname)
.unwrap_or_default(),
destination: host.get(&ssh_config::EntryType::Hostname).unwrap_or_default(),
port: host.get(&ssh_config::EntryType::Port),
proxy_command: host.get(&ssh_config::EntryType::ProxyCommand),
})
.collect();
.collect()
}

/// # Errors
///
/// Will return `Err` if the SSH configuration file cannot be parsed.
pub fn parse_config(raw_path: &String) -> Result<Vec<Host>, ParseConfigError> {
let normalized_path = shellexpand::tilde(&raw_path).to_string();
let path = std::fs::canonicalize(normalized_path)?;

// Parse the raw configuration file
let raw_hosts = ssh_config::Parser::new().parse_file(path)?;

// Call the extracted processing logic
let hosts = process_hosts(raw_hosts);

Ok(hosts)
}

#[cfg(test)]
mod tests {
use super::*;
use crate::ssh_config::{EntryType, Host};

#[test]
fn test_process_hosts_with_aliases() {
// 直接创建包含多个模式的Host对象
let mut ssh_config_host = Host::new(vec![
"server1".to_string(),
"server2".to_string(),
"dev-server".to_string()
]);

// 添加配置项
ssh_config_host.update((EntryType::Hostname, "example.com".to_string()));
ssh_config_host.update((EntryType::User, "testuser".to_string()));
ssh_config_host.update((EntryType::Port, "2222".to_string()));

// 创建原始主机列表
let raw_hosts = vec![ssh_config_host];

// 调用process_hosts函数
let hosts = process_hosts(raw_hosts);

// 验证结果
assert_eq!(hosts.len(), 1, "Should have one host entry");
assert_eq!(hosts[0].name, "server1", "First pattern should be the name");
assert_eq!(hosts[0].aliases, "server2, dev-server", "Remaining patterns should be aliases");
assert_eq!(hosts[0].destination, "example.com", "Hostname should be correct");
assert_eq!(hosts[0].user, Some("testuser".to_string()), "User should be correct");
assert_eq!(hosts[0].port, Some("2222".to_string()), "Port should be correct");
}

#[test]
fn test_process_hosts_with_empty_hostname() {
// 测试没有设置Hostname的情况
let mut ssh_config_host = Host::new(vec!["server1".to_string(), "server2".to_string()]);

// 不设置Hostname,这样会应用第一个模式作为Hostname
ssh_config_host.update((EntryType::User, "testuser".to_string()));

let raw_hosts = vec![ssh_config_host];
let hosts = process_hosts(raw_hosts);

// 验证结果 - Hostname应该被设置为第一个模式
assert_eq!(hosts.len(), 1);
assert_eq!(hosts[0].name, "server1");
assert_eq!(hosts[0].aliases, "server2");
assert_eq!(hosts[0].destination, "server1", "Destination should be set to first pattern");
}
}
85 changes: 48 additions & 37 deletions src/ssh_config/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,46 +177,57 @@ impl HostVecExt for Vec<Host> {
}

/// Apply patterns entries to non-pattern hosts and remove the pattern hosts.
///
/// Only copies UI-relevant parameters (User, Hostname, Port, ProxyCommand) from pattern hosts
/// to matching non-pattern hosts when the parameter doesn't already exist.
///
/// You might want to call [`HostVecExt::merge_same_hosts`] after this.
fn apply_patterns(&self) -> Self {
let mut hosts = self.spread();
let mut pattern_indexes = Vec::new();

for i in 0..hosts.len() {
let matching_pattern_regexes = hosts[i].matching_pattern_regexes();
if matching_pattern_regexes.is_empty() {
continue;
}

pattern_indexes.push(i);

for j in 0..hosts.len() {
if i == j {
continue;
}

if !hosts[j].matching_pattern_regexes().is_empty() {
continue;
}

for (regex, is_negated) in &matching_pattern_regexes {
if regex.is_match(&hosts[j].patterns[0]) == *is_negated {
continue;
}

let host = hosts[i].clone();
hosts[j].extend_if_not_contained(&host);
break;
}
}
}

for i in pattern_indexes.into_iter().rev() {
hosts.remove(i);
}

hosts
// UI展示关心的参数列表
const UI_RELEVANT_ENTRY_TYPES: [EntryType; 4] = [
EntryType::User,
EntryType::Hostname,
EntryType::Port,
EntryType::ProxyCommand,
];

let spread_hosts = self.spread();

// 分离模式主机和非模式主机
let (pattern_hosts, non_pattern_hosts): (Vec<_>, Vec<_>) =
spread_hosts.into_iter().partition(|host| !host.matching_pattern_regexes().is_empty());

// 对每个非模式主机应用匹配的模式主机参数
non_pattern_hosts
.into_iter()
.map(|mut host| {
// 先计算匹配的模式主机,避免同时借用host
let host_name = host.patterns[0].clone();

// 找出所有匹配的模式主机
let matching_patterns: Vec<_> = pattern_hosts
.iter()
.filter(|pattern_host| {
pattern_host.matching_pattern_regexes().iter().any(|(regex, is_negated)| {
regex.is_match(&host_name) != *is_negated
})
})
.collect();

// 应用参数
matching_patterns.iter().for_each(|pattern_host| {
UI_RELEVANT_ENTRY_TYPES.iter().for_each(|entry_type| {
if host.get(entry_type).is_none() {
if let Some(value) = pattern_host.get(entry_type) {
host.update((entry_type.clone(), value));
}
}
});
});

host
})
.collect()
}
}

Expand Down