diff --git a/url/src/lib.rs b/url/src/lib.rs index fd75d477a..8db0fc72f 100644 --- a/url/src/lib.rs +++ b/url/src/lib.rs @@ -182,6 +182,7 @@ pub struct Url { host_end: u32, host: HostInternal, port: Option, + port_int: Option, path_start: u32, // Before initial '/', if any query_start: Option, // Before '?', unlike Position::QueryStart fragment_start: Option, // Before '#', unlike Position::FragmentStart @@ -502,10 +503,9 @@ impl Url { } else { assert_eq!(self.byte_at(self.host_end), b':'); let port_str = self.slice(self.host_end + 1..self.path_start); - assert_eq!( - self.port, - Some(port_str.parse::().expect("Couldn't parse port?")) - ); + let port = port_str.parse::().expect("Couldn't parse port?"); + assert_eq!(self.port, Some(port)); + assert_eq!(self.port_int, Some(port)); } assert!( self.path_start as usize == self.serialization.len() @@ -518,6 +518,7 @@ impl Url { assert_eq!(self.host_end, self.scheme_end + 1); assert_eq!(self.host, HostInternal::None); assert_eq!(self.port, None); + assert_eq!(self.port_int, None); assert_eq!(self.path_start, self.scheme_end + 1); } if let Some(start) = self.query_start { @@ -545,6 +546,7 @@ impl Url { (self.host_str(), other.host_str()) == (None, Some("")) ); assert_eq!(self.port, other.port); + assert!(self.port == None || self.port_int == other.port_int); assert_eq!(self.path_start, other.path_start); assert_eq!(self.query_start, other.query_start); assert_eq!(self.fragment_start, other.fragment_start); @@ -915,8 +917,9 @@ impl Url { /// Return the port number for this URL, if any. /// - /// Note that default port numbers are never reflected by the serialization, - /// use the `port_or_known_default()` method if you want a default port number returned. + /// Note that default port numbers are never reflected in `port()`, + /// use the `port_or_known_default()` method if you want a default port number returned + /// or `port_int()` to get the port number regardless of defaults. /// /// # Examples /// @@ -942,6 +945,34 @@ impl Url { self.port } + /// Return the port number for this URL, if any. + /// + /// Note that this always returns the port number regardless of defaults. + /// + /// # Examples + /// + /// ``` + /// use url::Url; + /// # use url::ParseError; + /// + /// # fn run() -> Result<(), ParseError> { + /// let url = Url::parse("https://example.com")?; + /// assert_eq!(url.port_int(), None); + /// + /// let url = Url::parse("https://example.com:443/")?; + /// assert_eq!(url.port_int(), Some(443)); + /// + /// let url = Url::parse("ssh://example.com:22")?; + /// assert_eq!(url.port_int(), Some(22)); + /// # Ok(()) + /// # } + /// # run().unwrap(); + /// ``` + #[inline] + pub fn port_int(&self) -> Option { + self.port_int + } + /// Return the port number for this URL, or the default port number if it is known. /// /// This method only knows the default port number @@ -1460,8 +1491,6 @@ impl Url { /// Change this URL’s port number. /// - /// Note that default port numbers are not reflected in the serialization. - /// /// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; /// do nothing and return `Err`. /// @@ -1484,7 +1513,7 @@ impl Url { /// # run().unwrap(); /// ``` /// - /// Known default port numbers are not reflected: + /// Known default port numbers are not reflected in `port()` but in `port_int()`: /// /// ```rust /// use url::Url; @@ -1495,6 +1524,7 @@ impl Url { /// /// url.set_port(Some(443)).map_err(|_| "cannot be base")?; /// assert!(url.port().is_none()); + /// assert_eq!(url.port_int(), Some(443)); /// # Ok(()) /// # } /// # run().unwrap(); @@ -1524,14 +1554,15 @@ impl Url { if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { return Err(()); } + let port_int = port; if port.is_some() && port == parser::default_port(self.scheme()) { port = None } - self.set_port_internal(port); + self.set_port_internal(port, port_int); Ok(()) } - fn set_port_internal(&mut self, port: Option) { + fn set_port_internal(&mut self, port: Option, port_int: Option) { match (self.port, port) { (None, None) => {} (Some(_), None) => { @@ -1568,6 +1599,7 @@ impl Url { } } self.port = port; + self.port_int = port_int; } /// Change this URL’s host. @@ -1710,7 +1742,11 @@ impl Url { } /// opt_new_port: None means leave unchanged, Some(None) means remove any port number. - fn set_host_internal(&mut self, host: Host, opt_new_port: Option>) { + fn set_host_internal( + &mut self, + host: Host, + opt_new_port: Option<(Option, Option)>, + ) { let old_suffix_pos = if opt_new_port.is_some() { self.path_start } else { @@ -1730,8 +1766,9 @@ impl Url { self.host_end = to_u32(self.serialization.len()).unwrap(); self.host = host.into(); - if let Some(new_port) = opt_new_port { + if let Some((new_port, new_port_int)) = opt_new_port { self.port = new_port; + self.port_int = new_port_int; if let Some(port) = new_port { write!(&mut self.serialization, ":{}", port).unwrap(); } @@ -2177,6 +2214,7 @@ impl Url { host_end, host, port: None, + port_int: None, path_start: host_end, query_start: None, fragment_start: None, @@ -2233,6 +2271,7 @@ impl Url { ref host_end, ref host, ref port, + ref port_int, ref path_start, ref query_start, ref fragment_start, @@ -2245,6 +2284,7 @@ impl Url { host_end, host, port, + port_int, path_start, query_start, fragment_start, @@ -2273,6 +2313,7 @@ impl Url { host_end, host, port, + port_int, path_start, query_start, fragment_start, @@ -2285,6 +2326,7 @@ impl Url { host_end, host, port, + port_int, path_start, query_start, fragment_start, diff --git a/url/src/parser.rs b/url/src/parser.rs index 57be11052..ee696948e 100644 --- a/url/src/parser.rs +++ b/url/src/parser.rs @@ -485,6 +485,7 @@ impl<'a> Parser<'a> { let host_end = path_start; let host = HostInternal::None; let port = None; + let port_int = None; let remaining = if let Some(input) = input.split_prefix('/') { let path_start = self.serialization.len(); self.serialization.push('/'); @@ -500,6 +501,7 @@ impl<'a> Parser<'a> { host_end, host, port, + port_int, path_start, remaining, ) @@ -555,6 +557,7 @@ impl<'a> Parser<'a> { host_end, host, port: None, + port_int: None, path_start: host_end, query_start, fragment_start, @@ -606,6 +609,7 @@ impl<'a> Parser<'a> { host_end, host, port: None, + port_int: None, path_start: host_end, query_start, fragment_start, @@ -666,6 +670,7 @@ impl<'a> Parser<'a> { base_url.host_end, base_url.host, base_url.port, + base_url.port_int, base_url.path_start, remaining, ) @@ -686,6 +691,7 @@ impl<'a> Parser<'a> { host_end: path_start, host: HostInternal::None, port: None, + port_int: None, path_start, query_start, fragment_start, @@ -709,6 +715,7 @@ impl<'a> Parser<'a> { host_end: path_start, host: HostInternal::None, port: None, + port_int: None, path_start, query_start, fragment_start, @@ -792,6 +799,7 @@ impl<'a> Parser<'a> { base_url.host_end, base_url.host, base_url.port, + base_url.port_int, base_url.path_start, remaining, ) @@ -830,6 +838,7 @@ impl<'a> Parser<'a> { base_url.host_end, base_url.host, base_url.port, + base_url.port_int, base_url.path_start, remaining, ) @@ -851,7 +860,7 @@ impl<'a> Parser<'a> { let has_authority = before_authority != self.serialization.len(); // host state let host_start = to_u32(self.serialization.len())?; - let (host_end, host, port, remaining) = + let (host_end, host, port, port_int, remaining) = self.parse_host_and_port(remaining, scheme_end, scheme_type)?; if host == HostInternal::None && has_authority { return Err(ParseError::EmptyHost); @@ -867,6 +876,7 @@ impl<'a> Parser<'a> { host_end, host, port, + port_int, path_start, remaining, ) @@ -947,12 +957,13 @@ impl<'a> Parser<'a> { Ok((username_end, remaining)) } + #[allow(clippy::type_complexity)] fn parse_host_and_port<'i>( &mut self, input: Input<'i>, scheme_end: u32, scheme_type: SchemeType, - ) -> ParseResult<(u32, HostInternal, Option, Input<'i>)> { + ) -> ParseResult<(u32, HostInternal, Option, Option, Input<'i>)> { let (host, remaining) = Parser::parse_host(input, scheme_type)?; write!(&mut self.serialization, "{}", host).unwrap(); let host_end = to_u32(self.serialization.len())?; @@ -968,16 +979,16 @@ impl<'a> Parser<'a> { } }; - let (port, remaining) = if let Some(remaining) = remaining.split_prefix(':') { + let (port, port_int, remaining) = if let Some(remaining) = remaining.split_prefix(':') { let scheme = || default_port(&self.serialization[..scheme_end as usize]); Parser::parse_port(remaining, scheme, self.context)? } else { - (None, remaining) + (None, None, remaining) }; if let Some(port) = port { write!(&mut self.serialization, ":{}", port).unwrap() } - Ok((host_end, host.into(), port, remaining)) + Ok((host_end, host.into(), port, port_int, remaining)) } pub fn parse_host( @@ -1109,7 +1120,7 @@ impl<'a> Parser<'a> { mut input: Input<'_>, default_port: P, context: Context, - ) -> ParseResult<(Option, Input<'_>)> + ) -> ParseResult<(Option, Option, Input<'_>)> where P: Fn() -> Option, { @@ -1130,10 +1141,16 @@ impl<'a> Parser<'a> { input = remaining; } let mut opt_port = Some(port as u16); - if !has_any_digit || opt_port == default_port() { + let mut opt_port_int = opt_port; + if has_any_digit { + if opt_port == default_port() { + opt_port = None; + } + } else { opt_port = None; + opt_port_int = None; } - Ok((opt_port, input)) + Ok((opt_port, opt_port_int, input)) } pub fn parse_path_start<'i>( @@ -1368,6 +1385,7 @@ impl<'a> Parser<'a> { host_end: u32, host: HostInternal, port: Option, + port_int: Option, path_start: u32, remaining: Input<'_>, ) -> ParseResult { @@ -1381,6 +1399,7 @@ impl<'a> Parser<'a> { host_end, host, port, + port_int, path_start, query_start, fragment_start, diff --git a/url/src/quirks.rs b/url/src/quirks.rs index 0dbc6eb44..728dd280c 100644 --- a/url/src/quirks.rs +++ b/url/src/quirks.rs @@ -128,7 +128,7 @@ pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> { } else { Parser::parse_port(remaining, || default_port(scheme), Context::Setter) .ok() - .map(|(port, _remaining)| port) + .map(|(port, port_int, _remaining)| (port, port_int)) } } else { None @@ -141,7 +141,7 @@ pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> { if host == Host::Domain("".to_string()) { if !username(&url).is_empty() { return Err(()); - } else if let Some(Some(_)) = opt_port { + } else if let Some((_, Some(_))) = opt_port { return Err(()); } else if url.port().is_some() { return Err(()); @@ -215,8 +215,8 @@ pub fn set_port(url: &mut Url, new_port: &str) -> Result<(), ()> { Context::Setter, ) } - if let Ok((new_port, _remaining)) = result { - url.set_port_internal(new_port); + if let Ok((new_port, new_port_int, _remaining)) = result { + url.set_port_internal(new_port, new_port_int); Ok(()) } else { Err(()) diff --git a/url/tests/unit.rs b/url/tests/unit.rs index 1b745ac77..9dea9d284 100644 --- a/url/tests/unit.rs +++ b/url/tests/unit.rs @@ -845,3 +845,26 @@ fn pop_if_empty_in_bounds() { segments.pop_if_empty(); segments.pop(); } + +#[test] +fn port_int() { + let url = Url::parse("http://example.net:80").unwrap(); + assert_eq!(url.port(), None); + assert_eq!(url.port_or_known_default(), Some(80)); + assert_eq!(url.port_int(), Some(80)); + + let url = Url::parse("http://example.net").unwrap(); + assert_eq!(url.port(), None); + assert_eq!(url.port_or_known_default(), Some(80)); + assert_eq!(url.port_int(), None); + + let url = Url::parse("custom-protocol://example.net").unwrap(); + assert_eq!(url.port(), None); + assert_eq!(url.port_or_known_default(), None); + assert_eq!(url.port_int(), None); + + let url = Url::parse("custom-protocol://example.net:8000").unwrap(); + assert_eq!(url.port(), Some(8000)); + assert_eq!(url.port_or_known_default(), Some(8000)); + assert_eq!(url.port_int(), Some(8000)); +}