//! URL matching for autofill (docs/05 "Matching rules"). use url::Url; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum MatchLevel { None = 0, /// Same registrable domain (eTLD+1), different host. Domain = 1, /// Same host, different port. Host = 2, /// Same scheme, host and port. Exact = 3, } /// Compares a saved item URL with the page URL. Never matches across schemes, /// except that an `http` item may match an `https` page (an upgrade) but never the reverse. pub fn match_url(item_url: &str, page_url: &str) -> MatchLevel { let (Some(item), Some(page)) = (parse(item_url), parse(page_url)) else { return MatchLevel::None; }; let scheme_ok = item.scheme() == page.scheme() || (item.scheme() == "http" && page.scheme() == "https"); if !scheme_ok { return MatchLevel::None; } let (Some(ih), Some(ph)) = (item.host_str(), page.host_str()) else { return MatchLevel::None; }; let (ih, ph) = (ih.to_ascii_lowercase(), ph.to_ascii_lowercase()); if ih == ph { return if item.port_or_known_default() == page.port_or_known_default() && item.scheme() == page.scheme() { MatchLevel::Exact } else { MatchLevel::Host }; } same_registrable_domain(&ih, &ph) } #[cfg(feature = "psl")] fn same_registrable_domain(a: &str, b: &str) -> MatchLevel { match (psl::domain_str(a), psl::domain_str(b)) { (Some(a), Some(b)) if a == b => MatchLevel::Domain, _ => MatchLevel::None, } } /// Without the Public Suffix List there is no safe way to tell `a.example.com` / `b.example.com` /// (same site) from `a.github.io` / `b.github.io` (different sites), so different hosts never match. #[cfg(not(feature = "psl"))] fn same_registrable_domain(_: &str, _: &str) -> MatchLevel { MatchLevel::None } fn parse(s: &str) -> Option { let s = s.trim(); // Saved URLs are often stored without a scheme ("example.com/login"). if s.contains("://") { Url::parse(s).ok() } else { Url::parse(&format!("https://{s}")).ok() } } #[cfg(test)] mod tests { use super::*; use MatchLevel::*; #[test] fn levels() { assert_eq!(match_url("https://example.com", "https://example.com/login"), Exact); assert_eq!(match_url("example.com", "https://example.com/"), Exact); assert_eq!(match_url("https://example.com:8443", "https://example.com"), Host); assert_eq!(match_url("https://example.com", "https://login.example.com"), Domain); assert_eq!(match_url("https://a.github.io", "https://b.github.io"), None); // public suffix assert_eq!(match_url("https://example.com", "https://example.com.evil.net"), None); assert_eq!(match_url("https://example.com", "http://example.com"), None); // no downgrade assert_eq!(match_url("http://example.com", "https://example.com"), Host); // upgrade allowed assert_eq!(match_url("https://example.co.uk", "https://other.co.uk"), None); } }