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