feat: 支持多版本 target 配置并优化执行前校验
- 将 MirrorTarget.target 改造为 TargetConfig 枚举(支持 Static 与 Versioned) - TargetConfig 独立实现 FromLua,解耦反序列化与业务校验 - to_command 调整为显式接收执行路径,简化构建逻辑 - 增加未选择版本时的前置拦截与友好提示
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
use std::fmt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
||||||
@@ -89,3 +90,41 @@ pub enum Error {
|
|||||||
#[error("用户自定义扩展错误: {0}")]
|
#[error("用户自定义扩展错误: {0}")]
|
||||||
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
|
Other(#[source] Box<dyn std::error::Error + Send + Sync>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// 将 MirrorTarget 构建为 Command 时可能触发的阻断性错误
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TargetExecutionError {
|
||||||
|
/// 多版本工具未激活版本 (current == None)
|
||||||
|
UnselectedVersion {
|
||||||
|
tool: String,
|
||||||
|
},
|
||||||
|
/// 目标可执行文件物理不存在
|
||||||
|
NotFound {
|
||||||
|
tool: String,
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for TargetExecutionError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::UnselectedVersion { tool } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"工具 '{}' 当前未指定有效版本。\n提示: 请先运行 'mr use {}' 选择版本。",
|
||||||
|
tool, tool
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Self::NotFound { tool, path } => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"工具 '{}' 的目标程序不存在: {:?}",
|
||||||
|
tool, path
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for TargetExecutionError {}
|
||||||
@@ -5,6 +5,7 @@ mod plugins;
|
|||||||
mod runtime;
|
mod runtime;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod validators;
|
mod validators;
|
||||||
|
mod model;
|
||||||
|
|
||||||
pub use layout::Layout;
|
pub use layout::Layout;
|
||||||
// pub use mirror_shim::mirror::Mirror;
|
// pub use mirror_shim::mirror::Mirror;
|
||||||
@@ -15,4 +16,5 @@ pub use plugins::register_basics;
|
|||||||
pub use utils::normalize_path_for_lua;
|
pub use utils::normalize_path_for_lua;
|
||||||
pub use validators::validate_command_name;
|
pub use validators::validate_command_name;
|
||||||
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
|
// 把 Result 和 Error 提升到根命名空间,给外部带来极致便利
|
||||||
pub use error::{Error, Result,validation_error,conversion_err};
|
pub use error::{Error, Result,validation_error,conversion_err,TargetExecutionError};
|
||||||
|
pub use model::TargetConfig;
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
use std::ffi::OsString;
|
||||||
|
use mlua::{FromLua, Lua, LuaString, Value};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use crate::utils::lua_string_2_os_string;
|
||||||
|
use crate::validation_error;
|
||||||
|
use crate::validators::ensure_no_nul;
|
||||||
|
|
||||||
|
/// 目标可执行文件的定位配置(支持静态路径与多版本 Table)
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TargetConfig{
|
||||||
|
/// 静态单版本工具
|
||||||
|
Static(PathBuf),
|
||||||
|
/// 多版本管理
|
||||||
|
Versioned{
|
||||||
|
/// 工具主根目录(供 mr use 扫描候选版本)
|
||||||
|
home:PathBuf,
|
||||||
|
/// 版本真相源文件名(如 ".version",可选,默认 ".version")
|
||||||
|
version_file:Option<String>,
|
||||||
|
/// 当前激活版本的可执行文件完整路径(若未激活或不存在则为 None)
|
||||||
|
current:Option<PathBuf>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Default for TargetConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
TargetConfig::Static(PathBuf::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TargetConfig {
|
||||||
|
/// 垫片程序(mirror-shim)专用快速通道:提取最终可执行路径
|
||||||
|
///
|
||||||
|
/// - Static: 返回自身路径
|
||||||
|
/// - Versioned: 返回 current 路径(若未激活则返回 None)
|
||||||
|
|
||||||
|
pub fn executable_path(&self) -> Option<&PathBuf> {
|
||||||
|
match self {
|
||||||
|
TargetConfig::Static(path) => Some(path),
|
||||||
|
TargetConfig::Versioned { current, .. } => current.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 管理程序(mr-cli)专用辅助方法:判断是否支持多版本
|
||||||
|
pub fn is_versioned(&self) -> bool {
|
||||||
|
matches!(self, TargetConfig::Versioned { .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromLua for TargetConfig {
|
||||||
|
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
||||||
|
match value {
|
||||||
|
// 分支 1:静态单版本工具 target = "..."
|
||||||
|
Value::String(s) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target", &os_str)?;
|
||||||
|
Ok(TargetConfig::Static(PathBuf::from(os_str)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分支 2:多版本管理工具 target = { home = "...", ... }
|
||||||
|
Value::Table(table) => {
|
||||||
|
// 1. home (必填字段)
|
||||||
|
let home = match table.get::<Option<Value>>("home")? {
|
||||||
|
Some(Value::String(s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target.home", &os_str)?;
|
||||||
|
PathBuf::from(os_str)
|
||||||
|
}
|
||||||
|
None | Some(Value::Nil) => {
|
||||||
|
return Err(validation_error("多版本 target 表缺少必填字段 'home'"));
|
||||||
|
}
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.home 需为路径字符串,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. version_file (选填字段,未填或 nil 时为 None)
|
||||||
|
let version_file = match table.get::<Option<Value>>("version_file")? {
|
||||||
|
Some(Value::String(s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target.version_file", &os_str)?;
|
||||||
|
Some(os_str.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
None | Some(Value::Nil) => None,
|
||||||
|
Some(other) => {
|
||||||
|
return Err(validation_error(format!(
|
||||||
|
"target.version_file 需为文件名字符串,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. current (选填/可为 nil,表示未激活或当前未指定版本)
|
||||||
|
let current = match table.get::<Option<Value>>("current")? {
|
||||||
|
Some(Value::String(s)) => {
|
||||||
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
|
ensure_no_nul(&"target.current", &os_str)?;
|
||||||
|
Some(PathBuf::from(os_str))
|
||||||
|
}
|
||||||
|
// Lua 侧 ver 为 nil 时,current 拼出的也是 nil,此时合法解析为 None
|
||||||
|
None | Some(Value::Nil) => 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!(
|
||||||
|
"target 需为有效路径字符串或多版本配置表,实际类型是 {}",
|
||||||
|
other.type_name()
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -9,3 +9,63 @@ pub fn normalize_path_for_lua(path: &Path) -> String {
|
|||||||
simplified.to_string_lossy().replace('\\', "/")
|
simplified.to_string_lossy().replace('\\', "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lua_string_2_os_string(s: &LuaString) -> mlua::Result<OsString> {
|
||||||
|
let raw_bytes = &s.as_bytes();
|
||||||
|
// 1. Unix 平台:直接零拷贝透传原始字节(无损支持任意编码)
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
Ok(OsStr::from_bytes(raw_bytes).to_os_string())
|
||||||
|
}
|
||||||
|
// 2. Windows 平台:优先按 UTF-8 解码,失败则按当前系统本地代码页 (ANSI/GBK) 转换
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
// 先尝试标准的 UTF-8
|
||||||
|
if let Ok(utf8_str) = std::str::from_utf8(raw_bytes) {
|
||||||
|
return Ok(OsString::from(utf8_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
use std::os::windows::ffi::OsStringExt;
|
||||||
|
use windows_sys::Win32::Globalization::{
|
||||||
|
CP_ACP, MB_ERR_INVALID_CHARS, MultiByteToWideChar,
|
||||||
|
};
|
||||||
|
|
||||||
|
if raw_bytes.is_empty() {
|
||||||
|
return Ok(OsString::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = MultiByteToWideChar(
|
||||||
|
CP_ACP,
|
||||||
|
MB_ERR_INVALID_CHARS,
|
||||||
|
raw_bytes.as_ptr(),
|
||||||
|
raw_bytes.len() as i32,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if len <= 0 {
|
||||||
|
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
||||||
|
return Err(mlua::Error::FromLuaConversionError {
|
||||||
|
from: "LuaString",
|
||||||
|
to: "OsString".to_string(),
|
||||||
|
message: Some(
|
||||||
|
format!("字符串{:?}包含无效或当前系统无法识别的编码字节", s).to_string(),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = vec![0u16; len as usize];
|
||||||
|
MultiByteToWideChar(
|
||||||
|
CP_ACP,
|
||||||
|
MB_ERR_INVALID_CHARS,
|
||||||
|
raw_bytes.as_ptr(),
|
||||||
|
raw_bytes.len() as i32,
|
||||||
|
buf.as_mut_ptr(),
|
||||||
|
len,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(OsString::from_wide(&buf))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::fmt;
|
||||||
use crate::error::validation_error;
|
use crate::error::validation_error;
|
||||||
use mlua::Value;
|
use mlua::Value;
|
||||||
|
|
||||||
@@ -39,3 +40,26 @@ pub fn validate_command_name(name: &Value) -> mlua::Result<String> {
|
|||||||
}
|
}
|
||||||
Ok(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(())
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ use crate::sys::{
|
|||||||
ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG,
|
ERROR_ELEVATION_REQUIRED, EXIT_FAILED_LOAD_SHIM, EXIT_FAILED_SPAWN_PROG, EXIT_FAILED_WAIT_PROG,
|
||||||
EXIT_PROG_TERMINATED, execute_elevated,
|
EXIT_PROG_TERMINATED, execute_elevated,
|
||||||
};
|
};
|
||||||
use mirror_core::{Layout, LuaRuntime, register_basics};
|
use mirror_core::{Layout, LuaRuntime, TargetConfig, TargetExecutionError, register_basics};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
@@ -46,13 +46,25 @@ pub fn run(layout: &Layout) -> i32 {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1. 提取生效路径,缺失版本时直接抛出 UnselectedVersion
|
||||||
|
let target_path = match mr.target.executable_path() {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
warn!(
|
||||||
|
"[mirror] 工具 '{}' 当前未指定生效版本。\n提示: 请先运行 'mr use {}' 选择版本。",
|
||||||
|
target_name, target_name
|
||||||
|
);
|
||||||
|
return EXIT_FAILED_LOAD_SHIM;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 3. 解析调用参数与代理 Mirror 配置
|
// 3. 解析调用参数与代理 Mirror 配置
|
||||||
let calling_args: Vec<_> = env::args_os().skip(1).collect();
|
let calling_args: Vec<_> = env::args_os().skip(1).collect();
|
||||||
|
|
||||||
let combined_args = mr.args.iter().chain(calling_args.iter());
|
let combined_args = mr.args.iter().chain(calling_args.iter());
|
||||||
|
|
||||||
// 构建 Command:复用 ShimConfig::to_command(含 args/env 注入),避免重复逻辑
|
// 构建 Command:复用 ShimConfig::to_command(含 args/env 注入),避免重复逻辑
|
||||||
let mut cmd = mr.to_command(combined_args);
|
let mut cmd = mr.to_command(target_path, combined_args);
|
||||||
|
|
||||||
let mut child = match cmd.spawn() {
|
let mut child = match cmd.spawn() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
@@ -60,12 +72,12 @@ pub fn run(layout: &Layout) -> i32 {
|
|||||||
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
|
// 提权回退时需要完整参数:配置默认参数 + 调用方透传参数
|
||||||
let elevated_args: Vec<OsString> = cmd.get_args().map(|s| s.to_os_string()).collect();
|
let elevated_args: Vec<OsString> = cmd.get_args().map(|s| s.to_os_string()).collect();
|
||||||
|
|
||||||
exit(execute_elevated(&mr.target, &elevated_args, Some(&mr.env)))
|
exit(execute_elevated(target_path, &elevated_args, Some(&mr.env)))
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"启动目标程序 [{}] 时发生错误: {}",
|
"启动目标程序 [{}] 时发生错误: {}",
|
||||||
mr.target.to_string_lossy(),
|
target_path.to_string_lossy(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
return EXIT_FAILED_SPAWN_PROG;
|
return EXIT_FAILED_SPAWN_PROG;
|
||||||
@@ -77,7 +89,7 @@ pub fn run(layout: &Layout) -> i32 {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"等待目标程序 [{}] 执行完毕时发生错误: {}",
|
"等待目标程序 [{}] 执行完毕时发生错误: {}",
|
||||||
mr.target.to_string_lossy(),
|
target_path.to_string_lossy(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
EXIT_FAILED_WAIT_PROG
|
EXIT_FAILED_WAIT_PROG
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use crate::validators::LuaValidator;
|
use crate::validators::LuaValidator;
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use mirror_core::validate_command_name;
|
use mirror_core::validate_command_name;
|
||||||
use mirror_core::{Layout, LuaRuntime};
|
use mirror_core::{Layout, LuaRuntime, TargetConfig};
|
||||||
use mirror_core::{conversion_err, validation_error};
|
use mirror_core::{TargetExecutionError, conversion_err, validation_error};
|
||||||
use mlua::{FromLua, Lua, Table, Value};
|
use mlua::{FromLua, Lua, Table, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use tracing::{debug, trace};
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ impl FromLua for MirrorRegistry {
|
|||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct MirrorTarget {
|
pub struct MirrorTarget {
|
||||||
/// 目标程序可执行文件路径
|
/// 目标程序可执行文件路径
|
||||||
pub target: PathBuf,
|
pub target: TargetConfig,
|
||||||
/// 默认预设参数列表
|
/// 默认预设参数列表
|
||||||
pub args: Vec<OsString>,
|
pub args: Vec<OsString>,
|
||||||
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
|
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
|
||||||
@@ -104,12 +104,13 @@ impl MirrorTarget {
|
|||||||
expanded_args
|
expanded_args
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_command<I, S>(&self, combined_args: I) -> Command
|
pub fn to_command<P, I, S>(&self, target: P, combined_args: I) -> Command
|
||||||
where
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
S: AsRef<std::ffi::OsStr>,
|
S: AsRef<std::ffi::OsStr>,
|
||||||
{
|
{
|
||||||
let mut cmd = Command::new(&self.target);
|
let mut cmd = Command::new(target.as_ref());
|
||||||
|
|
||||||
// 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名
|
// 2. 传入预打平的别名字典,查表并展开所有以 `mr:` 为前缀的别名
|
||||||
let final_args = self.resolve_args(combined_args);
|
let final_args = self.resolve_args(combined_args);
|
||||||
@@ -128,7 +129,7 @@ impl MirrorTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromLua for MirrorTarget {
|
impl FromLua for MirrorTarget {
|
||||||
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||||
// 脚本返回必须是一个 Table 变体
|
// 脚本返回必须是一个 Table 变体
|
||||||
let table = match value {
|
let table = match value {
|
||||||
Value::Table(t) => t,
|
Value::Table(t) => t,
|
||||||
@@ -143,9 +144,12 @@ impl FromLua for MirrorTarget {
|
|||||||
// 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误)
|
// 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误)
|
||||||
let target = match table.get::<Option<Value>>("target")? {
|
let target = match table.get::<Option<Value>>("target")? {
|
||||||
None | Some(Value::Nil) => {
|
None | Some(Value::Nil) => {
|
||||||
return Err(validation_error("缺少必填字段 target(应为字符串路径)"));
|
return Err(validation_error(
|
||||||
|
"缺少必填字段 target(应为字符串路径或多版本配置表)",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Some(target_val) => LuaValidator::parse_target(&target_val)?,
|
// Some(target_val) => LuaValidator::parse_target(&target_val)?,
|
||||||
|
Some(target_val) => TargetConfig::from_lua(target_val, lua)?,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
|
// 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use mirror_core::validation_error;
|
use mirror_core::{TargetConfig, validation_error};
|
||||||
use mlua::{LuaString, Table, Value};
|
use mlua::{LuaString, Table, Value};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
@@ -212,17 +212,79 @@ impl LuaValidator {
|
|||||||
}
|
}
|
||||||
impl LuaValidator {
|
impl LuaValidator {
|
||||||
/// 解析并校验 `target`
|
/// 解析并校验 `target`
|
||||||
pub fn parse_target(value: &Value) -> mlua::Result<PathBuf> {
|
pub fn parse_target(value: &Value) -> mlua::Result<TargetConfig> {
|
||||||
let ctx = Self::Target;
|
let ctx = Self::Target;
|
||||||
match value {
|
match value {
|
||||||
Value::String(s) => {
|
Value::String(s) => {
|
||||||
let os_str = lua_string_2_os_string(&s)?;
|
let os_str = lua_string_2_os_string(&s)?;
|
||||||
Self::ensure_no_nul(&ctx, &os_str)?;
|
Self::ensure_no_nul(&ctx, &os_str)?;
|
||||||
Ok(PathBuf::from(os_str))
|
Ok(TargetConfig::Static(PathBuf::from(os_str)))
|
||||||
}
|
}
|
||||||
Value::Nil => Err(validation_error("缺少必填字段 target(应为字符串路径)")),
|
// 分支 2:多版本动态工具 target = { home = "...", version_file = "...", current = "..." }
|
||||||
|
Value::Table(t) => {
|
||||||
|
// 1. 解析 home(必填)
|
||||||
|
let home_val = t.get::<Value>("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::<Option<Value>>("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::<Option<Value>>("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!(
|
other => Err(validation_error(format!(
|
||||||
"{} 需为有效的路径且类型必须是字符串,实际类型是 {}",
|
"{} 需为有效的路径且类型必须是字符串或多版本配置表,实际类型是 {}",
|
||||||
ctx,
|
ctx,
|
||||||
other.type_name()
|
other.type_name()
|
||||||
))),
|
))),
|
||||||
@@ -534,7 +596,10 @@ mod tests {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(cfg.target, PathBuf::from("C:/tools/git.exe"));
|
assert_eq!(
|
||||||
|
cfg.target.executable_path().unwrap().to_path_buf(),
|
||||||
|
PathBuf::from("C:/tools/git.exe")
|
||||||
|
);
|
||||||
// assert_eq!(cfg.args, vec!["--no-pager", "2"]);
|
// assert_eq!(cfg.args, vec!["--no-pager", "2"]);
|
||||||
assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home"));
|
assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home"));
|
||||||
// PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段
|
// PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段
|
||||||
|
|||||||
Reference in New Issue
Block a user