refactor: 迁移 winapi 到 windows-sys,修复配置解析漏洞

- 依赖替换为 windows-sys 0.61,main.rs 全面适配新 API
  - 配置解析错误显式传播:env/args 类型错误、数组空洞、非法键不再静默
  吞错
  - 修正空环境变量与空参数语义,补充 UTF-8 与路径拼接校验
  - require 容错移至 Rust 侧,模块加载失败记录日志并跳过
  - 新增配置解析与运行时单元测试(19 个)
This commit is contained in:
2026-08-14 20:09:34 +08:00
parent ed5439eaa1
commit 5e69a6a980
13 changed files with 691 additions and 278 deletions

View File

@@ -1,18 +1,13 @@
use crate::error::ShimError;
use crate::{ShimConfig, ShimEnv};
use crate::{ShimConfig, ShimLayout};
use mlua::{FromLua, Lua, StdLib, Table, Value};
use std::path::Path;
use std::{env, fs};
/// 将 Path 转换为适合 Lua 使用的安全字符串路径
fn normalize_path_for_lua(path: &Path) -> String {
let path_str = path.to_string_lossy();
// 1. 剥离 Windows UNC 规范路径前缀 (\\?\)
let clean_str = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str);
// 2. 将反斜杠转换成正斜杠(在非 UNC 路径下Windows 和 Lua 均完美支持 /
// 这样既避免了 Lua 字符串转义隐患,又不会破坏 Windows 路径
clean_str.replace('\\', "/")
// 自动将 Windows UNC 规范路径转回传统路径
let simplified = dunce::simplified(path);
simplified.to_string_lossy().replace('\\', "/")
}
pub struct LuaRuntime {
@@ -21,66 +16,147 @@ pub struct LuaRuntime {
impl LuaRuntime {
/// 初始化限定权限的 Lua 沙箱环境
pub fn new(shim_env: &ShimEnv) -> Result<Self, ShimError> {
pub fn new(layout: &ShimLayout) -> Result<Self, ShimError> {
// 只加载安全的标准库,剥离 os / io 等风险模块
let lua = Lua::new_with(
StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE,
mlua::LuaOptions::default(),
)
.map_err(|e| ShimError::EnvError(format!("初始化 Lua 失败: {}", e)))?;
.map_err(|e| ShimError::Environment(format!("初始化 Lua 失败: {}", e)))?;
let globals = lua.globals();
// 统一使用 POSIX 风格路径规范化路径字符串
let root_dir_str = normalize_path_for_lua(&shim_env.root_dir);
let tools_dir_str = normalize_path_for_lua(&shim_env.tools_dir);
let root_dir = normalize_path_for_lua(&layout.root_dir);
let tools_dir = normalize_path_for_lua(&layout.tools_dir);
// 1. 注入锚点变量
// 1. 注入锚点变量 __SHIM_DIR__shim 安装根目录)
globals
.set("__SHIM_DIR__", root_dir_str.clone())
.map_err(|e| ShimError::EnvError(e.to_string()))?;
.set("__SHIM_DIR__", root_dir.clone())
.map_err(|e| ShimError::Environment(e.to_string()))?;
// 2. 安全暴露 get_env 供配置读取环境变量
let get_env = lua
.create_function(|_, key: String| -> mlua::Result<String> {
Ok(env::var(key).unwrap_or_default())
})
.map_err(|e| ShimError::EnvError(e.to_string()))?;
.map_err(|e| ShimError::Environment(e.to_string()))?;
globals
.set("get_env", get_env)
.map_err(|e| ShimError::EnvError(e.to_string()))?;
.map_err(|e| ShimError::Environment(e.to_string()))?;
// 3. 配置 package.path确保 require 行为正常
if let Ok(package) = globals.get::<Table>("package") {
let _ = package.set("cpath", "");
let _ = package.set("loadlib", Value::Nil);
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
path, root_dir, root_dir, tools_dir, tools_dir
);
let _ = package.set("path", new_path);
}
}
// 4. 包装 require配置模块缺失/加载失败时记录日志并跳过该条目,
// 而不是让整个 shims.lua 解析失败(排查问题时日志可见)
let original_require: mlua::Function = globals
.get("require")
.map_err(|e| ShimError::Environment(format!("获取 require 失败: {}", e)))?;
globals
.set("_rshim_original_require", &original_require)
.map_err(|e| ShimError::Environment(e.to_string()))?;
let wrapped_require = lua
.create_function(|lua, module: String| -> mlua::Result<Value> {
let original: mlua::Function = lua.globals().get("_rshim_original_require")?;
match original.call::<Value>(module.clone()) {
Ok(value) => Ok(value),
Err(e) => {
tracing::warn!(
module = %module,
error = %e,
"配置模块加载失败,已跳过该条目(可在独立配置文件中定义)"
);
Ok(Value::Nil)
}
}
})
.map_err(|e| ShimError::Environment(e.to_string()))?;
globals
.set("require", wrapped_require)
.map_err(|e| ShimError::Environment(e.to_string()))?;
Ok(Self { lua })
}
/// 执行指定脚本文件,直接返回完整的 Lua Table
pub fn evaluate_lua_script(&self, path: &Path) -> Result<Table, ShimError> {
let code = fs::read_to_string(path)?;
pub fn eval_script<T: FromLua>(&self, path: impl AsRef<Path>) -> Result<T, ShimError> {
let path = path.as_ref();
let bytes = fs::read(path)?;
let code = String::from_utf8(bytes).map_err(|e| {
ShimError::InvalidConfig(format!(
"{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码): {}",
path.display(),
e.utf8_error()
))
})?;
let chunk_name = format!("@{}", path.display());
self.lua
.load(&code)
.set_name(path.to_string_lossy())
.eval::<Table>()
.map_err(|e| ShimError::LuaExecutionError {
.set_name(&chunk_name)
.eval::<T>()
.map_err(|e| ShimError::LuaExecution {
file: path.display().to_string(),
source: e,
})
}
/// 将 Lua Value 解析转化为 ShimConfig 数据对象
pub fn parse_config(&self, value: Value) -> Result<ShimConfig, ShimError> {
ShimConfig::from_lua(value, &self.lua).map_err(|e| ShimError::InvalidConfig(e.to_string()))
// /// 将 Lua Value 解析转化为 ShimConfig 数据对象
// pub fn parse_config(&self, value: Value) -> Result<ShimConfig, ShimError> {
// ShimConfig::from_lua(value, &self.lua).map_err(|e| ShimError::InvalidConfig(e.to_string()))
// }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ShimLayout;
fn test_layout() -> ShimLayout {
let root = std::env::temp_dir().join("rshim-test-layout");
ShimLayout {
root_dir: root.clone(),
bin_dir: root.join("bin"),
tools_dir: root.join("tools"),
}
}
#[test]
fn missing_module_require_returns_nil() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let value: Value = runtime
.lua
.load(r#"return require("rshim_test_no_such_module")"#)
.eval()
.unwrap();
assert!(matches!(value, Value::Nil));
}
#[test]
fn builtin_module_require_still_works() {
let runtime = LuaRuntime::new(&test_layout()).unwrap();
let value: Value = runtime
.lua
.load(r#"return pcall(require, "string")"#)
.eval()
.unwrap();
assert!(matches!(value, Value::Boolean(true)));
}
}