diff --git a/Changelog.md b/Changelog.md index 27d449b4f..7d7cdcb2b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ - PAC 自动代理脚本内容无法动态调整 - 兼容从旧版服务模式升级 - Monaco 编辑器的行数上限 +- 已删除节点在手动分组中导致配置无法加载
✨ 新增功能 diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index df63f3220..24f0764a1 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -17,7 +17,7 @@ use crate::constants; use crate::utils::dirs; use crate::{config::Config, utils::tmpl}; use clash_verge_logging::{Type, logging}; -use serde_yaml_ng::Mapping; +use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; use std::collections::{HashMap, HashSet}; use tokio::fs; @@ -501,6 +501,88 @@ fn apply_builtin_scripts( config } +fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { + const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; + + let proxy_names = config + .get("proxies") + .and_then(|v| v.as_sequence()) + .map(|seq| { + seq.iter() + .filter_map(|item| match item { + Value::Mapping(map) => map + .get("name") + .and_then(Value::as_str) + .map(|name| name.to_owned().into()), + Value::String(name) => Some(name.to_owned().into()), + _ => None, + }) + .collect::>() + }) + .unwrap_or_default(); + + let group_names = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .map(|seq| { + seq.iter() + .filter_map(|item| { + item.as_mapping() + .and_then(|map| map.get("name")) + .and_then(Value::as_str) + .map(std::convert::Into::into) + }) + .collect::>() + }) + .unwrap_or_default(); + + let provider_names = config + .get("proxy-providers") + .and_then(Value::as_mapping) + .map(|map| { + map.keys() + .filter_map(Value::as_str) + .map(std::convert::Into::into) + .collect::>() + }) + .unwrap_or_default(); + + let mut allowed_names = proxy_names; + allowed_names.extend(group_names); + allowed_names.extend(provider_names.iter().cloned()); + allowed_names.extend(BUILTIN_POLICIES.iter().map(|p| (*p).into())); + + if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") { + for group in groups { + if let Some(group_map) = group.as_mapping_mut() { + let mut has_valid_provider = false; + + if let Some(Value::Sequence(uses)) = group_map.get_mut("use") { + uses.retain(|provider| match provider { + Value::String(name) => { + let exists = provider_names.contains(name.as_str()); + has_valid_provider = has_valid_provider || exists; + exists + } + _ => false, + }); + } + + if let Some(Value::Sequence(proxies)) = group_map.get_mut("proxies") { + proxies.retain(|proxy| match proxy { + Value::String(name) => { + allowed_names.contains(name.as_str()) || has_valid_provider + } + _ => true, + }); + } + } + } + } + + config +} + async fn apply_dns_settings(mut config: Mapping, enable_dns_settings: bool) -> Mapping { if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() { let dns_path = app_dir.join(constants::files::DNS_CONFIG); @@ -595,6 +677,8 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { // builtin scripts let mut config = apply_builtin_scripts(config, clash_core, enable_builtin); + config = cleanup_proxy_groups(config); + config = use_tun(config, enable_tun); config = use_sort(config); @@ -607,3 +691,175 @@ pub async fn enhance() -> (Mapping, Vec, HashMap) { (config, exists_keys, result_map) } + +#[cfg(test)] +mod tests { + use super::cleanup_proxy_groups; + + #[test] + fn remove_missing_proxies_from_groups() { + let config_str = r#" +proxies: + - name: "alive-node" + type: ss +proxy-groups: + - name: "manual" + type: select + proxies: + - "alive-node" + - "missing-node" + - "DIRECT" + - name: "nested" + type: select + proxies: + - "manual" + - "ghost" +"#; + + let mut config: serde_yaml_ng::Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); + config = cleanup_proxy_groups(config); + + let groups = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .cloned() + .expect("proxy-groups should be a sequence"); + + let manual_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") + }) + .and_then(|group| group.as_mapping()) + .expect("manual group should exist"); + + let manual_proxies = manual_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("manual proxies should be a sequence"); + + assert_eq!(manual_proxies.len(), 2); + assert!( + manual_proxies + .iter() + .any(|p| p.as_str() == Some("alive-node")) + ); + assert!(manual_proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); + + let nested_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("nested") + }) + .and_then(|group| group.as_mapping()) + .expect("nested group should exist"); + + let nested_proxies = nested_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("nested proxies should be a sequence"); + + assert_eq!(nested_proxies.len(), 1); + assert_eq!(nested_proxies[0].as_str(), Some("manual")); + } + + #[test] + fn keep_provider_backed_groups_intact() { + let config_str = r#" +proxy-providers: + providerA: + type: http + url: https://example.com + path: ./providerA.yaml +proxies: [] +proxy-groups: + - name: "manual" + type: select + use: + - "providerA" + - "ghostProvider" + proxies: + - "dynamic-node" + - "DIRECT" +"#; + + let mut config: serde_yaml_ng::Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); + config = cleanup_proxy_groups(config); + + let groups = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .cloned() + .expect("proxy-groups should be a sequence"); + + let manual_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") + }) + .and_then(|group| group.as_mapping()) + .expect("manual group should exist"); + + let uses = manual_group + .get("use") + .and_then(|v| v.as_sequence()) + .expect("use should be a sequence"); + assert_eq!(uses.len(), 1); + assert_eq!(uses[0].as_str(), Some("providerA")); + + let proxies = manual_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("proxies should be a sequence"); + assert_eq!(proxies.len(), 2); + assert!(proxies.iter().any(|p| p.as_str() == Some("dynamic-node"))); + assert!(proxies.iter().any(|p| p.as_str() == Some("DIRECT"))); + } + + #[test] + fn prune_invalid_provider_and_proxies_without_provider() { + let config_str = r#" +proxy-groups: + - name: "manual" + type: select + use: + - "ghost-provider" + proxies: + - "ghost-node" + - "DIRECT" +"#; + + let mut config: serde_yaml_ng::Mapping = + serde_yaml_ng::from_str(config_str).expect("Failed to parse test yaml"); + config = cleanup_proxy_groups(config); + + let groups = config + .get("proxy-groups") + .and_then(|v| v.as_sequence()) + .cloned() + .expect("proxy-groups should be a sequence"); + + let manual_group = groups + .iter() + .find(|group| { + group.get("name").and_then(serde_yaml_ng::Value::as_str) == Some("manual") + }) + .and_then(|group| group.as_mapping()) + .expect("manual group should exist"); + + let uses = manual_group + .get("use") + .and_then(|v| v.as_sequence()) + .expect("use should be a sequence"); + assert_eq!(uses.len(), 0); + + let proxies = manual_group + .get("proxies") + .and_then(|v| v.as_sequence()) + .expect("proxies should be a sequence"); + assert_eq!(proxies.len(), 1); + assert_eq!(proxies[0].as_str(), Some("DIRECT")); + } +}