diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index fce5bdf2f..75ad8d624 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -32,26 +32,6 @@ pub fn open_web_url(url: String) -> CmdResult<()> { open::that(url.as_str()).stringify_err() } -// TODO 后续可以为前端提供接口,当前作为托盘菜单使用 -/// 打开 Verge 最新日志 -#[tauri::command] -pub async fn open_app_log() -> CmdResult<()> { - let log_path = dirs::app_latest_log().stringify_err()?; - #[cfg(target_os = "windows")] - let log_path = crate::utils::help::snapshot_path(&log_path).stringify_err()?; - open::that(log_path).stringify_err() -} - -// TODO 后续可以为前端提供接口,当前作为托盘菜单使用 -/// 打开 Clash 最新日志 -#[tauri::command] -pub async fn open_core_log() -> CmdResult<()> { - let log_path = dirs::clash_latest_log().stringify_err()?; - #[cfg(target_os = "windows")] - let log_path = crate::utils::help::snapshot_path(&log_path).stringify_err()?; - open::that(log_path).stringify_err() -} - /// 打开/关闭开发者工具 #[tauri::command] pub fn open_devtools(app_handle: AppHandle) { diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 1d2f53b99..ea9b33e55 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -7,6 +7,7 @@ use clash_verge_logging::{Type, logging}; use serde::{Deserialize, Serialize}; use serde_yaml_ng::{Mapping, Value}; use std::{ + borrow::Cow, net::{IpAddr, Ipv4Addr, SocketAddr}, str::FromStr as _, }; @@ -267,11 +268,11 @@ impl IClashTemp { let val_str = val_str.trim(); let val = match val_str.starts_with(':') { - true => format!("127.0.0.1{val_str}"), - false => val_str.to_owned(), + true => Cow::Owned(format!("127.0.0.1{val_str}")), + false => Cow::Borrowed(val_str), }; - SocketAddr::from_str(val.as_str()).ok().map(|s| s.to_string()) + SocketAddr::from_str(&val).ok().map(|s| s.to_string()) } None => None, }) diff --git a/src-tauri/src/config/runtime.rs b/src-tauri/src/config/runtime.rs index eb3789513..d063e1350 100644 --- a/src-tauri/src/config/runtime.rs +++ b/src-tauri/src/config/runtime.rs @@ -37,23 +37,22 @@ impl IRuntime { } } - let patch_tun = patch.get("tun"); - if let Some(patch_tun_value) = patch_tun { - let mut tun = config - .get("tun") - .and_then(|val| val.as_mapping()) - .cloned() - .unwrap_or_else(Mapping::new); + let Some(patch_tun) = patch.get("tun") else { + return; + }; - if let Some(patch_tun_mapping) = patch_tun_value.as_mapping() { - for key in use_keys(patch_tun_mapping) { - if let Some(value) = patch_tun_mapping.get(key.as_str()) { - tun.insert(Value::from(key.as_str()), value.clone()); - } + let tun_key = Value::from("tun"); + if !matches!(config.get(&tun_key), Some(Value::Mapping(_))) { + config.insert(tun_key.clone(), Value::Mapping(Mapping::new())); + } + + if let (Some(patch_tun_mapping), Some(Value::Mapping(tun))) = (patch_tun.as_mapping(), config.get_mut(&tun_key)) + { + for key in use_keys(patch_tun_mapping) { + if let Some(value) = patch_tun_mapping.get(key.as_str()) { + tun.insert(Value::from(key.as_str()), value.clone()); } } - - config.insert("tun".into(), Value::from(tun)); } } diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index cc8e10637..109c4b03c 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -60,23 +60,16 @@ static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; async fn get_bypass() -> String { - let use_default = Config::verge().await.latest_arc().use_default_bypass.unwrap_or(true); - let res = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); - verge.system_proxy_bypass.clone() - }; - let custom_bypass = match res { - Some(bypass) => bypass, - None => "".into(), - }; + let verge = Config::verge().await.latest_arc(); + let use_default = verge.use_default_bypass.unwrap_or(true); + let custom_bypass = verge.system_proxy_bypass.as_deref().unwrap_or(""); if custom_bypass.is_empty() { DEFAULT_BYPASS.into() } else if use_default { format!("{DEFAULT_BYPASS},{custom_bypass}").into() } else { - custom_bypass + custom_bypass.into() } } @@ -141,18 +134,19 @@ impl Sysopt { None => Config::clash().await.latest_arc().get_mixed_port(), }; let pac_port = IVerge::get_singleton_port(); - let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( - verge.enable_system_proxy.unwrap_or_default(), - verge.proxy_auto_config.unwrap_or_default(), - verge.proxy_host.clone().unwrap_or_else(|| String::from("127.0.0.1")), - verge.enable_proxy_guard.unwrap_or_default(), - ); // 先 await, 避免持有锁导致的 Send 问题 let bypass = get_bypass().await; + let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( + verge.enable_system_proxy.unwrap_or_default(), + verge.proxy_auto_config.unwrap_or_default(), + verge.proxy_host.as_deref().unwrap_or("127.0.0.1"), + verge.enable_proxy_guard.unwrap_or_default(), + ); + let (sys, auto, guard_type) = { let (sys, auto) = &mut *self.inner_proxy.write(); - sys.host = proxy_host.clone().into(); + sys.host = proxy_host.into(); sys.port = port; sys.bypass = bypass.into(); auto.url = format!("http://{proxy_host}:{pac_port}/commands/pac"); diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index fcde700c9..eac413a7f 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -6,8 +6,11 @@ use crate::process::AsyncHandler; use crate::singleton; use crate::utils::window_manager::WindowManager; use crate::{ - Type, cmd, config::Config, feat, logging, module::lightweight::is_in_lightweight_mode, - utils::dirs::find_target_icons, + Type, cmd, + config::Config, + feat, logging, + module::lightweight::is_in_lightweight_mode, + utils::{dirs::find_target_icons, help}, }; use clash_verge_limiter::{Limiter, SystemClock, SystemLimiter}; use clash_verge_logging::logging_error; @@ -399,7 +402,7 @@ impl Tray { } } -fn create_hotkeys(hotkeys: &Option>) -> HashMap { +fn create_hotkeys(hotkeys: &Option>) -> HashMap<&str, &str> { hotkeys .as_ref() .map(|h| { @@ -412,13 +415,13 @@ fn create_hotkeys(hotkeys: &Option>) -> HashMap { if key.to_uppercase().contains("NUMPAD") { None } else { - Some((func.into(), key.into())) + Some((func, key)) } } _ => None, } }) - .collect::>() + .collect::>() }) .unwrap_or_default() } @@ -655,7 +658,7 @@ async fn create_tray_menu( MenuIds::DASHBOARD, &texts.dashboard, true, - hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()), + hotkeys.get("open_or_close_dashboard").copied(), )?; let rule_mode = &CheckMenuItem::with_id( @@ -664,7 +667,7 @@ async fn create_tray_menu( &texts.rule_mode, true, current_proxy_mode == "rule", - hotkeys.get("clash_mode_rule").map(|s| s.as_str()), + hotkeys.get("clash_mode_rule").copied(), )?; let global_mode = &CheckMenuItem::with_id( @@ -673,7 +676,7 @@ async fn create_tray_menu( &texts.global_mode, true, current_proxy_mode == "global", - hotkeys.get("clash_mode_global").map(|s| s.as_str()), + hotkeys.get("clash_mode_global").copied(), )?; let direct_mode = &CheckMenuItem::with_id( @@ -682,7 +685,7 @@ async fn create_tray_menu( &texts.direct_mode, true, current_proxy_mode == "direct", - hotkeys.get("clash_mode_direct").map(|s| s.as_str()), + hotkeys.get("clash_mode_direct").copied(), )?; let outbound_modes = if show_outbound_modes_inline { @@ -730,7 +733,7 @@ async fn create_tray_menu( &texts.system_proxy, true, system_proxy_enabled, - hotkeys.get("toggle_system_proxy").map(|s| s.as_str()), + hotkeys.get("toggle_system_proxy").copied(), )?; let tun_mode = &CheckMenuItem::with_id( @@ -739,7 +742,7 @@ async fn create_tray_menu( &texts.tun_mode, tun_mode_available, tun_mode_enabled, - hotkeys.get("toggle_tun_mode").map(|s| s.as_str()), + hotkeys.get("toggle_tun_mode").copied(), )?; let close_all_connections = &MenuItem::with_id( @@ -756,7 +759,7 @@ async fn create_tray_menu( &texts.lightweight_mode, true, is_lightweight_mode, - hotkeys.get("entry_lightweight_mode").map(|s| s.as_str()), + hotkeys.get("entry_lightweight_mode").copied(), )?; let copy_env = &MenuItem::with_id(app_handle, MenuIds::COPY_ENV, &texts.copy_env, true, None::<&str>)?; @@ -811,7 +814,7 @@ async fn create_tray_menu( ], )?; - let quit_accelerator = hotkeys.get("quit").map(|s| s.as_str()); + let quit_accelerator = hotkeys.get("quit").copied(); #[cfg(target_os = "macos")] let quit_accelerator = quit_accelerator.or(Some("Cmd+Q")); @@ -953,10 +956,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { let _ = cmd::open_logs_dir().await; } MenuIds::APP_LOG => { - let _ = cmd::open_app_log().await; + let _ = help::open_app_latest_log(); } MenuIds::CORE_LOG => { - let _ = cmd::open_core_log().await; + let _ = help::open_core_latest_log(); } MenuIds::RESTART_CLASH => feat::restart_clash_core().await, MenuIds::RESTART_APP => feat::restart_app().await, diff --git a/src-tauri/src/enhance/field.rs b/src-tauri/src/enhance/field.rs index f7a538b4b..04e73a786 100644 --- a/src-tauri/src/enhance/field.rs +++ b/src-tauri/src/enhance/field.rs @@ -1,6 +1,5 @@ use serde_yaml_ng::{Mapping, Value}; use smartstring::alias::String; -use std::collections::HashSet; pub const HANDLE_FIELDS: [&str; 12] = [ "mode", @@ -19,46 +18,62 @@ pub const HANDLE_FIELDS: [&str; 12] = [ pub const DEFAULT_FIELDS: [&str; 5] = ["proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"]; +fn lowercase_key(key: &str) -> Value { + let mut key = String::from(key); + key.make_ascii_lowercase(); + Value::from(key.as_str()) +} + pub fn use_lowercase(config: &Mapping) -> Mapping { - let mut ret = Mapping::new(); + let mut lowercased = Mapping::new(); for (key, value) in config.into_iter() { if let Some(key_str) = key.as_str() { - let mut key_str = String::from(key_str); - key_str.make_ascii_lowercase(); - ret.insert(Value::from(key_str.as_str()), value.clone()); + lowercased.insert(lowercase_key(key_str), value.clone()); } } - ret + lowercased } -pub fn use_sort(config: Mapping) -> Mapping { - let mut ret = Mapping::new(); +pub fn use_lowercase_owned(config: Mapping) -> Mapping { + let mut lowercased = Mapping::new(); + + for (key, value) in config { + if let Some(key_str) = key.as_str() { + lowercased.insert(lowercase_key(key_str), value); + } + } + lowercased +} + +pub fn use_sort(mut config: Mapping) -> Mapping { + let mut sorted = Mapping::new(); HANDLE_FIELDS.into_iter().for_each(|key| { let key = Value::from(key); - if let Some(value) = config.get(&key) { - ret.insert(key, value.clone()); + if let Some(value) = config.remove(&key) { + sorted.insert(key, value); } }); - let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect(); - - let config_keys: HashSet<&str> = config.keys().filter_map(|e| e.as_str()).collect(); - - config_keys.difference(&supported_keys).for_each(|&key| { - let key = Value::from(key); - if let Some(value) = config.get(&key) { - ret.insert(key, value.clone()); + let mut default_field_values = Mapping::new(); + for (key, value) in config { + if let Some(key_str) = key.as_str() { + if DEFAULT_FIELDS.contains(&key_str) { + default_field_values.insert(key, value); + } else if !HANDLE_FIELDS.contains(&key_str) { + sorted.insert(key, value); + } } - }); + } + DEFAULT_FIELDS.into_iter().for_each(|key| { let key = Value::from(key); - if let Some(value) = config.get(&key) { - ret.insert(key, value.clone()); + if let Some(value) = default_field_values.remove(&key) { + sorted.insert(key, value); } }); - ret + sorted } #[inline] diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 09d55ab41..23f0efb05 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -301,7 +301,7 @@ async fn process_global_items( if let ChainType::Merge(merge) = global_merge.data { exists_keys.extend(use_keys(&merge)); - config = use_merge(&merge, config.to_owned()); + config = use_merge(&merge, config); } if let ChainType::Script(script) = global_script.data { @@ -333,20 +333,20 @@ async fn process_profile_items( profile_name: &String, ) -> (Mapping, Vec, HashMap) { if let ChainType::Rules(rules) = rules_item.data { - config = use_seq(rules, config.to_owned(), "rules"); + config = use_seq(rules, config, "rules"); } if let ChainType::Proxies(proxies) = proxies_item.data { - config = use_seq(proxies, config.to_owned(), "proxies"); + config = use_seq(proxies, config, "proxies"); } if let ChainType::Groups(groups) = groups_item.data { - config = use_seq(groups, config.to_owned(), "proxy-groups"); + config = use_seq(groups, config, "proxy-groups"); } if let ChainType::Merge(merge) = merge_item.data { exists_keys.extend(use_keys(&merge)); - config = use_merge(&merge, config.to_owned()); + config = use_merge(&merge, config); } if let ChainType::Script(script) = script_item.data { diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs index 1e44c3769..15797fb04 100644 --- a/src-tauri/src/enhance/script.rs +++ b/src-tauri/src/enhance/script.rs @@ -1,6 +1,6 @@ use crate::process::AsyncHandler; -use super::use_lowercase; +use super::field::{use_lowercase, use_lowercase_owned}; use anyhow::{Error, Result}; use boa_engine::{Context, JsString, JsValue, Source, native_function::NativeFunction}; use clash_verge_logging::{Type, logging_error}; @@ -126,7 +126,7 @@ fn use_script_sync(script: String, config: &Mapping, name: &String) -> Result<(M let res: Result = parse_json_safely(&result); match res { - Ok(config) => Ok((use_lowercase(&config), outputs.lock().to_vec())), + Ok(config) => Ok((use_lowercase_owned(config), outputs.lock().to_vec())), Err(err) => { outputs .lock() diff --git a/src-tauri/src/enhance/seq.rs b/src-tauri/src/enhance/seq.rs index 5a3b0c5bc..826f2fc47 100644 --- a/src-tauri/src/enhance/seq.rs +++ b/src-tauri/src/enhance/seq.rs @@ -49,13 +49,13 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { Vec::new() }; - let mut new_seq = Sequence::new(); - new_seq.extend(prepend); + let mut updated_items = Sequence::new(); + updated_items.extend(prepend); - if let Some(Value::Sequence(origin)) = config.get(field) { + if let Some(Value::Sequence(existing_items)) = config.remove(field) { // Filter out deleted items - let filtered: Sequence = origin - .iter() + let kept_items: Sequence = existing_items + .into_iter() .filter(|item| { if let Value::String(s) = item { !delete.contains(s) @@ -69,25 +69,34 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { true } }) - .cloned() .collect(); - new_seq.extend(filtered); + updated_items.extend(kept_items); } - new_seq.extend(append); - config.insert(Value::String(field.into()), Value::Sequence(new_seq)); + updated_items.extend(append); + config.insert(Value::String(field.into()), Value::Sequence(updated_items)); - // If this is proxies field, we also need to filter proxy-groups - if field == "proxies" - && let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") - { - let mut new_groups = Sequence::new(); - let mut appended_to_selector = false; - for group in groups { - if let Value::Mapping(group_map) = group { - let mut proxies_seq = group_map.get("proxies").and_then(Value::as_sequence).map(|proxies| { + if field != "proxies" { + return config; + } + + let Some(proxy_groups_value) = config.remove("proxy-groups") else { + return config; + }; + + let Value::Sequence(proxy_groups) = proxy_groups_value else { + config.insert(Value::String("proxy-groups".into()), proxy_groups_value); + return config; + }; + + let mut updated_groups = Sequence::new(); + let mut appended_to_selector = false; + for group in proxy_groups { + if let Value::Mapping(mut group_map) = group { + let mut group_proxies = match group_map.remove("proxies") { + Some(Value::Sequence(proxies)) => Some( proxies - .iter() + .into_iter() .filter(|p| { if let Value::String(name) = p { !delete.contains(name) @@ -95,42 +104,46 @@ pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping { true } }) - .cloned() - .collect::() - }); - - if !appended_to_selector && !added_proxy_names.is_empty() && is_selector_group(group_map) { - let base_seq = proxies_seq.unwrap_or_else(Sequence::new); - let mut seq = Sequence::new(); - let mut existing = HashSet::new(); - for name in &added_proxy_names { - if existing.insert(name.clone()) { - seq.push(Value::String(name.clone())); - } - } - for value in base_seq { - if let Value::String(name) = &value - && !existing.insert(name.to_owned()) - { - continue; - } - - seq.push(value); - } - proxies_seq = Some(seq); - appended_to_selector = true; + .collect::(), + ), + Some(value) => { + group_map.insert(Value::String("proxies".into()), value); + None } + None => None, + }; - if let Some(seq) = proxies_seq { - group_map.insert(Value::String("proxies".into()), Value::Sequence(seq)); + if !appended_to_selector && !added_proxy_names.is_empty() && is_selector_group(&group_map) { + let base_group_proxies = group_proxies.unwrap_or_else(Sequence::new); + let mut merged_proxies = Sequence::new(); + let mut seen_proxy_names = HashSet::new(); + for name in &added_proxy_names { + if seen_proxy_names.insert(name.clone()) { + merged_proxies.push(Value::String(name.clone())); + } } - new_groups.push(Value::Mapping(group_map.to_owned())); - } else { - new_groups.push(group.to_owned()); + for value in base_group_proxies { + if let Value::String(name) = &value + && !seen_proxy_names.insert(name.to_owned()) + { + continue; + } + + merged_proxies.push(value); + } + group_proxies = Some(merged_proxies); + appended_to_selector = true; } + + if let Some(group_proxies) = group_proxies { + group_map.insert(Value::String("proxies".into()), Value::Sequence(group_proxies)); + } + updated_groups.push(Value::Mapping(group_map)); + } else { + updated_groups.push(group); } - config.insert(Value::String("proxy-groups".into()), Value::Sequence(new_groups)); } + config.insert(Value::String("proxy-groups".into()), Value::Sequence(updated_groups)); config } @@ -288,4 +301,27 @@ proxy-groups: let names: Vec<&str> = group2_proxies.iter().filter_map(Value::as_str).collect(); assert_eq!(names, vec!["proxy1"]); } + + #[test] + #[allow(clippy::unwrap_used)] + #[allow(clippy::expect_used)] + fn test_preserve_non_sequence_proxy_groups() { + let config_str = r#" +proxies: +- name: "proxy1" + type: "ss" +proxy-groups: "invalid" +"#; + let mut config: Mapping = serde_yaml_ng::from_str(config_str).expect("Failed to parse test config YAML"); + + let seq = SeqMap { + prepend: Sequence::new(), + append: Sequence::new(), + delete: vec!["proxy1".to_string()], + }; + + config = use_seq(seq, config, "proxies"); + + assert_eq!(config.get("proxy-groups").and_then(Value::as_str), Some("invalid")); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 17f585cde..fd5167db4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -141,8 +141,6 @@ mod app_init { cmd::open_logs_dir, cmd::open_web_url, cmd::open_core_dir, - cmd::open_app_log, - cmd::open_core_log, cmd::get_portable_flag, cmd::get_network_interfaces, cmd::get_system_hostname, diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 38517e8bb..316602e3c 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -3,7 +3,7 @@ use anyhow::{Context as _, Result, anyhow, bail}; use clash_verge_logging::{Type, logging}; use nanoid::nanoid; use serde::{Serialize, de::DeserializeOwned}; -use serde_yaml_ng::Mapping; +use serde_yaml_ng::{Mapping, Value}; #[cfg(target_os = "windows")] use std::path::Path; use std::{path::PathBuf, str::FromStr}; @@ -35,10 +35,10 @@ pub async fn read_mapping(path: &PathBuf) -> Result { val.apply_merge() .with_context(|| format!("failed to apply merge \"{}\"", path.display()))?; - Ok(val - .as_mapping() - .ok_or_else(|| anyhow!("failed to transform to yaml mapping \"{}\"", path.display()))? - .to_owned()) + match val { + Value::Mapping(map) => Ok(map), + _ => Err(anyhow!("failed to transform to yaml mapping \"{}\"", path.display())), + } } Err(err) => { let error_msg = format!("YAML syntax error in {}: {}", path.display(), err); @@ -66,10 +66,9 @@ pub async fn save_yaml(path: &PathBuf, data: &T, prefix: Op None => data_str, }; - let path_str = path.as_os_str().to_string_lossy().to_string(); tokio::fs::write(path, yaml_str.as_bytes()) .await - .with_context(|| format!("failed to save file \"{path_str}\""))?; + .with_context(|| format!("failed to save file \"{}\"", path.display()))?; tokio::time::sleep(std::time::Duration::from_millis(50)).await; Ok(()) } @@ -203,6 +202,22 @@ pub fn open_file(path: PathBuf) -> Result<()> { Ok(()) } +pub fn open_latest_log(path: PathBuf) -> Result<()> { + #[cfg(target_os = "windows")] + let path = snapshot_path(&path)?; + open_file(path) +} + +pub fn open_app_latest_log() -> Result<()> { + let path = crate::utils::dirs::app_latest_log()?; + open_latest_log(path) +} + +pub fn open_core_latest_log() -> Result<()> { + let path = crate::utils::dirs::clash_latest_log()?; + open_latest_log(path) +} + #[cfg(target_os = "linux")] pub fn linux_elevator() -> String { use std::process::Command;