1use std::fmt::{self, Display, Formatter};
2
3use crate::errors::NanoGetError;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Url {
8 pub scheme: String,
10 pub host: String,
12 pub port: u16,
14 pub path: String,
16 pub query: Option<String>,
18 explicit_port: bool,
19}
20
21impl Url {
22 pub fn parse(input: &str) -> Result<Self, NanoGetError> {
26 let trimmed = input.trim();
27 if trimmed.is_empty() {
28 return Err(NanoGetError::invalid_url("URL cannot be empty"));
29 }
30
31 let (scheme, remainder) = match trimmed.find("://") {
32 Some(index) => {
33 let scheme = &trimmed[..index];
34 validate_scheme(scheme)?;
35 (scheme.to_ascii_lowercase(), &trimmed[index + 3..])
36 }
37 None => ("http".to_string(), trimmed),
38 };
39
40 let without_fragment = strip_fragment(remainder);
41 let (authority, target) = split_authority_and_target(without_fragment)?;
42 let (host, port, explicit_port) = parse_authority(&scheme, authority)?;
43 let (path, query) = parse_target(target)?;
44
45 Ok(Self {
46 scheme,
47 host,
48 port,
49 path,
50 query,
51 explicit_port,
52 })
53 }
54
55 pub fn resolve(&self, location: &str) -> Result<Self, NanoGetError> {
64 let trimmed = strip_fragment(location.trim());
65 if trimmed.is_empty() {
66 return Err(NanoGetError::invalid_url(
67 "redirect location cannot be empty",
68 ));
69 }
70
71 if let Some(scheme) = uri_scheme_prefix(trimmed) {
72 if matches!(scheme.to_ascii_lowercase().as_str(), "http" | "https") {
73 return Self::parse(trimmed);
74 }
75 return Err(NanoGetError::UnsupportedScheme(scheme.to_ascii_lowercase()));
76 }
77
78 if trimmed.starts_with("//") {
79 return Self::parse(&format!("{}:{}", self.scheme, trimmed));
80 }
81
82 if let Some(query) = trimmed.strip_prefix('?') {
83 validate_target_component(query, "query")?;
84 return Ok(Self {
85 scheme: self.scheme.clone(),
86 host: self.host.clone(),
87 port: self.port,
88 path: self.path.clone(),
89 query: Some(query.to_string()),
90 explicit_port: self.explicit_port,
91 });
92 }
93
94 let (path, query) = if trimmed.starts_with('/') {
95 let (path, query) = parse_target(trimmed)?;
96 (normalize_path(&path), query)
97 } else {
98 let (relative_path, query) = split_path_and_query(trimmed);
99 validate_target_component(relative_path, "path")?;
100 if let Some(query) = query {
101 validate_target_component(query, "query")?;
102 }
103 let combined = format!("{}{}", base_directory(&self.path), relative_path);
104 (
105 normalize_path(&combined),
106 query.map(|value| value.to_string()),
107 )
108 };
109
110 Ok(Self {
111 scheme: self.scheme.clone(),
112 host: self.host.clone(),
113 port: self.port,
114 path,
115 query,
116 explicit_port: self.explicit_port,
117 })
118 }
119
120 pub fn origin_form(&self) -> String {
122 match &self.query {
123 Some(query) => format!("{}?{}", self.path, query),
124 None => self.path.clone(),
125 }
126 }
127
128 pub fn absolute_form(&self) -> String {
130 format!(
131 "{}://{}{}",
132 self.scheme,
133 self.host_header_value(),
134 self.origin_form()
135 )
136 }
137
138 pub fn authority_form(&self) -> String {
140 self.connect_host_with_port()
141 }
142
143 pub fn host_header_value(&self) -> String {
145 let host = format_host_for_authority(&self.host);
146 if self.explicit_port || !self.is_default_port() {
147 format!("{host}:{}", self.port)
148 } else {
149 host
150 }
151 }
152
153 pub fn connect_host_with_port(&self) -> String {
155 format!("{}:{}", format_host_for_authority(&self.host), self.port)
156 }
157
158 pub fn full_url(&self) -> String {
160 self.absolute_form()
161 }
162
163 pub fn is_https(&self) -> bool {
165 self.scheme == "https"
166 }
167
168 pub fn is_http(&self) -> bool {
170 self.scheme == "http"
171 }
172
173 pub fn is_default_port(&self) -> bool {
175 default_port_for_scheme(&self.scheme) == Some(self.port)
176 }
177
178 pub fn same_authority(&self, other: &Self) -> bool {
180 self.scheme == other.scheme && self.host == other.host && self.port == other.port
181 }
182
183 pub fn cache_key(&self) -> String {
185 self.full_url()
186 }
187}
188
189impl Display for Url {
190 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
191 write!(f, "{}", self.full_url())
192 }
193}
194
195pub trait ToUrl {
197 fn to_url(&self) -> Result<Url, NanoGetError>;
199}
200
201impl ToUrl for String {
202 fn to_url(&self) -> Result<Url, NanoGetError> {
203 Url::parse(self)
204 }
205}
206
207impl ToUrl for &str {
208 fn to_url(&self) -> Result<Url, NanoGetError> {
209 Url::parse(self)
210 }
211}
212
213impl ToUrl for &String {
214 fn to_url(&self) -> Result<Url, NanoGetError> {
215 Url::parse(self)
216 }
217}
218
219impl ToUrl for Url {
220 fn to_url(&self) -> Result<Url, NanoGetError> {
221 Ok(self.clone())
222 }
223}
224
225impl ToUrl for &Url {
226 fn to_url(&self) -> Result<Url, NanoGetError> {
227 Ok((*self).clone())
228 }
229}
230
231fn validate_scheme(scheme: &str) -> Result<(), NanoGetError> {
232 match scheme.to_ascii_lowercase().as_str() {
233 "http" | "https" => Ok(()),
234 other => Err(NanoGetError::UnsupportedScheme(other.to_string())),
235 }
236}
237
238fn strip_fragment(input: &str) -> &str {
239 input.split('#').next().unwrap_or(input)
240}
241
242fn split_authority_and_target(input: &str) -> Result<(&str, &str), NanoGetError> {
243 if input.is_empty() {
244 return Err(NanoGetError::invalid_url("missing host"));
245 }
246
247 match input.find(['/', '?']) {
248 Some(index) => Ok((&input[..index], &input[index..])),
249 None => Ok((input, "")),
250 }
251}
252
253fn parse_authority(scheme: &str, authority: &str) -> Result<(String, u16, bool), NanoGetError> {
254 if authority.is_empty() {
255 return Err(NanoGetError::invalid_url("missing host"));
256 }
257 if authority.contains('@') {
258 return Err(NanoGetError::invalid_url(
259 "user info in URLs is not supported",
260 ));
261 }
262
263 let default_port = default_port_for_scheme(scheme)
264 .ok_or_else(|| NanoGetError::UnsupportedScheme(scheme.to_string()))?;
265
266 if authority.starts_with('[') {
267 let closing = authority
268 .find(']')
269 .ok_or_else(|| NanoGetError::invalid_url("unterminated IPv6 host"))?;
270 let host = authority[1..closing].to_ascii_lowercase();
271 validate_ipv6_literal(&host)?;
272 let remainder = &authority[closing + 1..];
273 if remainder.is_empty() {
274 return Ok((host, default_port, false));
275 }
276 if let Some(port) = remainder.strip_prefix(':') {
277 return Ok((host, parse_port(port)?, true));
278 }
279 return Err(NanoGetError::invalid_url("invalid IPv6 authority"));
280 }
281
282 let (host, port, explicit_port) = match authority.rsplit_once(':') {
283 Some((host, port)) if !host.contains(':') => {
284 (host.to_ascii_lowercase(), parse_port(port)?, true)
285 }
286 Some(_) => {
287 return Err(NanoGetError::invalid_url(
288 "IPv6 hosts must use bracket notation",
289 ));
290 }
291 None => (authority.to_ascii_lowercase(), default_port, false),
292 };
293
294 validate_reg_name_host(&host)?;
295
296 Ok((host, port, explicit_port))
297}
298
299fn parse_port(input: &str) -> Result<u16, NanoGetError> {
300 input
301 .parse::<u16>()
302 .map_err(|_| NanoGetError::invalid_url(format!("invalid port: {input}")))
303}
304
305fn parse_target(target: &str) -> Result<(String, Option<String>), NanoGetError> {
306 if target.is_empty() {
307 return Ok(("/".to_string(), None));
308 }
309
310 if let Some(query) = target.strip_prefix('?') {
311 validate_target_component(query, "query")?;
312 return Ok(("/".to_string(), Some(query.to_string())));
313 }
314
315 if !target.starts_with('/') {
316 return Err(NanoGetError::invalid_url("path must start with `/`"));
317 }
318
319 let (path, query) = split_path_and_query(target);
320 validate_target_component(path, "path")?;
321 if let Some(query) = query {
322 validate_target_component(query, "query")?;
323 }
324 Ok((path.to_string(), query.map(|value| value.to_string())))
325}
326
327fn split_path_and_query(input: &str) -> (&str, Option<&str>) {
328 match input.find('?') {
329 Some(index) => (&input[..index], Some(&input[index + 1..])),
330 None => (input, None),
331 }
332}
333
334fn default_port_for_scheme(scheme: &str) -> Option<u16> {
335 match scheme {
336 "http" => Some(80),
337 "https" => Some(443),
338 _ => None,
339 }
340}
341
342fn uri_scheme_prefix(value: &str) -> Option<&str> {
343 let first = value.as_bytes().first().copied()?;
344 if !first.is_ascii_alphabetic() {
345 return None;
346 }
347
348 for (index, byte) in value.bytes().enumerate().skip(1) {
349 if byte == b':' {
350 return Some(&value[..index]);
351 }
352 if matches!(byte, b'/' | b'?' | b'#') {
353 return None;
354 }
355 if !byte.is_ascii_alphanumeric() && !matches!(byte, b'+' | b'-' | b'.') {
356 return None;
357 }
358 }
359 None
360}
361
362fn format_host_for_authority(host: &str) -> String {
363 if host.contains(':') {
364 format!("[{host}]")
365 } else {
366 host.to_string()
367 }
368}
369
370fn base_directory(path: &str) -> String {
371 if path.ends_with('/') {
372 return path.to_string();
373 }
374
375 match path.rfind('/') {
376 Some(index) if index > 0 => path[..index + 1].to_string(),
377 Some(_) => "/".to_string(),
378 None => "/".to_string(),
379 }
380}
381
382fn validate_reg_name_host(host: &str) -> Result<(), NanoGetError> {
383 if host.is_empty() {
384 return Err(NanoGetError::invalid_url("missing host"));
385 }
386
387 if !host.is_ascii() {
388 return Err(NanoGetError::invalid_url(
389 "host must be ASCII (use punycode for international domains)",
390 ));
391 }
392
393 if host.chars().any(|ch| {
394 ch.is_ascii_control()
395 || ch.is_ascii_whitespace()
396 || matches!(ch, '/' | '\\' | '?' | '#' | '[' | ']')
397 }) {
398 return Err(NanoGetError::invalid_url("invalid host"));
399 }
400
401 Ok(())
402}
403
404fn validate_ipv6_literal(host: &str) -> Result<(), NanoGetError> {
405 if host.is_empty() {
406 return Err(NanoGetError::invalid_url("unterminated IPv6 host"));
407 }
408
409 if host.parse::<std::net::Ipv6Addr>().is_ok() {
410 return Ok(());
411 }
412
413 Err(NanoGetError::invalid_url("invalid IPv6 authority"))
414}
415
416fn validate_target_component(value: &str, component: &str) -> Result<(), NanoGetError> {
417 if !value.is_ascii() {
418 return Err(NanoGetError::invalid_url(format!(
419 "invalid {component}: contains non-ASCII characters"
420 )));
421 }
422
423 if value
424 .chars()
425 .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace())
426 {
427 return Err(NanoGetError::invalid_url(format!(
428 "invalid {component}: contains control characters or whitespace"
429 )));
430 }
431 Ok(())
432}
433
434fn normalize_path(path: &str) -> String {
435 let mut input = path.to_string();
436 let mut output = String::new();
437
438 while !input.is_empty() {
439 if input.starts_with("../") {
440 input.drain(..3);
441 continue;
442 }
443 if input.starts_with("./") {
444 input.drain(..2);
445 continue;
446 }
447 if input.starts_with("/./") {
448 input.replace_range(..3, "/");
449 continue;
450 }
451 if input == "/." {
452 input.replace_range(..2, "/");
453 continue;
454 }
455 if input.starts_with("/../") {
456 input.replace_range(..4, "/");
457 pop_last_path_segment(&mut output);
458 continue;
459 }
460 if input == "/.." {
461 input.replace_range(..3, "/");
462 pop_last_path_segment(&mut output);
463 continue;
464 }
465 if input == "." || input == ".." {
466 input.clear();
467 continue;
468 }
469
470 let segment_end = if let Some(stripped) = input.strip_prefix('/') {
471 match stripped.find('/') {
472 Some(index) => index + 1,
473 None => input.len(),
474 }
475 } else {
476 input.find('/').unwrap_or(input.len())
477 };
478 output.push_str(&input[..segment_end]);
479 input.drain(..segment_end);
480 }
481
482 if output.is_empty() && path.starts_with('/') {
483 "/".to_string()
484 } else {
485 output
486 }
487}
488
489fn pop_last_path_segment(path: &mut String) {
490 if path.is_empty() {
491 return;
492 }
493
494 let candidate = path.strip_suffix('/').unwrap_or(path.as_str());
495 if let Some(index) = candidate.rfind('/') {
496 path.truncate(index);
497 } else {
498 path.clear();
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use std::panic::{self, AssertUnwindSafe};
505
506 use super::{
507 default_port_for_scheme, normalize_path, parse_target, split_authority_and_target, ToUrl,
508 Url,
509 };
510 use crate::errors::NanoGetError;
511
512 #[test]
513 fn parses_default_http_url() {
514 let url = Url::parse("example.com/a/b?c=1").unwrap();
515 assert_eq!(url.scheme, "http");
516 assert_eq!(url.host, "example.com");
517 assert_eq!(url.port, 80);
518 assert_eq!(url.path, "/a/b");
519 assert_eq!(url.query.as_deref(), Some("c=1"));
520 assert_eq!(url.origin_form(), "/a/b?c=1");
521 }
522
523 #[test]
524 fn parses_https_url_with_explicit_port() {
525 let url = Url::parse("https://example.com:8443/path").unwrap();
526 assert_eq!(url.scheme, "https");
527 assert_eq!(url.port, 8443);
528 assert_eq!(url.host_header_value(), "example.com:8443");
529 assert_eq!(url.connect_host_with_port(), "example.com:8443");
530 }
531
532 #[test]
533 fn parses_bracketed_ipv6_hosts() {
534 let url = Url::parse("http://[::1]:8080/").unwrap();
535 assert_eq!(url.host, "::1");
536 assert_eq!(url.connect_host_with_port(), "[::1]:8080");
537 assert_eq!(url.host_header_value(), "[::1]:8080");
538 }
539
540 #[test]
541 fn strips_fragments() {
542 let url = Url::parse("http://example.com/path?a=1#fragment").unwrap();
543 assert_eq!(url.origin_form(), "/path?a=1");
544 assert_eq!(url.full_url(), "http://example.com/path?a=1");
545 }
546
547 #[test]
548 fn parse_preserves_user_path_structure() {
549 let url = Url::parse("http://example.com/a//b/./c/../d").unwrap();
550 assert_eq!(url.path, "/a//b/./c/../d");
551 assert_eq!(url.origin_form(), "/a//b/./c/../d");
552 }
553
554 #[test]
555 fn resolves_relative_redirects() {
556 let base = Url::parse("http://example.com/a/b/index.html?x=1").unwrap();
557 let resolved = base.resolve("../next?y=2").unwrap();
558 assert_eq!(resolved.full_url(), "http://example.com/a/next?y=2");
559 }
560
561 #[test]
562 fn resolves_relative_redirects_with_dot_segments_without_collapsing_double_slashes() {
563 let base = Url::parse("http://example.com/a/b/index.html").unwrap();
564 let resolved = base.resolve("../x//y/./z/..").unwrap();
565 assert_eq!(resolved.full_url(), "http://example.com/a/x//y/");
566 }
567
568 #[test]
569 fn resolves_absolute_path_redirects() {
570 let base = Url::parse("https://example.com/one/two").unwrap();
571 let resolved = base.resolve("/rooted").unwrap();
572 assert_eq!(resolved.full_url(), "https://example.com/rooted");
573 }
574
575 #[test]
576 fn resolves_query_only_redirects() {
577 let base = Url::parse("http://example.com/path?a=1").unwrap();
578 let resolved = base.resolve("?b=2").unwrap();
579 assert_eq!(resolved.full_url(), "http://example.com/path?b=2");
580 }
581
582 #[test]
583 fn resolve_treats_only_scheme_prefixed_locations_as_absolute() {
584 let base = Url::parse("http://example.com/base").unwrap();
585 let resolved = base.resolve("/foo://bar").unwrap();
586 assert_eq!(resolved.full_url(), "http://example.com/foo://bar");
587
588 let error = base.resolve("ftp://example.com").unwrap_err();
589 assert!(matches!(error, NanoGetError::UnsupportedScheme(_)));
590 }
591
592 #[test]
593 fn resolve_treats_scheme_colon_prefix_as_absolute_uri_reference() {
594 let base = Url::parse("http://example.com/base").unwrap();
595 let error = base.resolve("foo:bar").unwrap_err();
596 assert!(matches!(
597 error,
598 NanoGetError::UnsupportedScheme(ref scheme) if scheme == "foo"
599 ));
600
601 let relative = base.resolve("./foo:bar").unwrap();
602 assert_eq!(relative.full_url(), "http://example.com/foo:bar");
603 }
604
605 #[test]
606 fn rejects_unsupported_schemes() {
607 let error = Url::parse("ftp://example.com").unwrap_err();
608 assert!(matches!(error, NanoGetError::UnsupportedScheme(ref value) if value == "ftp"));
609 }
610
611 #[test]
612 fn rejects_unbracketed_ipv6_hosts() {
613 let error = Url::parse("http://::1/path").unwrap_err();
614 assert!(matches!(error, NanoGetError::InvalidUrl(_)));
615 }
616
617 #[test]
618 fn rejects_empty_and_userinfo_urls() {
619 assert!(matches!(Url::parse(""), Err(NanoGetError::InvalidUrl(_))));
620 assert!(matches!(
621 Url::parse("http://user@example.com"),
622 Err(NanoGetError::InvalidUrl(_))
623 ));
624 }
625
626 #[test]
627 fn rejects_invalid_ports_and_ipv6_authorities() {
628 assert!(matches!(
629 Url::parse("http://example.com:abc"),
630 Err(NanoGetError::InvalidUrl(_))
631 ));
632 assert!(matches!(
633 Url::parse("http://[::1]bad"),
634 Err(NanoGetError::InvalidUrl(_))
635 ));
636 assert!(matches!(
637 Url::parse("http://[:::1]/"),
638 Err(NanoGetError::InvalidUrl(_))
639 ));
640 }
641
642 #[test]
643 fn resolves_scheme_relative_redirects() {
644 let base = Url::parse("https://example.com/one").unwrap();
645 let resolved = base.resolve("//cdn.example.com/path").unwrap();
646 assert_eq!(resolved.full_url(), "https://cdn.example.com/path");
647 }
648
649 #[test]
650 fn builds_absolute_and_authority_forms() {
651 let url = Url::parse("http://example.com:8080/path?x=1").unwrap();
652 assert_eq!(url.absolute_form(), "http://example.com:8080/path?x=1");
653 assert_eq!(url.authority_form(), "example.com:8080");
654 }
655
656 #[test]
657 fn detects_matching_authorities() {
658 let one = Url::parse("https://example.com/path").unwrap();
659 let two = Url::parse("https://example.com/other").unwrap();
660 let three = Url::parse("http://example.com/other").unwrap();
661 assert!(one.same_authority(&two));
662 assert!(!one.same_authority(&three));
663 }
664
665 #[test]
666 fn covers_display_and_tourl_variants() {
667 let url = Url::parse("http://example.com/path").unwrap();
668 assert_eq!(url.to_string(), "http://example.com/path");
669 assert_eq!(url.to_url().unwrap(), url);
670 assert_eq!(<&Url as ToUrl>::to_url(&&url).unwrap(), url);
671 }
672
673 #[test]
674 fn covers_missing_hosts_and_invalid_targets() {
675 assert!(matches!(
676 Url::parse("http://"),
677 Err(NanoGetError::InvalidUrl(_))
678 ));
679 assert!(matches!(
680 Url::parse("http:///path"),
681 Err(NanoGetError::InvalidUrl(_))
682 ));
683 assert!(matches!(
684 split_authority_and_target(""),
685 Err(NanoGetError::InvalidUrl(_))
686 ));
687 assert!(matches!(
688 Url::parse("http://:80/path"),
689 Err(NanoGetError::InvalidUrl(_))
690 ));
691 assert!(matches!(
692 parse_target("not-a-path"),
693 Err(NanoGetError::InvalidUrl(_))
694 ));
695 assert_eq!(
696 parse_target("?x=1").unwrap(),
697 ("/".to_string(), Some("x=1".to_string()))
698 );
699 }
700
701 #[test]
702 fn rejects_hosts_and_targets_with_controls_or_whitespace() {
703 assert!(matches!(
704 Url::parse("http://exa mple.com"),
705 Err(NanoGetError::InvalidUrl(_))
706 ));
707 assert!(matches!(
708 Url::parse("http://example.com/hello world"),
709 Err(NanoGetError::InvalidUrl(_))
710 ));
711 assert!(matches!(
712 Url::parse("http://example.com/path?x=\n1"),
713 Err(NanoGetError::InvalidUrl(_))
714 ));
715 assert!(matches!(
716 Url::parse("http://example.com/caf\u{00e9}"),
717 Err(NanoGetError::InvalidUrl(_))
718 ));
719 assert!(matches!(
720 Url::parse("http://example.com/path?q=\u{00e9}"),
721 Err(NanoGetError::InvalidUrl(_))
722 ));
723
724 let base = Url::parse("http://example.com/path").unwrap();
725 assert!(matches!(
726 base.resolve("/caf\u{00e9}"),
727 Err(NanoGetError::InvalidUrl(_))
728 ));
729 }
730
731 #[test]
732 fn covers_helper_branches_for_paths_and_ports() {
733 let ipv6 = Url::parse("http://[::1]/").unwrap();
734 assert_eq!(ipv6.authority_form(), "[::1]:80");
735
736 assert_eq!(default_port_for_scheme("http"), Some(80));
737 assert_eq!(default_port_for_scheme("https"), Some(443));
738 assert_eq!(default_port_for_scheme("ws"), None);
739
740 assert_eq!(super::base_directory("/a/b/"), "/a/b/");
741 assert_eq!(super::base_directory("/a"), "/");
742 assert_eq!(super::base_directory("a"), "/");
743 assert_eq!(normalize_path("/a/b/"), "/a/b/");
744 assert_eq!(normalize_path("/a//b/./c/../"), "/a//b/");
745
746 let base = Url::parse("http://example.com/path").unwrap();
747 let error = base.resolve(" ").unwrap_err();
748 assert!(matches!(error, NanoGetError::InvalidUrl(_)));
749 }
750
751 struct DeterministicRng {
752 state: u64,
753 }
754
755 impl DeterministicRng {
756 fn new(seed: u64) -> Self {
757 Self { state: seed }
758 }
759
760 fn next_u32(&mut self) -> u32 {
761 self.state = self
762 .state
763 .wrapping_mul(6_364_136_223_846_793_005)
764 .wrapping_add(1);
765 (self.state >> 32) as u32
766 }
767
768 fn next_location(&mut self, max_len: usize) -> String {
769 const ASCII: &[u8] =
770 b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/?#[]@!$&'()*+,;=%";
771 let len = (self.next_u32() as usize) % (max_len + 1);
772 let mut out = String::with_capacity(len);
773 for _ in 0..len {
774 let roll = self.next_u32() % 29;
775 if roll == 0 {
776 out.push('\u{00e9}');
777 } else if roll == 1 {
778 out.push('\u{2603}');
779 } else {
780 let idx = (self.next_u32() as usize) % ASCII.len();
781 out.push(ASCII[idx] as char);
782 }
783 }
784 out
785 }
786 }
787
788 #[test]
789 fn deterministic_url_parse_and_resolve_fuzz_harness_is_panic_free() {
790 let base = Url::parse("http://example.com/a/b/index.html?x=1").unwrap();
791 let corpus = [
792 "",
793 "http://example.com/path",
794 "https://example.com:8443/path?q=1",
795 "/foo://bar",
796 "foo:bar",
797 "//cdn.example.com/resource",
798 "?query=1",
799 "/caf\u{00e9}",
800 "http://[::1]/",
801 "http://[:::1]/",
802 "http://user@example.com",
803 "http://example.com/hello world",
804 ];
805
806 for input in corpus {
807 let run = panic::catch_unwind(AssertUnwindSafe(|| {
808 let _ = Url::parse(input);
809 let _ = base.resolve(input);
810 }));
811 assert!(run.is_ok(), "URL parsing panicked for corpus input");
812 }
813
814 let mut rng = DeterministicRng::new(0x1234_5678_ABCD_EF01);
815 for _ in 0..3_000 {
816 let input = rng.next_location(128);
817 let run = panic::catch_unwind(AssertUnwindSafe(|| {
818 let _ = Url::parse(&input);
819 let _ = base.resolve(&input);
820 }));
821 assert!(run.is_ok(), "URL parsing panicked for fuzz input");
822 }
823 }
824}