#[cfg(unix)] mod unix; #[cfg(unix)] pub(crate) use unix::*; #[cfg(windows)] pub(crate) const REATTACH_COMMAND_ENV_VAR: &str = "HERDR_REATTACH_COMMAND"; #[cfg(windows)] pub(crate) const REMOTE_KEYBINDINGS_ENV_VAR: &str = "HERDR_REMOTE_KEYBINDINGS"; #[cfg(windows)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum RemoteKeybindings { Local, Server, } #[cfg(windows)] impl RemoteKeybindings { fn parse(value: &str) -> Result { match value { "local" => Ok(Self::Local), "server" => Ok(Self::Server), _ => Err("--remote-keybindings must be 'local' or 'server'".to_string()), } } } #[cfg(windows)] #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RemoteLaunch { pub(crate) target: String, pub(crate) keybindings: RemoteKeybindings, pub(crate) live_handoff: bool, } #[cfg(windows)] pub(crate) fn extract_remote_args( args: &[String], ) -> Result<(Vec, Option), String> { let mut cleaned = Vec::with_capacity(args.len()); if let Some(program) = args.first() { cleaned.push(program.clone()); } let mut remote_target = None; let mut keybindings = RemoteKeybindings::Local; let mut keybindings_seen = false; let mut live_handoff = false; let mut index = 1; while index < args.len() { let arg = &args[index]; if arg == "--" { cleaned.extend_from_slice(&args[index..]); break; } if arg == "--handoff" { live_handoff = true; index += 1; continue; } if arg == "--remote" { if remote_target.is_some() { return Err("--remote can only be specified once".to_string()); } let Some(value) = args.get(index + 1) else { return Err("missing value for --remote".to_string()); }; remote_target = Some(validate_remote_target(value)?.to_owned()); index += 2; continue; } if let Some(value) = arg.strip_prefix("--remote=") { if remote_target.is_some() { return Err("--remote can only be specified once".to_string()); } remote_target = Some(validate_remote_target(value)?.to_owned()); index += 1; continue; } if arg == "--remote-keybindings" { if keybindings_seen { return Err("--remote-keybindings can only be specified once".to_string()); } let Some(value) = args.get(index + 1) else { return Err("missing value for --remote-keybindings".to_string()); }; keybindings = RemoteKeybindings::parse(value)?; keybindings_seen = true; index += 2; continue; } if let Some(value) = arg.strip_prefix("--remote-keybindings=") { if keybindings_seen { return Err("--remote-keybindings can only be specified once".to_string()); } keybindings = RemoteKeybindings::parse(value)?; keybindings_seen = true; index += 1; continue; } cleaned.push(arg.clone()); index += 1; } let remote = remote_target.map(|target| RemoteLaunch { target, keybindings, live_handoff, }); if remote.is_none() && keybindings_seen { return Err("--remote-keybindings requires --remote".to_string()); } if remote.is_none() && live_handoff { cleaned.push("--handoff".to_string()); } Ok((cleaned, remote)) } #[cfg(windows)] fn validate_remote_target(target: &str) -> Result<&str, String> { if target.is_empty() { return Err("missing value for --remote".to_string()); } if target.starts_with('-') { return Err("--remote target must not start with '-'".to_string()); } Ok(target) } #[cfg(windows)] pub(crate) fn run_remote(_remote: RemoteLaunch) -> std::io::Result<()> { debug_assert!(!crate::platform::capabilities().remote_attach); Err(std::io::Error::other( "remote mode is not supported on Windows yet", )) } #[cfg(windows)] pub(crate) fn run_remote_client_bridge() -> std::io::Result<()> { debug_assert!(!crate::platform::capabilities().remote_attach); Err(std::io::Error::other( "remote client bridge is not supported on Windows yet", )) } pub(crate) fn print_remote_error_hint(err: &std::io::Error, target: &str) { if is_remote_auth_error(err) { eprintln!( "hint: verify SSH access first with `{}`.", ssh_check_command(target) ); eprintln!( "hint: if your SSH key has a passphrase, load it into ssh-agent with `ssh-add` before running `herdr --remote`." ); } } fn is_remote_auth_error(err: &std::io::Error) -> bool { let message = err.to_string(); message.contains("Permission denied") && (message.contains("(publickey") || message.contains("(keyboard-interactive") || message.contains("(password")) } fn ssh_check_command(target: &str) -> String { format!("ssh {}", shell_quote(target)) } fn shell_quote(value: &str) -> String { if !value.is_empty() && value.chars().all(|ch| { ch.is_ascii_alphanumeric() || matches!( ch, '@' | '%' | '_' | '+' | '=' | ':' | ',' | '.' | '/' | '-' ) }) { return value.to_string(); } format!("'{}'", value.replace('\'', "'\\''")) } #[cfg(test)] mod tests { use super::*; #[test] fn remote_auth_error_matches_ssh_auth_denied() { let err = std::io::Error::other( "remote platform detection failed: user@host: Permission denied (publickey).", ); assert!(is_remote_auth_error(&err)); } #[test] fn remote_auth_error_matches_keyboard_interactive_denied() { let err = std::io::Error::other( "remote server status failed: user@host: Permission denied (keyboard-interactive).", ); assert!(is_remote_auth_error(&err)); } #[test] fn remote_auth_error_ignores_non_auth_errors() { let err = std::io::Error::other("remote platform detection failed: unsupported platform"); assert!(!is_remote_auth_error(&err)); } #[test] fn ssh_check_command_quotes_remote_target() { assert_eq!(ssh_check_command("host name"), "ssh 'host name'"); } }