/ app / src / utils.rs
utils.rs
  1  use anyhow::{Context, Result, bail};
  2  
  3  /// SSH connection information
  4  #[derive(Debug, PartialEq)]
  5  pub struct SshConnection {
  6      pub hostname: String,
  7      pub port: u16,
  8  }
  9  
 10  /// Parses a destination string into its components.
 11  /// Supports both "user@host:/path" and "host:/path" formats.
 12  /// When username is omitted, the current system username is used.
 13  /// Resolves hostnames and ports using SSH config if available.
 14  pub fn parse_destination(dest: &str) -> Result<(String, SshConnection, String)> {
 15      let (user_host_part, path_part) = dest
 16          .split_once(':')
 17          .context("Invalid destination format. Expected 'host:/path' or 'user@host:/path'")?;
 18  
 19      let (user, host) = if user_host_part.contains('@') {
 20          let parts: Vec<&str> = user_host_part.splitn(2, '@').collect();
 21          (parts[0].to_string(), parts[1].to_string())
 22      } else {
 23          // Use current username when not specified
 24          let current_user = std::env::var("USER")
 25              .or_else(|_| std::env::var("LOGNAME"))
 26              .unwrap_or_else(|_| "unknown".to_string());
 27          (current_user, user_host_part.to_string())
 28      };
 29  
 30      // Resolve host and port using SSH config
 31      let connection = resolve_ssh_connection(&host)?;
 32  
 33      Ok((user, connection, path_part.to_string()))
 34  }
 35  
 36  /// Resolves hostname and port from SSH config
 37  fn resolve_ssh_connection(host: &str) -> Result<SshConnection> {
 38      // Try to use ssh_config crate if available, otherwise use ssh command
 39      if let Ok(connection) = resolve_connection_with_ssh_config(host) {
 40          return Ok(connection);
 41      }
 42  
 43      // Fallback: try to resolve using ssh -G command
 44      if let Ok(connection) = resolve_connection_with_ssh_command(host) {
 45          return Ok(connection);
 46      }
 47  
 48      // Final fallback: use host as-is with default port
 49      Ok(SshConnection {
 50          hostname: host.to_string(),
 51          port: 22,
 52      })
 53  }
 54  
 55  /// Resolve hostname and port using ssh -G command
 56  fn resolve_connection_with_ssh_command(host: &str) -> Result<SshConnection> {
 57      use std::process::Command;
 58  
 59      let output = Command::new("ssh")
 60          .arg("-G")
 61          .arg(host)
 62          .output()
 63          .context("Failed to run ssh -G command")?;
 64  
 65      if !output.status.success() {
 66          bail!("ssh -G command failed for host: {}", host);
 67      }
 68  
 69      let stdout = String::from_utf8_lossy(&output.stdout);
 70      let mut hostname = host.to_string();
 71      let mut port = 22;
 72  
 73      for line in stdout.lines() {
 74          if line.starts_with("hostname ") {
 75              hostname = line[9..].trim().to_string();
 76          } else if line.starts_with("port ") {
 77              if let Ok(p) = line[5..].trim().parse::<u16>() {
 78                  port = p;
 79              }
 80          }
 81      }
 82  
 83      Ok(SshConnection { hostname, port })
 84  }
 85  
 86  /// Resolve hostname and port using ssh_config crate (if available)
 87  fn resolve_connection_with_ssh_config(host: &str) -> Result<SshConnection> {
 88      // This is a simplified version - in a real implementation, you'd use the ssh_config crate
 89      // For now, we'll check common SSH config locations manually
 90      let home_dir = std::env::var("HOME").unwrap_or_else(|_| "/home".to_string());
 91      let config_paths = [
 92          format!("{}/.ssh/config", home_dir),
 93          "/etc/ssh/ssh_config".to_string(),
 94      ];
 95  
 96      for config_path in &config_paths {
 97          if let Ok(content) = std::fs::read_to_string(config_path) {
 98              if let Some(connection) = parse_ssh_config_connection(&content, host) {
 99                  return Ok(connection);
100              }
101          }
102      }
103  
104      bail!("No SSH config found for host: {}", host)
105  }
106  
107  /// Simple SSH config parser for hostname and port
108  fn parse_ssh_config_connection(config: &str, host: &str) -> Option<SshConnection> {
109      let mut current_host = None;
110      let mut hostname = None;
111      let mut port = 22;
112  
113      for line in config.lines() {
114          let line = line.trim();
115          if line.is_empty() || line.starts_with('#') {
116              continue;
117          }
118  
119          let parts: Vec<&str> = line.split_whitespace().collect();
120          if parts.len() < 2 {
121              continue;
122          }
123  
124          match parts[0].to_lowercase().as_str() {
125              "host" => {
126                  current_host = Some(parts[1]);
127                  hostname = None; // Reset for new host
128                  port = 22; // Reset to default
129              }
130              "hostname" => {
131                  if let Some(h) = current_host {
132                      if h == host || (h == "*" && hostname.is_none()) {
133                          hostname = Some(parts[1].to_string());
134                      }
135                  }
136              }
137              "port" => {
138                  if let Some(h) = current_host {
139                      if h == host || (h == "*") {
140                          if let Ok(p) = parts[1].parse::<u16>() {
141                              port = p;
142                          }
143                      }
144                  }
145              }
146              _ => {}
147          }
148      }
149  
150      hostname.map(|h| SshConnection { hostname: h, port })
151  }