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 开发中。
137 lines
5.6 KiB
Rust
137 lines
5.6 KiB
Rust
//! `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);
|
||
}
|