mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +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 自动代理脚本内容无法动态调整
|
- PAC 自动代理脚本内容无法动态调整
|
||||||
- 兼容从旧版服务模式升级
|
- 兼容从旧版服务模式升级
|
||||||
- Monaco 编辑器的行数上限
|
- Monaco 编辑器的行数上限
|
||||||
|
- 已删除节点在手动分组中导致配置无法加载
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong> ✨ 新增功能 </strong></summary>
|
<summary><strong> ✨ 新增功能 </strong></summary>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use crate::constants;
|
|||||||
use crate::utils::dirs;
|
use crate::utils::dirs;
|
||||||
use crate::{config::Config, utils::tmpl};
|
use crate::{config::Config, utils::tmpl};
|
||||||
use clash_verge_logging::{Type, logging};
|
use clash_verge_logging::{Type, logging};
|
||||||
use serde_yaml_ng::Mapping;
|
use serde_yaml_ng::{Mapping, Value};
|
||||||
use smartstring::alias::String;
|
use smartstring::alias::String;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
@@ -501,6 +501,88 @@ fn apply_builtin_scripts(
|
|||||||
config
|
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 {
|
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() {
|
if enable_dns_settings && let Ok(app_dir) = dirs::app_home_dir() {
|
||||||
let dns_path = app_dir.join(constants::files::DNS_CONFIG);
|
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
|
// builtin scripts
|
||||||
let mut config = apply_builtin_scripts(config, clash_core, enable_builtin);
|
let mut config = apply_builtin_scripts(config, clash_core, enable_builtin);
|
||||||
|
|
||||||
|
config = cleanup_proxy_groups(config);
|
||||||
|
|
||||||
config = use_tun(config, enable_tun);
|
config = use_tun(config, enable_tun);
|
||||||
config = use_sort(config);
|
config = use_sort(config);
|
||||||
|
|
||||||
@@ -607,3 +691,175 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||||||
|
|
||||||
(config, exists_keys, result_map)
|
(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