Fix this code. Extreme revise it
use clap::{Arg, ArgGroup, Command};
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use lazy_static::lazy_static;
use prettytable::{row, Table};
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use rayon::prelude::*;
use regex::Regex;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use retry::{delay::Exponential, retry};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use thiserror::Error;
use tracing::{debug, error, info, warn};

/// Custom application error types.
#[derive(Debug, Error)]
enum AppError {
    #[error("IO error occurred: {0}")]
    Io(#[from] io::Error),
    #[error("HTTP request error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("Serialization error: {0}")]
    Serde(#[from] serde_json::Error),
    #[error("Configuration error: {0}")]
    Config(String),
    #[error("Invalid argument: {0}")]
    InvalidArg(String),
}

/// Represents information about a valid channel.
#[derive(Debug, Clone, Serialize)]
struct ChannelInfo {
    #[serde(skip_serializing)]
    colored_channel: ColoredString,
    #[serde(rename = "channel")]
    original_channel: String,
    roblox_exe: bool,
    deploy_history: bool,
    mac_deploy_history: bool,
    client_settings: bool,
}

/// Serializable version of ChannelInfo without colored_channel.
#[derive(Debug, Deserialize, Serialize)]
struct ChannelInfoSerializable {
    channel: String,
    roblox_exe: bool,
    deploy_history: bool,
    mac_deploy_history: bool,
    client_settings: bool,
}

/// Holds categorized words extracted from known channels.
#[derive(Debug, Default)]
struct ChannelCategories {
    features: HashSet<String>,
    actions: HashSet<String>,
    modifiers: HashSet<String>,
    endings: HashSet<String>,
    numbers: HashSet<String>,
    tests: HashSet<String>,
    improvements: HashSet<String>,
}

impl ChannelCategories {
    /// Populates categories based on known channel names.
    fn populate_from_known_channels(&mut self, channels: &[&str]) {
        for &channel in channels.iter() {
            self.categorize(channel);
        }
    }

    /// Categorizes a single channel name.
    fn categorize(&mut self, channel: &str) {
        // Skip excluded channels
        if EXCLUDED_CHANNELS.contains(&channel.parse::<String>().unwrap().as_str()) {
            return;
        }

        // Only process channels starting with 'Z'
        if !channel.starts_with('Z') {
            return;
        }

        // Remove the 'Z' prefix
        let core = &channel[1..];

        // Split by non-alphanumeric characters
        let re = Regex::new(r"[^a-zA-Z0-9]").unwrap();
        let parts: Vec<&str> = re.split(core).collect();

        // Categorize based on position or keyword patterns
        for (i, part) in parts.iter().enumerate() {
            let part_lower = part.to_lowercase();
            match i {
                0 => {
                    // Assume the first part is a feature
                    self.features.insert(part.to_string());
                }
                1 => {
                    // Assume the second part is an action
                    self.actions.insert(part.to_string());
                }
                2 => {
                    // Assume the third part can be a modifier, number, test, or improvement
                    if part_lower.contains("test") {
                        self.tests.insert(part.to_string());
                    } else if part_lower.contains("improvement") {
                        self.improvements.insert(part.to_string());
                    } else if part_lower.parse::<u32>().is_ok() {
                        self.numbers.insert(part.to_string());
                    } else {
                        self.modifiers.insert(part.to_string());
                    }
                }
                _ => {
                    // Handle additional parts if necessary
                    if part_lower.parse::<u32>().is_ok() {
                        self.numbers.insert(part.to_string());
                    }
                }
            }
        }
    }
}

/// List of excluded channels that should not be processed or generated.
const EXCLUDED_CHANNELS: &[&str] = &["Common", "Ganesh"];

/// Configuration structure to load from JSON.
#[derive(Debug, Deserialize)]
struct Config {
    exclusions: Vec<String>,
    known_channels: Vec<String>,
    patterns: Vec<String>,
}

lazy_static! {
    static ref REGEX_FEATURE: Regex = Regex::new(r"^(feature|bugfix|hotfix|project|release|test)$").unwrap();
    static ref REGEX_ACTION: Regex = Regex::new(r"^(deploy|integrate|release|test|check|monitor)$").unwrap();
    static ref REGEX_MODIFIER: Regex = Regex::new(r"^(alpha|beta|prod|test|canary)$").unwrap();
    static ref REGEX_TEST: Regex = Regex::new(r"^(test|testing|experiment)$").unwrap();
    static ref REGEX_IMPROVEMENT: Regex = Regex::new(r"^(improvement|enhancement)$").unwrap();
}

/// Entry point of the application.
fn main() -> Result<(), Box<dyn Error>> {
    // Initialize logging with customizable verbosity
    tracing_subscriber::fmt::init();

    // Parse command-line arguments using Clap
    let matches = Command::new("Advanced Roblox Channel Checker")
        .version("3.0")
        .author("Your Name <youremail@example.com>")
        .about("Advanced tool to generate and validate Roblox channels based on wordlist combinations and predefined patterns")
        .arg(
            Arg::new("config")
                .short('f')
                .long("config")
                .value_name("FILE")
                .help("Sets a custom configuration file (JSON)")
                .required(true),
        )
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .value_name("FILE")
                .help("Sets a custom input wordlist file")
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .value_name("FILE")
                .help("Sets an output file to save results in JSON format")
        )
        .arg(
            Arg::new("concurrency")
                .short('c')
                .long("concurrency")
                .value_name("NUMBER")
                .help("Sets the level of concurrency for HTTP requests")
                .default_value("8")
        )
        .arg(
            Arg::new("timeout")
                .short('t')
                .long("timeout")
                .value_name("SECONDS")
                .help("Sets the timeout for HTTP requests in seconds")
                .default_value("10")
        )
        .arg(
            Arg::new("prefixes")
                .short('p')
                .long("prefixes")
                .value_name("PREFIXES")
                .help("Comma-separated list of prefixes to add to words")
        )
        .arg(
            Arg::new("suffixes")
                .short('s')
                .long("suffixes")
                .value_name("SUFFIXES")
                .help("Comma-separated list of suffixes to add to words")
        )
        .arg(
            Arg::new("random-strings")
                .short('r')
                .long("random-strings")
                .value_name("NUMBER")
                .help("Number of random alphanumeric strings to generate and add to the wordlist")
        )
        .arg(
            Arg::new("max-length")
                .short('m')
                .long("max-length")
                .value_name("NUMBER")
                .help("Maximum number of words in a channel name combination")
                .default_value("4")
        )
        .arg(
            Arg::new("max-combinations")
                .short('x')
                .long("max-combinations")
                .value_name("NUMBER")
                .help("Maximum number of channel name combinations to generate")
        )
        .group(
            ArgGroup::new("input_group")
                .args(["input"])
                .required(false),
        )
        .get_matches();

    // Load configuration
    let config_path = matches
        .get_one::<String>("config")
        .expect("Configuration file is required");
    let config = load_config(config_path)?;

    // Override exclusions from config if provided as command-line (currently, exclusions are loaded from config)
    // Read wordlist from file or stdin
    let wordlist = if let Some(input_path) = matches.get_one::<String>("input") {
        read_wordlist_from_file(input_path)?
    } else {
        info!("Reading wordlist from stdin. Enter words followed by Enter. Press Ctrl+D (Unix) or Ctrl+Z (Windows) to finish:");
        read_wordlist_from_stdin()?
    };

    // Deduplicate and sanitize wordlist
    let mut unique_wordlist: Vec<String> = wordlist
        .into_iter()
        .unique()
        .filter(|word| is_valid_word(word))
        .collect();
    unique_wordlist.sort();

    // Wordlist Augmentation
    if let Some(prefixes_str) = matches.get_one::<String>("prefixes") {
        let prefixes: Vec<&str> = prefixes_str.split(',').map(|s| s.trim()).collect();
        augment_wordlist_with_prefixes(&mut unique_wordlist, &prefixes);
    }

    if let Some(suffixes_str) = matches.get_one::<String>("suffixes") {
        let suffixes: Vec<&str> = suffixes_str.split(',').map(|s| s.trim()).collect();
        augment_wordlist_with_suffixes(&mut unique_wordlist, &suffixes);
    }

    if let Some(num_random_str) = matches.get_one::<String>("random-strings") {
        let num_random: usize = num_random_str.parse().map_err(|_| {
            AppError::InvalidArg(format!(
                "Invalid number for random-strings: {}",
                num_random_str
            ))
        })?;
        augment_wordlist_with_random_strings(&mut unique_wordlist, num_random)?;
    }

    // Remove duplicates created by augmentation
    unique_wordlist = unique_wordlist.into_iter().unique().collect();

    if unique_wordlist.is_empty() {
        error!("Wordlist is empty after processing. Please provide valid words.");
        return Ok(());
    }

    info!("Final wordlist size: {}", unique_wordlist.len());

    // Initialize channel categories based on known channels
    let mut categories = ChannelCategories::default();
    categories.populate_from_known_channels(&config.known_channels.iter().map(|s| s.as_str()).collect::<Vec<&str>>());

    // Generate sanitized combinations
    let max_length = matches
        .get_one::<String>("max-length")
        .unwrap()
        .parse::<usize>()
        .map_err(|_| AppError::InvalidArg("Invalid number for max-length".to_string()))?;
    info!("Generating combinations up to length {}", max_length);

    let max_combinations = matches
        .get_one::<String>("max-combinations")
        .map(|s| s.parse::<usize>())
        .transpose()
        .map_err(|_| AppError::InvalidArg("Invalid number for max-combinations".to_string()))?;

    let patterns = if !config.patterns.is_empty() {
        config.patterns.clone()
    } else {
        vec![
            "Z{Feature}{Action}{Modifier}{Number}".to_string(),
            "Z{Feature}{Action}{Number}".to_string(),
            "Z{Feature}{Action}{Test}".to_string(),
            "Z{Feature}{Action}{Improvement}".to_string(),
            "Z{Feature}{Action}".to_string(),
        ]
    };

    let combinations = generate_combinations(&unique_wordlist, &patterns, max_length, max_combinations);
    info!("Total unique combinations generated: {}", combinations.len());

    // Configure HTTP client
    let timeout = matches
        .get_one::<String>("timeout")
        .unwrap()
        .parse::<u64>()
        .map_err(|_| AppError::InvalidArg("Invalid number for timeout".to_string()))?;
    let client = Client::builder()
        .timeout(Duration::from_secs(timeout))
        .build()?;

    // Setup progress bar
    let pb = ProgressBar::new(combinations.len() as u64);
    pb.set_style(
        ProgressStyle::default_bar()
            .template(
                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
            )
            .unwrap()
            .progress_chars("#>-"),
    );

    // Configure concurrency
    let concurrency = matches
        .get_one::<String>("concurrency")
        .unwrap()
        .parse::<usize>()
        .map_err(|_| AppError::InvalidArg("Invalid number for concurrency".to_string()))?;
    rayon::ThreadPoolBuilder::new()
        .num_threads(concurrency)
        .build_global()
        .expect("Failed to build Rayon thread pool");

    info!(
        "Starting channel checks with concurrency level: {}",
        concurrency
    );

    // Initialize caching mechanism
    let cache_path = "channel_cache.json";
    let cache = Arc::new(Mutex::new(load_cache(cache_path)?));

    // Shared state for rate limiting
    let rate_limit = Arc::new(Mutex::new(RateLimitState::Ready));

    let start_time = Instant::now();

    // Process combinations in parallel
    let results: Vec<ChannelInfo> = combinations
        .par_iter()
        .filter_map(|combination| {
            // Enforce rate limiting: Max 100 requests per minute
            match enforce_rate_limit(&rate_limit, 100, Duration::from_secs(60)) {
                Some(()) => {}
                None => {
                    // If waiting, skip this iteration (could implement better waiting logic)
                    return None;
                }
            }

            // Check cache first
            {
                let cache = cache.lock().unwrap();
                if let Some(cached_info) = cache.get(combination) {
                    pb.inc(1);
                    return Some(ChannelInfo {
                        colored_channel: color_code_channel(combination),
                        original_channel: cached_info.channel.clone(),
                        roblox_exe: cached_info.roblox_exe,
                        deploy_history: cached_info.deploy_history,
                        mac_deploy_history: cached_info.mac_deploy_history,
                        client_settings: cached_info.client_settings,
                    });
                }
            }

            // Perform channel check
            let result = check_channel(&client, combination, &categories);
            pb.inc(1);

            // Update cache if channel is valid
            if let Some(ref info) = result {
                let mut cache = cache.lock().unwrap();
                cache.insert(
                    combination.clone(),
                    ChannelInfoSerializable {
                        channel: info.original_channel.clone(),
                        roblox_exe: info.roblox_exe,
                        deploy_history: info.deploy_history,
                        mac_deploy_history: info.mac_deploy_history,
                        client_settings: info.client_settings,
                    },
                );
            }

            result
        })
        .collect();

    pb.finish_with_message("Channel checks completed.");

    let processing_time = start_time.elapsed();

    // Display results in table format
    if results.is_empty() {
        info!("No valid channels found.");
    } else {
        display_results_table(&results);
    }

    // Save results to file if output path is provided
    if let Some(output_path) = matches.get_one::<String>("output") {
        info!("Saving results to {}", output_path);
        save_results_to_file(&results, output_path)?;
    }

    // Save cache
    save_cache(&cache.lock().unwrap(), cache_path)?;

    // Generate and display report
    generate_and_display_report(&results, combinations.len(), processing_time)?;

    Ok(())
}

/// Loads cached channel checks from a JSON file.
fn load_cache(path: &str) -> Result<HashMap<String, ChannelInfoSerializable>, AppError> {
    if Path::new(path).exists() {
        let content = fs::read_to_string(path)?;
        let cache: HashMap<String, ChannelInfoSerializable> = serde_json::from_str(&content)?;
        Ok(cache)
    } else {
        Ok(HashMap::new())
    }
}

/// Augments the wordlist with a specified number of random alphanumeric strings.
fn augment_wordlist_with_random_strings(wordlist: &mut Vec<String>, num_strings: usize) -> Result<(), AppError> {
    let _rng = thread_rng();
    for _ in 0..num_strings {
        let random_string: String = _rng.clone()
            .sample_iter(&Alphanumeric)
            .take(8) // Length of random string
            .map(char::from)
            .collect();
        wordlist.push(random_string);
    }
    Ok(())
}

/// Augments the wordlist with given prefixes.
fn augment_wordlist_with_prefixes(wordlist: &mut Vec<String>, prefixes: &[&str]) {
    let mut new_words = Vec::new();
    for prefix in prefixes {
        for word in wordlist.iter() {
            let new_word = format!("{}{}", prefix, word);
            if is_valid_word(&new_word) {
                new_words.push(new_word);
            } else {
                warn!("Invalid prefix combination skipped: {}", new_word);
            }
        }
    }
    wordlist.append(&mut new_words);
}

/// Augments the wordlist with given suffixes.
fn augment_wordlist_with_suffixes(wordlist: &mut Vec<String>, suffixes: &[&str]) {
    let mut new_words = Vec::new();
    for suffix in suffixes {
        for word in wordlist.iter() {
            let new_word = format!("{}{}", word, suffix);
            if is_valid_word(&new_word) {
                new_words.push(new_word);
            } else {
                warn!("Invalid suffix combination skipped: {}", new_word);
            }
        }
    }
    wordlist.append(&mut new_words);
}

/// Saves the cache of channel checks to a JSON file.
fn save_cache(cache: &HashMap<String, ChannelInfoSerializable>, path: &str) -> Result<(), AppError> {
    let json = serde_json::to_string_pretty(cache)?;
    fs::write(path, json)?;
    Ok(())
}

/// Loads configuration from a JSON file.
fn load_config<P: AsRef<Path>>(path: P) -> Result<Config, AppError> {
    let content = fs::read_to_string(path)?;
    let config: Config = serde_json::from_str(&content)?;
    Ok(config)
}

/// Reads wordlist from a specified file.
fn read_wordlist_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<String>, AppError> {
    let content = fs::read_to_string(path)?;
    let wordlist = content
        .lines()
        .map(|line| line.trim().to_string())
        .filter(|line| !line.is_empty())
        .collect();
    Ok(wordlist)
}

/// Reads wordlist from standard input.
fn read_wordlist_from_stdin() -> Result<Vec<String>, AppError> {
    let stdin = io::stdin();
    let wordlist = stdin
        .lock()
        .lines()
        .map_while(Result::ok)
        .map(|line| line.trim().to_string())
        .filter(|line| !line.is_empty())
        .collect();
    Ok(wordlist)
}

/// Checks if a word is valid (e.g., does not contain invalid characters).
fn is_valid_word(word: &str) -> bool {
    let invalid_pattern = Regex::new(r"[^a-zA-Z0-9]").unwrap();
    if invalid_pattern.is_match(word) {
        warn!("Invalid word detected and skipped: {}", word);
        false
    } else {
        true
    }
}

/// Generates all unique combinations based on patterns up to a specified length,
/// avoiding duplicates and adhering to category constraints.
fn generate_combinations(
    wordlist: &[String],
    patterns: &[String],
    max_length: usize,
    max_combinations: Option<usize>,
) -> HashSet<String> {
    let mut combinations = HashSet::new();
    let rng = thread_rng();

    for pattern in patterns {
        let regex = Regex::new(r"\{(\w+)\}").unwrap();
        let placeholders: Vec<&str> = regex
            .captures_iter(pattern)
            .filter_map(|cap| cap.get(1).map(|m| m.as_str()))
            .collect();

        // Generate mappings for placeholders
        let mappings: Vec<Vec<String>> = placeholders
            .iter()
            .map(|&placeholder| match placeholder.to_lowercase().as_str() {
                "feature" => wordlist
                    .iter()
                    .filter(|w| REGEX_FEATURE.is_match(w))
                    .cloned()
                    .collect(),
                "action" => wordlist
                    .iter()
                    .filter(|w| REGEX_ACTION.is_match(w))
                    .cloned()
                    .collect(),
                "modifier" => wordlist
                    .iter()
                    .filter(|w| REGEX_MODIFIER.is_match(w))
                    .cloned()
                    .collect(),
                "number" => wordlist
                    .iter()
                    .filter(|w| w.parse::<u32>().is_ok())
                    .cloned()
                    .collect(),
                "test" => wordlist
                    .iter()
                    .filter(|w| REGEX_TEST.is_match(w))
                    .cloned()
                    .collect(),
                "improvement" => wordlist
                    .iter()
                    .filter(|w| REGEX_IMPROVEMENT.is_match(w))
                    .cloned()
                    .collect(),
                _ => vec![],
            })
            .collect();

        // Skip patterns with empty mappings
        if mappings.iter().any(|m| m.is_empty()) {
            warn!("Pattern '{}' has empty mappings and will be skipped.", pattern);
            continue;
        }

        // Generate all combinations for the current pattern
        let all_combos = mappings.iter().multi_cartesian_product();

        for combo in all_combos {
            // Concatenate parts based on the pattern
            let mut channel = pattern.clone();
            for (placeholder, word) in placeholders.iter().zip(combo.iter()) {
                let replacement = format!("{}", word);
                let regex_placeholder = Regex::new(&format!(r"\{{{}\}}", placeholder)).unwrap();
                channel = regex_placeholder.replace(&channel, replacement.as_str()).to_string();
            }

            channel = channel.replace("__", "_"); // Clean up any double underscores
            channel = channel.trim_end_matches('_').to_string(); // Remove trailing underscores

            // Validate the combination
            if is_valid_combination(&channel, max_length) {
                combinations.insert(channel);
                // Respect max_combinations limit if set
                if let Some(max) = max_combinations {
                    if combinations.len() >= max {
                        return combinations;
                    }
                }
            }
        }
    }

    combinations
}

/// Validates the combination based on length and non-repeating consecutive words.
fn is_valid_combination(channel: &str, max_length: usize) -> bool {
    if channel.is_empty() {
        return false;
    }

    // Split by uppercase letters to get words
    let words: Vec<&str> = Regex::new(r"(?=[A-Z])").unwrap().split(channel).collect();

    if words.len() > max_length {
        return false;
    }

    // Check for consecutive duplicates
    for window in words.windows(2) {
        if window[0].eq_ignore_ascii_case(window[1]) {
            return false;
        }
    }

    true
}

/// Enforces rate limiting by allowing a maximum number of requests within a time window.
fn enforce_rate_limit(rate_limit: &Arc<Mutex<RateLimitState>>, max_requests: usize, window: Duration) -> Option<()> {
    let mut rate = rate_limit.lock().unwrap();
    match *rate {
        RateLimitState::Ready => {
            // Start tracking
            *rate = RateLimitState::Waiting(Instant::now(), Duration::from_secs(0), max_requests, window);
            Some(())
        }
        RateLimitState::Waiting(ref start, ref delay, ref remaining, ref window_duration) => {
            if start.elapsed() >= *window_duration {
                // Reset the rate limiter
                *rate = RateLimitState::Waiting(Instant::now(), Duration::from_secs(0), max_requests, *window_duration);
                Some(())
            } else if *remaining > 0 {
                    // Allow the request
                    *rate = RateLimitState::Waiting(*start, *delay, remaining - 1, *window_duration);
                    Some(())
                } else {
                    // Wait until the window resets
                    warn!("Rate limit exceeded. Skipping this request.");
                    None
            }
        }
    }
}

/// Enum representing rate limiting state.
enum RateLimitState {
    Ready,
    Waiting(Instant, Duration, usize, Duration), // Start time, current delay, remaining requests, window duration
}

/// Checks a single channel and returns ChannelInfo if valid.
/// Utilizes categories for advanced validation.
fn check_channel(
    client: &Client,
    channel_name: &str,
    categories: &ChannelCategories,
) -> Option<ChannelInfo> {
    // Advanced validation based on categories
    if !validate_channel_format(channel_name, categories) {
        return None;
    }

    // Define base URLs
    let base_url = format!("http://setup.rbxcdn.com/channel/zfeature{}", channel_name);
    let client_settings_url = format!(
        "http://clientsettings.roblox.com/v2/settings/application/RCCBootstrapper/bucket/zfeature{}",
        channel_name
    );

    // Define endpoints to check
    let endpoints = [
        ("Roblox.exe", format!("{}/Roblox.exe", base_url)),
        ("DeployHistory.txt", format!("{}/DeployHistory.txt", base_url)),
        ("Mac DeployHistory.txt", format!("{}/mac/DeployHistory.txt", base_url)),
        ("ClientSettings", client_settings_url),
    ];

    let mut results = vec![];

    for (name, url) in &endpoints {
        let result = retry(Exponential::from_millis(500).take(3), || {
            debug!("Checking {} at {}", name, url);
            match client.get(url).send() {
                Ok(response) => {
                    if response.status() == StatusCode::OK {
                        if *name == "ClientSettings" {
                            // Check ClientSettings response body
                            let body = response.text().unwrap_or_default();
                            Ok(!body.contains(r#"{"applicationSettings":null}"#))
                        } else {
                            Ok(true) // Other endpoints are valid if status is OK
                        }
                    } else {
                        Ok(false)
                    }
                }
                Err(e) => {
                    error!("Error checking {}: {}", url, e);
                    Err(e)
                }
            }
        })
        .unwrap_or(false);
        results.push(result);
    }

    if results[0] {
        Some(ChannelInfo {
            colored_channel: color_code_channel(channel_name),
            original_channel: channel_name.to_string(),
            roblox_exe: results[0],
            deploy_history: results[1],
            mac_deploy_history: results[2],
            client_settings: results[3],
        })
    } else {
        None
    }
}

/// Validates the channel format based on categorized words.
fn validate_channel_format(channel: &str, categories: &ChannelCategories) -> bool {
    // Remove 'Z' prefix
    let core = &channel[1..];

    // Split by uppercase letters to get words
    let words: Vec<&str> = Regex::new(r"(?=[A-Z])").unwrap().split(core).collect();

    if words.is_empty() {
        warn!("Channel '{}' has no parts after 'Z' prefix.", channel);
        return false;
    }

    // Basic pattern checks based on categories
    if words.len() < 2 {
        warn!("Channel '{}' does not meet minimum parts requirement.", channel);
        return false;
    }

    // Check feature
    if !categories.features.contains(words[0]) {
        warn!(
            "Channel '{}' has an unknown feature '{}'.",
            channel, words[0]
        );
        return false;
    }

    // Check action
    if !categories.actions.contains(words[1]) {
        warn!(
            "Channel '{}' has an unknown action '{}'.",
            channel, words[1]
        );
        return false;
    }

    // Check modifiers, numbers, tests, or improvements
    for &part in &words[2..] {
        let part_lower = part.to_lowercase();
        if !(categories.modifiers.contains(part)
            || categories.numbers.contains(part)
            || categories.tests.contains(part)
            || categories.improvements.contains(part))
        {
            warn!(
                "Channel '{}' has an unknown or invalid part '{}'.",
                channel, part
            );
            return false;
        }
    }

    true
}

/// Applies color coding to channel names based on keywords.
fn color_code_channel(channel: &str) -> ColoredString {
    let channel_lower = channel.to_lowercase();
    if channel_lower.contains("feature") {
        channel.bold().blue()
    } else if channel_lower.contains("bugfix") || channel_lower.contains("hotfix") {
        channel.bold().yellow()
    } else if channel.len() <= 10 {
        channel.cyan()
    } else {
        channel.red()
    }
}

/// Displays the results in a formatted table.
fn display_results_table(results: &[ChannelInfo]) {
    let mut table = Table::new();
    table.add_row(row![
        "Channel",
        "Roblox.exe",
        "DeployHistory.txt",
        "Mac DeployHistory.txt",
        "ClientSettings"
    ]);

    for info in results {
        table.add_row(row![
            info.colored_channel,
            if info.roblox_exe { "✅".green() } else { "❌".red() },
            if info.deploy_history { "✅".green() } else { "❌".red() },
            if info.mac_deploy_history { "✅".green() } else { "❌".red() },
            if info.client_settings { "✅".green() } else { "❌".red() },
        ]);
    }
    table.printstd();
}

/// Saves results to a JSON file.
fn save_results_to_file(results: &[ChannelInfo], path: &str) -> Result<(), AppError> {
    // Serialize results excluding colored_channel
    let serializable_results: Vec<ChannelInfoSerializable> = results
        .iter()
        .map(|info| ChannelInfoSerializable {
            channel: info.original_channel.clone(),
            roblox_exe: info.roblox_exe,
            deploy_history: info.deploy_history,
            mac_deploy_history: info.mac_deploy_history,
            client_settings: info.client_settings,
        })
        .collect();

    let json = serde_json::to_string_pretty(&serializable_results)?;
    fs::write(path, json)?;
    Ok(())
}

/// Generates and displays a comprehensive report.
fn generate_and_display_report(
    results: &[ChannelInfo],
    total_checked: usize,
    processing_time: Duration,
) -> Result<(), AppError> {
    let valid = results.len();
    let invalid = total_checked - valid;

    // Breakdown by endpoint
    let mut breakdown: HashMap<&str, usize> = HashMap::new();
    for info in results {
        if info.roblox_exe {
            *breakdown.entry("Roblox.exe").or_insert(0) += 1;
        }
        if info.deploy_history {
            *breakdown.entry("DeployHistory.txt").or_insert(0) += 1;
        }
        if info.mac_deploy_history {
            *breakdown.entry("Mac DeployHistory.txt").or_insert(0) += 1;
        }
        if info.client_settings {
            *breakdown.entry("ClientSettings").or_insert(0) += 1;
        }
    }

    // Display report
    println!("\n{}", "=== Comprehensive Report ===".bold().underline());
    println!("Total Channels Checked : {}", total_checked);
    println!("Valid Channels Found  : {}", valid);
    println!("Invalid Channels Found: {}", invalid);
    println!("Processing Time       : {:.2?}", processing_time);
    println!("Breakdown by Endpoint :");

    for (endpoint, count) in breakdown.iter() {
        println!(" - {}: {}", endpoint.bold(), count);
    }

    Ok(())
}

error: regex syntax error: look-around, including look-ahead and look-behind, is not supported
   --> src/main.rs:664:41
    |
664 |     let words: Vec<&str> = Regex::new(r"(?=[A-Z])").unwrap().split(channel).collect();
    |                                         ^^^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#invalid_regex
    = note: `#[deny(clippy::invalid_regex)]` on by default

error: regex syntax error: look-around, including look-ahead and look-behind, is not supported
   --> src/main.rs:789:41
    |
789 |     let words: Vec<&str> = Regex::new(r"(?=[A-Z])").unwrap().split(core).collect();
    |                                         ^^^

Here's a stackoverflow of it:

13

I tried to implement this regular expression for checking if a string ("username") has a length between 3 and 30, contains only letters (a-z), numbers (0-9), and periods (.) (not consecutive):

use regex::Regex; // 1.3.5

fn main() {
    Regex::new(r"^(?=.{3,30}$)(?!\.)(?!.*\.$)(?!.*?\.\.)[a-z0-9.]+$").unwrap();
}
When trying to compile the regex, I get this error:

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
regex parse error:
   r"^(?=.{3,30}$)(?!\.)(?!.*\.$)(?!.*?\.\.)[a-z0-9.]+$").unwrap();
     ^^^
error: look-around, including look-ahead and look-behind, is not supported
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Is there an alternative regex or ways to validate strings with these requirements?

I could remove the length {3,30} and get string length as suggested, but for the second part (?!\.)(?!.*\.$)(?!.*?\.\.)[a-z0-9.]+$ (prevent consecutive dots)?

regexrust
Share
Improve this question
Follow
edited Apr 28, 2020 at 19:00
asked Apr 28, 2020 at 16:23
nbari's user avatar
nbari
26.6k1111 gold badges8282 silver badges142142 bronze badges
3
has a length between 3 and 30 — Get the String length in characters in Rust – 
Shepmaster
 CommentedApr 28, 2020 at 17:21 
Also, if you really want to reject the multiple dots, and use the length with just regular expressions, you could use two of them, the first ^[a-z0-9\.]{3,30}$ and a second regex \.\. and check that the first matches, and that the second doesn't match. But I think the solution below in my answer, along with a length check would be slightly faster. But, this might be a closer match to your model of the problem. – 
David Brown
 CommentedApr 28, 2020 at 19:53
Add a comment
1 Answer
Sorted by:

Highest score (default)
13

The issue at hand is what is meant by "regular expression". Wikipedia has good information on this, but a simple summary is that a regular language is one defined with a few simple operations, including literal matches, alternation, and the Kleene star (match zero or more). Regex libraries have added features that don't extend this language, but make it easier to use (such as being able to say [a-z] instead of (a|b|c|d|e|f...|z)).

Then, along came Perl, which implemented support for regular expressions. However, instead of using the commonly used NFA/DFA implementation for regular expressions, it implemented them using backtracking. There are two consequences of this, one, it allowed things beyond regular languages to be added, such as backtracking, and two, it can be really, really slow.

Many languages used these backtracking implementations of regular expressions, but there has been a somewhat recent resurgence of removing the features from the expressions that make them difficult to implement efficiently, specifically backtracking. Go has done this, the Re2 library is a C/C++ implementation of this. And, as you've discovered the regex crate also works this way. The advantage is that it always matches in linear time.

For your particular example, what you are trying to match is indeed still a regular language, it just has to be expressed differently. Let's start with the easy part, matching the characters, but not allowing consecutive dots. Instead of thinking of it this way, think of it as matching possibly a dot between the characters, but the characters themselves aren't options. In other words, we can match with: [a-z0-9](\.?[a-z0-9])*. We first match a single character. If you want to allow this to start with a dot, you could remove this part. Then we need zero or more occurrences of an optional dot followed by a single non-dot character. You could append a \.? if you want to allow a dot at the end.

The second requirement, of 3-30 characters would make this regex rather complicated, because our repeated sequence is of 1 or 2 characters. I would suggest, instead, just checking the length programmatically in addition to checking the regex. You could also make a second regex that checks the length, and check that both match (Regular languages do not have an and operation).

You may also find, depending on how your are matching, you may have to anchor the match (putting a ^ at the start and a $ at the end).

A solution to the full problem:

use regex::Regex; // 1.3.5

fn main() {
    let pat = Regex::new(r"^[a-z0-9](\.?[a-z0-9])*$").unwrap();
    let names = &[
        "valid123",
        "va.li.d.12.3",
        ".invalid",
        "invalid.",
        "double..dot",
        "ss",
        "really.long.name.that.is.too.long",
    ];
    for name in names {
        let len = name.len();
        let valid = pat.is_match(name) && len >= 3 && len <= 30;
        println!("{:?}: {:?}", name, valid);
    }
}