use cedarwood::Cedar;
use regex::Regex;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
RegexError(#[from] regex::Error),
}
pub const REGEXP_DOMAIN_OR_PREFIX: &str = r"^([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]*\.)+([a-zA-Z]{2,}|\*)";
fn reverse_string(text: &str) -> String {
text.chars().rev().collect::<String>()
}
#[derive(Debug, Clone)]
pub struct DomainMatchingRule {
prefix_cedar: Cedar,
suffix_cedar: Cedar,
prefix_dict: Vec<String>,
suffix_dict: Vec<String>,
}
impl TryFrom<Vec<&str>> for DomainMatchingRule {
type Error = Error;
fn try_from(domain_list: Vec<&str>) -> Result<Self, Self::Error> {
DomainMatchingRule::try_from(domain_list.as_slice())
}
}
impl TryFrom<Vec<String>> for DomainMatchingRule {
type Error = Error;
fn try_from(domain_list: Vec<String>) -> Result<Self, Self::Error> {
let domain_list: Vec<&str> = domain_list.iter().map(AsRef::as_ref).collect();
DomainMatchingRule::try_from(domain_list)
}
}
impl TryFrom<&[String]> for DomainMatchingRule {
type Error = Error;
fn try_from(domain_list: &[String]) -> Result<Self, Self::Error> {
let domain_list: Vec<&str> = domain_list.iter().map(AsRef::as_ref).collect();
DomainMatchingRule::try_from(domain_list)
}
}
impl TryFrom<&[&str]> for DomainMatchingRule {
type Error = Error;
fn try_from(domain_list: &[&str]) -> Result<Self, Self::Error> {
let start_with_star = Regex::new(r"^\*\..+")?;
let end_with_star = Regex::new(r".+\.\*$")?;
let re = Regex::new(&format!("{}{}{}", r"^", REGEXP_DOMAIN_OR_PREFIX, r"$"))?;
let dict: Vec<String> = domain_list
.iter()
.map(|d| if start_with_star.is_match(d) { &d[2..] } else { d })
.filter(|x| re.is_match(x) || (x.split('.').count() == 1))
.map(|y| y.to_ascii_lowercase())
.collect();
let prefix_dict: Vec<String> = dict
.iter()
.filter(|d| end_with_star.is_match(d))
.map(|d| d[..d.len() - 2].to_string())
.collect();
let suffix_dict: Vec<String> = dict
.iter()
.filter(|d| !end_with_star.is_match(d))
.map(|d| reverse_string(d))
.collect();
let prefix_kv: Vec<(&str, i32)> = prefix_dict
.iter()
.map(AsRef::as_ref)
.enumerate()
.map(|(k, s)| (s, k as i32))
.collect();
let mut prefix_cedar = Cedar::new();
prefix_cedar.build(&prefix_kv);
let suffix_kv: Vec<(&str, i32)> = suffix_dict
.iter()
.map(AsRef::as_ref)
.enumerate()
.map(|(k, s)| (s, k as i32))
.collect();
let mut suffix_cedar = Cedar::new();
suffix_cedar.build(&suffix_kv);
Ok(DomainMatchingRule {
prefix_cedar,
suffix_cedar,
prefix_dict,
suffix_dict,
})
}
}
#[inline]
fn find_match<'a>(name: &'a str, ceder: &'a Cedar, dict: &'a [String]) -> impl Iterator<Item = (String, usize)> + 'a {
ceder
.common_prefix_iter(name)
.map(|(x, matched_prefix_len)| (dict[x as usize].clone(), matched_prefix_len))
}
#[inline]
fn find_match_all_inner(name: &str, cedar: &Cedar, dict: &[String]) -> Vec<String> {
let matched_items = find_match(name, &cedar, &dict);
matched_items
.filter_map(|(found, _)| {
if is_matched_as_domain(&found, name) {
Some(found)
} else {
None
}
})
.collect()
}
#[inline]
fn find_match_inner(name: &str, cedar: &Cedar, dict: &[String]) -> bool {
let mut matched_items = find_match(name, &cedar, &dict);
matched_items.any(|(found, _)| is_matched_as_domain(&found, name))
}
#[inline]
fn find_match_longest_inner(name: &str, cedar: &Cedar, dict: &[String]) -> Option<String> {
let matched_items = find_match(name, &cedar, &dict);
let longest_matched_as_domain = matched_items
.filter(|(found, _)| is_matched_as_domain(found, name))
.max_by_key(|(_, len)| *len);
longest_matched_as_domain.map(|(found, _)| found)
}
#[inline]
fn is_matched_as_domain(matched: &str, domain_name: &str) -> bool {
if matched.len() == domain_name.len() {
true
} else if let Some(nth) = domain_name.chars().nth(matched.chars().count()) {
nth.to_string() == "."
} else {
false
}
}
impl DomainMatchingRule {
pub fn find_suffix_match(&self, domain_name: &str) -> bool {
let reverse_domain_name = reverse_string(domain_name);
find_match_inner(&reverse_domain_name, &self.suffix_cedar, &self.suffix_dict)
}
pub fn find_suffix_match_all(&self, domain_name: &str) -> Vec<String> {
let reverse_domain_name = reverse_string(domain_name);
find_match_all_inner(&reverse_domain_name, &self.suffix_cedar, &self.suffix_dict)
}
pub fn find_suffix_match_longest(&self, domain_name: &str) -> Option<String> {
let reverse_domain_name = reverse_string(domain_name);
find_match_longest_inner(&reverse_domain_name, &self.suffix_cedar, &self.suffix_dict)
}
pub fn find_prefix_match(&self, domain_name: &str) -> bool {
find_match_inner(domain_name, &self.prefix_cedar, &self.prefix_dict)
}
pub fn find_prefix_match_all(&self, domain_name: &str) -> Vec<String> {
find_match_all_inner(domain_name, &self.prefix_cedar, &self.prefix_dict)
}
pub fn find_prefix_match_longest(&self, domain_name: &str) -> Option<String> {
find_match_longest_inner(domain_name, &self.prefix_cedar, &self.prefix_dict)
}
pub fn is_matched(&self, domain_name: &str) -> bool {
if self.find_suffix_match(domain_name) {
return true;
}
if self.find_prefix_match(domain_name) {
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matching_works() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"www.google.com".to_string(),
"*.google.com".to_string(),
"yahoo.co.*".to_string(),
])
.unwrap();
assert!(domain_matching_rule.is_matched("wwxx.google.com"));
assert!(domain_matching_rule.is_matched("yahoo.co.jp"));
assert!(!domain_matching_rule.is_matched("www.yahoo.com"));
assert!(!domain_matching_rule.is_matched("www.yahoo.co.jp"));
}
#[test]
fn matching_works_regardless_of_dns0x20() {
let domain_matching_rule = DomainMatchingRule::try_from(vec!["GOOGLE.com".to_string()]).unwrap();
assert!(domain_matching_rule.is_matched("www.google.com"));
assert!(domain_matching_rule.is_matched("WWW.gOoGlE.COM".to_ascii_lowercase().as_str()));
}
#[test]
fn find_suffix_match_all_works() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"google.com".to_string(),
"*.google.com".to_string(),
"com".to_string(),
"example.com".to_string(),
])
.unwrap();
let matches = domain_matching_rule.find_suffix_match_all("test.google.com");
assert!(matches.contains(&"moc.elgoog".to_string())); assert!(matches.contains(&"moc".to_string())); assert_eq!(matches.len(), 2);
let matches = domain_matching_rule.find_suffix_match_all("example.com");
assert!(matches.contains(&"moc.elpmaxe".to_string())); assert!(matches.contains(&"moc".to_string())); assert_eq!(matches.len(), 2);
let matches = domain_matching_rule.find_suffix_match_all("yahoo.org");
assert!(matches.is_empty());
let matches = domain_matching_rule.find_suffix_match_all("google.com");
assert!(matches.contains(&"moc.elgoog".to_string()));
assert!(matches.contains(&"moc".to_string()));
assert_eq!(matches.len(), 2);
}
#[test]
fn find_suffix_match_longest_works() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"google.com".to_string(),
"*.google.com".to_string(),
"com".to_string(),
"example.com".to_string(),
])
.unwrap();
let longest = domain_matching_rule.find_suffix_match_longest("test.google.com");
assert_eq!(longest, Some("moc.elgoog".to_string()));
let longest = domain_matching_rule.find_suffix_match_longest("example.com");
assert_eq!(longest, Some("moc.elpmaxe".to_string()));
let longest = domain_matching_rule.find_suffix_match_longest("yahoo.org");
assert_eq!(longest, None);
let domain_matching_rule_single = DomainMatchingRule::try_from(vec!["test.org".to_string()]).unwrap();
let longest = domain_matching_rule_single.find_suffix_match_longest("test.org");
assert_eq!(longest, Some("gro.tset".to_string()));
}
#[test]
fn find_suffix_match_works() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"google.com".to_string(),
"*.google.com".to_string(),
"example.org".to_string(),
])
.unwrap();
assert!(domain_matching_rule.find_suffix_match("www.google.com"));
assert!(domain_matching_rule.find_suffix_match("google.com"));
assert!(domain_matching_rule.find_suffix_match("example.org"));
assert!(!domain_matching_rule.find_suffix_match("yahoo.com"));
assert!(!domain_matching_rule.find_suffix_match("google.org"));
}
#[test]
fn find_prefix_match_works() {
let domain_matching_rule =
DomainMatchingRule::try_from(vec!["www.example.*".to_string(), "blog.test.*".to_string()]).unwrap();
assert!(domain_matching_rule.find_prefix_match("www.example.com"));
assert!(domain_matching_rule.find_prefix_match("www.example.org"));
assert!(domain_matching_rule.find_prefix_match("blog.test.net"));
assert!(!domain_matching_rule.find_prefix_match("api.example.com"));
assert!(!domain_matching_rule.find_prefix_match("www.google.com"));
assert!(!domain_matching_rule.find_prefix_match("example.com"));
}
#[test]
fn find_prefix_match_all_works() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"www.example.*".to_string(),
"www.*".to_string(),
"blog.test.*".to_string(),
"example.*".to_string(),
])
.unwrap();
let matches = domain_matching_rule.find_prefix_match_all("www.example.com");
assert!(matches.contains(&"www.example".to_string()));
assert!(matches.contains(&"www".to_string()));
assert_eq!(matches.len(), 2);
let matches = domain_matching_rule.find_prefix_match_all("blog.test.net");
assert!(matches.contains(&"blog.test".to_string()));
assert_eq!(matches.len(), 1);
let matches = domain_matching_rule.find_prefix_match_all("api.google.com");
assert!(matches.is_empty());
let matches = domain_matching_rule.find_prefix_match_all("example.org");
assert!(matches.contains(&"example".to_string()));
assert_eq!(matches.len(), 1);
let matches = domain_matching_rule.find_prefix_match_all("www.test.co.uk");
assert!(matches.contains(&"www".to_string()));
assert_eq!(matches.len(), 1);
}
#[test]
fn find_prefix_match_longest_works() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"www.example.*".to_string(),
"www.*".to_string(),
"blog.test.*".to_string(),
"example.*".to_string(),
])
.unwrap();
let longest = domain_matching_rule.find_prefix_match_longest("www.example.com");
assert_eq!(longest, Some("www.example".to_string()));
let longest = domain_matching_rule.find_prefix_match_longest("blog.test.net");
assert_eq!(longest, Some("blog.test".to_string()));
let longest = domain_matching_rule.find_prefix_match_longest("api.google.com");
assert_eq!(longest, None);
let longest = domain_matching_rule.find_prefix_match_longest("example.org");
assert_eq!(longest, Some("example".to_string()));
let longest = domain_matching_rule.find_prefix_match_longest("www.test.co.uk");
assert_eq!(longest, Some("www".to_string()));
}
#[test]
fn edge_cases_work() {
let domain_matching_rule =
DomainMatchingRule::try_from(vec!["a.com".to_string(), "*.b.com".to_string(), "c.*".to_string()]).unwrap();
assert!(domain_matching_rule.find_suffix_match("a.com"));
assert!(domain_matching_rule.find_suffix_match("x.b.com"));
assert!(domain_matching_rule.find_prefix_match("c.org"));
let short_domain_rule = DomainMatchingRule::try_from(vec!["co".to_string()]).unwrap();
assert!(short_domain_rule.find_suffix_match("co"));
let empty_matches = domain_matching_rule.find_suffix_match_all("nonexistent.domain");
assert!(empty_matches.is_empty());
let no_longest = domain_matching_rule.find_suffix_match_longest("nonexistent.domain");
assert_eq!(no_longest, None);
}
#[test]
fn mixed_prefix_suffix_patterns_work() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"*.google.com".to_string(), "www.example.*".to_string(), "exact.domain.net".to_string(), ])
.unwrap();
assert!(domain_matching_rule.is_matched("api.google.com"));
assert!(domain_matching_rule.is_matched("mail.google.com"));
assert!(domain_matching_rule.is_matched("exact.domain.net"));
assert!(domain_matching_rule.is_matched("www.example.com"));
assert!(domain_matching_rule.is_matched("www.example.org"));
assert!(domain_matching_rule.is_matched("google.com")); assert!(!domain_matching_rule.is_matched("example.com")); assert!(!domain_matching_rule.is_matched("api.example.com"));
}
#[test]
fn debug_pattern_behavior() {
let domain_matching_rule = DomainMatchingRule::try_from(vec!["*.google.com".to_string()]).unwrap();
assert!(domain_matching_rule.is_matched("api.google.com"));
assert!(domain_matching_rule.is_matched("google.com"));
let domain_matching_rule2 = DomainMatchingRule::try_from(vec!["www.example.*".to_string()]).unwrap();
assert!(domain_matching_rule2.is_matched("www.example.com"));
assert!(!domain_matching_rule2.is_matched("example.com"));
assert!(!domain_matching_rule2.is_matched("api.example.com"));
}
#[test]
fn test_try_from_implementations() {
let domains_vec_str = vec!["google.com", "*.example.com"];
let rule1 = DomainMatchingRule::try_from(domains_vec_str).unwrap();
assert!(rule1.is_matched("google.com"));
assert!(rule1.is_matched("test.example.com"));
let domains_vec_string = vec!["google.com".to_string(), "*.example.com".to_string()];
let rule2 = DomainMatchingRule::try_from(domains_vec_string).unwrap();
assert!(rule2.is_matched("google.com"));
assert!(rule2.is_matched("test.example.com"));
let domains_slice_string = vec!["google.com".to_string(), "*.example.com".to_string()];
let rule3 = DomainMatchingRule::try_from(domains_slice_string.as_slice()).unwrap();
assert!(rule3.is_matched("google.com"));
assert!(rule3.is_matched("test.example.com"));
let domains_slice_str: &[&str] = &["google.com", "*.example.com"];
let rule4 = DomainMatchingRule::try_from(domains_slice_str).unwrap();
assert!(rule4.is_matched("google.com"));
assert!(rule4.is_matched("test.example.com"));
}
#[test]
fn test_invalid_regex_patterns() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"valid.com".to_string(),
"invalid..domain".to_string(), "*.valid.org".to_string(),
])
.unwrap();
assert!(domain_matching_rule.is_matched("valid.com"));
assert!(domain_matching_rule.is_matched("test.valid.org"));
assert!(!domain_matching_rule.is_matched("invalid.domain"));
}
#[test]
fn test_empty_input() {
let empty_rule = DomainMatchingRule::try_from(vec![] as Vec<String>).unwrap();
assert!(!empty_rule.is_matched("google.com"));
assert!(!empty_rule.is_matched("example.org"));
assert!(empty_rule.find_suffix_match_all("google.com").is_empty());
assert_eq!(empty_rule.find_suffix_match_longest("google.com"), None);
assert!(!empty_rule.find_suffix_match("google.com"));
assert!(!empty_rule.find_prefix_match("google.com"));
}
#[test]
fn test_complex_multilevel_domains() {
let domain_matching_rule = DomainMatchingRule::try_from(vec![
"*.api.service.example.com".to_string(),
"deep.nested.domain.*".to_string(),
"a.b.c.d.e.f.g.com".to_string(),
])
.unwrap();
assert!(domain_matching_rule.is_matched("v1.api.service.example.com"));
assert!(domain_matching_rule.is_matched("api.service.example.com"));
assert!(domain_matching_rule.is_matched("deep.nested.domain.org"));
assert!(domain_matching_rule.is_matched("deep.nested.domain.co.uk"));
assert!(domain_matching_rule.is_matched("a.b.c.d.e.f.g.com"));
assert!(!domain_matching_rule.is_matched("service.example.com"));
assert!(!domain_matching_rule.is_matched("nested.domain.org"));
}
}