convertor 2.6.12

A profile converter for surge/clash.
Documentation
use crate::config::client_config::ProxyClient;
use crate::core::profile::policy::Policy;
use crate::core::profile::proxy::Proxy;
use crate::core::profile::proxy_group::ProxyGroup;
use crate::core::profile::rule::{ProviderRule, Rule};
use crate::core::profile::rule_provider::RuleProvider;
use crate::core::profile::surge_profile::SurgeProfile;
use crate::core::renderer::Renderer;
use crate::error::RenderError;
use std::fmt::Write;
use tracing::instrument;

type Result<T> = core::result::Result<T, RenderError>;

pub const SURGE_RULE_PROVIDER_COMMENT_START: &str = "# Rule Provider from convertor";
pub const SURGE_RULE_PROVIDER_COMMENT_END: &str = "# End of Rule Provider";

pub struct SurgeRenderer;

impl Renderer for SurgeRenderer {
    type PROFILE = SurgeProfile;

    fn client() -> ProxyClient {
        ProxyClient::Surge
    }

    fn render_profile(profile: &Self::PROFILE) -> Result<String> {
        let mut output = String::new();

        let header = Self::render_header(profile)?;
        writeln!(output, "{}", header.trim())?;
        writeln!(output)?;

        let general = Self::render_general(profile)?;
        writeln!(output, "[General]")?;
        writeln!(output, "{}", general.trim())?;
        writeln!(output)?;

        let proxies = Self::render_proxies(&profile.proxies)?;
        writeln!(output, "[Proxy]")?;
        writeln!(output, "{}", proxies.trim())?;
        writeln!(output)?;

        let proxy_groups = Self::render_proxy_groups(&profile.proxy_groups)?;
        writeln!(output, "[Proxy Group]")?;
        writeln!(output, "{}", proxy_groups.trim())?;
        writeln!(output)?;

        let rules = Self::render_rules(&profile.rules)?;
        writeln!(output, "[Rule]")?;
        writeln!(output, "{}", rules.trim())?;
        writeln!(output)?;

        let url_rewrite = Self::render_url_rewrite(&profile.url_rewrite)?;
        writeln!(output, "[URL Rewrite]")?;
        writeln!(output, "{}", url_rewrite.trim())?;
        writeln!(output)?;

        let misc = Self::render_misc(&profile.misc)?;
        if !misc.trim().is_empty() {
            writeln!(output, "{}", misc.trim())?;
        }

        Ok(output)
    }

    fn render_general(profile: &Self::PROFILE) -> Result<String> {
        Self::render_lines(&profile.general, |line| Ok(line.clone()))
    }

    fn render_proxy(proxy: &Proxy) -> Result<String> {
        let mut output = String::new();
        if let Some(comment) = &proxy.comment {
            writeln!(output, "{comment}")?;
        }
        write!(
            output,
            "{}={},{},{},password={}",
            proxy.name, proxy.r#type, proxy.server, proxy.port, proxy.password
        )?;
        if let Some(cipher) = &proxy.cipher {
            write!(output, ",encrypt-method={cipher}")?;
        }
        if let Some(udp) = proxy.udp {
            write!(output, ",udp-relay={udp}")?;
        }
        if let Some(tfo) = proxy.tfo {
            write!(output, ",tfo={tfo}")?;
        }
        if let Some(sni) = &proxy.sni {
            write!(output, ",sni={sni}")?;
        }
        if let Some(skip_cert_verify) = proxy.skip_cert_verify {
            write!(output, ",skip-cert-verify={skip_cert_verify}")?;
        }
        Ok(output)
    }

    fn render_proxy_group(proxy_group: &ProxyGroup) -> Result<String> {
        let mut output = String::new();
        if let Some(comment) = &proxy_group.comment {
            writeln!(output, "{comment}")?;
        }
        write!(output, "{}={}", proxy_group.name, proxy_group.r#type.as_str())?;
        if !proxy_group.proxies.is_empty() {
            write!(output, ",{}", proxy_group.proxies.join(","))?;
        }
        Ok(output)
    }

    fn render_rule(rule: &Rule) -> Result<String> {
        let mut output = String::new();
        if let Some(comment) = &rule.comment {
            writeln!(output, "{comment}")?;
        }
        write!(output, "{}", rule.rule_type.as_str())?;
        if let Some(value) = &rule.value {
            write!(output, ",{value}")?;
        }
        write!(output, ",{}", Self::render_policy(&rule.policy)?)?;
        Ok(output)
    }

    fn render_rule_for_provider(rule: &Rule) -> Result<String> {
        Ok(format!(
            "{},{},{}",
            rule.rule_type.as_str(),
            rule.value.as_ref().expect("规则集中的规则必须有 value"),
            Self::render_policy(&rule.policy)?,
        ))
    }

    fn render_provider_rule(rule: &ProviderRule) -> Result<String> {
        let mut output = String::new();
        if let Some(comment) = &rule.comment {
            writeln!(output, "{comment}")?;
        }
        write!(output, "{},{}", rule.rule_type.as_str(), rule.value)?;
        Ok(output)
    }

    fn render_rule_providers(_: &[(String, RuleProvider)]) -> Result<String> {
        todo!("SurgeRenderer 不会渲染 [RuleProvider]");
    }

    fn render_rule_provider(_: &(String, RuleProvider)) -> Result<String> {
        todo!("SurgeRenderer 不会渲染 RuleProvider");
    }

    fn render_provider_name_for_policy(policy: &Policy) -> String {
        let mut output = if policy.is_subscription {
            "[Subscription".to_string()
        } else {
            format!("[{}", policy.name)
        };
        if let Some(option) = policy.option.as_ref() {
            output += format!(": {option}").as_str();
        }
        output.push(']');
        output
    }
}

impl SurgeRenderer {
    #[instrument(skip_all)]
    pub fn render_header(profile: &SurgeProfile) -> Result<String> {
        Ok(profile.header.to_string())
    }

    #[instrument(skip_all)]
    pub fn render_url_rewrite(url_rewrite: &[String]) -> Result<String> {
        Self::render_lines(url_rewrite, |line| Ok(line.clone()))
    }

    #[instrument(skip_all)]
    pub fn render_misc(misc: &[(String, Vec<String>)]) -> Result<String> {
        let mut output = String::new();
        for (key, values) in misc {
            writeln!(output, "{key}")?;
            let lines = Self::render_lines(values, |value| Ok(value.clone()))?;
            writeln!(output, "{lines}")?;
        }
        Ok(output)
    }
}