diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 9123af8cb..efebb1c4a 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,6 +1,6 @@ use super::CmdResult; use crate::feat; -use crate::utils::dirs; +use crate::utils::{dirs, yaml_emitter}; use crate::{ cmd::StringifyErr as _, config::{ClashInfo, Config}, @@ -136,14 +136,13 @@ pub async fn test_delay(url: String) -> CmdResult { #[tauri::command] pub async fn save_dns_config(dns_config: Mapping) -> CmdResult { use crate::utils::dirs; - use serde_yaml_ng; use tokio::fs; // 获取DNS配置文件路径 let dns_path = dirs::app_home_dir().stringify_err()?.join(constants::files::DNS_CONFIG); // 保存DNS配置到文件 - let yaml_str = serde_yaml_ng::to_string(&dns_config).stringify_err()?; + let yaml_str = yaml_emitter::to_mihomo_config_string(&dns_config).stringify_err()?; fs::write(&dns_path, yaml_str).await.stringify_err()?; logging!(info, Type::Config, "DNS config saved to {dns_path:?}"); diff --git a/src-tauri/src/cmd/runtime.rs b/src-tauri/src/cmd/runtime.rs index d048a776c..b9c88471d 100644 --- a/src-tauri/src/cmd/runtime.rs +++ b/src-tauri/src/cmd/runtime.rs @@ -1,5 +1,5 @@ use super::CmdResult; -use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager}; +use crate::{cmd::StringifyErr as _, config::Config, core::CoreManager, utils::yaml_emitter}; use anyhow::{Context as _, anyhow}; use clash_verge_logging::{Type, logging}; use serde_yaml_ng::Mapping; @@ -22,7 +22,7 @@ pub async fn get_runtime_yaml() -> CmdResult { config .ok_or_else(|| anyhow!("failed to parse config to yaml file")) .and_then(|config| { - serde_yaml_ng::to_string(config) + yaml_emitter::to_mihomo_config_string(config) .context("failed to convert config to yaml") .map(|s| s.into()) }) @@ -82,7 +82,7 @@ pub async fn get_runtime_proxy_chain_config(proxy_chain_exit_node: String) -> Cm config.insert("proxies".into(), proxies_chain); - serde_yaml_ng::to_string(&config) + yaml_emitter::to_mihomo_config_string(&config) .context("YAML generation failed") .map(|s| s.into()) .stringify_err() diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs index 37d87b271..7472b46c0 100644 --- a/src-tauri/src/core/backup.rs +++ b/src-tauri/src/core/backup.rs @@ -1,4 +1,5 @@ use crate::constants::files::DNS_CONFIG; +use crate::utils::yaml_emitter; use crate::{config::Config, process::AsyncHandler, utils::dirs}; use anyhow::Error; use arc_swap::{ArcSwap, ArcSwapOption}; @@ -287,7 +288,7 @@ pub async fn create_backup() -> Result<(String, PathBuf), Error> { obj.remove("webdav_url"); } zip.start_file(dirs::VERGE_CONFIG, options)?; - zip.write_all(serde_yaml_ng::to_string(&verge_config)?.as_bytes())?; + zip.write_all(yaml_emitter::to_mihomo_config_string(&verge_config)?.as_bytes())?; let dns_config_path = dirs::app_home_dir()?.join(DNS_CONFIG); if dns_config_path.exists() { diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 316602e3c..d67dc3083 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,4 +1,4 @@ -use crate::{config::with_encryption, enhance::seq::SeqMap}; +use crate::{config::with_encryption, enhance::seq::SeqMap, utils::yaml_emitter}; use anyhow::{Context as _, Result, anyhow, bail}; use clash_verge_logging::{Type, logging}; use nanoid::nanoid; @@ -59,7 +59,7 @@ pub async fn read_seq_map(path: &PathBuf) -> Result { /// save the data to the file /// can set `prefix` string to add some comments pub async fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { - let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?; + let data_str = with_encryption(|| async { yaml_emitter::to_mihomo_config_string(data) }).await?; let yaml_str = match prefix { Some(prefix) => format!("{prefix}\n\n{data_str}"), diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index b7f014028..b8b509339 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -17,3 +17,4 @@ pub mod tmpl; #[cfg(target_os = "macos")] pub mod tray_speed; pub mod window_manager; +pub mod yaml_emitter; diff --git a/src-tauri/src/utils/yaml_emitter.rs b/src-tauri/src/utils/yaml_emitter.rs new file mode 100644 index 000000000..cca6b8c4c --- /dev/null +++ b/src-tauri/src/utils/yaml_emitter.rs @@ -0,0 +1,104 @@ +use serde::{Serialize, ser}; +use serde_yaml_ng::Result; + +pub fn to_mihomo_config_string(data: &T) -> Result { + let value = serde_yaml_ng::to_value(data)?; + Ok(emit_value(&value, 0, &DEFAULT_OPTIONS)? + "\n") +} + +#[derive(Clone)] +struct Options { + checkout_wildcard: bool, +} + +const DEFAULT_OPTIONS: Options = Options { + checkout_wildcard: false, +}; + +fn serde_yaml_ng_to_string_without_last(value: &T) -> Result { + let value = serde_yaml_ng::to_string(value)?; + if value.ends_with('\n') { + Ok(value[..value.len() - 1].to_string()) + } else { + Ok(value) + } +} + +fn emit_value(value: &serde_yaml_ng::Value, depth: usize, options: &Options) -> Result { + match value { + serde_yaml_ng::Value::Null => serde_yaml_ng_to_string_without_last(&value), + serde_yaml_ng::Value::Bool(b) => serde_yaml_ng_to_string_without_last(&b), + serde_yaml_ng::Value::Number(number) => serde_yaml_ng_to_string_without_last(&number), + serde_yaml_ng::Value::String(s) => emit_string(s, options), + serde_yaml_ng::Value::Sequence(values) => { + if values.is_empty() { + return Ok("[]".to_string()); + } + let mut result = String::new(); + let mut first = true; + for value in values { + if !first || depth > 0 { + result.push('\n'); + } + first = false; + result.push_str(&" ".repeat(depth)); + result.push_str("- "); + result.push_str(&emit_value(value, depth + 1, options)?); + } + Ok(result) + } + serde_yaml_ng::Value::Mapping(mapping) => { + if mapping.is_empty() { + return Ok("{}".to_string()); + } + let mut result = String::new(); + let mut first = true; + for (key, value) in mapping { + if !first || depth > 0 { + result.push('\n'); + } + first = false; + result.push_str(&" ".repeat(depth)); + let new_key = emit_value(key, depth + 1, options)?; + result.push_str(&new_key); + result.push_str(": "); + let mut value_emiter_options = options.clone(); + if need_checkout_wildcard(key) { + value_emiter_options.checkout_wildcard = true; + } + result.push_str(&emit_value(value, depth + 1, &value_emiter_options)?); + } + Ok(result) + } + serde_yaml_ng::Value::Tagged(tagged_value) => { + let mut result = String::new(); + result.push_str(&format!("!{} ", tagged_value.tag)); + result.push_str(&emit_value(&tagged_value.value, depth, options)?); + Ok(result) + } + } +} + +fn emit_string(s: &str, options: &Options) -> Result { + let mut s = serde_yaml_ng_to_string_without_last(s)?; + if !s.starts_with('\"') && !s.starts_with('\'') && options.checkout_wildcard && contains_wildcard(&s) { + s = quote_string(&s) + } + Ok(s) +} + +fn quote_string(s: &str) -> String { + "\'".to_string() + s.replace('\'', "''").as_str() + "\'" +} + +fn need_checkout_wildcard(key: &serde_yaml_ng::Value) -> bool { + if let serde_yaml_ng::Value::String(key_str) = key { + key_str == "fake-ip-filter" + } else { + false + } +} + +fn contains_wildcard(value: &str) -> bool { + value.contains('*') || value.starts_with('+') || value.starts_with('.') +}