fix(download): LiteLoader 不再套 bmclapi 改写,Central 库接上 bmclapi/maven

bmclapi-liteloader 改成 enabled=false(规则保留作形状留档):2026-11-10 实探
`apply()` 的真实产物 https://bmclapi2.bangbang93.com/maven/versions.json 404 / 9 B、
/maven/com/mumfrey/liteloader/1.7.10/liteloader-1.7.10_04.jar 404 / 9 B、
/maven/com/mumfrey/liteloader/{maven-metadata.xml,1.12.2-SNAPSHOT/maven-metadata.xml}
全 404 / 9 B;官方 dl.liteloader.com 的 versions.json 200 / 49129 B、artifact
200 / 533903 B。引擎遇 404 只会白丢一次请求再前进(NotFound 不重试),官方不可达时
更是直接失败,所以镜像优先用户不该再为每个 LiteLoader 文件试一次镜像。

新增 bmclapi-maven-central(repo1.maven.org/maven2 → bmclapi/maven):bmclapi 的
/maven 不只是 libraries.minecraft.net 的镜像,Central 的路径逐字节一致(asm-all 5.2
247787 B、gson 2.10.1 283367 B、log4j-core 2.17.1 1790452 B,sha1 逐一相同),不存在
的坐标两侧都是 404(镜像回 9 B 纯文本,没有「200 + HTML」假阳性)。

快照仓 repo.mumfrey.com 不加规则:bmclapi /maven、maven.aliyun.com/repository/central、
repo.huaweicloud.com/repository/maven 对 com/mumfrey/liteloader/1.12.2-SNAPSHOT/
maven-metadata.xml 全是 404,只有官方 200 / 358 B,所以只登记探测结果、不铺未验证的规则。

测试:新增 liteloader_urls_stay_official_only(镜像优先模式也只出官方一条,把 enabled
翻回 true 的变异实测会红);library_urls_expand_to_the_maven_subpath_on_bmclapi 补
Central 用例;形态判据的 EXPECTED 表加 bmclapi-maven-central。不探外网。

version 保持 3 不升 4:download/jdk_sources.rs 与 tests/game/java_vendor_chain.rs 两处
pin 了 version == 3,后者不在本 mission 的 scope(第一轮 workspace test 就是被这条 pin
抓红的),升版留给专门一轮;规则改动与探测记录写进 mirrors.json 的 notes。
This commit is contained in:
AskaEth committed 2026-11-10 13:56:58 +08:00
1 parent 54995a4af8
commit 146eb3d8db
2 files changed
+55 -33

No files matched your search

+13 -2
View File
@@ -1,11 +1,16 @@
{
"version": 3,
"notes": [
"M225(规则修订,version 仍为 3):① bmclapi-liteloader 关闭——2026-11-10 实测镜像侧全 404,LiteLoader 走官方直连;② 新增 bmclapi-maven-central,把 repo1.maven.org 的库接到 bmclapi 的 /maven(实测逐字节一致);③ 快照仓 repo.mumfrey.com 无任何镜像,登记为「不加速」。**不升 version 的原因**:v4 会把 `download/jdk_sources.rs::builtin_table_is_v3_with_all_four_source_families` 与 `tests/game/java_vendor_chain.rs`(`mirror_table_v3_registers_all_four_source_families`)里的 `version == 3` pin 弄红,升版得同时改那两处——本轮只修规则,version 的世代标记留给一次专门的升版。",
"v3(D23/M36):adoptium 表收敛到清华 TUNA 一家(其余院校实测 404,登记保留但 enabled=false);新增 oracle_openjdk / microsoft_openjdk / zulu 三张表,供 game::java 的三发行版回退链使用。",
"华为云 / 腾讯云 / 阿里云没有 Adoptium 镜像:华为云门户对任意不存在的路径都回 200 + 12KB HTML(假阳性,M34 实测),腾讯云 /v1/source.js 与华为云 /v1/repositories 的权威清单里都没有 JDK 项。据此不再登记 adoptium.huaweicloud。",
"oracle_openjdk 的目录与文件名与 download.java.net 完全同构,但 hash 段(f2283984656d49d69e91c558476027ac)不可推导——候选 URL 只能来自对镜像目录页的解析,禁止拼装。",
"microsoft_openjdk 没有国内镜像,也没有 GitHub 资产(api.github.com 的 microsoft/openjdk releases 里 assets 全空);唯一渠道是 aka.ms 模板 301 → download.visualstudio.microsoft.com(国内约 400 KB/s)。",
"zulu 的 API size 字段与真实字节数差 133 B,只能用于展示;校验一律用 sha256_hash。"
"zulu 的 API size 字段与真实字节数差 133 B,只能用于展示;校验一律用 sha256_hash。",
"LiteLoader 的 bmclapi 改写已关闭(bmclapi-liteloader.enabled=false,规则保留作形状留档):2026-11-10 实探 curl -sS -L -o /dev/null -w '%{http_code} %{size_download}' —— 官方 https://dl.liteloader.com/versions/versions.json 200 / 49129 B、.../com/mumfrey/liteloader/1.7.10/liteloader-1.7.10_04.jar 200 / 533903 B;镜像侧(`apply()` 的真实产物:`from` 吃掉 `/versions` 后剩下 `/versions.json`)https://bmclapi2.bangbang93.com/maven/versions.json 404 / 9 B、/maven/com/mumfrey/liteloader/1.7.10/liteloader-1.7.10_04.jar 404 / 9 B、/maven/com/mumfrey/liteloader/{maven-metadata.xml,1.12.2-SNAPSHOT/maven-metadata.xml} 也是 404 / 9 B。(任务描述里的 `/maven/versions/versions.json` 也实测 404,但那条双段路径不是这条规则拼出来的,别照样抄。)引擎遇 404 会前进到官方(NotFound 不重试),所以留着这条只会让镜像优先用户每个文件白丢一次请求;官方不可达时更是直接失败。",
"bmclapi-maven-central 的依据:bmclapi 的 /maven 是真的 maven 代理(不止 libraries.minecraft.net)。2026-11-10 实测 —— 官方 repo1.maven.org 与镜像 200 且字节数一致、sha1 逐一相同:org/ow2/asm/asm-all/5.2 (247787 B)、com/google/code/gson/gson/2.10.1 (283367 B)、org/apache/logging/log4j/log4j-core/2.17.1 (1790452 B),另有 asm 9.3 / asm-util 9.3 / jna 5.13.0 / annotations 24.0.1 字节数一致;不存在的坐标两侧都是 404(镜像回 9 B 纯文本,无「200 + HTML」假阳性)。注意两点:① 镜像还是上游的超集(org/spongepowered/mixin/0.8.5 在 Central 是 404、镜像 200 / 1089277 B),命中超集不影响校验(清单自带 sha1);② 镜像对 Range 请求回 200 全量而非 206,所以这条规则只对不走分块的资源有实际意义(库文件是 Library,本来就 not prefers_chunked)。",
"快照仓不加速(不登记规则):LiteLoader 的快照分支固定在 https://repo.mumfrey.com/content/repositories/snapshots/(M222 的 SNAPSHOT_REPO)。2026-11-10 实测官方 .../com/mumfrey/liteloader/1.12.2-SNAPSHOT/maven-metadata.xml 200 / 358 B,而候选镜像 bmclapi /maven 404 / 9 B、maven.aliyun.com/repository/central 404 / 689 B、repo.huaweicloud.com/repository/maven 404 / 394 B —— 没有任何镜像有 com/mumfrey,所以这条还是直连,别按「同族都该有镜像」的直觉加规则。",
"repo.maven.apache.org(Central 的官方别名)实测 200 / 247787 B,镜像同路径也 200 / 247787 B,但本仓库没有任何生产代码用这个 host(发行版库基址用的是 repo1.maven.org),所以不登记第二条规则——要用时再加,别为「可能有人用」提前铺。"
],
"gh_proxies": [
{
@@ -156,10 +161,16 @@
"from": "https://meta.quiltmc.org",
"to": "https://bmclapi2.bangbang93.com/quilt-meta"
},
{
"id": "bmclapi-maven-central",
"from": "https://repo1.maven.org/maven2",
"to": "https://bmclapi2.bangbang93.com/maven"
},
{
"id": "bmclapi-liteloader",
"from": "https://dl.liteloader.com/versions",
"to": "https://bmclapi2.bangbang93.com/maven"
"to": "https://bmclapi2.bangbang93.com/maven",
"enabled": false
},
{
"id": "bmclapi-authlib-injector",
+42 -31
View File
@@ -30,29 +30,21 @@ fn prefix_rule_preserves_sub_path() {
.expect("libraries 规则缺失");
let candidate = rule.apply(LIBRARIES_URL).expect("重写失败");
assert_eq!(candidate.source, "bmclapi");
// 子路径逐字保留(`/maven` 是镜像侧的固定前缀,不属于子路径)。
//
// M210/D1:这条断言此前写的是「换成 `https://bmclapi2.bangbang93.com` 就完事」,等于把
// **丢 inject 的 bug 钉成了期望**——实测 70/70 个库 404(无 `/maven` 回 404 + 9 B,
// 带 `/maven` 回 200、sha1 与官方逐字节一致)。现在把 `/maven` 写进期望值。
assert_eq!(
candidate.url,
LIBRARIES_URL.replace(
"https://libraries.minecraft.net",
"https://bmclapi2.bangbang93.com/maven"
)
// 子路径逐字保留(`/maven` 是镜像侧的固定前缀,不属于子路径)。M210/D1:这条断言此前写成
// 「换成 `https://bmclapi2.bangbang93.com` 就完事」,把**丢 inject 的 bug 钉成了期望**——实测
// 无 `/maven` 是 70/70 个库 404 + 9 B,带 `/maven` 回 200 且 sha1 与官方逐字节一致。
let expected = LIBRARIES_URL.replace(
"https://libraries.minecraft.net",
"https://bmclapi2.bangbang93.com/maven",
);
assert_eq!(candidate.url, expected);
}
/// **守卫(M210/D1)**:每一条「给 libraries 用」的镜像规则,镜像前缀都必须带 `/maven`。
///
/// 官方库域名(`libraries.minecraft.net`)与各 loader 的 maven 在 BMCLAPI 上统一挂在 `/maven`
/// 下;`apply()` 是纯前缀替换,`to` 里少了这一段就会把 `com/google/guava/...` 直接拼到镜像根上。
///
/// 抓得住:删掉/改错任一条库镜像规则的 `inject`(本用例对 `bmclapi-libraries` 的变异会红)。
/// 抓不住:`/maven` **写歪**(如 `/maven2`、`/maven/` 之外的等价路径)只能靠行为用例
/// [`library_urls_expand_to_the_maven_subpath_on_bmclapi`];路径结构本就不同的镜像表(资源对象走
/// `/assets`、元数据走 `/fabric-meta`)不在此判据内;新增但 `from` 不像 maven 仓库的库镜像也不在此列。
/// **守卫(M210/D1)**:每一条「给 libraries 用」的镜像规则,镜像前缀都必须带 `/maven`。官方库域名与
/// 各 loader 的 maven 在 BMCLAPI 上都挂在 `/maven` 下,而 `apply()` 是纯前缀替换——`to` 里少了这一段
/// 就会把 `com/google/guava/...` 直接拼到镜像根上。抓得住:删掉/改错任一条库镜像规则的 `inject`;
/// 抓不住:`/maven` **写歪**(如 `/maven2`)只能靠行为用例 [`library_urls_expand_to_the_maven_subpath_on_bmclapi`];
/// `/assets`、`/fabric-meta` 这类结构本就不同的镜像表,以及 `from` 不像 maven 仓库的新库镜像,都不在列。
#[test]
fn every_library_mirror_rule_carries_the_maven_segment() {
let table = MirrorTable::builtin();
@@ -63,14 +55,14 @@ fn every_library_mirror_rule_carries_the_maven_segment() {
|| from.contains("maven")
|| host_of(from) == Some("libraries.minecraft.net")
}
// 判据覆盖的规则 id(内置表里就是这 5 条;`bmclapi-liteloader` 的 `from` 不像 maven 仓库,不在列,
// 它指向的镜像路径另有一个「镜像上不存在」的观察,见 M210 报告的 §5)。
const EXPECTED: [&str; 5] = [
// 判据覆盖的规则 id(内置表里就是这 6 条;`bmclapi-liteloader` 的 `from` 不像 maven 仓库,且已因 M225 关闭,不在列)。
const EXPECTED: [&str; 6] = [
"bmclapi-libraries",
"bmclapi-forge-maven",
"bmclapi-forge-maven-net",
"bmclapi-neoforge",
"bmclapi-fabric-maven",
"bmclapi-maven-central",
];
let mut checked: Vec<&str> = Vec::new();
for rule in table.rules.iter().filter(|rule| serves_maven(&rule.from)) {
@@ -99,11 +91,10 @@ fn every_library_mirror_rule_carries_the_maven_segment() {
);
}
/// **行为守卫(M210/D1)**:真实库 URL 展开后必须落在镜像的 `/maven/<子路径>` 上。
///
/// 与上一条互为补充:那条看规则形态,这条看用户实际会请求到的 URL。
/// 抓得住:`inject` 丢失、`to` 里手写 `/maven` 被删、或 `from`/`to` 被改成拼不出 `/maven` 的形态。
/// 抓不住:只覆盖这 4 个官方 maven 根(各处未必都写全);镜像侧真实可达性(要网络,留给 e2e)。
/// **行为守卫(M210/D1)**:真实库 URL 展开后必须落在镜像的 `/maven/<子路径>` 上。与上一条互为
/// 补充:那条看规则形态,这条看用户实际会请求到的 URL。抓得住:`inject` 丢失、`to` 里手写 `/maven`
/// 被删、或 `from`/`to` 被改成拼不出 `/maven` 的形态;抓不住:只覆盖这 5 个官方 maven 根(各处未必
/// 都写全),镜像侧真实可达性要网络(留给 e2e)。
#[test]
fn library_urls_expand_to_the_maven_subpath_on_bmclapi() {
let table = MirrorTable::builtin();
@@ -125,6 +116,11 @@ fn library_urls_expand_to_the_maven_subpath_on_bmclapi() {
"https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.80/neoforge-21.1.80-installer.jar",
"https://bmclapi2.bangbang93.com/maven/net/neoforged/neoforge/21.1.80/neoforge-21.1.80-installer.jar",
),
// M225:LiteLoader 的库来自 Central(`net.minecraft:launchwrapper` 才是 Mojang 库仓)。
(
"https://repo1.maven.org/maven2/org/ow2/asm/asm-all/5.2/asm-all-5.2.jar",
"https://bmclapi2.bangbang93.com/maven/org/ow2/asm/asm-all/5.2/asm-all-5.2.jar",
),
];
for (official, expected) in cases {
let candidates = table.expand(official, ResourceKind::Library, &region);
@@ -139,6 +135,22 @@ fn library_urls_expand_to_the_maven_subpath_on_bmclapi() {
}
}
/// **守卫(M225)**:LiteLoader 不套 bmclapi 改写——镜像侧 `versions.json` 与 artifact 实测 404 / 9 B,
/// 官方 200(curl 记录见 `mirrors.json` 的 notes)。抓得住:`bmclapi-liteloader.enabled` 被翻回 true、
/// 或有人给快照仓补来源不明的镜像规则;抓不住:镜像侧哪天真的补上了(那时改期望与 notes,别删测试)。
#[test]
fn liteloader_urls_stay_official_only() {
let (table, region) = (MirrorTable::builtin(), RegionProfile::mirror_first(0));
for url in [
"https://dl.liteloader.com/versions/versions.json",
"https://dl.liteloader.com/versions/com/mumfrey/liteloader/1.7.10/liteloader-1.7.10_04.jar",
"https://repo.mumfrey.com/content/repositories/snapshots/com/mumfrey/liteloader/1.12.2-SNAPSHOT/maven-metadata.xml",
] {
let got = table.expand(url, ResourceKind::Library, &region);
assert_eq!(got.len(), 1, "{url} 不该有镜像候选:{got:?}");
}
}
#[test]
fn asset_rule_injects_path_prefix() {
let table = MirrorTable::builtin();
@@ -222,9 +234,8 @@ fn pinned_or_disabled_gh_proxy_is_respected() {
.expect("自定义前缀未生效");
assert_eq!(proxy.url, format!("https://proxy.example/{GITHUB_URL}"));
// 「默认前缀」的唯一真相源是**地区档案**(M193:`MirrorTable::set_gh_proxy` /
// `default_gh_proxy` 已删除——它们改/读的是表里的前缀目录,而 `expand()` 读档案,
// 属于「看起来能改、其实没人读」。这条断言从此钉在档案上)。
// 「默认前缀」的唯一真相源是**地区档案**(M193:`MirrorTable::set_gh_proxy` / `default_gh_proxy`
// 已删除——它们改/读的是表里的前缀目录,而 `expand()` 读档案,属于「看起来能改、其实没人读」)。
assert_eq!(
RegionProfile::mirror_first(0).gh_proxy.as_deref(),
Some(unlucky_core::download::region::DEFAULT_GH_PROXY)