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
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
292 lines
10 KiB
Rust
292 lines
10 KiB
Rust
//! `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" }
|
||
);
|
||
}
|