fix(core): 守卫的剥注释不再吞文件 + 变异 D 变红(M213 复审 P1-1 / P2-1)
复审 P1-1:`code_lines()` 对每一行找 `/*`,找着就当块注释开头。本仓库的文档注释里到处是 glob(`/usr/lib/jvm/*`、`rules.d/*.yaml`、`crash-reports/*.txt`、`.github/workflows/**`), 字符串里也有(`"mods/*.jar"`),这些 `/*` 没有配对的 `*/`,于是 in_block 一旦为真就再也 不会变假 —— 该文件从那一行到 EOF 全部被吞掉。实测 crates/core/src 里 8/371 个文件中招, 其中一个是 game/java/discover.rs(它自己有一处 reg query),复审判的变异 D 因此全绿。 - code_lines 只在「去掉前导空白后以 /* 开头」的行上进入块注释态,行中/字符串里的 /* 一律 当普通代码。本仓库零处行首块注释,所以结构上不可能再吞代码。 - 新增 code_lines_is_not_fooled_by_glob_slashes_in_strings_or_docs:字符串里的 /* 不许 吞掉后一行、文档 glob 不许吞掉真代码、真块注释仍要被剥。 - 新增 the_scan_reaches_the_end_of_every_file:逐文件核对**最后一行代码的行号**必须出现在 扫面结果里(比数总行数尖锐 —— 文件普遍以 } 结尾,光比内容会被别处的 } 蒙混),外加 全仓可见行占比 ≥60% 的兜底(本机实测 81%)。 - 变异 D(discover.rs:156 改回裸 Command::new)现在 exit 101 并点名该行;A/B/C 复验仍红。 复审 P2-1:原注释说「两个 flag 不能叠加」,按 MSDN 这是错的。改成:两个 flag 在 Win32 层 可任意组合,真正的坑是 std 的 creation_flags 是 setter(后一次静默覆盖前一次)。结论不变 (launch/process.rs 仍零改动),顺带修正 DETACHED_PROCESS 的说明。
This commit is contained in:
1 parent
40a296471d
commit
6dd88a27a3
2 files changed
+165
-38
No files matched your search
@@ -14,12 +14,21 @@
|
||||
//!
|
||||
//! ## 谁**不许**走这里
|
||||
//!
|
||||
//! **游戏进程**([`crate::launch::process::RunningProcess::spawn`])不走这里:它要
|
||||
//! `CREATE_NEW_PROCESS_GROUP` 才能用 `taskkill /T` 回收整棵进程树,两者是同一枚
|
||||
//! `creation_flags` 参数上的不同取值,不能简单叠加。「游戏要不要带自己的控制台窗口」是
|
||||
//! **产品决定**(玩家要看游戏自己的输出窗口),不是顺手抑制的事——所以 M213 一个字没动它。
|
||||
//! **游戏进程**([`crate::launch::process::RunningProcess::spawn`])保留自己的
|
||||
//! `CREATE_NEW_PROCESS_GROUP`(`0x0000_0200`),以便 `taskkill /T` 递归回收整棵进程树。
|
||||
//!
|
||||
//! 这两个 flag 在 **Win32 层是可以任意组合的**:[Process Creation Flags][flags] 原文
|
||||
//! 「They can be specified in any combination, except as noted」,而两条各自 not 掉的只有
|
||||
//! `CREATE_NEW_CONSOLE` / `DETACHED_PROCESS`,与这一组无关。真正的坑在 **Rust std 这一层**:
|
||||
//! `CommandExt::creation_flags` 是 **setter 不是累加器**——「先 `quiet()` 再
|
||||
//! `.creation_flags(CREATE_NEW_PROCESS_GROUP)`」会把 `CREATE_NO_WINDOW` **静默覆盖掉**。
|
||||
//!
|
||||
//! 而「游戏要不要带自己的控制台窗口」是**产品决定**(玩家要看游戏自己那段输出),不是顺手
|
||||
//! 抑制的事——所以这里不复用 `quiet`,显式只设 group flag,M213 一个字没动它。
|
||||
//!
|
||||
//! 那边是唯一另一处合法的直接构造点,两处都由 `crates/core/tests/child_quiet.rs` 的守卫钉住。
|
||||
//!
|
||||
//! [flags]: https://learn.microsoft.com/windows/win32/procthread/process-creation-flags
|
||||
|
||||
use std::ffi::OsStr;
|
||||
use std::process::Command;
|
||||
@@ -28,7 +37,8 @@ use std::process::Command;
|
||||
///
|
||||
/// 见 <https://learn.microsoft.com/windows/win32/procthread/process-creation-flags>。
|
||||
/// 别和 `CREATE_NEW_CONSOLE`(`0x0000_0010`,意思正好相反)或
|
||||
/// `DETACHED_PROCESS`(`0x0000_0008`,连标准句柄都不给)搞混。
|
||||
/// `DETACHED_PROCESS`(`0x0000_0008`,子进程不继承父控制台、也不新建;标准句柄由
|
||||
/// `STARTUPINFO` 给,给了照样可用)搞混。
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
|
||||
@@ -17,27 +17,47 @@
|
||||
//! 但 `type A = B; type B = Command` 这种两跳的、或从别的模块 `pub use` 进来的类型别名抓不住。
|
||||
//! - **不经过 `::new` 的构造**:`std::process::Command` 目前只有 `new` / `default`,
|
||||
//! 没有 `From`,所以实际风险低;真出现了本文件看不见。
|
||||
//! - **行尾注释**:只看「整行是注释」的行。一条行尾注释里写 `Command::new` 会**误报**
|
||||
//! (方向安全:宁可红),因为这仓库的文档注释都是整行 `//` 风格,没必要为它写字符串解析。
|
||||
//! - **只剥「整行注释」**:行尾注释里写 `Command::new` 会**误报**;行中开始的跨行块注释
|
||||
//! (`let x = 1; /* …` 续行)之后那几行也会被当成代码,同样是**误报**。
|
||||
//! 两个方向都是「宁可红」,与 M213 复审 P1-1 那个假绿方向相反。
|
||||
//! - **空白**:判据是字面子串 `Command::new(` / `<别名>::new(`,不做空白归一化,
|
||||
//! 所以 `Command :: new(..)` 看不见。`rustfmt` 会把它规范回去,只有不经 `rustfmt`
|
||||
//! 的代码才有这个口子。
|
||||
//! - **别的 crate**:`crates/cli` / `crates/presenter` / `crates/app-iced` 也可能 spawn 进程,
|
||||
//! 本守卫的射程只有 `crates/core/src`(M213 的 scope)。
|
||||
//! 复审已确认至少一处漏网:`crates/app-iced/src/platform/locale.rs:86` 的 `reg.exe`。
|
||||
//! - **行为**:它只证明「代码里没有绕开构造器」,不证明「桌面上真的没闪窗口」。
|
||||
//! 窗口本身的目视验证不在本会话能力内,见 mission 报告。
|
||||
//!
|
||||
//! ## 变异验证(三条,全部实测过并已还原)
|
||||
//! ## 变异验证(四条,全部实测过并已还原)
|
||||
//!
|
||||
//! 1. 往 `download/region_system.rs` 加一处裸 `std::process::Command::new("tzutil")`
|
||||
//! ⇒ [`no_spawn_site_bypasses_the_quiet_constructor`] 红,报文点名
|
||||
//! `src/download/region_system.rs:163`(退出码 101)。
|
||||
//! ⇒ [`no_spawn_site_bypasses_the_quiet_constructor`] 红(退出码 101),报文点名该行
|
||||
//! (具体行号随夹具插在哪而变,第一轮 :163、复验 :162)。
|
||||
//! 2. 同一处改写成别名形式(`use std::process::Command as Spawn;` + `Spawn::new("tzutil")`)
|
||||
//! ⇒ 同样红,报文点名 `Spawn::new(...)`。
|
||||
//! 3. 删掉 `platform/child.rs` 里那行 `command.creation_flags(CREATE_NO_WINDOW)`
|
||||
//! ⇒ [`the_constructor_actually_passes_create_no_window`] 红。
|
||||
//! 4. 把 `game/java/discover.rs:156` 的 `quiet("reg")` 改回裸 `std::process::Command::new("reg")`
|
||||
//! ⇒ **必须**红(见下面 P1-1)。
|
||||
//!
|
||||
//! ### P1-1 复发记录:守卫自己被变异验了两遍才对的
|
||||
//!
|
||||
//! 第 3 条第一次跑时**没红**:那版断言搜的是整份源码,而 `quiet` 的文档注释里就写着
|
||||
//! `.creation_flags(CREATE_NO_WINDOW)`——注释把断言喂饱了。现在两个测试都先剥注释
|
||||
//! ([`code_lines`])再判,重跑才红。这条教训写在这里:**守卫本身也要被变异验过**,
|
||||
//! 否则它守的是注释。
|
||||
//! `.creation_flags(CREATE_NO_WINDOW)`——注释把断言喂饱了。改成先剥注释([`code_lines`])再判。
|
||||
//!
|
||||
//! 然后第 4 条(复审判的变异 D)**又没红**:那版 `code_lines` 对每一行找 `/*`,找着就当块注释
|
||||
//! 开头。可本仓库的文档注释里到处是 glob(`/usr/lib/jvm/*`、`rules.d/*.yaml`)、字符串里也有
|
||||
//! (`"mods/*.jar"`),这些 `/*` 没有配对的 `*/`,于是 `in_block` 一旦为真就再也不会变假,
|
||||
//! **该文件从那一行到 EOF 全部被吞掉**。实测 `crates/core/src` 里 **8/371 个文件**中招,其中
|
||||
//! 一个正是 `game/java/discover.rs`——守卫从来没看见过它的 `reg query`。
|
||||
//!
|
||||
//! 现在的 [`code_lines`] 只在**行首**(去掉前导空白)认 `/*`,本仓库零处行首块注释,所以结构上
|
||||
//! 不可能再吞代码;[`the_scan_reaches_the_end_of_every_file`] 专门盯住「整份文件被吞掉」这个
|
||||
//! 故障(逐文件核对最后一行代码的行号必须出现在扫面结果里)。
|
||||
//!
|
||||
//! 两次都是同一个教训:**守卫本身要被变异验过,而且要验在不止一个文件上**——变异 A/B 落在
|
||||
//! `region_system.rs`(那个文件恰好没中毒)才一直是绿的。
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -153,6 +173,88 @@ fn the_constructor_actually_passes_create_no_window() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_lines_is_not_fooled_by_glob_slashes_in_strings_or_docs() {
|
||||
// M213 复审 P1-1 的最小复现:**字符串**里的 `/*` 不许把后一行吞掉。
|
||||
let in_string = code_lines("let a = \"mods/*.jar\";\nlet b = 1;\n");
|
||||
assert!(
|
||||
in_string
|
||||
.iter()
|
||||
.any(|(number, line)| *number == 2 && line.contains("let b")),
|
||||
"字符串里的 `/*` 吞掉了后一行:{in_string:?}",
|
||||
);
|
||||
|
||||
// 同一份故障在**文档注释的 glob** 上的形态(`crates/core/src` 里 8 个文件都是这一种)。
|
||||
let in_doc_glob = code_lines("//! | Linux | `/usr/lib/jvm/*` |\nfn real_code() {}\n");
|
||||
assert_eq!(
|
||||
in_doc_glob.len(),
|
||||
1,
|
||||
"文档注释里的 glob 吞掉了真代码:{in_doc_glob:?}"
|
||||
);
|
||||
assert!(in_doc_glob[0].1.contains("fn real_code"));
|
||||
|
||||
// 真的块注释还是要被剥掉(别把上面两条修成「块注释一律不认」)。
|
||||
let block = code_lines("/* a\n b\n*/\nfn after() {}\n");
|
||||
assert_eq!(block.len(), 1, "块注释没被剥:{block:?}");
|
||||
assert!(block[0].1.contains("fn after"), "剥错了行:{block:?}");
|
||||
|
||||
// 同一行闭合的块注释,收尾后的残段是代码。
|
||||
let inline_close = code_lines("/* c */ fn inline() {}\n");
|
||||
assert_eq!(
|
||||
inline_close.len(),
|
||||
1,
|
||||
"同行闭合的块注释丢了残段:{inline_close:?}"
|
||||
);
|
||||
assert!(inline_close[0].1.contains("fn inline"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scan_reaches_the_end_of_every_file() {
|
||||
// 「整份文件被吞掉」这类故障自己红:逐文件核对**最后一行代码的行号**必须出现在扫面结果里。
|
||||
// 这条比「数总行数」尖锐——文件普遍以 `}` 结尾,光比内容会被别处的 `}` 蒙混过去。
|
||||
let root = src_root();
|
||||
let mut files = Vec::new();
|
||||
collect_rust_files(&root, &root, &mut files);
|
||||
files.sort();
|
||||
assert!(!files.is_empty(), "没扫到任何 .rs:{}", root.display());
|
||||
|
||||
let mut visible_total = 0usize;
|
||||
let mut line_total = 0usize;
|
||||
let mut starved: Vec<String> = Vec::new();
|
||||
for rel in &files {
|
||||
let text = read_source(&root, rel);
|
||||
let visible = code_lines(&text);
|
||||
visible_total += visible.len();
|
||||
line_total += text.lines().count();
|
||||
if visible.is_empty() {
|
||||
starved.push(format!("src/{rel}:整份文件没有一行可见代码"));
|
||||
continue;
|
||||
}
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let Some((last_index, last_line)) = lines.iter().enumerate().rev().find(|(_, line)| {
|
||||
let trimmed = line.trim();
|
||||
!trimmed.is_empty() && !trimmed.starts_with("//")
|
||||
}) else {
|
||||
starved.push(format!("src/{rel}:整份文件只有注释?"));
|
||||
continue;
|
||||
};
|
||||
let last_number = last_index + 1;
|
||||
if !visible.iter().any(|(number, _)| *number == last_number) {
|
||||
starved.push(format!(
|
||||
"src/{rel}:最后一行代码 `{}`(第 {last_number} 行)没被扫到 ⇒ 被吞到 EOF",
|
||||
last_line.trim()
|
||||
));
|
||||
}
|
||||
}
|
||||
assert!(starved.is_empty(), "扫面没走到底:\n{}", starved.join("\n"));
|
||||
|
||||
// 兜底:全仓可见行占比实测约 81%,门槛取 60% 留足余量。它只抓「大面积吞行」。
|
||||
assert!(
|
||||
visible_total * 10 >= line_total * 6,
|
||||
"可见行只有 {visible_total}/{line_total}(<60%),扫面异常缩水",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quiet_is_a_plain_command_that_still_captures_output() {
|
||||
// 抑制窗口不能顺手把输出吞了:`.output()` 的管道与退出码必须照旧可用。
|
||||
@@ -253,40 +355,55 @@ fn collect_rust_files(root: &Path, dir: &Path, out: &mut Vec<String>) {
|
||||
|
||||
/// 去掉注释后的可见行(行号保留,报错能指到原文件)。
|
||||
///
|
||||
/// 只剥「整行注释」与 `/* … */` 块;行尾注释不剥(见文件头的「抓不住什么」)。
|
||||
/// ## `/*` **只在行首**才算块注释开头(M213 复审 P1-1)
|
||||
///
|
||||
/// 上一版对每一行找 `/*`,找着就当块注释开头。可本仓库的文档注释里到处是 glob
|
||||
/// (`/usr/lib/jvm/*`、`rules.d/*.yaml`、`crash-reports/*.txt`、`.github/workflows/**`),
|
||||
/// 字符串里也有(`"mods/*.jar"`)。这些 `/*` 没有配对的 `*/`,于是 `in_block` 一旦为真就再也
|
||||
/// 不会变假,**该文件从那一行到 EOF 全部被吞掉**——`crates/core/src` 里 8/371 个文件如此,
|
||||
/// 其中一个正是 `game/java/discover.rs`(它自己有一处 `reg query`)。复审判的变异 D
|
||||
/// (把它改回裸构造)因此**全绿**:守卫根本没看见那处 spawn。
|
||||
///
|
||||
/// 现在只在「去掉前导空白后以 `/*` 开头」的行上进入块注释态,行中/字符串里的 `/*` 一律当普通
|
||||
/// 代码。本仓库没有任何行首块注释(`grep -rn '^[[:space:]]*/\*' crates/core/src` 零命中),
|
||||
/// 所以这条状态机在本仓库里实际只走「剥 `//` 整行注释」那一条路,**结构上不可能再吞代码**。
|
||||
///
|
||||
/// 代价见文件头:行中开始的跨行块注释之后那几行会被当成代码,方向是**误报**。
|
||||
fn code_lines(text: &str) -> Vec<(usize, &str)> {
|
||||
let mut visible: Vec<(usize, &str)> = Vec::new();
|
||||
let mut in_block = false;
|
||||
for (index, raw) in text.lines().enumerate() {
|
||||
let number = index + 1;
|
||||
let mut rest = raw;
|
||||
loop {
|
||||
if in_block {
|
||||
match rest.find("*/") {
|
||||
Some(end) => {
|
||||
rest = &rest[end + 2..];
|
||||
in_block = false;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
match rest.find("/*") {
|
||||
Some(start) => {
|
||||
visible.push((number, &rest[..start]));
|
||||
rest = &rest[start + 2..];
|
||||
in_block = true;
|
||||
}
|
||||
None => {
|
||||
visible.push((number, rest));
|
||||
break;
|
||||
}
|
||||
let trimmed = raw.trim_start();
|
||||
if in_block {
|
||||
// 块内只认 `*/`:它后面剩的残段可能是代码(`*/ let x = 1;`)。
|
||||
if let Some(end) = trimmed.find("*/") {
|
||||
in_block = false;
|
||||
push_tail(&mut visible, number, &trimmed[end + 2..]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with("/*") {
|
||||
match trimmed.find("*/") {
|
||||
Some(end) => push_tail(&mut visible, number, &trimmed[end + 2..]),
|
||||
None => in_block = true,
|
||||
}
|
||||
continue;
|
||||
}
|
||||
visible.push((number, raw));
|
||||
}
|
||||
visible
|
||||
.into_iter()
|
||||
.filter(|(_, line)| !line.trim_start().starts_with("//"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 块注释收尾后的残段:不是空、也不是新的一条整行注释,才算可见代码。
|
||||
fn push_tail<'a>(visible: &mut Vec<(usize, &'a str)>, number: usize, tail: &'a str) {
|
||||
let tail = tail.trim_start();
|
||||
if !tail.is_empty() && !tail.starts_with("//") {
|
||||
visible.push((number, tail));
|
||||
}
|
||||
}
|
||||
|
||||
/// 本文件里由 `std::process::Command` 导出的别名(`use ... Command as X;` / `type X = …Command;`)。
|
||||
|
||||
Reference in new issue
Block a user