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}