Coverage Report

Created: 2025-02-21 07:11

/rust/registry/src/index.crates.io-6f17d22bba15001f/url-2.5.4/src/quirks.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
//! Getters and setters for URL components implemented per <https://url.spec.whatwg.org/#api>
10
//!
11
//! Unless you need to be interoperable with web browsers,
12
//! you probably want to use `Url` method instead.
13
14
use crate::parser::{default_port, Context, Input, Parser, SchemeType};
15
use crate::{Host, ParseError, Position, Url};
16
use alloc::string::String;
17
use alloc::string::ToString;
18
19
/// Internal components / offsets of a URL.
20
///
21
/// https://user@pass:example.com:1234/foo/bar?baz#quux
22
///      |      |    |          | ^^^^|       |   |
23
///      |      |    |          | |   |       |   `----- fragment_start
24
///      |      |    |          | |   |       `--------- query_start
25
///      |      |    |          | |   `----------------- path_start
26
///      |      |    |          | `--------------------- port
27
///      |      |    |          `----------------------- host_end
28
///      |      |    `---------------------------------- host_start
29
///      |      `--------------------------------------- username_end
30
///      `---------------------------------------------- scheme_end
31
#[derive(Copy, Clone)]
32
#[cfg(feature = "expose_internals")]
33
pub struct InternalComponents {
34
    pub scheme_end: u32,
35
    pub username_end: u32,
36
    pub host_start: u32,
37
    pub host_end: u32,
38
    pub port: Option<u16>,
39
    pub path_start: u32,
40
    pub query_start: Option<u32>,
41
    pub fragment_start: Option<u32>,
42
}
43
44
/// Internal component / parsed offsets of the URL.
45
///
46
/// This can be useful for implementing efficient serialization
47
/// for the URL.
48
#[cfg(feature = "expose_internals")]
49
pub fn internal_components(url: &Url) -> InternalComponents {
50
    InternalComponents {
51
        scheme_end: url.scheme_end,
52
        username_end: url.username_end,
53
        host_start: url.host_start,
54
        host_end: url.host_end,
55
        port: url.port,
56
        path_start: url.path_start,
57
        query_start: url.query_start,
58
        fragment_start: url.fragment_start,
59
    }
60
}
61
62
/// <https://url.spec.whatwg.org/#dom-url-domaintoascii>
63
0
pub fn domain_to_ascii(domain: &str) -> String {
64
0
    match Host::parse(domain) {
65
0
        Ok(Host::Domain(domain)) => domain,
66
0
        _ => String::new(),
67
    }
68
0
}
69
70
/// <https://url.spec.whatwg.org/#dom-url-domaintounicode>
71
0
pub fn domain_to_unicode(domain: &str) -> String {
72
0
    match Host::parse(domain) {
73
0
        Ok(Host::Domain(ref domain)) => {
74
0
            let (unicode, _errors) = idna::domain_to_unicode(domain);
75
0
            unicode
76
        }
77
0
        _ => String::new(),
78
    }
79
0
}
80
81
/// Getter for <https://url.spec.whatwg.org/#dom-url-href>
82
0
pub fn href(url: &Url) -> &str {
83
0
    url.as_str()
84
0
}
85
86
/// Setter for <https://url.spec.whatwg.org/#dom-url-href>
87
0
pub fn set_href(url: &mut Url, value: &str) -> Result<(), ParseError> {
88
0
    *url = Url::parse(value)?;
89
0
    Ok(())
90
0
}
91
92
/// Getter for <https://url.spec.whatwg.org/#dom-url-origin>
93
0
pub fn origin(url: &Url) -> String {
94
0
    url.origin().ascii_serialization()
95
0
}
96
97
/// Getter for <https://url.spec.whatwg.org/#dom-url-protocol>
98
#[inline]
99
0
pub fn protocol(url: &Url) -> &str {
100
0
    &url.as_str()[..url.scheme().len() + ":".len()]
101
0
}
102
103
/// Setter for <https://url.spec.whatwg.org/#dom-url-protocol>
104
#[allow(clippy::result_unit_err)]
105
0
pub fn set_protocol(url: &mut Url, mut new_protocol: &str) -> Result<(), ()> {
106
    // The scheme state in the spec ignores everything after the first `:`,
107
    // but `set_scheme` errors if there is more.
108
0
    if let Some(position) = new_protocol.find(':') {
109
0
        new_protocol = &new_protocol[..position];
110
0
    }
111
0
    url.set_scheme(new_protocol)
112
0
}
113
114
/// Getter for <https://url.spec.whatwg.org/#dom-url-username>
115
#[inline]
116
0
pub fn username(url: &Url) -> &str {
117
0
    url.username()
118
0
}
119
120
/// Setter for <https://url.spec.whatwg.org/#dom-url-username>
121
#[allow(clippy::result_unit_err)]
122
0
pub fn set_username(url: &mut Url, new_username: &str) -> Result<(), ()> {
123
0
    url.set_username(new_username)
124
0
}
125
126
/// Getter for <https://url.spec.whatwg.org/#dom-url-password>
127
#[inline]
128
0
pub fn password(url: &Url) -> &str {
129
0
    url.password().unwrap_or("")
130
0
}
131
132
/// Setter for <https://url.spec.whatwg.org/#dom-url-password>
133
#[allow(clippy::result_unit_err)]
134
0
pub fn set_password(url: &mut Url, new_password: &str) -> Result<(), ()> {
135
0
    url.set_password(if new_password.is_empty() {
136
0
        None
137
    } else {
138
0
        Some(new_password)
139
    })
140
0
}
141
142
/// Getter for <https://url.spec.whatwg.org/#dom-url-host>
143
#[inline]
144
0
pub fn host(url: &Url) -> &str {
145
0
    &url[Position::BeforeHost..Position::AfterPort]
146
0
}
147
148
/// Setter for <https://url.spec.whatwg.org/#dom-url-host>
149
#[allow(clippy::result_unit_err)]
150
0
pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> {
151
0
    // If context object’s url’s cannot-be-a-base-URL flag is set, then return.
152
0
    if url.cannot_be_a_base() {
153
0
        return Err(());
154
0
    }
155
0
    // Host parsing rules are strict,
156
0
    // We don't want to trim the input
157
0
    let input = Input::new_no_trim(new_host);
158
0
    let host;
159
0
    let opt_port;
160
0
    {
161
0
        let scheme = url.scheme();
162
0
        let scheme_type = SchemeType::from(scheme);
163
0
        if scheme_type == SchemeType::File && new_host.is_empty() {
164
0
            url.set_host_internal(Host::Domain(String::new()), None);
165
0
            return Ok(());
166
0
        }
167
168
0
        if let Ok((h, remaining)) = Parser::parse_host(input, scheme_type) {
169
0
            host = h;
170
0
            opt_port = if let Some(remaining) = remaining.split_prefix(':') {
171
0
                if remaining.is_empty() {
172
0
                    None
173
                } else {
174
0
                    Parser::parse_port(remaining, || default_port(scheme), Context::Setter)
175
0
                        .ok()
176
0
                        .map(|(port, _remaining)| port)
177
                }
178
            } else {
179
0
                None
180
            };
181
        } else {
182
0
            return Err(());
183
        }
184
    }
185
    // Make sure we won't set an empty host to a url with a username or a port
186
0
    if host == Host::Domain("".to_string())
187
0
        && (!username(url).is_empty() || matches!(opt_port, Some(Some(_))) || url.port().is_some())
188
    {
189
0
        return Err(());
190
0
    }
191
0
    url.set_host_internal(host, opt_port);
192
0
    Ok(())
193
0
}
194
195
/// Getter for <https://url.spec.whatwg.org/#dom-url-hostname>
196
#[inline]
197
0
pub fn hostname(url: &Url) -> &str {
198
0
    url.host_str().unwrap_or("")
199
0
}
200
201
/// Setter for <https://url.spec.whatwg.org/#dom-url-hostname>
202
#[allow(clippy::result_unit_err)]
203
0
pub fn set_hostname(url: &mut Url, new_hostname: &str) -> Result<(), ()> {
204
0
    if url.cannot_be_a_base() {
205
0
        return Err(());
206
0
    }
207
0
    // Host parsing rules are strict we don't want to trim the input
208
0
    let input = Input::new_no_trim(new_hostname);
209
0
    let scheme_type = SchemeType::from(url.scheme());
210
0
    if scheme_type == SchemeType::File && new_hostname.is_empty() {
211
0
        url.set_host_internal(Host::Domain(String::new()), None);
212
0
        return Ok(());
213
0
    }
214
215
0
    if let Ok((host, _remaining)) = Parser::parse_host(input, scheme_type) {
216
0
        if let Host::Domain(h) = &host {
217
0
            if h.is_empty() {
218
                // Empty host on special not file url
219
0
                if SchemeType::from(url.scheme()) == SchemeType::SpecialNotFile
220
                    // Port with an empty host
221
0
                    ||!port(url).is_empty()
222
                    // Empty host that includes credentials
223
0
                    || !url.username().is_empty()
224
0
                    || !url.password().unwrap_or("").is_empty()
225
                {
226
0
                    return Err(());
227
0
                }
228
0
            }
229
0
        }
230
0
        url.set_host_internal(host, None);
231
0
        Ok(())
232
    } else {
233
0
        Err(())
234
    }
235
0
}
236
237
/// Getter for <https://url.spec.whatwg.org/#dom-url-port>
238
#[inline]
239
0
pub fn port(url: &Url) -> &str {
240
0
    &url[Position::BeforePort..Position::AfterPort]
241
0
}
242
243
/// Setter for <https://url.spec.whatwg.org/#dom-url-port>
244
#[allow(clippy::result_unit_err)]
245
0
pub fn set_port(url: &mut Url, new_port: &str) -> Result<(), ()> {
246
0
    let result;
247
0
    {
248
0
        // has_host implies !cannot_be_a_base
249
0
        let scheme = url.scheme();
250
0
        if !url.has_host() || url.host() == Some(Host::Domain("")) || scheme == "file" {
251
0
            return Err(());
252
0
        }
253
0
        result = Parser::parse_port(
254
0
            Input::new_no_trim(new_port),
255
0
            || default_port(scheme),
256
0
            Context::Setter,
257
0
        )
258
    }
259
0
    if let Ok((new_port, _remaining)) = result {
260
0
        url.set_port_internal(new_port);
261
0
        Ok(())
262
    } else {
263
0
        Err(())
264
    }
265
0
}
266
267
/// Getter for <https://url.spec.whatwg.org/#dom-url-pathname>
268
#[inline]
269
0
pub fn pathname(url: &Url) -> &str {
270
0
    url.path()
271
0
}
272
273
/// Setter for <https://url.spec.whatwg.org/#dom-url-pathname>
274
0
pub fn set_pathname(url: &mut Url, new_pathname: &str) {
275
0
    if url.cannot_be_a_base() {
276
0
        return;
277
0
    }
278
0
    if new_pathname.starts_with('/')
279
0
        || (SchemeType::from(url.scheme()).is_special()
280
            // \ is a segment delimiter for 'special' URLs"
281
0
            && new_pathname.starts_with('\\'))
282
    {
283
0
        url.set_path(new_pathname)
284
0
    } else if SchemeType::from(url.scheme()).is_special()
285
0
        || !new_pathname.is_empty()
286
0
        || !url.has_host()
287
    {
288
0
        let mut path_to_set = String::from("/");
289
0
        path_to_set.push_str(new_pathname);
290
0
        url.set_path(&path_to_set)
291
    } else {
292
0
        url.set_path(new_pathname)
293
    }
294
0
}
295
296
/// Getter for <https://url.spec.whatwg.org/#dom-url-search>
297
0
pub fn search(url: &Url) -> &str {
298
0
    trim(&url[Position::AfterPath..Position::AfterQuery])
299
0
}
300
301
/// Setter for <https://url.spec.whatwg.org/#dom-url-search>
302
0
pub fn set_search(url: &mut Url, new_search: &str) {
303
0
    url.set_query(match new_search {
304
0
        "" => None,
305
0
        _ if new_search.starts_with('?') => Some(&new_search[1..]),
306
0
        _ => Some(new_search),
307
    })
308
0
}
309
310
/// Getter for <https://url.spec.whatwg.org/#dom-url-hash>
311
0
pub fn hash(url: &Url) -> &str {
312
0
    trim(&url[Position::AfterQuery..])
313
0
}
314
315
/// Setter for <https://url.spec.whatwg.org/#dom-url-hash>
316
0
pub fn set_hash(url: &mut Url, new_hash: &str) {
317
0
    url.set_fragment(match new_hash {
318
        // If the given value is the empty string,
319
        // then set context object’s url’s fragment to null and return.
320
0
        "" => None,
321
        // Let input be the given value with a single leading U+0023 (#) removed, if any.
322
0
        _ if new_hash.starts_with('#') => Some(&new_hash[1..]),
323
0
        _ => Some(new_hash),
324
    })
325
0
}
326
327
0
fn trim(s: &str) -> &str {
328
0
    if s.len() == 1 {
329
0
        ""
330
    } else {
331
0
        s
332
    }
333
0
}