Files
Unlucky-Launcher/crates/core/tests/game/java_install.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

292 lines
10 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.
//! `core::game::java` 的安装侧集成测试:归档解压(幂等 / 顶层剥离 / 越界拒绝)、
//! 托管目录命名、下载请求装配、平台归档形态。
//!
//! 镜像选源见 `java_mirrors.rs`,MC↔Java 映射见 `java_mapping.rs`。
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use flate2::write::GzEncoder;
use flate2::Compression;
use unlucky_core::download::ResourceKind;
use unlucky_core::game::java::{
self, extract, install, probe, AdoptiumTarget, JdkArchive, JdkRelease,
};
/// 64 位十六进制(解压戳记只当字符串比对,不校验内容)。
const SHA_A: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
/// 另一个「新版本」的摘要。
const SHA_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
fn temp_dir(tag: &str) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_nanos());
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"ull-java-{tag}-{}-{nanos}-{seq}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("建临时目录");
dir
}
/// 造一个 Temurin 形状的 zip(顶层 `jdk-21.0.13+9/`)。
fn write_zip(path: &Path, entries: &[(&str, &[u8])]) {
let file = File::create(path).expect("建 zip");
let mut writer = zip::ZipWriter::new(file);
let options =
zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
for (name, body) in entries {
writer.start_file(*name, options).expect("写 zip 条目");
writer.write_all(body).expect("写 zip 内容");
}
writer.finish().expect("收尾 zip");
}
/// 造一个 Temurin 形状的 tar.gz。
fn write_tar_gz(path: &Path, entries: &[(&str, &[u8])]) {
let file = File::create(path).expect("建 tar.gz");
let encoder = GzEncoder::new(file, Compression::fast());
let mut builder = tar::Builder::new(encoder);
for (name, body) in entries {
let mut header = tar::Header::new_gnu();
header.set_size(body.len() as u64);
header.set_mode(0o755);
header.set_entry_type(tar::EntryType::Regular);
builder
.append_data(&mut header, name, *body)
.expect("写 tar 条目");
}
let encoder = builder.into_inner().expect("收尾 tar");
encoder.finish().expect("收尾 gzip");
}
fn java_bin(root: &Path) -> PathBuf {
root.join("bin").join(java::java_binary_name())
}
#[test]
fn zip_archive_loses_the_temurin_top_level_directory() {
let dir = temp_dir("zip");
let archive = dir.join("jdk.zip");
let top = "jdk-21.0.13+9";
write_zip(
&archive,
&[
(
&format!("{top}/bin/{}", java::java_binary_name()),
b"#!/bin/true\n",
),
(&format!("{top}/release"), b"JAVA_VERSION=\"21.0.13\"\n"),
],
);
let dest = dir.join("jdk-21-temurin");
let report = extract::extract_archive(&archive, SHA_A, 21, &dest).expect("解压应成功");
assert!(!report.skipped);
assert_eq!(report.files, 2);
assert!(java_bin(&dest).is_file(), "bin/java 应在顶层");
assert!(!dest.join(top).exists(), "顶层目录必须被剥掉");
let stamp = extract::read_stamp(&dest).expect("应写戳记");
assert_eq!(stamp.major, 21);
assert_eq!(stamp.sha256, SHA_A);
assert_eq!(stamp.files, 2);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn second_extract_with_same_sha_touches_nothing() {
let dir = temp_dir("idem");
let archive = dir.join("jdk.zip");
write_zip(
&archive,
&[(
&format!("jdk-21.0.13+9/bin/{}", java::java_binary_name()),
b"bin\n",
)],
);
let dest = dir.join("jdk-21-temurin");
extract::extract_archive(&archive, SHA_A, 21, &dest).expect("首解");
// 用户手工动过的痕迹:幂等命中时不能被抹掉。
let marker = dest.join("安装后手工放的.txt");
std::fs::write(&marker, b"keep").expect("写标记");
let again = extract::extract_archive(&archive, SHA_A, 21, &dest).expect("重解");
assert!(again.skipped, "同一 sha256 应整体跳过");
assert_eq!(again.files, 0);
assert!(marker.is_file(), "幂等命中时不应重写目录");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn new_sha_replaces_the_managed_directory() {
let dir = temp_dir("upgrade");
let archive = dir.join("jdk.zip");
write_zip(
&archive,
&[(
&format!("jdk-21.0.13+9/bin/{}", java::java_binary_name()),
b"v1\n",
)],
);
let dest = dir.join("jdk-21-temurin");
extract::extract_archive(&archive, SHA_A, 21, &dest).expect("首解");
let marker = dest.join("旧版残留");
std::fs::write(&marker, b"old").expect("写标记");
let report = extract::extract_archive(&archive, SHA_B, 21, &dest).expect("新摘要重解");
assert!(!report.skipped);
assert!(!marker.exists(), "官方发新版后旧目录应被替换");
assert_eq!(
extract::read_stamp(&dest).map(|stamp| stamp.sha256),
Some(SHA_B.to_string())
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn tar_gz_archive_is_supported_for_linux_and_mac() {
let dir = temp_dir("targz");
let archive = dir.join("OpenJDK21U-jdk_x64_linux_hotspot_21.0.13_9.tar.gz");
let top = "jdk-21.0.13+9";
write_tar_gz(
&archive,
&[
(&format!("{top}/bin/{}", java::java_binary_name()), b"bin\n"),
(&format!("{top}/lib/modules"), b"modules\n"),
],
);
let dest = dir.join("jdk-21-temurin");
let report = extract::extract_archive(&archive, SHA_A, 21, &dest).expect("解压 tar.gz");
assert_eq!(report.files, 2);
assert!(dest.join("bin").join(java::java_binary_name()).is_file());
assert!(dest.join("lib").join("modules").is_file());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn archive_without_a_single_top_directory_is_promoted_as_is() {
// 有的魔改发行版直接把 bin/ 放在根上:不能硬剥一层。
let dir = temp_dir("flat");
let archive = dir.join("flat.zip");
write_zip(
&archive,
&[
(
&format!("bin/{}", java::java_binary_name()),
b"bin\n".as_slice(),
),
("release", b"x\n".as_slice()),
],
);
let dest = dir.join("jdk-17-temurin");
extract::extract_archive(&archive, SHA_A, 17, &dest).expect("解压");
assert!(dest.join("bin").join(java::java_binary_name()).is_file());
assert!(dest.join("release").is_file());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn foreign_directory_is_never_clobbered() {
let dir = temp_dir("foreign");
let archive = dir.join("jdk.zip");
write_zip(
&archive,
&[(
&format!("jdk-21.0.13+9/bin/{}", java::java_binary_name()),
b"bin\n",
)],
);
let dest = dir.join("jdk-21-temurin");
std::fs::create_dir_all(&dest).expect("建目标目录");
std::fs::write(dest.join("用户的文件.txt"), b"mine").expect("写用户文件");
let error = extract::extract_archive(&archive, SHA_A, 21, &dest).expect_err("应拒绝覆盖");
assert_eq!(error.kind(), java::JavaErrorKind::Io);
assert!(dest.join("用户的文件.txt").is_file(), "用户文件必须还在");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn zip_slip_entries_do_not_escape_the_target_directory() {
let dir = temp_dir("slip");
let archive = dir.join("evil.zip");
write_zip(
&archive,
&[
(
&format!("jdk-21.0.13+9/bin/{}", java::java_binary_name()),
b"bin\n",
),
("../../escaped.txt", b"nope"),
],
);
let dest = dir.join("jdk-21-temurin");
let report = extract::extract_archive(&archive, SHA_A, 21, &dest).expect("越界条目只跳过");
assert_eq!(report.files, 1, "越界条目不应被写盘");
assert!(!dir.join("escaped.txt").exists());
assert!(!dir.parent().unwrap_or(&dir).join("escaped.txt").exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn managed_layout_names_carry_major_and_vendor() {
let root = Path::new("/tmp/java-managed");
let dest = install::managed_root_dir(root, 21, java::JDK_VENDOR);
assert_eq!(dest, root.join("jdk-21-temurin"));
assert_eq!(
probe::parse_managed_dir("jdk-21-temurin"),
Some((21, "temurin"))
);
assert_eq!(probe::managed_dir_name(8, "temurin"), "jdk-8-temurin");
}
#[test]
fn archive_request_carries_sha256_and_the_java_runtime_kind() {
let release = JdkRelease {
vendor: java::JdkVendor::Temurin,
major: 21,
version: "21.0.13+9".to_string(),
semver: Some("21.0.13+9.0.LTS".to_string()),
release_name: "jdk-21.0.13+9".to_string(),
archive: JdkArchive {
name: "OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip".to_string(),
url: "https://github.com/adoptium/temurin21-binaries/releases/download/x.zip"
.to_string(),
sha256: SHA_A.to_string(),
size: 1024,
},
};
let dest = Path::new("/tmp/x.zip");
let request = install::archive_request(&release, dest.to_path_buf());
assert_eq!(request.kind, ResourceKind::JavaRuntime);
assert_eq!(request.expected_size, Some(1024));
assert_eq!(
request
.integrity
.as_ref()
.map(|integrity| integrity.hex.as_str()),
Some(SHA_A)
);
assert!(request.validate().is_ok());
assert_eq!(
install::archive_path(Path::new("/tmp/java-managed"), &release),
Path::new("/tmp/java-managed/.cache/OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip")
);
}
#[test]
fn host_target_picks_the_platform_archive_shape() {
let target = AdoptiumTarget::host().expect("宿主应受支持");
assert_eq!(target.is_zip(), cfg!(windows));
assert_eq!(
target.archive_ext(),
if cfg!(windows) { "zip" } else { "tar.gz" }
);
}