use std::collections::HashSet;
use std::path::Path;
#[derive(Debug, Default, Clone)]
pub struct GfwList {
suffixes: HashSet<String>,
}
impl GfwList {
pub fn from_lines<I, S>(lines: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut suffixes = HashSet::new();
for line in lines {
if let Some(domain) = normalize(line.as_ref()) {
suffixes.insert(domain);
}
}
Self { suffixes }
}
pub fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
let text = std::fs::read_to_string(path)?;
Ok(Self::from_lines(text.lines()))
}
pub fn len(&self) -> usize {
self.suffixes.len()
}
pub fn is_empty(&self) -> bool {
self.suffixes.is_empty()
}
pub fn matches(&self, domain: &str) -> bool {
let domain = domain.trim_end_matches('.').to_ascii_lowercase();
if domain.is_empty() {
return false;
}
let bytes = domain.as_bytes();
if self.suffixes.contains(&domain) {
return true;
}
for (i, &b) in bytes.iter().enumerate() {
if b == b'.' {
if self.suffixes.contains(&domain[i + 1..]) {
return true;
}
}
}
false
}
}
fn normalize(line: &str) -> Option<String> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('!') {
return None;
}
let line = line
.trim_start_matches("*.")
.trim_start_matches('.')
.trim_end_matches('.')
.to_ascii_lowercase();
if line.is_empty() {
None
} else {
Some(line)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> GfwList {
GfwList::from_lines([
"# a comment",
"! autoproxy comment",
"",
"google.com",
"*.twitter.com",
".wikipedia.org",
"EXAMPLE.NET.",
])
}
#[test]
fn normalizes_and_skips_comments() {
let l = sample();
assert_eq!(l.len(), 4);
assert!(l.matches("google.com"));
assert!(l.matches("twitter.com"));
assert!(l.matches("wikipedia.org"));
assert!(l.matches("example.net"));
}
#[test]
fn matches_subdomains_only() {
let l = sample();
assert!(l.matches("www.google.com"));
assert!(l.matches("a.b.c.google.com"));
assert!(l.matches("WWW.Google.Com")); assert!(l.matches("api.twitter.com."));
assert!(!l.matches("google.com.cn")); assert!(!l.matches("notgoogle.com"));
assert!(!l.matches("com"));
assert!(!l.matches("baidu.com"));
assert!(!l.matches(""));
}
}