use std::{ fs, io, io::{IsTerminal, Write}, net::IpAddr, }; use crate::domain::{RuntimeConfig, Settings}; use crate::platform::{AppLock, AppPaths}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Target { Settings, Runtime, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ValueKind { Bool, Port, PositiveInteger, Ip, LogLevel, ProxyMode, TunStack, } #[derive(Debug, Clone, Copy)] struct SettingSpec { key: &'static str, target: Target, kind: ValueKind, } const SPECS: &[SettingSpec] = &[ spec("bypass.enabled", Target::Settings, ValueKind::Bool), spec("log.level", Target::Settings, ValueKind::LogLevel), spec( "log.max_size_mb", Target::Settings, ValueKind::PositiveInteger, ), spec("proxy.mode", Target::Runtime, ValueKind::ProxyMode), spec("proxy.listen", Target::Runtime, ValueKind::Ip), spec("proxy.mixed_port", Target::Runtime, ValueKind::Port), spec("proxy.http_port", Target::Runtime, ValueKind::Port), spec("proxy.socks_port", Target::Runtime, ValueKind::Port), spec("proxy.allow_lan", Target::Runtime, ValueKind::Bool), spec("proxy.ipv6", Target::Runtime, ValueKind::Bool), spec("api.enabled", Target::Runtime, ValueKind::Bool), spec("api.listen", Target::Runtime, ValueKind::Ip), spec("api.port", Target::Runtime, ValueKind::Port), spec("dns.enabled", Target::Runtime, ValueKind::Bool), spec("dns.listen", Target::Runtime, ValueKind::Ip), spec("dns.port", Target::Runtime, ValueKind::Port), spec("dns.ipv6", Target::Runtime, ValueKind::Bool), spec("tun.stack", Target::Runtime, ValueKind::TunStack), spec("tun.auto_route", Target::Runtime, ValueKind::Bool), spec( "tun.auto_detect_interface", Target::Runtime, ValueKind::Bool, ), spec("tun.dns_hijack", Target::Runtime, ValueKind::Bool), ]; const fn spec(key: &'static str, target: Target, kind: ValueKind) -> SettingSpec { SettingSpec { key, target, kind } } pub fn interactive(paths: &AppPaths) -> Result<(), io::Error> { if !io::stdin().is_terminal() { return list(paths); } let settings = Settings::load(&paths.settings_file())?; let runtime = RuntimeConfig::load(&paths.runtime_file())?; for (index, spec) in SPECS.iter().enumerate() { println!( "{:>2}) {:<28} {}", index + 1, spec.key, get_value(spec.key, &settings, &runtime)? ); } let selected = prompt("选择项目(0 取消): ")?; let number = selected .parse::() .map_err(|_| invalid("请输入列表中的数字"))?; if number == 0 { return Ok(()); } let spec = SPECS .get(number.saturating_sub(1)) .ok_or_else(|| invalid("选择超出范围"))?; let value = prompt(&format!("{} 新值: ", spec.key))?; set(paths, spec.key, Some(&value)) } pub fn list(paths: &AppPaths) -> Result<(), io::Error> { let settings = Settings::load(&paths.settings_file())?; let runtime = RuntimeConfig::load(&paths.runtime_file())?; for spec in SPECS { println!( "{:<28} {:<8} {}", spec.key, kind_name(spec.kind), get_value(spec.key, &settings, &runtime)? ); } Ok(()) } pub fn get(paths: &AppPaths, key: &str) -> Result<(), io::Error> { find_spec(key)?; let settings = Settings::load(&paths.settings_file())?; let runtime = RuntimeConfig::load(&paths.runtime_file())?; println!("{}", get_value(key, &settings, &runtime)?); Ok(()) } pub fn set(paths: &AppPaths, key: &str, value: Option<&str>) -> Result<(), io::Error> { let spec = find_spec(key)?; let value = match value { Some(value) => value.to_owned(), None if io::stdin().is_terminal() => prompt(&format!("{key} 新值: "))?, None => return Err(invalid("非交互调用必须提供 value")), }; let _lock = AppLock::acquire(&paths.lock_file())?; let mut settings = Settings::load(&paths.settings_file())?; let mut runtime = RuntimeConfig::load(&paths.runtime_file())?; set_value(key, &value, &mut settings, &mut runtime)?; validate_runtime(&runtime)?; match spec.target { Target::Settings => settings.save(&paths.settings_file())?, Target::Runtime => runtime.save(&paths.runtime_file())?, } invalidate_generated(paths)?; println!("已保存 {key}={value};需要重新 build/start 后生效。"); Ok(()) } pub fn reset(paths: &AppPaths, key: Option<&str>) -> Result<(), io::Error> { let _lock = AppLock::acquire(&paths.lock_file())?; let mut settings = Settings::load(&paths.settings_file())?; let mut runtime = RuntimeConfig::load(&paths.runtime_file())?; let defaults_settings = Settings::default(); let defaults_runtime = RuntimeConfig::default(); match key { Some(key) => { let spec = find_spec(key)?; let value = get_value(key, &defaults_settings, &defaults_runtime)?; set_value(key, &value, &mut settings, &mut runtime)?; validate_runtime(&runtime)?; match spec.target { Target::Settings => settings.save(&paths.settings_file())?, Target::Runtime => runtime.save(&paths.runtime_file())?, } println!("已恢复 {key}={value};需要重新 build/start 后生效。"); } None => { for spec in SPECS { let value = get_value(spec.key, &defaults_settings, &defaults_runtime)?; set_value(spec.key, &value, &mut settings, &mut runtime)?; } validate_runtime(&runtime)?; settings.save(&paths.settings_file())?; runtime.save(&paths.runtime_file())?; println!("已恢复全部公开设置;需要重新 build/start 后生效。"); } } invalidate_generated(paths)?; Ok(()) } fn find_spec(key: &str) -> Result<&'static SettingSpec, io::Error> { SPECS .iter() .find(|spec| spec.key == key) .ok_or_else(|| invalid(format!("未知 setting key `{key}`"))) } fn get_value(key: &str, settings: &Settings, runtime: &RuntimeConfig) -> Result { let value = match key { "bypass.enabled" => settings.bypass.enabled.to_string(), "log.level" => settings.log.level.clone(), "log.max_size_mb" => settings.log.max_size_mb.to_string(), "proxy.mode" => runtime.proxy.mode.clone(), "proxy.listen" => runtime.proxy.listen.clone(), "proxy.mixed_port" => runtime.proxy.mixed_port.to_string(), "proxy.http_port" => runtime.proxy.http_port.to_string(), "proxy.socks_port" => runtime.proxy.socks_port.to_string(), "proxy.allow_lan" => runtime.proxy.allow_lan.to_string(), "proxy.ipv6" => runtime.proxy.ipv6.to_string(), "api.enabled" => runtime.api.enabled.to_string(), "api.listen" => runtime.api.listen.clone(), "api.port" => runtime.api.port.to_string(), "dns.enabled" => runtime.dns.enabled.to_string(), "dns.listen" => runtime.dns.listen.clone(), "dns.port" => runtime.dns.port.to_string(), "dns.ipv6" => runtime.dns.ipv6.to_string(), "tun.stack" => runtime.tun.stack.clone(), "tun.auto_route" => runtime.tun.auto_route.to_string(), "tun.auto_detect_interface" => runtime.tun.auto_detect_interface.to_string(), "tun.dns_hijack" => runtime.tun.dns_hijack.to_string(), _ => return Err(invalid(format!("未知 setting key `{key}`"))), }; Ok(value) } fn set_value( key: &str, value: &str, settings: &mut Settings, runtime: &mut RuntimeConfig, ) -> Result<(), io::Error> { let spec = find_spec(key)?; validate_value(spec.kind, value)?; match key { "bypass.enabled" => settings.bypass.enabled = parse_bool(value)?, "log.level" => settings.log.level = value.to_ascii_lowercase(), "log.max_size_mb" => settings.log.max_size_mb = value.parse().map_err(parse_error)?, "proxy.mode" => runtime.proxy.mode = value.to_ascii_lowercase(), "proxy.listen" => runtime.proxy.listen = normalize_ip(value)?, "proxy.mixed_port" => runtime.proxy.mixed_port = parse_port(value)?, "proxy.http_port" => runtime.proxy.http_port = parse_port(value)?, "proxy.socks_port" => runtime.proxy.socks_port = parse_port(value)?, "proxy.allow_lan" => runtime.proxy.allow_lan = parse_bool(value)?, "proxy.ipv6" => runtime.proxy.ipv6 = parse_bool(value)?, "api.enabled" => runtime.api.enabled = parse_bool(value)?, "api.listen" => runtime.api.listen = normalize_ip(value)?, "api.port" => runtime.api.port = parse_port(value)?, "dns.enabled" => runtime.dns.enabled = parse_bool(value)?, "dns.listen" => runtime.dns.listen = normalize_ip(value)?, "dns.port" => runtime.dns.port = parse_port(value)?, "dns.ipv6" => runtime.dns.ipv6 = parse_bool(value)?, "tun.stack" => runtime.tun.stack = value.to_ascii_lowercase(), "tun.auto_route" => runtime.tun.auto_route = parse_bool(value)?, "tun.auto_detect_interface" => runtime.tun.auto_detect_interface = parse_bool(value)?, "tun.dns_hijack" => runtime.tun.dns_hijack = parse_bool(value)?, _ => return Err(invalid(format!("未知 setting key `{key}`"))), } Ok(()) } fn validate_value(kind: ValueKind, value: &str) -> Result<(), io::Error> { match kind { ValueKind::Bool => { parse_bool(value)?; } ValueKind::Port => { parse_port(value)?; } ValueKind::PositiveInteger => { let number: u32 = value.parse().map_err(parse_error)?; if number == 0 || number > 4096 { return Err(invalid("数值必须在 1..=4096")); } } ValueKind::Ip => { normalize_ip(value)?; } ValueKind::LogLevel => { if !matches!( value.to_ascii_lowercase().as_str(), "error" | "warn" | "info" | "debug" | "trace" ) { return Err(invalid("日志级别必须是 error|warn|info|debug|trace")); } } ValueKind::ProxyMode => { if !matches!( value.to_ascii_lowercase().as_str(), "rule" | "global" | "direct" ) { return Err(invalid("代理模式必须是 rule|global|direct")); } } ValueKind::TunStack => { if !matches!( value.to_ascii_lowercase().as_str(), "system" | "gvisor" | "mixed" ) { return Err(invalid("TUN stack 必须是 system|gvisor|mixed")); } } } Ok(()) } fn validate_runtime(runtime: &RuntimeConfig) -> Result<(), io::Error> { let ports = [ ("proxy.mixed_port", runtime.proxy.mixed_port), ("proxy.http_port", runtime.proxy.http_port), ("proxy.socks_port", runtime.proxy.socks_port), ("api.port", runtime.api.port), ("dns.port", runtime.dns.port), ]; for (index, (left_name, left)) in ports.iter().enumerate() { for (right_name, right) in ports.iter().skip(index + 1) { if left == right { return Err(invalid(format!( "端口冲突:{left_name} 和 {right_name} 都是 {left}" ))); } } } Ok(()) } fn parse_bool(value: &str) -> Result { match value.to_ascii_lowercase().as_str() { "true" | "on" => Ok(true), "false" | "off" => Ok(false), _ => Err(invalid("布尔值必须是 on|off|true|false")), } } fn parse_port(value: &str) -> Result { let port: u16 = value.parse().map_err(parse_error)?; if port == 0 { return Err(invalid("端口必须在 1..=65535")); } Ok(port) } fn normalize_ip(value: &str) -> Result { value .parse::() .map(|ip| ip.to_string()) .map_err(|_| invalid(format!("无效 IP 地址 `{value}`"))) } fn parse_error(error: std::num::ParseIntError) -> io::Error { invalid(error.to_string()) } fn invalidate_generated(paths: &AppPaths) -> Result<(), io::Error> { let generated = paths.generated_dir(); if generated.is_dir() { fs::remove_dir_all(&generated)?; } fs::create_dir_all(generated) } fn prompt(message: &str) -> Result { print!("{message}"); io::stdout().flush()?; let mut input = String::new(); io::stdin().read_line(&mut input)?; Ok(input.trim().to_owned()) } fn kind_name(kind: ValueKind) -> &'static str { match kind { ValueKind::Bool => "bool", ValueKind::Port => "port", ValueKind::PositiveInteger => "integer", ValueKind::Ip => "ip", ValueKind::LogLevel | ValueKind::ProxyMode | ValueKind::TunStack => "enum", } } fn invalid(message: impl Into) -> io::Error { io::Error::new(io::ErrorKind::InvalidInput, message.into()) } #[cfg(test)] mod tests { use super::{SPECS, set_value, validate_runtime}; use crate::domain::{RuntimeConfig, Settings}; use std::collections::HashSet; #[test] fn registry_keys_are_unique() { let mut keys = HashSet::new(); assert!(SPECS.iter().all(|spec| keys.insert(spec.key))); } #[test] fn accepts_typed_values_and_rejects_unknown_keys() { let mut settings = Settings::default(); let mut runtime = RuntimeConfig::default(); set_value("proxy.mode", "global", &mut settings, &mut runtime).unwrap(); set_value("proxy.allow_lan", "on", &mut settings, &mut runtime).unwrap(); assert_eq!(runtime.proxy.mode, "global"); assert!(runtime.proxy.allow_lan); assert!(set_value("active.tun", "on", &mut settings, &mut runtime).is_err()); } #[test] fn rejects_port_conflicts() { let mut runtime = RuntimeConfig::default(); runtime.api.port = runtime.proxy.mixed_port; assert!(validate_runtime(&runtime).is_err()); } }