feat: 拆分 mirror-core

- 拆分 mirror-core,优化项目结构
This commit is contained in:
2026-10-09 19:33:23 +08:00
parent 53579f9c65
commit c9ac3bda59
29 changed files with 1460 additions and 1070 deletions
+172
View File
@@ -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,
..
}
));
}
}
+78
View File
@@ -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 { .. }));
}
}
+120
View File
@@ -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(_)));
}
}
+27
View File
@@ -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(),
})
}
+52
View File
@@ -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],
},
})
}
+41
View File
@@ -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)
}