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