refactor: 新增别名解析流程

- 封装通用标量/数组校验与提取方法
- 将别名解析拆分为数据清洗、循环引用检测与拓扑展开三阶段
- 重命名项目为 mirror
- 其他优化
This commit is contained in:
2026-08-26 11:52:55 +08:00
parent 8aba995fb0
commit 9e1051737a
13 changed files with 460 additions and 154 deletions

30
Cargo.lock generated
View File

@@ -164,6 +164,21 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "mirror"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"encoding_rs",
"mlua",
"tinyjson",
"tracing",
"tracing-appender",
"tracing-subscriber",
"windows-sys",
]
[[package]] [[package]]
name = "mlua" name = "mlua"
version = "0.12.0" version = "0.12.0"
@@ -308,21 +323,6 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rshim"
version = "0.1.0"
dependencies = [
"anyhow",
"dunce",
"encoding_rs",
"mlua",
"tinyjson",
"tracing",
"tracing-appender",
"tracing-subscriber",
"windows-sys",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.3" version = "2.1.3"

View File

@@ -1,5 +1,5 @@
[package] [package]
name = "rshim" name = "mirror"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
rust-version = "1.94" rust-version = "1.94"
@@ -30,4 +30,9 @@ tracing = "0.1.44"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2" tracing-appender = "0.2"
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
tinyjson="2.5.1" tinyjson="2.5.1"
[features]
default = []
args=[]

View File

@@ -20,7 +20,7 @@ fn main() {
// cargo:rerun-if-changed -> 当指定文件变化时,重新运行 xxx // cargo:rerun-if-changed -> 当指定文件变化时,重新运行 xxx
println!("cargo:rerun-if-changed=build.rs"); 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()); 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 source = PathBuf::from(&manifest_dir).join("mirror.lua");
let destination = output_dir.join("rshim.lua"); let destination = output_dir.join("mirror.lua");
if source.exists() { if source.exists() {
fs::copy(&source, &destination).expect("Failed to copy mimic.lua"); fs::copy(&source, &destination).expect("Failed to copy mirror.lua");
println!("Copied mimic.lua to: {:?}", destination); println!("Copied mirror.lua to: {:?}", destination);
} else { } else {
panic!("mimic.lua not found at: {:?}", source); panic!("mirror.lua not found at: {:?}", source);
} }
} }

View File

@@ -1,4 +1,4 @@
-- mimic.lua (总控制台) -- mirror.lua (总控制台)
-- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools) -- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools)
local base_dir = __SHIM_DIR__ local base_dir = __SHIM_DIR__

View File

@@ -36,7 +36,7 @@ impl Layout {
let bin_dir = base_dir.join("bin"); let bin_dir = base_dir.join("bin");
let tools_dir = base_dir.join("tools"); 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 { Ok(Self {
base_dir, base_dir,

View File

@@ -4,7 +4,7 @@ mod layout;
mod loader; mod loader;
mod logger; mod logger;
mod runtime; mod runtime;
mod shim; mod mirror;
mod spec; mod spec;
pub mod sys; pub mod sys;
mod utils; mod utils;
@@ -12,8 +12,8 @@ mod validators;
pub use layout::Layout; pub use layout::Layout;
pub use runtime::LuaRuntime; pub use runtime::LuaRuntime;
pub use shim::Shim; pub use mirror::Mirror;
pub use spec::ShimSpec; pub use spec::MirrorSpec;
// pub use sys::{ // pub use sys::{
// ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG, // ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG,
// EXIT_PROG_TERMINATED, execute_elevated, set_console_ctrl_handler, // EXIT_PROG_TERMINATED, execute_elevated, set_console_ctrl_handler,

View File

@@ -1,4 +1,4 @@
use crate::spec::ShimSpec; use crate::spec::MirrorSpec;
use crate::validators::LuaValidator; use crate::validators::LuaValidator;
use crate::{Layout, LuaRuntime}; use crate::{Layout, LuaRuntime};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
@@ -16,7 +16,7 @@ pub enum Source {
pub struct SpecLoader; pub struct SpecLoader;
impl SpecLoader { impl SpecLoader {
pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result<ShimSpec> { pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result<MirrorSpec> {
match source { match source {
Source::Lua(r) => Self::resolve_lua_spec(&r, layout, target_name), Source::Lua(r) => Self::resolve_lua_spec(&r, layout, target_name),
Source::Json => Self::load_from_json(layout), Source::Json => Self::load_from_json(layout),
@@ -27,13 +27,13 @@ impl SpecLoader {
runtime: &LuaRuntime, runtime: &LuaRuntime,
layout: &Layout, layout: &Layout,
target_name: &str, target_name: &str,
) -> Result<ShimSpec> { ) -> Result<MirrorSpec> {
// 策略 1: 尝试加载全局配置文件 mimic.lua // 策略 1: 尝试加载全局配置文件 mirror.lua
let global_config = layout.base_dir.join("rshim.lua"); let global_config = layout.base_dir.join("mirror.lua");
// if !global_config.is_file() { // if !global_config.is_file() {
// bail!( // bail!(
// "未找到主配置文件: [{}],请确保在安装根目录创建 rshim.lua", // "未找到主配置文件: [{}],请确保在安装根目录创建 mirror.lua",
// global_config.display() // global_config.display()
// ); // );
// } // }
@@ -42,12 +42,12 @@ impl SpecLoader {
trace!(path = %global_config.display(), "发现全局配置文件,尝试解析"); trace!(path = %global_config.display(), "发现全局配置文件,尝试解析");
let root_table: Table = runtime.eval_script(&global_config)?; let root_table: Table = runtime.eval_script(&global_config)?;
// 检查 mimic.lua 中是否存在以 target_name 命名的 Table 节点 // 检查 mirror.lua 中是否存在以 target_name 命名的 Table 节点
if root_table if root_table
.contains_key(target_name) .contains_key(target_name)
.context("检查全局配置失败")? .context("检查全局配置失败")?
{ {
let target_val: ShimSpec = root_table let target_val: MirrorSpec = root_table
.get(target_name) .get(target_name)
.with_context(|| format!("解析配置 [{}] 失败: ", target_name))?; .with_context(|| format!("解析配置 [{}] 失败: ", target_name))?;
debug!( debug!(
@@ -73,7 +73,7 @@ impl SpecLoader {
if config_path.is_file() { if config_path.is_file() {
debug!(path = %config_path.display(), "找到独立配置文件,开始加载"); debug!(path = %config_path.display(), "找到独立配置文件,开始加载");
// 直接泛型反序列化为 ShimConfig // 直接泛型反序列化为 ShimConfig
return runtime.eval_script::<ShimSpec>(config_path); return runtime.eval_script::<MirrorSpec>(config_path);
} }
trace!(path = %config_path.display(), "独立配置文件不存在,跳过"); trace!(path = %config_path.display(), "独立配置文件不存在,跳过");
} }
@@ -81,14 +81,14 @@ impl SpecLoader {
// 策略 3: 所有查找失败,抛出错误 // 策略 3: 所有查找失败,抛出错误
warn!(target = %target_name, "未找到任何匹配的配置文件"); warn!(target = %target_name, "未找到任何匹配的配置文件");
bail!( bail!(
"未找到关于 '{}' 的配置。请检查 mimic.lua 或特定的 {}.lua 文件", "未找到关于 '{}' 的配置。请检查 mirror.lua 或特定的 {}.lua 文件",
target_name, target_name,
target_name target_name
); );
} }
/// 从 Lua 脚本文件加载 ShimSpec /// 从 Lua 脚本文件加载 ShimSpec
fn load_from_json(layout: &Layout) -> Result<ShimSpec> { fn load_from_json(layout: &Layout) -> Result<MirrorSpec> {
todo!("实现json来源") todo!("实现json来源")
} }

View File

@@ -1,5 +1,5 @@
use rshim::sys::*; use mirror::sys::*;
use rshim::Shim; use mirror::Mirror;
use std::{env, process::exit}; use std::{env, process::exit};
use tracing_subscriber::{fmt, EnvFilter}; use tracing_subscriber::{fmt, EnvFilter};
@@ -15,7 +15,7 @@ fn main() {
set_console_ctrl_handler(); set_console_ctrl_handler();
// 3. 解析调用参数与代理 Shim 配置 // 3. 解析调用参数与代理 Shim 配置
let calling_args: Vec<_> = env::args_os().skip(1).collect(); let calling_args: Vec<_> = env::args_os().skip(1).collect();
let shim = match Shim::load() { let shim = match Mirror::load() {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
eprintln!("加载代理(shim)配置时发生错误: {}", e); eprintln!("加载代理(shim)配置时发生错误: {}", e);

79
src/mirror.rs Normal file
View File

@@ -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<Self> {
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(&current_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<I, S>(&self, raw_args: I) -> Vec<OsString>
where
I: IntoIterator<Item = S>,
S: Into<OsString>,
{todo!()
// self.spec.resolve_args(raw_args)
}
pub fn to_command<I, S>(&self, runtime_args: I) -> Command
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
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
}
}

View File

@@ -1,4 +1,4 @@
use crate::{ShimSpec, Layout}; use crate::{MirrorSpec, Layout};
use anyhow::{Context, Result, anyhow, bail}; use anyhow::{Context, Result, anyhow, bail};
use mlua::{FromLua, Lua, StdLib, Table, Value}; use mlua::{FromLua, Lua, StdLib, Table, Value};
use std::ffi::OsStr; use std::ffi::OsStr;
@@ -86,7 +86,7 @@ impl LuaRuntime {
} }
// 4. 包装 require配置模块缺失/加载失败时记录日志并跳过该条目, // 4. 包装 require配置模块缺失/加载失败时记录日志并跳过该条目,
// 而不是让整个 mimic.lua 解析失败(排查问题时日志可见) // 而不是让整个 mirror.lua 解析失败(排查问题时日志可见)
let original_require: mlua::Function = globals let original_require: mlua::Function = globals
.get("require") .get("require")
.context("获取内置 require 函数失败")?; .context("获取内置 require 函数失败")?;
@@ -227,7 +227,7 @@ mod tests {
// 复现用户场景PATH = { base_dir .. "/tools/numa", get_env("PATH") } // 复现用户场景PATH = { base_dir .. "/tools/numa", get_env("PATH") }
// 宿主 PATH 即使含双引号,也应拆分后正常拼接,而不是报错 // 宿主 PATH 即使含双引号,也应拆分后正常拼接,而不是报错
let runtime = LuaRuntime::new(&test_layout()).unwrap(); let runtime = LuaRuntime::new(&test_layout()).unwrap();
let cfg: ShimSpec = runtime let cfg: MirrorSpec = runtime
.lua .lua
.load( .load(
r#" r#"

View File

@@ -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<ShimSpec> {
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(&current_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)
}
}

View File

@@ -1,6 +1,6 @@
use crate::validators::{JsonValidator, LuaValidator}; use crate::validators::{JsonValidator, LuaValidator};
use mlua::{FromLua, Lua, ObjectLike, Value}; use mlua::{FromLua, Lua, ObjectLike, Value};
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::ffi::OsString; use std::ffi::OsString;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
@@ -18,38 +18,94 @@ fn conversion_error(message: impl Into<String>) -> mlua::Error {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ShimSpec { pub struct MirrorSpec {
pub target: PathBuf, pub target: PathBuf,
pub args: Vec<OsString>, pub args: Vec<OsString>,
pub aliases: HashMap<String, Vec<OsString>>,
pub env: HashMap<String, OsString>, pub env: HashMap<String, OsString>,
} }
impl ShimSpec { impl MirrorSpec {
/// 根据配置快速构建准备执行的 Command 对象 /// 根据配置快速构建准备执行的 Command 对象
pub fn to_command<I, S>(&self, runtime_args: I) -> Command // pub fn to_command<I, S>(&self, runtime_args: I) -> Command
// where
// I: IntoIterator<Item = S>,
// S: AsRef<std::ffi::OsStr>,
// {
// 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<I, S>(&self, raw_args: I) -> Result<Vec<OsString>, String>
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>, S: Into<OsString>,
{ {
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) for arg in raw_args.into_iter().map(|s| s.into()) {
cmd.args(&self.args); self.expand_arg_recursive(&arg, &mut visited_stack, &mut final_args)?;
// 2. 透传外部动态运行时参数
cmd.args(runtime_args);
for (key, val) in &self.env {
// 直接应用环境变量Lua 端已经处理好字符串拼接或列表合并)
cmd.env(key, val);
} }
cmd Ok(final_args)
}
/// 递归展开核心函数带visited栈死环拦截
fn expand_arg_recursive(
&self,
arg: &OsString,
visited: &mut HashSet<String>,
out: &mut Vec<OsString>,
) -> 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 转换 /// 实现 FromLua Trait由 mlua 自动处理 Table 转换
impl FromLua for ShimSpec { impl FromLua for MirrorSpec {
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> { fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
// 脚本返回必须是一个 Table 变体 // 脚本返回必须是一个 Table 变体
let table = match value { let table = match value {
@@ -71,6 +127,7 @@ impl FromLua for ShimSpec {
}; };
// 可选字段: args缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) // 可选字段: args缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
let args = match table.get::<Option<Value>>("args")? { let args = match table.get::<Option<Value>>("args")? {
None | Some(Value::Nil) => Vec::new(), None | Some(Value::Nil) => Vec::new(),
Some(args_val) => LuaValidator::parse_args(&args_val)?, Some(args_val) => LuaValidator::parse_args(&args_val)?,
@@ -81,13 +138,18 @@ impl FromLua for ShimSpec {
None | Some(Value::Nil) => HashMap::new(), None | Some(Value::Nil) => HashMap::new(),
Some(env_val) => LuaValidator::parse_env(&env_val)?, Some(env_val) => LuaValidator::parse_env(&env_val)?,
};
let aliases = match table.get::<Option<Value>>("aliases")? {
None | Some(Value::Nil) => HashMap::new(),
Some(env_val) => LuaValidator::parse_aliases(&env_val)?,
}; };
println!("环境变量结果:{:?}", env); 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; type Error = anyhow::Error;
fn try_from(json_str: &str) -> Result<Self> { fn try_from(json_str: &str) -> Result<Self> {
@@ -117,10 +179,14 @@ impl TryFrom<&str> for ShimSpec {
Some(env_val) => JsonValidator::parse_env(env_val)?, Some(env_val) => JsonValidator::parse_env(env_val)?,
None => HashMap::new(), None => HashMap::new(),
}; };
let aliases = match map.get("aliases") {
Ok(ShimSpec { Some(env_val) => JsonValidator::parse_aliases(env_val)?,
None => HashMap::new(),
};
Ok(MirrorSpec {
target, target,
args, args,
aliases,
env, env,
}) })
} }

View File

@@ -1,11 +1,11 @@
use anyhow::{Context, Result, anyhow};
use mlua::{LuaString, Table, Value}; use mlua::{LuaString, Table, Value};
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::ffi::OsString; use std::ffi::OsString;
use std::fmt; use std::fmt;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use tinyjson::JsonValue; use tinyjson::JsonValue;
use anyhow::{anyhow, Context, Result};
/// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 /// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
fn conversion_error(message: impl Into<String>) -> mlua::Error { fn conversion_error(message: impl Into<String>) -> mlua::Error {
@@ -53,6 +53,7 @@ pub enum LuaValidator {
Args, Args,
/// 环境变量值:支持基础标量及多维嵌套 Table递归展平 /// 环境变量值:支持基础标量及多维嵌套 Table递归展平
Env, Env,
Aliases,
} }
impl fmt::Display for LuaValidator { impl fmt::Display for LuaValidator {
@@ -61,6 +62,7 @@ impl fmt::Display for LuaValidator {
Self::Target => write!(f, "目标路径 (target)"), Self::Target => write!(f, "目标路径 (target)"),
Self::Args => write!(f, "命令行参数 (args)"), Self::Args => write!(f, "命令行参数 (args)"),
Self::Env => write!(f, "环境变量 (env)"), Self::Env => write!(f, "环境变量 (env)"),
Self::Aliases => write!(f, "命令行别名 (aliases)"),
} }
} }
} }
@@ -69,7 +71,7 @@ impl LuaValidator {
match item { match item {
Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => Ok(()), Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => Ok(()),
Value::Table(tbl) => match self { 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!( Self::Args => Err(conversion_error(format!(
"{} 仅支持一维数组,不能包含嵌套 Table,索引位置: {index}", "{} 仅支持一维数组,不能包含嵌套 Table,索引位置: {index}",
self self
@@ -181,6 +183,23 @@ impl LuaValidator {
} }
Ok(()) 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 { impl LuaValidator {
/// 解析并校验 `target` /// 解析并校验 `target`
@@ -203,25 +222,33 @@ impl LuaValidator {
/// 解析并校验 `args` /// 解析并校验 `args`
pub fn parse_args(value: &Value) -> mlua::Result<Vec<OsString>> { pub fn parse_args(value: &Value) -> mlua::Result<Vec<OsString>> {
println!("跟踪args1{}", value.type_name()); {
let ctx = Self::Args; println!("跟踪args1{}", value.type_name());
match value { #[cfg(not(feature = "args"))]
Value::Table(tbl) => { Ok(Vec::new())
ctx.validate_sequence_table(tbl)?; }
#[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(); let mut raw_parts = Vec::new();
Self::collect_value_into(&ctx, value, &mut raw_parts)?; Self::collect_value_into(&ctx, value, &mut raw_parts)?;
for part in &raw_parts { for part in &raw_parts {
Self::ensure_no_nul(&ctx, &part)?; 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, name: &str,
raw_val: &Value, raw_val: &Value,
) -> mlua::Result<OsString> { ) -> mlua::Result<OsString> {
// Self::Env.validate(&raw_val)?; context.parse_raw_val(name, 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();
let ctx = format!("{} [{}]", context, name);
let mut parts = Vec::new();
Self::collect_value_into(&ctx, raw_val, &mut parts)?; Self::collect_value_into(&ctx, raw_val, &mut parts)?;
// 校验每个展开元素的 NUL 字符 // 校验每个展开元素的 NUL 字符
for part in &parts { for part in &parts {
Self::ensure_no_nul(&name, part)?; Self::ensure_no_nul(&name, part)?;
} }
// 3. 使用系统路径分隔符拼接数组列表 // 3. 使用系统路径分隔符拼接数组列表
let joined_os_str = std::env::join_paths(parts).map_err(|e| { let joined_os_str = std::env::join_paths(parts).map_err(|e| {
conversion_error(format!("{} 的值无法用系统路径分隔符拼接: {}", ctx, 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<Vec<OsString>> {
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<HashMap<String, Vec<OsString>>> {
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<String, Vec<OsString>> = HashMap::new();
for pair in table.pairs::<Value, Value>() {
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<String, Vec<OsString>> =
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<String, Vec<OsString>>,
visited_stack: &mut HashSet<String>,
) -> 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<String, Vec<OsString>>,
out: &mut Vec<OsString>,
) {
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") //当前JsonValidator还为完全可用后期会对其lua配置特别需要处理动态函数生成的值如get_env("PATH")
// 数据结构:{ // 数据结构:{
// "target": "C:/tools/git.exe", // "target": "C:/tools/git.exe",
@@ -353,7 +484,6 @@ impl LuaValidator {
pub struct JsonValidator; pub struct JsonValidator;
impl JsonValidator { impl JsonValidator {
/// 校验并解析 target (必填,非空字符串) /// 校验并解析 target (必填,非空字符串)
pub fn parse_target(val: &JsonValue) -> Result<PathBuf> { pub fn parse_target(val: &JsonValue) -> Result<PathBuf> {
let s: &String = val let s: &String = val
@@ -368,9 +498,8 @@ impl JsonValidator {
/// 校验并解析 args (选填,必须为字符串数组) /// 校验并解析 args (选填,必须为字符串数组)
pub fn parse_args(val: &JsonValue) -> Result<Vec<OsString>> { pub fn parse_args(val: &JsonValue) -> Result<Vec<OsString>> {
let args_vec: &Vec<JsonValue> = val let args_vec: &Vec<JsonValue> =
.get() val.get().ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?;
.ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?;
args_vec args_vec
.iter() .iter()
@@ -407,7 +536,9 @@ impl JsonValidator {
Ok(result) Ok(result)
} }
pub fn parse_aliases(val: &JsonValue) -> Result<HashMap<String, Vec<OsString>>> {
todo!()
}
// ========================================================================= // =========================================================================
// 私有辅助方法 // 私有辅助方法
// ========================================================================= // =========================================================================
@@ -439,15 +570,13 @@ impl JsonValidator {
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::ShimSpec; use crate::MirrorSpec;
use mlua::{FromLua, Lua}; use mlua::{FromLua, Lua};
fn parse(src: &str) -> mlua::Result<ShimSpec> { fn parse(src: &str) -> mlua::Result<MirrorSpec> {
let lua = Lua::new(); let lua = Lua::new();
// 模拟 runtime 注入的 get_env返回按平台分隔符拆分的段数组空变量返回空表 // 模拟 runtime 注入的 get_env返回按平台分隔符拆分的段数组空变量返回空表
let get_env = lua let get_env = lua
@@ -466,7 +595,7 @@ mod tests {
lua.globals().set("get_env", get_env).unwrap(); lua.globals().set("get_env", get_env).unwrap();
let value = lua.load(src).eval::<Value>()?; let value = lua.load(src).eval::<Value>()?;
let t = ShimSpec::from_lua(value, &lua); let t = MirrorSpec::from_lua(value, &lua);
println!("读取出的数据:{:?}", t.clone()?); println!("读取出的数据:{:?}", t.clone()?);
t t
} }
@@ -483,7 +612,25 @@ mod tests {
HOME = "C:/tools/home", HOME = "C:/tools/home",
CONST = 3, CONST = 3,
BOOL = true, 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;"), || path.starts_with("C:/tools/git/bin;C:/Windows;"),
"unexpected PATH: {path}" "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::<OsString>::new());
// nil 键在遍历表时会被当作空或不存在,不产生 key 或值为空 Vec
assert!(
cfg.aliases
.get("empty_alias")
.map_or(true, |v| v.is_empty())
);
} }
#[test] #[test]