use std::fmt; use crate::error::validation_error; use mlua::Value; pub fn validate_command_name(name: &Value) -> mlua::Result { let name_str = match name { Value::String(s) => s .to_str() .map_err(|_| validation_error("子命令名称必须是合法的 UTF-8 字符串"))? .to_string(), other => { return Err(validation_error(format!( "命令行键名类型错误:期望 string,实际是 {}", other.type_name() ))); } }; let trimmed = name_str.trim(); if trimmed.is_empty() { return Err(validation_error("子命令名称不能为空字符串")); } // 2. 禁止包含空格与不可见控制符(否则 shell 与 clap 无法正确定位) if name_str.contains(|c: char| c.is_whitespace()) { return Err(validation_error(format!( "子命令名称 [{name_str}] 非法:命令名不能包含空格或空白字符" ))); } if name_str.contains('=') { return Err(validation_error(format!( "子命令名称 [{}] 不能包含 '='", name_str ))); } if name_str.contains('\0') { return Err(validation_error(format!( "子命令名称 [{}] 不能包含 NUL 字符", name_str ))); } Ok(name_str) } pub fn ensure_no_nul(context: &impl fmt::Display, os_str: &std::ffi::OsStr) -> mlua::Result<()> { #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; if os_str.as_bytes().contains(&0) { return Err(conversion_error(format!( "{} 的值不能包含 NUL 字符", context ))); } } #[cfg(windows)] { use std::os::windows::ffi::OsStrExt; if os_str.encode_wide().any(|c| c == 0) { return Err(validation_error(format!( "{} 的值不能包含 NUL 字符", context ))); } } Ok(()) } use anyhow::{Context, Result, anyhow}; use crate::{TargetConfig,}; use mlua::{LuaString, Table, }; use std::collections::{HashMap, HashSet}; use std::ffi::OsString; use std::path::PathBuf; use std::str::FromStr; // use tinyjson::JsonValue; use crate::utils::{lua_string_2_os_string, parse_tokens}; use tracing::debug; /// Lua 值校验器:针对不同上下文定义校验规则 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LuaValidator { /// 目标程序路径:必须是字符串路径 Target, /// 命令行参数元素:仅支持一维基础标量,禁止嵌套 Table Args, /// 环境变量值:支持基础标量及多维嵌套 Table(递归展平) Env, Aliases, } impl fmt::Display for LuaValidator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Target => write!(f, "目标路径 (target)"), Self::Args => write!(f, "命令行参数 (args)"), Self::Env => write!(f, "环境变量 (env)"), Self::Aliases => write!(f, "命令行别名 (aliases)"), } } } impl LuaValidator { fn parse_sequence(&self, index: i64, item: &Value) -> mlua::Result<()> { match item { Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => Ok(()), Value::Table(tbl) => match self { Self::Env | Self::Aliases => self.validate_sequence_table(tbl), Self::Args => Err(validation_error(format!( "{} 仅支持一维数组,不能包含嵌套 Table,索引位置: {index}", self ))), Self::Target => Err(validation_error(format!("{} 仅支持字符串", self))), }, other => Err(validation_error(format!( "{} 第 {} 个元素类型无效: {}", self, index, other.type_name() ))), } } /// 校验 Table 是否为严格连续的纯数组,并递归校验其内部元素 fn validate_sequence_table(&self, tbl: &Table) -> mlua::Result<()> { let mut index = 1i64; // 1. 顺序遍历连续整数索引 1..N loop { let item: Value = tbl.raw_get(index)?; if matches!(&item, Value::Nil) { break; } self.parse_sequence(index, &item)?; index += 1; } // 2. 查漏:校验是否存在空洞索引或字典键 (Key-Value 键值对) for pair in tbl.pairs::() { let (key, _) = pair?; match key { Value::Integer(i) if i >= 1 && i < index => {} // 已遍历放行 Value::Integer(_) => { return Err(validation_error(format!( "{} 第 {} 个元素为 nil 或不存在(请检查漏写引号或变量未定义)", self, index ))); } _ => { return Err(validation_error(format!( "{} 必须是纯列表,不能包含键值对/字典结构", self ))); } } } Ok(()) } /// 底层 NUL 字符跨平台安全检查 fn ensure_no_nul(context: &impl fmt::Display, os_str: &std::ffi::OsStr) -> mlua::Result<()> { #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; if os_str.as_bytes().contains(&0) { return Err(conversion_error(format!( "{} 的值不能包含 NUL 字符", context ))); } } #[cfg(windows)] { use std::os::windows::ffi::OsStrExt; if os_str.encode_wide().any(|c| c == 0) { return Err(validation_error(format!( "{} 的值不能包含 NUL 字符", context ))); } } Ok(()) } /// 【纯粹数据转换与展开】将已通过校验的 Value 递归解析为 OsString 动态数组 fn collect_value_into(&self, value: &Value, out: &mut Vec) -> mlua::Result<()> { match value { Value::Nil => {} Value::String(s) => { let os_str = lua_string_2_os_string(s)?; match self { Self::Args | Self::Aliases => { if let Some(str_ref) = os_str.to_str() { // 使用 Tokenizer 切分空格与引号 out.extend(parse_tokens(str_ref)); } else { // 对于无法转为 UTF-8 的特殊二进制数据,作为整体追加 out.push(os_str); } } Self::Env => { out.push(os_str); } Self::Target => {} } } Value::Integer(i) => out.push(OsString::from(i.to_string())), Value::Number(n) => { tracing::warn!(%self, value = %n, "浮点数将按十进制格式转换为字符串"); out.push(OsString::from(n.to_string())); } Value::Boolean(b) => { tracing::warn!(%self, value = %b, "布尔值将转换为字符串"); out.push(OsString::from(b.to_string())); } Value::Table(tbl) => { let mut index = 1i64; loop { let item: Value = tbl.raw_get(index)?; if matches!(item, Value::Nil) { break; } Self::collect_value_into(self, &item, out)?; index += 1; } } _ => unreachable!("传入收集器的 Value 应已通过 validate 校验"), } Ok(()) } fn parse_name(name: &Value) -> mlua::Result { let name_str = match name { Value::String(s) => match s.to_str() { Ok(str_ref) => str_ref.to_string(), Err(_) => return Err(validation_error("环境变量名必须是合法的 UTF-8 字符串")), }, other => { return Err(validation_error(format!( "环境变量键名类型错误:期望 string,实际是 {}", other.type_name() ))); } }; if name_str.is_empty() { return Err(validation_error("环境变量名不能为空")); } if name_str.contains('=') { return Err(validation_error(format!( "环境变量名 [{}] 不能包含 '='", name_str ))); } if name_str.contains('\0') { return Err(validation_error(format!( "环境变量名 [{}] 不能包含 NUL 字符", name_str ))); } Ok(name_str) } fn parse_val(&self, name: &str, value: &Value) -> mlua::Result<()> { match value { Value::Nil | Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => Ok(()), Value::Table(tbl) => self.validate_sequence_table(tbl), other => Err(validation_error(format!( "{} [{}]不支持 {} 类型, 仅支持 string/number/boolean 或嵌套数组", self, name, other.type_name() ))), } } } impl LuaValidator { /// 解析并校验 `target` pub fn parse_target(value: &Value) -> mlua::Result { let ctx = Self::Target; match value { Value::String(s) => { let os_str = lua_string_2_os_string(&s)?; Self::ensure_no_nul(&ctx, &os_str)?; Ok(TargetConfig::Static(PathBuf::from(os_str))) } // 分支 2:多版本动态工具 target = { home = "...", version_file = "...", current = "..." } Value::Table(t) => { // 1. 解析 home(必填) let home_val = t.get::("home")?; let home = match home_val { Value::String(ref s) => { let os_str = lua_string_2_os_string(s)?; Self::ensure_no_nul(&"target.home", &os_str)?; PathBuf::from(os_str) } Value::Nil => { return Err(validation_error("多版本 target 表缺少必填字段 'home'")); } other => { return Err(validation_error(format!( "target.home 需为有效路径字符串,实际类型是 {}", other.type_name() ))); } }; // 2. 解析 version_file(选填,默认为 None,后续业务可 fallback 到 .version) let version_file = match t.get::>("version_file")? { Some(Value::String(ref s)) => { let os_str = lua_string_2_os_string(s)?; Self::ensure_no_nul(&"target.version_file", &os_str)?; Some(os_str.to_string_lossy().into_owned()) } Some(Value::Nil) | None => None, Some(other) => { return Err(validation_error(format!( "target.version_file 应为文件名字符串,实际类型是 {}", other.type_name() ))); } }; // 3. 解析 current(选填/可为 nil:未激活或找不到版本时允许为 None) let current = match t.get::>("current")? { Some(Value::String(ref s)) => { let os_str = lua_string_2_os_string(s)?; Self::ensure_no_nul(&"target.current", &os_str)?; Some(PathBuf::from(os_str)) } // Lua 侧 ver 为 nil 时,current 计算结果即为 nil,合法放行 Some(Value::Nil) | None => None, Some(other) => { return Err(validation_error(format!( "target.current 需为有效路径字符串或 nil,实际类型是 {}", other.type_name() ))); } }; Ok(TargetConfig::Versioned { home, version_file, current, }) } Value::Nil => Err(validation_error( "缺少必填字段 target(应为字符串路径或多版本配置表)", )), other => Err(validation_error(format!( "{} 需为有效的路径且类型必须是字符串或多版本配置表,实际类型是 {}", ctx, other.type_name() ))), } } /// 解析并校验 `args` pub fn parse_args(value: &Value) -> mlua::Result> { { debug!("args 配置已关闭 "); #[cfg(not(feature = "args"))] Ok(Vec::new()) } #[cfg(feature = "args")] { debug!("args 配置已开启"); let ctx = Self::Args; match value { Value::Table(tbl) => { ctx.validate_sequence_table(tbl)?; let capacity = tbl.raw_len().min(128); let mut raw_parts = Vec::with_capacity(capacity); Self::collect_value_into(&ctx, value, &mut raw_parts)?; for part in &raw_parts { Self::ensure_no_nul(&ctx, &part)?; } Ok(raw_parts) } other => Err(validation_error(format!( "{} 必须是数组列表,实际类型是 {}", ctx, other.type_name() )))?, } } } /// 校验环境变量名的合法性(Windows 约束:非空、不含 '='、不含 NUL) fn parse_env_name(name: &Value) -> mlua::Result { Self::parse_name(name) } /// 解析、校验并拼接单个环境变量键值对 (`key`, `value`) -> (`OsString`) fn parse_env_val( context: &LuaValidator, name: &str, raw_val: &Value, ) -> mlua::Result { context.parse_val(name, raw_val)?; // let ctx = format!("{} [{}]", context, name); let capacity = match raw_val { Value::Table(t) => t.raw_len().min(128), Value::Nil => 0, _ => 1, }; let mut parts = Vec::with_capacity(capacity); Self::collect_value_into(&context, raw_val, &mut parts)?; // context.collect_value_into(raw_val, &mut parts)?; // 校验每个展开元素的 NUL 字符 for part in &parts { Self::ensure_no_nul(&name, part)?; } // 3. 使用系统路径分隔符拼接数组列表 let joined_os_str = std::env::join_paths(parts).map_err(|e| { validation_error(format!("{} 的值无法用系统路径分隔符拼接: {}", context, e)) })?; Ok(joined_os_str) } /// 解析整个 `env` Table,直接返回安全的环境变量 Map pub fn parse_env(value: &Value) -> mlua::Result> { let ctx = Self::Env; let Value::Table(tbl) = value else { // 这里作为防御性校验,外部虽然 match 过了,但内层仍保持强类型安全 return Err(validation_error(format!( "{} 必须是键值表 (table),实际类型是 {}", ctx, value.type_name() ))); }; let mut env_map = HashMap::new(); let mut n = 0i32; for pair in tbl.pairs::() { n += 1; let (raw_key, raw_val) = pair?; debug!("env {} {}", n, raw_val.type_name()); // 1. 解析并校验 Key,拿到安全的 String let key = Self::parse_env_name(&raw_key)?; // 2. 借用 &name 传递给 Value 解析器作为上下文 let val = Self::parse_env_val(&ctx, &key, &raw_val)?; env_map.insert(key, val); } Ok(env_map) } } impl LuaValidator { /// 校验环境变量名的合法性(Windows 约束:非空、不含 '='、不含 NUL) fn parse_aliases_name(name: &Value) -> mlua::Result { Self::parse_name(name) } fn parse_aliases_val( context: &LuaValidator, name: &str, raw_val: &Value, ) -> mlua::Result> { context.parse_val(name, raw_val)?; // let ctx = format!("{} [{}]", context, name); // 预估容量:标量为 1,表取其实际长度(设置上限以防止异常输入) let capacity = match raw_val { Value::Table(t) => (t.raw_len() as usize).min(16), Value::Nil => 0, _ => 1, }; let mut parts = Vec::with_capacity(capacity); Self::collect_value_into(context, raw_val, &mut parts)?; Ok(parts) } /// 解析并打平别名表 (aliases) /// - 支持输入为 Nil / None / Table /// - 别名的值支持:String, Number, Boolean, Nil, " " 空白串, Table(连续数组) /// - 字符串作为整体参数保存,仅做 trim() 清理首尾空格,不按空格拆分 /// - 包含拓扑展开与死环检测 pub fn parse_aliases(value: &Value) -> mlua::Result>> { let ctx = Self::Aliases; // 1. 处理 nil / None 的情况,直接返回空 HashMap let Value::Table(table) = value else { return Err(validation_error(format!( "{} 必须是键值表 (table) ,当前类型: {}", ctx, value.type_name() ))); }; // 阶段一:提取原始别名映射 (Raw Extraction) let mut raw_aliases: HashMap> = HashMap::new(); for pair in table.pairs::() { let (raw_key, raw_val) = pair?; let key = Self::parse_aliases_name(&raw_key)?; let val = Self::parse_aliases_val(&ctx, &key, &raw_val)?; raw_aliases.insert(key, val); } // 阶段二:递归拓扑打平与循环引用检测 (Flattening & Cycle Detection) let mut visited_stack = HashSet::new(); for key in raw_aliases.keys() { visited_stack.clear(); Self::detect_alias_cycle(key, &raw_aliases, &mut visited_stack)?; } // 阶段三:无环前提下的高效展开 let mut flattened_aliases: HashMap> = HashMap::with_capacity(raw_aliases.len()); let mut resolved_args = Vec::new(); for key in raw_aliases.keys() { resolved_args.clear(); Self::expand_alias_dfs(key, &raw_aliases, &mut resolved_args); flattened_aliases.insert(key.clone(), resolved_args.clone()); } Ok(flattened_aliases) } /// 仅用于校验别名依赖图中是否存在死循环(不消耗额外的参数拼接内存) fn detect_alias_cycle( current_key: &str, raw_aliases: &HashMap>, visited_stack: &mut HashSet, ) -> mlua::Result<()> { // 递归栈中再次遇到相同的 Key,说明存在死循环 if visited_stack.contains(current_key) { return Err(validation_error(format!( "配置加载失败: 检测到别名循环嵌套依赖 'mr:{}'", current_key ))); } if let Some(args) = raw_aliases.get(current_key) { // 压栈 visited_stack.insert(current_key.to_string()); for arg in args { let arg_str = arg.to_string_lossy(); if let Some(sub_key) = arg_str.strip_prefix("mr:") { // 如果引用的子别名在映射表中存在,则深度优先校验 if raw_aliases.contains_key(sub_key) { Self::detect_alias_cycle(sub_key, raw_aliases, visited_stack)?; } } } // 出栈(回溯) visited_stack.remove(current_key); } Ok(()) } /// 安全拓扑展开:在保证绝对无环的前提下递归展开 mr: 前缀参数 fn expand_alias_dfs( current_key: &str, raw_aliases: &HashMap>, out: &mut Vec, ) { if let Some(args) = raw_aliases.get(current_key) { for arg in args { let arg_str = arg.to_string_lossy(); if let Some(sub_key) = arg_str.strip_prefix("mr:") { if raw_aliases.contains_key(sub_key) { // 安全地直接递归展开,无需再检查死循环 Self::expand_alias_dfs(sub_key, raw_aliases, out); } else { // 找不到对应的别名,按原样参数输出 out.push(arg.clone()); } } else { // 普通参数,直接输出 out.push(arg.clone()); } } } } }