diff --git a/crates/core/src/download/mirrors.json b/crates/core/src/download/mirrors.json index 722ffc8..49a0434 100644 --- a/crates/core/src/download/mirrors.json +++ b/crates/core/src/download/mirrors.json @@ -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", diff --git a/crates/core/tests/download/mirrors_test.rs b/crates/core/tests/download/mirrors_test.rs index 3ff95ba..2cbcce0 100644 --- a/crates/core/tests/download/mirrors_test.rs +++ b/crates/core/tests/download/mirrors_test.rs @@ -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, ®ion); @@ -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, ®ion); + 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)