fix(shim): 优化配置解析、跨平台编码与 UAC 提权逻辑

- 使用 `OsString`/`PathBuf` 替代 `String`,实现跨平台无损编码与命令行参数透传
- 完善 `FromLua` 转换与边界校验,拦截空洞 (`nil`) 及非整数键
- 修复环境变量拼接时由于内置分隔符导致的 `join_paths` 报错
- 优化 Windows 下 UAC 提权执行逻辑,改用宽字符 API (`ShellExecuteExW`)
This commit is contained in:
2026-08-18 11:15:30 +08:00
parent 5e69a6a980
commit e3cb065b35
7 changed files with 370 additions and 90 deletions

View File

@@ -1,6 +1,7 @@
use crate::error::ShimError;
use crate::{ShimConfig, ShimLayout};
use mlua::{FromLua, Lua, StdLib, Table, Value};
use std::ffi::OsStr;
use std::path::Path;
use std::{env, fs};
/// 将 Path 转换为适合 Lua 使用的安全字符串路径
@@ -36,9 +37,36 @@ impl LuaRuntime {
.map_err(|e| ShimError::Environment(e.to_string()))?;
// 2. 安全暴露 get_env 供配置读取环境变量
// 返回按平台路径分隔符拆分后的段数组(自动剥离引号包裹),
// 便于 PATH 等列表变量直接嵌入数组PATH = { prefix, get_env("PATH") }
let get_env = lua
.create_function(|_, key: String| -> mlua::Result<String> {
Ok(env::var(key).unwrap_or_default())
.create_function(|lua, key: String| -> mlua::Result<Table> {
// 缺失变量视为空字符串,拆分后得到空表(不贡献任何路径段)
let value = env::var_os(key).unwrap_or_default();
// 空输入返回空表否则按平台分隔符拆分split_paths 会剥离引号包裹)
if value.is_empty() {
return lua.create_table();
}
let table = lua.create_table()?;
for (i, p) in env::split_paths(&value).enumerate() {
// 3. 跨平台提取原始字节并转为 LuaString保证 100% 无损
#[cfg(unix)]
let lua_str = {
use std::os::unix::ffi::OsStrExt;
lua.create_string(p.as_os_str().as_bytes())?
};
#[cfg(windows)]
let lua_str = {
// Windows 路径是 UTF-16转成字符串或保持其字节表达
let s = p.to_string_lossy();
lua.create_string(s.as_bytes())?
};
table.set(i + 1, lua_str)?;
}
Ok(table)
})
.map_err(|e| ShimError::Environment(e.to_string()))?;
@@ -96,7 +124,7 @@ impl LuaRuntime {
/// 执行指定脚本文件,直接返回完整的 Lua Table
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T, ShimError> {
let path = path.as_ref();
// println!("path {:?}", path);
let bytes = fs::read(path)?;
let code = String::from_utf8(bytes).map_err(|e| {
ShimError::InvalidConfig(format!(
@@ -105,7 +133,7 @@ impl LuaRuntime {
e.utf8_error()
))
})?;
println!("code {:?}", code);
let chunk_name = format!("@{}", path.display());
self.lua
@@ -159,4 +187,98 @@ mod tests {
.unwrap();
assert!(matches!(value, Value::Boolean(true)));
}
#[test]
fn get_env_returns_split_table() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let value: Value = runtime
.lua
.load(r#"return get_env("PATH")"#)
.eval()
.unwrap();
let table = match value {
Value::Table(t) => t,
other => panic!("expected table, got {}", other.type_name()),
};
assert!(
table.raw_len() >= 1,
"PATH should have at least one segment"
);
// split_paths 会剥离引号包裹,拆分段不应再含双引号
for i in 1..=table.raw_len() {
let seg: String = table.raw_get(i).unwrap();
assert!(
!seg.contains('"'),
"segment should not contain quote: {seg:?}"
);
}
}
#[test]
fn get_env_missing_returns_empty_table() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let value: Value = runtime
.lua
.load(r#"return get_env("RSHIM_TEST_NO_SUCH_VAR_12345")"#)
.eval()
.unwrap();
match value {
Value::Table(t) => assert_eq!(t.raw_len(), 0),
other => panic!("expected table, got {}", other.type_name()),
}
}
#[test]
fn path_with_get_env_joins_without_quote_error() {
// 复现用户场景PATH = { base_dir .. "/tools/numa", get_env("PATH") }
// 宿主 PATH 即使含双引号,也应拆分后正常拼接,而不是报错
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let cfg: ShimConfig = runtime
.lua
.load(
r#"
return {
target = __SHIM_DIR__ .. "/tools/numa/numa.exe",
args = { "--help" },
env = {
PATH = { __SHIM_DIR__ .. "/tools/numa", get_env("PATH") }
}
}
"#,
)
.eval()
.unwrap();
let path = cfg.env.get("PATH").unwrap().to_str().unwrap();
let prefix = std::env::temp_dir()
.join("rshim-test-layout")
.to_string_lossy()
.replace('\\', "/")
+ "/tools/numa;";
assert!(path.starts_with(&prefix), "unexpected PATH: {path}");
// 宿主 PATH 的段应被附加在配置前缀之后
let host = std::env::var("PATH").unwrap_or_default();
if !host.is_empty() {
let host_first = std::env::split_paths(&host)
.next()
.unwrap()
.to_string_lossy()
.into_owned();
assert!(
path.contains(&host_first),
"missing host PATH segment: {host_first}"
);
}
// 宿主 PATH 里引号包裹的畸形段(如 "D:\\...\\bin;")应被原样保留,
// 而不是让整个配置加载失败
if host.contains('"') {
assert!(
path.contains('"'),
"quoted host segments should be preserved"
);
}
}
}