diff --git a/Cargo.lock b/Cargo.lock index 4e46bab..4cd5c96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,21 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "mirror" +version = "0.1.0" +dependencies = [ + "anyhow", + "dunce", + "encoding_rs", + "mlua", + "tinyjson", + "tracing", + "tracing-appender", + "tracing-subscriber", + "windows-sys", +] + [[package]] name = "mlua" version = "0.12.0" @@ -308,21 +323,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "rshim" -version = "0.1.0" -dependencies = [ - "anyhow", - "dunce", - "encoding_rs", - "mlua", - "tinyjson", - "tracing", - "tracing-appender", - "tracing-subscriber", - "windows-sys", -] - [[package]] name = "rustc-hash" version = "2.1.3" diff --git a/Cargo.toml b/Cargo.toml index 9e37233..4da076e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rshim" +name = "mirror" version = "0.1.0" edition = "2024" rust-version = "1.94" @@ -30,4 +30,9 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" encoding_rs = "0.8.35" -tinyjson="2.5.1" \ No newline at end of file +tinyjson="2.5.1" + + +[features] +default = [] +args=[] \ No newline at end of file diff --git a/build.rs b/build.rs index 93f68b0..c1d438b 100644 --- a/build.rs +++ b/build.rs @@ -20,7 +20,7 @@ fn main() { // cargo:rerun-if-changed -> 当指定文件变化时,重新运行 xxx println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=rshim.lua"); + println!("cargo:rerun-if-changed=mirror.lua"); println!("cargo:rustc-env=OUTPUT_DIR={}", output_dir.display()); // 创建目录 @@ -34,13 +34,13 @@ fn main() { } // 复制文件 - let source = PathBuf::from(&manifest_dir).join("mimic.lua"); - let destination = output_dir.join("rshim.lua"); + let source = PathBuf::from(&manifest_dir).join("mirror.lua"); + let destination = output_dir.join("mirror.lua"); if source.exists() { - fs::copy(&source, &destination).expect("Failed to copy mimic.lua"); - println!("Copied mimic.lua to: {:?}", destination); + fs::copy(&source, &destination).expect("Failed to copy mirror.lua"); + println!("Copied mirror.lua to: {:?}", destination); } else { - panic!("mimic.lua not found at: {:?}", source); + panic!("mirror.lua not found at: {:?}", source); } } diff --git a/mimic.lua b/mirror.lua similarity index 98% rename from mimic.lua rename to mirror.lua index 64a4b9a..5e7e4f5 100644 --- a/mimic.lua +++ b/mirror.lua @@ -1,4 +1,4 @@ --- mimic.lua (总控制台) +-- mirror.lua (总控制台) -- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools) local base_dir = __SHIM_DIR__ diff --git a/src/layout.rs b/src/layout.rs index c3350f4..5b4cb13 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -36,7 +36,7 @@ impl Layout { let bin_dir = base_dir.join("bin"); let tools_dir = base_dir.join("tools"); - let lua_file = base_dir.join("mimic.lua"); + let lua_file = base_dir.join("../mirror.lua"); Ok(Self { base_dir, diff --git a/src/lib.rs b/src/lib.rs index cefb0ac..2d49fbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod layout; mod loader; mod logger; mod runtime; -mod shim; +mod mirror; mod spec; pub mod sys; mod utils; @@ -12,8 +12,8 @@ mod validators; pub use layout::Layout; pub use runtime::LuaRuntime; -pub use shim::Shim; -pub use spec::ShimSpec; +pub use mirror::Mirror; +pub use spec::MirrorSpec; // pub use sys::{ // ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG, // EXIT_PROG_TERMINATED, execute_elevated, set_console_ctrl_handler, diff --git a/src/loader.rs b/src/loader.rs index 2eab9de..5bc1daa 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -1,4 +1,4 @@ -use crate::spec::ShimSpec; +use crate::spec::MirrorSpec; use crate::validators::LuaValidator; use crate::{Layout, LuaRuntime}; use anyhow::{Context, Result, bail}; @@ -16,7 +16,7 @@ pub enum Source { pub struct SpecLoader; impl SpecLoader { - pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result { + pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result { match source { Source::Lua(r) => Self::resolve_lua_spec(&r, layout, target_name), Source::Json => Self::load_from_json(layout), @@ -27,13 +27,13 @@ impl SpecLoader { runtime: &LuaRuntime, layout: &Layout, target_name: &str, - ) -> Result { - // 策略 1: 尝试加载全局配置文件 mimic.lua - let global_config = layout.base_dir.join("rshim.lua"); + ) -> Result { + // 策略 1: 尝试加载全局配置文件 mirror.lua + let global_config = layout.base_dir.join("mirror.lua"); // if !global_config.is_file() { // bail!( - // "未找到主配置文件: [{}],请确保在安装根目录创建 rshim.lua", + // "未找到主配置文件: [{}],请确保在安装根目录创建 mirror.lua", // global_config.display() // ); // } @@ -42,12 +42,12 @@ impl SpecLoader { trace!(path = %global_config.display(), "发现全局配置文件,尝试解析"); let root_table: Table = runtime.eval_script(&global_config)?; - // 检查 mimic.lua 中是否存在以 target_name 命名的 Table 节点 + // 检查 mirror.lua 中是否存在以 target_name 命名的 Table 节点 if root_table .contains_key(target_name) .context("检查全局配置失败")? { - let target_val: ShimSpec = root_table + let target_val: MirrorSpec = root_table .get(target_name) .with_context(|| format!("解析配置 [{}] 失败: ", target_name))?; debug!( @@ -73,7 +73,7 @@ impl SpecLoader { if config_path.is_file() { debug!(path = %config_path.display(), "找到独立配置文件,开始加载"); // 直接泛型反序列化为 ShimConfig - return runtime.eval_script::(config_path); + return runtime.eval_script::(config_path); } trace!(path = %config_path.display(), "独立配置文件不存在,跳过"); } @@ -81,14 +81,14 @@ impl SpecLoader { // 策略 3: 所有查找失败,抛出错误 warn!(target = %target_name, "未找到任何匹配的配置文件"); bail!( - "未找到关于 '{}' 的配置。请检查 mimic.lua 或特定的 {}.lua 文件", + "未找到关于 '{}' 的配置。请检查 mirror.lua 或特定的 {}.lua 文件", target_name, target_name ); } /// 从 Lua 脚本文件加载 ShimSpec - fn load_from_json(layout: &Layout) -> Result { + fn load_from_json(layout: &Layout) -> Result { todo!("实现json来源") } diff --git a/src/main.rs b/src/main.rs index 2def279..c879c80 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ -use rshim::sys::*; -use rshim::Shim; +use mirror::sys::*; +use mirror::Mirror; use std::{env, process::exit}; use tracing_subscriber::{fmt, EnvFilter}; @@ -15,7 +15,7 @@ fn main() { set_console_ctrl_handler(); // 3. 解析调用参数与代理 Shim 配置 let calling_args: Vec<_> = env::args_os().skip(1).collect(); - let shim = match Shim::load() { + let shim = match Mirror::load() { Ok(v) => v, Err(e) => { eprintln!("加载代理(shim)配置时发生错误: {}", e); diff --git a/src/mirror.rs b/src/mirror.rs new file mode 100644 index 0000000..c77199e --- /dev/null +++ b/src/mirror.rs @@ -0,0 +1,79 @@ +use crate::loader::Source; +use crate::loader::SpecLoader; +use crate::{Layout, LuaRuntime, MirrorSpec}; +use anyhow::{Context, Result}; +use std::env; +use std::process::Command; +use tracing::debug; +use std::ffi::OsString; +pub struct Mirror { + pub spec: MirrorSpec, + pub layout: Layout, + pub target_name: String, +} + +impl Mirror { + pub fn new() -> Result { + let current_exe = env::current_exe().context("获取代理程序路径失败: {}")?; + debug!("当前目录 {}", current_exe.display()); + + let target_name = current_exe + .file_stem() + .and_then(|s| s.to_str()) + .with_context(|| format!("无法从路径 [{}] 提取有效的程序名称", current_exe.display()))? + .to_lowercase(); + debug!( + target_name = %target_name, + current_exe = %current_exe.display(), + "开始加载 Shim 配置" + ); + let layout = Layout::discover(¤t_exe)?; + + debug!( + root_dir = %layout.base_dir.display(), + bin_dir = %layout.bin_dir.display(), + tools_dir = %layout.tools_dir.display(), + "Shim 目录布局解析完成" + ); + + let runtime = LuaRuntime::new(&layout)?; + let spec = SpecLoader::resolve_spec(&Source::Lua(runtime), &layout, &target_name)?; + + Ok(Self { + spec, + layout, + target_name, + }) + } + + /// 1. 核心参数路由解析(Delegation 到 Spec 的路由逻辑) + pub fn resolve_args(&self, raw_args: I) -> Vec + where + I: IntoIterator, + S: Into, + {todo!() + // self.spec.resolve_args(raw_args) + } + + pub fn to_command(&self, runtime_args: I) -> Command + where + I: IntoIterator, + S: AsRef, + { + let mut cmd = Command::new(&self.spec.target); + + // 1. 先追加配置里固定的默认参数 (例如: node, --max-old-space-size=4096) + cmd.args(&self.spec.args); + + // 2. 透传外部动态运行时参数 + cmd.args(runtime_args); + + for (key, val) in &self.spec.env { + // 直接应用环境变量(Lua 端已经处理好字符串拼接或列表合并) + cmd.env(key, val); + } + + cmd + } +} + diff --git a/src/runtime.rs b/src/runtime.rs index 5626c93..5ee43c6 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,4 +1,4 @@ -use crate::{ShimSpec, Layout}; +use crate::{MirrorSpec, Layout}; use anyhow::{Context, Result, anyhow, bail}; use mlua::{FromLua, Lua, StdLib, Table, Value}; use std::ffi::OsStr; @@ -86,7 +86,7 @@ impl LuaRuntime { } // 4. 包装 require:配置模块缺失/加载失败时记录日志并跳过该条目, - // 而不是让整个 mimic.lua 解析失败(排查问题时日志可见) + // 而不是让整个 mirror.lua 解析失败(排查问题时日志可见) let original_require: mlua::Function = globals .get("require") .context("获取内置 require 函数失败")?; @@ -227,7 +227,7 @@ mod tests { // 复现用户场景:PATH = { base_dir .. "/tools/numa", get_env("PATH") } // 宿主 PATH 即使含双引号,也应拆分后正常拼接,而不是报错 let runtime = LuaRuntime::new(&test_layout()).unwrap(); - let cfg: ShimSpec = runtime + let cfg: MirrorSpec = runtime .lua .load( r#" diff --git a/src/shim.rs b/src/shim.rs deleted file mode 100644 index 3f9e440..0000000 --- a/src/shim.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::loader::Source; -use crate::loader::SpecLoader; -use crate::{Layout, LuaRuntime, ShimSpec}; -use anyhow::{Context, Result}; -use std::env; -use tracing::debug; - -pub struct Shim; - -impl Shim { - pub fn load() -> Result { - let current_exe = env::current_exe().context("获取代理程序路径失败: {}")?; - debug!("当前目录 {}", current_exe.display()); - - let target_name = current_exe - .file_stem() - .and_then(|s| s.to_str()) - .with_context(|| format!("无法从路径 [{}] 提取有效的程序名称", current_exe.display()))? - .to_lowercase(); - debug!( - target_name = %target_name, - current_exe = %current_exe.display(), - "开始加载 Shim 配置" - ); - let layout = Layout::discover(¤t_exe)?; - - debug!( - root_dir = %layout.base_dir.display(), - bin_dir = %layout.bin_dir.display(), - tools_dir = %layout.tools_dir.display(), - "Shim 目录布局解析完成" - ); - - let runtime = LuaRuntime::new(&layout)?; - SpecLoader::resolve_spec(&Source::Lua(runtime), &layout, &target_name) - } -} diff --git a/src/spec.rs b/src/spec.rs index 569eab9..dc7fbe8 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,6 +1,6 @@ use crate::validators::{JsonValidator, LuaValidator}; use mlua::{FromLua, Lua, ObjectLike, Value}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::path::PathBuf; use std::process::Command; @@ -18,38 +18,94 @@ fn conversion_error(message: impl Into) -> mlua::Error { #[derive(Debug, Clone, Default)] -pub struct ShimSpec { +pub struct MirrorSpec { pub target: PathBuf, pub args: Vec, + pub aliases: HashMap>, pub env: HashMap, + } -impl ShimSpec { +impl MirrorSpec { /// 根据配置快速构建准备执行的 Command 对象 - pub fn to_command(&self, runtime_args: I) -> Command +// pub fn to_command(&self, runtime_args: I) -> Command +// where +// I: IntoIterator, +// S: AsRef, +// { +// let mut cmd = Command::new(&self.target); +// +// // 1. 先追加配置里固定的默认参数 (例如: node, --max-old-space-size=4096) +// cmd.args(&self.args); +// +// // 2. 透传外部动态运行时参数 +// cmd.args(runtime_args); +// +// for (key, val) in &self.env { +// // 直接应用环境变量(Lua 端已经处理好字符串拼接或列表合并) +// cmd.env(key, val); +// } +// +// cmd +// } + /// 全局解构与支持任意深度的别名嵌套展开 + pub fn resolve_args(&self, raw_args: I) -> Result, String> where I: IntoIterator, - S: AsRef, + S: Into, { - let mut cmd = Command::new(&self.target); + let mut final_args = Vec::new(); + let mut visited_stack = HashSet::new(); - // 1. 先追加配置里固定的默认参数 (例如: node, --max-old-space-size=4096) - cmd.args(&self.args); - - // 2. 透传外部动态运行时参数 - cmd.args(runtime_args); - - for (key, val) in &self.env { - // 直接应用环境变量(Lua 端已经处理好字符串拼接或列表合并) - cmd.env(key, val); + for arg in raw_args.into_iter().map(|s| s.into()) { + self.expand_arg_recursive(&arg, &mut visited_stack, &mut final_args)?; } - cmd + Ok(final_args) + } + + /// 递归展开核心函数(带visited栈死环拦截) + fn expand_arg_recursive( + &self, + arg: &OsString, + visited: &mut HashSet, + out: &mut Vec, + ) -> Result<(), String> { + let arg_str = arg.to_string_lossy(); + + // 检查是否以 mr: 开头 + if let Some(alias_key) = arg_str.strip_prefix("mr:") { + if let Some(expanded_args) = self.aliases.get(alias_key) { + // 核心死环检测:如果当前递归栈中已包含该 key,说明发生了循环引用! + if visited.contains(alias_key) { + return Err(format!( + "配置错误: 别名 'mr:{}' 存在循环嵌套依赖!", + alias_key + )); + } + + // 标记:入栈 + visited.insert(alias_key.to_string()); + + // 递归展开子项 + for sub_arg in expanded_args { + self.expand_arg_recursive(sub_arg, visited, out)?; + } + + // 回溯:出栈 + visited.remove(alias_key); + return Ok(()); + } + } + + // 非 mr: 参数或未匹配到别名,直接入队 + out.push(arg.clone()); + Ok(()) } } /// 实现 FromLua Trait,由 mlua 自动处理 Table 转换 -impl FromLua for ShimSpec { +impl FromLua for MirrorSpec { fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { // 脚本返回必须是一个 Table 变体 let table = match value { @@ -71,6 +127,7 @@ impl FromLua for ShimSpec { }; // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) + let args = match table.get::>("args")? { None | Some(Value::Nil) => Vec::new(), Some(args_val) => LuaValidator::parse_args(&args_val)?, @@ -81,13 +138,18 @@ impl FromLua for ShimSpec { None | Some(Value::Nil) => HashMap::new(), Some(env_val) => LuaValidator::parse_env(&env_val)?, + }; + let aliases = match table.get::>("aliases")? { + None | Some(Value::Nil) => HashMap::new(), + Some(env_val) => LuaValidator::parse_aliases(&env_val)?, + }; println!("环境变量结果:{:?}", env); - Ok(ShimSpec { target, args, env }) + Ok(MirrorSpec { target, args,aliases, env }) } } -impl TryFrom<&str> for ShimSpec { +impl TryFrom<&str> for MirrorSpec { type Error = anyhow::Error; fn try_from(json_str: &str) -> Result { @@ -117,10 +179,14 @@ impl TryFrom<&str> for ShimSpec { Some(env_val) => JsonValidator::parse_env(env_val)?, None => HashMap::new(), }; - - Ok(ShimSpec { + let aliases = match map.get("aliases") { + Some(env_val) => JsonValidator::parse_aliases(env_val)?, + None => HashMap::new(), + }; + Ok(MirrorSpec { target, args, + aliases, env, }) } diff --git a/src/validators.rs b/src/validators.rs index 202b7bb..63b39e1 100644 --- a/src/validators.rs +++ b/src/validators.rs @@ -1,11 +1,11 @@ +use anyhow::{Context, Result, anyhow}; use mlua::{LuaString, Table, Value}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::fmt; use std::path::PathBuf; use std::str::FromStr; use tinyjson::JsonValue; -use anyhow::{anyhow, Context, Result}; /// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 fn conversion_error(message: impl Into) -> mlua::Error { @@ -53,6 +53,7 @@ pub enum LuaValidator { Args, /// 环境变量值:支持基础标量及多维嵌套 Table(递归展平) Env, + Aliases, } impl fmt::Display for LuaValidator { @@ -61,6 +62,7 @@ impl fmt::Display for LuaValidator { Self::Target => write!(f, "目标路径 (target)"), Self::Args => write!(f, "命令行参数 (args)"), Self::Env => write!(f, "环境变量 (env)"), + Self::Aliases => write!(f, "命令行别名 (aliases)"), } } } @@ -69,7 +71,7 @@ impl LuaValidator { match item { Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => Ok(()), Value::Table(tbl) => match self { - Self::Env => self.validate_sequence_table(tbl), + Self::Env | Self::Aliases => self.validate_sequence_table(tbl), Self::Args => Err(conversion_error(format!( "{} 仅支持一维数组,不能包含嵌套 Table,索引位置: {index}", self @@ -181,6 +183,23 @@ impl LuaValidator { } Ok(()) } + + fn parse_raw_val(&self, name: &str, value: &Value) -> mlua::Result<()> { + match value { + Value::Nil + | Value::String(_) + | Value::Integer(_) + | Value::Number(_) + | Value::Boolean(_) => Ok(()), + Value::Table(tbl) => self.validate_sequence_table(tbl), + other => Err(conversion_error(format!( + "{} [{}]不支持 {} 类型, 仅支持 string/number/boolean 或嵌套数组", + self, + name, + other.type_name() + ))), + } + } } impl LuaValidator { /// 解析并校验 `target` @@ -203,25 +222,33 @@ impl LuaValidator { /// 解析并校验 `args` pub fn parse_args(value: &Value) -> mlua::Result> { - println!("跟踪args1{}", value.type_name()); - let ctx = Self::Args; - match value { - Value::Table(tbl) => { - ctx.validate_sequence_table(tbl)?; + { + println!("跟踪args1{}", value.type_name()); + #[cfg(not(feature = "args"))] + Ok(Vec::new()) + } + #[cfg(feature = "args")] + { + println!("跟踪args1{}", value.type_name()); + let ctx = Self::Args; + match value { + Value::Table(tbl) => { + ctx.validate_sequence_table(tbl)?; - let mut raw_parts = Vec::new(); - Self::collect_value_into(&ctx, value, &mut raw_parts)?; + let mut raw_parts = Vec::new(); + Self::collect_value_into(&ctx, value, &mut raw_parts)?; - for part in &raw_parts { - Self::ensure_no_nul(&ctx, &part)?; + for part in &raw_parts { + Self::ensure_no_nul(&ctx, &part)?; + } + Ok(raw_parts) } - Ok(raw_parts) + other => Err(conversion_error(format!( + "{} 必须是数组列表,实际类型是 {}", + ctx, + other.type_name() + )))?, } - other => Err(conversion_error(format!( - "{} 必须是数组列表,实际类型是 {}", - ctx, - other.type_name() - )))?, } } @@ -263,35 +290,17 @@ impl LuaValidator { name: &str, raw_val: &Value, ) -> mlua::Result { - // Self::Env.validate(&raw_val)?; - match raw_val { - Value::Nil - | Value::String(_) - | Value::Integer(_) - | Value::Number(_) - | Value::Boolean(_) => {} - Value::Table(tbl) => { - context.validate_sequence_table(tbl)?; - } - other => { - return Err(conversion_error(format!( - "{} [{}]不支持 {} 类型, 仅支持 string/number/boolean 或嵌套数组", - context, - name, - other.type_name() - ))); - } - } - let ctx = format!("{} [{}]", context, name); - let mut parts = Vec::new(); + context.parse_raw_val(name, raw_val)?; + let ctx = format!("{} [{}]", context, name); + + let mut parts = Vec::new(); Self::collect_value_into(&ctx, raw_val, &mut parts)?; // 校验每个展开元素的 NUL 字符 for part in &parts { Self::ensure_no_nul(&name, part)?; } - // 3. 使用系统路径分隔符拼接数组列表 let joined_os_str = std::env::join_paths(parts).map_err(|e| { conversion_error(format!("{} 的值无法用系统路径分隔符拼接: {}", ctx, e)) @@ -334,7 +343,129 @@ impl LuaValidator { } } -/// 专用于 JSON (tinyjson) 的类型校验与字段提取器 +impl LuaValidator { + fn parse_aliases_val( + context: &LuaValidator, + name: &str, + raw_val: &Value, + ) -> mlua::Result> { + context.parse_raw_val(name, raw_val)?; + + let ctx = format!("{} [{}]", context, name); + + let mut parts = Vec::new(); + Self::collect_value_into(&ctx, raw_val, &mut parts)?; + Ok(parts) + } + /// 解析并打平别名表 (aliases) + /// - 支持输入为 Nil / None / Table + /// - 别名的值支持:String, Number, Boolean, Nil, " " 空白串, Table(连续数组) + /// - 字符串作为整体参数保存,仅做 trim() 清理首尾空格,不按空格拆分 + /// - 包含拓扑展开与死环检测 + pub fn parse_aliases(value: &Value) -> mlua::Result>> { + let ctx = Self::Aliases; + // 1. 处理 nil / None 的情况,直接返回空 HashMap + let Value::Table(table) = value else { + return Err(conversion_error(format!( + "{} 必须是键值表 (table) ,当前类型: {}", + ctx, + value.type_name() + ))); + }; + + // 阶段一:提取原始别名映射 (Raw Extraction) + let mut raw_aliases: HashMap> = HashMap::new(); + + for pair in table.pairs::() { + let (raw_key, raw_val) = pair?; + let key = Self::parse_env_name(&raw_key)?; + let val = Self::parse_aliases_val(&ctx, &key, &raw_val)?; + + raw_aliases.insert(key, val); + } + // 阶段二:递归拓扑打平与循环引用检测 (Flattening & Cycle Detection) + let mut visited_stack = HashSet::new(); + + for key in raw_aliases.keys() { + visited_stack.clear(); + Self::detect_alias_cycle(key, &raw_aliases, &mut visited_stack)?; + } + + // 阶段三:无环前提下的高效展开 + let mut flattened_aliases: HashMap> = + HashMap::with_capacity(raw_aliases.len()); + let mut resolved_args = Vec::new(); + + for key in raw_aliases.keys() { + resolved_args.clear(); + Self::expand_alias_dfs(key, &raw_aliases, &mut resolved_args); + flattened_aliases.insert(key.clone(), resolved_args.clone()); + } + + Ok(flattened_aliases) + } + + /// 仅用于校验别名依赖图中是否存在死循环(不消耗额外的参数拼接内存) + fn detect_alias_cycle( + current_key: &str, + raw_aliases: &HashMap>, + visited_stack: &mut HashSet, + ) -> mlua::Result<()> { + // 递归栈中再次遇到相同的 Key,说明存在死循环 + if visited_stack.contains(current_key) { + return Err(conversion_error(format!( + "配置加载失败: 检测到别名循环嵌套依赖 'mr:{}'", + current_key + ))); + } + + if let Some(args) = raw_aliases.get(current_key) { + // 压栈 + visited_stack.insert(current_key.to_string()); + + for arg in args { + let arg_str = arg.to_string_lossy(); + if let Some(sub_key) = arg_str.strip_prefix("mr:") { + // 如果引用的子别名在映射表中存在,则深度优先校验 + if raw_aliases.contains_key(sub_key) { + Self::detect_alias_cycle(sub_key, raw_aliases, visited_stack)?; + } + } + } + + // 出栈(回溯) + visited_stack.remove(current_key); + } + + Ok(()) + } + /// 安全拓扑展开:在保证绝对无环的前提下递归展开 mr: 前缀参数 + fn expand_alias_dfs( + current_key: &str, + raw_aliases: &HashMap>, + out: &mut Vec, + ) { + if let Some(args) = raw_aliases.get(current_key) { + for arg in args { + let arg_str = arg.to_string_lossy(); + if let Some(sub_key) = arg_str.strip_prefix("mr:") { + if raw_aliases.contains_key(sub_key) { + // 安全地直接递归展开,无需再检查死循环 + Self::expand_alias_dfs(sub_key, raw_aliases, out); + } else { + // 找不到对应的别名,按原样参数输出 + out.push(arg.clone()); + } + } else { + // 普通参数,直接输出 + out.push(arg.clone()); + } + } + } + } +} + +/// 专用于 JSON (tinyjson) 的类型校验与字段提取器 //当前JsonValidator还为完全可用,后期会对其lua配置特别需要处理动态函数生成的值如:get_env("PATH") // 数据结构:{ // "target": "C:/tools/git.exe", @@ -353,7 +484,6 @@ impl LuaValidator { pub struct JsonValidator; impl JsonValidator { - /// 校验并解析 target (必填,非空字符串) pub fn parse_target(val: &JsonValue) -> Result { let s: &String = val @@ -368,9 +498,8 @@ impl JsonValidator { /// 校验并解析 args (选填,必须为字符串数组) pub fn parse_args(val: &JsonValue) -> Result> { - let args_vec: &Vec = val - .get() - .ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?; + let args_vec: &Vec = + val.get().ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?; args_vec .iter() @@ -407,7 +536,9 @@ impl JsonValidator { Ok(result) } - + pub fn parse_aliases(val: &JsonValue) -> Result>> { + todo!() + } // ========================================================================= // 私有辅助方法 // ========================================================================= @@ -439,15 +570,13 @@ impl JsonValidator { } } - - #[cfg(test)] mod tests { use super::*; - use crate::ShimSpec; + use crate::MirrorSpec; use mlua::{FromLua, Lua}; - fn parse(src: &str) -> mlua::Result { + fn parse(src: &str) -> mlua::Result { let lua = Lua::new(); // 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表) let get_env = lua @@ -466,7 +595,7 @@ mod tests { lua.globals().set("get_env", get_env).unwrap(); let value = lua.load(src).eval::()?; - let t = ShimSpec::from_lua(value, &lua); + let t = MirrorSpec::from_lua(value, &lua); println!("读取出的数据:{:?}", t.clone()?); t } @@ -483,7 +612,25 @@ mod tests { HOME = "C:/tools/home", CONST = 3, BOOL = true, + } + aliases = { + -- 1. 整体字符串(会自动 trim 首尾空格,保留为单个参数) + st = "status -s", + -- 2. 连续数组表 (Sequence Table) + lg = { "log", "--oneline", "-n", 10 }, + + -- 3. 标量数字与布尔值支持 + v = 1, + quiet = true, + + -- 4. 嵌套别名组合(加载期会自动展开并进行死环检测) + base_log = "log --graph", + all_log = { "mr:base_log", "--all" }, + + -- 5. nil 或空字符串(解析为空参数列表) + empty_alias = nil, + blank = " " } } "#, @@ -499,6 +646,52 @@ mod tests { || path.starts_with("C:/tools/git/bin;C:/Windows;"), "unexpected PATH: {path}" ); + // ==================== aliases 断言校验 ==================== + + // 1. 整体字符串:保留原样(trim 后),不切分空格 + assert_eq!( + cfg.aliases.get("st").unwrap(), + &vec![OsString::from("status -s")] + ); + + // 2. 连续数组表:按顺序转为 OsString 列表 + assert_eq!( + cfg.aliases.get("lg").unwrap(), + &vec![ + OsString::from("log"), + OsString::from("--oneline"), + OsString::from("-n"), + OsString::from("10") + ] + ); + + // 3. 标量数字与布尔值支持 + assert_eq!(cfg.aliases.get("v").unwrap(), &vec![OsString::from("1")]); + assert_eq!( + cfg.aliases.get("quiet").unwrap(), + &vec![OsString::from("true")] + ); + + // 4. 嵌套别名组合:加载期递归拓扑打平 + // base_log 本身为 "log --graph" + assert_eq!( + cfg.aliases.get("base_log").unwrap(), + &vec![OsString::from("log --graph")] + ); + // all_log 展开 mr:base_log 替换为 "log --graph",追加 "--all" + assert_eq!( + cfg.aliases.get("all_log").unwrap(), + &vec![OsString::from("log --graph"), OsString::from("--all")] + ); + + // 5. nil 与纯空白字符串:解析为空 Vec + assert_eq!(cfg.aliases.get("blank").unwrap(), &Vec::::new()); + // nil 键在遍历表时会被当作空或不存在,不产生 key 或值为空 Vec + assert!( + cfg.aliases + .get("empty_alias") + .map_or(true, |v| v.is_empty()) + ); } #[test]