Initial scaffold for kestrove-core
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
//! 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
|
||||
};
|
||||
}
|
||||
match (psl::domain_str(&ih), psl::domain_str(&ph)) {
|
||||
(Some(a), Some(b)) if a == b => MatchLevel::Domain,
|
||||
_ => MatchLevel::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Option<Url> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user