fix(shim): 优化配置解析、跨平台编码与 UAC 提权逻辑
- 使用 `OsString`/`PathBuf` 替代 `String`,实现跨平台无损编码与命令行参数透传 - 完善 `FromLua` 转换与边界校验,拦截空洞 (`nil`) 及非整数键 - 修复环境变量拼接时由于内置分隔符导致的 `join_paths` 报错 - 优化 Windows 下 UAC 提权执行逻辑,改用宽字符 API (`ShellExecuteExW`)
This commit is contained in:
130
src/runtime.rs
130
src/runtime.rs
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user