Files
Unlucky-Launcher/crates/core/src/download/mirrors.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

387 lines
12 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.
//! 镜像清单与 URL 前缀重写。
//!
//! 内置清单 `mirrors.json` 随源码嵌入(`include_str!`),发行后可由设置层追加自定义规则。
//! 重写语义承 analyse/03 §5.2 的「前缀替换而非全量重写」——保留子路径,因此 `/libraries`、
//! `/maven`、`/fabric-meta` 这类子路径天然兼容;`inject` 用于补上路径形状不同的场景
//! (资源对象:官方 `<xx>/<hash>`,BMCLAPI `/assets/<xx>/<hash>`)。
//!
//! gh-proxy 规则(用户定案):**只有国内模式 + GitHub host** 才套前缀,其它资源不受影响。
use serde::{Deserialize, Serialize};
pub use super::adoptium::AdoptiumMirror;
use super::jdk_sources::{MsOpenJdkTable, OracleMirror, ZuluTable};
use super::region::{RegionMode, RegionProfile};
use super::types::{Candidate, ResourceKind, SOURCE_OFFICIAL};
/// gh-proxy 候选的源标识。
pub const SOURCE_GH_PROXY: &str = "gh_proxy";
const fn default_true() -> bool {
true
}
/// 一条前缀重写规则。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrefixRule {
/// 规则 id(日志/诊断用)。
pub id: String,
/// 匹配的 URL 前缀(含 scheme)。
pub from: String,
/// 替换成的前缀。
pub to: String,
/// 插在 `to` 与剩余路径之间的内容(默认空)。
#[serde(default)]
pub inject: String,
/// 「仅加速不替代」:镜像优先模式下仍排在官方之后。
#[serde(default)]
pub fallback: bool,
/// 只对这些资源类型生效(空 = 全部)。
#[serde(default)]
pub kinds: Vec<ResourceKind>,
/// 是否启用(设置层可关)。
#[serde(default = "default_true")]
pub enabled: bool,
}
impl PrefixRule {
/// 构造一条最简规则。
#[must_use]
pub fn new(id: impl Into<String>, from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
id: id.into(),
from: from.into(),
to: to.into(),
inject: String::new(),
fallback: false,
kinds: Vec::new(),
enabled: true,
}
}
/// 设一个「仅加速」标记。
#[must_use]
pub fn fallback(mut self) -> Self {
self.fallback = true;
self
}
/// 设置注入片段。
#[must_use]
pub fn with_inject(mut self, inject: impl Into<String>) -> Self {
self.inject = inject.into();
self
}
/// 是否命中该 URL(前缀匹配 + 资源类型过滤 + 启用位)。
#[must_use]
pub fn matches(&self, url: &str, kind: ResourceKind) -> bool {
self.enabled
&& url.starts_with(&self.from)
&& (self.kinds.is_empty() || self.kinds.contains(&kind))
}
/// 生成镜像候选。
#[must_use]
pub fn apply(&self, url: &str) -> Option<Candidate> {
if !url.starts_with(&self.from) {
return None;
}
let rest = &url[self.from.len()..];
let rewritten = format!("{}{}{}", self.to, self.inject, rest);
Some(Candidate::new(source_id_for(&rewritten), rewritten))
}
}
/// 一个 gh-proxy 类前缀。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GhProxy {
/// 源标识。
pub id: String,
/// 展示名。
pub name: String,
/// 前缀(形如 `https://gh-proxy.com/`)。
pub prefix: String,
/// 是否启用。
#[serde(default = "default_true")]
pub enabled: bool,
}
/// 镜像表:内置规则 + 用户自定义规则。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MirrorTable {
/// 清单版本(便于诊断页显示「镜像表是哪一版」)。
pub version: u32,
/// 清单自带的说明(假阳性成因、不可推导项等,给运维看的原地注释)。
#[serde(default)]
pub notes: Vec<String>,
/// 前缀重写规则(前面的优先)。
#[serde(default)]
pub rules: Vec<PrefixRule>,
/// Adoptium 院校镜像。
#[serde(default)]
pub adoptium: Vec<AdoptiumMirror>,
/// Oracle OpenJDK 镜像(v3 新增)。
#[serde(default)]
pub oracle_openjdk: Vec<OracleMirror>,
/// Microsoft OpenJDK 分发模板(v3 新增)。
#[serde(default)]
pub microsoft_openjdk: Option<MsOpenJdkTable>,
/// Zulu 元数据(v3 新增)。
#[serde(default)]
pub zulu: Option<ZuluTable>,
/// gh-proxy 类前缀。
#[serde(default)]
pub gh_proxies: Vec<GhProxy>,
}
impl Default for MirrorTable {
fn default() -> Self {
Self::builtin()
}
}
impl MirrorTable {
/// 内置清单(编译期嵌入)。
#[must_use]
pub fn builtin() -> Self {
Self::parse(include_str!("mirrors.json")).unwrap_or_else(|err| {
debug_note(&format!("内置 mirrors.json 解析失败(不应发生):{err}"));
Self {
version: 0,
notes: Vec::new(),
rules: Vec::new(),
adoptium: Vec::new(),
oracle_openjdk: Vec::new(),
microsoft_openjdk: None,
zulu: None,
gh_proxies: Vec::new(),
}
})
}
/// 从 JSON 解析。
pub fn parse(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
/// 序列化成 JSON(诊断页/用户自定义文件回写用)。
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
/// 追加用户自定义规则(插到最前,优先于内置规则)。
pub fn add_custom_rule(&mut self, rule: PrefixRule) {
self.rules.insert(0, rule);
}
/// 覆盖 gh-proxy 前缀(设置项);传 `None` 表示不默认启用。
pub fn set_gh_proxy(&mut self, prefix: Option<&str>) {
match prefix {
Some(prefix) => {
if let Some(proxy) = self.gh_proxies.first_mut() {
proxy.prefix = prefix.to_string();
proxy.enabled = true;
} else {
self.gh_proxies.push(GhProxy {
id: "gh-proxy".to_string(),
name: "自定义 gh-proxy".to_string(),
prefix: prefix.to_string(),
enabled: true,
});
}
}
None => {
for proxy in &mut self.gh_proxies {
proxy.enabled = false;
}
}
}
}
/// 找到第一条命中的前缀规则。
#[must_use]
pub fn prefix_rule_for(&self, url: &str, kind: ResourceKind) -> Option<&PrefixRule> {
self.rules.iter().find(|rule| rule.matches(url, kind))
}
/// 清单里第一个启用的 gh-proxy 前缀(默认值来源;设置层用它初始化地区档案)。
#[must_use]
pub fn default_gh_proxy(&self) -> Option<&str> {
self.gh_proxies
.iter()
.find(|proxy| proxy.enabled && !proxy.prefix.is_empty())
.map(|proxy| proxy.prefix.as_str())
}
/// GitHub host 且允许时套 gh-proxy 前缀。
///
/// **生效前缀的唯一真相源是 `region.gh_proxy`**(设置层/地区判定写入,`None` = 用户关闭)。
/// 表格里的 `gh_proxies` 只是「已知可用前缀」清单,配合 `default_gh_proxy()` 给设置做默认值。
#[must_use]
pub fn gh_proxy_candidates(&self, url: &str, region: &RegionProfile) -> Vec<Candidate> {
if region.mode != RegionMode::MirrorFirst {
return Vec::new();
}
let Some(prefix) = region
.gh_proxy
.as_deref()
.map(str::trim)
.filter(|prefix| !prefix.is_empty())
else {
return Vec::new();
};
let Some(host) = host_of(url) else {
return Vec::new();
};
if !is_github_host(host) {
return Vec::new();
}
vec![Candidate::new(SOURCE_GH_PROXY, format!("{prefix}{url}"))]
}
/// 展开候选序列:镜像优先模式下镜像在前、官方兜底;直连模式下官方在前、镜像兜底。
#[must_use]
pub fn expand(&self, url: &str, kind: ResourceKind, region: &RegionProfile) -> Vec<Candidate> {
let official = Candidate::new(direct_source_id(url), url);
let mut mirrors: Vec<Candidate> = Vec::new();
let mut official_first = false;
for candidate in self.adoptium_candidates(url) {
push_unique(&mut mirrors, url, candidate);
}
if let Some(rule) = self.prefix_rule_for(url, kind) {
if let Some(candidate) = rule.apply(url) {
// 「仅加速不替代」的规则在镜像优先模式下也排官方之后。
official_first = rule.fallback;
push_unique(&mut mirrors, url, candidate);
}
}
for candidate in self.gh_proxy_candidates(url, region) {
push_unique(&mut mirrors, url, candidate);
}
if region.mode == RegionMode::MirrorFirst && !official_first {
mirrors.push(official);
mirrors
} else {
let mut out = Vec::with_capacity(mirrors.len() + 1);
out.push(official);
out.append(&mut mirrors);
out
}
}
}
fn push_unique(list: &mut Vec<Candidate>, official_url: &str, candidate: Candidate) {
if candidate.url == official_url || list.iter().any(|known| known.url == candidate.url) {
return;
}
list.push(candidate);
}
/// 直连候选的源标识。
///
/// 绝大多数资源是 Mojang/CDN 的「官方源」,标 `official`;但 JDK 的四家发行版里,
/// Oracle(国内镜像)/ MS(VS CDN)/ Zulu(Azul CDN)的**直连 URL 本身就是某个厂商的 CDN**,
/// 标成 `official` 会让用户和健康度记忆都看错(「来源 official」其实来自华为云)。
/// 所以:认得的厂商/镜像 host 用它的稳定 id,其余一律 `official`。
#[must_use]
pub fn direct_source_id(url: &str) -> String {
let id = source_id_for(url);
if MIRROR_SOURCE_IDS.contains(&id.as_str()) {
id
} else {
SOURCE_OFFICIAL.to_string()
}
}
/// 会被 `direct_source_id` 认出来的厂商/镜像源 id(都是「非官方 CDN/镜像」那一类)。
pub const MIRROR_SOURCE_IDS: &[&str] = &[
"bmclapi",
"tuna",
"ustc",
"sjtu",
"bfsu",
"nju",
"huawei",
"azul",
"microsoft",
SOURCE_GH_PROXY,
];
/// 从 URL 里抠出 host(无 `url` 依赖)。
#[must_use]
pub fn host_of(url: &str) -> Option<&str> {
let rest = url.split_once("://").map_or(url, |(_, rest)| rest);
let authority = rest.split(['/', '?', '#']).next()?;
let host_port = authority.rsplit('@').next()?;
let host = host_port.split(':').next()?;
if host.is_empty() {
None
} else {
Some(host)
}
}
/// 是否是 GitHub 系 host(gh-proxy 只对这些套前缀)。
#[must_use]
pub fn is_github_host(host: &str) -> bool {
matches!(
host,
"github.com"
| "raw.githubusercontent.com"
| "objects.githubusercontent.com"
| "codeload.github.com"
| "gist.github.com"
| "gist.githubusercontent.com"
) || host.ends_with(".githubusercontent.com")
}
/// URL 的源标识(健康度按此归并;不知名 host 直接用 host)。
#[must_use]
pub fn source_id_for(url: &str) -> String {
let Some(host) = host_of(url) else {
return SOURCE_OFFICIAL.to_string();
};
if host.ends_with("bangbang93.com") {
return "bmclapi".to_string();
}
if host.contains("tuna.tsinghua.edu.cn") {
return "tuna".to_string();
}
if host.contains("ustc.edu.cn") {
return "ustc".to_string();
}
if host.contains("sjtu.edu.cn") {
return "sjtu".to_string();
}
if host.contains("bfsu.edu.cn") {
return "bfsu".to_string();
}
if host.contains("nju.edu.cn") {
return "nju".to_string();
}
if host.contains("huaweicloud.com") {
return "huawei".to_string();
}
if host.contains("azul.com") {
return "azul".to_string();
}
if host.contains("visualstudio.microsoft.com") || host == "aka.ms" {
return "microsoft".to_string();
}
if host.contains("gh-proxy") {
return SOURCE_GH_PROXY.to_string();
}
host.to_string()
}
fn debug_note(message: &str) {
if cfg!(debug_assertions) {
eprintln!("[ull::download] {message}");
} else {
let _ = message;
}
}