- 新增 Lua 动态配置 tools 目录并返回完整路径 - 将 `resolve_args` 和 `to_command` 改为接受泛型 `IntoIterator`,解耦参数来源 - 统一使用 `chain` 零拷贝合并预设参数与运行时参数,消除多余堆分配 - 提权回退分支改为直接复用 `cmd.get_args()`,修复提权时 `mr:` 别名未展开的缺陷 - 规范参数与变量命名,完善代码注释
234 lines
8.0 KiB
Rust
234 lines
8.0 KiB
Rust
use mlua::LuaString;
|
||
use std::ffi::OsString;
|
||
use std::path::Path;
|
||
|
||
/// 将 Path 转换为适合 Lua 使用的安全字符串路径
|
||
pub fn normalize_path_for_lua(path: &Path) -> String {
|
||
// 自动将 Windows UNC 规范路径转回传统路径
|
||
let simplified = dunce::simplified(path);
|
||
simplified.to_string_lossy().replace('\\', "/")
|
||
}
|
||
|
||
/// 将 Lua 字符串转为 Rust 字符串;非 UTF-8 按系统默认的 ANSI/OEM (如 GBK) 进行安全解码
|
||
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))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 将输入的字符串按 Shell 规则切分为独立的 CLI 参数 Token
|
||
/// - 自动过滤连续空格
|
||
/// - 支持单引号 `'...'` 和双引号 `"..."` 包裹包含空格的参数
|
||
pub fn parse_tokens(input: &str) -> Vec<OsString> {
|
||
let mut tokens = Vec::new();
|
||
let mut current_token = String::new();
|
||
let mut in_quote: Option<char> = None;
|
||
let mut chars = input.chars().peekable();
|
||
|
||
while let Some(ch) = chars.next() {
|
||
match (ch, in_quote) {
|
||
// 处理转义字符 (例如 \")
|
||
('\\', _quote) => {
|
||
if let Some(&next_ch) = chars.peek() {
|
||
let should_escape = if cfg!(windows) {
|
||
// Windows 策略:只有在转义引号、反斜杠本身时才剥离 \
|
||
// (如果在双引号内部,空格也不应该被 \ 转义)
|
||
next_ch == '"' || next_ch == '\'' || next_ch == '\\'
|
||
} else {
|
||
// Unix 策略:标准 Shell 转义(引号、反斜杠、空格等)
|
||
next_ch == '"'
|
||
|| next_ch == '\''
|
||
|| next_ch == '\\'
|
||
|| next_ch.is_whitespace()
|
||
};
|
||
|
||
if should_escape {
|
||
chars.next(); // 消耗掉下一个字符
|
||
current_token.push(next_ch);
|
||
} else {
|
||
// 保留 Windows 路径分隔符或未知转义中的 \
|
||
current_token.push('\\');
|
||
}
|
||
} else {
|
||
// 结尾孤立的 \
|
||
current_token.push('\\');
|
||
}
|
||
}
|
||
// 遇到引号:开启或关闭引号包裹
|
||
('"' | '\'', None) => {
|
||
in_quote = Some(ch);
|
||
}
|
||
('"' | '\'', Some(q)) if q == ch => {
|
||
in_quote = None;
|
||
}
|
||
// 引号外部遇到空白字符:切分出一个完整的 Token
|
||
(ch, None) if ch.is_whitespace() => {
|
||
if !current_token.is_empty() {
|
||
tokens.push(OsString::from(std::mem::take(&mut current_token)));
|
||
}
|
||
}
|
||
// 其他字符或引号内部字符:直接追加
|
||
(ch, _) => {
|
||
current_token.push(ch);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收尾最后一个 Token
|
||
if !current_token.is_empty() {
|
||
tokens.push(OsString::from(current_token));
|
||
}
|
||
|
||
tokens
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use std::ffi::OsString;
|
||
/// 辅助宏:简化声明与断言对比
|
||
macro_rules! assert_tokens {
|
||
($input:expr, $expected:expr) => {
|
||
let actual = parse_tokens($input);
|
||
let expected_os: Vec<OsString> = $expected.into_iter().map(OsString::from).collect();
|
||
assert_eq!(
|
||
actual, expected_os,
|
||
"\n测试输入: {:?}\n期望输出: {:?}\n实际输出: {:?}",
|
||
$input, expected_os, actual
|
||
);
|
||
};
|
||
}
|
||
#[test]
|
||
fn test_parse_tokens_basic_split() {
|
||
// 场景 1:基础多参数拆分(空格分隔)
|
||
assert_tokens!("cargo run --verbose", vec!["cargo", "run", "--verbose"]);
|
||
assert_tokens!("git status", vec!["git", "status"]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_multi_alias_ref() {
|
||
// 场景 2:多别名混合与组合引用
|
||
assert_tokens!(
|
||
"mr:run --bin mr:base_flags",
|
||
vec!["mr:run", "--bin", "mr:base_flags"]
|
||
);
|
||
assert_tokens!(
|
||
"mr:app1 mr:app2 --flag",
|
||
vec!["mr:app1", "mr:app2", "--flag"]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_continuous_whitespaces() {
|
||
// 场景 3:连续多空格与制表符过滤
|
||
assert_tokens!(
|
||
"mr:run --bin \t my_app",
|
||
vec!["mr:run", "--bin", "my_app"]
|
||
);
|
||
assert_tokens!(" cargo build ", vec!["cargo", "build"]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_double_quotes() {
|
||
// 场景 4:双引号包裹包含空格的参数
|
||
assert_tokens!(
|
||
"git commit -m \"fix a bug\"",
|
||
vec!["git", "commit", "-m", "fix a bug"]
|
||
);
|
||
assert_tokens!("echo \"hello world\"", vec!["echo", "hello world"]);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_single_quotes() {
|
||
// 场景 5:单引号包裹包含空格的参数
|
||
assert_tokens!(
|
||
"gcc -O2 'my file.c' -o app",
|
||
vec!["gcc", "-O2", "my file.c", "-o", "app"]
|
||
);
|
||
assert_tokens!(
|
||
"python 'script with space.py'",
|
||
vec!["python", "script with space.py"]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_escaped_characters() {
|
||
// 场景 6:反斜杠转义字符
|
||
assert_tokens!("echo hello\\ world", vec!["echo", "hello\\", "world"]);
|
||
assert_tokens!(
|
||
"echo \"hello \\\"world\\\"\"",
|
||
vec!["echo", "hello \"world\""]
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_single_scalar_and_edge_cases() {
|
||
// 场景 7:单标量参数与边界情况
|
||
assert_tokens!("git", vec!["git"]);
|
||
assert_tokens!("8000", vec!["8000"]);
|
||
assert_tokens!("", Vec::<&str>::new());
|
||
assert_tokens!(" ", Vec::<&str>::new());
|
||
}
|
||
|
||
#[test]
|
||
fn test_parse_tokens_unclosed_quotes() {
|
||
// 场景 8:未闭合引号的容错处理(会尽量追加到当前 Token 中)
|
||
assert_tokens!("echo \"hello world", vec!["echo", "hello world"]);
|
||
}
|
||
}
|