Files
Unlucky-Launcher/crates/core/src/game/java/microsoft.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

262 lines
9.1 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.
//! Microsoft OpenJDK:`aka.ms` 模板下载 + 成对 `.sha256sum.txt` 校验。
//!
//! 分发形态与 Temurin / Oracle 都不同(research/06 §3.2):
//!
//! ```text
//! 下载 https://aka.ms/download-jdk/microsoft-jdk-{version}-{os}-{arch}.zip
//! → 301 → https://download.visualstudio.microsoft.com/download/pr/{guid}/{hash}/{同名}
//! 校验 https://aka.ms/download-jdk/<同一文件名>.sha256sum.txt # "<hash> <filename>"
//! ```
//!
//! 三条硬事实(都影响实现):
//!
//! 1. **版本是四段/五段**(`21.0.13.0.2` = JDK 21.0.13+2;11 线是 `11.0.32.1`),
//! 不能拿 Temurin 的三段号直接拼——所以这里有自己的 [`parse_version`] 与内置快照表;
//! 2. **没有 8**(微软已停发,官方页面让去下 Temurin 8),`supports()` 也这么判;
//! 3. **GitHub 侧没有任何资产**(`microsoft/openjdk` 的 release `assets` 全空),
//! 所以 gh-proxy 对这条链毫无用处——`domestic_mirrors` 实测为空数组。
use super::error::{JavaError, JavaErrorKind};
use super::net;
use super::release::{parse_sha256_text, JdkArchive, JdkRelease};
use super::target::AdoptiumTarget;
use super::vendor::JdkVendor;
use crate::download::MsOpenJdkTable;
/// 内置版本快照(`learn.microsoft.com/java/openjdk/download` 2026-10-30 的表,research/06 §1.3)。
///
/// 官方只有人读 HTML 表、没有 JSON API,逐版本探测又要 `aka.ms` 301(0.3–0.8 s/次),
/// 所以按 research/06 §3.3 的建议 A 内置快照;`size` 与实测字节数一致,用于长度对拍。
pub const SNAPSHOT: &[(u32, &str, u64)] = &[
(25, "25.0.5.0.2", 220_869_659),
(21, "21.0.13.0.2", 201_450_315),
(17, "17.0.21.0.2", 187_236_595),
(11, "11.0.32.1", 191_250_397),
];
/// 四段/五段版本号(`21.0.13.0.2` / `11.0.32.1`)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MsVersion {
/// 大版本(`21`)。
pub major: u32,
/// 各数字段(长度 3..=5)。
pub segments: Vec<u32>,
}
impl MsVersion {
/// 原样的版本串。
#[must_use]
pub fn text(&self) -> String {
self.segments
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(".")
}
}
/// 解析 MS 的版本串;段数必须 3..=5(MS 的语义:`major.update.patch[.build[.rev]]`)。
#[must_use]
pub fn parse_version(text: &str) -> Option<MsVersion> {
let mut segments = Vec::new();
for segment in text.trim().split('.') {
segments.push(segment.parse::<u32>().ok()?);
}
if segments.len() < 3 || segments.len() > 5 {
return None;
}
Some(MsVersion {
major: segments[0],
segments,
})
}
/// MS 的 os 段(`windows` / `macos` / `linux`)。
#[must_use]
pub fn ms_os(target: AdoptiumTarget) -> &'static str {
match target.os {
"mac" => "macos",
other => other,
}
}
/// MS 的 arch 段(`x64` / `aarch64`)。
#[must_use]
pub fn ms_arch(target: AdoptiumTarget) -> &'static str {
target.arch
}
/// 归档文件名(模板:`microsoft-jdk-{version}-{os}-{arch}.zip`,全平台都是 zip)。
#[must_use]
pub fn archive_name(version: &str, target: AdoptiumTarget) -> String {
format!(
"microsoft-jdk-{version}-{}-{}.zip",
ms_os(target),
ms_arch(target)
)
}
/// 把模板里的 `{version}` / `{os}` / `{arch}` 换掉。
#[must_use]
pub fn expand_template(template: &str, version: &str, target: AdoptiumTarget) -> String {
template
.replace("{version}", version)
.replace("{os}", ms_os(target))
.replace("{arch}", ms_arch(target))
}
/// 快照里该大版本的版本串(没有 → `None`)。
#[must_use]
pub fn snapshot_version(major: u32) -> Option<&'static str> {
SNAPSHOT
.iter()
.find(|(known, _, _)| *known == major)
.map(|(_, version, _)| *version)
}
/// 快照里该大版本的字节数。
#[must_use]
pub fn snapshot_size(major: u32) -> Option<u64> {
SNAPSHOT
.iter()
.find(|(known, _, _)| *known == major)
.map(|(_, _, size)| *size)
}
/// 查一次「某大版本的最新 MS OpenJDK」。
///
/// 两跳:内置快照挑版本 → 取 `.sha256sum.txt` 拿哈希。校验文件取不到就整家失败
/// (宁可降级到 Zulu,也不下一个没法验证的 200 MB 归档)。
///
/// # Errors
///
/// 表里没配模板、该大版本微软不发(含 8)、校验文件取不到或格式不对时返回错误。
pub async fn fetch_latest(
client: &reqwest::Client,
table: &MsOpenJdkTable,
major: u32,
target: AdoptiumTarget,
) -> Result<JdkRelease, JavaError> {
let version = snapshot_version(major).ok_or_else(|| {
JavaError::new(
JavaErrorKind::Metadata,
format!(
"Microsoft OpenJDK 没有 {major} 的 GA(只发 {})",
majors_text()
),
)
.with_hint("8 用 temurin;其它大版本用 oracle / zulu")
})?;
let filename = archive_name(version, target);
let sha_url = expand_template(&table.sha256_template, version, target);
let sha_text = net::get_text(client, &sha_url, "Microsoft OpenJDK 校验文件").await?;
let sha256 = parse_sha256_text(&sha_text).ok_or_else(|| {
JavaError::new(
JavaErrorKind::Integrity,
format!("{filename} 的 .sha256sum.txt 里没有 64 位哈希"),
)
})?;
Ok(JdkRelease {
vendor: JdkVendor::Microsoft,
major,
version: version.to_string(),
semver: None,
release_name: version.to_string(),
archive: JdkArchive {
name: filename.clone(),
url: expand_template(&table.template, version, target),
sha256,
size: snapshot_size(major).unwrap_or(0),
},
})
}
fn majors_text() -> String {
SNAPSHOT
.iter()
.map(|(major, _, _)| major.to_string())
.collect::<Vec<_>>()
.join(" / ")
}
#[cfg(test)]
mod tests {
use super::{
archive_name, expand_template, ms_os, parse_version, snapshot_size, snapshot_version,
SNAPSHOT,
};
use crate::game::java::target::AdoptiumTarget;
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
#[test]
fn snapshot_covers_the_four_majors_microsoft_ships() {
assert_eq!(snapshot_version(21), Some("21.0.13.0.2"));
assert_eq!(snapshot_version(11), Some("11.0.32.1"));
assert_eq!(snapshot_version(8), None, "微软已停发 8");
assert_eq!(snapshot_size(17), Some(187_236_595));
assert_eq!(
SNAPSHOT
.iter()
.map(|(major, _, _)| *major)
.collect::<Vec<_>>(),
vec![25, 21, 17, 11]
);
}
#[test]
fn four_and_five_segment_versions_have_their_own_semantics() {
// 21.0.13.0.2 = JDK 21.0.13+2(五段);11 线只有四段。
let ms = parse_version("21.0.13.0.2").expect("五段应解析");
assert_eq!(ms.major, 21);
assert_eq!(ms.segments.len(), 5);
assert_eq!(ms.text(), "21.0.13.0.2");
let eleven = parse_version("11.0.32.1").expect("四段应解析");
assert_eq!(eleven.major, 11);
assert_eq!(eleven.segments.len(), 4);
// Temurin 的三段号形态在这里同样成立,但语义由快照表定义,不共用 Temurin 的解析。
assert_eq!(
parse_version("17.0.21").map(|version| version.major),
Some(17)
);
assert_eq!(parse_version("21"), None);
assert_eq!(parse_version("21.0"), None);
assert_eq!(parse_version("21.0.13.0.2.1"), None);
assert_eq!(parse_version("21.0.x.2"), None);
}
#[test]
fn archive_names_and_templates_expand_per_platform() {
assert_eq!(
archive_name("21.0.13.0.2", WINDOWS),
"microsoft-jdk-21.0.13.0.2-windows-x64.zip"
);
let template = "https://aka.ms/download-jdk/microsoft-jdk-{version}-{os}-{arch}.zip";
assert_eq!(
expand_template(template, "17.0.21.0.2", WINDOWS),
"https://aka.ms/download-jdk/microsoft-jdk-17.0.21.0.2-windows-x64.zip"
);
assert_eq!(
expand_template(
"https://aka.ms/download-jdk/microsoft-jdk-{version}-{os}-{arch}.zip.sha256sum.txt",
"21.0.13.0.2",
AdoptiumTarget::new("linux", "aarch64")
),
"https://aka.ms/download-jdk/microsoft-jdk-21.0.13.0.2-linux-aarch64.zip.sha256sum.txt"
);
assert_eq!(ms_os(AdoptiumTarget::new("mac", "x64")), "macos");
}
#[test]
fn builtin_table_uses_the_official_templates() {
let table = crate::download::MirrorTable::builtin();
let microsoft = table.microsoft_openjdk().expect("内置表应带 MS 分发");
assert!(microsoft
.template
.starts_with("https://aka.ms/download-jdk/"));
assert!(microsoft.sha256_template.ends_with(".sha256sum.txt"));
assert!(microsoft.domestic_mirrors.is_empty(), "MS 无国内镜像");
assert_eq!(microsoft.majors, vec![25, 21, 17, 11]);
}
}