From dc4fac8391a9a8ef194d9f70fab64eb64e196df0 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 1 Nov 2026 10:48:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(core/game):=20M49=20=E2=80=94=E2=80=94=20Ja?= =?UTF-8?q?va=20=E5=8E=BB=E9=87=8D=E9=94=AE=E5=B9=B3=E5=8F=B0=E6=97=A0?= =?UTF-8?q?=E5=85=B3=E5=BD=92=E4=B8=80=E5=8C=96=EF=BC=88Linux=20CI=20?= =?UTF-8?q?=E4=B8=89=E7=BA=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `game::java::discover::path_key` 只在 `cfg!(windows)` 分支折分隔符,POSIX 上 把 `C:\a\b\java.exe` 原样留成带反斜杠的键,与 `C:/a/b/java.exe` 不同键 → `dedupe` 不去重。三条用例(Windows 语义无 cfg 门)因此在 `test (ubuntu-latest)` 挂:`path_key_normalises_separators`、 `dedupe_keeps_the_higher_priority_source`、 `java_discover::dedupe_collapses_one_install_found_twice`。 - `path_key`:`\?\` 前缀剥离与分隔符折成 `/` 改为**与平台无关**(同一个 `bin/java[.exe]` 用哪种拼法都是同一个文件,键必须相同);大小写折叠仍只有 Windows 做——POSIX 上 `Java` 与 `java` 是两个目录,折了会把两份真实安装误并。 已知取舍写进文档:POSIX 上文件名里的字面反斜杠会一并被当分隔符。 - 三条用例:不靠 `#[cfg(windows)]` 跳过。`path_key` 单测改成平台无关断言 + 大小写折叠的双侧断言(Windows 折 / POSIX 不折,两侧都钉住);集成用例的 「同一路径第二种写法」按平台给(Windows 用大小写+分隔符,POSIX 用分隔符)。 - 回归钉子:新增 `one_install_normalises_to_one_key_whatever_the_spelling` (普通写法 / 反斜杠写法 / `\?\` 前缀写法 / 混合写法 四种拼法同键)与 `path_key_folds_case_only_where_the_filesystem_does`。 验证:Linux(rust:1-bookworm 容器,ull-linux-target 热缓存) `cargo test -p unlucky-core --lib --test game` → 224 + 120 passed / 0 failed (基线是 lib 220/2 failed、game 119/1 failed); `cargo test --workspace --all-targets --no-fail-fast` → 34 个测试块 1137 passed / 0 failed(该次跑在 M48 launch 修复叠加的临时副本上,因为 M48 尚未合入 main,见报告)。Windows 本机 `cargo fmt --check`、 `cargo clippy -p unlucky-core --all-targets -D warnings`、 `cargo test -p unlucky-core` 全绿(17 个测试块 0 failed)。 --- crates/core/src/game/java/discover.rs | 72 ++++++++++++++++++++++--- crates/core/tests/game/java_discover.rs | 13 +++-- 2 files changed, 72 insertions(+), 13 deletions(-) diff --git a/crates/core/src/game/java/discover.rs b/crates/core/src/game/java/discover.rs index ded92af..b44a3e5 100644 --- a/crates/core/src/game/java/discover.rs +++ b/crates/core/src/game/java/discover.rs @@ -186,13 +186,24 @@ pub fn dedupe(runtimes: Vec) -> Vec { kept } -/// 去重键:去掉 Windows 的 `\\?\` 前缀、统一分隔符、大小写不敏感(Windows)。 +/// 去重键:剥掉 Windows 的 `\\?\` 前缀、分隔符一律折成 `/`、大小写只在 Windows 折叠。 +/// +/// 前两步与平台无关:同一条 `bin/java[.exe]` 用 `/` 还是 `\` 拼出来都是同一个文件, +/// 键必须相同——`JAVA_HOME` 的写法与扫盘拼出来的路径常在这点上不一致,键不齐就重复列。 +/// 大小写折叠**只能**跟着平台走:NTFS 不区分大小写,而 POSIX 上 `Java` 与 `java` 是两个 +/// 目录,折了会把两份真实安装误并成一个。 +/// +/// 代价(已知且有意的取舍):POSIX 上文件名里的字面反斜杠也会被当分隔符。JDK 安装路径带 +/// 反斜杠属于病态情况,换来的是「同一条 java 的两种拼法永远同键」这条平台无关的不变量。 #[must_use] pub fn path_key(path: &Path) -> String { let text = path.display().to_string(); - let text = text.strip_prefix(r"\\?\").unwrap_or(&text).to_string(); + let text = text + .strip_prefix(r"\\?\") + .unwrap_or(&text) + .replace('\\', "/"); if cfg!(windows) { - text.replace('/', "\\").to_ascii_lowercase() + text.to_ascii_lowercase() } else { text } @@ -236,11 +247,56 @@ mod tests { } #[test] - fn path_key_normalises_separators() { - let left = path_key(std::path::Path::new("C:/a/b/java.exe")); - let right = path_key(std::path::Path::new("C:\\a\\b\\java.exe")); - assert_eq!(left, right); - assert!(!path_key(std::path::Path::new(r"\\?\C:\a\java.exe")).starts_with(r"\\")); + fn path_key_normalises_separators_on_every_platform() { + // 平台无关的不变量:同一条 `bin/java` 用 `/` 还是 `\` 拼出来,键必须相同。 + // (M49:原来这里只在 Windows 上成立,Linux 上 `\` 原样留着 → 断言挂。) + let forward = path_key(std::path::Path::new("C:/a/b/java.exe")); + let backward = path_key(std::path::Path::new("C:\\a\\b\\java.exe")); + assert_eq!(forward, backward); + assert!( + !forward.contains('\\'), + "键里不该残留反斜杠(与平台无关):{forward}" + ); + assert!( + !forward.starts_with(r"\\"), + "`\\\\?\\` 前缀不进键:{forward}" + ); + } + + #[test] + fn one_install_normalises_to_one_key_whatever_the_spelling() { + // 回归钉子:`JAVA_HOME` 与扫盘常对同一个 java 给出不同写法,四种拼法必须同键。 + let spellings = [ + "C:/Program Files/Java/jdk-21/bin/java.exe", + r"C:\Program Files\Java\jdk-21\bin\java.exe", + r"\\?\C:\Program Files\Java\jdk-21\bin\java.exe", + r"C:\Program Files\Java\jdk-21/bin/java.exe", + ]; + let key = path_key(std::path::Path::new(spellings[0])); + for spelling in spellings { + assert_eq!( + path_key(std::path::Path::new(spelling)), + key, + "同一安装的两种写法必须同键:{spelling}" + ); + } + } + + #[test] + fn path_key_folds_case_only_where_the_filesystem_does() { + // 大小写折叠跟平台走,两侧都钉住:NTFS 不区分大小写 → 折; + // POSIX 上 `Java` 与 `java` 是两个目录 → 不折(折了会把两份真实安装误并)。 + let upper = path_key(std::path::Path::new( + "C:/Program Files/Java/jdk-21/bin/java.exe", + )); + let lower = path_key(std::path::Path::new( + "c:/program files/java/jdk-21/bin/java.exe", + )); + if cfg!(windows) { + assert_eq!(upper, lower, "Windows:大小写不同的同一路径应同键"); + } else { + assert_ne!(upper, lower, "POSIX:大小写是有效区别,不应同键"); + } } #[test] diff --git a/crates/core/tests/game/java_discover.rs b/crates/core/tests/game/java_discover.rs index cb25780..dd18892 100644 --- a/crates/core/tests/game/java_discover.rs +++ b/crates/core/tests/game/java_discover.rs @@ -96,17 +96,20 @@ fn scanned_java_is_still_used_when_nothing_else_matches() { #[test] fn dedupe_collapses_one_install_found_twice() { // `JAVA_HOME` 指向 Program Files 里的 JDK → 同一个 java 不应列两次。 + // 第二条的「另一种写法」随平台给:Windows 上大小写和分隔符都可能不一致(NTFS 不区分 + // 大小写),POSIX 上大小写是有效区别,只能靠分隔符拼出同一路径的第二种写法。 + let duplicate = if cfg!(windows) { + "c:/program files/java/jdk-21/bin/java.exe" + } else { + "C:\\Program Files\\Java\\jdk-21/bin/java.exe" + }; let candidates = discover::dedupe(vec![ runtime( "C:/Program Files/Java/jdk-21/bin/java.exe", 21, JavaOrigin::JavaHome, ), - runtime( - "c:/program files/java/jdk-21/bin/java.exe", - 21, - JavaOrigin::System, - ), + runtime(duplicate, 21, JavaOrigin::System), ]); assert_eq!(candidates.len(), 1); assert_eq!(candidates[0].origin, JavaOrigin::JavaHome);