fix(enhance): replace DNS fields in merge overrides

Replace DNS map fields and hosts instead of retaining old entries, while preserving unspecified DNS fields. Keep DNS override priority and explicit empty values intact.

Refs #7993
This commit is contained in:
wonfen
2026-09-22 10:17:19 +08:00
parent bffe91db5e
commit 6a85d0344e
3 changed files with 82 additions and 20 deletions
+45 -17
View File
@@ -1,5 +1,3 @@
use clash_verge_logging::{Type, logging};
use super::field::use_lowercase;
use serde_yaml_ng::{self, Mapping, Value};
@@ -18,20 +16,19 @@ fn deep_merge(a: &mut Value, b: Value) {
}
}
pub fn use_merge(merge: &Mapping, config: Mapping) -> Mapping {
let mut config = Value::from(config);
let merge = use_lowercase(merge);
deep_merge(&mut config, Value::from(merge));
config.as_mapping().cloned().unwrap_or_else(|| {
logging!(
error,
Type::Core,
"Failed to convert merged config to mapping, using empty mapping"
);
Mapping::new()
})
pub fn use_merge(merge: &Mapping, mut config: Mapping) -> Mapping {
for (key, value) in use_lowercase(merge) {
match (config.get_mut(&key), value) {
(Some(Value::Mapping(existing)), Value::Mapping(overlay)) if key.as_str() == Some("dns") => {
existing.extend(overlay);
}
(Some(existing), value) if key.as_str() != Some("hosts") => deep_merge(existing, value),
(_, value) => {
config.insert(key, value);
}
}
}
config
}
#[test]
@@ -54,18 +51,49 @@ fn test_merge() -> anyhow::Result<()> {
enable: true
dns:
enable: true
nameserver-policy:
new.example: 1.1.1.1
fallback-filter:
geoip: false
hosts:
new.example: 192.0.2.2
";
let config = r"
rules:
- aaaaa
script1: test
tun:
mtu: 1500
dns:
nameserver: [9.9.9.9]
nameserver-policy:
old.example: 8.8.8.8
fallback-filter:
geoip: true
domain: [old.example]
hosts:
old.example: 192.0.2.1
";
let merge = serde_yaml_ng::from_str::<Mapping>(merge)?;
let config = serde_yaml_ng::from_str::<Mapping>(config)?;
let _ = serde_yaml_ng::to_string(&use_merge(&merge, config))?;
let result = use_merge(&merge, config);
assert_eq!(result["dns"]["nameserver-policy"], merge["dns"]["nameserver-policy"]);
assert_eq!(result["dns"]["fallback-filter"], merge["dns"]["fallback-filter"]);
assert_eq!(result["hosts"], merge["hosts"]);
assert_eq!(result["dns"]["nameserver"], serde_yaml_ng::to_value(["9.9.9.9"])?);
assert_eq!(result["tun"]["mtu"], Value::from(1500));
assert_eq!(result["rules"], merge["rules"]);
assert_eq!(result["script1"], Value::from("test"));
let cleared = serde_yaml_ng::from_str::<Mapping>(
"dns: {enable: false, nameserver-policy: {}, fallback-filter: {}, nameserver: []}\nhosts: {}",
)?;
let result = use_merge(&cleared, result);
assert_eq!(result["dns"], cleared["dns"]);
assert_eq!(result["hosts"], cleared["hosts"]);
Ok(())
}
+32
View File
@@ -1433,6 +1433,38 @@ mod tests {
serde_yaml_ng::from_str(yaml).expect("test config should be valid")
}
#[test]
fn merge_replaces_dns_fields_with_or_without_dns_settings() {
let profile = mapping(
"dns: {nameserver: [9.9.9.9], nameserver-policy: {profile.example: 9.9.9.9}}\n\
hosts: {profile.example: 192.0.2.1}",
);
let settings = mapping(
"dns: {ipv6: true, nameserver: [8.8.8.8], nameserver-policy: {settings.example: 8.8.8.8}}\n\
hosts: {settings.example: 192.0.2.2}",
);
let merge = mapping(
"dns: {ipv6: false, nameserver-policy: {merge.example: 1.1.1.1}}\n\
hosts: {merge.example: 192.0.2.3}",
);
for enabled in [false, true] {
let (config, owns_ipv6) = if enabled {
super::merge_dns_config(profile.clone(), settings.clone())
} else {
(profile.clone(), false)
};
let authoritative = AuthoritativeFields::capture(&config, &[], owns_ipv6);
let result = authoritative.enforce(super::use_merge(&merge, config));
assert_eq!(result["dns"]["nameserver-policy"], merge["dns"]["nameserver-policy"]);
assert_eq!(result["hosts"], merge["hosts"]);
let source = if enabled { &settings } else { &profile };
assert_eq!(result["dns"]["nameserver"], source["dns"]["nameserver"]);
assert_eq!(result["dns"]["ipv6"], serde_yaml_ng::Value::from(enabled));
}
}
#[tokio::test]
async fn manual_overrides_follow_expected_priority() {
let mut config = mapping(