From 17cc1f613110f64050ffa35f093b0d143f89c6e5 Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:18:31 -0400 Subject: [PATCH] Parse bracketed IPv6 file endpoints --- src/bin/dosh-client.rs | 48 +++++++++++++++++++++++++-------------- src/file_transfer.rs | 51 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 5ade700..22c34cc 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -9600,23 +9600,24 @@ mod tests { input_matches_escape, is_local_status_target, is_resume_response_for_client, latest_release_download_url, load_first_native_identity_with_prompt, native_proxy_udp_warning, newest_client_trace_path_from, parse_dynamic_forward, - parse_escape_key, parse_local_forward, parse_remote_forward, parse_ssh_config, - parse_trace_line, parse_trace_options, parse_trace_report_options, parse_trace_summary, - parse_update_options, post_submit_hold_duration, queue_pending_user_input, - queue_stale_pending_user_input, raw_contains_host_table, recv_response_until, - refresh_live_addr, release_tag_download_url, release_tag_from_effective_url, - release_version_from_tag, render_status_clear, render_status_overlay, requested_env, - resolved_startup_command, retire_stream_state, retransmit_stream_opens, - rewrite_forward_command, sanitize_trace_name, selected_predict_mode, selected_udp_host, - server_version_mismatch, should_flush_terminal_input_after_contact, - should_health_log_client_start, should_hold_during_startup_gate, - should_hold_post_submit_input, should_reconnect_before_input_for_local_sleep, - should_repaint_idle_terminal, should_strip_unowned_terminal_reports, - split_after_command_submit, split_trace_tokens, ssh_command_target, ssh_config_uses_proxy, - ssh_destination_host, ssh_username, ssh_with_user, startup_command, status_ssh_target, - strip_stale_mouse_reports, strip_terminal_focus_reports, strip_unowned_terminal_reports, - summarize_trace_file, summarize_trace_file_with_mode, terminal_private_mode_transition, - toml_bare_key_or_quoted, top_trace_events, trace_report_warnings, unix_update_script, + parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path, + parse_ssh_config, parse_trace_line, parse_trace_options, parse_trace_report_options, + parse_trace_summary, parse_update_options, post_submit_hold_duration, + queue_pending_user_input, queue_stale_pending_user_input, raw_contains_host_table, + recv_response_until, refresh_live_addr, release_tag_download_url, + release_tag_from_effective_url, release_version_from_tag, render_status_clear, + render_status_overlay, requested_env, resolved_startup_command, retire_stream_state, + retransmit_stream_opens, rewrite_forward_command, sanitize_trace_name, + selected_predict_mode, selected_udp_host, server_version_mismatch, + should_flush_terminal_input_after_contact, should_health_log_client_start, + should_hold_during_startup_gate, should_hold_post_submit_input, + should_reconnect_before_input_for_local_sleep, should_repaint_idle_terminal, + should_strip_unowned_terminal_reports, split_after_command_submit, split_trace_tokens, + ssh_command_target, ssh_config_uses_proxy, ssh_destination_host, ssh_username, + ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports, + strip_terminal_focus_reports, strip_unowned_terminal_reports, summarize_trace_file, + summarize_trace_file_with_mode, terminal_private_mode_transition, toml_bare_key_or_quoted, + top_trace_events, trace_report_warnings, unix_update_script, update_binary_version_for_installer, update_installer_url, update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias, wake_repaint_retry_deadline, windows_update_script, @@ -11709,6 +11710,19 @@ mod tests { assert!(valid_forward_host("server.example.com")); } + #[test] + fn single_remote_path_accepts_bracketed_ipv6_hosts() { + assert_eq!( + parse_single_remote_path("[2001:db8::1]:/tmp/file", "cat").unwrap(), + ("2001:db8::1".to_string(), "/tmp/file".to_string()) + ); + assert_eq!( + parse_single_remote_path("[::1]:", "ls").unwrap(), + ("::1".to_string(), ".".to_string()) + ); + assert!(parse_single_remote_path("[::1]", "ls").is_err()); + } + #[test] fn recover_removes_only_matching_cached_credentials() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/file_transfer.rs b/src/file_transfer.rs index 53b8020..3453049 100644 --- a/src/file_transfer.rs +++ b/src/file_transfer.rs @@ -165,6 +165,12 @@ pub fn parse_copy_endpoint(raw: &str) -> CopyEndpoint { if looks_like_windows_path(raw) { return CopyEndpoint::Local(PathBuf::from(raw)); } + if let Some((host, path)) = parse_bracketed_remote_endpoint(raw) { + return CopyEndpoint::Remote { host, path }; + } + if raw.starts_with('[') { + return CopyEndpoint::Local(PathBuf::from(raw)); + } let Some(index) = raw.find(':') else { return CopyEndpoint::Local(PathBuf::from(raw)); }; @@ -183,6 +189,25 @@ pub fn parse_copy_endpoint(raw: &str) -> CopyEndpoint { } } +fn parse_bracketed_remote_endpoint(raw: &str) -> Option<(String, String)> { + let rest = raw.strip_prefix('[')?; + let close = rest.find(']')?; + let host = &rest[..close]; + let suffix = &rest[close + 1..]; + if host.is_empty() || host.contains('/') || host.contains('\\') || !suffix.starts_with(':') { + return None; + } + let path = &suffix[1..]; + Some(( + host.to_string(), + if path.is_empty() { + ".".to_string() + } else { + path.to_string() + }, + )) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum CopyEndpoint { Local(PathBuf), @@ -277,4 +302,30 @@ mod tests { CopyEndpoint::Local(PathBuf::from("C:\\Users\\palav\\x")) ); } + + #[test] + fn copy_endpoint_parses_bracketed_ipv6_remote_paths() { + assert_eq!( + parse_copy_endpoint("[2001:db8::1]:/var/log/syslog"), + CopyEndpoint::Remote { + host: "2001:db8::1".to_string(), + path: "/var/log/syslog".to_string() + } + ); + assert_eq!( + parse_copy_endpoint("[::1]:"), + CopyEndpoint::Remote { + host: "::1".to_string(), + path: ".".to_string() + } + ); + assert_eq!( + parse_copy_endpoint("[bad/host]:path"), + CopyEndpoint::Local(PathBuf::from("[bad/host]:path")) + ); + assert_eq!( + parse_copy_endpoint("[::1]"), + CopyEndpoint::Local(PathBuf::from("[::1]")) + ); + } }