use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;
#[derive(Clone)]
pub struct NxZones {
zones: HashSet<String>,
}
impl Default for NxZones {
fn default() -> Self {
Self::new()
}
}
impl NxZones {
#[allow(dead_code)]
pub fn new() -> Self {
Self {
zones: HashSet::new(),
}
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut zones = HashSet::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let mut domain = trimmed.to_lowercase();
if !domain.ends_with('.') {
domain.push('.');
}
zones.insert(domain);
}
Ok(Self { zones })
}
pub fn is_nonexistent(&self, domain: &str) -> bool {
if self.zones.is_empty() {
return false;
}
if self.zones.contains(".") {
return true;
}
let mut normalized = domain.to_lowercase();
if !normalized.ends_with('.') {
normalized.push('.');
}
if self.zones.contains(&normalized) {
return true;
}
let mut parts: Vec<&str> = normalized.split('.').collect();
while parts.len() > 2 {
parts.remove(0);
let parent = parts.join(".");
if self.zones.contains(&parent) {
return true;
}
}
false
}
pub fn len(&self) -> usize {
self.zones.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.zones.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_is_nonexistent_empty() {
let zones = NxZones::new();
assert!(!zones.is_nonexistent("example.com"));
assert!(!zones.is_nonexistent("test.example.com"));
}
#[test]
fn test_is_nonexistent_exact_match() {
let mut zones = NxZones::new();
zones.zones.insert("example.com.".to_string());
assert!(zones.is_nonexistent("example.com"));
assert!(zones.is_nonexistent("example.com."));
assert!(!zones.is_nonexistent("test.com"));
}
#[test]
fn test_is_nonexistent_subdomain() {
let mut zones = NxZones::new();
zones.zones.insert("example.com.".to_string());
assert!(zones.is_nonexistent("sub.example.com"));
assert!(zones.is_nonexistent("deep.sub.example.com"));
assert!(!zones.is_nonexistent("notexample.com"));
}
#[test]
fn test_load_from_file() -> io::Result<()> {
let mut file = NamedTempFile::new()?;
writeln!(file, "nonexistent.com")?;
writeln!(file, "# Comment line")?;
writeln!(file)?;
writeln!(file, "invalid.org.")?;
let zones = NxZones::load_from_file(file.path())?;
assert_eq!(zones.len(), 2);
assert!(zones.is_nonexistent("nonexistent.com"));
assert!(zones.is_nonexistent("sub.nonexistent.com"));
assert!(zones.is_nonexistent("invalid.org"));
assert!(!zones.is_nonexistent("other.com"));
Ok(())
}
#[test]
fn test_root_zone_matches_every_name() -> io::Result<()> {
let mut file = NamedTempFile::new()?;
writeln!(file, ".")?;
let zones = NxZones::load_from_file(file.path())?;
assert!(zones.is_nonexistent("example.com"));
Ok(())
}
}