]>
Commit | Line | Data |
---|---|---|
abe05a73 XL |
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 | ||
5869c6ff XL |
14 | use crate::parser::{default_port, Context, Input, Parser, SchemeType}; |
15 | use crate::{Host, ParseError, Position, Url}; | |
abe05a73 | 16 | |
2b03887a FG |
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 | ||
abe05a73 XL |
60 | /// https://url.spec.whatwg.org/#dom-url-domaintoascii |
61 | pub fn domain_to_ascii(domain: &str) -> String { | |
62 | match Host::parse(domain) { | |
63 | Ok(Host::Domain(domain)) => 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(domain) { | |
71 | Ok(Host::Domain(ref domain)) => { | |
72 | let (unicode, _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(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 | |
17df50a5 | 102 | #[allow(clippy::result_unit_err)] |
abe05a73 XL |
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) = 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 | |
17df50a5 | 119 | #[allow(clippy::result_unit_err)] |
abe05a73 XL |
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("") | |
128 | } | |
129 | ||
130 | /// Setter for https://url.spec.whatwg.org/#dom-url-password | |
17df50a5 | 131 | #[allow(clippy::result_unit_err)] |
abe05a73 | 132 | pub fn set_password(url: &mut Url, new_password: &str) -> Result<(), ()> { |
e74abb32 XL |
133 | url.set_password(if new_password.is_empty() { |
134 | None | |
135 | } else { | |
136 | Some(new_password) | |
137 | }) | |
abe05a73 XL |
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 | |
17df50a5 | 147 | #[allow(clippy::result_unit_err)] |
abe05a73 | 148 | pub fn set_host(url: &mut Url, new_host: &str) -> Result<(), ()> { |
f035d41b | 149 | // If context object’s url’s cannot-be-a-base-URL flag is set, then return. |
abe05a73 | 150 | if url.cannot_be_a_base() { |
e74abb32 | 151 | return Err(()); |
abe05a73 | 152 | } |
f035d41b XL |
153 | // Host parsing rules are strict, |
154 | // We don't want to trim the input | |
155 | let input = Input::no_trim(new_host); | |
abe05a73 XL |
156 | let host; |
157 | let opt_port; | |
158 | { | |
159 | let scheme = url.scheme(); | |
f035d41b | 160 | let scheme_type = SchemeType::from(scheme); |
5869c6ff XL |
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 | ||
f035d41b XL |
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 { | |
abe05a73 | 172 | Parser::parse_port(remaining, || default_port(scheme), Context::Setter) |
e74abb32 XL |
173 | .ok() |
174 | .map(|(port, _remaining)| port) | |
f035d41b XL |
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 | |
2b03887a FG |
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(()); | |
abe05a73 XL |
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("") | |
197 | } | |
198 | ||
199 | /// Setter for https://url.spec.whatwg.org/#dom-url-hostname | |
17df50a5 | 200 | #[allow(clippy::result_unit_err)] |
abe05a73 XL |
201 | pub fn set_hostname(url: &mut Url, new_hostname: &str) -> Result<(), ()> { |
202 | if url.cannot_be_a_base() { | |
e74abb32 | 203 | return Err(()); |
abe05a73 | 204 | } |
f035d41b XL |
205 | // Host parsing rules are strict we don't want to trim the input |
206 | let input = Input::no_trim(new_hostname); | |
207 | let scheme_type = SchemeType::from(url.scheme()); | |
5869c6ff XL |
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 | ||
f035d41b XL |
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 | |
2b03887a | 219 | ||!port(url).is_empty() |
f035d41b XL |
220 | // Empty host that includes credentials |
221 | || !url.username().is_empty() | |
2b03887a | 222 | || !url.password().unwrap_or("").is_empty() |
f035d41b XL |
223 | { |
224 | return Err(()); | |
225 | } | |
226 | } | |
227 | } | |
abe05a73 XL |
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 | |
17df50a5 | 242 | #[allow(clippy::result_unit_err)] |
abe05a73 XL |
243 | pub fn set_port(url: &mut Url, new_port: &str) -> Result<(), ()> { |
244 | let result; | |
245 | { | |
246 | // has_host implies !cannot_be_a_base | |
247 | let scheme = url.scheme(); | |
2c00a5a8 | 248 | if !url.has_host() || url.host() == Some(Host::Domain("")) || scheme == "file" { |
e74abb32 | 249 | return Err(()); |
abe05a73 | 250 | } |
e74abb32 XL |
251 | result = Parser::parse_port( |
252 | Input::new(new_port), | |
253 | || default_port(scheme), | |
254 | Context::Setter, | |
255 | ) | |
abe05a73 XL |
256 | } |
257 | if let Ok((new_port, _remaining)) = 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 { | |
e74abb32 | 268 | url.path() |
abe05a73 XL |
269 | } |
270 | ||
271 | /// Setter for https://url.spec.whatwg.org/#dom-url-pathname | |
272 | pub fn set_pathname(url: &mut Url, new_pathname: &str) { | |
f035d41b XL |
273 | if url.cannot_be_a_base() { |
274 | return; | |
275 | } | |
5869c6ff | 276 | if new_pathname.starts_with('/') |
f035d41b XL |
277 | || (SchemeType::from(url.scheme()).is_special() |
278 | // \ is a segment delimiter for 'special' URLs" | |
5869c6ff | 279 | && new_pathname.starts_with('\\')) |
f035d41b | 280 | { |
abe05a73 | 281 | url.set_path(new_pathname) |
f035d41b XL |
282 | } else { |
283 | let mut path_to_set = String::from("/"); | |
284 | path_to_set.push_str(new_pathname); | |
285 | url.set_path(&path_to_set) | |
abe05a73 XL |
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) { | |
f035d41b XL |
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 | }) | |
abe05a73 XL |
318 | } |
319 | ||
320 | fn trim(s: &str) -> &str { | |
321 | if s.len() == 1 { | |
322 | "" | |
323 | } else { | |
324 | s | |
325 | } | |
326 | } |