url/
path_segments.rs

1// Copyright 2016 The rust-url developers.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use crate::parser::{self, to_u32, SchemeType};
10use crate::Url;
11use alloc::string::String;
12use core::str;
13
14/// Exposes methods to manipulate the path of an URL that is not cannot-be-base.
15///
16/// The path always starts with a `/` slash, and is made of slash-separated segments.
17/// There is always at least one segment (which may be the empty string).
18///
19/// Examples:
20///
21/// ```rust
22/// use url::Url;
23///
24/// # #[cfg(feature = "std")]
25/// # use std::error::Error;
26/// # #[cfg(not(feature = "std"))]
27/// # use core::error::Error;
28///
29/// # fn run() -> Result<(), Box<dyn Error>> {
30/// let mut url = Url::parse("mailto:me@example.com")?;
31/// assert!(url.path_segments_mut().is_err());
32///
33/// let mut url = Url::parse("http://example.net/foo/index.html")?;
34/// url.path_segments_mut().map_err(|_| "cannot be base")?
35///     .pop().push("img").push("2/100%.png");
36/// assert_eq!(url.as_str(), "http://example.net/foo/img/2%2F100%25.png");
37/// # Ok(())
38/// # }
39/// # run().unwrap();
40/// ```
41#[derive(Debug)]
42pub struct PathSegmentsMut<'a> {
43    url: &'a mut Url,
44    after_first_slash: usize,
45    after_path: String,
46    old_after_path_position: u32,
47}
48
49// Not re-exported outside the crate
50pub fn new(url: &mut Url) -> PathSegmentsMut<'_> {
51    let after_path = url.take_after_path();
52    let old_after_path_position = to_u32(url.serialization.len()).unwrap();
53    // Special urls always have a non empty path
54    if SchemeType::from(url.scheme()).is_special() {
55        debug_assert!(url.byte_at(url.path_start) == b'/');
56    } else {
57        debug_assert!(
58            url.serialization.len() == url.path_start as usize
59                || url.byte_at(url.path_start) == b'/'
60        );
61    }
62    PathSegmentsMut {
63        after_first_slash: url.path_start as usize + "/".len(),
64        url,
65        old_after_path_position,
66        after_path,
67    }
68}
69
70impl<'a> Drop for PathSegmentsMut<'a> {
71    fn drop(&mut self) {
72        self.url
73            .restore_after_path(self.old_after_path_position, &self.after_path)
74    }
75}
76
77impl<'a> PathSegmentsMut<'a> {
78    /// Remove all segments in the path, leaving the minimal `url.path() == "/"`.
79    ///
80    /// Returns `&mut Self` so that method calls can be chained.
81    ///
82    /// Example:
83    ///
84    /// ```rust
85    /// use url::Url;
86    ///
87    /// # #[cfg(feature = "std")]
88    /// # use std::error::Error;
89    /// # #[cfg(not(feature = "std"))]
90    /// # use core::error::Error;
91    ///
92    /// # fn run() -> Result<(), Box<dyn Error>> {
93    /// let mut url = Url::parse("https://github.com/servo/rust-url/")?;
94    /// url.path_segments_mut().map_err(|_| "cannot be base")?
95    ///     .clear().push("logout");
96    /// assert_eq!(url.as_str(), "https://github.com/logout");
97    /// # Ok(())
98    /// # }
99    /// # run().unwrap();
100    /// ```
101    pub fn clear(&mut self) -> &mut Self {
102        self.url.serialization.truncate(self.after_first_slash);
103        self
104    }
105
106    /// Remove the last segment of this URL’s path if it is empty,
107    /// except if these was only one segment to begin with.
108    ///
109    /// In other words, remove one path trailing slash, if any,
110    /// unless it is also the initial slash (so this does nothing if `url.path() == "/")`.
111    ///
112    /// Returns `&mut Self` so that method calls can be chained.
113    ///
114    /// Example:
115    ///
116    /// ```rust
117    /// use url::Url;
118    ///
119    /// # #[cfg(feature = "std")]
120    /// # use std::error::Error;
121    /// # #[cfg(not(feature = "std"))]
122    /// # use core::error::Error;
123    ///
124    /// # fn run() -> Result<(), Box<dyn Error>> {
125    /// let mut url = Url::parse("https://github.com/servo/rust-url/")?;
126    /// url.path_segments_mut().map_err(|_| "cannot be base")?
127    ///     .push("pulls");
128    /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url//pulls");
129    ///
130    /// let mut url = Url::parse("https://github.com/servo/rust-url/")?;
131    /// url.path_segments_mut().map_err(|_| "cannot be base")?
132    ///     .pop_if_empty().push("pulls");
133    /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url/pulls");
134    /// # Ok(())
135    /// # }
136    /// # run().unwrap();
137    /// ```
138    pub fn pop_if_empty(&mut self) -> &mut Self {
139        if self.after_first_slash >= self.url.serialization.len() {
140            return self;
141        }
142        if self.url.serialization[self.after_first_slash..].ends_with('/') {
143            self.url.serialization.pop();
144        }
145        self
146    }
147
148    /// Remove the last segment of this URL’s path.
149    ///
150    /// If the path only has one segment, make it empty such that `url.path() == "/"`.
151    ///
152    /// Returns `&mut Self` so that method calls can be chained.
153    pub fn pop(&mut self) -> &mut Self {
154        if self.after_first_slash >= self.url.serialization.len() {
155            return self;
156        }
157        let last_slash = self.url.serialization[self.after_first_slash..]
158            .rfind('/')
159            .unwrap_or(0);
160        self.url
161            .serialization
162            .truncate(self.after_first_slash + last_slash);
163        self
164    }
165
166    /// Append the given segment at the end of this URL’s path.
167    ///
168    /// See the documentation for `.extend()`.
169    ///
170    /// Returns `&mut Self` so that method calls can be chained.
171    pub fn push(&mut self, segment: &str) -> &mut Self {
172        self.extend(Some(segment))
173    }
174
175    /// Append each segment from the given iterator at the end of this URL’s path.
176    ///
177    /// Each segment is percent-encoded like in `Url::parse` or `Url::join`,
178    /// except that `%` and `/` characters are also encoded (to `%25` and `%2F`).
179    /// This is unlike `Url::parse` where `%` is left as-is in case some of the input
180    /// is already percent-encoded, and `/` denotes a path segment separator.)
181    ///
182    /// Note that, in addition to slashes between new segments,
183    /// this always adds a slash between the existing path and the new segments
184    /// *except* if the existing path is `"/"`.
185    /// If the previous last segment was empty (if the path had a trailing slash)
186    /// the path after `.extend()` will contain two consecutive slashes.
187    /// If that is undesired, call `.pop_if_empty()` first.
188    ///
189    /// To obtain a behavior similar to `Url::join`, call `.pop()` unconditionally first.
190    ///
191    /// Returns `&mut Self` so that method calls can be chained.
192    ///
193    /// Example:
194    ///
195    /// ```rust
196    /// use url::Url;
197    ///
198    /// # #[cfg(feature = "std")]
199    /// # use std::error::Error;
200    /// # #[cfg(not(feature = "std"))]
201    /// # use core::error::Error;
202    ///
203    /// # fn run() -> Result<(), Box<dyn Error>> {
204    /// let mut url = Url::parse("https://github.com/")?;
205    /// let org = "servo";
206    /// let repo = "rust-url";
207    /// let issue_number = "188";
208    /// url.path_segments_mut().map_err(|_| "cannot be base")?
209    ///     .extend(&[org, repo, "issues", issue_number]);
210    /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url/issues/188");
211    /// # Ok(())
212    /// # }
213    /// # run().unwrap();
214    /// ```
215    ///
216    /// In order to make sure that parsing the serialization of an URL gives the same URL,
217    /// a segment is ignored if it is `"."` or `".."`:
218    ///
219    /// ```rust
220    /// use url::Url;
221    ///
222    /// # #[cfg(feature = "std")]
223    /// # use std::error::Error;
224    /// # #[cfg(not(feature = "std"))]
225    /// # use core::error::Error;
226    ///
227    /// # fn run() -> Result<(), Box<dyn Error>> {
228    /// let mut url = Url::parse("https://github.com/servo")?;
229    /// url.path_segments_mut().map_err(|_| "cannot be base")?
230    ///     .extend(&["..", "rust-url", ".", "pulls"]);
231    /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url/pulls");
232    /// # Ok(())
233    /// # }
234    /// # run().unwrap();
235    /// ```
236    pub fn extend<I>(&mut self, segments: I) -> &mut Self
237    where
238        I: IntoIterator,
239        I::Item: AsRef<str>,
240    {
241        let scheme_type = SchemeType::from(self.url.scheme());
242        let path_start = self.url.path_start as usize;
243        self.url.mutate(|parser| {
244            parser.context = parser::Context::PathSegmentSetter;
245            for segment in segments {
246                let segment = segment.as_ref();
247                if matches!(segment, "." | "..") {
248                    continue;
249                }
250                if parser.serialization.len() > path_start + 1
251                    // Non special url's path might still be empty
252                    || parser.serialization.len() == path_start
253                {
254                    parser.serialization.push('/');
255                }
256                let mut has_host = true; // FIXME account for this?
257                parser.parse_path(
258                    scheme_type,
259                    &mut has_host,
260                    path_start,
261                    parser::Input::new_no_trim(segment),
262                );
263            }
264        });
265        self
266    }
267}