From 2ceec54790424949bb345c05839991fb53330666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Mon, 25 May 2026 23:14:26 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=85=A8=E9=9D=A2=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=2014=20=E4=B8=AA=20bug=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20Rust=20=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL: - PathTable/MergePreview 操作后不重新渲染(加 dataVersion 版本号机制) - moveUp/moveDown 后 selectedIndices 过时(更新到新位置) HIGH: - ImportDialog 显示 "true" 而非路径数量(改为 number 类型) - F1 快捷键无效果(添加 onHelp 回调) - useKeyboard 每次渲染重复注册事件(改用 ref 模式) - batch delete 撤销顺序错误(拆分为独立记录) - importPaths 存储数组引用而非副本 - StringList.all 暴露内部数组(改为返回副本) - expand_env_vars 静默吞 API 错误(加 log::warn) - join_path 写入前未修剪路径(加 trim 避免注册表污染) MEDIUM: - handleClean 总传 () => true 不验证无效路径 - HelpDialog/ImportDialog 缺 Escape 关闭 - initDarkMode 不同步 Zustand store - 多处硬编码中文改为 i18n.t() - Rust unsafe 块补全 SAFETY 注释 新增 Rust 测试: system.rs 4 个单元测试 Co-Authored-By: Claude Opus 4.7 --- src-tauri/src/commands/registry.rs | 9 +- src-tauri/src/commands/system.rs | 44 ++++++++- src/components/dialogs/HelpDialog.tsx | 10 ++ src/components/dialogs/ImportDialog.tsx | 30 ++++-- src/components/layout/AppShell.tsx | 5 +- src/components/path-list/MergePreview.tsx | 2 + src/components/path-list/PathTable.tsx | 2 + src/core/string-list.ts | 2 +- src/hooks/use-keyboard.ts | 31 +++--- src/store/app-store.ts | 115 +++++++++++----------- src/store/theme-store.ts | 16 ++- 11 files changed, 173 insertions(+), 93 deletions(-) diff --git a/src-tauri/src/commands/registry.rs b/src-tauri/src/commands/registry.rs index 09c17f2..85b4a58 100644 --- a/src-tauri/src/commands/registry.rs +++ b/src-tauri/src/commands/registry.rs @@ -77,7 +77,12 @@ fn split_path(raw: &str) -> Vec { .collect() } -/// 用分号连接路径列表 +/// 用分号连接路径列表(去除首尾空格避免污染注册表) fn join_path(paths: &[String]) -> String { - paths.join(";") + paths + .iter() + .map(|p| p.trim()) + .filter(|p| !p.is_empty()) + .collect::>() + .join(";") } diff --git a/src-tauri/src/commands/system.rs b/src-tauri/src/commands/system.rs index 2e74282..0acd2c3 100644 --- a/src-tauri/src/commands/system.rs +++ b/src-tauri/src/commands/system.rs @@ -29,28 +29,32 @@ pub fn expand_env_vars(path: &str) -> String { return path.to_string(); } - // 转为 UTF-16 宽字符串 + // 转为 UTF-16 宽字符串(以 null 结尾) let wide_path: Vec = path .encode_utf16() .chain(std::iter::once(0)) .collect(); - // 先查询需要的缓冲区大小 (lpDst=NULL) + // SAFETY: wide_path 是以 null 结尾的 UTF-16 字符串,lpDst 为 null 且 nSize 为 0, + // 根据 MSDN 文档此时 API 只查询所需缓冲区大小而不写入数据 let required = unsafe { ExpandEnvironmentStringsW(wide_path.as_ptr(), std::ptr::null_mut(), 0) }; if required == 0 { + log::warn!("expand_env_vars: API 查询缓冲区失败, 返回原始路径: {path}"); return path.to_string(); } - // 实际展开 + // SAFETY: buffer 容量为 required(API 返回的精确大小),wide_path 以 null 结尾, + // 且两个指针指向不同的内存区域,不存在重叠 let mut buffer: Vec = vec![0; required as usize]; let result = unsafe { ExpandEnvironmentStringsW(wide_path.as_ptr(), buffer.as_mut_ptr(), required) }; if result == 0 { + log::warn!("expand_env_vars: 展开失败, 返回原始路径: {path}"); return path.to_string(); } @@ -66,8 +70,11 @@ pub fn broadcast_env_change() { const WM_SETTINGCHANGE: u32 = 0x001A; const SMTO_ABORTIFHUNG: u32 = 0x0002; + // SAFETY: env_str 是以 null 结尾的 UTF-16 字符串,所有指针和常量均遵循 Win32 API 约定 let env_str: Vec = "Environment\0".encode_utf16().collect(); + // SAFETY: env_str.as_ptr() 指向以 null 结尾的字符串,HWND_BROADCAST 是合法句柄, + // lpdwResult 为 null 表示不需要返回值,其他参数均为常量 let result = unsafe { SendMessageTimeoutW( HWND_BROADCAST, @@ -108,3 +115,34 @@ extern "system" { lpdwResult: *mut usize, ) -> isize; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_path_env_var_always_valid() { + assert!(validate_path("%JAVA_HOME%\\bin")); + } + + #[test] + fn expand_env_vars_no_percent_returns_original() { + let result = expand_env_vars("C:\\Windows"); + assert_eq!(result, "C:\\Windows"); + } + + #[test] + fn expand_env_vars_with_invalid_var_returns_original() { + // 展开不存在的变量可能会回归原始值或产生部分展开;测试是否不会崩溃 + let result = expand_env_vars("%__NONEXISTENT_VAR__%"); + // 至少不应为空白 + assert!(!result.is_empty()); + } + + #[test] + fn check_admin_returns_bool() { + let result = check_admin(); + // 在任意机器上应返回 true 或 false,不应 panic + assert!((result == true) || (result == false)); + } +} diff --git a/src/components/dialogs/HelpDialog.tsx b/src/components/dialogs/HelpDialog.tsx index 8789561..8327385 100644 --- a/src/components/dialogs/HelpDialog.tsx +++ b/src/components/dialogs/HelpDialog.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; interface HelpDialogProps { @@ -8,6 +9,15 @@ interface HelpDialogProps { export function HelpDialog({ open, onClose }: HelpDialogProps) { const { t } = useTranslation(); + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open, onClose]); + if (!open) return null; return ( diff --git a/src/components/dialogs/ImportDialog.tsx b/src/components/dialogs/ImportDialog.tsx index 636b892..47978db 100644 --- a/src/components/dialogs/ImportDialog.tsx +++ b/src/components/dialogs/ImportDialog.tsx @@ -1,22 +1,32 @@ +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; interface ImportDialogProps { open: boolean; - hasSystem: boolean; - hasUser: boolean; + systemCount: number; + userCount: number; onSelect: (target: 'system' | 'user' | 'both') => void; onCancel: () => void; } export function ImportDialog({ open, - hasSystem, - hasUser, + systemCount, + userCount, onSelect, onCancel, }: ImportDialogProps) { const { t } = useTranslation(); + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onCancel(); + }; + window.addEventListener('keydown', handler); + return () => window.removeEventListener('keydown', handler); + }, [open, onCancel]); + if (!open) return null; return ( @@ -32,12 +42,12 @@ export function ImportDialog({ >

{t('dialog.importTarget')}

- {hasSystem && `系统变量: ${hasSystem}`} - {hasSystem && hasUser && ' | '} - {hasUser && `用户变量: ${hasUser}`} + {systemCount > 0 && `系统变量: ${systemCount} 条`} + {systemCount > 0 && userCount > 0 && ' | '} + {userCount > 0 && `用户变量: ${userCount} 条`}

- {hasSystem && ( + {systemCount > 0 && (