feat: 拆分 mirror-core
- 拆分 mirror-core,优化项目结构
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
use super::as_table;
|
||||
use super::scalar::{ScalarMode, to_os_strings};
|
||||
use super::sequence::as_strict_sequence;
|
||||
use crate::config::naming::parse_name;
|
||||
use crate::{Error, NameKind, Result};
|
||||
use mlua::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
|
||||
const CONTEXT: &str = "命令行别名 (aliases)";
|
||||
/// 数组嵌套上限,防止畸形配置打爆调用栈
|
||||
const MAX_DEPTH: usize = 16;
|
||||
/// `mr:` 引用链的最大深度
|
||||
const MAX_ALIAS_DEPTH: usize = 32;
|
||||
|
||||
/// `aliases = { name = value }` → 已展开的别名表。
|
||||
///
|
||||
/// value 是"命令行片段":字符串按 shell 规则分词,数组逐项处理。
|
||||
/// `mr:other` 在加载期递归展开;引用不存在的别名时原样保留;循环引用报错。
|
||||
pub(crate) fn parse_aliases(value: &Value) -> Result<HashMap<String, Vec<OsString>>> {
|
||||
let table = as_table(value, CONTEXT)?;
|
||||
let mut raw: HashMap<String, Vec<OsString>> = HashMap::with_capacity(table.raw_len().min(128));
|
||||
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (raw_key, raw_value) = pair?;
|
||||
let name = parse_name(NameKind::Alias, &raw_key)?;
|
||||
let context = format!("别名 '{name}'");
|
||||
|
||||
let mut parts = Vec::new();
|
||||
flatten(&raw_value, &context, 0, &mut parts)?;
|
||||
raw.insert(name, parts);
|
||||
}
|
||||
|
||||
expand_graph(&raw)
|
||||
}
|
||||
|
||||
/// 递归展平标量与嵌套数组,字符串按 shell 规则分词
|
||||
fn flatten(value: &Value, context: &str, depth: usize, out: &mut Vec<OsString>) -> Result<()> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{context} 的数组嵌套层级过深(超过 {MAX_DEPTH} 层)"
|
||||
)));
|
||||
}
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
for item in as_strict_sequence(table, context)? {
|
||||
flatten(&item, context, depth + 1, out)?;
|
||||
}
|
||||
}
|
||||
scalar => out.extend(to_os_strings(scalar, ScalarMode::Tokens, context)?),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 一次 DFS 完成"环检测 + 递归展开 + 结果缓存"
|
||||
fn expand_graph(raw: &HashMap<String, Vec<OsString>>) -> Result<HashMap<String, Vec<OsString>>> {
|
||||
let mut cache: HashMap<String, Vec<OsString>> = HashMap::with_capacity(raw.len());
|
||||
let mut stack: Vec<String> = Vec::new();
|
||||
|
||||
for key in raw.keys() {
|
||||
resolve(key, raw, &mut cache, &mut stack)?;
|
||||
}
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
key: &str,
|
||||
raw: &HashMap<String, Vec<OsString>>,
|
||||
cache: &mut HashMap<String, Vec<OsString>>,
|
||||
stack: &mut Vec<String>,
|
||||
) -> Result<Vec<OsString>> {
|
||||
if let Some(done) = cache.get(key) {
|
||||
return Ok(done.clone());
|
||||
}
|
||||
if stack.iter().any(|pending| pending == key) {
|
||||
return Err(Error::AliasCycle {
|
||||
name: key.to_string(),
|
||||
});
|
||||
}
|
||||
if stack.len() >= MAX_ALIAS_DEPTH {
|
||||
return Err(Error::AliasTooDeep {
|
||||
max: MAX_ALIAS_DEPTH,
|
||||
});
|
||||
}
|
||||
|
||||
stack.push(key.to_string());
|
||||
|
||||
let mut expanded = Vec::new();
|
||||
if let Some(args) = raw.get(key) {
|
||||
for arg in args {
|
||||
let text = arg.to_string_lossy();
|
||||
let referenced = text
|
||||
.strip_prefix("mr:")
|
||||
.filter(|name| raw.contains_key(*name))
|
||||
.map(str::to_string);
|
||||
|
||||
match referenced {
|
||||
Some(name) => expanded.extend(resolve(&name, raw, cache, stack)?),
|
||||
None => expanded.push(arg.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
cache.insert(key.to_string(), expanded.clone());
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn with_aliases(code: &str) -> Result<HashMap<String, Vec<OsString>>> {
|
||||
let lua = Lua::new();
|
||||
let value: Value = lua.load(code).eval().unwrap();
|
||||
parse_aliases(&value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokenizes_values_and_expands_references() {
|
||||
let aliases = with_aliases(
|
||||
r#"return { base = "log --graph", all = { "mr:base", "--all" }, blank = " " }"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
aliases.get("base").unwrap(),
|
||||
&vec![OsString::from("log"), OsString::from("--graph")]
|
||||
);
|
||||
assert_eq!(
|
||||
aliases.get("all").unwrap(),
|
||||
&vec![
|
||||
OsString::from("log"),
|
||||
OsString::from("--graph"),
|
||||
OsString::from("--all"),
|
||||
]
|
||||
);
|
||||
assert!(aliases.get("blank").unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_number_and_boolean_are_supported() {
|
||||
let aliases = with_aliases(r#"return { n = 10, b = true }"#).unwrap();
|
||||
assert_eq!(aliases.get("n").unwrap(), &vec![OsString::from("10")]);
|
||||
assert_eq!(aliases.get("b").unwrap(), &vec![OsString::from("true")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_reference_is_kept_literal() {
|
||||
let aliases = with_aliases(r#"return { a = "mr:nope" }"#).unwrap();
|
||||
assert_eq!(aliases.get("a").unwrap(), &vec![OsString::from("mr:nope")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_is_reported() {
|
||||
let err = with_aliases(r#"return { a = "mr:b", b = "mr:a" }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::AliasCycle { .. }), "unexpected: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_alias_name_is_reported() {
|
||||
let err = with_aliases(r#"return { [""] = "x" }"#).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::Alias,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use super::as_table;
|
||||
use super::scalar::{ScalarMode, to_os_strings};
|
||||
use super::sequence::as_strict_sequence;
|
||||
use crate::{Error, Result};
|
||||
use mlua::Value;
|
||||
use std::ffi::OsString;
|
||||
|
||||
const CONTEXT: &str = "命令行参数 (args)";
|
||||
|
||||
/// `args = { … }`:一个数组元素就是一个参数(空串保留为空参数)。
|
||||
///
|
||||
/// 只支持一维基础标量:嵌套 table 会让"一个元素 = 一个参数"的语义失效。
|
||||
pub(crate) fn parse_args(value: &Value) -> Result<Vec<OsString>> {
|
||||
let table = as_table(value, CONTEXT)?;
|
||||
let items = as_strict_sequence(table, CONTEXT)?;
|
||||
|
||||
let mut args = Vec::with_capacity(items.len());
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if matches!(item, Value::Table(_)) {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{CONTEXT} 仅支持一维数组,第 {} 个元素不能是 table",
|
||||
i + 1
|
||||
)));
|
||||
}
|
||||
let context = format!("{CONTEXT} 第 {} 个元素", i + 1);
|
||||
args.extend(to_os_strings(item, ScalarMode::Verbatim, &context)?);
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn with_args(code: &str) -> Result<Vec<OsString>> {
|
||||
let lua = Lua::new();
|
||||
let value: Value = lua.load(code).eval().unwrap();
|
||||
parse_args(&value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_scalars_and_keeps_empty_string() {
|
||||
let args = with_args(r#"return { "--no-pager", 2, true, "" }"#).unwrap();
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
OsString::from("--no-pager"),
|
||||
OsString::from("2"),
|
||||
OsString::from("true"),
|
||||
OsString::from(""),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_array_is_empty() {
|
||||
assert!(with_args("return {}").unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_nested_table() {
|
||||
let err = with_args(r#"return { { "a" } }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sparse_array() {
|
||||
let err = with_args(r#"return { "a", nil, "b" }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_table() {
|
||||
let err = with_args(r#"return "-B""#).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use super::as_table;
|
||||
use super::scalar::{ScalarMode, to_os_strings};
|
||||
use super::sequence::as_strict_sequence;
|
||||
use crate::config::naming::parse_name;
|
||||
use crate::{Error, NameKind, Result};
|
||||
use mlua::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
|
||||
const CONTEXT: &str = "环境变量 (env)";
|
||||
/// 数组嵌套上限,防止畸形配置打爆调用栈
|
||||
const MAX_DEPTH: usize = 16;
|
||||
|
||||
/// `env = { KEY = value }` → 环境变量表。
|
||||
///
|
||||
/// value 可以是标量,也可以是任意深度的数组;数组会递归展平,然后按系统
|
||||
/// 路径分隔符(Windows `;`)拼接。空字符串 / 空数组 → 空值(清空该变量)。
|
||||
pub(crate) fn parse_env(value: &Value) -> Result<HashMap<String, OsString>> {
|
||||
let table = as_table(value, CONTEXT)?;
|
||||
let mut env = HashMap::with_capacity(table.raw_len().min(128));
|
||||
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (raw_key, raw_value) = pair?;
|
||||
let name = parse_name(NameKind::EnvVar, &raw_key)?;
|
||||
let context = format!("环境变量 '{name}' 的值");
|
||||
|
||||
let mut parts = Vec::new();
|
||||
flatten(&raw_value, &context, 0, &mut parts)?;
|
||||
|
||||
let joined = std::env::join_paths(parts)
|
||||
.map_err(|e| Error::ParseError(format!("{context} 无法用系统路径分隔符拼接: {e}")))?;
|
||||
env.insert(name, joined);
|
||||
}
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
/// 递归展平标量与嵌套数组(env 的字符串不做分词,整段作为一个路径段)
|
||||
fn flatten(value: &Value, context: &str, depth: usize, out: &mut Vec<OsString>) -> Result<()> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{context} 的数组嵌套层级过深(超过 {MAX_DEPTH} 层)"
|
||||
)));
|
||||
}
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
for item in as_strict_sequence(table, context)? {
|
||||
flatten(&item, context, depth + 1, out)?;
|
||||
}
|
||||
}
|
||||
scalar => out.extend(to_os_strings(scalar, ScalarMode::Verbatim, context)?),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn with_env(code: &str) -> Result<HashMap<String, OsString>> {
|
||||
let lua = Lua::new();
|
||||
let value: Value = lua.load(code).eval().unwrap();
|
||||
parse_env(&value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattens_nested_arrays_with_path_separator() {
|
||||
let env = with_env(r#"return { PATH = { "C:/a", { "C:/b", "C:/c" } } }"#).unwrap();
|
||||
assert_eq!(env.get("PATH").unwrap().to_str(), Some("C:/a;C:/b;C:/c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalars_and_empty_values() {
|
||||
let env = with_env(r#"return { A = "", B = {}, C = 3, D = true }"#).unwrap();
|
||||
assert_eq!(env.get("A").unwrap().to_str(), Some(""));
|
||||
assert_eq!(env.get("B").unwrap().to_str(), Some(""));
|
||||
assert_eq!(env.get("C").unwrap().to_str(), Some("3"));
|
||||
assert_eq!(env.get("D").unwrap().to_str(), Some("true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_key() {
|
||||
let err = with_env(r#"return { ["FOO=1"] = "x" }"#).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::EnvVar,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
let err = with_env(r#"return { [""] = "x" }"#).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::EnvVar,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sparse_array_and_wrong_types() {
|
||||
let err = with_env(r#"return { P = { "a", nil, "b" } }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseError(_)));
|
||||
|
||||
let err = with_env(r#"return { F = function() end }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
|
||||
let err = with_env(r#"return "PATH=C:/x""#).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_nul_in_value() {
|
||||
let err = with_env(r#"return { P = { string.char(0) } }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ContainsNul(_)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! Lua 值 → Rust 值的转换:`args` / `env` / `aliases`。
|
||||
//!
|
||||
//! 这一层只管"值本身":数组形状、元素类型、键合法性、分词与拼接。
|
||||
//! 产出的错误统一是 `crate::Error` 的结构化变体;字段级语义
|
||||
//! (必填、默认值)由 `model.rs` 决定。
|
||||
|
||||
mod aliases;
|
||||
mod args;
|
||||
mod env;
|
||||
mod scalar;
|
||||
mod sequence;
|
||||
|
||||
pub(crate) use aliases::parse_aliases;
|
||||
pub(crate) use args::parse_args;
|
||||
pub(crate) use env::parse_env;
|
||||
|
||||
use crate::{Error, Result};
|
||||
use mlua::{Table, Value};
|
||||
|
||||
/// 值必须是 table,否则报 `TypeMismatch`
|
||||
pub(crate) fn as_table<'a>(value: &'a Value, context: &str) -> Result<&'a Table> {
|
||||
value.as_table().ok_or_else(|| Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "键值表 (table)",
|
||||
actual: value.type_name(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::args::tokens::parse_tokens;
|
||||
use crate::config::convert::{ensure_no_nul, lua_string_2_os_string};
|
||||
use crate::{Error, Result};
|
||||
use mlua::Value;
|
||||
use std::ffi::OsString;
|
||||
|
||||
/// 字符串元素的处理策略
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum ScalarMode {
|
||||
/// 整体作为一个参数(args:一个数组元素 = 一个参数,空串保留为空参数)
|
||||
Verbatim,
|
||||
/// 按 shell 规则分词(aliases:"status -s" → 两个参数)
|
||||
Tokens,
|
||||
}
|
||||
|
||||
/// 标量 → 参数列表
|
||||
///
|
||||
/// - `Nil` 产生 0 个参数(在 env 里表示"清空该变量")
|
||||
/// - 字符串 / 整数 / 浮点 / 布尔都可以转成字符串
|
||||
/// - 其它类型报 `TypeMismatch`
|
||||
/// - 产出的每个 `OsString` 都会做 NUL 检查
|
||||
pub(super) fn to_os_strings(
|
||||
value: &Value,
|
||||
mode: ScalarMode,
|
||||
context: &str,
|
||||
) -> Result<Vec<OsString>> {
|
||||
let os_str: OsString = match value {
|
||||
Value::String(s) => lua_string_2_os_string(s)?,
|
||||
Value::Integer(i) => i.to_string().into(),
|
||||
Value::Number(n) => n.to_string().into(),
|
||||
Value::Boolean(b) => b.to_string().into(),
|
||||
Value::Nil => return Ok(Vec::new()),
|
||||
other => {
|
||||
return Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "string / number / boolean",
|
||||
actual: other.type_name(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ensure_no_nul(&context, &os_str)?;
|
||||
|
||||
Ok(match mode {
|
||||
ScalarMode::Verbatim => vec![os_str],
|
||||
ScalarMode::Tokens => match os_str.to_str() {
|
||||
Some(text) => parse_tokens(text),
|
||||
// 非 UTF-8 字节没法按 shell 规则切分,整体作为一个参数
|
||||
None => vec![os_str],
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use crate::{Error, Result};
|
||||
use mlua::{Table, Value};
|
||||
|
||||
/// 严格连续的纯数组校验:一次遍历 `pairs`,要求键恰好是 `1..=n` 的连续整数。
|
||||
///
|
||||
/// 返回按顺序排列的元素。空洞(`{ "a", nil, "b" }`)或字典键都会报错。
|
||||
pub(super) fn as_strict_sequence(table: &Table, context: &str) -> Result<Vec<Value>> {
|
||||
let mut slots: Vec<Option<Value>> = Vec::with_capacity(table.raw_len().min(128));
|
||||
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (key, value) = pair?;
|
||||
let index = match key {
|
||||
Value::Integer(i) if i >= 1 => (i - 1) as usize,
|
||||
other => {
|
||||
return Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "连续整数下标 (数组)",
|
||||
actual: other.type_name(),
|
||||
});
|
||||
}
|
||||
};
|
||||
if slots.len() <= index {
|
||||
slots.resize_with(index + 1, || None);
|
||||
}
|
||||
slots[index] = Some(value);
|
||||
}
|
||||
|
||||
let mut items = Vec::with_capacity(slots.len());
|
||||
for (i, slot) in slots.into_iter().enumerate() {
|
||||
match slot {
|
||||
Some(value) => items.push(value),
|
||||
None => {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{context} 第 {} 个元素为 nil 或不存在(请检查漏写引号或变量未定义)",
|
||||
i + 1
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
Reference in New Issue
Block a user