Unlucky Launcher v0.0.1-alpha
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 开发中。
This commit is contained in:
AskaEth committed 2026-10-31 21:54:10 +08:00
commit bc41dfcf7d
517 files changed
+92844

No files matched your search

+291
View File
@@ -0,0 +1,291 @@
//! `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" }
);
}