From f59040c11ef1a32ca6b40ec15d1126823bacbf85 Mon Sep 17 00:00:00 2001 From: CNWei Date: Wed, 19 Aug 2026 11:34:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(config):=20=E4=BC=98=E5=8C=96=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完善 `FromLua` 转换与边界校验,拦截空洞 (`nil`) 及非整数键 --- Cargo.toml | 4 +- src/config.rs | 201 ++++++++++++++++++++++++++++++------------------- src/error.rs | 26 ------- src/layout.rs | 19 ++--- src/lib.rs | 2 - src/runtime.rs | 47 ++++++------ src/shim.rs | 36 ++++----- 7 files changed, 168 insertions(+), 167 deletions(-) delete mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index c1cbe8a..c184180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ opt-level = "z" panic = "abort" [dependencies] -thiserror = "2.0.20" +anyhow = "1.0.104" -mlua = { version = "0.12.0", features = ["lua54", "vendored"] } +mlua = { version = "0.12.0", features = ["lua54", "vendored","send"] } windows-sys = { version = "0.61.2", features = [ "Win32_Foundation", "Win32_System_Com", diff --git a/src/config.rs b/src/config.rs index 900d924..a809083 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,10 @@ -use mlua::{FromLua, Lua, LuaString, ObjectLike, Table, Value}; +use mlua::{FromLua, Lua, LuaString, Table, Value}; use std::collections::HashMap; use std::ffi::OsString; +use std::fmt; +use std::fmt::Display; use std::path::PathBuf; use std::process::Command; - /// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 fn conversion_error(message: impl Into) -> mlua::Error { mlua::Error::FromLuaConversionError { @@ -13,8 +14,8 @@ fn conversion_error(message: impl Into) -> mlua::Error { } } -/// 将 Lua 字符串转为 Rust 字符串;非 UTF-8 字节替换为 U+FFFD 并告警 -fn lua_string_to_os_string(s: LuaString) -> Result { +/// 将 Lua 字符串转为 Rust 字符串;非 UTF-8 按系统默认的 ANSI/OEM (如 GBK) 进行安全解码 +fn lua_string_2_os_string(s: LuaString) -> mlua::Result { let raw_bytes = &s.as_bytes().to_vec(); // 1. Unix 平台:直接零拷贝透传原始字节(无损支持任意编码) #[cfg(unix)] @@ -42,7 +43,7 @@ fn lua_string_to_os_string(s: LuaString) -> Result { } /// 按连续整数下标遍历表的序列部分;存在空洞或非序列键时返回错误,避免静默截断 -fn for_each_sequence_item( +fn validate_each_sequence_item( tbl: &Table, mut f: impl FnMut(Value) -> mlua::Result<()>, ) -> mlua::Result<()> { @@ -70,9 +71,63 @@ fn for_each_sequence_item( } Ok(()) } +/// 值分流与上下文分发枚举 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ValueShunt { + /// 命令行参数元素:仅支持一维基础标量,禁止嵌套 Table + Args, + /// 环境变量值:支持基础标量及多维嵌套 Table(递归展平) + Env, +} +impl Display for ValueShunt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Args => write!(f, "命令行参数 (args)"), + Self::Env => write!(f, "环境变量 (env)"), + } + } +} + +impl ValueShunt { + fn expected_types_desc(&self) -> &str { + match self { + Self::Args => "string/number/boolean(不支持嵌套数组)", + Self::Env => "string/number/boolean 或包含这些类型的数组/嵌套数组", + } + } + /// 递归/标量收集实现 + pub fn collect_into(&self, value: Value, out: &mut Vec) -> mlua::Result<()> { + match value { + Value::Nil => {} + Value::String(s) => out.push(lua_string_2_os_string(s)?), + Value::Integer(i) => out.push(OsString::from(i.to_string())), + Value::Number(n) => { + tracing::warn!(context = %self, value = %n, "浮点数将按十进制格式转换为字符串"); + out.push(OsString::from(n.to_string())); + } + Value::Boolean(b) => { + tracing::warn!(context = %self, value = %b, "布尔值将转换为字符串"); + out.push(OsString::from(b.to_string())); + } + // 嵌套 Table 仅在 EnvValue 下允许递归展开(支持 get_env("PATH") 等返回的子表) + Value::Table(tbl) if *self == Self::Env => { + validate_each_sequence_item(&tbl, |item| self.collect_into(item, out))?; + } + other => { + return Err(conversion_error(format!( + "{} 不支持类型 {}(仅支持 {})", + self, + other.type_name(), + self.expected_types_desc() + ))); + } + } + Ok(()) + } +} /// 校验环境变量名的合法性(Windows 约束:非空、不含 '='、不含 NUL) -fn validate_env_key(key: &str) -> mlua::Result<()> { +fn validate_env_var_name(key: &str) -> mlua::Result<()> { if key.is_empty() { return Err(conversion_error("环境变量名不能为空")); } @@ -92,10 +147,10 @@ fn validate_env_key(key: &str) -> mlua::Result<()> { } /// 递归将任意 Lua Value 展开为扁平的字符串片段列表 -fn collect_env_strings(value: Value, out: &mut Vec) -> mlua::Result<()> { +fn collect_env_segments(value: Value, out: &mut Vec) -> mlua::Result<()> { match value { // 1. 字符串 - Value::String(s) => out.push(lua_string_to_os_string(s)?), + Value::String(s) => out.push(lua_string_2_os_string(s)?), // 2. 整数与浮点数 Value::Integer(i) => out.push(OsString::from(i.to_string())), Value::Number(n) => { @@ -108,7 +163,9 @@ fn collect_env_strings(value: Value, out: &mut Vec) -> mlua::Result<() out.push(OsString::from(b.to_string())); } // 4. 表/数组:必须是连续整数下标的纯序列,递归解包(支持任意深度的嵌套数组) - Value::Table(tbl) => for_each_sequence_item(&tbl, |item| collect_env_strings(item, out))?, + Value::Table(tbl) => { + validate_each_sequence_item(&tbl, |item| collect_env_segments(item, out))? + } // 5. 安全忽 // 略 nil Value::Nil => {} @@ -170,7 +227,7 @@ impl FromLua for ShimConfig { // 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误) let target = match table.get::("target")? { - Value::String(s) => lua_string_to_os_string(s)?, + Value::String(s) => lua_string_2_os_string(s)?, Value::Nil => { return Err(conversion_error("缺少必填字段 target(应为字符串路径)")); } @@ -181,90 +238,76 @@ impl FromLua for ShimConfig { ))); } }; + let mut args = Vec::new(); + // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) - // let args = match table.get::>("args")? { - // None => Vec::new(), - // Some(Value::Table(tbl)) => { - // let mut args = Vec::new(); - // for_each_sequence_item(&tbl, |item| match item { - // Value::String(s) => { - // args.push(lua_string_to_string(s)); - // Ok(()) - // } - // other => Err(conversion_error(format!( - // "args 数组元素必须是字符串,实际是 {}", - // other.type_name() - // ))), - // })?; - // args - // } - let args = match table.get::>("args")? { - None => Vec::new(), + match table.get::>("args")? { + None | Some(Value::Nil) => {} Some(Value::Table(tbl)) => { - let mut args = Vec::new(); - for_each_sequence_item(&tbl, |item| match item { - Value::String(s) => { - args.push(lua_string_to_os_string(s)?); - Ok(()) - } - other => Err(conversion_error(format!( - "args 数组元素必须是字符串,实际是 {}", - other.type_name() - ))), + validate_each_sequence_item(&tbl, |item| { + ValueShunt::Args.collect_into(item, &mut args) })?; - args } Some(other) => { return Err(conversion_error(format!( - "args 必须是字符串数组,实际是 {}", + "args 必须是数组列表,实际类型是 {}", other.type_name() ))); } }; // 可选字段: env(只允许缺失/nil,其他类型由 Option 转换报错,不再静默忽略) - let env_table = table.get::>("env")?; + let env_table = table.get::>("env")?; println!("env_table:{:?}", env_table); let mut env = HashMap::new(); - if let Some(env_table) = env_table { - for pair in env_table.pairs::() { - let (key, value) = pair?; - validate_env_key(&key)?; + match env_table { + None | Some(Value::Nil) => {} + Some(Value::Table(tbl)) => { + for pair in tbl.pairs::() { + let (name, value) = pair?; + validate_env_var_name(&name)?; - let mut parts = Vec::new(); - collect_env_strings(value, &mut parts)?; + let mut parts = Vec::new(); + ValueShunt::Env.collect_into(value, &mut parts)?; - // 空数组/空串也显式设置(空值表示清空该变量), - // 与“未配置该变量(继承宿主环境)”相区分 - let joined_os_str = std::env::join_paths(parts).map_err(|e| { - conversion_error(format!( - "环境变量 [{}] 的值无法用系统路径分隔符拼接: {}", - key, e - )) - })?; - println!("环境变量拼接结果:{:?}", joined_os_str); - // 检查是否包含非法 NUL 字符 - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - if joined_os_str.as_bytes().contains(&0) { - return Err(conversion_error(format!( - "环境变量 [{}] 的值不能包含 NUL 字符", - key - ))); + // 空数组/空串也显式设置(空值表示清空该变量), + // 与“未配置该变量(继承宿主环境)”相区分 + let joined_os_str = std::env::join_paths(parts).map_err(|e| { + conversion_error(format!( + "环境变量 [{}] 的值无法用系统路径分隔符拼接: {}", + name, e + )) + })?; + println!("环境变量拼接结果:{:?}", joined_os_str); + // 检查是否包含非法 NUL 字符 + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + if joined_os_str.as_bytes().contains(&0) { + return Err(conversion_error(format!( + "环境变量 [{}] 的值不能包含 NUL 字符", + key + ))); + } } - } - #[cfg(windows)] - { - use std::os::windows::ffi::OsStrExt; - if joined_os_str.encode_wide().any(|c| c == 0) { - return Err(conversion_error(format!( - "环境变量 [{}] 的值不能包含 NUL 字符", - key - ))); + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + if joined_os_str.encode_wide().any(|c| c == 0) { + return Err(conversion_error(format!( + "环境变量 [{}] 的值不能包含 NUL 字符", + name + ))); + } } - } - env.insert(key, joined_os_str); + env.insert(name, joined_os_str); + } + } + Some(other) => { + return Err(conversion_error(format!( + "env 必须是键值表 (table),实际类型是 {}", + other.type_name() + ))); } } println!("环境变量结果:{:?}", env); @@ -315,7 +358,11 @@ mod tests { PATH = { "C:/tools/git/bin", "C:/Windows",get_env("PATH") }, HOME = "C:/tools/home", CONST = 3, - BOOL = true + BOOL = true, + MUT ={ + A=3, + B=true + } } } "#, diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 16cb5dc..0000000 --- a/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use thiserror::Error; // 推荐引入 thiserror 库,若不使用可手动实现 std::fmt::Display - -#[derive(Debug, Error)] -pub enum ShimError { - #[error("路径解析失败: {0}")] - PathResolution(String), - - #[error("获取环境信息失败: {0}")] - Environment(String), - - #[error("配置文件未找到: {0}")] - ConfigMissing(String), - - #[error("Lua 运行时/语法错误 [{file}]: {source}")] - LuaExecution { - file: String, - #[source] - source: mlua::Error, - }, - - #[error("配置格式非法: {0}")] - InvalidConfig(String), - - #[error("IO 错误: {0}")] - Io(#[from] std::io::Error), -} diff --git a/src/layout.rs b/src/layout.rs index aa41040..a54cfeb 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,4 +1,4 @@ -use crate::error::ShimError; +use anyhow::{Context, Result, bail}; use std::path::{Path, PathBuf}; pub struct ShimLayout { @@ -9,27 +9,18 @@ pub struct ShimLayout { impl ShimLayout { /// 从当前可执行文件解析 shim 安装目录布局 - pub fn from_executable(exe_path: impl AsRef) -> Result { + pub fn from_executable(exe_path: impl AsRef) -> Result { let exe_path = exe_path.as_ref(); let bin_dir = exe_path .parent() - .ok_or_else(|| { - ShimError::PathResolution(format!( - "无法获取程序 [{}] 的父级 bin 目录", - exe_path.display() - )) - })? + .with_context(|| format!("无法获取程序 [{}] 的父级 bin 目录", exe_path.display()))? .to_path_buf(); + eprintln!("bin_dir目录 {}", bin_dir.display()); let root_dir = bin_dir .parent() - .ok_or_else(|| { - ShimError::PathResolution(format!( - "无法获取 bin 目录 [{}] 的父级 root 目录", - bin_dir.display() - )) - })? + .with_context(|| format!("无法获取 bin 目录 [{}] 的父级 root 目录", bin_dir.display()))? .to_path_buf(); eprintln!("root_dir 目录 {}", root_dir.display()); diff --git a/src/lib.rs b/src/lib.rs index 11507be..2f24eac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,10 @@ mod config; -mod error; mod layout; mod logger; mod runtime; mod shim; pub use config::ShimConfig; -pub use error::ShimError; pub use layout::ShimLayout; pub use runtime::LuaRuntime; pub use shim::Shim; diff --git a/src/runtime.rs b/src/runtime.rs index 0122a10..ac2f52d 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,5 +1,5 @@ -use crate::error::ShimError; use crate::{ShimConfig, ShimLayout}; +use anyhow::{Context, Result, anyhow, bail}; use mlua::{FromLua, Lua, StdLib, Table, Value}; use std::ffi::OsStr; use std::path::Path; @@ -17,13 +17,13 @@ pub struct LuaRuntime { impl LuaRuntime { /// 初始化限定权限的 Lua 沙箱环境 - pub fn new(layout: &ShimLayout) -> Result { + pub fn new(layout: &ShimLayout) -> Result { // 只加载安全的标准库,剥离 os / io 等风险模块 let lua = Lua::new_with( StdLib::TABLE | StdLib::STRING | StdLib::MATH | StdLib::PACKAGE, mlua::LuaOptions::default(), ) - .map_err(|e| ShimError::Environment(format!("初始化 Lua 失败: {}", e)))?; + .context("初始化 Lua 失败")?; let globals = lua.globals(); @@ -34,7 +34,7 @@ impl LuaRuntime { // 1. 注入锚点变量 __SHIM_DIR__(shim 安装根目录) globals .set("__SHIM_DIR__", root_dir.clone()) - .map_err(|e| ShimError::Environment(e.to_string()))?; + .context("设置 __SHIM_DIR__ 环境变量失败")?; // 2. 安全暴露 get_env 供配置读取环境变量 // 返回按平台路径分隔符拆分后的段数组(自动剥离引号包裹), @@ -68,11 +68,11 @@ impl LuaRuntime { Ok(table) }) - .map_err(|e| ShimError::Environment(e.to_string()))?; + .context("注册 get_env 函数失败")?; globals .set("get_env", get_env) - .map_err(|e| ShimError::Environment(e.to_string()))?; + .context("挂载 get_env 全局函数失败")?; // 3. 配置 package.path,确保 require 行为正常 if let Ok(package) = globals.get::
("package") { @@ -92,10 +92,10 @@ impl LuaRuntime { // 而不是让整个 shims.lua 解析失败(排查问题时日志可见) let original_require: mlua::Function = globals .get("require") - .map_err(|e| ShimError::Environment(format!("获取 require 失败: {}", e)))?; + .context("获取内置 require 函数失败")?; globals .set("_rshim_original_require", &original_require) - .map_err(|e| ShimError::Environment(e.to_string()))?; + .context("备份原始 require 函数失败")?; let wrapped_require = lua .create_function(|lua, module: String| -> mlua::Result { @@ -112,26 +112,27 @@ impl LuaRuntime { } } }) - .map_err(|e| ShimError::Environment(e.to_string()))?; + .context("创建包装版 require 函数失败")?; globals .set("require", wrapped_require) - .map_err(|e| ShimError::Environment(e.to_string()))?; + .context("重载 require 函数失败")?; Ok(Self { lua }) } /// 执行指定脚本文件,直接返回完整的 Lua Table - pub fn eval_script(&self, path: impl AsRef) -> Result { + pub fn eval_script(&self, path: impl AsRef) -> Result { 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!( - "{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码): {}", - path.display(), - e.utf8_error() - )) + let bytes = + fs::read(path).with_context(|| format!("无法读取配置文件: {}", path.display()))?; + + let code = String::from_utf8(bytes).with_context(|| { + format!( + "{} 不是有效的 UTF-8 文件(请将 Lua 配置文件保存为 UTF-8 编码)", + path.display() + ) })?; println!("code {:?}", code); let chunk_name = format!("@{}", path.display()); @@ -140,15 +141,13 @@ impl LuaRuntime { .load(&code) .set_name(&chunk_name) .eval::() - .map_err(|e| ShimError::LuaExecution { - file: path.display().to_string(), - source: e, - }) + // .map_err(|e| anyhow!(e.to_string())) + .with_context(|| format!("执行 Lua 配置文件失败: {}", path.display())) } // /// 将 Lua Value 解析转化为 ShimConfig 数据对象 - // pub fn parse_config(&self, value: Value) -> Result { - // ShimConfig::from_lua(value, &self.lua).map_err(|e| ShimError::InvalidConfig(e.to_string())) + // pub fn parse_config(&self, value: Value) -> Result { + // ShimConfig::from_lua(value, &self.lua).map_err(|e| Error::InvalidConfig(e.to_string())) // } } diff --git a/src/shim.rs b/src/shim.rs index 7410e63..bf2fbd2 100644 --- a/src/shim.rs +++ b/src/shim.rs @@ -1,28 +1,19 @@ -use crate::ShimError; use crate::{LuaRuntime, ShimConfig, ShimLayout}; +use anyhow::{Context, Result, bail}; use mlua::{Table, Value}; -use std::{ - env, - io::{Error, ErrorKind}, -}; +use std::env; use tracing::{debug, trace, warn}; pub struct Shim; impl Shim { - pub fn load() -> Result { - let current_exe = env::current_exe() - .map_err(|e| Error::new(ErrorKind::Other, format!("获取代理程序路径失败: {}", e)))?; + pub fn load() -> Result { + let current_exe = env::current_exe().context("获取代理程序路径失败: {}")?; debug!("当前目录 {}", current_exe.display()); let target_name = current_exe .file_stem() .and_then(|s| s.to_str()) - .ok_or_else(|| { - ShimError::PathResolution(format!( - "无法从路径 [{}] 提取有效的程序名称", - current_exe.display() - )) - })? + .with_context(|| format!("无法从路径 [{}] 提取有效的程序名称", current_exe.display()))? .to_lowercase(); debug!( target_name = %target_name, @@ -47,7 +38,7 @@ impl Shim { runtime: &LuaRuntime, paths: &ShimLayout, target_name: &str, - ) -> Result { + ) -> Result { // 策略 1: 尝试加载全局配置文件 shims.lua let global_config = paths.root_dir.join("rshim.lua"); if global_config.is_file() { @@ -57,11 +48,11 @@ impl Shim { // 检查 shims.lua 中是否存在以 target_name 命名的 Table 节点 if root_table .contains_key(target_name) - .map_err(|e| ShimError::InvalidConfig(format!("检查全局配置失败: {}", e)))? + .context("检查全局配置失败")? { - let target_val: ShimConfig = root_table.get(target_name).map_err(|e| { - ShimError::InvalidConfig(format!("解析配置 [{}] 失败: {}", target_name, e)) - })?; + let target_val: ShimConfig = root_table + .get(target_name) + .with_context(|| format!("解析配置 [{}] 失败: ", target_name))?; debug!( target = %target_name, source = %global_config.display(), @@ -92,9 +83,10 @@ impl Shim { // 策略 3: 所有查找失败,抛出错误 warn!(target = %target_name, "未找到任何匹配的配置文件"); - Err(ShimError::ConfigMissing(format!( + bail!( "未找到关于 '{}' 的配置。请检查 shims.lua 或特定的 {}.lua 文件", - target_name, target_name - ))) + target_name, + target_name + ); } }