From cf6218b25e0134e1b12bdf90d98189d94b18f170 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 27 Oct 2025 17:55:23 +0800 Subject: Add socket address helper for domain resolution --- src/utils/socket_addr_helper.rs | 186 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 src/utils/socket_addr_helper.rs (limited to 'src/utils') diff --git a/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs new file mode 100644 index 0000000..c6805da --- /dev/null +++ b/src/utils/socket_addr_helper.rs @@ -0,0 +1,186 @@ +use std::net::SocketAddr; +use tokio::net::lookup_host; + +/// Helper function to parse a string into a SocketAddr with optional default port +pub async fn get_socket_addr( + address_str: impl AsRef, + default_port: u16, +) -> Result { + let address = address_str.as_ref().trim(); + + // Check if the address contains a port + if let Some((host, port_str)) = parse_host_and_port(address) { + let port = port_str.parse::().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid port number '{}': {}", port_str, e), + ) + })?; + + return resolve_to_socket_addr(&host, port).await; + } + + // No port specified, use default port + resolve_to_socket_addr(address, default_port).await +} + +/// Parse host and port from address string +fn parse_host_and_port(address: &str) -> Option<(&str, &str)> { + if address.starts_with('[') { + if let Some(close_bracket) = address.find(']') { + if close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' { + let host = &address[1..close_bracket]; + let port = &address[close_bracket + 2..]; + return Some((host, port)); + } + } + } + + // Handle IPv4 addresses and hostnames with ports + if let Some(colon_pos) = address.rfind(':') { + // Check if this is not part of an IPv6 address without brackets + if !address.contains('[') && !address.contains(']') { + let host = &address[..colon_pos]; + let port = &address[colon_pos + 1..]; + + // Basic validation to avoid false positives + if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) { + return Some((host, port)); + } + } + } + + None +} + +/// Resolve host to SocketAddr, handling both IP addresses and domain names +async fn resolve_to_socket_addr(host: &str, port: u16) -> Result { + // First try to parse as IP address (IPv4 or IPv6) + if let Ok(ip_addr) = host.parse() { + return Ok(SocketAddr::new(ip_addr, port)); + } + + // If it's not a valid IP address, treat it as a domain name and perform DNS lookup + let lookup_addr = format!("{}:{}", host, port); + let mut addrs = lookup_host(&lookup_addr).await?; + + if let Some(addr) = addrs.next() { + Ok(addr) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Could not resolve host '{}'", host), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_ipv4_with_port() { + let result = get_socket_addr("127.0.0.1:8080", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + assert_eq!(addr.ip().to_string(), "127.0.0.1"); + } + + #[tokio::test] + async fn test_ipv4_without_port() { + let result = get_socket_addr("192.168.1.1", 443).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + assert_eq!(addr.ip().to_string(), "192.168.1.1"); + } + + #[tokio::test] + async fn test_ipv6_with_port() { + let result = get_socket_addr("[::1]:8080", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + assert_eq!(addr.ip().to_string(), "::1"); + } + + #[tokio::test] + async fn test_ipv6_without_port() { + let result = get_socket_addr("[::1]", 443).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + assert_eq!(addr.ip().to_string(), "::1"); + } + + #[tokio::test] + async fn test_invalid_port() { + let result = get_socket_addr("127.0.0.1:99999", 80).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_empty_string() { + let result = get_socket_addr("", 80).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_whitespace_trimming() { + let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + } + + #[tokio::test] + async fn test_domain_name_with_port() { + // This test will only pass if localhost resolves + let result = get_socket_addr("localhost:8080", 80).await; + if result.is_ok() { + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + // localhost should resolve to 127.0.0.1 or ::1 + assert!(addr.ip().is_loopback()); + } + } + + #[tokio::test] + async fn test_domain_name_without_port() { + // This test will only pass if localhost resolves + let result = get_socket_addr("localhost", 443).await; + if result.is_ok() { + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + // localhost should resolve to 127.0.0.1 or ::1 + assert!(addr.ip().is_loopback()); + } + } + + #[tokio::test] + async fn test_parse_host_and_port() { + // IPv4 with port + assert_eq!( + parse_host_and_port("192.168.1.1:8080"), + Some(("192.168.1.1", "8080")) + ); + + // IPv6 with port + assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080"))); + + // Hostname with port + assert_eq!( + parse_host_and_port("example.com:443"), + Some(("example.com", "443")) + ); + + // No port + assert_eq!(parse_host_and_port("192.168.1.1"), None); + assert_eq!(parse_host_and_port("example.com"), None); + + // Invalid cases + assert_eq!(parse_host_and_port(":"), None); + assert_eq!(parse_host_and_port("192.168.1.1:"), None); + } +} -- cgit From ed027187568c91a14c545c1962a219552e4654e7 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 15:27:11 +0800 Subject: Add input utility functions for user confirmation --- src/utils/input.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/utils/input.rs (limited to 'src/utils') diff --git a/src/utils/input.rs b/src/utils/input.rs new file mode 100644 index 0000000..217cede --- /dev/null +++ b/src/utils/input.rs @@ -0,0 +1,52 @@ +/// Confirm the current operation +/// Waits for user input of 'y' or 'n' +pub async fn confirm_hint(text: impl Into) -> bool { + use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader}; + + let prompt = text.into().trim().to_string(); + + let mut stdout = io::stdout(); + let mut stdin = BufReader::new(io::stdin()); + + stdout + .write_all(prompt.as_bytes()) + .await + .expect("Failed to write prompt"); + stdout.flush().await.expect("Failed to flush stdout"); + + let mut input = String::new(); + stdin + .read_line(&mut input) + .await + .expect("Failed to read input"); + + input.trim().eq_ignore_ascii_case("y") +} + +/// Confirm the current operation, or execute a closure if rejected +/// Waits for user input of 'y' or 'n' +/// If 'n' is entered, executes the provided closure and returns false +pub async fn confirm_hint_or(text: impl Into, on_reject: F) -> bool +where + F: FnOnce(), +{ + let confirmed = confirm_hint(text).await; + if !confirmed { + on_reject(); + } + confirmed +} + +/// Confirm the current operation, and execute a closure if confirmed +/// Waits for user input of 'y' or 'n' +/// If 'y' is entered, executes the provided closure and returns true +pub async fn confirm_hint_then(text: impl Into, on_confirm: F) -> bool +where + F: FnOnce(), +{ + let confirmed = confirm_hint(text).await; + if confirmed { + on_confirm(); + } + confirmed +} -- cgit From 0c0499abfb94d57d9b81c63b3df6e7e5e42a570d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 16:23:02 +0800 Subject: Apply clippy suggestion --- src/utils/socket_addr_helper.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/utils') diff --git a/src/utils/socket_addr_helper.rs b/src/utils/socket_addr_helper.rs index c6805da..fd7b346 100644 --- a/src/utils/socket_addr_helper.rs +++ b/src/utils/socket_addr_helper.rs @@ -17,7 +17,7 @@ pub async fn get_socket_addr( ) })?; - return resolve_to_socket_addr(&host, port).await; + return resolve_to_socket_addr(host, port).await; } // No port specified, use default port @@ -26,15 +26,13 @@ pub async fn get_socket_addr( /// Parse host and port from address string fn parse_host_and_port(address: &str) -> Option<(&str, &str)> { - if address.starts_with('[') { - if let Some(close_bracket) = address.find(']') { - if close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' { + if address.starts_with('[') + && let Some(close_bracket) = address.find(']') + && close_bracket + 1 < address.len() && address.as_bytes()[close_bracket + 1] == b':' { let host = &address[1..close_bracket]; let port = &address[close_bracket + 2..]; return Some((host, port)); } - } - } // Handle IPv4 addresses and hostnames with ports if let Some(colon_pos) = address.rfind(':') { -- cgit From 251218ed09d640d7af44f26c6917d8fdb90fc263 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Wed, 29 Oct 2025 16:34:43 +0800 Subject: Add input_with_editor function for text editing This function opens the system editor with default text in a cache file, reads back the modified content after editing, and removes comment lines. --- src/utils/input.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) (limited to 'src/utils') diff --git a/src/utils/input.rs b/src/utils/input.rs index 217cede..a728c77 100644 --- a/src/utils/input.rs +++ b/src/utils/input.rs @@ -1,3 +1,5 @@ +use tokio::{fs, process::Command}; + /// Confirm the current operation /// Waits for user input of 'y' or 'n' pub async fn confirm_hint(text: impl Into) -> bool { @@ -50,3 +52,54 @@ where } confirmed } + +/// Input text using the system editor +/// Opens the system editor (from EDITOR environment variable) with default text in a cache file, +/// then reads back the modified content after the editor closes, removing comment lines +pub async fn input_with_editor( + default_text: impl AsRef, + cache_file: impl AsRef, + comment_char: impl AsRef, +) -> Result { + let cache_path = cache_file.as_ref(); + let default_content = default_text.as_ref(); + let comment_prefix = comment_char.as_ref(); + + // Write default text to cache file + fs::write(cache_path, default_content).await?; + + // Get editor from environment variable + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string()); + + // Open editor with cache file + let status = Command::new(editor).arg(cache_path).status().await?; + + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Editor exited with non-zero status", + )); + } + + // Read the modified content + let content = fs::read_to_string(cache_path).await?; + + // Remove comment lines and trim + let processed_content: String = content + .lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with(comment_prefix) { + None + } else { + Some(line) + } + }) + .collect::>() + .join("\n"); + + // Delete the cache file + let _ = fs::remove_file(cache_path).await; + + Ok(processed_content) +} -- cgit