infrarust 1.0.1

A Rust adaptation of the Infrared Minecraft proxy
Documentation
use std::io::{self, Read};
use std::time::Duration;
use std::{fs, path::Path};

use log::debug;
use serde::{de::DeserializeOwned, Deserialize};
use walkdir::WalkDir;

use crate::network::proxy_protocol::errors::ProxyProtocolError;
use crate::network::proxy_protocol::ProtocolResult;
use crate::proxy_modes::ProxyModeEnum;

#[derive(Debug, Clone, Copy)]
pub enum FileType {
    Yaml,
}

pub struct FileProvider {
    pub config_path: String,
    pub proxies_path: String,
    pub file_type: FileType,
}

impl FileProvider {
    pub fn new(config_path: String, proxies_path: String, file_type: FileType) -> Self {
        Self {
            config_path,
            proxies_path,
            file_type,
        }
    }

    pub fn load_config(&self) -> ProtocolResult<InfrarustConfig> {
        let decoder = match self.file_type {
            FileType::Yaml => yaml_decoder,
        };

        let config_path = fs::canonicalize(&self.config_path)?;
        let mut file = fs::File::open(&config_path)?;
        let mut contents = String::new();
        file.read_to_string(&mut contents)?;

        let mut config: InfrarustConfig = decoder(&contents)?;
        debug!("Loaded config: {:?}", config);

        config.server_configs = self.load_server_configs()?;
        Ok(config)
    }

    fn load_server_configs(&self) -> ProtocolResult<Vec<ServerConfig>> {
        let proxies_path = fs::canonicalize(&self.proxies_path)?;
        let mut configs = Vec::new();

        for entry in WalkDir::new(&proxies_path)
            .follow_links(true)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.file_type().is_file())
        {
            let path = entry.path();
            let config = self.load_server_config(path)?;
            debug!("Loaded server config: {:?}", config);
            if !config.is_empty() {
                configs.push(config);
            }
        }

        Ok(configs)
    }

    fn load_server_config<P: AsRef<Path>>(&self, path: P) -> ProtocolResult<ServerConfig> {
        let decoder = match self.file_type {
            FileType::Yaml => yaml_decoder,
        };

        let mut file = fs::File::open(path)?;
        let mut contents = String::new();

        file.read_to_string(&mut contents)?;

        decoder(&contents)
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct ServerConfig {
    pub domains: Vec<String>,
    pub addresses: Vec<String>,
    #[serde(rename = "sendProxyProtocol")]
    pub send_proxy_protocol: Option<bool>,
    #[serde(rename = "proxyMode")]
    pub proxy_mode: Option<ProxyModeEnum>,
}

impl Default for ServerConfig {
    fn default() -> Self {
        ServerConfig {
            domains: Vec::new(),
            addresses: Vec::new(),
            send_proxy_protocol: Some(false),
            proxy_mode: Some(ProxyModeEnum::default()),
        }
    }
}

#[derive(Debug, Deserialize, Clone, Default)]
pub struct InfrarustConfig {
    pub bind: Option<String>,
    pub domains: Option<Vec<String>>,
    pub addresses: Option<Vec<String>>,
    pub keepalive_timeout: Option<Duration>,

    #[serde(skip)]
    pub server_configs: Vec<ServerConfig>,
}

impl ServerConfig {
    pub fn is_empty(&self) -> bool {
        self.domains.is_empty() && self.addresses.is_empty()
    }
}

impl InfrarustConfig {
    pub fn is_empty(&self) -> bool {
        self.bind.is_none() && self.domains.is_none() && self.addresses.is_none()
    }
}

fn yaml_decoder<T: DeserializeOwned>(content: &str) -> ProtocolResult<T> {
    debug!("Decoding YAML content: {}", content);
    serde_yaml::from_str(content)
        .map_err(|e| ProxyProtocolError::Io(io::Error::new(io::ErrorKind::InvalidData, e)))
}

#[cfg(test)]
mod tests {
    use std::fs;

    use super::*;
    use tempfile::TempDir;

    #[test]
    fn test_file_provider() {
        let temp_dir = TempDir::new().unwrap();
        let config_path = temp_dir.path().join("config.yml");
        let proxies_path = temp_dir.path().join("proxies");

        fs::create_dir(&proxies_path).unwrap();

        fs::write(&config_path, "bind: ':25565'\n").unwrap();
        fs::write(
            proxies_path.join("server1.yml"),
            "domains: ['example.com']\naddresses: ['127.0.0.1:25566']\n",
        )
        .unwrap();

        let provider = FileProvider::new(
            config_path.to_str().unwrap().to_string(),
            proxies_path.to_str().unwrap().to_string(),
            FileType::Yaml,
        );

        let config = provider.load_config().unwrap();
        assert!(!config.server_configs.is_empty());
    }
}