fix(game): 复审 r1 五项——运行目录不许被两实例共用 + 配置写面越界 + versions 断链 + 链接守卫反转 + 快照漏目录
复审 p2-5items 的五项,全部按「先能红的用例,再改实现」: R1(默认目录可被不同实例隐式共用,产品缺陷):新增 `adopt/run_dir.rs`——登记前读**每个已有实例** 的 `gameDir`(现役设置 + M27 前档案的遗留字段),用「真实路径 + 拼回不存在的尾段 + 大小写归一」 (Windows/macOS)比较;撞上就顺延 `<base>-2/-3…`(上限 32,撞满 `adopt_no_free_run_dir`)并在 `notes` 点名撞的是哪个实例;**非空且无人认领**仍拒绝(那是用户数据,不偷偷换目录)。这样 `Better MC` 与 `Better-MC`(slug 同名)、Windows 上 `Foo` 与 `foo`(同目录)都不会共用一个运行目录。 大小写归一在 macOS 上是**保守**选择:大小写敏感卷上会多一次顺延,换来的是「绝不共用」在任何卷上成立。 R2(配置写面 junction 写穿到外部游戏根,产品缺陷):`run_dir::ensure_config_write_surface()` 在 `plan`(dry-run 也报)与 `adopt`(写盘前)各核一次——`config/`、`config/instances/`、 `profiles.json` 的真实落点必须在 `<.ULL>` 内,且**不得经链接落进游戏根**(错误按真实归属分类: 「逃出 .ULL」vs「落进游戏根」)。`.ULL` 本身在游戏根内是布局事实,附加判据在该布局下零判别力, 主判据仍然拦下一切跑出 `.ULL` 的落点——这不是给链接开后门。新码 `adopt_config_surface_unsafe`。 R3(dangling versions 被当不存在,产品缺陷):`scan` 的 `read_dir` `NotFound` 再三分—— `lstat(versions)` 看见入口(断链符号链接/目录联接/目标被移走)或游戏根解析失败(祖先断链)⇒ `adopt_versions_unreadable`;两者都不是才算「真没有」。`inspect` 也补了同族判据(条目存在但 不是可读目录 ⇒ `directory_unreadable`,不再混成「没装」)。 R4(强制链接测试逻辑反转,假绿):`fixture::link_gate(created, required) -> Run|Skip|Fail` 是纯 真值表,`link_guard_with()` 显式传参(`link_guard()` 只读环境变量);新增 `link_guard_executes_all_three_branches` 用 `catch_unwind` **执行** Fail/Skip 两个分支。 四处调用点统一走 guard;建链接/联接的夹具收进 `fixture.rs`。 R5(快照遗漏目录节点,假绿):新增 `tests/adopt/snapshot.rs`——全节点快照(根/目录/文件/链接 各记类型 + 适用 mtime;目录记 mtime、链接只记目标不跟随、Windows 用重解析点属性认 junction、 读盘失败直接 panic)。**CLI 测试用 `#[path]` 复用同一份**,判据只一处;新增 `fixture_cases` 五条工具自测,`plan`/dry-run 的零写盘判据改用 `snapshot_opt`(连空目录都不许出现)。 验证(本轮持久日志,`target/m221-r1/`,git 忽略、不入库): - `cargo fmt --all --check` 0;`cargo check -p unlucky-core -p unlucky-cli --all-targets` 0; - `cargo test -p unlucky-core --test adopt` **37 passed / 0 failed**(另跑一次 `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 同样 37 passed); - `cargo test -p unlucky-cli --test adopt_test` **11 passed / 0 failed**; - `cargo clippy -p unlucky-core -p unlucky-cli --all-targets -- -D warnings` 0; - 四次**实现-only 变异**(缺省不查认领 / 配置写面不核 / versions 的 NotFound 当空 / 快照不记目录) 全部以**断言红**收场(不是编译错误、不是环境错误),每次还原后与基线清单逐文件一致,收尾再跑绿。 界限:只覆盖 core+cli 的定向目标(不等于 workspace 全量);没有启动真实 Minecraft;GUI 未接; 判据是检查那一刻的快照,TOCTOU 与非 junction 重解析点仍列边界。
This commit is contained in:
1 parent
9f5c001387
commit
8110cde3f5
19 files changed
+1363
-272
No files matched your search
@@ -3,8 +3,6 @@
|
||||
//! 每条断言下面都标了「抓得住 / 抓不住」:抓得住的是变异实现会变红的具体结论,
|
||||
//! 抓不住的是只能靠人读的东西(措辞、是否还有别的静默吞掉路径)。
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use unlucky_core::game::adopt::{self, AdoptCandidate};
|
||||
|
||||
use crate::fixture::{assert_unchanged, foreign_minecraft, snapshot, TempRoot};
|
||||
@@ -208,8 +206,8 @@ fn version_ids_that_cannot_be_paths_are_rejected() {
|
||||
///
|
||||
/// 抓得住:`ensure_inside` 不比较解析后的真实路径(那样 `1.20.1-link` 会变成「可登记」)。
|
||||
/// 抓不住 / 已知局限:
|
||||
/// - 本机不允许建符号链接(Windows 未开开发者模式、或非管理员)时这条会打印 `[symlink-guard]`
|
||||
/// 并**跳过**——显式记录而不是静静通过;设 `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 可让它必跑(建不出来即红);
|
||||
/// - 本机不允许建符号链接(Windows 未开开发者模式、或非管理员)时这条会打印 `[link-guard]`
|
||||
/// 并**跳过**——显式记录而不是静静通过;设 `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 则建不出来**必红**;
|
||||
/// - 目录联接(junction)由下面单独那条用例覆盖(不需要管理员权限,是更常见的形态)。
|
||||
#[test]
|
||||
fn a_symlinked_version_pointing_outside_the_game_root_is_rejected() {
|
||||
@@ -223,14 +221,11 @@ fn a_symlinked_version_pointing_outside_the_game_root_is_rejected() {
|
||||
)
|
||||
.expect("写外部清单");
|
||||
let link = root.game_root().join("versions/1.20.1-link");
|
||||
if !try_symlink_dir(&outside.join("1.20.1"), &link) {
|
||||
// 显式记录 + 环境变量可强制:不允许「什么都没跑还说绿」。
|
||||
assert_eq!(
|
||||
std::env::var("ULL_ADOPT_TEST_SYMLINK_REQUIRE").as_deref(),
|
||||
Ok("1"),
|
||||
"[symlink-guard] 本机不允许建符号链接,这条用例没有跑:\
|
||||
要强制它必跑设 ULL_ADOPT_TEST_SYMLINK_REQUIRE=1(建不出来即红)"
|
||||
);
|
||||
// 建不出来时:非强制模式打印一行并跳过;`ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 必红(R4)。
|
||||
if !crate::fixture::link_guard(
|
||||
crate::fixture::try_symlink_dir(&outside.join("1.20.1"), &link),
|
||||
"根外符号链接",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,7 +245,7 @@ fn a_symlinked_version_pointing_outside_the_game_root_is_rejected() {
|
||||
/// 游戏根内的符号链接(指向同一棵树里的真实版本目录)**不**拒绝:那是合法的省盘做法。
|
||||
///
|
||||
/// 抓得住:`check_symlink` 一刀切拒绝所有符号链接(这条会红)。
|
||||
/// 抓不住:本机建不出链接时同上跳过(打印 `[symlink-guard]`)。
|
||||
/// 抓不住:本机建不出链接时同上跳过(打印 `[link-guard]`,强制模式下必红)。
|
||||
#[test]
|
||||
fn a_symlink_inside_the_game_root_is_allowed() {
|
||||
let root = TempRoot::new("symlink-in");
|
||||
@@ -264,12 +259,10 @@ fn a_symlink_inside_the_game_root_is_allowed() {
|
||||
)
|
||||
.expect("写清单");
|
||||
let link = root.game_root().join("versions/1.20.1-thin");
|
||||
if !try_symlink_dir(&real, &link) {
|
||||
assert_eq!(
|
||||
std::env::var("ULL_ADOPT_TEST_SYMLINK_REQUIRE").as_deref(),
|
||||
Ok("1"),
|
||||
"[symlink-guard] 本机不允许建符号链接,这条用例没有跑"
|
||||
);
|
||||
if !crate::fixture::link_guard(
|
||||
crate::fixture::try_symlink_dir(&real, &link),
|
||||
"根内符号链接",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let candidate = candidate_of(&root, "1.20.1-thin");
|
||||
@@ -280,15 +273,6 @@ fn a_symlink_inside_the_game_root_is_allowed() {
|
||||
);
|
||||
}
|
||||
|
||||
/// 建目录符号链接(`true` = 建成;`false` = 本机没权限/不支持)。
|
||||
fn try_symlink_dir(target: &Path, link: &Path) -> bool {
|
||||
#[cfg(unix)]
|
||||
let created = std::os::unix::fs::symlink(target, link).is_ok();
|
||||
#[cfg(windows)]
|
||||
let created = std::os::windows::fs::symlink_dir(target, link).is_ok();
|
||||
created
|
||||
}
|
||||
|
||||
/// `versions/` 不存在 = 空结果(新数据根不是错误),但**不存在的游戏根不会被建出来**。
|
||||
///
|
||||
/// 抓得住:`scan` 顺手 `create_dir_all(versions)`(下面的断言会红),或把「不存在」当错误。
|
||||
@@ -315,7 +299,8 @@ fn a_missing_versions_dir_is_empty_not_an_error() {
|
||||
/// - 判据退回 `is_symlink`(除 Windows junction 外全都看不到,此时候选会变成「可登记」,第一条断言红);
|
||||
/// - 判据排到**解析清单之后**:根外那份清单是合法 JSON 且 id 与目录名一致,一旦走到解析,
|
||||
/// `manifest_id` 就会被填上(第二条断言红)——所以它至少能证明「没解析过根外清单」;
|
||||
/// - 库里没有 `mklink` 时打印 `[symlink-guard]` 并跳过(设 `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 必跑)。
|
||||
/// - 建不出链接时走 `fixture::link_guard`:非强制模式打印 `[link-guard]` 并跳过,
|
||||
/// 强制模式(`ULL_ADOPT_TEST_SYMLINK_REQUIRE=1`)**必红**(真值表见 `fixture::link_gate_truth_table`)。
|
||||
///
|
||||
/// 抓不住(证据边界,别把它读大):**只 stat/只 open 不解析**的读取这里看不出来
|
||||
/// (`manifest_id` 为 `None` 只说明没解析过,不说明没碰过文件);「边界检查排在
|
||||
@@ -335,12 +320,10 @@ fn a_junction_pointing_outside_is_rejected_without_reading_it() {
|
||||
)
|
||||
.expect("写外部清单");
|
||||
let link = root.game_root().join("versions").join("1.20.1-junction");
|
||||
if !crate::fixture::try_junction(&outside, &link) {
|
||||
assert_eq!(
|
||||
std::env::var("ULL_ADOPT_TEST_SYMLINK_REQUIRE").as_deref(),
|
||||
Ok("1"),
|
||||
"[symlink-guard] 本机建不出目录联接(mklink /J 失败),这条用例没有跑"
|
||||
);
|
||||
if !crate::fixture::link_guard(
|
||||
crate::fixture::try_junction(&outside, &link),
|
||||
"根外目录联接",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user