refactor: 配置文件引擎迁移至 mlua 并优化环境变量注入

- 配置文件解析引擎由 shim 替换为 mlua (Lua 5.4)
- 优化路由策略:统一以根目录 shims.lua 为入口,支持 tools/ 目录模块化降级探测
- 自动注入 __SHIM_DIR__ 绝对路径与 package.path 模块搜索路径
- 环境变量改用 Command::envs 安全注入,并在 UAC 提权时透传
This commit is contained in:
2026-08-12 20:19:50 +08:00
parent 7934dde74d
commit 4aa619f07e
6 changed files with 516 additions and 105 deletions

View File

@@ -1,89 +1,166 @@
use fs_err as fs;
use mlua::{Error as LuaError, Lua, Table, Value};
use std::{
collections::HashMap,
env,
env, fs,
io::{Error, ErrorKind},
path::{Path, PathBuf},
};
pub struct Shim {
pub target_path: PathBuf,
pub args: Option<Vec<String>>,
pub envs: Option<HashMap<String, String>>,
}
impl Shim {
pub fn init() -> Result<Self, Error> {
let shim_path = get_shim_file_path()?;
let kvs = parse_shim_file(&shim_path)?;
let target_path = match kvs.get("path") {
Some(p) => PathBuf::from(p),
None => {
return Err(Error::new(
ErrorKind::NotFound,
format!("no path key in {}", shim_path.to_string_lossy()),
let (_exe_path, _exe_dir, root_dir, tools_dir, exe_name) = Self::get_exe_context()?;
let lua = Lua::new();
let globals = lua.globals();
// 1. 将根目录 (Root) 注入为 __SHIM_DIR__作为全局相对路径的锚点
let root_dir_str = root_dir.to_string_lossy().replace('\\', "/");
let tools_dir_str = tools_dir.to_string_lossy().replace('\\', "/");
globals
.set("__SHIM_DIR__", root_dir_str.clone())
.map_err(|e| Error::new(ErrorKind::Other, format!("注入 __SHIM_DIR__ 失败: {}", e)))?;
// 2. 注册 get_env 获取环境变量的函数
let get_env = lua
.create_function(|_, key: String| -> Result<String, LuaError> {
Ok(env::var(key).unwrap_or_default())
})
.map_err(|e| Error::new(ErrorKind::Other, format!("注册 get_env 失败: {}", e)))?;
globals
.set("get_env", get_env)
.map_err(|e| Error::new(ErrorKind::Other, e.to_string()))?;
// 3. 增强 package.path让 require 自动在当前目录及 conf.d 目录下寻找模块
if let Ok(package) = globals.get::<Table>("package") {
if let Ok(path) = package.get::<String>("path") {
let new_path = format!(
"{};{}/?.lua;{}/?/init.lua;{}/?.lua;{}/?/init.lua",
path, root_dir_str, root_dir_str, tools_dir_str, tools_dir_str
);
let _ = package.set("path", new_path);
}
}
Self::resolve_config(&lua, &root_dir, &tools_dir, &exe_name)
}
fn get_exe_context() -> Result<(PathBuf, PathBuf, PathBuf, PathBuf, String), Error> {
let current_exe = env::current_exe().map_err(|e| {
Error::new(
ErrorKind::Other,
format!("获取代理程序(shim)路径失败: {}", e),
)
})?;
let exe_dir = current_exe
.parent()
.unwrap_or_else(|| Path::new(""))
.to_path_buf();
// root_dir 是 bin 的上一级(项目的根目录)
let root_dir = exe_dir
.parent()
.unwrap_or_else(|| Path::new(""))
.to_path_buf();
// tools_dir 是 root 下的 tools 目录
let tools_dir = root_dir.join("tools");
let exe_name = current_exe
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_lowercase();
Ok((current_exe, exe_dir, root_dir, tools_dir, exe_name))
}
/// 执行 Lua 脚本文件并返回 Table
fn evaluate_lua_script(lua: &Lua, config_file: &Path) -> Result<Table, Error> {
let code = fs::read_to_string(config_file)?; // 这里不安全需要优化
lua.load(&code)
.set_name(config_file.to_string_lossy().into_owned())
.eval::<Table>()
.map_err(|e| {
Error::new(
ErrorKind::InvalidData,
format!("Lua 脚本解析错误 ({}): {}", config_file.display(), e),
)
})
}
/// 利用 mlua 原生类型转换,将 Lua Table 映射为 Rust Shim 结构体
fn from_table(table: Table) -> Result<Self, Error> {
let path_str: String = table
.get("path")
.map_err(|_| Error::new(ErrorKind::InvalidData, "配置文件中缺少必需的 'path' 字段"))?;
let target_path = PathBuf::from(path_str);
let args: Option<Vec<String>> = table.get("args").ok();
let envs: Option<HashMap<String, String>> = table.get("env").ok();
Ok(Self {
target_path,
args,
envs,
})
}
fn resolve_config(
lua: &Lua,
root_dir: &Path,
tools_dir: &Path,
exe_name: &str,
) -> Result<Self, Error> {
// 1. 尝试全局统一入口 shims.lua
let entry_config = root_dir.join("shims.lua");
if entry_config.exists() {
let root_table = Self::evaluate_lua_script(lua, &entry_config)?;
// 如果 shims.lua 中存在以当前程序名命名的配置项,直接接管并返回
if let Ok(Value::Table(v)) = root_table.get::<Value>(exe_name) {
return Self::from_table(v);
}
// 如果 shims.lua 存在但未接管 exe_name代码会自然“穿透”流转到下方寻找独立配置
}
// 2. 降级探测特定同名独立配置文件(优先 tools/{exe_name}.lua次选 root/{exe_name}.lua
let specific_config_tools = tools_dir.join(format!("{}.lua", exe_name));
let specific_config_root = root_dir.join(format!("{}.lua", exe_name));
let specific_config = if specific_config_tools.exists() {
Some(specific_config_tools)
} else if specific_config_root.exists() {
Some(specific_config_root)
} else {
None
};
if let Some(config_file) = specific_config {
let config_table = Self::evaluate_lua_script(lua, &config_file)?;
return if config_table.contains_key("path").unwrap_or(false) {
Self::from_table(config_table)
} else {
Err(Error::new(
ErrorKind::InvalidData,
format!(
"独立配置文件 [{}] 必须包含 'path' 字段",
config_file.display()
),
))
}
};
let args = kvs.get("args").map(|a| {
a.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>()
});
Ok(Self { target_path, args })
};
}
// 3. 两阶段探测均失败后的统一报错
Err(Error::new(
ErrorKind::NotFound,
format!(
"未找到关于 '{}' 的配置。请确保在 shims.lua 中进行了定义,或者存在独立的 {}.lua 配置文件。",
exe_name, exe_name
),
))
}
}
fn get_shim_file_path() -> Result<PathBuf, Error> {
let mut current_exe = env::current_exe().map_err(|e| {
Error::new(
ErrorKind::Other,
format!("acquiring shim executable path: {}", e),
)
})?;
if !current_exe.set_extension("shim") {
return Err(Error::new(
ErrorKind::Other,
format!("{} is not a file", current_exe.to_string_lossy()),
));
}
Ok(current_exe)
}
use unicode_bom::Bom;
fn parse_shim_file(shim_path: &Path) -> Result<HashMap<String, String>, Error> {
let mut kvs = HashMap::new();
let raw_content = fs::read_to_string(shim_path).map_err(|e| {
Error::new(
ErrorKind::Other,
format!("reading {}: {}", shim_path.to_string_lossy(), e),
)
})?;
//NOTE: expedient trick for utf-8 with bom
let bom = Bom::from(raw_content.as_bytes());
for line in raw_content[bom.len()..]
.lines()
.filter(|l| !l.trim().is_empty())
{
let mut components = line.split("=");
let key = match components.next() {
Some(k) => k.trim(),
None => {
return Err(Error::new(
ErrorKind::InvalidData,
format!("invalid line in shim file: {}", line),
));
}
};
let value = match components.next() {
Some(v) => v.trim(),
None => {
return Err(Error::new(
ErrorKind::InvalidData,
format!("invalid line in shim file: {}", line),
));
}
};
kvs.insert(key.to_string(), value.to_string());
}
Ok(kvs)
}