mirror of
https://github.com/veeso/termscp.git
synced 2026-09-26 14:01:17 -07:00
Resolve SSH host parameters in auth forms and CLI connections while preserving explicit user, bookmark, and parsed alias values. Continue forwarding SSH config files for HostName and other SSH options, and document the precedence in English and Chinese. Closes #441
315 lines
9.6 KiB
Rust
315 lines
9.6 KiB
Rust
//! ## Remote CLI Arguments
|
|
//!
|
|
//! Parses positional and bookmark-based CLI arguments into the normalized remote
|
|
//! connection parameters used by the application.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use super::Args;
|
|
use crate::filetransfer::FileTransferParams;
|
|
use crate::utils;
|
|
|
|
/// Address type
|
|
enum AddrType {
|
|
Address,
|
|
Bookmark,
|
|
}
|
|
|
|
/// Args for remote connection
|
|
#[derive(Debug)]
|
|
pub struct RemoteArgs {
|
|
/// Optional host bridge selected for the session.
|
|
pub host_bridge: Remote,
|
|
/// Target remote selected for the session.
|
|
pub remote: Remote,
|
|
/// Optional local working directory override.
|
|
pub local_dir: Option<PathBuf>,
|
|
}
|
|
|
|
impl Default for RemoteArgs {
|
|
fn default() -> Self {
|
|
Self {
|
|
host_bridge: Remote::None,
|
|
remote: Remote::None,
|
|
local_dir: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<&Args> for RemoteArgs {
|
|
type Error = String;
|
|
|
|
fn try_from(args: &Args) -> Result<Self, Self::Error> {
|
|
let mut remote_args = RemoteArgs::default();
|
|
// validate arguments
|
|
match (args.bookmark.len(), args.positional.len()) {
|
|
(0, positional) if positional < 4 => Ok(()),
|
|
(1, positional) if positional < 3 => Ok(()),
|
|
(2, positional) if positional < 2 => Ok(()),
|
|
(_, _) => Err("Too many arguments".to_string()),
|
|
}?;
|
|
// parse bookmark first
|
|
let last_item_index = (args.bookmark.len() + args.positional.len()).saturating_sub(1);
|
|
|
|
let mut hosts = vec![];
|
|
|
|
for (i, (addr_type, arg)) in args
|
|
.bookmark
|
|
.iter()
|
|
.map(|x| (AddrType::Bookmark, x))
|
|
.chain(args.positional.iter().map(|x| (AddrType::Address, x)))
|
|
.enumerate()
|
|
{
|
|
// check if has password
|
|
let password = args.password.get(i).cloned();
|
|
|
|
// check if is last item and so a possible local dir
|
|
if i == last_item_index && Path::new(arg).exists() {
|
|
remote_args.local_dir = Some(PathBuf::from(arg));
|
|
continue;
|
|
}
|
|
|
|
let remote = match addr_type {
|
|
AddrType::Address => Self::parse_remote_address(arg).map(|parsed| {
|
|
Remote::Host(HostParams::new(
|
|
parsed.file_transfer_params,
|
|
parsed.port_explicit,
|
|
password,
|
|
))
|
|
})?,
|
|
AddrType::Bookmark => Remote::Bookmark(BookmarkParams::new(arg, password.as_ref())),
|
|
};
|
|
|
|
// set remote
|
|
hosts.push(remote);
|
|
}
|
|
|
|
// set args based on hosts len
|
|
if hosts.len() == 1 {
|
|
remote_args.remote = hosts
|
|
.pop()
|
|
.ok_or_else(|| String::from("Missing remote host configuration"))?;
|
|
} else if hosts.len() == 2 {
|
|
remote_args.host_bridge = hosts
|
|
.pop()
|
|
.ok_or_else(|| String::from("Missing host-bridge configuration"))?;
|
|
remote_args.remote = hosts
|
|
.pop()
|
|
.ok_or_else(|| String::from("Missing remote host configuration"))?;
|
|
}
|
|
|
|
Ok(remote_args)
|
|
}
|
|
}
|
|
|
|
impl RemoteArgs {
|
|
/// Parse remote address
|
|
fn parse_remote_address(remote: &str) -> Result<utils::parser::ParsedRemote, String> {
|
|
utils::parser::parse_remote_opt_with_metadata(remote)
|
|
.map_err(|e| format!("Bad address option: {e}"))
|
|
}
|
|
}
|
|
|
|
/// Remote argument type
|
|
#[allow(clippy::large_enum_variant)]
|
|
#[derive(Debug)]
|
|
pub enum Remote {
|
|
/// Bookmark name argument
|
|
Bookmark(BookmarkParams),
|
|
/// Host argument
|
|
Host(HostParams),
|
|
/// Unspecified
|
|
None,
|
|
}
|
|
|
|
impl Remote {
|
|
/// Returns whether this CLI slot was left unspecified.
|
|
pub fn is_none(&self) -> bool {
|
|
matches!(self, Self::None)
|
|
}
|
|
}
|
|
|
|
/// Bookmark parameters
|
|
#[derive(Debug)]
|
|
pub struct BookmarkParams {
|
|
/// bookmark name
|
|
pub name: String,
|
|
/// bookmark password
|
|
pub password: Option<String>,
|
|
}
|
|
|
|
/// Host parameters
|
|
#[derive(Debug)]
|
|
pub struct HostParams {
|
|
/// file transfer parameters
|
|
pub file_transfer_params: FileTransferParams,
|
|
/// Whether the address explicitly provided a port.
|
|
pub port_explicit: bool,
|
|
/// host password specified in arguments
|
|
pub password: Option<String>,
|
|
}
|
|
|
|
impl BookmarkParams {
|
|
pub fn new<S: AsRef<str>>(name: S, password: Option<S>) -> Self {
|
|
Self {
|
|
name: name.as_ref().to_string(),
|
|
password: password.map(|x| x.as_ref().to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl HostParams {
|
|
pub fn new<S: AsRef<str>>(
|
|
params: FileTransferParams,
|
|
port_explicit: bool,
|
|
password: Option<S>,
|
|
) -> Self {
|
|
Self {
|
|
file_transfer_params: params,
|
|
port_explicit,
|
|
password: password.map(|x| x.as_ref().to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_should_make_remote_args_from_args_one_remote() {
|
|
let args = Args {
|
|
positional: vec!["scp://host1".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
assert!(matches!(remote_args.host_bridge, Remote::None));
|
|
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
|
assert_eq!(remote_args.local_dir, None);
|
|
}
|
|
|
|
#[test]
|
|
fn should_preserve_explicit_port_from_positional_remote() {
|
|
for (remote, port_explicit) in [("scp://host", false), ("scp://host:22", true)] {
|
|
let args = Args {
|
|
positional: vec![remote.to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
let Remote::Host(params) = remote_args.remote else {
|
|
panic!("expected positional remote to be a host");
|
|
};
|
|
|
|
assert_eq!(params.port_explicit, port_explicit, "{remote}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_should_make_remote_args_from_args_two_remotes() {
|
|
let args = Args {
|
|
positional: vec!["scp://host1".to_string(), "scp://host2".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
|
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
|
assert_eq!(remote_args.local_dir, None);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(posix)]
|
|
fn test_should_make_remote_args_from_two_remotes_and_local_dir() {
|
|
let args = Args {
|
|
positional: vec![
|
|
"scp://host1".to_string(),
|
|
"scp://host2".to_string(),
|
|
"/home".to_string(),
|
|
],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
|
assert!(matches!(remote_args.remote, Remote::Host(_)));
|
|
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_should_make_remote_args_from_args_one_bookmarks() {
|
|
let args = Args {
|
|
bookmark: vec!["foo".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
assert!(matches!(remote_args.host_bridge, Remote::None));
|
|
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
|
assert_eq!(remote_args.local_dir, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_should_make_remote_args_from_args_two_bookmarks() {
|
|
let args = Args {
|
|
bookmark: vec!["foo".to_string(), "bar".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
|
|
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
|
assert_eq!(remote_args.local_dir, None);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(posix)]
|
|
fn test_should_make_remote_args_from_two_bookmarks_and_local_dir() {
|
|
let args = Args {
|
|
bookmark: vec!["foo".to_string(), "bar".to_string()],
|
|
positional: vec!["/home".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
assert!(matches!(remote_args.host_bridge, Remote::Bookmark(_)));
|
|
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
|
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_should_make_remote_args_from_one_bookmark_and_one_remote() {
|
|
let args = Args {
|
|
bookmark: vec!["foo".to_string()],
|
|
positional: vec!["scp://host1".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
|
|
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
|
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
|
assert_eq!(remote_args.local_dir, None);
|
|
}
|
|
|
|
#[test]
|
|
#[cfg(posix)]
|
|
fn test_should_make_remote_args_from_one_bookmark_and_one_remote_with_local_dir() {
|
|
let args = Args {
|
|
positional: vec!["scp://host1".to_string(), "/home".to_string()],
|
|
bookmark: vec!["foo".to_string()],
|
|
..Default::default()
|
|
};
|
|
|
|
let remote_args = RemoteArgs::try_from(&args).unwrap();
|
|
|
|
assert!(matches!(remote_args.host_bridge, Remote::Host(_)));
|
|
assert!(matches!(remote_args.remote, Remote::Bookmark(_)));
|
|
assert_eq!(remote_args.local_dir, Some(PathBuf::from("/home")));
|
|
}
|
|
}
|