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 | pub fn domain_to_ascii(domain: &str) -> String { |
64 | match Host::parse(input:domain) { |
65 | Ok(Host::Domain(domain: String)) => domain, |
66 | _ => String::new(), |
67 | } |
68 | } |
69 | |
70 | /// <https://url.spec.whatwg.org/#dom-url-domaintounicode> |
71 | pub fn domain_to_unicode(domain: &str) -> String { |
72 | match Host::parse(input:domain) { |
73 | Ok(Host::Domain(ref domain: &String)) => { |
74 | let (unicode: String, _errors: Result<(), Errors>) = idna::domain_to_unicode(domain); |
75 | unicode |
76 | } |
77 | _ => String::new(), |
78 | } |
79 | } |
80 | |
81 | /// Getter for <https://url.spec.whatwg.org/#dom-url-href> |
82 | pub fn href(url: &Url) -> &str { |
83 | url.as_str() |
84 | } |
85 | |
86 | /// Setter for <https://url.spec.whatwg.org/#dom-url-href> |
87 | pub fn set_href(url: &mut Url, value: &str) -> Result<(), ParseError> { |
88 | *url = Url::parse(input:value)?; |
89 | Ok(()) |
90 | } |
91 | |
92 | /// Getter for <https://url.spec.whatwg.org/#dom-url-origin> |
93 | pub fn origin(url: &Url) -> String { |
94 | url.origin().ascii_serialization() |
95 | } |
96 | |
97 | /// Getter for <https://url.spec.whatwg.org/#dom-url-protocol> |
98 | #[inline ] |
99 | pub fn protocol(url: &Url) -> &str { |
100 | &url.as_str()[..url.scheme().len() + ":" .len()] |
101 | } |
102 | |
103 | /// Setter for <https://url.spec.whatwg.org/#dom-url-protocol> |
104 | #[allow (clippy::result_unit_err)] |
105 | 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 | if let Some(position: usize) = new_protocol.find(':' ) { |
109 | new_protocol = &new_protocol[..position]; |
110 | } |
111 | url.set_scheme(new_protocol) |
112 | } |
113 | |
114 | /// Getter for <https://url.spec.whatwg.org/#dom-url-username> |
115 | #[inline ] |
116 | pub fn username(url: &Url) -> &str { |
117 | url.username() |
118 | } |
119 | |
120 | /// Setter for <https://url.spec.whatwg.org/#dom-url-username> |
121 | #[allow (clippy::result_unit_err)] |
122 | pub fn set_username(url: &mut Url, new_username: &str) -> Result<(), ()> { |
123 | url.set_username(new_username) |
124 | } |
125 | |
126 | /// Getter for <https://url.spec.whatwg.org/#dom-url-password> |
127 | #[inline ] |
128 | pub fn password(url: &Url) -> &str { |
129 | url.password().unwrap_or(default:"" ) |
130 | } |
131 | |
132 | /// Setter for <https://url.spec.whatwg.org/#dom-url-password> |
133 | #[allow (clippy::result_unit_err)] |
134 | pub fn set_password(url: &mut Url, new_password: &str) -> Result<(), ()> { |
135 | url.set_password(if new_password.is_empty() { |
136 | None |
137 | } else { |
138 | Some(new_password) |
139 | }) |
140 | } |
141 | |
142 | /// Getter for <https://url.spec.whatwg.org/#dom-url-host> |
143 | #[inline ] |
144 | pub fn host(url: &Url) -> &str { |
145 | &url[Position::BeforeHost..Position::AfterPort] |
146 | } |
147 | |
148 | /// Setter for <https://url.spec.whatwg.org/#dom-url-host> |
149 | #[allow (clippy::result_unit_err)] |
150 | pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> { |
151 | // If context object’s url’s cannot-be-a-base-URL flag is set, then return. |
152 | if url.cannot_be_a_base() { |
153 | return Err(()); |
154 | } |
155 | // Host parsing rules are strict, |
156 | // We don't want to trim the input |
157 | let input = Input::new_no_trim(new_host); |
158 | let host; |
159 | let opt_port; |
160 | { |
161 | let scheme = url.scheme(); |
162 | let scheme_type = SchemeType::from(scheme); |
163 | if scheme_type == SchemeType::File && new_host.is_empty() { |
164 | url.set_host_internal(Host::Domain(String::new()), None); |
165 | return Ok(()); |
166 | } |
167 | |
168 | if let Ok((h, remaining)) = Parser::parse_host(input, scheme_type) { |
169 | host = h; |
170 | opt_port = if let Some(remaining) = remaining.split_prefix(':' ) { |
171 | if remaining.is_empty() { |
172 | None |
173 | } else { |
174 | Parser::parse_port(remaining, || default_port(scheme), Context::Setter) |
175 | .ok() |
176 | .map(|(port, _remaining)| port) |
177 | } |
178 | } else { |
179 | None |
180 | }; |
181 | } else { |
182 | return Err(()); |
183 | } |
184 | } |
185 | // Make sure we won't set an empty host to a url with a username or a port |
186 | if host == Host::Domain("" .to_string()) |
187 | && (!username(url).is_empty() || matches!(opt_port, Some(Some(_))) || url.port().is_some()) |
188 | { |
189 | return Err(()); |
190 | } |
191 | url.set_host_internal(host, opt_port); |
192 | Ok(()) |
193 | } |
194 | |
195 | /// Getter for <https://url.spec.whatwg.org/#dom-url-hostname> |
196 | #[inline ] |
197 | pub fn hostname(url: &Url) -> &str { |
198 | url.host_str().unwrap_or(default:"" ) |
199 | } |
200 | |
201 | /// Setter for <https://url.spec.whatwg.org/#dom-url-hostname> |
202 | #[allow (clippy::result_unit_err)] |
203 | pub fn set_hostname(url: &mut Url, new_hostname: &str) -> Result<(), ()> { |
204 | if url.cannot_be_a_base() { |
205 | return Err(()); |
206 | } |
207 | // Host parsing rules are strict we don't want to trim the input |
208 | let input = Input::new_no_trim(new_hostname); |
209 | let scheme_type = SchemeType::from(url.scheme()); |
210 | if scheme_type == SchemeType::File && new_hostname.is_empty() { |
211 | url.set_host_internal(Host::Domain(String::new()), None); |
212 | return Ok(()); |
213 | } |
214 | |
215 | if let Ok((host, _remaining)) = Parser::parse_host(input, scheme_type) { |
216 | if let Host::Domain(h) = &host { |
217 | if h.is_empty() { |
218 | // Empty host on special not file url |
219 | if SchemeType::from(url.scheme()) == SchemeType::SpecialNotFile |
220 | // Port with an empty host |
221 | ||!port(url).is_empty() |
222 | // Empty host that includes credentials |
223 | || !url.username().is_empty() |
224 | || !url.password().unwrap_or("" ).is_empty() |
225 | { |
226 | return Err(()); |
227 | } |
228 | } |
229 | } |
230 | url.set_host_internal(host, None); |
231 | Ok(()) |
232 | } else { |
233 | Err(()) |
234 | } |
235 | } |
236 | |
237 | /// Getter for <https://url.spec.whatwg.org/#dom-url-port> |
238 | #[inline ] |
239 | pub fn port(url: &Url) -> &str { |
240 | &url[Position::BeforePort..Position::AfterPort] |
241 | } |
242 | |
243 | /// Setter for <https://url.spec.whatwg.org/#dom-url-port> |
244 | #[allow (clippy::result_unit_err)] |
245 | pub fn set_port(url: &mut Url, new_port: &str) -> Result<(), ()> { |
246 | let result: Result<(Option, Input<'_>), …>; |
247 | { |
248 | // has_host implies !cannot_be_a_base |
249 | let scheme: &str = url.scheme(); |
250 | if !url.has_host() || url.host() == Some(Host::Domain("" )) || scheme == "file" { |
251 | return Err(()); |
252 | } |
253 | result = Parser::parse_port( |
254 | Input::new_no_trim(new_port), |
255 | || default_port(scheme), |
256 | Context::Setter, |
257 | ) |
258 | } |
259 | if let Ok((new_port: Option, _remaining: Input<'_>)) = result { |
260 | url.set_port_internal(new_port); |
261 | Ok(()) |
262 | } else { |
263 | Err(()) |
264 | } |
265 | } |
266 | |
267 | /// Getter for <https://url.spec.whatwg.org/#dom-url-pathname> |
268 | #[inline ] |
269 | pub fn pathname(url: &Url) -> &str { |
270 | url.path() |
271 | } |
272 | |
273 | /// Setter for <https://url.spec.whatwg.org/#dom-url-pathname> |
274 | pub fn set_pathname(url: &mut Url, new_pathname: &str) { |
275 | if url.cannot_be_a_base() { |
276 | return; |
277 | } |
278 | if new_pathname.starts_with('/' ) |
279 | || (SchemeType::from(url.scheme()).is_special() |
280 | // \ is a segment delimiter for 'special' URLs" |
281 | && new_pathname.starts_with(' \\' )) |
282 | { |
283 | url.set_path(new_pathname) |
284 | } else if SchemeType::from(url.scheme()).is_special() |
285 | || !new_pathname.is_empty() |
286 | || !url.has_host() |
287 | { |
288 | let mut path_to_set: String = String::from("/" ); |
289 | path_to_set.push_str(string:new_pathname); |
290 | url.set_path(&path_to_set) |
291 | } else { |
292 | url.set_path(new_pathname) |
293 | } |
294 | } |
295 | |
296 | /// Getter for <https://url.spec.whatwg.org/#dom-url-search> |
297 | pub fn search(url: &Url) -> &str { |
298 | trim(&url[Position::AfterPath..Position::AfterQuery]) |
299 | } |
300 | |
301 | /// Setter for <https://url.spec.whatwg.org/#dom-url-search> |
302 | pub fn set_search(url: &mut Url, new_search: &str) { |
303 | url.set_query(match new_search { |
304 | "" => None, |
305 | _ if new_search.starts_with('?' ) => Some(&new_search[1..]), |
306 | _ => Some(new_search), |
307 | }) |
308 | } |
309 | |
310 | /// Getter for <https://url.spec.whatwg.org/#dom-url-hash> |
311 | pub fn hash(url: &Url) -> &str { |
312 | trim(&url[Position::AfterQuery..]) |
313 | } |
314 | |
315 | /// Setter for <https://url.spec.whatwg.org/#dom-url-hash> |
316 | pub fn set_hash(url: &mut Url, new_hash: &str) { |
317 | 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 | "" => None, |
321 | // Let input be the given value with a single leading U+0023 (#) removed, if any. |
322 | _ if new_hash.starts_with('#' ) => Some(&new_hash[1..]), |
323 | _ => Some(new_hash), |
324 | }) |
325 | } |
326 | |
327 | fn trim(s: &str) -> &str { |
328 | if s.len() == 1 { |
329 | "" |
330 | } else { |
331 | s |
332 | } |
333 | } |
334 | |