diff options
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 166 |
1 files changed, 155 insertions, 11 deletions
@@ -120,7 +120,7 @@ url = { version = "2", features = ["serde"] } ``` */ -#![doc(html_root_url = "https://docs.rs/url/2.2.1")] +#![doc(html_root_url = "https://docs.rs/url/2.2.2")] #[macro_use] extern crate matches; @@ -320,6 +320,8 @@ impl Url { /// Parse a string as an URL, with this URL as the base URL. /// + /// The inverse of this is [`make_relative`]. + /// /// Note: a trailing slash is significant. /// Without it, the last path component is considered to be a “file” name /// to be removed to get at the “directory” that is used as the base: @@ -349,11 +351,144 @@ impl Url { /// with this URL as the base URL, a [`ParseError`] variant will be returned. /// /// [`ParseError`]: enum.ParseError.html + /// [`make_relative`]: #method.make_relative #[inline] pub fn join(&self, input: &str) -> Result<Url, crate::ParseError> { Url::options().base_url(Some(self)).parse(input) } + /// Creates a relative URL if possible, with this URL as the base URL. + /// + /// This is the inverse of [`join`]. + /// + /// # Examples + /// + /// ```rust + /// use url::Url; + /// # use url::ParseError; + /// + /// # fn run() -> Result<(), ParseError> { + /// let base = Url::parse("https://example.net/a/b.html")?; + /// let url = Url::parse("https://example.net/a/c.png")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png")); + /// + /// let base = Url::parse("https://example.net/a/b/")?; + /// let url = Url::parse("https://example.net/a/b/c.png")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("c.png")); + /// + /// let base = Url::parse("https://example.net/a/b/")?; + /// let url = Url::parse("https://example.net/a/d/c.png")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("../d/c.png")); + /// + /// let base = Url::parse("https://example.net/a/b.html?c=d")?; + /// let url = Url::parse("https://example.net/a/b.html?e=f")?; + /// let relative = base.make_relative(&url); + /// assert_eq!(relative.as_ref().map(|s| s.as_str()), Some("?e=f")); + /// # Ok(()) + /// # } + /// # run().unwrap(); + /// ``` + /// + /// # Errors + /// + /// If this URL can't be a base for the given URL, `None` is returned. + /// This is for example the case if the scheme, host or port are not the same. + /// + /// [`join`]: #method.join + pub fn make_relative(&self, url: &Url) -> Option<String> { + if self.cannot_be_a_base() { + return None; + } + + // Scheme, host and port need to be the same + if self.scheme() != url.scheme() || self.host() != url.host() || self.port() != url.port() { + return None; + } + + // We ignore username/password at this point + + // The path has to be transformed + let mut relative = String::new(); + + // Extract the filename of both URIs, these need to be handled separately + fn extract_path_filename(s: &str) -> (&str, &str) { + let last_slash_idx = s.rfind('/').unwrap_or(0); + let (path, filename) = s.split_at(last_slash_idx); + if filename.is_empty() { + (path, "") + } else { + (path, &filename[1..]) + } + } + + let (base_path, base_filename) = extract_path_filename(self.path()); + let (url_path, url_filename) = extract_path_filename(url.path()); + + let mut base_path = base_path.split('/').peekable(); + let mut url_path = url_path.split('/').peekable(); + + // Skip over the common prefix + while base_path.peek().is_some() && base_path.peek() == url_path.peek() { + base_path.next(); + url_path.next(); + } + + // Add `..` segments for the remainder of the base path + for base_path_segment in base_path { + // Skip empty last segments + if base_path_segment.is_empty() { + break; + } + + if !relative.is_empty() { + relative.push('/'); + } + + relative.push_str(".."); + } + + // Append the remainder of the other URI + for url_path_segment in url_path { + if !relative.is_empty() { + relative.push('/'); + } + + relative.push_str(url_path_segment); + } + + // Add the filename if they are not the same + if base_filename != url_filename { + // If the URIs filename is empty this means that it was a directory + // so we'll have to append a '/'. + // + // Otherwise append it directly as the new filename. + if url_filename.is_empty() { + relative.push('/'); + } else { + if !relative.is_empty() { + relative.push('/'); + } + relative.push_str(url_filename); + } + } + + // Query and fragment are only taken from the other URI + if let Some(query) = url.query() { + relative.push('?'); + relative.push_str(query); + } + + if let Some(fragment) = url.fragment() { + relative.push('#'); + relative.push_str(fragment); + } + + Some(relative) + } + /// Return a default `ParseOptions` that can fully configure the URL parser. /// /// # Examples @@ -417,14 +552,15 @@ impl Url { /// # fn run() -> Result<(), ParseError> { /// let url_str = "https://example.net/"; /// let url = Url::parse(url_str)?; - /// assert_eq!(url.into_string(), url_str); + /// assert_eq!(String::from(url), url_str); /// # Ok(()) /// # } /// # run().unwrap(); /// ``` #[inline] + #[deprecated(since = "2.3.0", note = "use Into<String>")] pub fn into_string(self) -> String { - self.serialization + self.into() } /// For internal testing, not part of the public API. @@ -1433,7 +1569,7 @@ impl Url { /// Return an object with methods to manipulate this URL’s path segments. /// /// Return `Err(())` if this URL is cannot-be-a-base. - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn path_segments_mut(&mut self) -> Result<PathSegmentsMut<'_>, ()> { if self.cannot_be_a_base() { Err(()) @@ -1517,7 +1653,7 @@ impl Url { /// # } /// # run().unwrap(); /// ``` - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_port(&mut self, mut port: Option<u16>) -> Result<(), ()> { // has_host implies !cannot_be_a_base if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { @@ -1788,7 +1924,7 @@ impl Url { /// # run().unwrap(); /// ``` /// - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_ip_host(&mut self, address: IpAddr) -> Result<(), ()> { if self.cannot_be_a_base() { return Err(()); @@ -1828,7 +1964,7 @@ impl Url { /// # } /// # run().unwrap(); /// ``` - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_password(&mut self, password: Option<&str>) -> Result<(), ()> { // has_host implies !cannot_be_a_base if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { @@ -1921,7 +2057,7 @@ impl Url { /// # } /// # run().unwrap(); /// ``` - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn set_username(&mut self, username: &str) -> Result<(), ()> { // has_host implies !cannot_be_a_base if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { @@ -2163,7 +2299,7 @@ impl Url { /// # } /// ``` #[cfg(any(unix, windows, target_os = "redox"))] - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> { let mut serialization = "file://".to_owned(); let host_start = serialization.len() as u32; @@ -2200,7 +2336,7 @@ impl Url { /// Note that `std::path` does not consider trailing slashes significant /// and usually does not include them (e.g. in `Path::parent()`). #[cfg(any(unix, windows, target_os = "redox"))] - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> { let mut url = Url::from_file_path(path)?; if !url.serialization.ends_with('/') { @@ -2317,7 +2453,7 @@ impl Url { /// for a Windows path, is not UTF-8.) #[inline] #[cfg(any(unix, windows, target_os = "redox"))] - #[allow(clippy::clippy::result_unit_err)] + #[allow(clippy::result_unit_err)] pub fn to_file_path(&self) -> Result<PathBuf, ()> { if let Some(segments) = self.path_segments() { let host = match self.host() { @@ -2375,6 +2511,13 @@ impl fmt::Display for Url { } } +/// String converstion. +impl From<Url> for String { + fn from(value: Url) -> String { + value.serialization + } +} + /// Debug the serialization of this URL. impl fmt::Debug for Url { #[inline] @@ -2382,6 +2525,7 @@ impl fmt::Debug for Url { formatter .debug_struct("Url") .field("scheme", &self.scheme()) + .field("cannot_be_a_base", &self.cannot_be_a_base()) .field("username", &self.username()) .field("password", &self.password()) .field("host", &self.host()) |