libsubconverter/settings/external/
ini_external.rs

1use serde::Deserialize;
2use std::collections::HashMap;
3
4use super::super::ini_bindings::{FromIni, FromIniWithDelimiter};
5use crate::models::ruleset::RulesetConfigs;
6use crate::models::{ProxyGroupConfigs, RegexMatchConfig, RegexMatchConfigs, RulesetConfig};
7use crate::settings::{import_items, Settings};
8use crate::utils::http::parse_proxy;
9
10/// INI external settings structure
11#[derive(Debug, Clone, Deserialize, Default)]
12#[serde(default)]
13pub struct IniExternalSettings {
14    // Rule bases
15    pub clash_rule_base: String,
16    pub surge_rule_base: String,
17    pub surfboard_rule_base: String,
18    pub mellow_rule_base: String,
19    pub quan_rule_base: String,
20    pub quanx_rule_base: String,
21    pub loon_rule_base: String,
22    pub sssub_rule_base: String,
23    pub singbox_rule_base: String,
24
25    // Rule generation options
26    pub enable_rule_generator: bool,
27    pub overwrite_original_rules: bool,
28
29    // Emoji options
30    pub add_emoji: Option<bool>,
31    pub remove_old_emoji: Option<bool>,
32    pub emojis: Vec<String>,
33
34    // Filtering options
35    pub include_remarks: Vec<String>,
36    pub exclude_remarks: Vec<String>,
37
38    // Rulesets and proxy groups (stored as raw strings)
39    pub rulesets: Vec<String>,
40    pub custom_proxy_groups: Vec<String>,
41
42    // fields
43    pub rename_nodes: Vec<String>,
44    // Rename rules
45
46    // Template arguments
47    pub tpl_args: Option<HashMap<String, String>>,
48
49    // processed fields
50    #[serde(skip)]
51    pub parsed_custom_proxy_groups: ProxyGroupConfigs,
52    #[serde(skip)]
53    pub parsed_rulesets: Vec<RulesetConfig>,
54    #[serde(skip)]
55    pub parsed_rename: Vec<RegexMatchConfig>,
56    #[serde(skip)]
57    pub parsed_emojis: Vec<RegexMatchConfig>,
58}
59
60impl IniExternalSettings {
61    pub fn new() -> Self {
62        Self::default()
63    }
64
65    /// Load settings from INI format
66    pub fn load_from_ini(&mut self, content: &str) -> Result<(), Box<dyn std::error::Error>> {
67        let mut current_section = String::new();
68
69        for line in content.lines() {
70            let trimmed = line.trim();
71
72            // Skip empty lines and comments
73            if trimmed.is_empty() || trimmed.starts_with(';') || trimmed.starts_with('#') {
74                continue;
75            }
76
77            // Check for section header
78            if trimmed.starts_with('[') && trimmed.ends_with(']') {
79                current_section = trimmed[1..trimmed.len() - 1].to_string();
80                continue;
81            }
82
83            // Process key-value pairs
84            if let Some(pos) = trimmed.find('=') {
85                let key = trimmed[..pos].trim();
86                let value = trimmed[pos + 1..].trim();
87
88                match current_section.as_str() {
89                    "custom" => self.process_custom_section(key, value),
90                    "template" => self.process_template_section(key, value),
91                    _ => {} // Ignore unknown sections
92                }
93            }
94        }
95        Ok(())
96    }
97
98    fn process_custom_section(&mut self, key: &str, value: &str) {
99        match key {
100            "clash_rule_base" => self.clash_rule_base = value.to_string(),
101            "surge_rule_base" => self.surge_rule_base = value.to_string(),
102            "surfboard_rule_base" => self.surfboard_rule_base = value.to_string(),
103            "mellow_rule_base" => self.mellow_rule_base = value.to_string(),
104            "quan_rule_base" => self.quan_rule_base = value.to_string(),
105            "quanx_rule_base" => self.quanx_rule_base = value.to_string(),
106            "loon_rule_base" => self.loon_rule_base = value.to_string(),
107            "sssub_rule_base" => self.sssub_rule_base = value.to_string(),
108            "singbox_rule_base" => self.singbox_rule_base = value.to_string(),
109            "enable_rule_generator" => {
110                self.enable_rule_generator = parse_bool_with_true_default(value)
111            }
112            "overwrite_original_rules" => self.overwrite_original_rules = parse_bool(value),
113            "add_emoji" => self.add_emoji = Some(parse_bool(value)),
114            "remove_old_emoji" => self.remove_old_emoji = Some(parse_bool(value)),
115            "include_remarks" => {
116                self.include_remarks = value.split(',').map(|s| s.trim().to_string()).collect();
117            }
118            "exclude_remarks" => {
119                self.exclude_remarks = value.split(',').map(|s| s.trim().to_string()).collect();
120            }
121            "ruleset" | "surge_ruleset" => {
122                self.rulesets.push(value.to_string());
123            }
124            "custom_proxy_group" => {
125                self.custom_proxy_groups.push(value.to_string());
126            }
127            "emoji" => {
128                self.emojis.push(value.to_string());
129            }
130            "rename" => {
131                self.rename_nodes.push(value.to_string());
132            }
133            _ => {}
134        }
135    }
136
137    fn process_template_section(&mut self, key: &str, value: &str) {
138        // Initialize tpl_args if it's None
139        if self.tpl_args.is_none() {
140            self.tpl_args = Some(HashMap::new());
141        }
142
143        // Add the key-value pair to the template arguments
144        if let Some(ref mut args) = self.tpl_args {
145            args.insert(key.to_string(), value.to_string());
146        }
147    }
148
149    pub async fn process_imports(&mut self) -> Result<(), Box<dyn std::error::Error>> {
150        let global = Settings::current();
151        let proxy_config = parse_proxy(&global.proxy_config);
152        // Process rename nodes
153        import_items(
154            &mut self.rename_nodes,
155            false,
156            &proxy_config,
157            &global.base_path,
158        )
159        .await?;
160        self.parsed_rename = RegexMatchConfigs::from_ini_with_delimiter(&self.rename_nodes, "@");
161
162        // Process emoji rules
163        import_items(&mut self.emojis, false, &proxy_config, &global.base_path).await?;
164        self.parsed_emojis = RegexMatchConfigs::from_ini_with_delimiter(&self.emojis, ",");
165
166        // Process imports for rulesets
167        import_items(
168            &mut self.rulesets,
169            global.api_mode,
170            &proxy_config,
171            &global.base_path,
172        )
173        .await?;
174        self.parsed_rulesets = RulesetConfigs::from_ini(&self.rulesets);
175        // Process imports for proxy groups
176        let mut custom_proxy_groups = self.custom_proxy_groups.clone();
177        import_items(
178            &mut custom_proxy_groups,
179            global.api_mode,
180            &proxy_config,
181            &global.base_path,
182        )
183        .await?;
184        self.parsed_custom_proxy_groups = ProxyGroupConfigs::from_ini(&custom_proxy_groups);
185
186        Ok(())
187    }
188}
189
190/// Parse a string as boolean
191fn parse_bool(value: &str) -> bool {
192    value.to_lowercase() == "true" || value == "1"
193}
194
195fn parse_bool_with_true_default(value: &str) -> bool {
196    if value.is_empty() {
197        true
198    } else {
199        parse_bool(value)
200    }
201}