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
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:
commit
fc48094249
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/*` |
|
//! | 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(), ®istry_homes())
|
||||||
for root in standard_roots() {
|
|
||||||
let binary = root.join("bin").join(java_binary_name());
|
|
||||||
if binary.is_file() {
|
|
||||||
found.push(binary);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for home in registry_homes() {
|
/// 同 [`scan_java_binaries`],但两个来源都由调用方给(测试注入用)。
|
||||||
let binary = home.join("bin").join(java_binary_name());
|
///
|
||||||
if binary.is_file() {
|
/// 去重口径与 [`dedupe`] 同一套([`path_key`]):同一个 JDK 被「扫目录」和「注册表」两支
|
||||||
found.push(binary);
|
/// 同时报上来时只留一条——注册表里的 `Path` 常带末尾反斜杠,`PathBuf` 直接比也能收敛,
|
||||||
}
|
/// 但大小写/`\\?\` 前缀这些写法差异只有规范化键兜得住。
|
||||||
}
|
#[must_use]
|
||||||
found.sort();
|
pub fn java_binaries_in(roots: &[PathBuf], registry_homes: &[PathBuf]) -> Vec<PathBuf> {
|
||||||
found.dedup();
|
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
|
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(®).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