Files
Unlucky-Launcher/crates/core/src/game/adopt.rs
T
AskaEth 347a3cc90d fix(game): 复审 r2 三项——Win32 尾点/尾空格别名 + 写面与游戏树重叠 + 更高祖先断链
r1 五项里 R4/R5 与 slug/大小写等直接案例已由 r2 复审独立通过;这三条是它补证出的**剩余边界**,
只做限定修复,不推翻重写:

r2.1(未存在认领路径的 Win32 尾点/尾空格别名):比较键新增 `fspaths::comparison_real()`——
已存在前缀用 `canonicalize`,**尚不存在的尾段按平台语义归一**:Windows 普通路径折叠尾 `.`/空格
(`Foo.` 与 `Foo ` 都是 `Foo`,Win32 建目录时就这么做),**verbatim 路径(`\?\…`)不折叠**
(那是另一个物理目录,合法)。原先 `dir_key` 只统一分隔符与大小写,于是一个尚未创建的
`custom` 认领 `Foo.` 与缺省派生 `Foo` 键不相等却被判定「不撞」,两个实例共用一个运行目录。

r2.2(`data`/`.ULL` 整体映射到受保护游戏树时仍静默外写):新增
`fspaths::ensure_surfaces_clear_of_game_tree()`——默认写盘面(`.ULL`、`config/`、
`config/instances/`、`profiles.json`、`data/`,以及缺省档的运行目录)**解析后不得落进游戏根**;
`plan`(dry-run 也报)与 `adopt`(写盘前)各核一次,重叠即 `adopt_write_surface_in_game_tree`
具名拒绝并提示把数据目录挪到游戏树之外。整体迁移到游戏树**之外**照旧允许(判据是落点不是链接)。
配置写面判据同时收窄职责:`ensure_config_write_surface` 只管「在 `.ULL` 内」,
不再带「`.ULL` 在游戏根里」的豁免特例——重叠由上面那条统一负责。

r2.3(游戏根更高祖先断链仍当空列表):`scan` 的 `NotFound` 分支改为沿祖先找**最近的已存在入口**,
它能 `lstat` 到但 `canonicalize` 不出来(断链的符号链接/目录联接、目标被搬走)⇒
`adopt_versions_unreadable`;一路到盘根都不存在才是「真正的新数据根」,空结果仍允许。
原先只查 `versions` 与游戏根两层,迁移联接在更高层断开时会误报空库。

新错误码:`adopt_write_surface_in_game_tree`(CLI hint 与 help 拒绝清单同步)。

验证(持久日志在 `target/m221-r2/`,git 忽略、不入库;上一轮 `target/m221-r1/` 证据未覆盖):
- `cargo fmt --all --check` 0;`cargo check -p unlucky-core -p unlucky-cli --all-targets` 0;
- `cargo test -p unlucky-core --test adopt` **43 passed / 0 failed**(另跑一次
  `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 同样 43 passed);
- `cargo test -p unlucky-cli --test adopt_test` **13 passed / 0 failed**;
- `cargo clippy -p unlucky-core -p unlucky-cli --all-targets -- -D warnings` 0;
- 额外:`cargo test -p unlucky-core --lib` **778 passed**、`cargo test -p unlucky-cli` 全部目标通过;
- 三次**实现-only 变异**(不折叠 Win32 尾点尾空格 / 不核游戏树重叠 / NotFound 直接当空)
  全部以**断言红**收场(1 / 4 / 2 条失败,非编译错误、非环境错误),每次还原后与基线逐文件一致,
  收尾再跑 core+cli 双绿。

界限:只覆盖 core+cli 的定向目标(不等于 workspace 全量);没有启动真实 Minecraft;GUI 未接;
判据是检查那一刻的快照(TOCTOU 看不到);Win32 8.3 短名与非 junction 重解析点未覆盖。
2026-11-09 23:56:02 +08:00

60 lines
3.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 登记「已经铺在盘上的版本」(从其他启动器的 `.minecraft` 只读接入,M221)。
//!
//! 依据:`DevDocs/artifacts/m218/SURVEY-p4.md`(④ 从其他启动器导入数据)与
//! `DevDocs/00-债务台账.md` §21 的第 ④ 条;模型口径仍按 D20(实例隔离只改**运行目录**,
//! `versions/` `assets/` `libraries/` 恒共享,见 [`crate::game::isolation::SHARED_LAYERS`])。
//!
//! # 一句话纪律
//!
//! **登记只读外部、只写自己**:本模块从不往游戏根写一个字节(不搬文件、不补清单、不改 mtime),
//! 唯一的写盘是 `profiles.json` + `config/instances/<id>.json`(+ 缺省运行目录的准备,若选了它)。
//! 因此「导入前后对 `versions/` `libraries/` 做全量哈希必须完全不变」是可以用测试钉死的事实。
//!
//! # 为什么要有它(现状的洞)
//!
//! [`GameLayout::installed_versions`] 早就会扫 `versions/`,但**没有任何代码把扫到的东西变成实例**。
//! 于是「别人的 `.minecraft` 已经装好了 1.20.1」这件事在 ULL 里看不见,用户只能重装一遍
//! (下载全部资源)。这个模块把那批版本变成实例档案,内容一个字节不搬。
//!
//! # 缺省隔离档**不能**沿用全局 `isolated`(本模块最要紧的一条)
//!
//! 全局 `instances.mode` 缺省是 `isolated`,而 `isolated` 的运行目录正是 `versions/<versionId>/`
//! ([`crate::game::isolation::derive_game_dir`])。若登记沿用这条缺省,第一次启动就会在
//! **别人的版本目录**里长出 `mods/` `saves/` `config/` `logs/` —— 不覆盖任何文件,但与
//! 「多启动器共存」的意图相反。所以 [`AdoptTarget`] 的缺省值是
//! [`AdoptTarget::InstanceDir`](落 ULL 数据区 `<data>/instances/<实例目录名>`),
//! 想共用/写进对方目录必须**显式**选,并且 [`target_notes`] 会把后果说清楚。
//!
//! # 分层
//!
//! - `candidate`:候选与逐项**结构化失败**(坏 JSON / 缺父 / 环 / 不可读 / 越界 / 符号链接);
//! - `error`:整次调用失败([`AdoptError`],带稳定错误码);
//! - `inspect`:只读扫描与逐项检查([`scan`] / [`inspect`]);
//! - `paths`:路径判据(版本 id 能不能当路径、符号链接有没有跑出游戏根);
//! - `fspaths`:路径语义(真实化 / Win32 尾点尾空格归一)与写盘面判据(配置在 `.ULL` 内、
//! 默认写盘面不得与受保护游戏树重叠);
//! - `run_dir`:运行目录派生(含**碰撞消歧**)与「缺省目录必须落在数据区」;
//! - `plan`:把候选 + 用户选择算成一次登记的落点([`plan`]),真正落盘是 [`adopt`]。
//!
//! # 不做什么
//!
//! - **不读账户凭据**:`launcher_profiles.json` / `launcher_accounts.json` / HMCL 的
//! `accounts.json` 一个都不碰(`AGENTS.md` §6 同族纪律);
//! - **不做多根**(D20 未改):扫的永远是**当前**游戏根(`core.game_home`),
//! 「同时管多个 `.minecraft`」是另一个更贵的模型改动,见 SURVEY-p4 §4.2;
//! - **不下载、不校验完整性**:版本 JSON 里 `libraries[].downloads.artifact.url` 指向 maven
//! 中心是正常形态,登记不因此拒绝,但也不假装校验过(人读输出里如实写成「未校验」)。
pub mod candidate;
mod error;
mod fspaths;
mod inspect;
mod paths;
mod plan;
mod run_dir;
pub use candidate::{AdoptCandidate, AdoptProblem, AdoptTarget, RegisteredInstance};
pub use error::AdoptError;
pub use inspect::{inspect, scan, AdoptScan};
pub use plan::{adopt, plan, target_notes, AdoptOutcome, AdoptPlan, AdoptRequest};