rsubdomain 1.2.14

A high-performance subdomain brute-force tool written in Rust
Documentation
use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufRead};

use crate::subdata;

pub(super) fn resolve_dictionary(
    dictionary: Option<&Vec<String>>,
    dictionary_file: Option<&str>,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    if let Some(dictionary) = dictionary {
        return Ok(normalize_dictionary_entries(dictionary.iter().cloned()));
    }

    if let Some(file_path) = dictionary_file {
        return load_dictionary_from_file(file_path);
    }

    Ok(subdata::get_default_sub_next_data()
        .iter()
        .map(|value| (*value).to_string())
        .collect())
}

pub(super) fn load_dictionary_from_file(
    file_path: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let file = File::open(file_path)?;
    let reader = io::BufReader::new(file);
    let lines = reader.lines().collect::<Result<Vec<_>, _>>()?;
    Ok(normalize_dictionary_entries(lines))
}

pub(super) fn normalize_dictionary_entries<I>(entries: I) -> Vec<String>
where
    I: IntoIterator<Item = String>,
{
    let mut seen = HashSet::new();
    let mut normalized = Vec::new();

    for entry in entries {
        let value = entry.trim();
        if value.is_empty() || value.starts_with('#') {
            continue;
        }

        let value = value.trim_matches('.');
        if value.is_empty() || value.chars().any(char::is_whitespace) {
            continue;
        }

        let value = value.to_string();
        if seen.insert(value.clone()) {
            normalized.push(value);
        }
    }

    normalized
}

#[cfg(test)]
mod tests {
    use super::{normalize_dictionary_entries, resolve_dictionary};

    #[test]
    fn normalize_dictionary_entries_skips_blank_comments_and_duplicates() {
        let entries = vec![
            "".to_string(),
            "  ".to_string(),
            "# comment".to_string(),
            "www".to_string(),
            ".mail.".to_string(),
            "www".to_string(),
            "api v2".to_string(),
            "api.v2".to_string(),
        ];

        let normalized = normalize_dictionary_entries(entries);

        assert_eq!(normalized, vec!["www", "mail", "api.v2"]);
    }

    #[test]
    fn resolve_dictionary_uses_embedded_defaults_when_no_source_is_provided() {
        let dictionary = resolve_dictionary(None, None).expect("embedded dictionary should load");

        assert!(!dictionary.is_empty());
        assert_eq!(dictionary[0], "vk");
    }
}