Files
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

137 lines
5.6 KiB
Rust
Raw Permalink 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::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);
}