From 0eaefebf3bb3ce3d446e8c7841b81f9fe6cf3546 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 9 Nov 2026 22:16:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(core):=20=E5=90=8C=E4=B8=80=20loader=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=20id=20=E6=92=9E=E4=B8=A4=E6=9D=A1=E7=88=B6?= =?UTF-8?q?=E9=93=BE=E6=97=B6=E5=85=B7=E5=90=8D=E6=8A=A5=E9=94=99=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E8=A6=86=E7=9B=96=E7=9B=98=E4=B8=8A=E6=B8=85=E5=8D=95?= =?UTF-8?q?=EF=BC=88=E6=97=A7=E5=AE=9E=E4=BE=8B=E4=BC=9A=E9=9D=99=E9=BB=98?= =?UTF-8?q?=E6=8D=A2=E7=88=B6=E9=93=BE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/core/src/addon/loader/plan.rs | 29 ++++++++++++++ crates/core/tests/liteloader/guards.rs | 55 ++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/crates/core/src/addon/loader/plan.rs b/crates/core/src/addon/loader/plan.rs index 087c3dc..06ecabe 100644 --- a/crates/core/src/addon/loader/plan.rs +++ b/crates/core/src/addon/loader/plan.rs @@ -182,6 +182,21 @@ impl LoaderInstallPlan { /// 父版本缺了)。**不能默默回落成叠原版**:那会装出一份和用户意图不同的实例。 pub fn build(input: LoaderPlanInput<'_>) -> Result { let release = input.release; + // 同一个 loader 版本 id 不能同时表示两条父链(`<原版>-liteloader-` 与 + // 「叠在 Forge 上」撞同一个目录):盘上那份的 `inheritsFrom` 与本次请求不符就具名报错, + // 而不是把旧清单覆盖掉——旧实例会静默换父链,启动行为跟着变。 + if let Some(existing) = existing_parent(input.layout, &release.version_id) { + if existing != release.parent_version_id { + return Err(LoaderError::unsupported(format!( + "{} 已装在盘上,父版本是 {existing},与本次请求的 {} 不同", + release.version_id, release.parent_version_id + )) + .with_hint(format!( + "loader 版本 id 由「原版版本 + 加载器 + 加载器版本」决定,叠在别的层上会撞同一个目录:先删掉 versions/{}(旧实例也会失去这一层),或换一个加载器版本", + release.version_id + ))); + } + } if !input.parent_installed && !release.parent_is_vanilla() { return Err(LoaderError::not_found(format!( "父版本 {} 还没装好:{} {} 要叠在它上面", @@ -262,3 +277,17 @@ impl LoaderInstallPlan { summary } } + +/// 盘上那份 `versions//.json` 的 `inheritsFrom`(不存在 / 读不出 / 没写 → `None`)。 +/// +/// 只用来挡「同一版本 id 撞两条父链」,不参与其它判定:读失败不该让安装失败, +/// 所以错误一律吞成 `None`(真正的父子链校验由 [`crate::game::inherit::resolve`] 与 +/// [`LoaderPlanInput::parent_installed`] 负责)。 +fn existing_parent(layout: &GameLayout, version_id: &str) -> Option { + let text = std::fs::read_to_string(layout.version_json(version_id)).ok()?; + let manifest = VersionJson::from_json_str(&text).ok()?; + manifest + .inherits_from + .map(|parent| parent.trim().to_string()) + .filter(|parent| !parent.is_empty()) +} diff --git a/crates/core/tests/liteloader/guards.rs b/crates/core/tests/liteloader/guards.rs index 37e6902..4d45bfe 100644 --- a/crates/core/tests/liteloader/guards.rs +++ b/crates/core/tests/liteloader/guards.rs @@ -155,3 +155,58 @@ fn cancel_leaves_no_manifest() { unlucky_core::game::GameError::MissingParent { .. } )); } + +/// 同一个 loader 版本 id 被两条父链请求:**具名报错**,且盘上那份清单一个字节都不动。 +/// +/// 为什么必须有这道门:`<原版>-liteloader-` 对「叠原版」与「叠 Forge」是同一个目录名, +/// 直接覆盖会让已经登记、指向这个版本 id 的旧实例**静默换父链**(启动行为跟着变)。 +#[test] +fn parent_collision_is_refused_instead_of_overwriting() { + let temp = TempDir::new("parent-collision"); + let layout = GameLayout::new(temp.path()); + put_version(&layout, "1.12.2", VANILLA_1122); + put_version( + &layout, + "1.12.2-forge-14.23.5.2860", + crate::support::FORGE_1122, + ); + + let vanilla_release = liteloader_release("1.12.2"); + let existing = serde_json::json!({ + "id": vanilla_release.version_id, + "inheritsFrom": "1.12.2", + "mainClass": "net.minecraft.launchwrapper.Launch", + "arguments": {"game": ["--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker"]}, + "libraries": [] + }); + put_version( + &layout, + &vanilla_release.version_id, + &serde_json::to_string_pretty(&existing).expect("序列化"), + ); + let before = std::fs::read_to_string(layout.version_json(&vanilla_release.version_id)) + .expect("读旧清单"); + + let forge_release = liteloader_release("1.12.2-forge-14.23.5.2860"); + assert_eq!( + forge_release.version_id, vanilla_release.version_id, + "两条父链撞同一个 id" + ); + let error = LoaderInstallPlan::build(LoaderPlanInput { + layout: &layout, + release: &forge_release, + environment: &Environment::host(), + parent_installed: true, + }) + .expect_err("撞父链必须报错"); + assert_eq!(error.kind(), LoaderErrorKind::Unsupported); + assert!( + error.message().contains("1.12.2-forge-14.23.5.2860") && error.message().contains("1.12.2"), + "要说清两条父链各是什么:{}", + error.message() + ); + + let after = std::fs::read_to_string(layout.version_json(&vanilla_release.version_id)) + .expect("读旧清单"); + assert_eq!(before, after, "报错之后盘上那份清单不许被改动"); +}