Merge branch 'feat/javareg-registry-fallback-for-microsoft'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
GPU evidence / GPU 证据 — 真实产物判据 (push) Canceled after 0s
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Canceled after 0s
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Canceled after 0s
Dev Guards / GPU 证据门禁(同 main 口径;无 GPU 只跑自测) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s

This commit is contained in:
AskaEth committed 2026-11-12 20:20:31 +08:00
commit fc48094249
2 files changed
+405 -79

No files matched your search

+28 -79
View File
@@ -6,7 +6,7 @@
//! //!
//! | 平台 | 扫描位置 | //! | 平台 | 扫描位置 |
//! | --- | --- | //! | --- | --- |
//! | Windows | `%ProgramFiles%\{Java, Eclipse Adoptium, Microsoft, Zulu, Amazon Corretto, BellSoft, Semeru}\*`(含 `ProgramFiles(x86)` / `ProgramW6432` 变体,WOW64 才看得到 32 位安装)、`%APPDATA%\.minecraft\runtime\*`(**官方启动器装的运行时**,跨产品兼容)、`HKLM\SOFTWARE\JavaSoft\JDK`(老 Oracle 写的,新版发行版通常没有→容错跳过) | //! | Windows | `%ProgramFiles%\{Java, Eclipse Adoptium, Microsoft, Zulu, Amazon Corretto, BellSoft, Semeru}\*`(含 `ProgramFiles(x86)` / `ProgramW6432` 变体,WOW64 才看得到 32 位安装)、`%APPDATA%\.minecraft\runtime\*`(**官方启动器装的运行时**,跨产品兼容)、注册表两支(实现在 `discover/registry.rs`,入口 [`registry_homes`]):`HKLM\SOFTWARE\JavaSoft\JDK`(老 Oracle 写的)与 `HKLM\SOFTWARE\Microsoft\JDK\<ver>\<flavor>\MSI`(**Microsoft OpenJDK 的 MSI**,装到自定义目录时只有这里知道) |
//! | Linux | `/usr/lib/jvm/*`、`/usr/java/*` | //! | Linux | `/usr/lib/jvm/*`、`/usr/java/*` |
//! | Termux | `$PREFIX/lib/jvm/*`(`java-17-openjdk` 这类;`$PREFIX` 由 [`crate::platform::termux`] 识别) | //! | Termux | `$PREFIX/lib/jvm/*`(`java-17-openjdk` 这类;`$PREFIX` 由 [`crate::platform::termux`] 识别) |
//! | macOS | `/Library/Java/JavaVirtualMachines/*.jdk/Contents/Home` | //! | macOS | `/Library/Java/JavaVirtualMachines/*.jdk/Contents/Home` |
@@ -22,13 +22,18 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::platform::child::quiet;
use crate::platform::paths::{EnvSource, ProcessEnv}; use crate::platform::paths::{EnvSource, ProcessEnv};
use crate::platform::termux; use crate::platform::termux;
use super::extract::java_binary_name; use super::extract::java_binary_name;
use super::probe::{self, discover_managed_roots, system_candidates, JavaOrigin, JavaRuntime}; use super::probe::{self, discover_managed_roots, system_candidates, JavaOrigin, JavaRuntime};
mod registry;
pub use registry::{
parse_java_home, parse_msi_paths, registry_homes, registry_homes_from, RegSource,
};
/// 全部发现源(托管 → `JAVA_HOME` → `PATH` → 标准安装位置),已按路径去重。 /// 全部发现源(托管 → `JAVA_HOME` → `PATH` → 标准安装位置),已按路径去重。
#[must_use] #[must_use]
pub fn candidates(managed_root: &Path) -> Vec<JavaRuntime> { pub fn candidates(managed_root: &Path) -> Vec<JavaRuntime> {
@@ -53,24 +58,28 @@ pub fn standard_candidates() -> Vec<JavaRuntime> {
.collect() .collect()
} }
/// 扫描标准安装位置,返回 `bin/java[.exe]` 路径表(稳定排序,未探测)。 /// 扫描标准安装位置 + 注册表,返回 `bin/java[.exe]` 路径表(按[`path_key`]稳定排序、去重,未探测)。
#[must_use] #[must_use]
pub fn scan_java_binaries() -> Vec<PathBuf> { pub fn scan_java_binaries() -> Vec<PathBuf> {
let mut found: Vec<PathBuf> = Vec::new(); java_binaries_in(&standard_roots(), &registry_homes())
for root in standard_roots() { }
let binary = root.join("bin").join(java_binary_name());
if binary.is_file() { /// 同 [`scan_java_binaries`],但两个来源都由调用方给(测试注入用)。
found.push(binary); ///
} /// 去重口径与 [`dedupe`] 同一套([`path_key`]):同一个 JDK 被「扫目录」和「注册表」两支
} /// 同时报上来时只留一条——注册表里的 `Path` 常带末尾反斜杠,`PathBuf` 直接比也能收敛,
for home in registry_homes() { /// 但大小写/`\\?\` 前缀这些写法差异只有规范化键兜得住。
let binary = home.join("bin").join(java_binary_name()); #[must_use]
if binary.is_file() { pub fn java_binaries_in(roots: &[PathBuf], registry_homes: &[PathBuf]) -> Vec<PathBuf> {
found.push(binary); let name = java_binary_name();
} let mut found: Vec<PathBuf> = roots
} .iter()
found.sort(); .chain(registry_homes)
found.dedup(); .map(|root| root.join("bin").join(name))
.filter(|binary| binary.is_file())
.collect();
found.sort_by_key(|binary| path_key(binary));
found.dedup_by_key(|binary| path_key(binary));
found found
} }
@@ -142,48 +151,6 @@ fn child_dirs(dir: &Path) -> Vec<PathBuf> {
.collect() .collect()
} }
/// 注册表里登记的 `JavaHome`(只查老 Oracle 的 `JavaSoft\JDK`;查不到返回空)。
///
/// 不走额外依赖:`reg.exe` 在 Win10+ 恒定存在,输出格式也稳定
/// (` JavaHome REG_SZ C:\Program Files\Java\jdk-17`)。
#[must_use]
pub fn registry_homes() -> Vec<PathBuf> {
if !cfg!(windows) {
return Vec::new();
}
let mut homes = Vec::new();
for view in ["64", "32"] {
let Ok(output) = quiet("reg")
.args(["query", "HKLM\\SOFTWARE\\JavaSoft\\JDK", "/s", "/reg:"])
.arg(view)
.output()
else {
continue;
};
if !output.status.success() {
continue;
}
homes.extend(parse_java_home(&String::from_utf8_lossy(&output.stdout)));
}
homes.sort();
homes.dedup();
homes
}
/// 从 `reg query /s` 的输出里抠出 `JavaHome` 的值(纯函数,便于用真实输出做单测)。
#[must_use]
pub fn parse_java_home(output: &str) -> Vec<PathBuf> {
output
.lines()
.filter_map(|line| {
let rest = line.trim().strip_prefix("JavaHome")?;
let (_, value) = rest.split_once("REG_SZ")?;
let home = value.trim();
(!home.is_empty()).then(|| PathBuf::from(home))
})
.collect()
}
/// 按路径去重,保留**先出现**的那个(调用方按优先级排好序)。 /// 按路径去重,保留**先出现**的那个(调用方按优先级排好序)。
#[must_use] #[must_use]
pub fn dedupe(runtimes: Vec<JavaRuntime>) -> Vec<JavaRuntime> { pub fn dedupe(runtimes: Vec<JavaRuntime>) -> Vec<JavaRuntime> {
@@ -225,7 +192,7 @@ pub fn path_key(path: &Path) -> String {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{dedupe, parse_java_home, path_key, standard_roots}; use super::{dedupe, path_key, standard_roots};
use crate::game::java::probe::{JavaOrigin, JavaRuntime}; use crate::game::java::probe::{JavaOrigin, JavaRuntime};
// 这两个只有 Termux 扫描的两个测试用,而那两个测试是 `cfg(unix)`——导入必须同样 gate, // 这两个只有 Termux 扫描的两个测试用,而那两个测试是 `cfg(unix)`——导入必须同样 gate,
// 否则 Windows 上不编译测试、导入就成了未使用(`-D warnings` 直接判错)。 // 否则 Windows 上不编译测试、导入就成了未使用(`-D warnings` 直接判错)。
@@ -319,24 +286,6 @@ mod tests {
} }
} }
#[test]
fn parses_real_reg_query_output() {
// 实测形态(`reg query HKLM\SOFTWARE\JavaSoft\JDK /s`)。
let output = "\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\r\n CurrentVersion REG_SZ 1.8\r\n\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\1.8\r\n JavaHome REG_SZ C:\\Program Files\\Java\\jdk1.8.0_402\r\n";
let homes = parse_java_home(output);
assert_eq!(
homes,
vec![std::path::PathBuf::from(
r"C:\Program Files\Java\jdk1.8.0_402"
)]
);
}
#[test]
fn missing_registry_key_is_not_an_error() {
assert!(parse_java_home("错误: 系统找不到指定的注册表项或值。\r\n").is_empty());
}
#[test] #[test]
fn standard_roots_never_panic_on_missing_directories() { fn standard_roots_never_panic_on_missing_directories() {
// 本机大概率缺若干目录;这里只要求「能跑、不 panic」。 // 本机大概率缺若干目录;这里只要求「能跑、不 panic」。
@@ -0,0 +1,377 @@
//! `discover` 的「注册表」这一支:从 `HKLM` 里读出 JDK 的安装根。
//!
//! 两条来源,都按 64 位 + 32 位视图各查一遍(`reg query <键> /s /reg:64|32`):
//!
//! | 键 | 值 | 谁写的 |
//! | --- | --- | --- |
//! | `HKLM\SOFTWARE\JavaSoft\JDK\*` | `JavaHome`(`REG_SZ`) | 老 Oracle 安装器;新发行版通常不写 |
//! | `HKLM\SOFTWARE\Microsoft\JDK\<ver>\<flavor>\MSI` | `Path`(`REG_SZ`) | Microsoft OpenJDK 的 MSI |
//!
//! 第二条是本模块存在的理由:MS JDK 装到**自定义目录**(不在 Program Files 下)时,
//! 扫目录那支看不见它,只有注册表知道它在哪。
//!
//! 键不存在是常态(本机实测 `JavaSoft\JDK` 就没有)→ 静默跳过,绝不报错。
//! 读注册表走真 `reg.exe`(Win10+ 恒定存在),但**解析与合并都是纯函数 + [`RegSource`]
//! 注入**:单测喂真实 `reg query` 的输出样本,不依赖本机到底装了什么。
use std::path::PathBuf;
use crate::platform::child::quiet;
/// 老 Oracle 安装器登记 `JavaHome` 的键。
const JAVASOFT_KEY: &str = r"HKLM\SOFTWARE\JavaSoft\JDK";
/// Microsoft OpenJDK 的 MSI 登记安装路径的键(`<ver>\<flavor>\MSI` 下面才是 `Path`)。
const MICROSOFT_KEY: &str = r"HKLM\SOFTWARE\Microsoft\JDK";
/// 注册表视图,两个都要试:32 位 JDK 的 MSI 只写 32 位视图。
const VIEWS: [&str; 2] = ["64", "32"];
/// 一次 `reg query /s` 的原始输出来源。
///
/// `None` 与「键不存在」同义——调用方一律当「这一支没有东西」,不区分「查不到」与「查不了」。
pub trait RegSource {
/// 查 `key` 在 `view`(`"64"` / `"32"`)视图下的递归输出。
fn key_output(&self, key: &str, view: &str) -> Option<String>;
}
/// 真 `reg.exe`(生产路径)。
pub struct ProcessReg;
impl RegSource for ProcessReg {
fn key_output(&self, key: &str, view: &str) -> Option<String> {
let output = quiet("reg")
.args(["query", key, "/s", "/reg:"])
.arg(view)
.output()
.ok()?;
if !output.status.success() {
return None;
}
Some(String::from_utf8_lossy(&output.stdout).into_owned())
}
}
/// 注册表里登记的 JDK 根目录(两条来源合并,稳定排序去重;查不到返回空)。
#[must_use]
pub fn registry_homes() -> Vec<PathBuf> {
if !cfg!(windows) {
return Vec::new();
}
registry_homes_from(&ProcessReg)
}
/// 同 [`registry_homes`],但注册表可注入(测试喂假输出,不碰本机)。
#[must_use]
pub fn registry_homes_from(reg: &dyn RegSource) -> Vec<PathBuf> {
let mut homes = query_views(reg, JAVASOFT_KEY, parse_java_home);
homes.extend(query_views(reg, MICROSOFT_KEY, parse_msi_paths));
homes.sort();
homes.dedup();
homes
}
/// 一个键在两个视图下各查一次,解析结果拼接(单个视图失败不影响另一个)。
fn query_views(reg: &dyn RegSource, key: &str, parse: fn(&str) -> Vec<PathBuf>) -> Vec<PathBuf> {
let mut homes = Vec::new();
for view in VIEWS {
if let Some(output) = reg.key_output(key, view) {
homes.extend(parse(&output));
}
}
homes
}
/// 从 `reg query /s` 的输出里抠出 `JavaHome` 的值(纯函数,便于用真实输出做单测)。
#[must_use]
pub fn parse_java_home(output: &str) -> Vec<PathBuf> {
output
.lines()
.filter_map(|line| {
let rest = line.trim().strip_prefix("JavaHome")?;
let (_, value) = rest.split_once("REG_SZ")?;
let home = value.trim();
(!home.is_empty()).then(|| PathBuf::from(home))
})
.collect()
}
/// 从 `reg query /s` 的输出里抠出 MS JDK 的 `Path` 值。
///
/// 判据是**当前键的最后一段叫 `MSI`**(大小写不限),不是数层数——`<ver>\<flavor>\MSI`
/// 是标准层深,而安装器版本不同时会多一层(`<flavor>\x64\MSI`)或某一层为空,
/// 只要键尾还是 `MSI` 就照收。同层里别的值(`Main`/`EnvironmentPath` 这些 `REG_DWORD`)
/// 以及非 `MSI` 键下的 `Path` 都不认。
#[must_use]
pub fn parse_msi_paths(output: &str) -> Vec<PathBuf> {
let mut in_msi = false;
let mut paths = Vec::new();
for line in output.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("HKEY_") {
in_msi = trimmed
.rsplit('\\')
.next()
.is_some_and(|last| last.eq_ignore_ascii_case("MSI"));
continue;
}
if !in_msi {
continue;
}
let Some(rest) = trimmed.strip_prefix("Path") else {
continue;
};
// 值名得整段叫 `Path`(`PathFoo REG_SZ ...` 不算):名、类型、数据之间是空白。
if !rest.starts_with(char::is_whitespace) {
continue;
}
let Some((_, value)) = rest.split_once("REG_SZ") else {
continue;
};
let path = value.trim();
if !path.is_empty() {
paths.push(PathBuf::from(path));
}
}
paths
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use super::{parse_java_home, parse_msi_paths, registry_homes_from, RegSource};
use crate::game::java::discover::java_binaries_in;
/// 真实 `reg query "HKLM\SOFTWARE\JavaSoft\JDK" /s` 的输出样本(本机没有这个键,
/// 所以这段是旧机上的实测形态,M49 起就钉在测试里)。
const JAVASOFT_SAMPLE: &str = "\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\r\n CurrentVersion REG_SZ 1.8\r\n\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\JDK\\1.8\r\n JavaHome REG_SZ C:\\Program Files\\Java\\jdk1.8.0_402\r\n";
/// 真实 `reg query "HKLM\SOFTWARE\Microsoft\JDK" /s /reg:64` 的输出样本
/// (本机实测原文,21 与 25 两条,`Path` 末尾带反斜杠;同键里 `JavaHome` 是 `REG_DWORD`)。
const MICROSOFT_SAMPLE: &str = concat!(
"\r\nHKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\21.0.11.10\r\n\r\n",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\21.0.11.10\\hotspot\r\n\r\n",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\21.0.11.10\\hotspot\\MSI\r\n",
" Path REG_SZ C:\\Program Files\\Microsoft\\jdk-21.0.11.10-hotspot\\\r\n",
" Main REG_DWORD 0x1\r\n",
" EnvironmentPath REG_DWORD 0x1\r\n",
" JavaHome REG_DWORD 0x1\r\n\r\n",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\25.0.3.9\r\n\r\n",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\25.0.3.9\\hotspot\r\n\r\n",
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\25.0.3.9\\hotspot\\MSI\r\n",
" Path REG_SZ C:\\Program Files\\Microsoft\\jdk-25.0.3.9-hotspot\\\r\n",
" Main REG_DWORD 0x1\r\n",
);
/// `reg query` 的「键不存在」原文(GBK 解出来仍是这句)。
const MISSING_KEY: &str = "错误: 系统找不到指定的注册表项或值。\r\n";
/// 假注册表:给 (键, 视图) 配输出,没配的当「键不存在」。
struct FakeReg {
replies: Vec<(&'static str, &'static str, String)>,
}
impl FakeReg {
fn new(replies: Vec<(&'static str, &'static str, String)>) -> Self {
Self { replies }
}
}
impl RegSource for FakeReg {
fn key_output(&self, key: &str, view: &str) -> Option<String> {
self.replies
.iter()
.find(|(k, v, _)| *k == key && *v == view)
.map(|(_, _, output)| output.clone())
}
}
/// 记录被查过的 (键, 视图),一律回「没有」。
#[derive(Default)]
struct Recorder {
seen: RefCell<Vec<(String, String)>>,
}
impl RegSource for Recorder {
fn key_output(&self, key: &str, view: &str) -> Option<String> {
self.seen
.borrow_mut()
.push((key.to_owned(), view.to_owned()));
None
}
}
fn java_file() -> &'static str {
crate::game::java::extract::java_binary_name()
}
#[test]
fn parses_real_java_soft_output() {
assert_eq!(
parse_java_home(JAVASOFT_SAMPLE),
vec![PathBuf::from(r"C:\Program Files\Java\jdk1.8.0_402")]
);
}
#[test]
fn parses_real_microsoft_msi_output() {
// 同键里的 `JavaHome REG_DWORD` 不许被当成 JavaSoft 那支的 `JavaHome`。
assert!(
parse_java_home(MICROSOFT_SAMPLE).is_empty(),
"MSI 键里的 JavaHome 是 REG_DWORD,JavaSoft 的解析器不该收"
);
assert_eq!(
parse_msi_paths(MICROSOFT_SAMPLE),
vec![
PathBuf::from(r"C:\Program Files\Microsoft\jdk-21.0.11.10-hotspot\"),
PathBuf::from(r"C:\Program Files\Microsoft\jdk-25.0.3.9-hotspot\"),
]
);
}
#[test]
fn missing_registry_key_is_not_an_error() {
assert!(parse_java_home(MISSING_KEY).is_empty());
assert!(parse_msi_paths(MISSING_KEY).is_empty());
// 两个键都查不到 → 空表,不是错误。
let reg = FakeReg::new(Vec::new());
assert!(registry_homes_from(&reg).is_empty());
}
#[test]
fn msi_path_with_and_without_trailing_backslash_is_the_same_home() {
// 安装器写不写末尾反斜杠都有(本机 21/25 两条带),拼 `bin\java.exe` 的结果必须一致。
let with = parse_msi_paths(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\21\\hotspot\\MSI\r\n Path REG_SZ C:\\Program Files\\Microsoft\\jdk-21\\\r\n",
);
let without = parse_msi_paths(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\21\\hotspot\\MSI\r\n Path REG_SZ C:\\Program Files\\Microsoft\\jdk-21\r\n",
);
assert_eq!(with, without, "末尾反斜杠只是写法差异");
assert_eq!(with[0].join("bin"), without[0].join("bin"));
// 两条同时出现(两条支/两个视图各报一次)也要收敛成一条。
let mut both = with.clone();
both.extend(without);
both.sort();
both.dedup();
assert_eq!(both.len(), 1);
}
#[test]
fn msi_paths_survive_irregular_version_layers() {
let output = concat!(
// 多一层:`<ver>\<flavor>\x64\MSI`
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\21.0.11.10\\hotspot\\x64\\MSI\r\n",
" Path REG_SZ D:\\jdk\\ms-21\\\r\n\r\n",
// 空一层:版本目录名空着,直接 `<flavor>\MSI`
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\\\hotspot\\MSI\r\n",
" Path REG_SZ D:\\jdk\\ms-empty-ver\r\n\r\n",
// 键尾不是 MSI → 那个 `Path` 不认
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\25.0.3.9\\hotspot\r\n",
" Path REG_SZ D:\\jdk\\not-an-msi-layer\r\n\r\n",
// 值名不是整段 `Path` → 不认
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\26\\hotspot\\MSI\r\n",
" PathExtra REG_SZ D:\\jdk\\path-extra\r\n",
);
assert_eq!(
parse_msi_paths(output),
vec![
PathBuf::from(r"D:\jdk\ms-21\"),
PathBuf::from(r"D:\jdk\ms-empty-ver"),
]
);
}
#[test]
fn both_registry_views_are_queried_for_both_keys() {
// 与 JavaSoft 那支同口径:两个键 × 两个视图,一个都不能省。
// 键名在这里写**字面量**(不用常量):常量写错、键名被改,这两条就红。
let reg = Recorder::default();
let _ = registry_homes_from(&reg);
let seen = reg.seen.borrow();
for key in [
r"HKLM\SOFTWARE\JavaSoft\JDK",
r"HKLM\SOFTWARE\Microsoft\JDK",
] {
for view in ["64", "32"] {
assert!(
seen.iter().any(|(k, v)| k == key && v == view),
"少查了 {key} /reg:{view}:{seen:?}"
);
}
}
}
#[test]
fn injection_registry_only_lists_the_ms_jdks() {
// 注入式验收:Program Files / PATH / JAVA_HOME 三个来源全屏蔽(根表给空),
// 只让注册表这一支出声——本机装没装 MS JDK 不参与判定。
let base = std::env::temp_dir().join(format!("ull-ms-jdk-{}", std::process::id()));
let mut output = String::from("\r\n");
let mut expected: Vec<PathBuf> = Vec::new();
for version in ["21.0.11.10", "25.0.3.9"] {
let home = base.join(format!("jdk-{version}-hotspot"));
std::fs::create_dir_all(home.join("bin")).unwrap();
std::fs::write(home.join("bin").join(java_file()), b"").unwrap();
// 照实测形态写:`Path` 末尾带反斜杠,注册表里是 Windows 分隔符。
output.push_str(&format!(
"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\JDK\\{version}\\hotspot\\MSI\r\n Path REG_SZ {}{}\r\n\r\n",
home.display(),
std::path::MAIN_SEPARATOR
));
expected.push(home.join("bin").join(java_file()));
}
// `JavaSoft` 与 32 位视图都「键不存在」:只有 MS 键的 64 位视图有东西。
// 键名用字面量:常量被改坏(比如写成 `Microsoft\JRE`)时这一条必须红。
let reg = FakeReg::new(vec![(r"HKLM\SOFTWARE\Microsoft\JDK", "64", output)]);
let homes = registry_homes_from(&reg);
assert_eq!(homes.len(), 2, "MSI 的两条登记都要抠出来:{homes:?}");
let binaries = java_binaries_in(&[], &homes);
assert_eq!(binaries, expected, "只走注册表也要列出这两条 MS JDK");
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn same_jdk_from_two_branches_is_listed_once() {
// 扫目录那支与注册表那支同时命中同一个 JDK:名字里多了末尾反斜杠也只是写法差异。
let base = std::env::temp_dir().join(format!("ull-ms-dup-{}", std::process::id()));
let home = base.join("jdk-21.0.11.10-hotspot");
std::fs::create_dir_all(home.join("bin")).unwrap();
std::fs::write(home.join("bin").join(java_file()), b"").unwrap();
let from_registry = format!("{}{}", home.display(), std::path::MAIN_SEPARATOR);
let roots = [home.clone()];
let homes = [PathBuf::from(&from_registry), home.clone()];
let binaries = java_binaries_in(&roots, &homes);
assert_eq!(binaries.len(), 1, "同一个 JDK 不许列两次:{binaries:?}");
assert_eq!(binaries[0], home.join("bin").join(java_file()));
std::fs::remove_dir_all(&base).ok();
}
#[test]
fn registry_homes_are_absolute_on_this_machine() {
// 真跑一遍 `reg.exe`(本机有没有 JDK 都算通过):路径必须绝对、且已去重。
let homes = super::registry_homes();
for home in &homes {
assert!(Path::new(home).is_absolute(), "注册表路径应绝对:{home:?}");
}
let mut keys: Vec<String> = homes
.iter()
.map(|home| crate::game::java::discover::path_key(home))
.collect();
let total = keys.len();
keys.sort();
keys.dedup();
assert_eq!(keys.len(), total, "注册表结果不该重复:{homes:?}");
}
}