refactor: 新增别名解析流程
- 封装通用标量/数组校验与提取方法 - 将别名解析拆分为数据清洗、循环引用检测与拓扑展开三阶段 - 重命名项目为 mirror - 其他优化
This commit is contained in:
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
tinyjson="2.5.1"
|
||||
|
||||
|
||||
[features]
|
||||
default = []
|
||||
args=[]
|
||||
12
build.rs
12
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- mimic.lua (总控制台)
|
||||
-- mirror.lua (总控制台)
|
||||
-- __SHIM_DIR__ 已经在 Rust 中注入完毕,并且是正斜杠格式 (例如 C:/Tools)
|
||||
local base_dir = __SHIM_DIR__
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ShimSpec> {
|
||||
pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result<MirrorSpec> {
|
||||
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<ShimSpec> {
|
||||
// 策略 1: 尝试加载全局配置文件 mimic.lua
|
||||
let global_config = layout.base_dir.join("rshim.lua");
|
||||
) -> Result<MirrorSpec> {
|
||||
// 策略 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::<ShimSpec>(config_path);
|
||||
return runtime.eval_script::<MirrorSpec>(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<ShimSpec> {
|
||||
fn load_from_json(layout: &Layout) -> Result<MirrorSpec> {
|
||||
todo!("实现json来源")
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
79
src/mirror.rs
Normal file
79
src/mirror.rs
Normal 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(¤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<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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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#"
|
||||
|
||||
37
src/shim.rs
37
src/shim.rs
@@ -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(¤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)
|
||||
}
|
||||
}
|
||||
108
src/spec.rs
108
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<String>) -> mlua::Error {
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ShimSpec {
|
||||
pub struct MirrorSpec {
|
||||
pub target: PathBuf,
|
||||
pub args: Vec<OsString>,
|
||||
pub aliases: HashMap<String, Vec<OsString>>,
|
||||
pub env: HashMap<String, OsString>,
|
||||
|
||||
}
|
||||
|
||||
impl ShimSpec {
|
||||
impl MirrorSpec {
|
||||
/// 根据配置快速构建准备执行的 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
|
||||
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)
|
||||
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<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 转换
|
||||
impl FromLua for ShimSpec {
|
||||
impl FromLua for MirrorSpec {
|
||||
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
||||
// 脚本返回必须是一个 Table 变体
|
||||
let table = match value {
|
||||
@@ -71,6 +127,7 @@ impl FromLua for ShimSpec {
|
||||
};
|
||||
|
||||
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
|
||||
|
||||
let args = match table.get::<Option<Value>>("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::<Option<Value>>("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<Self> {
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<String>) -> 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<Vec<OsString>> {
|
||||
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<OsString> {
|
||||
// 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<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")
|
||||
// 数据结构:{
|
||||
// "target": "C:/tools/git.exe",
|
||||
@@ -353,7 +484,6 @@ impl LuaValidator {
|
||||
pub struct JsonValidator;
|
||||
|
||||
impl JsonValidator {
|
||||
|
||||
/// 校验并解析 target (必填,非空字符串)
|
||||
pub fn parse_target(val: &JsonValue) -> Result<PathBuf> {
|
||||
let s: &String = val
|
||||
@@ -368,9 +498,8 @@ impl JsonValidator {
|
||||
|
||||
/// 校验并解析 args (选填,必须为字符串数组)
|
||||
pub fn parse_args(val: &JsonValue) -> Result<Vec<OsString>> {
|
||||
let args_vec: &Vec<JsonValue> = val
|
||||
.get()
|
||||
.ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?;
|
||||
let args_vec: &Vec<JsonValue> =
|
||||
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<HashMap<String, Vec<OsString>>> {
|
||||
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<ShimSpec> {
|
||||
fn parse(src: &str) -> mlua::Result<MirrorSpec> {
|
||||
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::<Value>()?;
|
||||
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::<OsString>::new());
|
||||
// nil 键在遍历表时会被当作空或不存在,不产生 key 或值为空 Vec
|
||||
assert!(
|
||||
cfg.aliases
|
||||
.get("empty_alias")
|
||||
.map_or(true, |v| v.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user