]>
Commit | Line | Data |
---|---|---|
abe05a73 XL |
1 | // Copyright 2013-2015 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 | /*! | |
10 | ||
11 | rust-url is an implementation of the [URL Standard](http://url.spec.whatwg.org/) | |
12 | for the [Rust](http://rust-lang.org/) programming language. | |
13 | ||
14 | ||
15 | # URL parsing and data structures | |
16 | ||
17 | First, URL parsing may fail for various reasons and therefore returns a `Result`. | |
18 | ||
19 | ``` | |
20 | use url::{Url, ParseError}; | |
21 | ||
22 | assert!(Url::parse("http://[:::1]") == Err(ParseError::InvalidIpv6Address)) | |
23 | ``` | |
24 | ||
25 | Let’s parse a valid URL and look at its components. | |
26 | ||
27 | ``` | |
e74abb32 | 28 | use url::{Url, Host, Position}; |
abe05a73 XL |
29 | # use url::ParseError; |
30 | # fn run() -> Result<(), ParseError> { | |
31 | let issue_list_url = Url::parse( | |
32 | "https://github.com/rust-lang/rust/issues?labels=E-easy&state=open" | |
33 | )?; | |
34 | ||
35 | ||
36 | assert!(issue_list_url.scheme() == "https"); | |
37 | assert!(issue_list_url.username() == ""); | |
38 | assert!(issue_list_url.password() == None); | |
39 | assert!(issue_list_url.host_str() == Some("github.com")); | |
40 | assert!(issue_list_url.host() == Some(Host::Domain("github.com"))); | |
41 | assert!(issue_list_url.port() == None); | |
42 | assert!(issue_list_url.path() == "/rust-lang/rust/issues"); | |
43 | assert!(issue_list_url.path_segments().map(|c| c.collect::<Vec<_>>()) == | |
44 | Some(vec!["rust-lang", "rust", "issues"])); | |
45 | assert!(issue_list_url.query() == Some("labels=E-easy&state=open")); | |
e74abb32 | 46 | assert!(&issue_list_url[Position::BeforePath..] == "/rust-lang/rust/issues?labels=E-easy&state=open"); |
abe05a73 XL |
47 | assert!(issue_list_url.fragment() == None); |
48 | assert!(!issue_list_url.cannot_be_a_base()); | |
49 | # Ok(()) | |
50 | # } | |
51 | # run().unwrap(); | |
52 | ``` | |
53 | ||
54 | Some URLs are said to be *cannot-be-a-base*: | |
55 | they don’t have a username, password, host, or port, | |
56 | and their "path" is an arbitrary string rather than slash-separated segments: | |
57 | ||
58 | ``` | |
59 | use url::Url; | |
60 | # use url::ParseError; | |
61 | ||
62 | # fn run() -> Result<(), ParseError> { | |
63 | let data_url = Url::parse("data:text/plain,Hello?World#")?; | |
64 | ||
65 | assert!(data_url.cannot_be_a_base()); | |
66 | assert!(data_url.scheme() == "data"); | |
67 | assert!(data_url.path() == "text/plain,Hello"); | |
68 | assert!(data_url.path_segments().is_none()); | |
69 | assert!(data_url.query() == Some("World")); | |
70 | assert!(data_url.fragment() == Some("")); | |
71 | # Ok(()) | |
72 | # } | |
73 | # run().unwrap(); | |
74 | ``` | |
75 | ||
76 | ||
77 | # Base URL | |
78 | ||
79 | Many contexts allow URL *references* that can be relative to a *base URL*: | |
80 | ||
81 | ```html | |
82 | <link rel="stylesheet" href="../main.css"> | |
83 | ``` | |
84 | ||
e74abb32 | 85 | Since parsed URLs are absolute, giving a base is required for parsing relative URLs: |
abe05a73 XL |
86 | |
87 | ``` | |
88 | use url::{Url, ParseError}; | |
89 | ||
90 | assert!(Url::parse("../main.css") == Err(ParseError::RelativeUrlWithoutBase)) | |
91 | ``` | |
92 | ||
93 | Use the `join` method on an `Url` to use it as a base URL: | |
94 | ||
95 | ``` | |
96 | use url::Url; | |
97 | # use url::ParseError; | |
98 | ||
99 | # fn run() -> Result<(), ParseError> { | |
100 | let this_document = Url::parse("http://servo.github.io/rust-url/url/index.html")?; | |
101 | let css_url = this_document.join("../main.css")?; | |
102 | assert_eq!(css_url.as_str(), "http://servo.github.io/rust-url/main.css"); | |
103 | # Ok(()) | |
104 | # } | |
105 | # run().unwrap(); | |
106 | */ | |
107 | ||
f035d41b | 108 | #![doc(html_root_url = "https://docs.rs/url/2.1.1")] |
abe05a73 | 109 | |
0731742a | 110 | #[macro_use] |
e74abb32 XL |
111 | extern crate matches; |
112 | extern crate idna; | |
113 | extern crate percent_encoding; | |
114 | #[cfg(feature = "serde")] | |
115 | extern crate serde; | |
abe05a73 | 116 | |
abe05a73 | 117 | use host::HostInternal; |
e74abb32 XL |
118 | use parser::{to_u32, Context, Parser, SchemeType, PATH_SEGMENT, USERINFO}; |
119 | use percent_encoding::{percent_decode, percent_encode, utf8_percent_encode}; | |
abe05a73 XL |
120 | use std::borrow::Borrow; |
121 | use std::cmp; | |
e74abb32 XL |
122 | #[cfg(feature = "serde")] |
123 | use std::error::Error; | |
124 | use std::fmt::{self, Write}; | |
abe05a73 XL |
125 | use std::hash; |
126 | use std::io; | |
127 | use std::mem; | |
e74abb32 | 128 | use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; |
abe05a73 XL |
129 | use std::ops::{Range, RangeFrom, RangeTo}; |
130 | use std::path::{Path, PathBuf}; | |
131 | use std::str; | |
132 | ||
e74abb32 XL |
133 | pub use host::Host; |
134 | pub use origin::{OpaqueOrigin, Origin}; | |
2c00a5a8 | 135 | pub use parser::{ParseError, SyntaxViolation}; |
e74abb32 XL |
136 | pub use path_segments::PathSegmentsMut; |
137 | pub use query_encoding::EncodingOverride; | |
abe05a73 XL |
138 | pub use slicing::Position; |
139 | ||
abe05a73 XL |
140 | mod host; |
141 | mod origin; | |
abe05a73 | 142 | mod parser; |
e74abb32 XL |
143 | mod path_segments; |
144 | mod query_encoding; | |
abe05a73 XL |
145 | mod slicing; |
146 | ||
147 | pub mod form_urlencoded; | |
e74abb32 XL |
148 | #[doc(hidden)] |
149 | pub mod quirks; | |
abe05a73 XL |
150 | |
151 | /// A parsed URL record. | |
152 | #[derive(Clone)] | |
153 | pub struct Url { | |
154 | /// Syntax in pseudo-BNF: | |
155 | /// | |
156 | /// url = scheme ":" [ hierarchical | non-hierarchical ] [ "?" query ]? [ "#" fragment ]? | |
157 | /// non-hierarchical = non-hierarchical-path | |
158 | /// non-hierarchical-path = /* Does not start with "/" */ | |
159 | /// hierarchical = authority? hierarchical-path | |
160 | /// authority = "//" userinfo? host [ ":" port ]? | |
161 | /// userinfo = username [ ":" password ]? "@" | |
162 | /// hierarchical-path = [ "/" path-segment ]+ | |
163 | serialization: String, | |
164 | ||
165 | // Components | |
e74abb32 XL |
166 | scheme_end: u32, // Before ':' |
167 | username_end: u32, // Before ':' (if a password is given) or '@' (if not) | |
abe05a73 XL |
168 | host_start: u32, |
169 | host_end: u32, | |
170 | host: HostInternal, | |
171 | port: Option<u16>, | |
e74abb32 XL |
172 | path_start: u32, // Before initial '/', if any |
173 | query_start: Option<u32>, // Before '?', unlike Position::QueryStart | |
174 | fragment_start: Option<u32>, // Before '#', unlike Position::FragmentStart | |
abe05a73 XL |
175 | } |
176 | ||
177 | /// Full configuration for the URL parser. | |
178 | #[derive(Copy, Clone)] | |
179 | pub struct ParseOptions<'a> { | |
180 | base_url: Option<&'a Url>, | |
e74abb32 XL |
181 | encoding_override: EncodingOverride<'a>, |
182 | violation_fn: Option<&'a dyn Fn(SyntaxViolation)>, | |
abe05a73 XL |
183 | } |
184 | ||
185 | impl<'a> ParseOptions<'a> { | |
186 | /// Change the base URL | |
187 | pub fn base_url(mut self, new: Option<&'a Url>) -> Self { | |
188 | self.base_url = new; | |
189 | self | |
190 | } | |
191 | ||
192 | /// Override the character encoding of query strings. | |
193 | /// This is a legacy concept only relevant for HTML. | |
e74abb32 XL |
194 | pub fn encoding_override(mut self, new: EncodingOverride<'a>) -> Self { |
195 | self.encoding_override = new; | |
2c00a5a8 XL |
196 | self |
197 | } | |
198 | ||
199 | /// Call the provided function or closure for a non-fatal `SyntaxViolation` | |
200 | /// when it occurs during parsing. Note that since the provided function is | |
201 | /// `Fn`, the caller might need to utilize _interior mutability_, such as with | |
202 | /// a `RefCell`, to collect the violations. | |
203 | /// | |
204 | /// ## Example | |
205 | /// ``` | |
206 | /// use std::cell::RefCell; | |
207 | /// use url::{Url, SyntaxViolation}; | |
208 | /// # use url::ParseError; | |
209 | /// # fn run() -> Result<(), url::ParseError> { | |
210 | /// let violations = RefCell::new(Vec::new()); | |
211 | /// let url = Url::options() | |
212 | /// .syntax_violation_callback(Some(&|v| violations.borrow_mut().push(v))) | |
213 | /// .parse("https:////example.com")?; | |
214 | /// assert_eq!(url.as_str(), "https://example.com/"); | |
215 | /// assert_eq!(violations.into_inner(), | |
216 | /// vec!(SyntaxViolation::ExpectedDoubleSlash)); | |
217 | /// # Ok(()) | |
218 | /// # } | |
219 | /// # run().unwrap(); | |
220 | /// ``` | |
e74abb32 XL |
221 | pub fn syntax_violation_callback(mut self, new: Option<&'a dyn Fn(SyntaxViolation)>) -> Self { |
222 | self.violation_fn = new; | |
abe05a73 XL |
223 | self |
224 | } | |
225 | ||
226 | /// Parse an URL string with the configuration so far. | |
227 | pub fn parse(self, input: &str) -> Result<Url, ::ParseError> { | |
228 | Parser { | |
229 | serialization: String::with_capacity(input.len()), | |
230 | base_url: self.base_url, | |
231 | query_encoding_override: self.encoding_override, | |
2c00a5a8 | 232 | violation_fn: self.violation_fn, |
abe05a73 | 233 | context: Context::UrlParser, |
e74abb32 XL |
234 | } |
235 | .parse_url(input) | |
abe05a73 XL |
236 | } |
237 | } | |
238 | ||
239 | impl Url { | |
240 | /// Parse an absolute URL from a string. | |
241 | /// | |
242 | /// # Examples | |
243 | /// | |
244 | /// ```rust | |
245 | /// use url::Url; | |
246 | /// # use url::ParseError; | |
247 | /// | |
248 | /// # fn run() -> Result<(), ParseError> { | |
249 | /// let url = Url::parse("https://example.net")?; | |
250 | /// # Ok(()) | |
251 | /// # } | |
252 | /// # run().unwrap(); | |
253 | /// ``` | |
2c00a5a8 | 254 | /// |
abe05a73 XL |
255 | /// # Errors |
256 | /// | |
257 | /// If the function can not parse an absolute URL from the given string, | |
258 | /// a [`ParseError`] variant will be returned. | |
259 | /// | |
260 | /// [`ParseError`]: enum.ParseError.html | |
261 | #[inline] | |
262 | pub fn parse(input: &str) -> Result<Url, ::ParseError> { | |
263 | Url::options().parse(input) | |
264 | } | |
265 | ||
266 | /// Parse an absolute URL from a string and add params to its query string. | |
267 | /// | |
268 | /// Existing params are not removed. | |
269 | /// | |
270 | /// # Examples | |
271 | /// | |
272 | /// ```rust | |
273 | /// use url::Url; | |
274 | /// # use url::ParseError; | |
275 | /// | |
276 | /// # fn run() -> Result<(), ParseError> { | |
277 | /// let url = Url::parse_with_params("https://example.net?dont=clobberme", | |
278 | /// &[("lang", "rust"), ("browser", "servo")])?; | |
279 | /// # Ok(()) | |
280 | /// # } | |
281 | /// # run().unwrap(); | |
282 | /// ``` | |
283 | /// | |
284 | /// # Errors | |
285 | /// | |
286 | /// If the function can not parse an absolute URL from the given string, | |
287 | /// a [`ParseError`] variant will be returned. | |
288 | /// | |
289 | /// [`ParseError`]: enum.ParseError.html | |
290 | #[inline] | |
291 | pub fn parse_with_params<I, K, V>(input: &str, iter: I) -> Result<Url, ::ParseError> | |
e74abb32 XL |
292 | where |
293 | I: IntoIterator, | |
294 | I::Item: Borrow<(K, V)>, | |
295 | K: AsRef<str>, | |
296 | V: AsRef<str>, | |
abe05a73 XL |
297 | { |
298 | let mut url = Url::options().parse(input); | |
299 | ||
300 | if let Ok(ref mut url) = url { | |
301 | url.query_pairs_mut().extend_pairs(iter); | |
302 | } | |
303 | ||
304 | url | |
305 | } | |
306 | ||
307 | /// Parse a string as an URL, with this URL as the base URL. | |
308 | /// | |
309 | /// Note: a trailing slash is significant. | |
310 | /// Without it, the last path component is considered to be a “file” name | |
311 | /// to be removed to get at the “directory” that is used as the base: | |
312 | /// | |
313 | /// # Examples | |
314 | /// | |
315 | /// ```rust | |
316 | /// use url::Url; | |
317 | /// # use url::ParseError; | |
2c00a5a8 | 318 | /// |
abe05a73 XL |
319 | /// # fn run() -> Result<(), ParseError> { |
320 | /// let base = Url::parse("https://example.net/a/b.html")?; | |
321 | /// let url = base.join("c.png")?; | |
322 | /// assert_eq!(url.as_str(), "https://example.net/a/c.png"); // Not /a/b.html/c.png | |
323 | /// | |
324 | /// let base = Url::parse("https://example.net/a/b/")?; | |
325 | /// let url = base.join("c.png")?; | |
326 | /// assert_eq!(url.as_str(), "https://example.net/a/b/c.png"); | |
327 | /// # Ok(()) | |
328 | /// # } | |
329 | /// # run().unwrap(); | |
330 | /// ``` | |
331 | /// | |
332 | /// # Errors | |
333 | /// | |
2c00a5a8 | 334 | /// If the function can not parse an URL from the given string |
abe05a73 XL |
335 | /// with this URL as the base URL, a [`ParseError`] variant will be returned. |
336 | /// | |
337 | /// [`ParseError`]: enum.ParseError.html | |
338 | #[inline] | |
339 | pub fn join(&self, input: &str) -> Result<Url, ::ParseError> { | |
340 | Url::options().base_url(Some(self)).parse(input) | |
341 | } | |
342 | ||
343 | /// Return a default `ParseOptions` that can fully configure the URL parser. | |
344 | /// | |
345 | /// # Examples | |
346 | /// | |
347 | /// Get default `ParseOptions`, then change base url | |
348 | /// | |
349 | /// ```rust | |
350 | /// use url::Url; | |
351 | /// # use url::ParseError; | |
352 | /// # fn run() -> Result<(), ParseError> { | |
353 | /// let options = Url::options(); | |
354 | /// let api = Url::parse("https://api.example.com")?; | |
355 | /// let base_url = options.base_url(Some(&api)); | |
356 | /// let version_url = base_url.parse("version.json")?; | |
357 | /// assert_eq!(version_url.as_str(), "https://api.example.com/version.json"); | |
358 | /// # Ok(()) | |
359 | /// # } | |
360 | /// # run().unwrap(); | |
361 | /// ``` | |
362 | pub fn options<'a>() -> ParseOptions<'a> { | |
363 | ParseOptions { | |
364 | base_url: None, | |
e74abb32 XL |
365 | encoding_override: None, |
366 | violation_fn: None, | |
abe05a73 XL |
367 | } |
368 | } | |
369 | ||
370 | /// Return the serialization of this URL. | |
371 | /// | |
372 | /// This is fast since that serialization is already stored in the `Url` struct. | |
373 | /// | |
374 | /// # Examples | |
375 | /// | |
376 | /// ```rust | |
377 | /// use url::Url; | |
378 | /// # use url::ParseError; | |
379 | /// | |
380 | /// # fn run() -> Result<(), ParseError> { | |
381 | /// let url_str = "https://example.net/"; | |
382 | /// let url = Url::parse(url_str)?; | |
383 | /// assert_eq!(url.as_str(), url_str); | |
384 | /// # Ok(()) | |
385 | /// # } | |
386 | /// # run().unwrap(); | |
387 | /// ``` | |
388 | #[inline] | |
389 | pub fn as_str(&self) -> &str { | |
390 | &self.serialization | |
391 | } | |
392 | ||
393 | /// Return the serialization of this URL. | |
394 | /// | |
395 | /// This consumes the `Url` and takes ownership of the `String` stored in it. | |
396 | /// | |
397 | /// # Examples | |
398 | /// | |
399 | /// ```rust | |
400 | /// use url::Url; | |
401 | /// # use url::ParseError; | |
402 | /// | |
403 | /// # fn run() -> Result<(), ParseError> { | |
404 | /// let url_str = "https://example.net/"; | |
405 | /// let url = Url::parse(url_str)?; | |
406 | /// assert_eq!(url.into_string(), url_str); | |
407 | /// # Ok(()) | |
408 | /// # } | |
409 | /// # run().unwrap(); | |
410 | /// ``` | |
411 | #[inline] | |
412 | pub fn into_string(self) -> String { | |
413 | self.serialization | |
414 | } | |
415 | ||
416 | /// For internal testing, not part of the public API. | |
417 | /// | |
418 | /// Methods of the `Url` struct assume a number of invariants. | |
419 | /// This checks each of these invariants and panic if one is not met. | |
420 | /// This is for testing rust-url itself. | |
421 | #[doc(hidden)] | |
422 | pub fn check_invariants(&self) -> Result<(), String> { | |
423 | macro_rules! assert { | |
424 | ($x: expr) => { | |
425 | if !$x { | |
e74abb32 XL |
426 | return Err(format!( |
427 | "!( {} ) for URL {:?}", | |
428 | stringify!($x), | |
429 | self.serialization | |
430 | )); | |
abe05a73 | 431 | } |
e74abb32 | 432 | }; |
abe05a73 XL |
433 | } |
434 | ||
435 | macro_rules! assert_eq { | |
436 | ($a: expr, $b: expr) => { | |
437 | { | |
438 | let a = $a; | |
439 | let b = $b; | |
440 | if a != b { | |
441 | return Err(format!("{:?} != {:?} ({} != {}) for URL {:?}", | |
442 | a, b, stringify!($a), stringify!($b), | |
443 | self.serialization)) | |
444 | } | |
445 | } | |
446 | } | |
447 | } | |
448 | ||
449 | assert!(self.scheme_end >= 1); | |
e74abb32 XL |
450 | assert!(matches!(self.byte_at(0), b'a'..=b'z' | b'A'..=b'Z')); |
451 | assert!(self | |
452 | .slice(1..self.scheme_end) | |
453 | .chars() | |
454 | .all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '+' | '-' | '.'))); | |
abe05a73 XL |
455 | assert_eq!(self.byte_at(self.scheme_end), b':'); |
456 | ||
e74abb32 | 457 | if self.slice(self.scheme_end + 1..).starts_with("//") { |
abe05a73 | 458 | // URL with authority |
f035d41b XL |
459 | if self.username_end != self.serialization.len() as u32 { |
460 | match self.byte_at(self.username_end) { | |
461 | b':' => { | |
462 | assert!(self.host_start >= self.username_end + 2); | |
463 | assert_eq!(self.byte_at(self.host_start - 1), b'@'); | |
464 | } | |
465 | b'@' => assert!(self.host_start == self.username_end + 1), | |
466 | _ => assert_eq!(self.username_end, self.scheme_end + 3), | |
abe05a73 | 467 | } |
abe05a73 XL |
468 | } |
469 | assert!(self.host_start >= self.username_end); | |
470 | assert!(self.host_end >= self.host_start); | |
471 | let host_str = self.slice(self.host_start..self.host_end); | |
472 | match self.host { | |
473 | HostInternal::None => assert_eq!(host_str, ""), | |
474 | HostInternal::Ipv4(address) => assert_eq!(host_str, address.to_string()), | |
475 | HostInternal::Ipv6(address) => { | |
476 | let h: Host<String> = Host::Ipv6(address); | |
477 | assert_eq!(host_str, h.to_string()) | |
478 | } | |
479 | HostInternal::Domain => { | |
480 | if SchemeType::from(self.scheme()).is_special() { | |
481 | assert!(!host_str.is_empty()) | |
482 | } | |
483 | } | |
484 | } | |
485 | if self.path_start == self.host_end { | |
486 | assert_eq!(self.port, None); | |
487 | } else { | |
488 | assert_eq!(self.byte_at(self.host_end), b':'); | |
489 | let port_str = self.slice(self.host_end + 1..self.path_start); | |
e74abb32 XL |
490 | assert_eq!( |
491 | self.port, | |
492 | Some(port_str.parse::<u16>().expect("Couldn't parse port?")) | |
493 | ); | |
abe05a73 | 494 | } |
f035d41b XL |
495 | assert!( |
496 | self.path_start as usize == self.serialization.len() | |
497 | || matches!(self.byte_at(self.path_start), b'/' | b'#' | b'?') | |
498 | ); | |
abe05a73 XL |
499 | } else { |
500 | // Anarchist URL (no authority) | |
501 | assert_eq!(self.username_end, self.scheme_end + 1); | |
502 | assert_eq!(self.host_start, self.scheme_end + 1); | |
503 | assert_eq!(self.host_end, self.scheme_end + 1); | |
504 | assert_eq!(self.host, HostInternal::None); | |
505 | assert_eq!(self.port, None); | |
506 | assert_eq!(self.path_start, self.scheme_end + 1); | |
507 | } | |
508 | if let Some(start) = self.query_start { | |
f035d41b | 509 | assert!(start >= self.path_start); |
abe05a73 XL |
510 | assert_eq!(self.byte_at(start), b'?'); |
511 | } | |
512 | if let Some(start) = self.fragment_start { | |
f035d41b | 513 | assert!(start >= self.path_start); |
abe05a73 XL |
514 | assert_eq!(self.byte_at(start), b'#'); |
515 | } | |
516 | if let (Some(query_start), Some(fragment_start)) = (self.query_start, self.fragment_start) { | |
517 | assert!(fragment_start > query_start); | |
518 | } | |
519 | ||
520 | let other = Url::parse(self.as_str()).expect("Failed to parse myself?"); | |
521 | assert_eq!(&self.serialization, &other.serialization); | |
522 | assert_eq!(self.scheme_end, other.scheme_end); | |
523 | assert_eq!(self.username_end, other.username_end); | |
524 | assert_eq!(self.host_start, other.host_start); | |
525 | assert_eq!(self.host_end, other.host_end); | |
e74abb32 XL |
526 | assert!( |
527 | self.host == other.host || | |
abe05a73 XL |
528 | // XXX No host round-trips to empty host. |
529 | // See https://github.com/whatwg/url/issues/79 | |
e74abb32 XL |
530 | (self.host_str(), other.host_str()) == (None, Some("")) |
531 | ); | |
abe05a73 XL |
532 | assert_eq!(self.port, other.port); |
533 | assert_eq!(self.path_start, other.path_start); | |
534 | assert_eq!(self.query_start, other.query_start); | |
535 | assert_eq!(self.fragment_start, other.fragment_start); | |
536 | Ok(()) | |
537 | } | |
538 | ||
539 | /// Return the origin of this URL (<https://url.spec.whatwg.org/#origin>) | |
540 | /// | |
541 | /// Note: this returns an opaque origin for `file:` URLs, which causes | |
542 | /// `url.origin() != url.origin()`. | |
543 | /// | |
544 | /// # Examples | |
545 | /// | |
546 | /// URL with `ftp` scheme: | |
547 | /// | |
548 | /// ```rust | |
549 | /// use url::{Host, Origin, Url}; | |
550 | /// # use url::ParseError; | |
551 | /// | |
552 | /// # fn run() -> Result<(), ParseError> { | |
553 | /// let url = Url::parse("ftp://example.com/foo")?; | |
554 | /// assert_eq!(url.origin(), | |
555 | /// Origin::Tuple("ftp".into(), | |
556 | /// Host::Domain("example.com".into()), | |
557 | /// 21)); | |
558 | /// # Ok(()) | |
559 | /// # } | |
560 | /// # run().unwrap(); | |
561 | /// ``` | |
562 | /// | |
563 | /// URL with `blob` scheme: | |
564 | /// | |
565 | /// ```rust | |
566 | /// use url::{Host, Origin, Url}; | |
567 | /// # use url::ParseError; | |
568 | /// | |
569 | /// # fn run() -> Result<(), ParseError> { | |
570 | /// let url = Url::parse("blob:https://example.com/foo")?; | |
571 | /// assert_eq!(url.origin(), | |
572 | /// Origin::Tuple("https".into(), | |
573 | /// Host::Domain("example.com".into()), | |
574 | /// 443)); | |
575 | /// # Ok(()) | |
576 | /// # } | |
577 | /// # run().unwrap(); | |
578 | /// ``` | |
579 | /// | |
580 | /// URL with `file` scheme: | |
581 | /// | |
582 | /// ```rust | |
583 | /// use url::{Host, Origin, Url}; | |
584 | /// # use url::ParseError; | |
585 | /// | |
586 | /// # fn run() -> Result<(), ParseError> { | |
587 | /// let url = Url::parse("file:///tmp/foo")?; | |
588 | /// assert!(!url.origin().is_tuple()); | |
589 | /// | |
590 | /// let other_url = Url::parse("file:///tmp/foo")?; | |
591 | /// assert!(url.origin() != other_url.origin()); | |
592 | /// # Ok(()) | |
593 | /// # } | |
594 | /// # run().unwrap(); | |
595 | /// ``` | |
596 | /// | |
597 | /// URL with other scheme: | |
598 | /// | |
599 | /// ```rust | |
600 | /// use url::{Host, Origin, Url}; | |
601 | /// # use url::ParseError; | |
602 | /// | |
603 | /// # fn run() -> Result<(), ParseError> { | |
604 | /// let url = Url::parse("foo:bar")?; | |
605 | /// assert!(!url.origin().is_tuple()); | |
606 | /// # Ok(()) | |
607 | /// # } | |
608 | /// # run().unwrap(); | |
609 | /// ``` | |
610 | #[inline] | |
611 | pub fn origin(&self) -> Origin { | |
612 | origin::url_origin(self) | |
613 | } | |
614 | ||
615 | /// Return the scheme of this URL, lower-cased, as an ASCII string without the ':' delimiter. | |
616 | /// | |
617 | /// # Examples | |
618 | /// | |
619 | /// ``` | |
620 | /// use url::Url; | |
621 | /// # use url::ParseError; | |
622 | /// | |
623 | /// # fn run() -> Result<(), ParseError> { | |
624 | /// let url = Url::parse("file:///tmp/foo")?; | |
625 | /// assert_eq!(url.scheme(), "file"); | |
626 | /// # Ok(()) | |
627 | /// # } | |
628 | /// # run().unwrap(); | |
629 | /// ``` | |
630 | #[inline] | |
631 | pub fn scheme(&self) -> &str { | |
632 | self.slice(..self.scheme_end) | |
633 | } | |
634 | ||
635 | /// Return whether the URL has an 'authority', | |
636 | /// which can contain a username, password, host, and port number. | |
637 | /// | |
638 | /// URLs that do *not* are either path-only like `unix:/run/foo.socket` | |
639 | /// or cannot-be-a-base like `data:text/plain,Stuff`. | |
640 | /// | |
641 | /// # Examples | |
642 | /// | |
643 | /// ``` | |
644 | /// use url::Url; | |
645 | /// # use url::ParseError; | |
646 | /// | |
647 | /// # fn run() -> Result<(), ParseError> { | |
648 | /// let url = Url::parse("ftp://rms@example.com")?; | |
649 | /// assert!(url.has_authority()); | |
650 | /// | |
651 | /// let url = Url::parse("unix:/run/foo.socket")?; | |
652 | /// assert!(!url.has_authority()); | |
653 | /// | |
654 | /// let url = Url::parse("data:text/plain,Stuff")?; | |
655 | /// assert!(!url.has_authority()); | |
656 | /// # Ok(()) | |
657 | /// # } | |
658 | /// # run().unwrap(); | |
659 | /// ``` | |
660 | #[inline] | |
661 | pub fn has_authority(&self) -> bool { | |
662 | debug_assert!(self.byte_at(self.scheme_end) == b':'); | |
663 | self.slice(self.scheme_end..).starts_with("://") | |
664 | } | |
665 | ||
666 | /// Return whether this URL is a cannot-be-a-base URL, | |
667 | /// meaning that parsing a relative URL string with this URL as the base will return an error. | |
668 | /// | |
669 | /// This is the case if the scheme and `:` delimiter are not followed by a `/` slash, | |
670 | /// as is typically the case of `data:` and `mailto:` URLs. | |
671 | /// | |
672 | /// # Examples | |
673 | /// | |
674 | /// ``` | |
675 | /// use url::Url; | |
676 | /// # use url::ParseError; | |
677 | /// | |
678 | /// # fn run() -> Result<(), ParseError> { | |
679 | /// let url = Url::parse("ftp://rms@example.com")?; | |
680 | /// assert!(!url.cannot_be_a_base()); | |
681 | /// | |
682 | /// let url = Url::parse("unix:/run/foo.socket")?; | |
683 | /// assert!(!url.cannot_be_a_base()); | |
684 | /// | |
685 | /// let url = Url::parse("data:text/plain,Stuff")?; | |
686 | /// assert!(url.cannot_be_a_base()); | |
687 | /// # Ok(()) | |
688 | /// # } | |
689 | /// # run().unwrap(); | |
690 | /// ``` | |
691 | #[inline] | |
692 | pub fn cannot_be_a_base(&self) -> bool { | |
f035d41b | 693 | !self.slice(self.scheme_end + 1..).starts_with('/') |
abe05a73 XL |
694 | } |
695 | ||
696 | /// Return the username for this URL (typically the empty string) | |
697 | /// as a percent-encoded ASCII string. | |
698 | /// | |
699 | /// # Examples | |
700 | /// | |
701 | /// ``` | |
702 | /// use url::Url; | |
703 | /// # use url::ParseError; | |
704 | /// | |
705 | /// # fn run() -> Result<(), ParseError> { | |
706 | /// let url = Url::parse("ftp://rms@example.com")?; | |
707 | /// assert_eq!(url.username(), "rms"); | |
708 | /// | |
709 | /// let url = Url::parse("ftp://:secret123@example.com")?; | |
710 | /// assert_eq!(url.username(), ""); | |
711 | /// | |
712 | /// let url = Url::parse("https://example.com")?; | |
713 | /// assert_eq!(url.username(), ""); | |
714 | /// # Ok(()) | |
715 | /// # } | |
716 | /// # run().unwrap(); | |
717 | /// ``` | |
718 | pub fn username(&self) -> &str { | |
719 | if self.has_authority() { | |
720 | self.slice(self.scheme_end + ("://".len() as u32)..self.username_end) | |
721 | } else { | |
722 | "" | |
723 | } | |
724 | } | |
725 | ||
726 | /// Return the password for this URL, if any, as a percent-encoded ASCII string. | |
727 | /// | |
728 | /// # Examples | |
729 | /// | |
730 | /// ``` | |
731 | /// use url::Url; | |
732 | /// # use url::ParseError; | |
733 | /// | |
734 | /// # fn run() -> Result<(), ParseError> { | |
735 | /// let url = Url::parse("ftp://rms:secret123@example.com")?; | |
736 | /// assert_eq!(url.password(), Some("secret123")); | |
737 | /// | |
738 | /// let url = Url::parse("ftp://:secret123@example.com")?; | |
739 | /// assert_eq!(url.password(), Some("secret123")); | |
740 | /// | |
741 | /// let url = Url::parse("ftp://rms@example.com")?; | |
742 | /// assert_eq!(url.password(), None); | |
743 | /// | |
744 | /// let url = Url::parse("https://example.com")?; | |
745 | /// assert_eq!(url.password(), None); | |
746 | /// # Ok(()) | |
747 | /// # } | |
748 | /// # run().unwrap(); | |
749 | /// ``` | |
750 | pub fn password(&self) -> Option<&str> { | |
751 | // This ':' is not the one marking a port number since a host can not be empty. | |
752 | // (Except for file: URLs, which do not have port numbers.) | |
f035d41b XL |
753 | if self.has_authority() |
754 | && self.username_end != self.serialization.len() as u32 | |
755 | && self.byte_at(self.username_end) == b':' | |
756 | { | |
abe05a73 XL |
757 | debug_assert!(self.byte_at(self.host_start - 1) == b'@'); |
758 | Some(self.slice(self.username_end + 1..self.host_start - 1)) | |
759 | } else { | |
760 | None | |
761 | } | |
762 | } | |
763 | ||
764 | /// Equivalent to `url.host().is_some()`. | |
765 | /// | |
766 | /// # Examples | |
767 | /// | |
768 | /// ``` | |
769 | /// use url::Url; | |
770 | /// # use url::ParseError; | |
771 | /// | |
772 | /// # fn run() -> Result<(), ParseError> { | |
773 | /// let url = Url::parse("ftp://rms@example.com")?; | |
774 | /// assert!(url.has_host()); | |
775 | /// | |
776 | /// let url = Url::parse("unix:/run/foo.socket")?; | |
777 | /// assert!(!url.has_host()); | |
778 | /// | |
779 | /// let url = Url::parse("data:text/plain,Stuff")?; | |
780 | /// assert!(!url.has_host()); | |
781 | /// # Ok(()) | |
782 | /// # } | |
783 | /// # run().unwrap(); | |
784 | /// ``` | |
785 | pub fn has_host(&self) -> bool { | |
786 | !matches!(self.host, HostInternal::None) | |
787 | } | |
788 | ||
789 | /// Return the string representation of the host (domain or IP address) for this URL, if any. | |
790 | /// | |
791 | /// Non-ASCII domains are punycode-encoded per IDNA. | |
792 | /// IPv6 addresses are given between `[` and `]` brackets. | |
793 | /// | |
794 | /// Cannot-be-a-base URLs (typical of `data:` and `mailto:`) and some `file:` URLs | |
795 | /// don’t have a host. | |
796 | /// | |
797 | /// See also the `host` method. | |
798 | /// | |
799 | /// # Examples | |
800 | /// | |
801 | /// ``` | |
802 | /// use url::Url; | |
803 | /// # use url::ParseError; | |
804 | /// | |
805 | /// # fn run() -> Result<(), ParseError> { | |
806 | /// let url = Url::parse("https://127.0.0.1/index.html")?; | |
807 | /// assert_eq!(url.host_str(), Some("127.0.0.1")); | |
808 | /// | |
809 | /// let url = Url::parse("ftp://rms@example.com")?; | |
810 | /// assert_eq!(url.host_str(), Some("example.com")); | |
811 | /// | |
812 | /// let url = Url::parse("unix:/run/foo.socket")?; | |
813 | /// assert_eq!(url.host_str(), None); | |
814 | /// | |
815 | /// let url = Url::parse("data:text/plain,Stuff")?; | |
816 | /// assert_eq!(url.host_str(), None); | |
817 | /// # Ok(()) | |
818 | /// # } | |
819 | /// # run().unwrap(); | |
820 | /// ``` | |
821 | pub fn host_str(&self) -> Option<&str> { | |
822 | if self.has_host() { | |
823 | Some(self.slice(self.host_start..self.host_end)) | |
824 | } else { | |
825 | None | |
826 | } | |
827 | } | |
828 | ||
829 | /// Return the parsed representation of the host for this URL. | |
830 | /// Non-ASCII domain labels are punycode-encoded per IDNA. | |
831 | /// | |
832 | /// Cannot-be-a-base URLs (typical of `data:` and `mailto:`) and some `file:` URLs | |
833 | /// don’t have a host. | |
834 | /// | |
835 | /// See also the `host_str` method. | |
836 | /// | |
837 | /// # Examples | |
838 | /// | |
839 | /// ``` | |
840 | /// use url::Url; | |
841 | /// # use url::ParseError; | |
842 | /// | |
843 | /// # fn run() -> Result<(), ParseError> { | |
844 | /// let url = Url::parse("https://127.0.0.1/index.html")?; | |
845 | /// assert!(url.host().is_some()); | |
846 | /// | |
847 | /// let url = Url::parse("ftp://rms@example.com")?; | |
848 | /// assert!(url.host().is_some()); | |
849 | /// | |
850 | /// let url = Url::parse("unix:/run/foo.socket")?; | |
851 | /// assert!(url.host().is_none()); | |
852 | /// | |
853 | /// let url = Url::parse("data:text/plain,Stuff")?; | |
854 | /// assert!(url.host().is_none()); | |
855 | /// # Ok(()) | |
856 | /// # } | |
857 | /// # run().unwrap(); | |
858 | /// ``` | |
859 | pub fn host(&self) -> Option<Host<&str>> { | |
860 | match self.host { | |
861 | HostInternal::None => None, | |
862 | HostInternal::Domain => Some(Host::Domain(self.slice(self.host_start..self.host_end))), | |
863 | HostInternal::Ipv4(address) => Some(Host::Ipv4(address)), | |
864 | HostInternal::Ipv6(address) => Some(Host::Ipv6(address)), | |
865 | } | |
866 | } | |
867 | ||
868 | /// If this URL has a host and it is a domain name (not an IP address), return it. | |
869 | /// | |
870 | /// # Examples | |
871 | /// | |
872 | /// ``` | |
873 | /// use url::Url; | |
874 | /// # use url::ParseError; | |
875 | /// | |
876 | /// # fn run() -> Result<(), ParseError> { | |
877 | /// let url = Url::parse("https://127.0.0.1/")?; | |
878 | /// assert_eq!(url.domain(), None); | |
879 | /// | |
880 | /// let url = Url::parse("mailto:rms@example.net")?; | |
881 | /// assert_eq!(url.domain(), None); | |
882 | /// | |
883 | /// let url = Url::parse("https://example.com/")?; | |
884 | /// assert_eq!(url.domain(), Some("example.com")); | |
885 | /// # Ok(()) | |
886 | /// # } | |
887 | /// # run().unwrap(); | |
888 | /// ``` | |
889 | pub fn domain(&self) -> Option<&str> { | |
890 | match self.host { | |
891 | HostInternal::Domain => Some(self.slice(self.host_start..self.host_end)), | |
892 | _ => None, | |
893 | } | |
894 | } | |
895 | ||
896 | /// Return the port number for this URL, if any. | |
897 | /// | |
e74abb32 XL |
898 | /// Note that default port numbers are never reflected by the serialization, |
899 | /// use the `port_or_known_default()` method if you want a default port number returned. | |
900 | /// | |
abe05a73 XL |
901 | /// # Examples |
902 | /// | |
903 | /// ``` | |
904 | /// use url::Url; | |
905 | /// # use url::ParseError; | |
906 | /// | |
907 | /// # fn run() -> Result<(), ParseError> { | |
908 | /// let url = Url::parse("https://example.com")?; | |
909 | /// assert_eq!(url.port(), None); | |
910 | /// | |
e74abb32 XL |
911 | /// let url = Url::parse("https://example.com:443/")?; |
912 | /// assert_eq!(url.port(), None); | |
913 | /// | |
abe05a73 XL |
914 | /// let url = Url::parse("ssh://example.com:22")?; |
915 | /// assert_eq!(url.port(), Some(22)); | |
916 | /// # Ok(()) | |
917 | /// # } | |
918 | /// # run().unwrap(); | |
919 | /// ``` | |
920 | #[inline] | |
921 | pub fn port(&self) -> Option<u16> { | |
922 | self.port | |
923 | } | |
924 | ||
925 | /// Return the port number for this URL, or the default port number if it is known. | |
926 | /// | |
927 | /// This method only knows the default port number | |
928 | /// of the `http`, `https`, `ws`, `wss`, `ftp`, and `gopher` schemes. | |
929 | /// | |
930 | /// For URLs in these schemes, this method always returns `Some(_)`. | |
931 | /// For other schemes, it is the same as `Url::port()`. | |
932 | /// | |
933 | /// # Examples | |
934 | /// | |
935 | /// ``` | |
936 | /// use url::Url; | |
937 | /// # use url::ParseError; | |
938 | /// | |
939 | /// # fn run() -> Result<(), ParseError> { | |
940 | /// let url = Url::parse("foo://example.com")?; | |
941 | /// assert_eq!(url.port_or_known_default(), None); | |
942 | /// | |
943 | /// let url = Url::parse("foo://example.com:1456")?; | |
944 | /// assert_eq!(url.port_or_known_default(), Some(1456)); | |
945 | /// | |
946 | /// let url = Url::parse("https://example.com")?; | |
947 | /// assert_eq!(url.port_or_known_default(), Some(443)); | |
948 | /// # Ok(()) | |
949 | /// # } | |
950 | /// # run().unwrap(); | |
951 | /// ``` | |
952 | #[inline] | |
953 | pub fn port_or_known_default(&self) -> Option<u16> { | |
954 | self.port.or_else(|| parser::default_port(self.scheme())) | |
955 | } | |
956 | ||
e74abb32 | 957 | /// Resolve a URL’s host and port number to `SocketAddr`. |
abe05a73 | 958 | /// |
e74abb32 XL |
959 | /// If the URL has the default port number of a scheme that is unknown to this library, |
960 | /// `default_port_number` provides an opportunity to provide the actual port number. | |
961 | /// In non-example code this should be implemented either simply as `|| None`, | |
962 | /// or by matching on the URL’s `.scheme()`. | |
abe05a73 | 963 | /// |
e74abb32 XL |
964 | /// If the host is a domain, it is resolved using the standard library’s DNS support. |
965 | /// | |
966 | /// # Examples | |
967 | /// | |
968 | /// ```no_run | |
969 | /// let url = url::Url::parse("https://example.net/").unwrap(); | |
970 | /// let addrs = url.socket_addrs(|| None).unwrap(); | |
971 | /// std::net::TcpStream::connect(&*addrs) | |
972 | /// # ; | |
973 | /// ``` | |
abe05a73 | 974 | /// |
e74abb32 XL |
975 | /// ``` |
976 | /// /// With application-specific known default port numbers | |
977 | /// fn socket_addrs(url: url::Url) -> std::io::Result<Vec<std::net::SocketAddr>> { | |
978 | /// url.socket_addrs(|| match url.scheme() { | |
979 | /// "socks5" | "socks5h" => Some(1080), | |
980 | /// _ => None, | |
981 | /// }) | |
abe05a73 XL |
982 | /// } |
983 | /// ``` | |
e74abb32 XL |
984 | pub fn socket_addrs( |
985 | &self, | |
986 | default_port_number: impl Fn() -> Option<u16>, | |
987 | ) -> io::Result<Vec<SocketAddr>> { | |
988 | // Note: trying to avoid the Vec allocation by returning `impl AsRef<[SocketAddr]>` | |
989 | // causes borrowck issues because the return value borrows `default_port_number`: | |
990 | // | |
991 | // https://github.com/rust-lang/rfcs/blob/master/text/1951-expand-impl-trait.md#scoping-for-type-and-lifetime-parameters | |
992 | // | |
993 | // > This RFC proposes that *all* type parameters are considered in scope | |
994 | // > for `impl Trait` in return position | |
995 | ||
996 | fn io_result<T>(opt: Option<T>, message: &str) -> io::Result<T> { | |
997 | opt.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, message)) | |
998 | } | |
999 | ||
1000 | let host = io_result(self.host(), "No host name in the URL")?; | |
1001 | let port = io_result( | |
1002 | self.port_or_known_default().or_else(default_port_number), | |
1003 | "No port number in the URL", | |
1004 | )?; | |
1005 | Ok(match host { | |
1006 | Host::Domain(domain) => (domain, port).to_socket_addrs()?.collect(), | |
1007 | Host::Ipv4(ip) => vec![(ip, port).into()], | |
1008 | Host::Ipv6(ip) => vec![(ip, port).into()], | |
abe05a73 XL |
1009 | }) |
1010 | } | |
1011 | ||
1012 | /// Return the path for this URL, as a percent-encoded ASCII string. | |
1013 | /// For cannot-be-a-base URLs, this is an arbitrary string that doesn’t start with '/'. | |
1014 | /// For other URLs, this starts with a '/' slash | |
1015 | /// and continues with slash-separated path segments. | |
1016 | /// | |
1017 | /// # Examples | |
1018 | /// | |
1019 | /// ```rust | |
1020 | /// use url::{Url, ParseError}; | |
1021 | /// | |
1022 | /// # fn run() -> Result<(), ParseError> { | |
1023 | /// let url = Url::parse("https://example.com/api/versions?page=2")?; | |
1024 | /// assert_eq!(url.path(), "/api/versions"); | |
1025 | /// | |
1026 | /// let url = Url::parse("https://example.com")?; | |
1027 | /// assert_eq!(url.path(), "/"); | |
1028 | /// | |
1029 | /// let url = Url::parse("https://example.com/countries/việt nam")?; | |
1030 | /// assert_eq!(url.path(), "/countries/vi%E1%BB%87t%20nam"); | |
1031 | /// # Ok(()) | |
1032 | /// # } | |
1033 | /// # run().unwrap(); | |
1034 | /// ``` | |
1035 | pub fn path(&self) -> &str { | |
1036 | match (self.query_start, self.fragment_start) { | |
1037 | (None, None) => self.slice(self.path_start..), | |
e74abb32 | 1038 | (Some(next_component_start), _) | (None, Some(next_component_start)) => { |
abe05a73 XL |
1039 | self.slice(self.path_start..next_component_start) |
1040 | } | |
1041 | } | |
1042 | } | |
1043 | ||
1044 | /// Unless this URL is cannot-be-a-base, | |
1045 | /// return an iterator of '/' slash-separated path segments, | |
1046 | /// each as a percent-encoded ASCII string. | |
1047 | /// | |
1048 | /// Return `None` for cannot-be-a-base URLs. | |
1049 | /// | |
1050 | /// When `Some` is returned, the iterator always contains at least one string | |
1051 | /// (which may be empty). | |
1052 | /// | |
1053 | /// # Examples | |
1054 | /// | |
1055 | /// ``` | |
1056 | /// use url::Url; | |
1057 | /// # use std::error::Error; | |
1058 | /// | |
1059 | /// # fn run() -> Result<(), Box<Error>> { | |
1060 | /// let url = Url::parse("https://example.com/foo/bar")?; | |
1061 | /// let mut path_segments = url.path_segments().ok_or_else(|| "cannot be base")?; | |
1062 | /// assert_eq!(path_segments.next(), Some("foo")); | |
1063 | /// assert_eq!(path_segments.next(), Some("bar")); | |
1064 | /// assert_eq!(path_segments.next(), None); | |
1065 | /// | |
1066 | /// let url = Url::parse("https://example.com")?; | |
1067 | /// let mut path_segments = url.path_segments().ok_or_else(|| "cannot be base")?; | |
1068 | /// assert_eq!(path_segments.next(), Some("")); | |
1069 | /// assert_eq!(path_segments.next(), None); | |
1070 | /// | |
1071 | /// let url = Url::parse("data:text/plain,HelloWorld")?; | |
1072 | /// assert!(url.path_segments().is_none()); | |
1073 | /// | |
1074 | /// let url = Url::parse("https://example.com/countries/việt nam")?; | |
1075 | /// let mut path_segments = url.path_segments().ok_or_else(|| "cannot be base")?; | |
1076 | /// assert_eq!(path_segments.next(), Some("countries")); | |
1077 | /// assert_eq!(path_segments.next(), Some("vi%E1%BB%87t%20nam")); | |
1078 | /// # Ok(()) | |
1079 | /// # } | |
1080 | /// # run().unwrap(); | |
1081 | /// ``` | |
1082 | pub fn path_segments(&self) -> Option<str::Split<char>> { | |
1083 | let path = self.path(); | |
1084 | if path.starts_with('/') { | |
1085 | Some(path[1..].split('/')) | |
1086 | } else { | |
1087 | None | |
1088 | } | |
1089 | } | |
1090 | ||
1091 | /// Return this URL’s query string, if any, as a percent-encoded ASCII string. | |
1092 | /// | |
1093 | /// # Examples | |
1094 | /// | |
1095 | /// ```rust | |
1096 | /// use url::Url; | |
1097 | /// # use url::ParseError; | |
1098 | /// | |
1099 | /// fn run() -> Result<(), ParseError> { | |
1100 | /// let url = Url::parse("https://example.com/products?page=2")?; | |
1101 | /// let query = url.query(); | |
1102 | /// assert_eq!(query, Some("page=2")); | |
1103 | /// | |
1104 | /// let url = Url::parse("https://example.com/products")?; | |
1105 | /// let query = url.query(); | |
1106 | /// assert!(query.is_none()); | |
1107 | /// | |
1108 | /// let url = Url::parse("https://example.com/?country=español")?; | |
1109 | /// let query = url.query(); | |
1110 | /// assert_eq!(query, Some("country=espa%C3%B1ol")); | |
1111 | /// # Ok(()) | |
1112 | /// # } | |
1113 | /// # run().unwrap(); | |
1114 | /// ``` | |
1115 | pub fn query(&self) -> Option<&str> { | |
1116 | match (self.query_start, self.fragment_start) { | |
1117 | (None, _) => None, | |
1118 | (Some(query_start), None) => { | |
1119 | debug_assert!(self.byte_at(query_start) == b'?'); | |
1120 | Some(self.slice(query_start + 1..)) | |
1121 | } | |
1122 | (Some(query_start), Some(fragment_start)) => { | |
1123 | debug_assert!(self.byte_at(query_start) == b'?'); | |
1124 | Some(self.slice(query_start + 1..fragment_start)) | |
1125 | } | |
1126 | } | |
1127 | } | |
1128 | ||
1129 | /// Parse the URL’s query string, if any, as `application/x-www-form-urlencoded` | |
1130 | /// and return an iterator of (key, value) pairs. | |
1131 | /// | |
1132 | /// # Examples | |
1133 | /// | |
1134 | /// ```rust | |
1135 | /// use std::borrow::Cow; | |
1136 | /// | |
1137 | /// use url::Url; | |
1138 | /// # use url::ParseError; | |
1139 | /// | |
1140 | /// # fn run() -> Result<(), ParseError> { | |
1141 | /// let url = Url::parse("https://example.com/products?page=2&sort=desc")?; | |
1142 | /// let mut pairs = url.query_pairs(); | |
1143 | /// | |
1144 | /// assert_eq!(pairs.count(), 2); | |
1145 | /// | |
1146 | /// assert_eq!(pairs.next(), Some((Cow::Borrowed("page"), Cow::Borrowed("2")))); | |
1147 | /// assert_eq!(pairs.next(), Some((Cow::Borrowed("sort"), Cow::Borrowed("desc")))); | |
1148 | /// # Ok(()) | |
1149 | /// # } | |
1150 | /// # run().unwrap(); | |
1151 | /// | |
1152 | ||
1153 | #[inline] | |
1154 | pub fn query_pairs(&self) -> form_urlencoded::Parse { | |
1155 | form_urlencoded::parse(self.query().unwrap_or("").as_bytes()) | |
1156 | } | |
1157 | ||
1158 | /// Return this URL’s fragment identifier, if any. | |
1159 | /// | |
1160 | /// A fragment is the part of the URL after the `#` symbol. | |
1161 | /// The fragment is optional and, if present, contains a fragment identifier | |
1162 | /// that identifies a secondary resource, such as a section heading | |
1163 | /// of a document. | |
1164 | /// | |
1165 | /// In HTML, the fragment identifier is usually the id attribute of a an element | |
1166 | /// that is scrolled to on load. Browsers typically will not send the fragment portion | |
1167 | /// of a URL to the server. | |
1168 | /// | |
1169 | /// **Note:** the parser did *not* percent-encode this component, | |
1170 | /// but the input may have been percent-encoded already. | |
1171 | /// | |
1172 | /// # Examples | |
1173 | /// | |
1174 | /// ```rust | |
1175 | /// use url::Url; | |
1176 | /// # use url::ParseError; | |
1177 | /// | |
1178 | /// # fn run() -> Result<(), ParseError> { | |
1179 | /// let url = Url::parse("https://example.com/data.csv#row=4")?; | |
1180 | /// | |
1181 | /// assert_eq!(url.fragment(), Some("row=4")); | |
1182 | /// | |
1183 | /// let url = Url::parse("https://example.com/data.csv#cell=4,1-6,2")?; | |
1184 | /// | |
1185 | /// assert_eq!(url.fragment(), Some("cell=4,1-6,2")); | |
1186 | /// # Ok(()) | |
1187 | /// # } | |
1188 | /// # run().unwrap(); | |
1189 | /// ``` | |
1190 | pub fn fragment(&self) -> Option<&str> { | |
1191 | self.fragment_start.map(|start| { | |
1192 | debug_assert!(self.byte_at(start) == b'#'); | |
1193 | self.slice(start + 1..) | |
1194 | }) | |
1195 | } | |
1196 | ||
1197 | fn mutate<F: FnOnce(&mut Parser) -> R, R>(&mut self, f: F) -> R { | |
1198 | let mut parser = Parser::for_setter(mem::replace(&mut self.serialization, String::new())); | |
1199 | let result = f(&mut parser); | |
1200 | self.serialization = parser.serialization; | |
1201 | result | |
1202 | } | |
1203 | ||
1204 | /// Change this URL’s fragment identifier. | |
1205 | /// | |
1206 | /// # Examples | |
1207 | /// | |
1208 | /// ```rust | |
1209 | /// use url::Url; | |
1210 | /// # use url::ParseError; | |
1211 | /// | |
1212 | /// # fn run() -> Result<(), ParseError> { | |
1213 | /// let mut url = Url::parse("https://example.com/data.csv")?; | |
1214 | /// assert_eq!(url.as_str(), "https://example.com/data.csv"); | |
1215 | ||
1216 | /// url.set_fragment(Some("cell=4,1-6,2")); | |
2c00a5a8 | 1217 | /// assert_eq!(url.as_str(), "https://example.com/data.csv#cell=4,1-6,2"); |
abe05a73 XL |
1218 | /// assert_eq!(url.fragment(), Some("cell=4,1-6,2")); |
1219 | /// | |
1220 | /// url.set_fragment(None); | |
2c00a5a8 | 1221 | /// assert_eq!(url.as_str(), "https://example.com/data.csv"); |
abe05a73 XL |
1222 | /// assert!(url.fragment().is_none()); |
1223 | /// # Ok(()) | |
1224 | /// # } | |
1225 | /// # run().unwrap(); | |
1226 | /// ``` | |
1227 | pub fn set_fragment(&mut self, fragment: Option<&str>) { | |
1228 | // Remove any previous fragment | |
1229 | if let Some(start) = self.fragment_start { | |
1230 | debug_assert!(self.byte_at(start) == b'#'); | |
1231 | self.serialization.truncate(start as usize); | |
1232 | } | |
1233 | // Write the new one | |
1234 | if let Some(input) = fragment { | |
1235 | self.fragment_start = Some(to_u32(self.serialization.len()).unwrap()); | |
1236 | self.serialization.push('#'); | |
f035d41b | 1237 | self.mutate(|parser| parser.parse_fragment(parser::Input::no_trim(input))) |
abe05a73 XL |
1238 | } else { |
1239 | self.fragment_start = None | |
1240 | } | |
1241 | } | |
1242 | ||
1243 | fn take_fragment(&mut self) -> Option<String> { | |
1244 | self.fragment_start.take().map(|start| { | |
1245 | debug_assert!(self.byte_at(start) == b'#'); | |
1246 | let fragment = self.slice(start + 1..).to_owned(); | |
1247 | self.serialization.truncate(start as usize); | |
1248 | fragment | |
1249 | }) | |
1250 | } | |
1251 | ||
1252 | fn restore_already_parsed_fragment(&mut self, fragment: Option<String>) { | |
1253 | if let Some(ref fragment) = fragment { | |
1254 | assert!(self.fragment_start.is_none()); | |
1255 | self.fragment_start = Some(to_u32(self.serialization.len()).unwrap()); | |
1256 | self.serialization.push('#'); | |
1257 | self.serialization.push_str(fragment); | |
1258 | } | |
1259 | } | |
1260 | ||
1261 | /// Change this URL’s query string. | |
1262 | /// | |
1263 | /// # Examples | |
1264 | /// | |
1265 | /// ```rust | |
1266 | /// use url::Url; | |
1267 | /// # use url::ParseError; | |
1268 | /// | |
1269 | /// # fn run() -> Result<(), ParseError> { | |
1270 | /// let mut url = Url::parse("https://example.com/products")?; | |
1271 | /// assert_eq!(url.as_str(), "https://example.com/products"); | |
1272 | /// | |
1273 | /// url.set_query(Some("page=2")); | |
2c00a5a8 | 1274 | /// assert_eq!(url.as_str(), "https://example.com/products?page=2"); |
abe05a73 XL |
1275 | /// assert_eq!(url.query(), Some("page=2")); |
1276 | /// # Ok(()) | |
1277 | /// # } | |
1278 | /// # run().unwrap(); | |
1279 | /// ``` | |
1280 | pub fn set_query(&mut self, query: Option<&str>) { | |
1281 | let fragment = self.take_fragment(); | |
1282 | ||
1283 | // Remove any previous query | |
1284 | if let Some(start) = self.query_start.take() { | |
1285 | debug_assert!(self.byte_at(start) == b'?'); | |
1286 | self.serialization.truncate(start as usize); | |
1287 | } | |
1288 | // Write the new query, if any | |
1289 | if let Some(input) = query { | |
1290 | self.query_start = Some(to_u32(self.serialization.len()).unwrap()); | |
1291 | self.serialization.push('?'); | |
e74abb32 | 1292 | let scheme_type = SchemeType::from(self.scheme()); |
abe05a73 | 1293 | let scheme_end = self.scheme_end; |
e74abb32 | 1294 | self.mutate(|parser| { |
f035d41b XL |
1295 | let vfn = parser.violation_fn; |
1296 | parser.parse_query( | |
1297 | scheme_type, | |
1298 | scheme_end, | |
1299 | parser::Input::trim_tab_and_newlines(input, vfn), | |
1300 | ) | |
e74abb32 | 1301 | }); |
abe05a73 XL |
1302 | } |
1303 | ||
1304 | self.restore_already_parsed_fragment(fragment); | |
1305 | } | |
1306 | ||
1307 | /// Manipulate this URL’s query string, viewed as a sequence of name/value pairs | |
1308 | /// in `application/x-www-form-urlencoded` syntax. | |
1309 | /// | |
1310 | /// The return value has a method-chaining API: | |
1311 | /// | |
1312 | /// ```rust | |
1313 | /// # use url::{Url, ParseError}; | |
1314 | /// | |
1315 | /// # fn run() -> Result<(), ParseError> { | |
1316 | /// let mut url = Url::parse("https://example.net?lang=fr#nav")?; | |
1317 | /// assert_eq!(url.query(), Some("lang=fr")); | |
1318 | /// | |
1319 | /// url.query_pairs_mut().append_pair("foo", "bar"); | |
1320 | /// assert_eq!(url.query(), Some("lang=fr&foo=bar")); | |
1321 | /// assert_eq!(url.as_str(), "https://example.net/?lang=fr&foo=bar#nav"); | |
1322 | /// | |
1323 | /// url.query_pairs_mut() | |
1324 | /// .clear() | |
1325 | /// .append_pair("foo", "bar & baz") | |
1326 | /// .append_pair("saisons", "\u{00C9}t\u{00E9}+hiver"); | |
1327 | /// assert_eq!(url.query(), Some("foo=bar+%26+baz&saisons=%C3%89t%C3%A9%2Bhiver")); | |
1328 | /// assert_eq!(url.as_str(), | |
1329 | /// "https://example.net/?foo=bar+%26+baz&saisons=%C3%89t%C3%A9%2Bhiver#nav"); | |
1330 | /// # Ok(()) | |
1331 | /// # } | |
1332 | /// # run().unwrap(); | |
1333 | /// ``` | |
1334 | /// | |
1335 | /// Note: `url.query_pairs_mut().clear();` is equivalent to `url.set_query(Some(""))`, | |
1336 | /// not `url.set_query(None)`. | |
1337 | /// | |
1338 | /// The state of `Url` is unspecified if this return value is leaked without being dropped. | |
1339 | pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<UrlQuery> { | |
1340 | let fragment = self.take_fragment(); | |
1341 | ||
1342 | let query_start; | |
1343 | if let Some(start) = self.query_start { | |
1344 | debug_assert!(self.byte_at(start) == b'?'); | |
1345 | query_start = start as usize; | |
1346 | } else { | |
1347 | query_start = self.serialization.len(); | |
1348 | self.query_start = Some(to_u32(query_start).unwrap()); | |
1349 | self.serialization.push('?'); | |
1350 | } | |
1351 | ||
e74abb32 XL |
1352 | let query = UrlQuery { |
1353 | url: Some(self), | |
1354 | fragment, | |
1355 | }; | |
abe05a73 XL |
1356 | form_urlencoded::Serializer::for_suffix(query, query_start + "?".len()) |
1357 | } | |
1358 | ||
1359 | fn take_after_path(&mut self) -> String { | |
1360 | match (self.query_start, self.fragment_start) { | |
1361 | (Some(i), _) | (None, Some(i)) => { | |
1362 | let after_path = self.slice(i..).to_owned(); | |
1363 | self.serialization.truncate(i as usize); | |
1364 | after_path | |
e74abb32 | 1365 | } |
abe05a73 XL |
1366 | (None, None) => String::new(), |
1367 | } | |
1368 | } | |
1369 | ||
1370 | /// Change this URL’s path. | |
1371 | /// | |
1372 | /// # Examples | |
1373 | /// | |
1374 | /// ```rust | |
1375 | /// use url::Url; | |
1376 | /// # use url::ParseError; | |
1377 | /// | |
1378 | /// # fn run() -> Result<(), ParseError> { | |
1379 | /// let mut url = Url::parse("https://example.com")?; | |
1380 | /// url.set_path("api/comments"); | |
2c00a5a8 | 1381 | /// assert_eq!(url.as_str(), "https://example.com/api/comments"); |
abe05a73 XL |
1382 | /// assert_eq!(url.path(), "/api/comments"); |
1383 | /// | |
1384 | /// let mut url = Url::parse("https://example.com/api")?; | |
1385 | /// url.set_path("data/report.csv"); | |
2c00a5a8 | 1386 | /// assert_eq!(url.as_str(), "https://example.com/data/report.csv"); |
abe05a73 XL |
1387 | /// assert_eq!(url.path(), "/data/report.csv"); |
1388 | /// # Ok(()) | |
1389 | /// # } | |
1390 | /// # run().unwrap(); | |
1391 | /// ``` | |
1392 | pub fn set_path(&mut self, mut path: &str) { | |
1393 | let after_path = self.take_after_path(); | |
1394 | let old_after_path_pos = to_u32(self.serialization.len()).unwrap(); | |
1395 | let cannot_be_a_base = self.cannot_be_a_base(); | |
1396 | let scheme_type = SchemeType::from(self.scheme()); | |
1397 | self.serialization.truncate(self.path_start as usize); | |
1398 | self.mutate(|parser| { | |
1399 | if cannot_be_a_base { | |
1400 | if path.starts_with('/') { | |
1401 | parser.serialization.push_str("%2F"); | |
1402 | path = &path[1..]; | |
1403 | } | |
1404 | parser.parse_cannot_be_a_base_path(parser::Input::new(path)); | |
1405 | } else { | |
e74abb32 | 1406 | let mut has_host = true; // FIXME |
abe05a73 XL |
1407 | parser.parse_path_start(scheme_type, &mut has_host, parser::Input::new(path)); |
1408 | } | |
1409 | }); | |
1410 | self.restore_after_path(old_after_path_pos, &after_path); | |
1411 | } | |
1412 | ||
1413 | /// Return an object with methods to manipulate this URL’s path segments. | |
1414 | /// | |
1415 | /// Return `Err(())` if this URL is cannot-be-a-base. | |
1416 | pub fn path_segments_mut(&mut self) -> Result<PathSegmentsMut, ()> { | |
1417 | if self.cannot_be_a_base() { | |
1418 | Err(()) | |
1419 | } else { | |
1420 | Ok(path_segments::new(self)) | |
1421 | } | |
1422 | } | |
1423 | ||
1424 | fn restore_after_path(&mut self, old_after_path_position: u32, after_path: &str) { | |
1425 | let new_after_path_position = to_u32(self.serialization.len()).unwrap(); | |
1426 | let adjust = |index: &mut u32| { | |
1427 | *index -= old_after_path_position; | |
1428 | *index += new_after_path_position; | |
1429 | }; | |
e74abb32 XL |
1430 | if let Some(ref mut index) = self.query_start { |
1431 | adjust(index) | |
1432 | } | |
1433 | if let Some(ref mut index) = self.fragment_start { | |
1434 | adjust(index) | |
1435 | } | |
abe05a73 XL |
1436 | self.serialization.push_str(after_path) |
1437 | } | |
1438 | ||
1439 | /// Change this URL’s port number. | |
1440 | /// | |
e74abb32 XL |
1441 | /// Note that default port numbers are not reflected in the serialization. |
1442 | /// | |
abe05a73 XL |
1443 | /// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; |
1444 | /// do nothing and return `Err`. | |
1445 | /// | |
1446 | /// # Examples | |
1447 | /// | |
1448 | /// ``` | |
1449 | /// use url::Url; | |
1450 | /// # use std::error::Error; | |
1451 | /// | |
1452 | /// # fn run() -> Result<(), Box<Error>> { | |
1453 | /// let mut url = Url::parse("ssh://example.net:2048/")?; | |
1454 | /// | |
1455 | /// url.set_port(Some(4096)).map_err(|_| "cannot be base")?; | |
1456 | /// assert_eq!(url.as_str(), "ssh://example.net:4096/"); | |
1457 | /// | |
1458 | /// url.set_port(None).map_err(|_| "cannot be base")?; | |
1459 | /// assert_eq!(url.as_str(), "ssh://example.net/"); | |
1460 | /// # Ok(()) | |
1461 | /// # } | |
1462 | /// # run().unwrap(); | |
1463 | /// ``` | |
1464 | /// | |
e74abb32 XL |
1465 | /// Known default port numbers are not reflected: |
1466 | /// | |
1467 | /// ```rust | |
1468 | /// use url::Url; | |
1469 | /// # use std::error::Error; | |
1470 | /// | |
1471 | /// # fn run() -> Result<(), Box<Error>> { | |
1472 | /// let mut url = Url::parse("https://example.org/")?; | |
1473 | /// | |
1474 | /// url.set_port(Some(443)).map_err(|_| "cannot be base")?; | |
1475 | /// assert!(url.port().is_none()); | |
1476 | /// # Ok(()) | |
1477 | /// # } | |
1478 | /// # run().unwrap(); | |
1479 | /// ``` | |
1480 | /// | |
abe05a73 XL |
1481 | /// Cannot set port for cannot-be-a-base URLs: |
1482 | /// | |
1483 | /// ``` | |
1484 | /// use url::Url; | |
1485 | /// # use url::ParseError; | |
1486 | /// | |
1487 | /// # fn run() -> Result<(), ParseError> { | |
1488 | /// let mut url = Url::parse("mailto:rms@example.net")?; | |
1489 | /// | |
1490 | /// let result = url.set_port(Some(80)); | |
1491 | /// assert!(result.is_err()); | |
1492 | /// | |
1493 | /// let result = url.set_port(None); | |
1494 | /// assert!(result.is_err()); | |
1495 | /// # Ok(()) | |
1496 | /// # } | |
1497 | /// # run().unwrap(); | |
1498 | /// ``` | |
1499 | pub fn set_port(&mut self, mut port: Option<u16>) -> Result<(), ()> { | |
2c00a5a8 XL |
1500 | // has_host implies !cannot_be_a_base |
1501 | if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { | |
e74abb32 | 1502 | return Err(()); |
abe05a73 XL |
1503 | } |
1504 | if port.is_some() && port == parser::default_port(self.scheme()) { | |
1505 | port = None | |
1506 | } | |
1507 | self.set_port_internal(port); | |
1508 | Ok(()) | |
1509 | } | |
1510 | ||
1511 | fn set_port_internal(&mut self, port: Option<u16>) { | |
1512 | match (self.port, port) { | |
1513 | (None, None) => {} | |
1514 | (Some(_), None) => { | |
e74abb32 XL |
1515 | self.serialization |
1516 | .drain(self.host_end as usize..self.path_start as usize); | |
abe05a73 XL |
1517 | let offset = self.path_start - self.host_end; |
1518 | self.path_start = self.host_end; | |
e74abb32 XL |
1519 | if let Some(ref mut index) = self.query_start { |
1520 | *index -= offset | |
1521 | } | |
1522 | if let Some(ref mut index) = self.fragment_start { | |
1523 | *index -= offset | |
1524 | } | |
abe05a73 XL |
1525 | } |
1526 | (Some(old), Some(new)) if old == new => {} | |
1527 | (_, Some(new)) => { | |
1528 | let path_and_after = self.slice(self.path_start..).to_owned(); | |
1529 | self.serialization.truncate(self.host_end as usize); | |
1530 | write!(&mut self.serialization, ":{}", new).unwrap(); | |
1531 | let old_path_start = self.path_start; | |
1532 | let new_path_start = to_u32(self.serialization.len()).unwrap(); | |
1533 | self.path_start = new_path_start; | |
1534 | let adjust = |index: &mut u32| { | |
1535 | *index -= old_path_start; | |
1536 | *index += new_path_start; | |
1537 | }; | |
e74abb32 XL |
1538 | if let Some(ref mut index) = self.query_start { |
1539 | adjust(index) | |
1540 | } | |
1541 | if let Some(ref mut index) = self.fragment_start { | |
1542 | adjust(index) | |
1543 | } | |
abe05a73 XL |
1544 | self.serialization.push_str(&path_and_after); |
1545 | } | |
1546 | } | |
1547 | self.port = port; | |
1548 | } | |
1549 | ||
1550 | /// Change this URL’s host. | |
1551 | /// | |
1552 | /// Removing the host (calling this with `None`) | |
1553 | /// will also remove any username, password, and port number. | |
1554 | /// | |
1555 | /// # Examples | |
1556 | /// | |
1557 | /// Change host: | |
1558 | /// | |
1559 | /// ``` | |
1560 | /// use url::Url; | |
1561 | /// # use url::ParseError; | |
1562 | /// | |
1563 | /// # fn run() -> Result<(), ParseError> { | |
1564 | /// let mut url = Url::parse("https://example.net")?; | |
1565 | /// let result = url.set_host(Some("rust-lang.org")); | |
1566 | /// assert!(result.is_ok()); | |
1567 | /// assert_eq!(url.as_str(), "https://rust-lang.org/"); | |
1568 | /// # Ok(()) | |
1569 | /// # } | |
1570 | /// # run().unwrap(); | |
1571 | /// ``` | |
1572 | /// | |
1573 | /// Remove host: | |
1574 | /// | |
1575 | /// ``` | |
1576 | /// use url::Url; | |
1577 | /// # use url::ParseError; | |
2c00a5a8 | 1578 | /// |
abe05a73 XL |
1579 | /// # fn run() -> Result<(), ParseError> { |
1580 | /// let mut url = Url::parse("foo://example.net")?; | |
1581 | /// let result = url.set_host(None); | |
1582 | /// assert!(result.is_ok()); | |
1583 | /// assert_eq!(url.as_str(), "foo:/"); | |
1584 | /// # Ok(()) | |
1585 | /// # } | |
1586 | /// # run().unwrap(); | |
1587 | /// ``` | |
1588 | /// | |
1589 | /// Cannot remove host for 'special' schemes (e.g. `http`): | |
1590 | /// | |
1591 | /// ``` | |
1592 | /// use url::Url; | |
1593 | /// # use url::ParseError; | |
2c00a5a8 | 1594 | /// |
abe05a73 XL |
1595 | /// # fn run() -> Result<(), ParseError> { |
1596 | /// let mut url = Url::parse("https://example.net")?; | |
1597 | /// let result = url.set_host(None); | |
1598 | /// assert!(result.is_err()); | |
1599 | /// assert_eq!(url.as_str(), "https://example.net/"); | |
1600 | /// # Ok(()) | |
1601 | /// # } | |
1602 | /// # run().unwrap(); | |
1603 | /// ``` | |
1604 | /// | |
1605 | /// Cannot change or remove host for cannot-be-a-base URLs: | |
1606 | /// | |
1607 | /// ``` | |
1608 | /// use url::Url; | |
1609 | /// # use url::ParseError; | |
2c00a5a8 | 1610 | /// |
abe05a73 XL |
1611 | /// # fn run() -> Result<(), ParseError> { |
1612 | /// let mut url = Url::parse("mailto:rms@example.net")?; | |
1613 | /// | |
1614 | /// let result = url.set_host(Some("rust-lang.org")); | |
1615 | /// assert!(result.is_err()); | |
1616 | /// assert_eq!(url.as_str(), "mailto:rms@example.net"); | |
1617 | /// | |
1618 | /// let result = url.set_host(None); | |
1619 | /// assert!(result.is_err()); | |
1620 | /// assert_eq!(url.as_str(), "mailto:rms@example.net"); | |
1621 | /// # Ok(()) | |
1622 | /// # } | |
1623 | /// # run().unwrap(); | |
1624 | /// ``` | |
1625 | /// | |
1626 | /// # Errors | |
1627 | /// | |
1628 | /// If this URL is cannot-be-a-base or there is an error parsing the given `host`, | |
1629 | /// a [`ParseError`] variant will be returned. | |
1630 | /// | |
1631 | /// [`ParseError`]: enum.ParseError.html | |
1632 | pub fn set_host(&mut self, host: Option<&str>) -> Result<(), ParseError> { | |
1633 | if self.cannot_be_a_base() { | |
e74abb32 | 1634 | return Err(ParseError::SetHostOnCannotBeABaseUrl); |
abe05a73 XL |
1635 | } |
1636 | ||
1637 | if let Some(host) = host { | |
1638 | if host == "" && SchemeType::from(self.scheme()).is_special() { | |
1639 | return Err(ParseError::EmptyHost); | |
1640 | } | |
f035d41b XL |
1641 | let mut host_substr = host; |
1642 | // Otherwise, if c is U+003A (:) and the [] flag is unset, then | |
1643 | if !host.starts_with('[') || !host.ends_with(']') { | |
1644 | match host.find(':') { | |
1645 | Some(0) => { | |
1646 | // If buffer is the empty string, validation error, return failure. | |
1647 | return Err(ParseError::InvalidDomainCharacter); | |
1648 | } | |
1649 | // Let host be the result of host parsing buffer | |
1650 | Some(colon_index) => { | |
1651 | host_substr = &host[..colon_index]; | |
1652 | } | |
1653 | None => {} | |
1654 | } | |
1655 | } | |
2c00a5a8 | 1656 | if SchemeType::from(self.scheme()).is_special() { |
f035d41b | 1657 | self.set_host_internal(Host::parse(host_substr)?, None); |
2c00a5a8 | 1658 | } else { |
f035d41b | 1659 | self.set_host_internal(Host::parse_opaque(host_substr)?, None); |
2c00a5a8 | 1660 | } |
abe05a73 | 1661 | } else if self.has_host() { |
f035d41b XL |
1662 | let scheme_type = SchemeType::from(self.scheme()); |
1663 | if scheme_type.is_special() { | |
e74abb32 | 1664 | return Err(ParseError::EmptyHost); |
f035d41b XL |
1665 | } else { |
1666 | if self.serialization.len() == self.path_start as usize { | |
1667 | self.serialization.push('/'); | |
1668 | } | |
abe05a73 XL |
1669 | } |
1670 | debug_assert!(self.byte_at(self.scheme_end) == b':'); | |
1671 | debug_assert!(self.byte_at(self.path_start) == b'/'); | |
1672 | let new_path_start = self.scheme_end + 1; | |
e74abb32 XL |
1673 | self.serialization |
1674 | .drain(new_path_start as usize..self.path_start as usize); | |
abe05a73 XL |
1675 | let offset = self.path_start - new_path_start; |
1676 | self.path_start = new_path_start; | |
1677 | self.username_end = new_path_start; | |
1678 | self.host_start = new_path_start; | |
1679 | self.host_end = new_path_start; | |
1680 | self.port = None; | |
e74abb32 XL |
1681 | if let Some(ref mut index) = self.query_start { |
1682 | *index -= offset | |
1683 | } | |
1684 | if let Some(ref mut index) = self.fragment_start { | |
1685 | *index -= offset | |
1686 | } | |
abe05a73 XL |
1687 | } |
1688 | Ok(()) | |
1689 | } | |
1690 | ||
1691 | /// opt_new_port: None means leave unchanged, Some(None) means remove any port number. | |
1692 | fn set_host_internal(&mut self, host: Host<String>, opt_new_port: Option<Option<u16>>) { | |
e74abb32 XL |
1693 | let old_suffix_pos = if opt_new_port.is_some() { |
1694 | self.path_start | |
1695 | } else { | |
1696 | self.host_end | |
1697 | }; | |
abe05a73 XL |
1698 | let suffix = self.slice(old_suffix_pos..).to_owned(); |
1699 | self.serialization.truncate(self.host_start as usize); | |
1700 | if !self.has_authority() { | |
1701 | debug_assert!(self.slice(self.scheme_end..self.host_start) == ":"); | |
1702 | debug_assert!(self.username_end == self.host_start); | |
1703 | self.serialization.push('/'); | |
1704 | self.serialization.push('/'); | |
1705 | self.username_end += 2; | |
1706 | self.host_start += 2; | |
1707 | } | |
1708 | write!(&mut self.serialization, "{}", host).unwrap(); | |
1709 | self.host_end = to_u32(self.serialization.len()).unwrap(); | |
1710 | self.host = host.into(); | |
1711 | ||
1712 | if let Some(new_port) = opt_new_port { | |
1713 | self.port = new_port; | |
1714 | if let Some(port) = new_port { | |
1715 | write!(&mut self.serialization, ":{}", port).unwrap(); | |
1716 | } | |
1717 | } | |
1718 | let new_suffix_pos = to_u32(self.serialization.len()).unwrap(); | |
1719 | self.serialization.push_str(&suffix); | |
1720 | ||
1721 | let adjust = |index: &mut u32| { | |
1722 | *index -= old_suffix_pos; | |
1723 | *index += new_suffix_pos; | |
1724 | }; | |
1725 | adjust(&mut self.path_start); | |
e74abb32 XL |
1726 | if let Some(ref mut index) = self.query_start { |
1727 | adjust(index) | |
1728 | } | |
1729 | if let Some(ref mut index) = self.fragment_start { | |
1730 | adjust(index) | |
1731 | } | |
abe05a73 XL |
1732 | } |
1733 | ||
1734 | /// Change this URL’s host to the given IP address. | |
1735 | /// | |
1736 | /// If this URL is cannot-be-a-base, do nothing and return `Err`. | |
1737 | /// | |
1738 | /// Compared to `Url::set_host`, this skips the host parser. | |
1739 | /// | |
1740 | /// # Examples | |
1741 | /// | |
1742 | /// ```rust | |
1743 | /// use url::{Url, ParseError}; | |
1744 | /// | |
1745 | /// # fn run() -> Result<(), ParseError> { | |
1746 | /// let mut url = Url::parse("http://example.com")?; | |
1747 | /// url.set_ip_host("127.0.0.1".parse().unwrap()); | |
1748 | /// assert_eq!(url.host_str(), Some("127.0.0.1")); | |
1749 | /// assert_eq!(url.as_str(), "http://127.0.0.1/"); | |
1750 | /// # Ok(()) | |
1751 | /// # } | |
1752 | /// # run().unwrap(); | |
1753 | /// ``` | |
1754 | /// | |
1755 | /// Cannot change URL's from mailto(cannot-be-base) to ip: | |
1756 | /// | |
1757 | /// ```rust | |
1758 | /// use url::{Url, ParseError}; | |
1759 | /// | |
1760 | /// # fn run() -> Result<(), ParseError> { | |
1761 | /// let mut url = Url::parse("mailto:rms@example.com")?; | |
1762 | /// let result = url.set_ip_host("127.0.0.1".parse().unwrap()); | |
1763 | /// | |
1764 | /// assert_eq!(url.as_str(), "mailto:rms@example.com"); | |
1765 | /// assert!(result.is_err()); | |
1766 | /// # Ok(()) | |
1767 | /// # } | |
1768 | /// # run().unwrap(); | |
1769 | /// ``` | |
1770 | /// | |
1771 | pub fn set_ip_host(&mut self, address: IpAddr) -> Result<(), ()> { | |
1772 | if self.cannot_be_a_base() { | |
e74abb32 | 1773 | return Err(()); |
abe05a73 XL |
1774 | } |
1775 | ||
1776 | let address = match address { | |
1777 | IpAddr::V4(address) => Host::Ipv4(address), | |
1778 | IpAddr::V6(address) => Host::Ipv6(address), | |
1779 | }; | |
1780 | self.set_host_internal(address, None); | |
1781 | Ok(()) | |
1782 | } | |
1783 | ||
1784 | /// Change this URL’s password. | |
1785 | /// | |
1786 | /// If this URL is cannot-be-a-base or does not have a host, do nothing and return `Err`. | |
1787 | /// | |
1788 | /// # Examples | |
1789 | /// | |
1790 | /// ```rust | |
1791 | /// use url::{Url, ParseError}; | |
1792 | /// | |
1793 | /// # fn run() -> Result<(), ParseError> { | |
1794 | /// let mut url = Url::parse("mailto:rmz@example.com")?; | |
1795 | /// let result = url.set_password(Some("secret_password")); | |
1796 | /// assert!(result.is_err()); | |
1797 | /// | |
1798 | /// let mut url = Url::parse("ftp://user1:secret1@example.com")?; | |
1799 | /// let result = url.set_password(Some("secret_password")); | |
1800 | /// assert_eq!(url.password(), Some("secret_password")); | |
1801 | /// | |
1802 | /// let mut url = Url::parse("ftp://user2:@example.com")?; | |
1803 | /// let result = url.set_password(Some("secret2")); | |
1804 | /// assert!(result.is_ok()); | |
1805 | /// assert_eq!(url.password(), Some("secret2")); | |
1806 | /// # Ok(()) | |
1807 | /// # } | |
1808 | /// # run().unwrap(); | |
1809 | /// ``` | |
1810 | pub fn set_password(&mut self, password: Option<&str>) -> Result<(), ()> { | |
2c00a5a8 XL |
1811 | // has_host implies !cannot_be_a_base |
1812 | if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { | |
e74abb32 | 1813 | return Err(()); |
abe05a73 XL |
1814 | } |
1815 | if let Some(password) = password { | |
1816 | let host_and_after = self.slice(self.host_start..).to_owned(); | |
1817 | self.serialization.truncate(self.username_end as usize); | |
1818 | self.serialization.push(':'); | |
e74abb32 XL |
1819 | self.serialization |
1820 | .extend(utf8_percent_encode(password, USERINFO)); | |
abe05a73 XL |
1821 | self.serialization.push('@'); |
1822 | ||
1823 | let old_host_start = self.host_start; | |
1824 | let new_host_start = to_u32(self.serialization.len()).unwrap(); | |
1825 | let adjust = |index: &mut u32| { | |
1826 | *index -= old_host_start; | |
1827 | *index += new_host_start; | |
1828 | }; | |
1829 | self.host_start = new_host_start; | |
1830 | adjust(&mut self.host_end); | |
1831 | adjust(&mut self.path_start); | |
e74abb32 XL |
1832 | if let Some(ref mut index) = self.query_start { |
1833 | adjust(index) | |
1834 | } | |
1835 | if let Some(ref mut index) = self.fragment_start { | |
1836 | adjust(index) | |
1837 | } | |
abe05a73 XL |
1838 | |
1839 | self.serialization.push_str(&host_and_after); | |
e74abb32 XL |
1840 | } else if self.byte_at(self.username_end) == b':' { |
1841 | // If there is a password to remove | |
abe05a73 XL |
1842 | let has_username_or_password = self.byte_at(self.host_start - 1) == b'@'; |
1843 | debug_assert!(has_username_or_password); | |
1844 | let username_start = self.scheme_end + 3; | |
1845 | let empty_username = username_start == self.username_end; | |
e74abb32 | 1846 | let start = self.username_end; // Remove the ':' |
abe05a73 XL |
1847 | let end = if empty_username { |
1848 | self.host_start // Remove the '@' as well | |
1849 | } else { | |
e74abb32 | 1850 | self.host_start - 1 // Keep the '@' to separate the username from the host |
abe05a73 | 1851 | }; |
e74abb32 | 1852 | self.serialization.drain(start as usize..end as usize); |
abe05a73 XL |
1853 | let offset = end - start; |
1854 | self.host_start -= offset; | |
1855 | self.host_end -= offset; | |
1856 | self.path_start -= offset; | |
e74abb32 XL |
1857 | if let Some(ref mut index) = self.query_start { |
1858 | *index -= offset | |
1859 | } | |
1860 | if let Some(ref mut index) = self.fragment_start { | |
1861 | *index -= offset | |
1862 | } | |
abe05a73 XL |
1863 | } |
1864 | Ok(()) | |
1865 | } | |
1866 | ||
1867 | /// Change this URL’s username. | |
1868 | /// | |
1869 | /// If this URL is cannot-be-a-base or does not have a host, do nothing and return `Err`. | |
1870 | /// # Examples | |
1871 | /// | |
1872 | /// Cannot setup username from mailto(cannot-be-base) | |
1873 | /// | |
1874 | /// ```rust | |
1875 | /// use url::{Url, ParseError}; | |
1876 | /// | |
1877 | /// # fn run() -> Result<(), ParseError> { | |
1878 | /// let mut url = Url::parse("mailto:rmz@example.com")?; | |
1879 | /// let result = url.set_username("user1"); | |
1880 | /// assert_eq!(url.as_str(), "mailto:rmz@example.com"); | |
1881 | /// assert!(result.is_err()); | |
1882 | /// # Ok(()) | |
1883 | /// # } | |
1884 | /// # run().unwrap(); | |
1885 | /// ``` | |
1886 | /// | |
1887 | /// Setup username to user1 | |
1888 | /// | |
1889 | /// ```rust | |
1890 | /// use url::{Url, ParseError}; | |
1891 | /// | |
1892 | /// # fn run() -> Result<(), ParseError> { | |
1893 | /// let mut url = Url::parse("ftp://:secre1@example.com/")?; | |
1894 | /// let result = url.set_username("user1"); | |
1895 | /// assert!(result.is_ok()); | |
1896 | /// assert_eq!(url.username(), "user1"); | |
1897 | /// assert_eq!(url.as_str(), "ftp://user1:secre1@example.com/"); | |
1898 | /// # Ok(()) | |
1899 | /// # } | |
1900 | /// # run().unwrap(); | |
1901 | /// ``` | |
1902 | pub fn set_username(&mut self, username: &str) -> Result<(), ()> { | |
2c00a5a8 XL |
1903 | // has_host implies !cannot_be_a_base |
1904 | if !self.has_host() || self.host() == Some(Host::Domain("")) || self.scheme() == "file" { | |
e74abb32 | 1905 | return Err(()); |
abe05a73 XL |
1906 | } |
1907 | let username_start = self.scheme_end + 3; | |
1908 | debug_assert!(self.slice(self.scheme_end..username_start) == "://"); | |
1909 | if self.slice(username_start..self.username_end) == username { | |
e74abb32 | 1910 | return Ok(()); |
abe05a73 XL |
1911 | } |
1912 | let after_username = self.slice(self.username_end..).to_owned(); | |
1913 | self.serialization.truncate(username_start as usize); | |
e74abb32 XL |
1914 | self.serialization |
1915 | .extend(utf8_percent_encode(username, USERINFO)); | |
abe05a73 XL |
1916 | |
1917 | let mut removed_bytes = self.username_end; | |
1918 | self.username_end = to_u32(self.serialization.len()).unwrap(); | |
1919 | let mut added_bytes = self.username_end; | |
1920 | ||
1921 | let new_username_is_empty = self.username_end == username_start; | |
1922 | match (new_username_is_empty, after_username.chars().next()) { | |
1923 | (true, Some('@')) => { | |
1924 | removed_bytes += 1; | |
1925 | self.serialization.push_str(&after_username[1..]); | |
1926 | } | |
1927 | (false, Some('@')) | (_, Some(':')) | (true, _) => { | |
1928 | self.serialization.push_str(&after_username); | |
1929 | } | |
1930 | (false, _) => { | |
1931 | added_bytes += 1; | |
1932 | self.serialization.push('@'); | |
1933 | self.serialization.push_str(&after_username); | |
1934 | } | |
1935 | } | |
1936 | ||
1937 | let adjust = |index: &mut u32| { | |
1938 | *index -= removed_bytes; | |
1939 | *index += added_bytes; | |
1940 | }; | |
1941 | adjust(&mut self.host_start); | |
1942 | adjust(&mut self.host_end); | |
1943 | adjust(&mut self.path_start); | |
e74abb32 XL |
1944 | if let Some(ref mut index) = self.query_start { |
1945 | adjust(index) | |
1946 | } | |
1947 | if let Some(ref mut index) = self.fragment_start { | |
1948 | adjust(index) | |
1949 | } | |
abe05a73 XL |
1950 | Ok(()) |
1951 | } | |
1952 | ||
1953 | /// Change this URL’s scheme. | |
1954 | /// | |
1955 | /// Do nothing and return `Err` if: | |
1956 | /// | |
1957 | /// * The new scheme is not in `[a-zA-Z][a-zA-Z0-9+.-]+` | |
1958 | /// * This URL is cannot-be-a-base and the new scheme is one of | |
1959 | /// `http`, `https`, `ws`, `wss`, `ftp`, or `gopher` | |
1960 | /// | |
1961 | /// # Examples | |
1962 | /// | |
1963 | /// Change the URL’s scheme from `https` to `foo`: | |
1964 | /// | |
1965 | /// ``` | |
1966 | /// use url::Url; | |
1967 | /// # use url::ParseError; | |
2c00a5a8 | 1968 | /// |
abe05a73 XL |
1969 | /// # fn run() -> Result<(), ParseError> { |
1970 | /// let mut url = Url::parse("https://example.net")?; | |
f035d41b XL |
1971 | /// let result = url.set_scheme("http"); |
1972 | /// assert_eq!(url.as_str(), "http://example.net/"); | |
abe05a73 XL |
1973 | /// assert!(result.is_ok()); |
1974 | /// # Ok(()) | |
1975 | /// # } | |
1976 | /// # run().unwrap(); | |
1977 | /// ``` | |
f035d41b | 1978 | /// Change the URL’s scheme from `foo` to `bar`: |
abe05a73 | 1979 | /// |
f035d41b XL |
1980 | /// ``` |
1981 | /// use url::Url; | |
1982 | /// # use url::ParseError; | |
1983 | /// | |
1984 | /// # fn run() -> Result<(), ParseError> { | |
1985 | /// let mut url = Url::parse("foo://example.net")?; | |
1986 | /// let result = url.set_scheme("bar"); | |
1987 | /// assert_eq!(url.as_str(), "bar://example.net"); | |
1988 | /// assert!(result.is_ok()); | |
1989 | /// # Ok(()) | |
1990 | /// # } | |
1991 | /// # run().unwrap(); | |
1992 | /// ``` | |
abe05a73 XL |
1993 | /// |
1994 | /// Cannot change URL’s scheme from `https` to `foõ`: | |
1995 | /// | |
1996 | /// ``` | |
1997 | /// use url::Url; | |
1998 | /// # use url::ParseError; | |
2c00a5a8 | 1999 | /// |
abe05a73 XL |
2000 | /// # fn run() -> Result<(), ParseError> { |
2001 | /// let mut url = Url::parse("https://example.net")?; | |
2002 | /// let result = url.set_scheme("foõ"); | |
2003 | /// assert_eq!(url.as_str(), "https://example.net/"); | |
2004 | /// assert!(result.is_err()); | |
2005 | /// # Ok(()) | |
2006 | /// # } | |
2007 | /// # run().unwrap(); | |
2008 | /// ``` | |
2009 | /// | |
2010 | /// Cannot change URL’s scheme from `mailto` (cannot-be-a-base) to `https`: | |
2011 | /// | |
2012 | /// ``` | |
2013 | /// use url::Url; | |
2014 | /// # use url::ParseError; | |
2c00a5a8 | 2015 | /// |
abe05a73 XL |
2016 | /// # fn run() -> Result<(), ParseError> { |
2017 | /// let mut url = Url::parse("mailto:rms@example.net")?; | |
2018 | /// let result = url.set_scheme("https"); | |
2019 | /// assert_eq!(url.as_str(), "mailto:rms@example.net"); | |
2020 | /// assert!(result.is_err()); | |
2021 | /// # Ok(()) | |
2022 | /// # } | |
2023 | /// # run().unwrap(); | |
2024 | /// ``` | |
f035d41b XL |
2025 | /// Cannot change the URL’s scheme from `foo` to `https`: |
2026 | /// | |
2027 | /// ``` | |
2028 | /// use url::Url; | |
2029 | /// # use url::ParseError; | |
2030 | /// | |
2031 | /// # fn run() -> Result<(), ParseError> { | |
2032 | /// let mut url = Url::parse("foo://example.net")?; | |
2033 | /// let result = url.set_scheme("https"); | |
2034 | /// assert_eq!(url.as_str(), "foo://example.net"); | |
2035 | /// assert!(result.is_err()); | |
2036 | /// # Ok(()) | |
2037 | /// # } | |
2038 | /// # run().unwrap(); | |
2039 | /// ``` | |
2040 | /// Cannot change the URL’s scheme from `http` to `foo`: | |
2041 | /// | |
2042 | /// ``` | |
2043 | /// use url::Url; | |
2044 | /// # use url::ParseError; | |
2045 | /// | |
2046 | /// # fn run() -> Result<(), ParseError> { | |
2047 | /// let mut url = Url::parse("http://example.net")?; | |
2048 | /// let result = url.set_scheme("foo"); | |
2049 | /// assert_eq!(url.as_str(), "http://example.net/"); | |
2050 | /// assert!(result.is_err()); | |
2051 | /// # Ok(()) | |
2052 | /// # } | |
2053 | /// # run().unwrap(); | |
2054 | /// ``` | |
abe05a73 XL |
2055 | pub fn set_scheme(&mut self, scheme: &str) -> Result<(), ()> { |
2056 | let mut parser = Parser::for_setter(String::new()); | |
2057 | let remaining = parser.parse_scheme(parser::Input::new(scheme))?; | |
f035d41b XL |
2058 | let new_scheme_type = SchemeType::from(&parser.serialization); |
2059 | let old_scheme_type = SchemeType::from(self.scheme()); | |
2060 | // If url’s scheme is a special scheme and buffer is not a special scheme, then return. | |
2061 | if (new_scheme_type.is_special() && !old_scheme_type.is_special()) || | |
2062 | // If url’s scheme is not a special scheme and buffer is a special scheme, then return. | |
2063 | (!new_scheme_type.is_special() && old_scheme_type.is_special()) || | |
2064 | // If url includes credentials or has a non-null port, and buffer is "file", then return. | |
2065 | // If url’s scheme is "file" and its host is an empty host or null, then return. | |
2066 | (new_scheme_type.is_file() && self.has_authority()) | |
e74abb32 XL |
2067 | { |
2068 | return Err(()); | |
abe05a73 | 2069 | } |
f035d41b XL |
2070 | |
2071 | if !remaining.is_empty() || (!self.has_host() && new_scheme_type.is_special()) { | |
2072 | return Err(()); | |
2073 | } | |
abe05a73 XL |
2074 | let old_scheme_end = self.scheme_end; |
2075 | let new_scheme_end = to_u32(parser.serialization.len()).unwrap(); | |
2076 | let adjust = |index: &mut u32| { | |
2077 | *index -= old_scheme_end; | |
2078 | *index += new_scheme_end; | |
2079 | }; | |
2080 | ||
2081 | self.scheme_end = new_scheme_end; | |
2082 | adjust(&mut self.username_end); | |
2083 | adjust(&mut self.host_start); | |
2084 | adjust(&mut self.host_end); | |
2085 | adjust(&mut self.path_start); | |
e74abb32 XL |
2086 | if let Some(ref mut index) = self.query_start { |
2087 | adjust(index) | |
2088 | } | |
2089 | if let Some(ref mut index) = self.fragment_start { | |
2090 | adjust(index) | |
2091 | } | |
abe05a73 XL |
2092 | |
2093 | parser.serialization.push_str(self.slice(old_scheme_end..)); | |
2094 | self.serialization = parser.serialization; | |
f035d41b XL |
2095 | |
2096 | // Update the port so it can be removed | |
2097 | // If it is the scheme's default | |
2098 | // we don't mind it silently failing | |
2099 | // if there was no port in the first place | |
2100 | let previous_port = self.port(); | |
2101 | let _ = self.set_port(previous_port); | |
2102 | ||
abe05a73 XL |
2103 | Ok(()) |
2104 | } | |
2105 | ||
2106 | /// Convert a file name as `std::path::Path` into an URL in the `file` scheme. | |
2107 | /// | |
2108 | /// This returns `Err` if the given path is not absolute or, | |
2109 | /// on Windows, if the prefix is not a disk prefix (e.g. `C:`) or a UNC prefix (`\\`). | |
2110 | /// | |
2111 | /// # Examples | |
2112 | /// | |
2113 | /// On Unix-like platforms: | |
2114 | /// | |
2115 | /// ``` | |
2116 | /// # if cfg!(unix) { | |
2117 | /// use url::Url; | |
2c00a5a8 | 2118 | /// |
abe05a73 XL |
2119 | /// # fn run() -> Result<(), ()> { |
2120 | /// let url = Url::from_file_path("/tmp/foo.txt")?; | |
2121 | /// assert_eq!(url.as_str(), "file:///tmp/foo.txt"); | |
2122 | /// | |
2123 | /// let url = Url::from_file_path("../foo.txt"); | |
2124 | /// assert!(url.is_err()); | |
2125 | /// | |
2126 | /// let url = Url::from_file_path("https://google.com/"); | |
2127 | /// assert!(url.is_err()); | |
2128 | /// # Ok(()) | |
2129 | /// # } | |
2130 | /// # run().unwrap(); | |
2131 | /// # } | |
2132 | /// ``` | |
e74abb32 | 2133 | #[cfg(any(unix, windows, target_os = "redox"))] |
abe05a73 XL |
2134 | pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> { |
2135 | let mut serialization = "file://".to_owned(); | |
2136 | let host_start = serialization.len() as u32; | |
2137 | let (host_end, host) = path_to_file_url_segments(path.as_ref(), &mut serialization)?; | |
2138 | Ok(Url { | |
e74abb32 | 2139 | serialization, |
abe05a73 XL |
2140 | scheme_end: "file".len() as u32, |
2141 | username_end: host_start, | |
e74abb32 XL |
2142 | host_start, |
2143 | host_end, | |
2144 | host, | |
abe05a73 XL |
2145 | port: None, |
2146 | path_start: host_end, | |
2147 | query_start: None, | |
2148 | fragment_start: None, | |
2149 | }) | |
2150 | } | |
2151 | ||
2152 | /// Convert a directory name as `std::path::Path` into an URL in the `file` scheme. | |
2153 | /// | |
2154 | /// This returns `Err` if the given path is not absolute or, | |
2155 | /// on Windows, if the prefix is not a disk prefix (e.g. `C:`) or a UNC prefix (`\\`). | |
2156 | /// | |
2157 | /// Compared to `from_file_path`, this ensure that URL’s the path has a trailing slash | |
2158 | /// so that the entire path is considered when using this URL as a base URL. | |
2159 | /// | |
2160 | /// For example: | |
2161 | /// | |
2162 | /// * `"index.html"` parsed with `Url::from_directory_path(Path::new("/var/www"))` | |
2163 | /// as the base URL is `file:///var/www/index.html` | |
2164 | /// * `"index.html"` parsed with `Url::from_file_path(Path::new("/var/www"))` | |
2165 | /// as the base URL is `file:///var/index.html`, which might not be what was intended. | |
2166 | /// | |
2167 | /// Note that `std::path` does not consider trailing slashes significant | |
2168 | /// and usually does not include them (e.g. in `Path::parent()`). | |
e74abb32 | 2169 | #[cfg(any(unix, windows, target_os = "redox"))] |
abe05a73 XL |
2170 | pub fn from_directory_path<P: AsRef<Path>>(path: P) -> Result<Url, ()> { |
2171 | let mut url = Url::from_file_path(path)?; | |
2172 | if !url.serialization.ends_with('/') { | |
2173 | url.serialization.push('/') | |
2174 | } | |
2175 | Ok(url) | |
2176 | } | |
2177 | ||
2178 | /// Serialize with Serde using the internal representation of the `Url` struct. | |
2179 | /// | |
2180 | /// The corresponding `deserialize_internal` method sacrifices some invariant-checking | |
2181 | /// for speed, compared to the `Deserialize` trait impl. | |
2182 | /// | |
2183 | /// This method is only available if the `serde` Cargo feature is enabled. | |
2184 | #[cfg(feature = "serde")] | |
2185 | #[deny(unused)] | |
e74abb32 XL |
2186 | pub fn serialize_internal<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
2187 | where | |
2188 | S: serde::Serializer, | |
2189 | { | |
abe05a73 XL |
2190 | use serde::Serialize; |
2191 | // Destructuring first lets us ensure that adding or removing fields forces this method | |
2192 | // to be updated | |
e74abb32 XL |
2193 | let Url { |
2194 | ref serialization, | |
2195 | ref scheme_end, | |
2196 | ref username_end, | |
2197 | ref host_start, | |
2198 | ref host_end, | |
2199 | ref host, | |
2200 | ref port, | |
2201 | ref path_start, | |
2202 | ref query_start, | |
2203 | ref fragment_start, | |
2204 | } = *self; | |
2205 | ( | |
2206 | serialization, | |
2207 | scheme_end, | |
2208 | username_end, | |
2209 | host_start, | |
2210 | host_end, | |
2211 | host, | |
2212 | port, | |
2213 | path_start, | |
2214 | query_start, | |
2215 | fragment_start, | |
2216 | ) | |
2217 | .serialize(serializer) | |
abe05a73 XL |
2218 | } |
2219 | ||
2220 | /// Serialize with Serde using the internal representation of the `Url` struct. | |
2221 | /// | |
2222 | /// The corresponding `deserialize_internal` method sacrifices some invariant-checking | |
2223 | /// for speed, compared to the `Deserialize` trait impl. | |
2224 | /// | |
2225 | /// This method is only available if the `serde` Cargo feature is enabled. | |
2226 | #[cfg(feature = "serde")] | |
2227 | #[deny(unused)] | |
e74abb32 XL |
2228 | pub fn deserialize_internal<'de, D>(deserializer: D) -> Result<Self, D::Error> |
2229 | where | |
2230 | D: serde::Deserializer<'de>, | |
2231 | { | |
2232 | use serde::de::{Deserialize, Error, Unexpected}; | |
2233 | let ( | |
2234 | serialization, | |
2235 | scheme_end, | |
2236 | username_end, | |
2237 | host_start, | |
2238 | host_end, | |
2239 | host, | |
2240 | port, | |
2241 | path_start, | |
2242 | query_start, | |
2243 | fragment_start, | |
2244 | ) = Deserialize::deserialize(deserializer)?; | |
abe05a73 | 2245 | let url = Url { |
e74abb32 XL |
2246 | serialization, |
2247 | scheme_end, | |
2248 | username_end, | |
2249 | host_start, | |
2250 | host_end, | |
2251 | host, | |
2252 | port, | |
2253 | path_start, | |
2254 | query_start, | |
2255 | fragment_start, | |
abe05a73 XL |
2256 | }; |
2257 | if cfg!(debug_assertions) { | |
e74abb32 XL |
2258 | url.check_invariants().map_err(|reason| { |
2259 | let reason: &str = &reason; | |
2260 | Error::invalid_value(Unexpected::Other("value"), &reason) | |
2261 | })? | |
abe05a73 XL |
2262 | } |
2263 | Ok(url) | |
2264 | } | |
2265 | ||
abe05a73 XL |
2266 | /// Assuming the URL is in the `file` scheme or similar, |
2267 | /// convert its path to an absolute `std::path::Path`. | |
2268 | /// | |
2269 | /// **Note:** This does not actually check the URL’s `scheme`, | |
2270 | /// and may give nonsensical results for other schemes. | |
2271 | /// It is the user’s responsibility to check the URL’s scheme before calling this. | |
2272 | /// | |
2273 | /// ``` | |
2274 | /// # use url::Url; | |
2275 | /// # let url = Url::parse("file:///etc/passwd").unwrap(); | |
2276 | /// let path = url.to_file_path(); | |
2277 | /// ``` | |
2278 | /// | |
2279 | /// Returns `Err` if the host is neither empty nor `"localhost"` (except on Windows, where | |
2280 | /// `file:` URLs may have a non-local host), | |
2281 | /// or if `Path::new_opt()` returns `None`. | |
2282 | /// (That is, if the percent-decoded path contains a NUL byte or, | |
2283 | /// for a Windows path, is not UTF-8.) | |
2284 | #[inline] | |
e74abb32 | 2285 | #[cfg(any(unix, windows, target_os = "redox"))] |
abe05a73 XL |
2286 | pub fn to_file_path(&self) -> Result<PathBuf, ()> { |
2287 | if let Some(segments) = self.path_segments() { | |
2288 | let host = match self.host() { | |
2289 | None | Some(Host::Domain("localhost")) => None, | |
2290 | Some(_) if cfg!(windows) && self.scheme() == "file" => { | |
e74abb32 XL |
2291 | Some(&self.serialization[self.host_start as usize..self.host_end as usize]) |
2292 | } | |
2293 | _ => return Err(()), | |
abe05a73 XL |
2294 | }; |
2295 | ||
2296 | return file_url_segments_to_pathbuf(host, segments); | |
2297 | } | |
2298 | Err(()) | |
2299 | } | |
2300 | ||
2301 | // Private helper methods: | |
2302 | ||
2303 | #[inline] | |
e74abb32 XL |
2304 | fn slice<R>(&self, range: R) -> &str |
2305 | where | |
2306 | R: RangeArg, | |
2307 | { | |
abe05a73 XL |
2308 | range.slice_of(&self.serialization) |
2309 | } | |
2310 | ||
2311 | #[inline] | |
2312 | fn byte_at(&self, i: u32) -> u8 { | |
2313 | self.serialization.as_bytes()[i as usize] | |
2314 | } | |
2315 | } | |
2316 | ||
abe05a73 XL |
2317 | /// Parse a string as an URL, without a base URL or encoding override. |
2318 | impl str::FromStr for Url { | |
2319 | type Err = ParseError; | |
2320 | ||
2321 | #[inline] | |
2322 | fn from_str(input: &str) -> Result<Url, ::ParseError> { | |
2323 | Url::parse(input) | |
2324 | } | |
2325 | } | |
2326 | ||
2327 | /// Display the serialization of this URL. | |
2328 | impl fmt::Display for Url { | |
2329 | #[inline] | |
2330 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { | |
2331 | fmt::Display::fmt(&self.serialization, formatter) | |
2332 | } | |
2333 | } | |
2334 | ||
2335 | /// Debug the serialization of this URL. | |
2336 | impl fmt::Debug for Url { | |
2337 | #[inline] | |
2338 | fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { | |
2339 | fmt::Debug::fmt(&self.serialization, formatter) | |
2340 | } | |
2341 | } | |
2342 | ||
2343 | /// URLs compare like their serialization. | |
2344 | impl Eq for Url {} | |
2345 | ||
2346 | /// URLs compare like their serialization. | |
2347 | impl PartialEq for Url { | |
2348 | #[inline] | |
2349 | fn eq(&self, other: &Self) -> bool { | |
2350 | self.serialization == other.serialization | |
2351 | } | |
2352 | } | |
2353 | ||
2354 | /// URLs compare like their serialization. | |
2355 | impl Ord for Url { | |
2356 | #[inline] | |
2357 | fn cmp(&self, other: &Self) -> cmp::Ordering { | |
2358 | self.serialization.cmp(&other.serialization) | |
2359 | } | |
2360 | } | |
2361 | ||
2362 | /// URLs compare like their serialization. | |
2363 | impl PartialOrd for Url { | |
2364 | #[inline] | |
2365 | fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { | |
2366 | self.serialization.partial_cmp(&other.serialization) | |
2367 | } | |
2368 | } | |
2369 | ||
2370 | /// URLs hash like their serialization. | |
2371 | impl hash::Hash for Url { | |
2372 | #[inline] | |
e74abb32 XL |
2373 | fn hash<H>(&self, state: &mut H) |
2374 | where | |
2375 | H: hash::Hasher, | |
2376 | { | |
abe05a73 XL |
2377 | hash::Hash::hash(&self.serialization, state) |
2378 | } | |
2379 | } | |
2380 | ||
2381 | /// Return the serialization of this URL. | |
2382 | impl AsRef<str> for Url { | |
2383 | #[inline] | |
2384 | fn as_ref(&self) -> &str { | |
2385 | &self.serialization | |
2386 | } | |
2387 | } | |
2388 | ||
2389 | trait RangeArg { | |
2390 | fn slice_of<'a>(&self, s: &'a str) -> &'a str; | |
2391 | } | |
2392 | ||
2393 | impl RangeArg for Range<u32> { | |
2394 | #[inline] | |
2395 | fn slice_of<'a>(&self, s: &'a str) -> &'a str { | |
e74abb32 | 2396 | &s[self.start as usize..self.end as usize] |
abe05a73 XL |
2397 | } |
2398 | } | |
2399 | ||
2400 | impl RangeArg for RangeFrom<u32> { | |
2401 | #[inline] | |
2402 | fn slice_of<'a>(&self, s: &'a str) -> &'a str { | |
e74abb32 | 2403 | &s[self.start as usize..] |
abe05a73 XL |
2404 | } |
2405 | } | |
2406 | ||
2407 | impl RangeArg for RangeTo<u32> { | |
2408 | #[inline] | |
2409 | fn slice_of<'a>(&self, s: &'a str) -> &'a str { | |
e74abb32 | 2410 | &s[..self.end as usize] |
abe05a73 XL |
2411 | } |
2412 | } | |
2413 | ||
2414 | /// Serializes this URL into a `serde` stream. | |
2415 | /// | |
2416 | /// This implementation is only available if the `serde` Cargo feature is enabled. | |
e74abb32 | 2417 | #[cfg(feature = "serde")] |
abe05a73 | 2418 | impl serde::Serialize for Url { |
e74abb32 XL |
2419 | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> |
2420 | where | |
2421 | S: serde::Serializer, | |
2422 | { | |
abe05a73 XL |
2423 | serializer.serialize_str(self.as_str()) |
2424 | } | |
2425 | } | |
2426 | ||
2427 | /// Deserializes this URL from a `serde` stream. | |
2428 | /// | |
2429 | /// This implementation is only available if the `serde` Cargo feature is enabled. | |
e74abb32 XL |
2430 | #[cfg(feature = "serde")] |
2431 | impl<'de> serde::Deserialize<'de> for Url { | |
2432 | fn deserialize<D>(deserializer: D) -> Result<Url, D::Error> | |
2433 | where | |
2434 | D: serde::Deserializer<'de>, | |
2435 | { | |
2436 | use serde::de::{Error, Unexpected, Visitor}; | |
2437 | ||
2438 | struct UrlVisitor; | |
2439 | ||
2440 | impl<'de> Visitor<'de> for UrlVisitor { | |
2441 | type Value = Url; | |
2442 | ||
2443 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { | |
2444 | formatter.write_str("a string representing an URL") | |
2445 | } | |
2446 | ||
2447 | fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> | |
2448 | where | |
2449 | E: Error, | |
2450 | { | |
2451 | Url::parse(s) | |
2452 | .map_err(|err| Error::invalid_value(Unexpected::Str(s), &err.description())) | |
2453 | } | |
2454 | } | |
2455 | ||
2456 | deserializer.deserialize_str(UrlVisitor) | |
abe05a73 XL |
2457 | } |
2458 | } | |
2459 | ||
2460 | #[cfg(any(unix, target_os = "redox"))] | |
e74abb32 XL |
2461 | fn path_to_file_url_segments( |
2462 | path: &Path, | |
2463 | serialization: &mut String, | |
2464 | ) -> Result<(u32, HostInternal), ()> { | |
abe05a73 XL |
2465 | use std::os::unix::prelude::OsStrExt; |
2466 | if !path.is_absolute() { | |
e74abb32 | 2467 | return Err(()); |
abe05a73 XL |
2468 | } |
2469 | let host_end = to_u32(serialization.len()).unwrap(); | |
2470 | let mut empty = true; | |
2471 | // skip the root component | |
2472 | for component in path.components().skip(1) { | |
2473 | empty = false; | |
2474 | serialization.push('/'); | |
2475 | serialization.extend(percent_encode( | |
e74abb32 XL |
2476 | component.as_os_str().as_bytes(), |
2477 | PATH_SEGMENT, | |
2478 | )); | |
abe05a73 XL |
2479 | } |
2480 | if empty { | |
2481 | // An URL’s path must not be empty. | |
2482 | serialization.push('/'); | |
2483 | } | |
2484 | Ok((host_end, HostInternal::None)) | |
2485 | } | |
2486 | ||
2487 | #[cfg(windows)] | |
e74abb32 XL |
2488 | fn path_to_file_url_segments( |
2489 | path: &Path, | |
2490 | serialization: &mut String, | |
2491 | ) -> Result<(u32, HostInternal), ()> { | |
abe05a73 XL |
2492 | path_to_file_url_segments_windows(path, serialization) |
2493 | } | |
2494 | ||
2495 | // Build this unconditionally to alleviate https://github.com/servo/rust-url/issues/102 | |
2496 | #[cfg_attr(not(windows), allow(dead_code))] | |
e74abb32 XL |
2497 | fn path_to_file_url_segments_windows( |
2498 | path: &Path, | |
2499 | serialization: &mut String, | |
2500 | ) -> Result<(u32, HostInternal), ()> { | |
2501 | use std::path::{Component, Prefix}; | |
abe05a73 | 2502 | if !path.is_absolute() { |
e74abb32 | 2503 | return Err(()); |
abe05a73 XL |
2504 | } |
2505 | let mut components = path.components(); | |
2506 | ||
f035d41b | 2507 | let host_start = serialization.len() + 1; |
abe05a73 XL |
2508 | let host_end; |
2509 | let host_internal; | |
2510 | match components.next() { | |
2511 | Some(Component::Prefix(ref p)) => match p.kind() { | |
2512 | Prefix::Disk(letter) | Prefix::VerbatimDisk(letter) => { | |
2513 | host_end = to_u32(serialization.len()).unwrap(); | |
2514 | host_internal = HostInternal::None; | |
2515 | serialization.push('/'); | |
2516 | serialization.push(letter as char); | |
2517 | serialization.push(':'); | |
e74abb32 | 2518 | } |
abe05a73 XL |
2519 | Prefix::UNC(server, share) | Prefix::VerbatimUNC(server, share) => { |
2520 | let host = Host::parse(server.to_str().ok_or(())?).map_err(|_| ())?; | |
2521 | write!(serialization, "{}", host).unwrap(); | |
2522 | host_end = to_u32(serialization.len()).unwrap(); | |
2523 | host_internal = host.into(); | |
2524 | serialization.push('/'); | |
2525 | let share = share.to_str().ok_or(())?; | |
e74abb32 XL |
2526 | serialization.extend(percent_encode(share.as_bytes(), PATH_SEGMENT)); |
2527 | } | |
2528 | _ => return Err(()), | |
abe05a73 XL |
2529 | }, |
2530 | ||
e74abb32 | 2531 | _ => return Err(()), |
abe05a73 XL |
2532 | } |
2533 | ||
f035d41b | 2534 | let mut path_only_has_prefix = true; |
abe05a73 | 2535 | for component in components { |
e74abb32 XL |
2536 | if component == Component::RootDir { |
2537 | continue; | |
2538 | } | |
f035d41b | 2539 | path_only_has_prefix = false; |
abe05a73 XL |
2540 | // FIXME: somehow work with non-unicode? |
2541 | let component = component.as_os_str().to_str().ok_or(())?; | |
2542 | serialization.push('/'); | |
e74abb32 | 2543 | serialization.extend(percent_encode(component.as_bytes(), PATH_SEGMENT)); |
abe05a73 | 2544 | } |
f035d41b XL |
2545 | // A windows drive letter must end with a slash. |
2546 | if serialization.len() > host_start | |
2547 | && parser::is_windows_drive_letter(&serialization[host_start..]) | |
2548 | && path_only_has_prefix | |
2549 | { | |
2550 | serialization.push('/'); | |
2551 | } | |
abe05a73 XL |
2552 | Ok((host_end, host_internal)) |
2553 | } | |
2554 | ||
2555 | #[cfg(any(unix, target_os = "redox"))] | |
e74abb32 XL |
2556 | fn file_url_segments_to_pathbuf( |
2557 | host: Option<&str>, | |
2558 | segments: str::Split<char>, | |
2559 | ) -> Result<PathBuf, ()> { | |
abe05a73 XL |
2560 | use std::ffi::OsStr; |
2561 | use std::os::unix::prelude::OsStrExt; | |
abe05a73 XL |
2562 | |
2563 | if host.is_some() { | |
2564 | return Err(()); | |
2565 | } | |
2566 | ||
e74abb32 XL |
2567 | let mut bytes = if cfg!(target_os = "redox") { |
2568 | b"file:".to_vec() | |
2569 | } else { | |
2570 | Vec::new() | |
2571 | }; | |
abe05a73 XL |
2572 | for segment in segments { |
2573 | bytes.push(b'/'); | |
2574 | bytes.extend(percent_decode(segment.as_bytes())); | |
2575 | } | |
f035d41b XL |
2576 | // A windows drive letter must end with a slash. |
2577 | if bytes.len() > 2 { | |
2578 | if matches!(bytes[bytes.len() - 2], b'a'..=b'z' | b'A'..=b'Z') | |
2579 | && matches!(bytes[bytes.len() - 1], b':' | b'|') | |
2580 | { | |
2581 | bytes.push(b'/'); | |
2582 | } | |
2583 | } | |
abe05a73 XL |
2584 | let os_str = OsStr::from_bytes(&bytes); |
2585 | let path = PathBuf::from(os_str); | |
e74abb32 XL |
2586 | debug_assert!( |
2587 | path.is_absolute(), | |
2588 | "to_file_path() failed to produce an absolute Path" | |
2589 | ); | |
abe05a73 XL |
2590 | Ok(path) |
2591 | } | |
2592 | ||
2593 | #[cfg(windows)] | |
e74abb32 XL |
2594 | fn file_url_segments_to_pathbuf( |
2595 | host: Option<&str>, | |
2596 | segments: str::Split<char>, | |
2597 | ) -> Result<PathBuf, ()> { | |
abe05a73 XL |
2598 | file_url_segments_to_pathbuf_windows(host, segments) |
2599 | } | |
2600 | ||
2601 | // Build this unconditionally to alleviate https://github.com/servo/rust-url/issues/102 | |
2602 | #[cfg_attr(not(windows), allow(dead_code))] | |
e74abb32 XL |
2603 | fn file_url_segments_to_pathbuf_windows( |
2604 | host: Option<&str>, | |
2605 | mut segments: str::Split<char>, | |
2606 | ) -> Result<PathBuf, ()> { | |
abe05a73 XL |
2607 | let mut string = if let Some(host) = host { |
2608 | r"\\".to_owned() + host | |
2609 | } else { | |
2610 | let first = segments.next().ok_or(())?; | |
2611 | ||
2612 | match first.len() { | |
2613 | 2 => { | |
2614 | if !first.starts_with(parser::ascii_alpha) || first.as_bytes()[1] != b':' { | |
e74abb32 | 2615 | return Err(()); |
abe05a73 XL |
2616 | } |
2617 | ||
2618 | first.to_owned() | |
e74abb32 | 2619 | } |
abe05a73 XL |
2620 | |
2621 | 4 => { | |
2622 | if !first.starts_with(parser::ascii_alpha) { | |
e74abb32 | 2623 | return Err(()); |
abe05a73 XL |
2624 | } |
2625 | let bytes = first.as_bytes(); | |
2626 | if bytes[1] != b'%' || bytes[2] != b'3' || (bytes[3] != b'a' && bytes[3] != b'A') { | |
e74abb32 | 2627 | return Err(()); |
abe05a73 XL |
2628 | } |
2629 | ||
2630 | first[0..1].to_owned() + ":" | |
e74abb32 | 2631 | } |
abe05a73 XL |
2632 | |
2633 | _ => return Err(()), | |
2634 | } | |
2635 | }; | |
2636 | ||
2637 | for segment in segments { | |
2638 | string.push('\\'); | |
2639 | ||
2640 | // Currently non-unicode windows paths cannot be represented | |
2641 | match String::from_utf8(percent_decode(segment.as_bytes()).collect()) { | |
2642 | Ok(s) => string.push_str(&s), | |
2643 | Err(..) => return Err(()), | |
2644 | } | |
2645 | } | |
2646 | let path = PathBuf::from(string); | |
e74abb32 XL |
2647 | debug_assert!( |
2648 | path.is_absolute(), | |
2649 | "to_file_path() failed to produce an absolute Path" | |
2650 | ); | |
abe05a73 XL |
2651 | Ok(path) |
2652 | } | |
2653 | ||
abe05a73 XL |
2654 | /// Implementation detail of `Url::query_pairs_mut`. Typically not used directly. |
2655 | #[derive(Debug)] | |
2656 | pub struct UrlQuery<'a> { | |
8faf50e0 | 2657 | url: Option<&'a mut Url>, |
abe05a73 XL |
2658 | fragment: Option<String>, |
2659 | } | |
2660 | ||
2661 | impl<'a> Drop for UrlQuery<'a> { | |
2662 | fn drop(&mut self) { | |
8faf50e0 XL |
2663 | if let Some(url) = self.url.take() { |
2664 | url.restore_already_parsed_fragment(self.fragment.take()) | |
2665 | } | |
abe05a73 XL |
2666 | } |
2667 | } |