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

+136
View File
@@ -0,0 +1,136 @@
//! `core::game::java::location`:落点三模式(Portable / Standard / Custom)+ 孤儿目录清理。
//!
//! 三模式的口径来自 D23/D6:Portable 保持 v1 布局、Standard 走 OS 标准目录的
//! `<发行方>/<版本>` 分层、Custom 由 `java.installRoot` 覆盖;清理只碰我们自己的形状。
use std::path::PathBuf;
use unlucky_core::game::java::location::{
cleanup_orphans, sanitize_version, standard_java_root, JavaInstallRoot, LocationMode,
};
use unlucky_core::game::java::JdkVendor;
use unlucky_core::platform::layout::LauncherLayout;
use unlucky_core::platform::paths::{MapEnv, Platform};
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-loc-{tag}-{}-{nanos}-{seq}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("建临时目录");
dir
}
#[test]
fn standard_root_follows_each_platform_convention() {
let env = MapEnv::new()
.with("APPDATA", "C:/Users/u/AppData/Roaming")
.with("HOME", "/home/u")
.with("USERPROFILE", "C:/Users/u");
// 期望值一律用 `join` 拼(Windows 上的分隔符与 `Path` 比较细节交给标准库)。
let unix_home = PathBuf::from("/home/u");
assert_eq!(
standard_java_root(Platform::Windows, &env),
Some(PathBuf::from("C:/Users/u/AppData/Roaming").join("Java")),
"Windows 用户原话:%APPDATA%\\Java"
);
assert_eq!(
standard_java_root(Platform::MacOs, &env),
Some(unix_home.join("Library/Application Support").join("Java"))
);
assert_eq!(
standard_java_root(Platform::Linux, &env),
Some(unix_home.join(".local/share").join("java")),
"Linux 走 XDG 约定(无 XDG_DATA_HOME 时回落 ~/.local/share)"
);
// `Path::is_absolute()` 的口径是编译目标决定的:Windows 上 `C:/data/u` 才是绝对路径,
// 而 `/data/u` 只是「rooted」。夹具按目标平台给,才能同时验到「绝对 → 采用」。
let xdg_root = if cfg!(windows) {
"C:/data/u"
} else {
"/data/u"
};
let xdg = MapEnv::new()
.with("HOME", "/home/u")
.with("XDG_DATA_HOME", xdg_root);
assert_eq!(
standard_java_root(Platform::Linux, &xdg),
Some(PathBuf::from(xdg_root).join("java"))
);
// 相对路径的 XDG_DATA_HOME 按规范忽略 → 回落 HOME。
let relative = MapEnv::new()
.with("HOME", "/home/u")
.with("XDG_DATA_HOME", "data/u");
assert_eq!(
standard_java_root(Platform::Linux, &relative),
Some(unix_home.join(".local/share").join("java"))
);
assert_eq!(standard_java_root(Platform::Linux, &MapEnv::new()), None);
}
#[test]
fn portable_and_custom_modes_derive_the_documented_directories() {
let launcher = LauncherLayout::new("/data/.ULL");
let portable = JavaInstallRoot::portable(&launcher);
assert_eq!(portable.mode, LocationMode::Portable);
assert_eq!(
portable.jdk_dir(21, JdkVendor::Temurin, "21.0.13+9"),
PathBuf::from("/data/.ULL/data/java-managed/jdk-21-temurin"),
"便携模式必须保持 v1 布局(M33/M35 兼容)"
);
let custom = JavaInstallRoot::custom("/opt/java");
assert_eq!(custom.mode, LocationMode::Custom);
assert_eq!(
custom.jdk_dir(21, JdkVendor::Oracle, "21.0.2"),
PathBuf::from("/opt/java/oracle/21.0.2"),
"Custom 用 <vendor>/<version>(D23)"
);
assert_eq!(custom.cache_dir(), PathBuf::from("/opt/java/.cache"));
assert!(custom.describe().contains("custom"));
}
#[test]
fn version_strings_are_sanitized_for_directory_names() {
assert_eq!(sanitize_version("21.0.13+9"), "21.0.13+9");
assert_eq!(sanitize_version("21.0.13.0.2"), "21.0.13.0.2");
assert_eq!(sanitize_version("21.0.13+3"), "21.0.13+3");
assert_eq!(sanitize_version(" 21.0.2 "), "21.0.2");
assert_eq!(sanitize_version("../evil"), ".._evil");
assert_eq!(sanitize_version(""), "unknown");
}
#[test]
fn cleanup_removes_staging_and_empty_shells_but_never_foreign_dirs() {
let root = temp_dir("cleanup");
// 半成品:解压中断的暂存目录。
std::fs::create_dir_all(root.join(".staging-jdk-21-temurin")).expect("建暂存");
std::fs::write(root.join(".staging-jdk-21-temurin/bin"), b"x").expect("写暂存文件");
// 空壳 vendor 目录(Standard 布局的残留)。
std::fs::create_dir_all(root.join("oracle")).expect("建 vendor");
// 用户自己的目录(非空、无戳记):绝不能删。
std::fs::create_dir_all(root.join("my-own-jdk")).expect("建用户目录");
std::fs::write(root.join("my-own-jdk/notes.txt"), b"mine").expect("写用户文件");
// 归档缓存目录:保留。
std::fs::create_dir_all(root.join(".cache")).expect("建缓存");
let report = cleanup_orphans(&root);
assert!(
!root.join(".staging-jdk-21-temurin").exists(),
"暂存目录应删"
);
assert!(!root.join("oracle").exists(), "空 vendor 目录应删");
assert!(
root.join("my-own-jdk/notes.txt").is_file(),
"用户目录必须还在"
);
assert!(root.join(".cache").is_dir(), "缓存目录不能删");
assert_eq!(report.removed.len(), 2, "{:?}", report.removed);
let _ = std::fs::remove_dir_all(&root);
}