| 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 | |