1use ::url::{Host, Url};
4use xxhash_rust::xxh3::xxh3_64;
5
6pub fn normalize(base: &Url, href: &str) -> Option<Url> {
14 let href = href.trim();
15 if href.is_empty() || href.starts_with('#') {
16 return None;
17 }
18 let mut url = base.join(href).ok()?;
19 if !matches!(url.scheme(), "http" | "https") {
20 return None;
21 }
22 url.set_fragment(None);
23
24 if let Some(Host::Domain(domain)) = url.host() {
25 if domain.is_empty() {
26 return None;
27 }
28 if let Some(stripped) = domain.strip_suffix('.') {
29 let stripped = stripped.to_owned();
30 url.set_host(Some(&stripped)).ok()?;
31 }
32 } else if url.host().is_none() {
33 return None;
34 }
35
36 let path = normalize_escapes(url.path());
37 if path != url.path() {
38 url.set_path(&path);
39 }
40 if let Some(query) = url.query() {
41 let query = normalize_escapes(query);
42 if Some(query.as_str()) != url.query() {
43 url.set_query(Some(&query));
44 }
45 }
46 if url.query() == Some("") {
47 url.set_query(None);
48 }
49 Some(url)
50}
51
52pub fn url_hash(url: &Url) -> u64 {
54 xxh3_64(url.as_str().as_bytes())
55}
56
57fn normalize_escapes(s: &str) -> String {
58 let bytes = s.as_bytes();
59 let mut out = String::with_capacity(s.len());
60 let mut i = 0;
61 while i < bytes.len() {
62 if bytes[i] == b'%' && i + 2 < bytes.len() {
63 let (hi, lo) = (bytes[i + 1], bytes[i + 2]);
64 if let (Some(h), Some(l)) = (hex_value(hi), hex_value(lo)) {
65 let decoded = h * 16 + l;
66 if is_unreserved(decoded) {
67 out.push(decoded as char);
68 } else {
69 out.push('%');
70 out.push(hi.to_ascii_uppercase() as char);
71 out.push(lo.to_ascii_uppercase() as char);
72 }
73 i += 3;
74 continue;
75 }
76 }
77 let ch = s[i..].chars().next().expect("index is on a char boundary");
79 out.push(ch);
80 i += ch.len_utf8();
81 }
82 out
83}
84
85fn hex_value(b: u8) -> Option<u8> {
86 match b {
87 b'0'..=b'9' => Some(b - b'0'),
88 b'a'..=b'f' => Some(b - b'a' + 10),
89 b'A'..=b'F' => Some(b - b'A' + 10),
90 _ => None,
91 }
92}
93
94fn is_unreserved(b: u8) -> bool {
95 b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_' | b'~')
96}