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 
9 use crate::parser::{self, to_u32, SchemeType};
10 use crate::Url;
11 use 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)]
37 pub 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
new(url: &mut Url) -> PathSegmentsMut<'_>45 pub 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 
65 impl<'a> Drop for PathSegmentsMut<'a> {
drop(&mut self)66     fn drop(&mut self) {
67         self.url
68             .restore_after_path(self.old_after_path_position, &self.after_path)
69     }
70 }
71 
72 impl<'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     /// ```
clear(&mut self) -> &mut Self92     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     /// ```
pop_if_empty(&mut self) -> &mut Self125     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.
pop(&mut self) -> &mut Self140     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.
push(&mut self, segment: &str) -> &mut Self158     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     /// ```
extend<I>(&mut self, segments: I) -> &mut Self where I: IntoIterator, I::Item: AsRef<str>,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 }
247