Coverage Report

Created: 2025-02-25 06:39

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