svn 0.1.8

Async Rust SVN client for Subversion svn://, svn+ssh://, and ra_svn workflows.
Documentation
use std::time::Duration;

use crate::{SvnError, SvnUrl};

use super::config::{SshConfig, SshHostKeyPolicy};
use super::openssh::{HostParams, expand_tilde_str, normalize_identity_file_path};

#[derive(Clone, Debug)]
pub(super) struct ResolvedSshSettings {
    pub(super) connect_host: String,
    pub(super) connect_port: u16,
    pub(super) known_hosts_host: String,
    pub(super) username: String,
    pub(super) identity_files: Vec<std::path::PathBuf>,
    pub(super) identity_agent: Option<String>,
    pub(super) identities_only: bool,
    pub(super) host_key: SshHostKeyPolicy,
    pub(super) accept_new_host_keys: bool,
    pub(super) connect_timeout: Duration,
}

fn default_ssh_username() -> Option<String> {
    std::env::var("USER")
        .ok()
        .or_else(|| std::env::var("USERNAME").ok())
        .and_then(|username| (!username.trim().is_empty()).then_some(username))
}

fn url_username(url: &SvnUrl) -> Option<String> {
    let rest = url.url.strip_prefix("svn+ssh://")?;
    let authority = rest
        .split_once('/')
        .map(|(authority, _)| authority)
        .unwrap_or(rest);
    let (user, _) = authority.rsplit_once('@')?;
    (!user.trim().is_empty()).then(|| user.to_string())
}

pub(super) fn resolve_ssh_settings(
    url: &SvnUrl,
    ssh: &SshConfig,
    connect_timeout: Duration,
    openssh: Option<&HostParams>,
) -> Result<ResolvedSshSettings, SvnError> {
    let connect_host = openssh
        .and_then(|params| params.host_name.as_ref())
        .cloned()
        .unwrap_or_else(|| url.host.clone());

    let connect_port = if url.port != 22 {
        url.port
    } else {
        openssh.and_then(|params| params.port).unwrap_or(url.port)
    };

    let connect_timeout = openssh
        .and_then(|params| params.connect_timeout)
        .map(|timeout| timeout.min(connect_timeout))
        .unwrap_or(connect_timeout);

    let mut host_key = ssh.host_key.clone();
    let mut accept_new_host_keys = ssh.accept_new_host_keys;

    if matches!(host_key, SshHostKeyPolicy::KnownHosts)
        && let Some(path) = openssh.and_then(|params| params.user_known_hosts_file.as_deref())
    {
        host_key = SshHostKeyPolicy::KnownHostsFile(expand_tilde_str(path));
    }

    if let Some(value) = openssh.and_then(|params| params.strict_host_key_checking.as_deref()) {
        match value.trim().to_ascii_lowercase().as_str() {
            "no" => {
                host_key = SshHostKeyPolicy::AcceptAny;
                accept_new_host_keys = false;
            }
            "accept-new" => accept_new_host_keys = true,
            _ => {}
        }
    }

    let known_hosts_host = openssh
        .and_then(|params| params.host_key_alias.as_ref())
        .cloned()
        .unwrap_or_else(|| url.host.clone());

    let identity_agent = openssh
        .and_then(|params| params.identity_agent.as_ref())
        .cloned();

    let identities_only = openssh
        .and_then(|params| params.identities_only)
        .unwrap_or(false);

    let identity_files = openssh
        .and_then(|params| params.identity_file.as_ref())
        .cloned()
        .unwrap_or_default()
        .into_iter()
        .map(|path| normalize_identity_file_path(&path))
        .collect();

    let username = if let Some(username) = ssh.username_override() {
        username.to_string()
    } else if let Some(username) = url_username(url) {
        username
    } else if let Some(username) = openssh.and_then(|params| params.user.as_deref()) {
        username.to_string()
    } else if let Some(username) = default_ssh_username() {
        username
    } else {
        return Err(SvnError::InvalidUrl(
            "svn+ssh requires an SSH username (set SshConfig::with_username, include it in the URL, or configure User in ~/.ssh/config)"
                .to_string(),
        ));
    };

    Ok(ResolvedSshSettings {
        connect_host,
        connect_port,
        known_hosts_host,
        username,
        identity_files,
        identity_agent,
        identities_only,
        host_key,
        accept_new_host_keys,
        connect_timeout,
    })
}