fix(mhchem): 修复 ". __* " 正则转录错误导致的 panic=abort 崩溃
mhchem 转译器中 ". __* " 模式的 regex 被误抄成
^([.\u{22C5}\u{00B7}\u{2022}|[*])\s*
即把 alternation 的 `|` 放进了字符类内部。fancy-regex 把 `[...|[*]`
当作单个畸形字符类,报 ParseError(20, InvalidClass);`re!` 宏的
`.unwrap()` 因此 panic。
上游 mhchemParser 4.2.2 是 `[...]|[*]`(alternation 在类外),修正为
^([.\u{22C5}\u{00B7}\u{2022}]|[*])\s*
触发路径:用户提交含 `\ce{·}`/`\ce{•}`/`\ce{.}` 等化学式的文章 →
render_markdown_enhanced → katex::render_* → expand_chem → mhchem::ce,
首次编译该 LazyLock 正则即 panic。
关键放大因素:本项目 release profile 为 panic="abort",`to_tex` 内的
`std::panic::catch_unwind` 在此设置下无法捕获 panic,进程直接 SIGABRT。
修正了 `ce`/`pu` 的文档注释,不再谎称"绝不 panic"。
新增:
- 回归测试 ce_dot_bullet_bond_inputs_dont_panic:覆盖触发输入。
- 保护性测试 all_match_patterns_compile:强制编译 match_pattern 全部分支,
任何同类转录错误在测试期而非生产期暴露。
This commit is contained in:
parent
3e9a3aa70d
commit
feaf76bcd9
@ -412,7 +412,7 @@ fn match_pattern(name: &str, input: &str) -> Option<MMatch> {
|
|||||||
", " => re_match(re!("^[,;]\\s*")),
|
", " => re_match(re!("^[,;]\\s*")),
|
||||||
"," => re_match(re!("^[,;]")),
|
"," => re_match(re!("^[,;]")),
|
||||||
"." => re_match(re!("^[.]")),
|
"." => re_match(re!("^[.]")),
|
||||||
". __* " => re_match(re!("^([.\u{22C5}\u{00B7}\u{2022}|[*])\\s*")),
|
". __* " => re_match(re!("^([.\u{22C5}\u{00B7}\u{2022}]|[*])\\s*")),
|
||||||
"..." => re_match(re!("^\\.\\.\\.(?=$|[^.])")),
|
"..." => re_match(re!("^\\.\\.\\.(?=$|[^.])")),
|
||||||
"^a" => re_match(re!("^\\^([0-9]+|[^\\\\_])")),
|
"^a" => re_match(re!("^\\^([0-9]+|[^\\\\_])")),
|
||||||
"^\\x" => re_match(re!("^\\^(\\\\[a-zA-Z]+)\\s*")),
|
"^\\x" => re_match(re!("^\\^(\\\\[a-zA-Z]+)\\s*")),
|
||||||
@ -1197,17 +1197,24 @@ fn get_operator(a: &str) -> String {
|
|||||||
// 公开 API
|
// 公开 API
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
/// 把 `\ce{...}` 内容转译为 LaTeX。任何内部异常回退为原样输出。
|
/// 把 `\ce{...}` 内容转译为 LaTeX。
|
||||||
|
///
|
||||||
|
/// 注意:`to_tex` 内的 `catch_unwind` 在 `panic = "abort"`(本项目 release
|
||||||
|
/// profile)下**无法**捕获 panic——它只是开发构建的护栏。生产环境的真正保障
|
||||||
|
/// 是 `all_match_patterns_compile` 测试(编译期确保所有 `re!` 正则合法)+
|
||||||
|
/// 状态机的 watchdog 防死循环。坏公式仍可能产出退化输出,但不应 panic。
|
||||||
pub fn ce(input: &str) -> String {
|
pub fn ce(input: &str) -> String {
|
||||||
to_tex(input, "ce")
|
to_tex(input, "ce")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把 `\pu{...}` 内容转译为 LaTeX。任何内部异常回退为原样输出。
|
/// 把 `\pu{...}` 内容转译为 LaTeX(容错同 [`ce`])。
|
||||||
pub fn pu(input: &str) -> String {
|
pub fn pu(input: &str) -> String {
|
||||||
to_tex(input, "pu")
|
to_tex(input, "pu")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_tex(input: &str, kind: &str) -> String {
|
fn to_tex(input: &str, kind: &str) -> String {
|
||||||
|
// 仅在 panic=unwind(dev/test)时有效;release 的 panic=abort 下此处的
|
||||||
|
// catch_unwind 无法阻止进程终止。保留它是为了本地开发时坏公式不炸测试。
|
||||||
let result = std::panic::catch_unwind(|| {
|
let result = std::panic::catch_unwind(|| {
|
||||||
let parsed = go(input, kind);
|
let parsed = go(input, kind);
|
||||||
texify_go(&parsed, kind != "tex")
|
texify_go(&parsed, kind != "tex")
|
||||||
@ -1275,4 +1282,38 @@ mod tests {
|
|||||||
// 应含上标(^...)且无 panic。
|
// 应含上标(^...)且无 panic。
|
||||||
assert!(!tex.is_empty(), "离子应产出非空: {tex}");
|
assert!(!tex.is_empty(), "离子应产出非空: {tex}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 回归测试:`". __* "` 模式的 regex 曾因转录错误把 `|` 放进了字符类
|
||||||
|
/// 内部(`[.\u{22C5}\u{00B7}\u{2022}|[*]`),fancy-regex 报
|
||||||
|
/// `ParseError(20, InvalidClass)`,`re!` 宏的 `.unwrap()` 触发 panic。
|
||||||
|
/// 上游 mhchemParser 是 `[...]|[*]`(alternation 在类外)。
|
||||||
|
/// 这些输入会强制该 LazyLock 正则编译,必须产出非空且不 panic。
|
||||||
|
#[test]
|
||||||
|
fn ce_dot_bullet_bond_inputs_dont_panic() {
|
||||||
|
for s in [".", "·", "•", "⋅", "*", ". ", "••", "· ", "H·OH", "CaCO3 · H2O"] {
|
||||||
|
let tex = ce(s);
|
||||||
|
assert!(!tex.is_empty(), "ce({s:?}) 不应为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 保护性:强制 `match_pattern` 的每个分支至少运行一次,让每个 `re!`
|
||||||
|
/// 定义的 regex 都被编译。任一转录错误都会在此暴露,而非等到生产环境
|
||||||
|
/// 被特定输入触发后 panic=abort 整个进程。
|
||||||
|
#[test]
|
||||||
|
fn all_match_patterns_compile() {
|
||||||
|
for name in [
|
||||||
|
"empty", "else", "else2", "space", "space A", "space$", "a-z", "x", "x$", "i$",
|
||||||
|
"letters", "\\greek", "one lowercase latin letter $",
|
||||||
|
"$one lowercase latin letter$ $", "one lowercase greek letter $", "digits",
|
||||||
|
"-9.,9", "-9.,9 no missing 0", "(-)(9)^(-9)", "_{(state of aggregation)}$",
|
||||||
|
"{[(", ")]}", ", ", ",", ".", ". __* ", "...", "^a", "^\\x", "^(-1)", "'", "_9",
|
||||||
|
"_\\x", "^_", "{}^", "{}", "=<>", "#", "+", "-$", "-9", "- orbital overlap", "-",
|
||||||
|
"pm-operator", "operator", "arrowUpDown", "->", "CMT", "1st-level escape", "\\,",
|
||||||
|
"\\ca", "\\x", "orbital", "others", "oxidation$", "d-oxidation$", "1/2$",
|
||||||
|
"(KV letters),", "uprightEntities", "/", "//", "*",
|
||||||
|
] {
|
||||||
|
// 返回值不重要,只要不 panic(regex 编译成功)即可。
|
||||||
|
let _ = match_pattern(name, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user