mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 16:30:52 +08:00
fix(enhance): prevent crash when deleted proxies remain in manual groups (#5522)
* feat(enhance): cleanup stale proxies and policies before final config * feat(enhance): preserve provider-backed groups when sanitizing proxies * docs: Changelog.md * refactor: to_owned --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
- PAC 自动代理脚本内容无法动态调整
|
||||
- 兼容从旧版服务模式升级
|
||||
- Monaco 编辑器的行数上限
|
||||
- 已删除节点在手动分组中导致配置无法加载
|
||||
|
||||
<details>
|
||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||
|
||||
@@ -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::<HashSet<String>>()
|
||||
})
|
||||
.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::<HashSet<String>>()
|
||||
})
|
||||
.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::<HashSet<String>>()
|
||||
})
|
||||
.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<String>, HashMap<String, ResultLog>) {
|
||||
// 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<String>, HashMap<String, ResultLog>) {
|
||||
|
||||
(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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user