diff --git a/crates/utils/src/net.rs b/crates/utils/src/net.rs index fa13b79ec..a94f14b4c 100644 --- a/crates/utils/src/net.rs +++ b/crates/utils/src/net.rs @@ -110,9 +110,35 @@ pub fn reset_dns_resolver() { /// helper for validating if the provided arg is an ip address. pub fn is_socket_addr(addr: &str) -> bool { - // TODO IPv6 zone information? + addr.parse::().is_ok() || addr.parse::().is_ok() || is_ipv6_addr_with_zone(addr) +} - addr.parse::().is_ok() || addr.parse::().is_ok() +fn is_ipv6_addr_with_zone(addr: &str) -> bool { + let Some(zone_start) = addr.find('%') else { + return false; + }; + + if addr.starts_with('[') { + let Some(end_bracket) = addr[zone_start..].find(']').map(|pos| zone_start + pos) else { + return false; + }; + let zone = &addr[zone_start + 1..end_bracket]; + return zone_start > 1 + && is_valid_ipv6_zone(zone) + && addr[end_bracket..].starts_with("]:") + && addr[1..zone_start].parse::().is_ok() + && addr[end_bracket + 2..].parse::().is_ok(); + } + + let zone = &addr[zone_start + 1..]; + zone_start > 0 && is_valid_ipv6_zone(zone) && addr[..zone_start].parse::().is_ok() +} + +fn is_valid_ipv6_zone(zone: &str) -> bool { + !zone.is_empty() + && zone + .bytes() + .all(|ch| matches!(ch, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')) } /// checks if server_addr is valid and local host. @@ -708,4 +734,13 @@ mod test { }; assert_eq!(host_zero_port.to_string(), "example.com:0"); } + + #[test] + fn test_is_socket_addr_accepts_ipv6_zone_identifier() { + assert!(is_socket_addr("fe80::1%en0")); + assert!(is_socket_addr("[fe80::1%en0]:9000")); + assert!(!is_socket_addr("fe80::1%en0:9000")); + assert!(!is_socket_addr("fe80::1%en0 ")); + assert!(!is_socket_addr("fe80::1%\t")); + } } diff --git a/crates/utils/src/string.rs b/crates/utils/src/string.rs index c6589bf12..bf1440d80 100644 --- a/crates/utils/src/string.rs +++ b/crates/utils/src/string.rs @@ -236,12 +236,13 @@ pub fn match_as_pattern_prefix(pattern: &str, text: &str) -> bool { text.len() <= pattern.len() } -static ELLIPSES_RE: LazyLock = LazyLock::new(|| Regex::new(r"(.*)(\{[0-9a-z]*\.\.\.[0-9a-z]*\})(.*)").unwrap()); +static ELLIPSES_RE: LazyLock = LazyLock::new(|| Regex::new(r"(.*)(\{[0-9A-Fa-f]*\.\.\.[0-9A-Fa-f]*\})(.*)").unwrap()); /// Ellipses constants const OPEN_BRACES: &str = "{"; const CLOSE_BRACES: &str = "}"; const ELLIPSES: &str = "..."; +const MAX_ELLIPSES_RANGE_SIZE: usize = 10_000; /// ellipses pattern, describes the range and also the /// associated prefix and suffixes. @@ -358,7 +359,7 @@ pub fn find_ellipses_patterns(arg: &str) -> Result { Some(caps) => caps, None => { return Err(Error::other(format!( - "Invalid ellipsis format in ({arg}), Ellipsis range must be provided in format {{N...M}} where N and M are positive integers, M must be greater than N, with an allowed minimum range of 4" + "Invalid ellipsis format in ({arg}), Ellipsis range must be provided in format {{N...M}} where N and M are decimal or hexadecimal positive integers, M must be greater than N, with a maximum expanded range size of {MAX_ELLIPSES_RANGE_SIZE}" ))); } }; @@ -397,7 +398,7 @@ pub fn find_ellipses_patterns(arg: &str) -> Result { || p.suffix.contains(CLOSE_BRACES) { return Err(Error::other(format!( - "Invalid ellipsis format in ({arg}), Ellipsis range must be provided in format {{N...M}} where N and M are positive integers, M must be greater than N, with an allowed minimum range of 4" + "Invalid ellipsis format in ({arg}), Ellipsis range must be provided in format {{N...M}} where N and M are decimal or hexadecimal positive integers, M must be greater than N, with a maximum expanded range size of {MAX_ELLIPSES_RANGE_SIZE}" ))); } } @@ -432,6 +433,7 @@ pub fn has_ellipses>(s: &[T]) -> bool { /// example: /// {1...64} /// {33...64} +/// {0a...0f} /// /// # Arguments /// * `pattern` - A string slice representing the ellipses range pattern @@ -468,17 +470,37 @@ pub fn parse_ellipses_range(pattern: &str) -> Result> { return Err(Error::other("Invalid argument")); } - // TODO: Add support for hexadecimals. - let start = ellipses_range[0].parse::().map_err(Error::other)?; - let end = ellipses_range[1].parse::().map_err(Error::other)?; + let is_hex_range = ellipses_range + .iter() + .any(|value| value.bytes().any(|ch| matches!(ch, b'a'..=b'f' | b'A'..=b'F'))); + let is_upper_hex_range = ellipses_range + .iter() + .any(|value| value.bytes().any(|ch| matches!(ch, b'A'..=b'F'))); + let radix = if is_hex_range { 16 } else { 10 }; + let start = usize::from_str_radix(ellipses_range[0], radix).map_err(Error::other)?; + let end = usize::from_str_radix(ellipses_range[1], radix).map_err(Error::other)?; if start > end { return Err(Error::other("Invalid argument:range start cannot be bigger than end")); } - let mut ret: Vec = Vec::with_capacity(end - start + 1); + let range_size = end + .checked_sub(start) + .and_then(|size| size.checked_add(1)) + .ok_or_else(|| Error::other("Invalid argument:range size overflow"))?; + if range_size > MAX_ELLIPSES_RANGE_SIZE { + return Err(Error::other("Invalid argument:range is too large")); + } + + let mut ret: Vec = Vec::with_capacity(range_size); for i in start..=end { - if ellipses_range[0].starts_with('0') && ellipses_range[0].len() > 1 { + if is_hex_range { + if is_upper_hex_range { + ret.push(format!("{i:0width$X}", width = ellipses_range[1].len())); + } else { + ret.push(format!("{i:0width$x}", width = ellipses_range[1].len())); + } + } else if ellipses_range[0].starts_with('0') && ellipses_range[0].len() > 1 { ret.push(format!("{:0width$}", i, width = ellipses_range[1].len())); } else { ret.push(format!("{i}")); @@ -846,6 +868,18 @@ mod tests { vec!["036"], ], }, + TestCase { + num: 22, + pattern: "{0a...0f}", + success: true, + want: vec![vec!["0a"], vec!["0b"], vec!["0c"], vec!["0d"], vec!["0e"], vec!["0f"]], + }, + TestCase { + num: 23, + pattern: "{0A...0F}", + success: true, + want: vec![vec!["0A"], vec!["0B"], vec!["0C"], vec!["0D"], vec!["0E"], vec!["0F"]], + }, ]; for test_case in test_cases { @@ -872,6 +906,18 @@ mod tests { } } + #[test] + fn test_find_ellipses_patterns_error_mentions_hex_ranges() { + let err = find_ellipses_patterns("{1..64}").unwrap_err(); + assert!(err.to_string().contains("decimal or hexadecimal"), "unexpected error message: {err}"); + } + + #[test] + fn test_parse_ellipses_range_rejects_oversized_ranges() { + let err = parse_ellipses_range("{0...10000}").unwrap_err(); + assert!(err.to_string().contains("range is too large"), "unexpected error message: {err}"); + } + #[test] #[cfg(target_os = "windows")] fn test_strings_has_prefix_fold_handles_unicode_without_panicking() {