226 lines
7.7 KiB
Rust
226 lines
7.7 KiB
Rust
use mlua::LuaString;
|
|
use std::ffi::OsString;
|
|
|
|
/// 将 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"]);
|
|
}
|
|
}
|