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 parser::{self, SchemeType, to_u32};
10 use std::str;
11 use Url;
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<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) -> PathSegmentsMut45 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     debug_assert!(url.byte_at(url.path_start) == b'/');
49     PathSegmentsMut {
50         after_first_slash: url.path_start as usize + "/".len(),
51         url: url,
52         old_after_path_position: old_after_path_position,
53         after_path: after_path,
54     }
55 }
56 
57 impl<'a> Drop for PathSegmentsMut<'a> {
drop(&mut self)58     fn drop(&mut self) {
59         self.url.restore_after_path(self.old_after_path_position, &self.after_path)
60     }
61 }
62 
63 impl<'a> PathSegmentsMut<'a> {
64     /// Remove all segments in the path, leaving the minimal `url.path() == "/"`.
65     ///
66     /// Returns `&mut Self` so that method calls can be chained.
67     ///
68     /// Example:
69     ///
70     /// ```rust
71     /// use url::Url;
72     /// # use std::error::Error;
73     ///
74     /// # fn run() -> Result<(), Box<Error>> {
75     /// let mut url = Url::parse("https://github.com/servo/rust-url/")?;
76     /// url.path_segments_mut().map_err(|_| "cannot be base")?
77     ///     .clear().push("logout");
78     /// assert_eq!(url.as_str(), "https://github.com/logout");
79     /// # Ok(())
80     /// # }
81     /// # run().unwrap();
82     /// ```
clear(&mut self) -> &mut Self83     pub fn clear(&mut self) -> &mut Self {
84         self.url.serialization.truncate(self.after_first_slash);
85         self
86     }
87 
88     /// Remove the last segment of this URL’s path if it is empty,
89     /// except if these was only one segment to begin with.
90     ///
91     /// In other words, remove one path trailing slash, if any,
92     /// unless it is also the initial slash (so this does nothing if `url.path() == "/")`.
93     ///
94     /// Returns `&mut Self` so that method calls can be chained.
95     ///
96     /// Example:
97     ///
98     /// ```rust
99     /// use url::Url;
100     /// # use std::error::Error;
101     ///
102     /// # fn run() -> Result<(), Box<Error>> {
103     /// let mut url = Url::parse("https://github.com/servo/rust-url/")?;
104     /// url.path_segments_mut().map_err(|_| "cannot be base")?
105     ///     .push("pulls");
106     /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url//pulls");
107     ///
108     /// let mut url = Url::parse("https://github.com/servo/rust-url/")?;
109     /// url.path_segments_mut().map_err(|_| "cannot be base")?
110     ///     .pop_if_empty().push("pulls");
111     /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url/pulls");
112     /// # Ok(())
113     /// # }
114     /// # run().unwrap();
115     /// ```
pop_if_empty(&mut self) -> &mut Self116     pub fn pop_if_empty(&mut self) -> &mut Self {
117         if self.url.serialization[self.after_first_slash..].ends_with('/') {
118             self.url.serialization.pop();
119         }
120         self
121     }
122 
123     /// Remove the last segment of this URL’s path.
124     ///
125     /// If the path only has one segment, make it empty such that `url.path() == "/"`.
126     ///
127     /// Returns `&mut Self` so that method calls can be chained.
pop(&mut self) -> &mut Self128     pub fn pop(&mut self) -> &mut Self {
129         let last_slash = self.url.serialization[self.after_first_slash..].rfind('/').unwrap_or(0);
130         self.url.serialization.truncate(self.after_first_slash + last_slash);
131         self
132     }
133 
134     /// Append the given segment at the end of this URL’s path.
135     ///
136     /// See the documentation for `.extend()`.
137     ///
138     /// Returns `&mut Self` so that method calls can be chained.
push(&mut self, segment: &str) -> &mut Self139     pub fn push(&mut self, segment: &str) -> &mut Self {
140         self.extend(Some(segment))
141     }
142 
143     /// Append each segment from the given iterator at the end of this URL’s path.
144     ///
145     /// Each segment is percent-encoded like in `Url::parse` or `Url::join`,
146     /// except that `%` and `/` characters are also encoded (to `%25` and `%2F`).
147     /// This is unlike `Url::parse` where `%` is left as-is in case some of the input
148     /// is already percent-encoded, and `/` denotes a path segment separator.)
149     ///
150     /// Note that, in addition to slashes between new segments,
151     /// this always adds a slash between the existing path and the new segments
152     /// *except* if the existing path is `"/"`.
153     /// If the previous last segment was empty (if the path had a trailing slash)
154     /// the path after `.extend()` will contain two consecutive slashes.
155     /// If that is undesired, call `.pop_if_empty()` first.
156     ///
157     /// To obtain a behavior similar to `Url::join`, call `.pop()` unconditionally first.
158     ///
159     /// Returns `&mut Self` so that method calls can be chained.
160     ///
161     /// Example:
162     ///
163     /// ```rust
164     /// use url::Url;
165     /// # use std::error::Error;
166     ///
167     /// # fn run() -> Result<(), Box<Error>> {
168     /// let mut url = Url::parse("https://github.com/")?;
169     /// let org = "servo";
170     /// let repo = "rust-url";
171     /// let issue_number = "188";
172     /// url.path_segments_mut().map_err(|_| "cannot be base")?
173     ///     .extend(&[org, repo, "issues", issue_number]);
174     /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url/issues/188");
175     /// # Ok(())
176     /// # }
177     /// # run().unwrap();
178     /// ```
179     ///
180     /// In order to make sure that parsing the serialization of an URL gives the same URL,
181     /// a segment is ignored if it is `"."` or `".."`:
182     ///
183     /// ```rust
184     /// use url::Url;
185     /// # use std::error::Error;
186     ///
187     /// # fn run() -> Result<(), Box<Error>> {
188     /// let mut url = Url::parse("https://github.com/servo")?;
189     /// url.path_segments_mut().map_err(|_| "cannot be base")?
190     ///     .extend(&["..", "rust-url", ".", "pulls"]);
191     /// assert_eq!(url.as_str(), "https://github.com/servo/rust-url/pulls");
192     /// # Ok(())
193     /// # }
194     /// # run().unwrap();
195     /// ```
extend<I>(&mut self, segments: I) -> &mut Self where I: IntoIterator, I::Item: AsRef<str>196     pub fn extend<I>(&mut self, segments: I) -> &mut Self
197     where I: IntoIterator, I::Item: AsRef<str> {
198         let scheme_type = SchemeType::from(self.url.scheme());
199         let path_start = self.url.path_start as usize;
200         self.url.mutate(|parser| {
201             parser.context = parser::Context::PathSegmentSetter;
202             for segment in segments {
203                 let segment = segment.as_ref();
204                 if matches!(segment, "." | "..") {
205                     continue
206                 }
207                 if parser.serialization.len() > path_start + 1 {
208                     parser.serialization.push('/');
209                 }
210                 let mut has_host = true;  // FIXME account for this?
211                 parser.parse_path(scheme_type, &mut has_host, path_start,
212                                   parser::Input::new(segment));
213             }
214         });
215         self
216     }
217 }
218