feat(core): JDK 发现补 Microsoft 注册表支(HKLM\SOFTWARE\Microsoft\JDK\*\MSI\Path)(M281)
`registry_homes()` 原来只读 `HKLM\SOFTWARE\JavaSoft\JDK`,而 MS JDK 的 MSI 把安装路径 登记在 `HKLM\SOFTWARE\Microsoft\JDK\<ver>\<flavor>\MSI` 的 `Path`(本机 21/25 两条, JavaSoft 那个键在本机根本不存在)。装在自定义目录(不在 Program Files 下)的 MS JDK 因此漏扫——Program Files 那支兜住的只是默认位置。 - 新增 `parse_msi_paths()`:判据是「当前键尾段叫 MSI」(不数层数),多一层/空层都认; 同键里的 `JavaHome REG_DWORD`、非 MSI 键下的 `Path`、值名不是整段 `Path` 的都不认。 - 注册表读取改为可注入:`RegSource` trait + `ProcessReg`(真 `reg.exe`), `registry_homes_from(&dyn RegSource)` 让单测喂真实 `reg query` 输出样本,不依赖本机。 - 两个键 × 64/32 两个视图各查一遍,与 JavaSoft 那支同口径;键不存在照旧静默跳过。 - `scan_java_binaries()` 拆出 `java_binaries_in(roots, homes)`:注入式验收可以屏蔽 Program Files / PATH / JAVA_HOME,只走注册表;去重改用仓库既有的 `path_key` 口径 (末尾反斜杠、`\?\` 前缀、大小写都不会重复列)。 - 注册表这支整体搬进 `discover/registry.rs`(`mod registry;` + `pub use`, `discover::registry_homes` / `parse_java_home` 公开路径不变,`mod.rs` 一行未动)。
This commit is contained in:
1 parent
749d6a8fe2
commit
23e98635dd
2 files changed
+404
-78
No files matched your search
@@ -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/*` |
|
||||
//! | Termux | `$PREFIX/lib/jvm/*`(`java-17-openjdk` 这类;`$PREFIX` 由 [`crate::platform::termux`] 识别) |
|
||||
//! | macOS | `/Library/Java/JavaVirtualMachines/*.jdk/Contents/Home` |
|
||||
@@ -22,13 +22,18 @@
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::platform::child::quiet;
|
||||
use crate::platform::paths::{EnvSource, ProcessEnv};
|
||||
use crate::platform::termux;
|
||||
|
||||
use super::extract::java_binary_name;
|
||||
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` → 标准安装位置),已按路径去重。
|
||||
#[must_use]
|
||||
pub fn candidates(managed_root: &Path) -> Vec<JavaRuntime> {
|
||||
@@ -53,24 +58,28 @@ pub fn standard_candidates() -> Vec<JavaRuntime> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 扫描标准安装位置,返回 `bin/java[.exe]` 路径表(稳定排序,未探测)。
|
||||
/// 扫描标准安装位置 + 注册表,返回 `bin/java[.exe]` 路径表(按[`path_key`]稳定排序、去重,未探测)。
|
||||
#[must_use]
|
||||
pub fn scan_java_binaries() -> Vec<PathBuf> {
|
||||
let mut found: Vec<PathBuf> = Vec::new();
|
||||
for root in standard_roots() {
|
||||
let binary = root.join("bin").join(java_binary_name());
|
||||
if binary.is_file() {
|
||||
found.push(binary);
|
||||
java_binaries_in(&standard_roots(), ®istry_homes())
|
||||
}
|
||||
}
|
||||
for home in registry_homes() {
|
||||
let binary = home.join("bin").join(java_binary_name());
|
||||
if binary.is_file() {
|
||||
found.push(binary);
|
||||
}
|
||||
}
|
||||
found.sort();
|
||||
found.dedup();
|
||||
|
||||
/// 同 [`scan_java_binaries`],但两个来源都由调用方给(测试注入用)。
|
||||
///
|
||||
/// 去重口径与 [`dedupe`] 同一套([`path_key`]):同一个 JDK 被「扫目录」和「注册表」两支
|
||||
/// 同时报上来时只留一条——注册表里的 `Path` 常带末尾反斜杠,`PathBuf` 直接比也能收敛,
|
||||
/// 但大小写/`\\?\` 前缀这些写法差异只有规范化键兜得住。
|
||||
#[must_use]
|
||||
pub fn java_binaries_in(roots: &[PathBuf], registry_homes: &[PathBuf]) -> Vec<PathBuf> {
|
||||
let name = java_binary_name();
|
||||
let mut found: Vec<PathBuf> = roots
|
||||
.iter()
|
||||
.chain(registry_homes)
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -142,48 +151,6 @@ fn child_dirs(dir: &Path) -> Vec<PathBuf> {
|
||||
.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]
|
||||
pub fn dedupe(runtimes: Vec<JavaRuntime>) -> Vec<JavaRuntime> {
|
||||
@@ -225,7 +192,7 @@ pub fn path_key(path: &Path) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
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};
|
||||
// 这两个只有 Termux 扫描的两个测试用,而那两个测试是 `cfg(unix)`——导入必须同样 gate,
|
||||
// 否则 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]
|
||||
fn standard_roots_never_panic_on_missing_directories() {
|
||||
// 本机大概率缺若干目录;这里只要求「能跑、不 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(®).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(®);
|
||||
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(®);
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user