Replace url crate with minimal custom parser in gix-url
Unified · Split
| @@ -2510,7 +2510,6 @@ dependencies = [ · 2509 unmodified lines | |||
| 2510 | "percent-encoding", | 2510 | "percent-encoding", |
| 2511 | "serde", | 2511 | "serde", |
| 2512 | "thiserror 2.0.17", | 2512 | "thiserror 2.0.17", |
| 2513 | "url", | ||
| 2514 | ] | 2513 | ] |
| 2515 | 2514 | ||
| 2516 | [[package]] | 2515 | [[package]] |
| @@ -101,7 +101,12 @@ mod mutate { · 100 unmodified lines | |||
| 101 | self.username = url.user().map(ToOwned::to_owned); | 101 | self.username = url.user().map(ToOwned::to_owned); |
| 102 | self.password = url.password().map(ToOwned::to_owned); | 102 | self.password = url.password().map(ToOwned::to_owned); |
| 103 | self.host = url.host().map(ToOwned::to_owned).map(|mut host| { | 103 | self.host = url.host().map(ToOwned::to_owned).map(|mut host| { |
| 104 | if let Some(port) = url.port { | 104 | let port = url.port.filter(|port| { |
| 105 | url.scheme | ||
| 106 | .default_port() | ||
| 107 | .is_none_or(|default_port| *port != default_port) | ||
| 108 | }); | ||
| 109 | if let Some(port) = port { | ||
| 105 | use std::fmt::Write; | 110 | use std::fmt::Write; |
| 106 | write!(host, ":{port}").expect("infallible"); | 111 | write!(host, ":{port}").expect("infallible"); |
| 107 | } | 112 | } |
| @@ -24,7 +24,6 @@ gix-path = { version = "^0.10.21", path = "../gix-path" } · 23 unmodified lines | |||
| 24 | 24 | ||
| 25 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } | 25 | serde = { version = "1.0.114", optional = true, default-features = false, features = ["std", "derive"] } |
| 26 | thiserror = "2.0.17" | 26 | thiserror = "2.0.17" |
| 27 | url = "2.5.2" | ||
| 28 | bstr = { version = "1.12.0", default-features = false, features = ["std"] } | 27 | bstr = { version = "1.12.0", default-features = false, features = ["std"] } |
| 29 | percent-encoding = "2.3.1" | 28 | percent-encoding = "2.3.1" |
| 30 | 29 | ||
| @@ -22,6 +22,9 @@ mod impls; · 21 unmodified lines | |||
| 22 | /// | 22 | /// |
| 23 | pub mod parse; | 23 | pub mod parse; |
| 24 | 24 | ||
| 25 | /// Minimal URL parser to replace the `url` crate dependency | ||
| 26 | mod simple_url; | ||
| 27 | |||
| 25 | /// Parse the given `bytes` as a [git url](Url). | 28 | /// Parse the given `bytes` as a [git url](Url). |
| 26 | /// | 29 | /// |
| 27 | /// # Note | 30 | /// # Note |
| @@ -19,7 +19,7 @@ pub enum Error { · 18 unmodified lines | |||
| 19 | Url { | 19 | Url { |
| 20 | url: String, | 20 | url: String, |
| 21 | kind: UrlKind, | 21 | kind: UrlKind, |
| 22 | source: url::ParseError, | 22 | source: crate::simple_url::UrlParseError, |
| 23 | }, | 23 | }, |
| 24 | 24 | ||
| 25 | #[error("The host portion of the following URL is too long ({} bytes, {len} bytes total): {truncated_url:?}", truncated_url.len())] | 25 | #[error("The host portion of the following URL is too long ({} bytes, {len} bytes total): {truncated_url:?}", truncated_url.len())] |
| @@ -99,30 +99,33 @@ pub(crate) fn url(input: &BStr, protocol_end: usize) -> Result<crate::Url, Error · 73 unmodified lines | |||
| 99 | }); | 99 | }); |
| 100 | } | 100 | } |
| 101 | let (input, url) = input_to_utf8_and_url(input, UrlKind::Url)?; | 101 | let (input, url) = input_to_utf8_and_url(input, UrlKind::Url)?; |
| 102 | let scheme = url.scheme().into(); | 102 | let scheme = Scheme::from(url.scheme.as_str()); |
| 103 | 103 | ||
| 104 | if matches!(scheme, Scheme::Git | Scheme::Ssh) && url.path().is_empty() { | 104 | if matches!(scheme, Scheme::Git | Scheme::Ssh) && url.path.is_empty() { |
| 105 | return Err(Error::MissingRepositoryPath { | 105 | return Err(Error::MissingRepositoryPath { |
| 106 | url: input.into(), | 106 | url: input.into(), |
| 107 | kind: UrlKind::Url, | 107 | kind: UrlKind::Url, |
| 108 | }); | 108 | }); |
| 109 | } | 109 | } |
| 110 | 110 | ||
| 111 | if url.cannot_be_a_base() { | 111 | // Normalize empty path to "/" for http/https URLs only |
| 112 | return Err(Error::RelativeUrl { url: input.to_owned() }); | 112 | let path = if url.path.is_empty() && matches!(scheme, Scheme::Http | Scheme::Https) { |
| 113 | } | 113 | "/".into() |
| 114 | } else { | ||
| 115 | url.path.into() | ||
| 116 | }; | ||
| 114 | 117 | ||
| 115 | Ok(crate::Url { | 118 | Ok(crate::Url { |
| 116 | serialize_alternative_form: false, | 119 | serialize_alternative_form: false, |
| 117 | scheme, | 120 | scheme, |
| 118 | user: url_user(&url, UrlKind::Url)?, | 121 | user: url_user(&url, UrlKind::Url)?, |
| 119 | password: url | 122 | password: url |
| 120 | .password() | 123 | .password |
| 121 | .map(|s| percent_decoded_utf8(s, UrlKind::Url)) | 124 | .map(|s| percent_decoded_utf8(s, UrlKind::Url)) |
| 122 | .transpose()?, | 125 | .transpose()?, |
| 123 | host: url.host_str().map(Into::into), | 126 | host: url.host, |
| 124 | port: url.port(), | 127 | port: url.port, |
| 125 | path: url.path().into(), | 128 | path, |
| 126 | }) | 129 | }) |
| 127 | } | 130 | } |
| 128 | 131 | ||
| @@ -156,7 +159,8 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { · 27 unmodified lines | |||
| 156 | // should never differ in any other way (ssh URLs should not contain a query or fragment part). | 159 | // should never differ in any other way (ssh URLs should not contain a query or fragment part). |
| 157 | // To avoid the various off-by-one errors caused by the `/` characters, we keep using the path | 160 | // To avoid the various off-by-one errors caused by the `/` characters, we keep using the path |
| 158 | // determined above and can therefore skip parsing it here as well. | 161 | // determined above and can therefore skip parsing it here as well. |
| 159 | let url = url::Url::parse(&format!("ssh://{host}")).map_err(|source| Error::Url { | 162 | let url_string = format!("ssh://{host}"); |
| 163 | let url = crate::simple_url::ParsedUrl::parse(&url_string).map_err(|source| Error::Url { | ||
| 160 | url: input.to_owned(), | 164 | url: input.to_owned(), |
| 161 | kind: UrlKind::Scp, | 165 | kind: UrlKind::Scp, |
| 162 | source, | 166 | source, |
| @@ -164,23 +168,23 @@ pub(crate) fn scp(input: &BStr, colon: usize) -> Result<crate::Url, Error> { · 1 unmodified line | |||
| 164 | 168 | ||
| 165 | Ok(crate::Url { | 169 | Ok(crate::Url { |
| 166 | serialize_alternative_form: true, | 170 | serialize_alternative_form: true, |
| 167 | scheme: url.scheme().into(), | 171 | scheme: Scheme::from(url.scheme.as_str()), |
| 168 | user: url_user(&url, UrlKind::Scp)?, | 172 | user: url_user(&url, UrlKind::Scp)?, |
| 169 | password: url | 173 | password: url |
| 170 | .password() | 174 | .password |
| 171 | .map(|s| percent_decoded_utf8(s, UrlKind::Scp)) | 175 | .map(|s| percent_decoded_utf8(s, UrlKind::Scp)) |
| 172 | .transpose()?, | 176 | .transpose()?, |
| 173 | host: url.host_str().map(Into::into), | 177 | host: url.host, |
| 174 | port: url.port(), | 178 | port: url.port, |
| 175 | path: path.into(), | 179 | path: path.into(), |
| 176 | }) | 180 | }) |
| 177 | } | 181 | } |
| 178 | 182 | ||
| 179 | fn url_user(url: &url::Url, kind: UrlKind) -> Result<Option<String>, Error> { | 183 | fn url_user(url: &crate::simple_url::ParsedUrl<'_>, kind: UrlKind) -> Result<Option<String>, Error> { |
| 180 | if url.username().is_empty() && url.password().is_none() { | 184 | if url.username.is_empty() && url.password.is_none() { |
| 181 | Ok(None) | 185 | Ok(None) |
| 182 | } else { | 186 | } else { |
| 183 | Ok(Some(percent_decoded_utf8(url.username(), kind)?)) | 187 | Ok(Some(percent_decoded_utf8(url.username, kind)?)) |
| 184 | } | 188 | } |
| 185 | } | 189 | } |
| 186 | 190 | ||
| @@ -269,13 +273,22 @@ fn input_to_utf8(input: &BStr, kind: UrlKind) -> Result<&str, Error> { · 82 unmodified lines | |||
| 269 | }) | 273 | }) |
| 270 | } | 274 | } |
| 271 | 275 | ||
| 272 | fn input_to_utf8_and_url(input: &BStr, kind: UrlKind) -> Result<(&str, url::Url), Error> { | 276 | fn input_to_utf8_and_url(input: &BStr, kind: UrlKind) -> Result<(&str, crate::simple_url::ParsedUrl<'_>), Error> { |
| 273 | let input = input_to_utf8(input, kind)?; | 277 | let input = input_to_utf8(input, kind)?; |
| 274 | url::Url::parse(input) | 278 | crate::simple_url::ParsedUrl::parse(input) |
| 275 | .map(|url| (input, url)) | 279 | .map(|url| (input, url)) |
| 276 | .map_err(|source| Error::Url { | 280 | .map_err(|source| { |
| 277 | url: input.to_owned(), | 281 | // If the parser rejected it as RelativeUrlWithoutBase, map to Error::RelativeUrl |
| 278 | kind, | 282 | // to match the expected error type for malformed URLs like "invalid:://" |
| 279 | source, | 283 | match source { |
| 284 | crate::simple_url::UrlParseError::RelativeUrlWithoutBase => { | ||
| 285 | Error::RelativeUrl { url: input.to_owned() } | ||
| 286 | } | ||
| 287 | _ => Error::Url { | ||
| 288 | url: input.to_owned(), | ||
| 289 | kind, | ||
| 290 | source, | ||
| 291 | }, | ||
| 292 | } | ||
| 280 | }) | 293 | }) |
| 281 | } | 294 | } |
| @@ -48,6 +48,17 @@ impl Scheme { · 47 unmodified lines | |||
| 48 | Ext(name) => name.as_str(), | 48 | Ext(name) => name.as_str(), |
| 49 | } | 49 | } |
| 50 | } | 50 | } |
| 51 | |||
| 52 | /// Return the default port for this scheme, or `None` if it is not known. | ||
| 53 | pub fn default_port(&self) -> Option<u16> { | ||
| 54 | match self { | ||
| 55 | Scheme::Http => Some(80), | ||
| 56 | Scheme::Https => Some(443), | ||
| 57 | Scheme::Ssh => Some(22), | ||
| 58 | Scheme::Git => Some(9418), | ||
| 59 | _ => None, | ||
| 60 | } | ||
| 61 | } | ||
| 51 | } | 62 | } |
| 52 | 63 | ||
| 53 | impl std::fmt::Display for Scheme { | 64 | impl std::fmt::Display for Scheme { |
| @@ -0,0 +1,250 @@ | |||
| 1 | /// A minimal URL parser that extracts only what we need for git URLs. | ||
| 2 | /// This is a replacement for the `url` crate dependency. | ||
| 3 | #[derive(Debug)] | ||
| 4 | pub(crate) struct ParsedUrl<'a> { | ||
| 5 | pub scheme: String, // Owned to allow normalization to lowercase | ||
| 6 | pub username: &'a str, | ||
| 7 | pub password: Option<&'a str>, | ||
| 8 | pub host: Option<String>, // Owned to allow normalization to lowercase | ||
| 9 | pub port: Option<u16>, | ||
| 10 | pub path: &'a str, | ||
| 11 | } | ||
| 12 | |||
| 13 | /// Minimal parse error type to replace url::ParseError | ||
| 14 | #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] | ||
| 15 | #[allow(missing_docs)] | ||
| 16 | pub enum UrlParseError { | ||
| 17 | #[error("relative URL without a base")] | ||
| 18 | RelativeUrlWithoutBase, | ||
| 19 | #[error("invalid port number - must be between 1-65535")] | ||
| 20 | InvalidPort, | ||
| 21 | #[error("invalid domain character")] | ||
| 22 | InvalidDomainCharacter, | ||
| 23 | #[error("Scheme requires host")] | ||
| 24 | SchemeRequiresHost, | ||
| 25 | } | ||
| 26 | |||
| 27 | /// Check if a character is valid in a URL scheme. | ||
| 28 | /// Valid scheme characters: alphanumeric, +, -, or . | ||
| 29 | fn is_valid_scheme_char(c: char) -> bool { | ||
| 30 | c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.' | ||
| 31 | } | ||
| 32 | |||
| 33 | impl<'a> ParsedUrl<'a> { | ||
| 34 | /// Parse a URL string into its components. | ||
| 35 | /// Expected format: scheme://[user[:password]@]host[:port]/path | ||
| 36 | pub(crate) fn parse(input: &'a str) -> Result<Self, UrlParseError> { | ||
| 37 | // Find scheme by looking for first ':' | ||
| 38 | let first_colon = input.find(':').ok_or(UrlParseError::RelativeUrlWithoutBase)?; | ||
| 39 | let scheme_str = &input[..first_colon]; | ||
| 40 | // Normalize scheme to lowercase for case-insensitive matching (matches url crate behavior) | ||
| 41 | let scheme = scheme_str.to_ascii_lowercase(); | ||
| 42 | let Some(after_scheme) = input[first_colon..].strip_prefix("://") else { | ||
| 43 | return Err(UrlParseError::RelativeUrlWithoutBase); | ||
| 44 | }; | ||
| 45 | |||
| 46 | // Check for relative URL (scheme without proper authority) | ||
| 47 | if scheme_str.is_empty() { | ||
| 48 | return Err(UrlParseError::RelativeUrlWithoutBase); | ||
| 49 | } | ||
| 50 | |||
| 51 | // Validate scheme characters (check original before lowercase conversion) | ||
| 52 | if !scheme_str.chars().all(is_valid_scheme_char) { | ||
| 53 | return Err(UrlParseError::RelativeUrlWithoutBase); | ||
| 54 | } | ||
| 55 | |||
| 56 | // Find path start (first '/' after scheme) | ||
| 57 | let path_start = after_scheme.find('/').unwrap_or(after_scheme.len()); | ||
| 58 | let authority = &after_scheme[..path_start]; | ||
| 59 | let path = if path_start < after_scheme.len() { | ||
| 60 | &after_scheme[path_start..] | ||
| 61 | } else { | ||
| 62 | // No path specified - leave empty (caller can default to / if needed) | ||
| 63 | "" | ||
| 64 | }; | ||
| 65 | |||
| 66 | // Parse authority: [user[:password]@]host[:port] | ||
| 67 | let (username, password, host, port) = if let Some(at_pos) = authority.rfind('@') { | ||
| 68 | // Has user info | ||
| 69 | let user_info = &authority[..at_pos]; | ||
| 70 | let host_port = &authority[at_pos + 1..]; | ||
| 71 | |||
| 72 | let (user, pass) = if let Some(colon_pos) = user_info.find(':') { | ||
| 73 | let pass_str = &user_info[colon_pos + 1..]; | ||
| 74 | // Treat empty password as None | ||
| 75 | let pass = if pass_str.is_empty() { None } else { Some(pass_str) }; | ||
| 76 | (&user_info[..colon_pos], pass) | ||
| 77 | } else { | ||
| 78 | (user_info, None) | ||
| 79 | }; | ||
| 80 | |||
| 81 | let (h, p) = Self::parse_host_port(host_port)?; | ||
| 82 | // If we have user info, we must have a host | ||
| 83 | if h.is_none() { | ||
| 84 | return Err(UrlParseError::InvalidDomainCharacter); | ||
| 85 | } | ||
| 86 | (user, pass, h, p) | ||
| 87 | } else { | ||
| 88 | // No user info | ||
| 89 | let (h, p) = Self::parse_host_port(authority)?; | ||
| 90 | ("", None, h, p) | ||
| 91 | }; | ||
| 92 | |||
| 93 | // Standard schemes (http, https, git, ssh) require a host | ||
| 94 | // Scheme is already lowercase at this point | ||
| 95 | let requires_host = matches!(scheme.as_str(), "http" | "https" | "git" | "ssh" | "ftp" | "ftps"); | ||
| 96 | if requires_host && host.is_none() { | ||
| 97 | return Err(UrlParseError::SchemeRequiresHost); | ||
| 98 | } | ||
| 99 | |||
| 100 | Ok(ParsedUrl { | ||
| 101 | scheme, | ||
| 102 | username, | ||
| 103 | password, | ||
| 104 | host, | ||
| 105 | port, | ||
| 106 | path, | ||
| 107 | }) | ||
| 108 | } | ||
| 109 | |||
| 110 | fn parse_host_port(host_port: &str) -> Result<(Option<String>, Option<u16>), UrlParseError> { | ||
| 111 | if host_port.is_empty() { | ||
| 112 | return Ok((None, None)); | ||
| 113 | } | ||
| 114 | |||
| 115 | // Handle IPv6 addresses: [::1] or [::1]:port | ||
| 116 | if host_port.starts_with('[') { | ||
| 117 | if let Some(bracket_end) = host_port.find(']') { | ||
| 118 | // IPv6 addresses are case-insensitive, normalize to lowercase | ||
| 119 | let host = Some(host_port[..=bracket_end].to_ascii_lowercase()); | ||
| 120 | let remaining = &host_port[bracket_end + 1..]; | ||
| 121 | |||
| 122 | if remaining.is_empty() { | ||
| 123 | return Ok((host, None)); | ||
| 124 | } else if let Some(port_str) = remaining.strip_prefix(':') { | ||
| 125 | let port = port_str.parse::<u16>().map_err(|_| UrlParseError::InvalidPort)?; | ||
| 126 | // Validate port is in valid range (1-65535, port 0 is invalid) | ||
| 127 | if port == 0 { | ||
| 128 | return Err(UrlParseError::InvalidPort); | ||
| 129 | } | ||
| 130 | return Ok((host, Some(port))); | ||
| 131 | } else { | ||
| 132 | return Err(UrlParseError::InvalidDomainCharacter); | ||
| 133 | } | ||
| 134 | } else { | ||
| 135 | return Err(UrlParseError::InvalidDomainCharacter); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | // Handle regular host:port | ||
| 140 | // Use rfind to handle IPv6 addresses without brackets (edge case) | ||
| 141 | if let Some(colon_pos) = host_port.rfind(':') { | ||
| 142 | // Check if this looks like a port (all digits after colon) | ||
| 143 | let potential_port = &host_port[colon_pos + 1..]; | ||
| 144 | if potential_port.is_empty() { | ||
| 145 | // Empty port like "host:" - strip the trailing colon | ||
| 146 | let host_str = &host_port[..colon_pos]; | ||
| 147 | return Ok((Some(Self::normalize_hostname(host_str)?), None)); | ||
| 148 | } else if potential_port.chars().all(|c| c.is_ascii_digit()) { | ||
| 149 | let host_str = &host_port[..colon_pos]; | ||
| 150 | let host = Self::normalize_hostname(host_str)?; | ||
| 151 | let port = potential_port.parse::<u16>().map_err(|_| UrlParseError::InvalidPort)?; | ||
| 152 | // Validate port is in valid range (1-65535, port 0 is invalid) | ||
| 153 | if port == 0 { | ||
| 154 | return Err(UrlParseError::InvalidPort); | ||
| 155 | } | ||
| 156 | return Ok((Some(host), Some(port))); | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | // No port, just host | ||
| 161 | Ok((Some(Self::normalize_hostname(host_port)?), None)) | ||
| 162 | } | ||
| 163 | |||
| 164 | /// Check if a string looks like a valid DNS hostname (for normalization purposes) | ||
| 165 | /// Valid DNS names contain only alphanumeric, hyphens, dots, underscores, and wildcards | ||
| 166 | fn is_normalizable_hostname(host: &str) -> bool { | ||
| 167 | // Allow alphanumeric, -, ., _, and * (for patterns) | ||
| 168 | host.chars() | ||
| 169 | .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '.' | '_' | '*')) | ||
| 170 | } | ||
| 171 | |||
| 172 | /// Validate and possibly normalize a hostname | ||
| 173 | /// Valid DNS hostnames are normalized to lowercase | ||
| 174 | /// Invalid strings (like injection attempts) are preserved as-is but ? is rejected | ||
| 175 | fn normalize_hostname(host: &str) -> Result<String, UrlParseError> { | ||
| 176 | // Reject ? character which git's url parser rejects | ||
| 177 | if host.contains('?') { | ||
| 178 | return Err(UrlParseError::InvalidDomainCharacter); | ||
| 179 | } | ||
| 180 | |||
| 181 | // Only normalize if it looks like a valid DNS hostname | ||
| 182 | // Preserve case for security checks if it contains special characters | ||
| 183 | if Self::is_normalizable_hostname(host) { | ||
| 184 | Ok(host.to_ascii_lowercase()) | ||
| 185 | } else { | ||
| 186 | Ok(host.to_owned()) | ||
| 187 | } | ||
| 188 | } | ||
| 189 | } | ||
| 190 | |||
| 191 | #[cfg(test)] | ||
| 192 | mod tests { | ||
| 193 | use super::*; | ||
| 194 | |||
| 195 | #[test] | ||
| 196 | fn test_simple_url() { | ||
| 197 | let url = ParsedUrl::parse("http://example.com/path").unwrap(); | ||
| 198 | assert_eq!(url.scheme, "http"); | ||
| 199 | assert_eq!(url.host.as_deref(), Some("example.com")); | ||
| 200 | assert_eq!(url.path, "/path"); | ||
| 201 | assert_eq!(url.username, ""); | ||
| 202 | assert_eq!(url.password, None); | ||
| 203 | assert_eq!(url.port, None); | ||
| 204 | } | ||
| 205 | |||
| 206 | #[test] | ||
| 207 | fn test_url_with_port() { | ||
| 208 | let url = ParsedUrl::parse("http://example.com:8080/path").unwrap(); | ||
| 209 | assert_eq!(url.scheme, "http"); | ||
| 210 | assert_eq!(url.host.as_deref(), Some("example.com")); | ||
| 211 | assert_eq!(url.port, Some(8080)); | ||
| 212 | assert_eq!(url.path, "/path"); | ||
| 213 | } | ||
| 214 | |||
| 215 | #[test] | ||
| 216 | fn test_url_with_user() { | ||
| 217 | let url = ParsedUrl::parse("http://user@example.com/path").unwrap(); | ||
| 218 | assert_eq!(url.scheme, "http"); | ||
| 219 | assert_eq!(url.username, "user"); | ||
| 220 | assert_eq!(url.host.as_deref(), Some("example.com")); | ||
| 221 | assert_eq!(url.path, "/path"); | ||
| 222 | } | ||
| 223 | |||
| 224 | #[test] | ||
| 225 | fn test_url_with_user_and_password() { | ||
| 226 | let url = ParsedUrl::parse("http://user:pass@example.com/path").unwrap(); | ||
| 227 | assert_eq!(url.scheme, "http"); | ||
| 228 | assert_eq!(url.username, "user"); | ||
| 229 | assert_eq!(url.password, Some("pass")); | ||
| 230 | assert_eq!(url.host.as_deref(), Some("example.com")); | ||
| 231 | assert_eq!(url.path, "/path"); | ||
| 232 | } | ||
| 233 | |||
| 234 | #[test] | ||
| 235 | fn test_url_with_ipv6() { | ||
| 236 | let url = ParsedUrl::parse("http://[::1]/path").unwrap(); | ||
| 237 | assert_eq!(url.scheme, "http"); | ||
| 238 | assert_eq!(url.host.as_deref(), Some("[::1]")); | ||
| 239 | assert_eq!(url.path, "/path"); | ||
| 240 | } | ||
| 241 | |||
| 242 | #[test] | ||
| 243 | fn test_url_with_ipv6_and_port() { | ||
| 244 | let url = ParsedUrl::parse("http://[::1]:8080/path").unwrap(); | ||
| 245 | assert_eq!(url.scheme, "http"); | ||
| 246 | assert_eq!(url.host.as_deref(), Some("[::1]")); | ||
| 247 | assert_eq!(url.port, Some(8080)); | ||
| 248 | assert_eq!(url.path, "/path"); | ||
| 249 | } | ||
| 250 | } | ||
| @@ -185,8 +185,8 @@ mod baseline { · 184 unmodified lines | |||
| 185 | 185 | ||
| 186 | pub fn max_num_failures(&self) -> usize { | 186 | pub fn max_num_failures(&self) -> usize { |
| 187 | match self { | 187 | match self { |
| 188 | Kind::Unix => 198, | 188 | Kind::Unix => 195, |
| 189 | Kind::Windows => 198 + 6, | 189 | Kind::Windows => 195 + 6, |
| 190 | } | 190 | } |
| 191 | } | 191 | } |
| 192 | 192 | ||
| @@ -141,13 +141,9 @@ mod windows { · 140 unmodified lines | |||
| 141 | 141 | ||
| 142 | #[test] | 142 | #[test] |
| 143 | fn url_from_absolute_path() -> crate::Result { | 143 | fn url_from_absolute_path() -> crate::Result { |
| 144 | // Test with a Windows path directly instead of using url::Url::from_directory_path | ||
| 144 | assert_url( | 145 | assert_url( |
| 145 | url::Url::from_directory_path(r"c:\users\1") | 146 | r"C:\users\1\", |
| 146 | .expect("valid") | ||
| 147 | .to_file_path() | ||
| 148 | .expect("valid path") | ||
| 149 | .to_string_lossy() | ||
| 150 | .as_ref(), | ||
| 151 | url_alternate(Scheme::File, None, None, None, br"C:\users\1\"), | 147 | url_alternate(Scheme::File, None, None, None, br"C:\users\1\"), |
| 152 | )?; | 148 | )?; |
| 153 | // A special hack to support URLs on windows that are prefixed with `/` even though absolute. | 149 | // A special hack to support URLs on windows that are prefixed with `/` even though absolute. |
| @@ -189,13 +185,9 @@ mod unix { · 35 unmodified lines | |||
| 189 | 185 | ||
| 190 | #[test] | 186 | #[test] |
| 191 | fn url_from_absolute_path() -> crate::Result { | 187 | fn url_from_absolute_path() -> crate::Result { |
| 188 | // Test with a simple file path instead of using url::Url::from_directory_path | ||
| 192 | assert_url_roundtrip( | 189 | assert_url_roundtrip( |
| 193 | url::Url::from_directory_path("/users/foo") | 190 | "/users/foo/", |
| 194 | .expect("valid") | ||
| 195 | .to_file_path() | ||
| 196 | .expect("valid path") | ||
| 197 | .to_string_lossy() | ||
| 198 | .as_ref(), | ||
| 199 | url_alternate(Scheme::File, None, None, None, b"/users/foo/"), | 191 | url_alternate(Scheme::File, None, None, None, b"/users/foo/"), |
| 200 | ) | 192 | ) |
| 201 | } | 193 | } |