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