1#[cfg(not(feature = "std"))]
13use alloc::string::ToString;
14use alloc::string::String;
15use core::fmt;
16
17use azul_css::{impl_result, impl_result_inner, AzString};
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
21#[repr(C)]
22pub struct Url {
23 pub href: AzString,
25 pub scheme: AzString,
27 pub host: AzString,
29 pub port: u16,
31 pub path: AzString,
33 pub query: AzString,
35 pub fragment: AzString,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41#[repr(C)]
42pub struct UrlParseError {
43 pub message: AzString,
45}
46
47impl fmt::Display for UrlParseError {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(f, "{}", self.message.as_str())
50 }
51}
52
53#[cfg(feature = "std")]
54impl std::error::Error for UrlParseError {}
55
56impl_result!(
58 Url,
59 UrlParseError,
60 ResultUrlUrlParseError,
61 copy = false,
62 [Debug, Clone, PartialEq, Eq]
63);
64
65impl Url {
66 #[cfg(feature = "url")]
68 pub fn parse(s: &str) -> Result<Self, UrlParseError> {
69 use ::url::Url as UrlParser;
70
71 let parsed = UrlParser::parse(s).map_err(|e| UrlParseError {
72 message: AzString::from(e.to_string()),
73 })?;
74
75 Ok(Self {
76 href: AzString::from(parsed.as_str().to_string()),
77 scheme: AzString::from(parsed.scheme().to_string()),
78 host: AzString::from(parsed.host_str().unwrap_or("").to_string()),
79 port: parsed.port().unwrap_or(0),
80 path: AzString::from(parsed.path().to_string()),
81 query: AzString::from(parsed.query().unwrap_or("").to_string()),
82 fragment: AzString::from(parsed.fragment().unwrap_or("").to_string()),
83 })
84 }
85
86 #[must_use] pub fn from_parts(scheme: &str, host: &str, port: u16, path: &str) -> Self {
88 let port_str = if port == 0
89 || (scheme == "http" && port == 80)
90 || (scheme == "https" && port == 443)
91 {
92 String::new()
93 } else {
94 alloc::format!(":{port}")
95 };
96
97 let href = alloc::format!("{scheme}://{host}{port_str}{path}");
98
99 Self {
100 href: AzString::from(href),
101 scheme: AzString::from(scheme.to_string()),
102 host: AzString::from(host.to_string()),
103 port,
104 path: AzString::from(path.to_string()),
105 query: AzString::from(String::new()),
106 fragment: AzString::from(String::new()),
107 }
108 }
109
110 #[must_use] pub fn as_str(&self) -> &str {
112 self.href.as_str()
113 }
114
115 #[must_use] pub fn is_https(&self) -> bool {
117 self.scheme.as_str() == "https"
118 }
119
120 #[must_use] pub fn is_http(&self) -> bool {
122 self.scheme.as_str() == "http"
123 }
124
125 #[must_use] pub fn effective_port(&self) -> u16 {
127 if self.port != 0 {
128 self.port
129 } else if self.is_https() {
130 443
131 } else if self.is_http() {
132 80
133 } else {
134 0
135 }
136 }
137
138 #[cfg(feature = "url")]
140 pub fn join(&self, path: &str) -> Result<Self, UrlParseError> {
141 use ::url::Url as UrlParser;
142
143 let base = UrlParser::parse(self.href.as_str()).map_err(|e| UrlParseError {
144 message: AzString::from(e.to_string()),
145 })?;
146
147 let joined = base.join(path).map_err(|e| UrlParseError {
148 message: AzString::from(e.to_string()),
149 })?;
150
151 Self::parse(joined.as_str())
152 }
153
154 #[cfg(not(feature = "url"))]
156 pub const fn parse(_s: &str) -> Result<Self, UrlParseError> {
160 Err(UrlParseError {
161 message: AzString::from_const_str("url feature not enabled"),
162 })
163 }
164
165 #[cfg(not(feature = "url"))]
167 pub const fn join(&self, _path: &str) -> Result<Self, UrlParseError> {
171 Err(UrlParseError {
172 message: AzString::from_const_str("url feature not enabled"),
173 })
174 }
175}
176
177impl fmt::Display for Url {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 write!(f, "{}", self.href.as_str())
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 #[cfg(feature = "url")]
189 fn test_url_parse() {
190 let url = Url::parse("https://example.com:8080/path?query=1#frag").unwrap();
191 assert_eq!(url.scheme.as_str(), "https");
192 assert_eq!(url.host.as_str(), "example.com");
193 assert_eq!(url.port, 8080);
194 assert_eq!(url.path.as_str(), "/path");
195 assert_eq!(url.query.as_str(), "query=1");
196 assert_eq!(url.fragment.as_str(), "frag");
197 }
198
199 #[test]
200 fn test_url_from_parts() {
201 let url = Url::from_parts("https", "example.com", 443, "/api");
202 assert!(url.is_https());
203 assert_eq!(url.effective_port(), 443);
204 }
205}
206
207#[cfg(test)]
208mod autotest_generated {
209 use alloc::format;
210
211 use super::*;
212
213 fn assert_url_invariants(u: &Url) {
218 assert_eq!(u.as_str(), u.href.as_str());
220 assert_eq!(format!("{u}"), u.href.as_str());
221
222 assert_eq!(u.is_https(), u.scheme.as_str() == "https");
224 assert_eq!(u.is_http(), u.scheme.as_str() == "http");
225 assert!(!(u.is_http() && u.is_https()));
226
227 let expected_port = if u.port != 0 {
229 u.port
230 } else if u.is_https() {
231 443
232 } else if u.is_http() {
233 80
234 } else {
235 0
236 };
237 assert_eq!(u.effective_port(), expected_port);
238
239 assert_eq!(u.clone(), *u);
241 }
242
243 #[test]
248 fn default_url_is_inert_and_does_not_panic() {
249 let u = Url::default();
250 assert_eq!(u.as_str(), "");
251 assert_eq!(u.href.as_str(), "");
252 assert_eq!(u.scheme.as_str(), "");
253 assert_eq!(u.host.as_str(), "");
254 assert_eq!(u.port, 0);
255 assert!(!u.is_https());
256 assert!(!u.is_http());
257 assert_eq!(u.effective_port(), 0);
259 assert_eq!(format!("{u}"), "");
260 assert_url_invariants(&u);
261 }
262
263 #[test]
264 fn effective_port_covers_every_scheme_port_combination() {
265 assert_eq!(
267 Url::from_parts("https", "h", 80, "/").effective_port(),
268 80,
269 "explicit port must win over the https default"
270 );
271 assert_eq!(Url::from_parts("http", "h", 443, "/").effective_port(), 443);
272 assert_eq!(
273 Url::from_parts("http", "h", u16::MAX, "/").effective_port(),
274 u16::MAX
275 );
276 assert_eq!(Url::from_parts("https", "h", 1, "/").effective_port(), 1);
277
278 assert_eq!(Url::from_parts("https", "h", 0, "/").effective_port(), 443);
280 assert_eq!(Url::from_parts("http", "h", 0, "/").effective_port(), 80);
281 assert_eq!(Url::from_parts("ftp", "h", 0, "/").effective_port(), 0);
282 assert_eq!(Url::from_parts("", "", 0, "").effective_port(), 0);
283 }
284
285 #[test]
286 fn predicates_are_case_sensitive_and_reject_near_misses() {
287 for scheme in [
289 "HTTPS", "Https", "httpss", "https ", " https", "http\0", "ws", "httpx", "",
290 ] {
291 let u = Url::from_parts(scheme, "example.com", 0, "/");
292 assert!(
293 !u.is_https(),
294 "scheme {scheme:?} must not be treated as https"
295 );
296 assert_url_invariants(&u);
297 }
298 for scheme in ["HTTP", "Http", "httpx", "https", "htt", ""] {
299 let u = Url::from_parts(scheme, "example.com", 0, "/");
300 assert!(
301 !u.is_http(),
302 "scheme {scheme:?} must not be treated as http"
303 );
304 }
305 assert!(Url::from_parts("https", "h", 0, "/").is_https());
306 assert!(Url::from_parts("http", "h", 0, "/").is_http());
307 }
308
309 #[test]
314 fn from_parts_omits_default_and_sentinel_ports_only() {
315 assert_eq!(
317 Url::from_parts("https", "example.com", 0, "/a").as_str(),
318 "https://example.com/a"
319 );
320 assert_eq!(
322 Url::from_parts("http", "example.com", 80, "/").as_str(),
323 "http://example.com/"
324 );
325 assert_eq!(
326 Url::from_parts("https", "example.com", 443, "/").as_str(),
327 "https://example.com/"
328 );
329 assert_eq!(
331 Url::from_parts("https", "example.com", 80, "/").as_str(),
332 "https://example.com:80/"
333 );
334 assert_eq!(
335 Url::from_parts("http", "example.com", 443, "/").as_str(),
336 "http://example.com:443/"
337 );
338 assert_eq!(
340 Url::from_parts("http", "example.com", u16::MAX, "/").as_str(),
341 "http://example.com:65535/"
342 );
343 assert_eq!(
344 Url::from_parts("ftp", "example.com", 443, "/").as_str(),
345 "ftp://example.com:443/"
346 );
347 }
348
349 #[test]
350 fn from_parts_keeps_the_port_field_even_when_elided_from_href() {
351 let u = Url::from_parts("https", "example.com", 443, "/api");
354 assert_eq!(u.port, 443);
355 assert!(!u.as_str().contains(":443"));
356 assert_eq!(u.effective_port(), 443);
357 assert_url_invariants(&u);
358 }
359
360 #[test]
361 fn from_parts_fields_mirror_the_arguments_verbatim() {
362 let u = Url::from_parts("https", "example.com", 8080, "/a/b");
363 assert_eq!(u.scheme.as_str(), "https");
364 assert_eq!(u.host.as_str(), "example.com");
365 assert_eq!(u.port, 8080);
366 assert_eq!(u.path.as_str(), "/a/b");
367 assert_eq!(u.query.as_str(), "");
369 assert_eq!(u.fragment.as_str(), "");
370 assert_eq!(u.as_str(), "https://example.com:8080/a/b");
371 assert_url_invariants(&u);
372 }
373
374 #[test]
375 fn from_parts_does_not_panic_on_extreme_or_empty_arguments() {
376 let u = Url::from_parts("", "", 0, "");
378 assert_eq!(u.as_str(), "://");
379 assert_url_invariants(&u);
380
381 for (scheme, host, port, path) in [
384 ("://", "://", 1, "://"),
385 ("http", "user:pw@host", 0, "/x"),
386 ("http", "[::1]", 8080, "/x"),
387 ("http", "a b c", 0, "/p a t h"),
388 ("http", "example.com", 0, "no-leading-slash"),
389 ("http", "example.com", 0, "?query#frag"),
390 ("\n\t", "\r", 65535, "\0"),
391 ] {
392 let u = Url::from_parts(scheme, host, port, path);
393 assert_eq!(u.scheme.as_str(), scheme);
394 assert_eq!(u.host.as_str(), host);
395 assert_eq!(u.port, port);
396 assert_eq!(u.path.as_str(), path);
397 assert!(u.as_str().starts_with(scheme));
398 assert_url_invariants(&u);
399 }
400 }
401
402 #[test]
403 fn from_parts_handles_unicode_without_panicking_or_mangling() {
404 let u = Url::from_parts("https", "例え.テスト", 0, "/パス/😀");
405 assert_eq!(u.host.as_str(), "例え.テスト");
406 assert_eq!(u.path.as_str(), "/パス/😀");
407 assert_eq!(u.as_str(), "https://例え.テスト/パス/😀");
409 assert!(u.is_https());
410 assert_url_invariants(&u);
411
412 let u = Url::from_parts("http", "e\u{0301}xample", 1, "/\u{1F600}");
414 assert!(u.as_str().contains('\u{0301}'));
415 assert_url_invariants(&u);
416 }
417
418 #[test]
419 fn from_parts_handles_huge_inputs_without_hanging() {
420 let host = "a".repeat(100_000);
421 let path = "/".to_string() + &"b".repeat(100_000);
422 let u = Url::from_parts("https", &host, 8080, &path);
423 assert_eq!(u.as_str().len(), 5 + 3 + 100_000 + 5 + 100_001);
425 assert_eq!(u.host.as_str().len(), 100_000);
426 assert!(u.is_https());
427 assert_eq!(u.effective_port(), 8080);
428 assert_url_invariants(&u);
429 }
430
431 #[test]
436 fn url_display_never_reinterprets_the_href() {
437 for href in ["", "://", "not a url", "{}{{}", "%s%n", "\u{1F600}"] {
439 let u = Url {
440 href: AzString::from(href),
441 ..Url::default()
442 };
443 assert_eq!(format!("{u}"), href);
444 assert_eq!(u.as_str(), href);
445 }
446 }
447
448 #[test]
449 fn url_parse_error_display_is_verbatim_and_panic_free() {
450 let e = UrlParseError {
452 message: AzString::from(""),
453 };
454 assert_eq!(format!("{e}"), "");
455
456 for msg in [
458 "relative URL without a base",
459 "{}",
460 "{0} {1} {}",
461 "%s %n %p",
462 "\u{1F600} invalid",
463 "e\u{0301}",
464 "\0\t\n",
465 ] {
466 let e = UrlParseError {
467 message: AzString::from(msg),
468 };
469 assert_eq!(format!("{e}"), msg);
470 }
471
472 let big = "x".repeat(100_000);
474 let e = UrlParseError {
475 message: AzString::from(big.as_str()),
476 };
477 assert_eq!(format!("{e}").len(), 100_000);
478
479 let e = UrlParseError {
481 message: AzString::from_const_str("url feature not enabled"),
482 };
483 assert_eq!(format!("{e}"), "url feature not enabled");
484 }
485
486 #[test]
491 fn ffi_result_round_trips_both_variants() {
492 let ok: Result<Url, UrlParseError> = Ok(Url::from_parts("https", "a.b", 0, "/"));
493 let ffi: ResultUrlUrlParseError = ok.clone().into();
494 assert!(ffi.is_ok());
495 assert!(!ffi.is_err());
496 let back: Result<Url, UrlParseError> = ffi.into();
497 assert_eq!(back, ok);
498
499 let err: Result<Url, UrlParseError> = Err(UrlParseError {
500 message: AzString::from("boom"),
501 });
502 let ffi: ResultUrlUrlParseError = err.clone().into();
503 assert!(ffi.is_err());
504 assert!(!ffi.is_ok());
505 assert_eq!(
506 ffi.as_result().unwrap_err().message.as_str(),
507 "boom",
508 "as_result() must borrow the same error payload"
509 );
510 let back: Result<Url, UrlParseError> = ffi.into();
511 assert_eq!(back, err);
512 }
513
514 #[cfg(feature = "std")]
515 #[test]
516 fn eq_implies_equal_hash() {
517 use std::{
518 collections::hash_map::DefaultHasher,
519 hash::{Hash, Hasher},
520 };
521
522 fn hash_of(u: &Url) -> u64 {
523 let mut h = DefaultHasher::new();
524 u.hash(&mut h);
525 h.finish()
526 }
527
528 let a = Url::from_parts("https", "example.com", 8080, "/a");
529 let b = Url::from_parts("https", "example.com", 8080, "/a");
530 assert_eq!(a, b);
531 assert_eq!(hash_of(&a), hash_of(&b));
532
533 let c = Url::from_parts("https", "example.com", 443, "/a");
535 let d = Url::from_parts("https", "example.com", 0, "/a");
536 assert_eq!(c.as_str(), d.as_str(), "hrefs are identical…");
537 assert_ne!(c, d, "…but the port field still distinguishes them");
538 }
539
540 #[cfg(feature = "url")]
550 fn assert_no_panic_and_idempotent(s: &str) {
551 match Url::parse(s) {
552 Ok(u) => {
553 assert_url_invariants(&u);
554 let again = Url::parse(u.as_str())
555 .expect("a serialized URL must always re-parse (idempotent normalization)");
556 assert_eq!(again, u, "parse(serialize(x)) must equal x for input {s:?}");
557 assert_eq!(again.as_str(), u.as_str());
558 }
559 Err(e) => {
560 assert!(
562 !e.message.as_str().is_empty(),
563 "error for {s:?} must have a message"
564 );
565 assert_eq!(format!("{e}"), e.message.as_str());
566 }
567 }
568 }
569
570 #[cfg(feature = "url")]
571 #[test]
572 fn parse_valid_minimal_positive_control() {
573 let u = Url::parse("http://example.com").expect("minimal absolute URL must parse");
574 assert_eq!(u.scheme.as_str(), "http");
575 assert_eq!(u.host.as_str(), "example.com");
576 assert_eq!(u.port, 0);
578 assert_eq!(u.effective_port(), 80);
579 assert!(u.is_http());
580 assert!(!u.is_https());
581 assert_eq!(u.path.as_str(), "/");
583 assert_eq!(u.query.as_str(), "");
584 assert_eq!(u.fragment.as_str(), "");
585 assert_url_invariants(&u);
586 }
587
588 #[cfg(feature = "url")]
589 #[test]
590 fn parse_rejects_empty_and_whitespace_only_input() {
591 for s in ["", " ", " ", "\t", "\n", "\t\n", "\r\n \t "] {
592 let r = Url::parse(s);
593 assert!(r.is_err(), "{s:?} must not parse as an absolute URL");
594 let e = r.unwrap_err();
595 assert!(!e.message.as_str().is_empty());
596 }
597 }
598
599 #[cfg(feature = "url")]
600 #[test]
601 fn parse_rejects_garbage_without_panicking() {
602 for s in [
603 "not a url",
604 "///",
605 "::::",
606 "http://",
607 "\u{0}\u{1}\u{2}",
608 "\u{FFFD}\u{FFFD}",
609 "?query-only",
610 "#fragment-only",
611 "/absolute/path/only",
612 "../relative",
613 ] {
614 let r = Url::parse(s);
615 if let Ok(ref u) = r {
616 assert_url_invariants(u);
618 } else {
619 assert!(!r.unwrap_err().message.as_str().is_empty());
620 }
621 }
622 assert!(Url::parse("not a url").is_err());
624 assert!(Url::parse("/absolute/path/only").is_err());
625 assert!(Url::parse("http://").is_err(), "empty host must be an error");
626 }
627
628 #[cfg(feature = "url")]
629 #[test]
630 fn parse_rejects_out_of_range_and_non_numeric_ports() {
631 assert!(Url::parse("http://example.com:65536/").is_err());
633 assert!(Url::parse("http://example.com:99999/").is_err());
634 assert!(Url::parse("http://example.com:4294967296/").is_err());
635 assert!(Url::parse("http://example.com:18446744073709551616/").is_err());
636 assert!(Url::parse("http://example.com:-1/").is_err());
637 assert!(Url::parse("http://example.com:NaN/").is_err());
638 assert!(Url::parse("http://example.com:inf/").is_err());
639 assert!(Url::parse("http://example.com:+80/").is_err());
640 assert!(Url::parse(&format!("http://example.com:{}/", "9".repeat(10_000))).is_err());
641
642 let u = Url::parse("http://example.com:65535/").expect("65535 is a valid port");
644 assert_eq!(u.port, u16::MAX);
645 assert_eq!(u.effective_port(), u16::MAX);
646 assert_url_invariants(&u);
647 }
648
649 #[cfg(feature = "url")]
650 #[test]
651 fn parse_port_zero_collides_with_the_no_port_sentinel() {
652 if let Ok(u) = Url::parse("http://example.com:0/") {
656 assert_eq!(u.port, 0);
657 assert_eq!(
658 u.effective_port(),
659 80,
660 "explicit :0 is swallowed by the 0 sentinel"
661 );
662 assert_url_invariants(&u);
663 }
664 }
665
666 #[cfg(feature = "url")]
667 #[test]
668 fn parse_default_ports_are_reported_via_effective_port() {
669 let u = Url::parse("https://example.com:443/").unwrap();
672 assert!(u.port == 0 || u.port == 443);
673 assert_eq!(u.effective_port(), 443);
674 assert_url_invariants(&u);
675
676 let u = Url::parse("http://example.com:80/").unwrap();
677 assert!(u.port == 0 || u.port == 80);
678 assert_eq!(u.effective_port(), 80);
679 assert_url_invariants(&u);
680
681 let u = Url::parse("ftp://example.com/").unwrap();
683 assert_eq!(u.effective_port(), 0);
684 assert_url_invariants(&u);
685 }
686
687 #[cfg(feature = "url")]
688 #[test]
689 fn parse_is_idempotent_across_hostile_inputs() {
690 for s in [
691 "http://example.com/0",
693 "http://example.com/-0",
694 "http://example.com/9223372036854775807",
695 "http://example.com/-9223372036854775808",
696 "http://example.com/18446744073709551615",
697 "http://example.com/?n=NaN&i=inf&e=1e400&t=1e-400",
698 "http://example.com/#-0.0",
699 " https://example.com/ ",
701 "\thttps://example.com/\n",
702 "https://example.com/valid;garbage",
703 "https://example.com/a?b=c;d#e;f",
704 "https://user:pw@example.com:8080/p?q#f",
706 "https://example.com/%2e%2e/%2E%2E/",
707 "https://example.com/a//b///c",
708 "https://example.com/?",
709 "https://example.com/#",
710 "https://example.com/?#",
711 "http://[::1]:8080/",
712 "http://[2001:db8::1]/",
713 "http://127.0.0.1:8080/",
714 "file:///etc/passwd",
715 "data:text/plain,hello",
716 "mailto:user@example.com",
717 "urn:isbn:0451450523",
718 "blob:https://example.com/uuid",
719 "https://example.com/%",
721 "https://example.com/%zz",
722 "https://example.com/%00",
723 "https://example.com/%%%%",
724 "https://example.com/\u{1F600}",
726 "https://example.com/e\u{0301}\u{0301}\u{0301}",
727 "https://example.com/?q=\u{4F8B}\u{3048}",
728 "https://example.com/#\u{200B}\u{FEFF}",
729 "https://\u{4F8B}\u{3048}.\u{30C6}\u{30B9}\u{30C8}/",
730 ] {
731 assert_no_panic_and_idempotent(s);
732 }
733 }
734
735 #[cfg(feature = "url")]
736 #[test]
737 fn parse_normalizes_leading_and_trailing_whitespace_deterministically() {
738 match Url::parse(" https://example.com/ ") {
741 Ok(u) => {
742 assert_eq!(u.host.as_str(), "example.com");
743 assert_eq!(u.as_str(), "https://example.com/");
744 assert!(!u.as_str().contains(' '));
745 assert_url_invariants(&u);
746 }
747 Err(e) => assert!(!e.message.as_str().is_empty()),
748 }
749 }
750
751 #[cfg(feature = "url")]
752 #[test]
753 fn parse_unicode_host_is_idna_encoded_to_ascii() {
754 let u = Url::parse("https://\u{4F8B}\u{3048}.\u{30C6}\u{30B9}\u{30C8}/")
755 .expect("an IDN host must parse");
756 assert!(
757 u.host.as_str().is_ascii(),
758 "host must be punycode/ASCII after IDNA, got {:?}",
759 u.host.as_str()
760 );
761 assert!(!u.host.as_str().is_empty());
762 assert!(u.as_str().is_ascii(), "serialized href must be ASCII");
763 assert_url_invariants(&u);
764 }
765
766 #[cfg(feature = "url")]
767 #[test]
768 fn parse_unicode_path_is_percent_encoded() {
769 let u = Url::parse("https://example.com/\u{1F600}").expect("emoji path must parse");
770 assert!(
771 u.path.as_str().is_ascii(),
772 "path must be percent-encoded, got {:?}",
773 u.path.as_str()
774 );
775 assert!(u.as_str().is_ascii());
776 assert_url_invariants(&u);
777 }
778
779 #[cfg(feature = "url")]
780 #[test]
781 fn parse_survives_extremely_long_input() {
782 let long = format!("http://example.com/{}", "a".repeat(1_000_000));
784 let u = Url::parse(&long).expect("a long-but-valid URL must parse");
785 assert_eq!(u.host.as_str(), "example.com");
786 assert_eq!(u.path.as_str().len(), 1 + 1_000_000);
787 assert_url_invariants(&u);
788
789 let long_q = format!("http://example.com/?{}", "k=v&".repeat(250_000));
791 assert_no_panic_and_idempotent(&long_q);
792 let long_host = format!("http://{}/", "h".repeat(100_000));
793 assert_no_panic_and_idempotent(&long_host);
794 }
795
796 #[cfg(feature = "url")]
797 #[test]
798 fn parse_survives_deeply_nested_and_repetitive_input() {
799 let nested = format!("http://example.com/{}", "a/".repeat(10_000));
801 let u = Url::parse(&nested).expect("deeply nested path must parse");
802 assert_eq!(u.path.as_str().matches('/').count(), 10_001);
803 assert_url_invariants(&u);
804
805 let dotdot = format!("http://example.com/{}", "../".repeat(10_000));
807 let u = Url::parse(&dotdot).expect("dot-dot flood must parse");
808 assert_eq!(u.host.as_str(), "example.com", "must not escape the origin");
809 assert!(u.path.as_str().starts_with('/'));
810 assert!(
811 !u.path.as_str().contains(".."),
812 "dot-dot segments must be resolved away, got {:?}",
813 u.path.as_str()
814 );
815 assert_url_invariants(&u);
816
817 let brackets = format!(
819 "http://example.com/{}{}",
820 "(".repeat(10_000),
821 ")".repeat(10_000)
822 );
823 assert_no_panic_and_idempotent(&brackets);
824 }
825
826 #[cfg(feature = "url")]
827 #[test]
828 fn parse_round_trips_a_fully_populated_url() {
829 let src = "https://example.com:8080/path?query=1#frag";
830 let u = Url::parse(src).unwrap();
831 assert_eq!(u.as_str(), src);
833 let u2 = Url::parse(u.as_str()).unwrap();
835 assert_eq!(u2, u);
836 assert_eq!(format!("{u2}"), format!("{u}"));
837 assert_url_invariants(&u);
838 }
839
840 #[cfg(feature = "url")]
841 #[test]
842 fn from_parts_output_reparses_into_the_same_url() {
843 let built = Url::from_parts("https", "example.com", 8080, "/a/b");
845 let parsed = Url::parse(built.as_str()).expect("from_parts output must be parseable");
846 assert_eq!(parsed, built, "from_parts must be a faithful serializer");
847
848 let built = Url::from_parts("https", "example.com", 443, "/a");
852 let parsed = Url::parse(built.as_str()).unwrap();
853 assert_eq!(parsed.as_str(), built.as_str());
854 assert_eq!(built.port, 443);
855 assert_eq!(parsed.port, 0);
856 assert_ne!(parsed, built, "port-field asymmetry across the round trip");
857 assert_eq!(parsed.effective_port(), built.effective_port());
858 assert_eq!(parsed.effective_port(), 443);
859 }
860
861 #[cfg(feature = "url")]
866 #[test]
867 fn join_valid_minimal_positive_control() {
868 let base = Url::parse("https://example.com/a/b").unwrap();
869 let j = base.join("c").expect("relative join must work");
870 assert_eq!(j.as_str(), "https://example.com/a/c");
871 assert_eq!(j.host.as_str(), "example.com");
872 assert!(j.is_https());
873 assert_eq!(j.effective_port(), 443);
874 assert_url_invariants(&j);
875
876 assert_eq!(
878 base.join("/root").unwrap().as_str(),
879 "https://example.com/root"
880 );
881 assert_eq!(
882 base.join("http://other.com/x").unwrap().host.as_str(),
883 "other.com"
884 );
885 assert_eq!(base.join("#f").unwrap().fragment.as_str(), "f");
886 assert_eq!(base.join("?q=1").unwrap().query.as_str(), "q=1");
887 }
888
889 #[cfg(feature = "url")]
890 #[test]
891 fn join_on_an_unparseable_base_errors_instead_of_panicking() {
892 let e = Url::default()
894 .join("/x")
895 .expect_err("joining onto an empty base must fail");
896 assert!(!e.message.as_str().is_empty());
897
898 for bad in [
900 Url::from_parts("", "", 0, ""),
901 Url::from_parts("://", "://", 1, "://"),
902 Url::from_parts("http", "", 0, "/x"),
903 ] {
904 let r = bad.join("/y");
905 if let Ok(ref u) = r {
906 assert_url_invariants(u);
907 } else {
908 assert!(!r.unwrap_err().message.as_str().is_empty());
909 }
910 }
911 }
912
913 #[cfg(feature = "url")]
914 #[test]
915 fn join_never_panics_on_hostile_relative_inputs() {
916 let base = Url::parse("https://example.com/a/b?q=1#f").unwrap();
917 for path in [
918 "",
919 " ",
920 " ",
921 "\t\n",
922 "..",
923 "../..",
924 "/",
925 "//",
926 "///",
927 "//other.com/x",
928 "?",
929 "#",
930 "?#",
931 ":",
932 "::::",
933 "not a path",
934 "valid;garbage",
935 " padded ",
936 "%",
937 "%zz",
938 "%00",
939 "\u{1F600}",
940 "e\u{0301}",
941 "\u{4F8B}\u{3048}",
942 "\u{0}",
943 "0",
944 "-0",
945 "9223372036854775807",
946 "NaN",
947 "inf",
948 "javascript:alert(1)",
949 "data:text/plain,x",
950 "mailto:a@b.c",
951 ] {
952 match base.join(path) {
953 Ok(u) => {
954 assert_url_invariants(&u);
955 let again = Url::parse(u.as_str())
958 .expect("join() output must always re-parse");
959 assert_eq!(again, u, "join({path:?}) is not idempotent");
960 }
961 Err(e) => assert!(
962 !e.message.as_str().is_empty(),
963 "join({path:?}) error needs a message"
964 ),
965 }
966 }
967 }
968
969 #[cfg(feature = "url")]
970 #[test]
971 fn join_cannot_escape_the_origin_with_a_dot_dot_flood() {
972 let base = Url::parse("https://example.com/a/b/c").unwrap();
973 let escape = "../".repeat(10_000);
974 let u = base
975 .join(&escape)
976 .expect("a dot-dot flood must resolve, not fail");
977 assert_eq!(u.host.as_str(), "example.com");
978 assert_eq!(u.scheme.as_str(), "https");
979 assert_eq!(u.path.as_str(), "/", "must clamp at the root");
980 assert!(!u.path.as_str().contains(".."));
981 assert_url_invariants(&u);
982 }
983
984 #[cfg(feature = "url")]
985 #[test]
986 fn join_survives_extremely_long_relative_paths() {
987 let base = Url::parse("https://example.com/").unwrap();
988 let long = "a".repeat(1_000_000);
989 let u = base.join(&long).expect("a long relative path must join");
990 assert_eq!(u.host.as_str(), "example.com");
991 assert_eq!(u.path.as_str().len(), 1 + 1_000_000);
992 assert_url_invariants(&u);
993
994 let nested = "x/".repeat(10_000);
996 let u = base.join(&nested).expect("deep nesting must join");
997 assert_eq!(u.path.as_str().matches('/').count(), 10_001);
998 assert_url_invariants(&u);
999 }
1000
1001 #[cfg(feature = "url")]
1002 #[test]
1003 fn join_result_is_a_fully_populated_url_not_a_partial_one() {
1004 let base = Url::parse("http://example.com:8080/a?old=1#oldfrag").unwrap();
1007 let u = base.join("b?new=2#newfrag").unwrap();
1008 assert_eq!(u.as_str(), "http://example.com:8080/b?new=2#newfrag");
1009 assert_eq!(u.scheme.as_str(), "http");
1010 assert_eq!(u.host.as_str(), "example.com");
1011 assert_eq!(u.port, 8080);
1012 assert_eq!(u.path.as_str(), "/b");
1013 assert_eq!(u.query.as_str(), "new=2");
1014 assert_eq!(u.fragment.as_str(), "newfrag");
1015 assert_eq!(u.effective_port(), 8080);
1016 assert_url_invariants(&u);
1017 }
1018
1019 #[cfg(not(feature = "url"))]
1024 #[test]
1025 fn stub_parse_always_errors_and_never_panics() {
1026 let huge = "a".repeat(1_000_000);
1027 let nested = "(".repeat(10_000);
1028 for s in [
1029 "",
1030 " ",
1031 "\t\n",
1032 "https://example.com/",
1033 "not a url",
1034 "0",
1035 "-0",
1036 "NaN",
1037 "inf",
1038 "9223372036854775807",
1039 "\u{1F600}",
1040 "e\u{0301}",
1041 "\u{0}",
1042 huge.as_str(),
1043 nested.as_str(),
1044 ] {
1045 let e = Url::parse(s).expect_err("the stub must always fail");
1046 assert_eq!(e.message.as_str(), "url feature not enabled");
1047 assert_eq!(format!("{e}"), "url feature not enabled");
1048 }
1049 }
1050
1051 #[cfg(not(feature = "url"))]
1052 #[test]
1053 fn stub_join_always_errors_for_every_base_and_path() {
1054 let huge = "b".repeat(1_000_000);
1055 let bases = [
1056 Url::default(),
1057 Url::from_parts("https", "example.com", 8080, "/a"),
1058 Url::from_parts("", "", 0, ""),
1059 ];
1060 for base in &bases {
1061 for path in ["", " ", "../..", "\u{1F600}", "x", huge.as_str()] {
1062 let e = base.join(path).expect_err("the stub must always fail");
1063 assert_eq!(e.message.as_str(), "url feature not enabled");
1064 }
1065 assert_url_invariants(base);
1067 }
1068 }
1069
1070 #[cfg(not(feature = "url"))]
1071 #[test]
1072 fn stub_parse_and_join_are_usable_in_const_context() {
1073 const PARSED: Result<Url, UrlParseError> = Url::parse("https://example.com/");
1076 assert!(PARSED.is_err());
1077 }
1078}