diff --git a/.gitignore b/.gitignore index ad67955..fcd113b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ target # Contains mutation testing data **/mutants.out*/ +# Claude Code +.claude/ +*-cwd + # RustRover # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1701d87 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +基于 Tauri 2.0 的 UPX 可视化加壳/脱壳工具,Windows 平台专用。 + +### 技术栈 +- **前端**:原生 HTML + TailwindCSS + JavaScript(ui/ 目录) +- **后端**:Rust + Tauri 2.0(src-tauri/ 目录) +- **核心**:UPX 可执行文件(upx/upx.exe) + +### 应用架构 +- **单窗口应用**:自定义标题栏(无边框窗口 `decorations: false`) +- **拖放支持**:支持文件/文件夹拖放到窗口,自动判断操作区域 +- **多线程批处理**:根据 CPU 核心数动态调整并发数 +- **配置持久化**:配置保存在程序目录的 `upx_gui_config.json` + +## 常用命令 + +```bash +# 开发模式运行(热重载) +cargo tauri dev + +# 编译发行版 +cargo tauri build + +# 编译产物位置 +# src-tauri/target/release/bundle/ +# 生成 MSI 和 NSIS 两种安装包 +``` + +## 代码结构 + +### 前端 (ui/) +- `index.html` - 主界面,包含双操作按钮布局 +- `js/main.js` - 前端逻辑,约 740 行,包含: + - 拖放检测与区域判断 + - 动态批处理(基于 CPU 核心数) + - 配置的保存/加载 + - 日志系统(带数量限制和自动清理) +- `css/style.css` - 主样式 +- `css/tailwind-custom.css` - Tailwind 自定义 + +### 后端 (src-tauri/src/) +- `main.rs` - 唯一的 Rust 源文件,约 500 行,包含所有 Tauri commands: + - `process_upx` - 执行压缩/解压 + - `scan_folder` - 递归扫描 exe/dll 文件 + - `get_upx_version` - 获取 UPX 版本 + - `refresh_icon_cache` - 刷新 Windows 图标缓存 + - `save_config` / `load_config` - 配置持久化 + +## 重要细节 + +### UPX 路径解析 +开发环境:`../upx/upx.exe` +打包后:`_up_/upx/upx.exe`(相对于程序目录) + +### 编码处理 +UPX 在 Windows 上输出 GBK 编码,使用 `encoding_rs::GBK` 转换为 UTF-8 + +### 批处理并发策略 +```javascript +batchSize = Math.max(2, Math.min(cpuCores * 2, 16)) +``` +最小 2,最大 16,默认为 CPU 核心数的 2 倍 + +### Tauri 2.0 特性 +- 使用 `window.__TAURI__` 全局对象(需启用 `withGlobalTauri: true`) +- 命令调用:`invoke('command_name', { options })` +- 拖放事件:`listen('tauri://drag-drop', handler)` diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index b1390f4..377e56c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,6 +13,10 @@ use std::os::windows::process::CommandExt; #[cfg(target_os = "windows")] const CREATE_NO_WINDOW: u32 = 0x08000000; +// ============================================================================ +// 数据结构定义 +// ============================================================================ + #[derive(Debug, Serialize, Deserialize)] struct UpxOptions { mode: String, @@ -53,17 +57,17 @@ impl Default for AppConfig { } } -// 获取配置文件路径 +// ============================================================================ +// 路径解析 +// ============================================================================ + fn get_config_path() -> Option { - if let Ok(exe_path) = std::env::current_exe() { - if let Some(exe_dir) = exe_path.parent() { - return Some(exe_dir.join("upx_gui_config.json")); - } - } - None + std::env::current_exe() + .ok()? + .parent() + .map(|p| p.join("upx_gui_config.json")) } -// 获取UPX可执行文件路径 fn get_upx_path() -> Option { // 打包后的位置 if let Ok(exe_path) = std::env::current_exe() { @@ -77,252 +81,140 @@ fn get_upx_path() -> Option { // 开发环境 let dev_upx = PathBuf::from("../upx/upx.exe"); - if dev_upx.exists() { - return Some(dev_upx); - } - - None + dev_upx.exists().then_some(dev_upx) } -#[tauri::command] -async fn process_upx(options: UpxOptions) -> Result { - // 获取UPX可执行文件路径 - let upx_path = get_upx_path().ok_or("未找到 UPX 工具!请确保安装完整")?; +// ============================================================================ +// 命令构建辅助 +// ============================================================================ - // 检查 UPX 是否可用 - let mut upx_check = Command::new(&upx_path); - - // 在 Windows 上隐藏控制台窗口 - #[cfg(target_os = "windows")] - upx_check.creation_flags(CREATE_NO_WINDOW); - - let upx_check = upx_check.arg("--version").output(); - - if upx_check.is_err() { - return Err("UPX 工具无法执行!".to_string()); - } - - // 检查输入文件是否存在 - if !Path::new(&options.input_file).exists() { - return Err(format!("输入文件不存在: {}", options.input_file)); - } - - // 检查文件是否可写(如果需要覆盖) - if options.input_file == options.output_file { - let metadata = - fs::metadata(&options.input_file).map_err(|e| format!("无法读取文件属性: {}", e))?; - - if metadata.permissions().readonly() { - return Err("文件为只读,请先修改文件属性".to_string()); - } - } - - // 备份原文件 - if options.backup { - let backup_path = format!("{}.bak", options.input_file); - if let Err(e) = fs::copy(&options.input_file, &backup_path) { - return Err(format!("备份文件失败: {}", e)); - } - } - - // 构建 UPX 命令 - let mut cmd = Command::new(&upx_path); - - // 在 Windows 上隐藏控制台窗口 +fn create_silent_command(upx_path: &PathBuf) -> Command { + let mut cmd = Command::new(upx_path); #[cfg(target_os = "windows")] cmd.creation_flags(CREATE_NO_WINDOW); - - // 判断是否覆盖原文件 - let is_overwrite = options.input_file == options.output_file; - - match options.mode.as_str() { - "compress" => { - // 加壳模式 - if options.ultra_brute { - cmd.arg("--ultra-brute"); - cmd.arg("--no-lzma"); // 禁用LZMA以避免解压速度过慢 - } else if options.compression_level == "best" { - cmd.arg("--best"); - } else { - cmd.arg(format!("-{}", options.compression_level)); - } - - // 添加强制压缩参数 - if options.force { - cmd.arg("--force"); - } - - if is_overwrite { - // 直接覆盖原文件,不使用 -o 参数 - cmd.arg(&options.input_file); - } else { - // 输出到指定文件 - cmd.arg(&options.input_file); - cmd.arg("-o"); - cmd.arg(&options.output_file); - } - cmd.arg("--force-overwrite"); - } - "decompress" => { - // 脱壳模式 - cmd.arg("-d"); - - // 添加强制压缩参数(脱壳时也可能需要) - if options.force { - cmd.arg("--force"); - } - - if is_overwrite { - // 直接覆盖原文件 - cmd.arg(&options.input_file); - } else { - // 输出到指定文件 - cmd.arg(&options.input_file); - cmd.arg("-o"); - cmd.arg(&options.output_file); - } - cmd.arg("--force-overwrite"); - } - _ => { - return Err("未知的操作模式".to_string()); - } - } - - // 在处理前记录原始文件大小 - let original_size = fs::metadata(&options.input_file) - .map(|m| m.len()) - .unwrap_or(0); - - // 在后台线程执行命令,避免阻塞 UI - let output_file_clone = options.output_file.clone(); - let original_size_clone = original_size; - - tokio::task::spawn_blocking(move || { - match cmd.output() { - Ok(output) => { - // 在 Windows 上 UPX 输出是 GBK 编码,需要转换为 UTF-8 - let (stdout, _, _) = GBK.decode(&output.stdout); - let (stderr, _, _) = GBK.decode(&output.stderr); - - if output.status.success() { - // 获取处理后的文件大小 - let output_size = fs::metadata(&output_file_clone) - .map(|m| m.len()) - .unwrap_or(0); - - let ratio = if original_size_clone > 0 { - (output_size as f64 / original_size_clone as f64 * 100.0) as i32 - } else { - 100 - }; - - // 过滤并格式化输出信息 - let upx_output = format_upx_output(&stdout, &stderr); - - let result = format!( - "操作成功!\n输出: {}\n原始大小: {}\n处理后大小: {}\n压缩率: {}%{}", - output_file_clone, - format_bytes(original_size_clone), - format_bytes(output_size), - ratio, - upx_output - ); - Ok(result) - } else { - // 解析并优化错误信息 - let error_msg = parse_upx_error(&stdout, &stderr); - Err(error_msg) - } - } - Err(e) => Err(format!("执行 UPX 命令失败: {}", e)), - } - }) - .await - .map_err(|e| format!("任务执行错误: {}", e))? + cmd +} + +fn build_compress_args( + options: &UpxOptions, + is_overwrite: bool, +) -> impl Iterator + '_ { + let mut args = Vec::new(); + + // 压缩级别 + if options.ultra_brute { + args.push("--ultra-brute".to_string()); + args.push("--no-lzma".to_string()); + } else if options.compression_level == "best" { + args.push("--best".to_string()); + } else { + args.push(format!("-{}", options.compression_level)); + } + + // 强制压缩 + if options.force { + args.push("--force".to_string()); + } + + // 输入输出 + args.push(options.input_file.clone()); + if !is_overwrite { + args.push("-o".to_string()); + args.push(options.output_file.clone()); + } + args.push("--force-overwrite".to_string()); + + args.into_iter() +} + +fn build_decompress_args( + options: &UpxOptions, + is_overwrite: bool, +) -> impl Iterator + '_ { + let mut args = vec!["-d".to_string(), options.input_file.clone()]; + + if options.force { + args.push("--force".to_string()); + } + + if !is_overwrite { + args.push("-o".to_string()); + args.push(options.output_file.clone()); + } + args.push("--force-overwrite".to_string()); + + args.into_iter() +} + +// ============================================================================ +// 输出处理 +// ============================================================================ + +const IGNORED_PREFIXES: &[&str] = &[ + "---", + "File size", + "Ratio", + "Format", + "Name", + "Ultimate Packer", + "Copyright", + "UPX ", +]; + +fn filter_output_lines(text: &str) -> Vec<&str> { + text.lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.is_empty() { + return None; + } + let should_keep = !IGNORED_PREFIXES + .iter() + .any(|prefix| trimmed.starts_with(prefix)); + should_keep.then_some(trimmed) + }) + .collect() } -// 格式化 UPX 成功输出信息 fn format_upx_output(stdout: &str, stderr: &str) -> String { let combined = format!("{}{}", stdout, stderr); - - // 过滤掉不需要的行 - let output_lines: Vec<&str> = combined - .lines() - .filter(|line| { - let l = line.trim(); - !l.is_empty() - && !l.starts_with("---") - && !l.starts_with("File size") - && !l.starts_with("Ratio") - && !l.starts_with("Format") - && !l.starts_with("Name") - && !l.starts_with("Ultimate Packer") - && !l.starts_with("Copyright") - && !l.starts_with("UPX ") - }) - .collect(); - - if output_lines.is_empty() { + let lines = filter_output_lines(&combined); + if lines.is_empty() { return String::new(); } - - format!("\n\nUPX 输出:\n{}", output_lines.join("\n")) + format!("\n\nUPX 输出:\n{}", lines.join("\n")) } -// 解析并优化 UPX 错误信息 fn parse_upx_error(stdout: &str, stderr: &str) -> String { let combined = format!("{}{}", stdout, stderr); - // 常见错误模式匹配 - if combined.contains("AlreadyPackedException") || combined.contains("already packed") { - return "[错误] 文件已经被 UPX 加壳过了\n\n解决方案:\n - 如果要重新压缩,请先使用「脱壳解压」功能\n - 或者选择其他未加壳的文件".to_string(); + let error_patterns: &[(&[&str], &str)] = &[ + (&["AlreadyPackedException", "already packed"], "[错误] 文件已经被 UPX 加壳过了\n\n解决方案:\n - 如果要重新压缩,请先使用「脱壳解压」功能\n - 或者选择其他未加壳的文件"), + (&["NotPackedException", "not packed"], "[错误] 文件未被 UPX 加壳,无法脱壳\n\n解决方案:\n - 请确认文件是否使用 UPX 加壳\n - 或者选择「加壳压缩」功能"), + (&["CantPackException"], "[错误] 无法压缩此文件\n\n可能原因:\n - 文件格式不支持\n - 文件已损坏\n - 文件受保护(尝试启用「强制压缩」选项)"), + (&["OverlayException"], "[错误] 文件包含附加数据(Overlay)\n\n解决方案:\n - 某些文件在末尾附加了额外数据\n - 尝试启用「强制压缩」选项\n - 或使用其他工具移除附加数据"), + (&["IOException", "can't open"], "[错误] 文件访问失败\n\n可能原因:\n - 文件被其他程序占用\n - 文件权限不足\n - 文件路径包含特殊字符"), + (&["NotCompressibleException"], "[错误] 文件无法压缩\n\n可能原因:\n - 文件已经高度压缩\n - 压缩后反而会变大\n - UPX 自动跳过了此文件"), + ]; + + for (patterns, message) in error_patterns { + if patterns.iter().any(|p| combined.contains(p)) { + return message.to_string(); + } } - if combined.contains("NotPackedException") || combined.contains("not packed") { - return "[错误] 文件未被 UPX 加壳,无法脱壳\n\n解决方案:\n - 请确认文件是否使用 UPX 加壳\n - 或者选择「加壳压缩」功能".to_string(); - } - - if combined.contains("CantPackException") { - return "[错误] 无法压缩此文件\n\n可能原因:\n - 文件格式不支持\n - 文件已损坏\n - 文件受保护(尝试启用「强制压缩」选项)".to_string(); - } - - if combined.contains("OverlayException") { - return "[错误] 文件包含附加数据(Overlay)\n\n解决方案:\n - 某些文件在末尾附加了额外数据\n - 尝试启用「强制压缩」选项\n - 或使用其他工具移除附加数据".to_string(); - } - - if combined.contains("IOException") || combined.contains("can't open") { - return "[错误] 文件访问失败\n\n可能原因:\n - 文件被其他程序占用\n - 文件权限不足\n - 文件路径包含特殊字符".to_string(); - } - - if combined.contains("NotCompressibleException") { - return "[错误] 文件无法压缩\n\n可能原因:\n - 文件已经高度压缩\n - 压缩后反而会变大\n - UPX 自动跳过了此文件".to_string(); - } - - // 如果没有匹配到特定错误,返回简化的原始信息 - let error_lines: Vec<&str> = combined - .lines() - .filter(|line| { - let l = line.trim(); - !l.is_empty() - && !l.starts_with("---") - && !l.starts_with("File size") - && !l.starts_with("Ratio") - && !l.starts_with("Format") - && !l.starts_with("Name") - }) - .collect(); - - if error_lines.is_empty() { + // 通用错误信息 + let lines = filter_output_lines(&combined); + if lines.is_empty() { return "[错误] UPX 处理失败\n\n请检查文件是否正常,或尝试其他选项".to_string(); } - - format!( - "[错误] UPX 处理失败\n\n错误信息:\n{}", - error_lines.join("\n") - ) + format!("[错误] UPX 处理失败\n\n错误信息:\n{}", lines.join("\n")) } -// 格式化字节大小 +// ============================================================================ +// 工具函数 +// ============================================================================ + fn format_bytes(bytes: u64) -> String { const KB: u64 = 1024; const MB: u64 = KB * 1024; @@ -339,7 +231,120 @@ fn format_bytes(bytes: u64) -> String { } } -// 扫描文件夹获取所有exe和dll文件 +// ============================================================================ +// UPX 处理核心 +// ============================================================================ + +fn validate_upx_and_file(upx_path: &PathBuf, input_file: &str) -> Result<(), String> { + // 检查 UPX 可用性 + create_silent_command(upx_path) + .arg("--version") + .output() + .map_err(|_| "UPX 工具无法执行!".to_string())?; + + // 检查输入文件 + if !Path::new(input_file).exists() { + return Err(format!("输入文件不存在: {}", input_file)); + } + + Ok(()) +} + +fn validate_file_writable(file: &str) -> Result<(), String> { + let metadata = fs::metadata(file).map_err(|e| format!("无法读取文件属性: {}", e))?; + if metadata.permissions().readonly() { + return Err("文件为只读,请先修改文件属性".to_string()); + } + Ok(()) +} + +fn create_backup(file: &str) -> Result<(), String> { + let backup_path = format!("{}.bak", file); + fs::copy(file, &backup_path) + .map(|_| ()) + .map_err(|e| format!("备份文件失败: {}", e)) +} + +fn execute_upx( + cmd: &mut Command, + output_file: String, + original_size: u64, +) -> Result { + let output = cmd + .output() + .map_err(|e| format!("执行 UPX 命令失败: {}", e))?; + + let (stdout, _, _) = GBK.decode(&output.stdout); + let (stderr, _, _) = GBK.decode(&output.stderr); + + if output.status.success() { + let output_size = fs::metadata(&output_file).map(|m| m.len()).unwrap_or(0); + + let ratio = if original_size > 0 { + (output_size as f64 / original_size as f64 * 100.0) as i32 + } else { + 100 + }; + + let upx_output = format_upx_output(&stdout, &stderr); + + Ok(format!( + "操作成功!\n输出: {}\n原始大小: {}\n处理后大小: {}\n压缩率: {}%{}", + output_file, + format_bytes(original_size), + format_bytes(output_size), + ratio, + upx_output + )) + } else { + Err(parse_upx_error(&stdout, &stderr)) + } +} + +#[tauri::command] +async fn process_upx(options: UpxOptions) -> Result { + let upx_path = get_upx_path().ok_or("未找到 UPX 工具!请确保安装完整")?; + + validate_upx_and_file(&upx_path, &options.input_file)?; + + let is_overwrite = options.input_file == options.output_file; + if is_overwrite { + validate_file_writable(&options.input_file)?; + } + + if options.backup { + create_backup(&options.input_file)?; + } + + let original_size = fs::metadata(&options.input_file) + .map(|m| m.len()) + .unwrap_or(0); + + let mut cmd = create_silent_command(&upx_path); + + match options.mode.as_str() { + "compress" => { + cmd.args(build_compress_args(&options, is_overwrite)); + } + "decompress" => { + cmd.args(build_decompress_args(&options, is_overwrite)); + } + _ => return Err("未知的操作模式".to_string()), + } + + let output_file_clone = options.output_file.clone(); + + tokio::task::spawn_blocking(move || execute_upx(&mut cmd, output_file_clone, original_size)) + .await + .map_err(|e| format!("任务执行错误: {}", e))? +} + +// ============================================================================ +// 文件夹扫描 +// ============================================================================ + +const SUPPORTED_EXTENSIONS: &[&str] = &["exe", "dll"]; + fn scan_folder_recursive(folder_path: &Path, include_subfolders: bool) -> Vec { let mut files = Vec::new(); @@ -348,13 +353,11 @@ fn scan_folder_recursive(folder_path: &Path, include_subfolders: bool) -> Vec Result, String> { Ok(scan_folder_recursive(path, options.include_subfolders)) } -// 获取UPX版本 +// ============================================================================ +// UPX 版本查询 +// ============================================================================ + #[tauri::command] fn get_upx_version() -> Result { let upx_path = get_upx_path().ok_or("未找到 UPX 工具!")?; - let mut cmd = Command::new(&upx_path); + let output = create_silent_command(&upx_path) + .arg("--version") + .output() + .map_err(|_| "UPX未找到".to_string())?; - // 在 Windows 上隐藏控制台窗口 - #[cfg(target_os = "windows")] - cmd.creation_flags(CREATE_NO_WINDOW); - - match cmd.arg("--version").output() { - Ok(output) => { - let (version_str, _, _) = GBK.decode(&output.stdout); - let lines: Vec<&str> = version_str.lines().collect(); - if !lines.is_empty() { - Ok(lines[0].to_string()) - } else { - Err("无法获取UPX版本".to_string()) - } - } - Err(_) => Err("UPX未找到".to_string()), - } + let (version_str, _, _) = GBK.decode(&output.stdout); + version_str + .lines() + .next() + .map(|s| s.to_string()) + .ok_or_else(|| "无法获取UPX版本".to_string()) +} + +// ============================================================================ +// 图标缓存刷新(仅 Windows) +// ============================================================================ + +#[cfg(target_os = "windows")] +fn refresh_icon_cache_internal() { + use std::env; + + // 关闭 Explorer + let _ = Command::new("taskkill") + .args(["/f", "/im", "explorer.exe"]) + .creation_flags(CREATE_NO_WINDOW) + .output(); + + std::thread::sleep(std::time::Duration::from_millis(500)); + + // 删除图标缓存 + if let Ok(userprofile) = env::var("USERPROFILE") { + let cache_db = format!("{}\\AppData\\Local\\IconCache.db", userprofile); + let _ = fs::remove_file(cache_db); + + // 删除缩略图缓存 + let explorer_path = format!( + "{}\\AppData\\Local\\Microsoft\\Windows\\Explorer", + userprofile + ); + if let Ok(entries) = fs::read_dir(explorer_path) { + for entry in entries.flatten() { + let path = entry.path(); + if path + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.starts_with("thumbcache_")) + .unwrap_or(false) + { + let _ = fs::remove_file(path); + } + } + } + } + + std::thread::sleep(std::time::Duration::from_millis(500)); + + // 重启 Explorer + let _ = Command::new("explorer.exe") + .creation_flags(CREATE_NO_WINDOW) + .spawn(); } -// 刷新图标缓存 #[tauri::command] #[allow(dead_code)] async fn refresh_icon_cache() -> Result<(), String> { - // 后台执行刷新 tokio::task::spawn_blocking(move || { #[cfg(target_os = "windows")] - { - // 1. 关闭 Explorer - let _ = Command::new("taskkill") - .args(["/f", "/im", "explorer.exe"]) - .creation_flags(CREATE_NO_WINDOW) - .output(); - - std::thread::sleep(std::time::Duration::from_millis(500)); - - // 2. 删除图标缓存 - if let Ok(userprofile) = std::env::var("USERPROFILE") { - let cache_db = format!("{}\\AppData\\Local\\IconCache.db", userprofile); - let _ = fs::remove_file(&cache_db); - - // 删除缩略图缓存 - let explorer_path = format!( - "{}\\AppData\\Local\\Microsoft\\Windows\\Explorer", - userprofile - ); - if let Ok(entries) = fs::read_dir(&explorer_path) { - for entry in entries.flatten() { - let path = entry.path(); - if let Some(name) = path.file_name().and_then(|n| n.to_str()) { - if name.starts_with("thumbcache_") { - let _ = fs::remove_file(&path); - } - } - } - } - } - - std::thread::sleep(std::time::Duration::from_millis(500)); - - // 3. 重启 Explorer - let _ = Command::new("explorer.exe") - .creation_flags(CREATE_NO_WINDOW) - .spawn(); - } - }); + refresh_icon_cache_internal(); + }) + .await + .ok(); Ok(()) } -// 保存配置 +// ============================================================================ +// 配置持久化 +// ============================================================================ + #[tauri::command] fn save_config(config: AppConfig) -> Result<(), String> { let config_path = get_config_path().ok_or("无法获取配置文件路径")?; @@ -466,7 +480,6 @@ fn save_config(config: AppConfig) -> Result<(), String> { fs::write(&config_path, json).map_err(|e| format!("保存配置文件失败: {}", e)) } -// 加载配置 #[tauri::command] fn load_config() -> Result { let config_path = get_config_path().ok_or("无法获取配置文件路径")?; @@ -480,6 +493,10 @@ fn load_config() -> Result { serde_json::from_str(&json).map_err(|e| format!("解析配置文件失败: {}", e)) } +// ============================================================================ +// Tauri 应用入口 +// ============================================================================ + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) diff --git a/ui/js/main.js b/ui/js/main.js index 1f7d5e4..67de344 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -3,19 +3,14 @@ const { open, save } = window.__TAURI__.dialog; const { getCurrentWindow } = window.__TAURI__.window; const { listen } = window.__TAURI__.event; -// 获取当前窗口实例 const appWindow = getCurrentWindow(); -// 性能配置:动态批处理大小 const PERFORMANCE_CONFIG = { - cpuCores: navigator.hardwareConcurrency || 4, // CPU逻辑核心数 - batchSize: null // 将在初始化时计算 + cpuCores: navigator.hardwareConcurrency || 4, + batchSize: null }; -// 计算最优批处理大小 function calculateOptimalBatchSize() { - // 根据CPU核心数计算:每个核心处理1-2个任务 - // 最小为2,最大为16(避免过度并发) const optimal = Math.max(2, Math.min(PERFORMANCE_CONFIG.cpuCores * 2, 16)); PERFORMANCE_CONFIG.batchSize = optimal; return optimal; @@ -87,30 +82,24 @@ document.addEventListener('DOMContentLoaded', async () => { }, { passive: true }); }); -// 屏蔽刷新快捷键 function preventRefresh() { - // 使用 Map 提高查找效率 - const blockedKeys = new Set(['F5']); + const BLOCKED_COMBINATIONS = [ + { key: 'F5', message: '刷新功能已禁用' }, + { ctrlKey: true, key: 'r', message: '刷新功能已禁用' }, + { ctrlKey: true, key: 'w', message: null } + ]; document.addEventListener('keydown', (e) => { - // F5 刷新 - if (blockedKeys.has(e.key)) { - e.preventDefault(); - addLog('刷新功能已禁用', 'warning'); - return false; - } + for (const combo of BLOCKED_COMBINATIONS) { + const isMatch = combo.ctrlKey + ? e.ctrlKey && e.key.toLowerCase() === combo.key + : e.key === combo.key; - // Ctrl+R 或 Ctrl+Shift+R 刷新 - if (e.ctrlKey && e.key.toLowerCase() === 'r') { - e.preventDefault(); - addLog('刷新功能已禁用', 'warning'); - return false; - } - - // Ctrl+W 关闭窗口 - if (e.ctrlKey && e.key.toLowerCase() === 'w') { - e.preventDefault(); - return false; + if (isMatch) { + e.preventDefault(); + if (combo.message) addLog(combo.message, 'warning'); + return false; + } } }, { passive: false }); } @@ -147,78 +136,48 @@ function initTitleClick() { }); } -// 操作按钮初始化 function initOperationButtons() { - // 刷新图标缓存按钮 - refreshIconBtn.addEventListener('click', async () => { - await handleRefreshIcon(); - }); + refreshIconBtn.addEventListener('click', handleRefreshIcon); + settingsBtn.addEventListener('click', showSettingsModal); + closeSettingsBtn.addEventListener('click', handleCloseSettings); + settingsModal.addEventListener('click', handleModalBackdropClick); - // 设置按钮 - 显示全局设置 - settingsBtn.addEventListener('click', () => { - showSettingsModal(); - }); + compressBtn.addEventListener('click', () => handleOperationClick('compress')); + decompressBtn.addEventListener('click', () => handleOperationClick('decompress')); +} - // 关闭设置按钮 - closeSettingsBtn.addEventListener('click', async () => { +async function handleCloseSettings() { + await saveCurrentConfig(); + hideSettingsModal(); + addLog('设置已保存', 'success'); +} + +async function handleModalBackdropClick(e) { + if (e.target === settingsModal) { await saveCurrentConfig(); hideSettingsModal(); - addLog('设置已保存', 'success'); - }); + } +} - // 点击背景关闭弹窗 - settingsModal.addEventListener('click', async (e) => { - if (e.target === settingsModal) { - await saveCurrentConfig(); - hideSettingsModal(); - } - }); +async function handleOperationClick(mode) { + const modeName = mode === 'compress' ? '加壳压缩' : '脱壳解压'; + const processFile = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile; + const selectFile = mode === 'compress' ? handleCompress : handleDecompress; - // 加壳压缩按钮 - 直接执行 - compressBtn.addEventListener('click', async () => { - // 检查是否有多个文件 - if (window.droppedFiles && window.droppedFiles.length > 0) { - const files = window.droppedFiles; - window.droppedFiles = null; - addLog('开始批量加壳压缩...', 'info'); - await processBatchFiles(files, 'compress'); - } - // 检查是否有单个文件 - else if (window.droppedFile) { - const filePath = window.droppedFile; - window.droppedFile = null; - addLog('开始加壳压缩...', 'info'); - await handleCompressWithFile(filePath); - } - // 没有拖放文件,弹出选择 - else { - addLog('选择文件进行加壳...', 'info'); - await handleCompress(); - } - }); - - // 脱壳解压按钮 - 直接执行 - decompressBtn.addEventListener('click', async () => { - // 检查是否有多个文件 - if (window.droppedFiles && window.droppedFiles.length > 0) { - const files = window.droppedFiles; - window.droppedFiles = null; - addLog('开始批量脱壳解压...', 'info'); - await processBatchFiles(files, 'decompress'); - } - // 检查是否有单个文件 - else if (window.droppedFile) { - const filePath = window.droppedFile; - window.droppedFile = null; - addLog('开始脱壳解压...', 'info'); - await handleDecompressWithFile(filePath); - } - // 没有拖放文件,弹出选择 - else { - addLog('选择文件进行脱壳...', 'info'); - await handleDecompress(); - } - }); + if (window.droppedFiles?.length > 0) { + const files = window.droppedFiles; + window.droppedFiles = null; + addLog(`开始批量${modeName}...`, 'info'); + await processBatchFiles(files, mode); + } else if (window.droppedFile) { + const filePath = window.droppedFile; + window.droppedFile = null; + addLog(`开始${modeName}...`, 'info'); + await processFile(filePath); + } else { + addLog(`选择文件进行${modeName === '加壳压缩' ? '加壳' : '脱壳'}...`, 'info'); + await selectFile(); + } } // 级别描述映射(全局常量) @@ -280,7 +239,6 @@ async function scanFolder(folderPath, includeSubfolders) { } } -// 批量处理文件 async function processBatchFiles(files, mode) { if (files.length === 0) { addLog('没有找到可处理的文件', 'warning'); @@ -290,104 +248,99 @@ async function processBatchFiles(files, mode) { addLog(`批量处理模式 - 找到 ${files.length} 个文件`, 'info'); addLog(`使用 ${PERFORMANCE_CONFIG.batchSize} 并发处理(CPU核心: ${PERFORMANCE_CONFIG.cpuCores})`, 'info'); + const handler = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile; let successCount = 0; let failCount = 0; - - // 动态并行处理,根据CPU核心数自动调整 const batchSize = PERFORMANCE_CONFIG.batchSize; + for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, Math.min(i + batchSize, files.length)); - const batchStart = i + 1; - const batchEnd = Math.min(i + batchSize, files.length); - - // 显示进度 - addLog(`处理进度: ${batchStart}-${batchEnd}/${files.length}`, 'info'); + addLog(`处理进度: ${i + 1}-${Math.min(i + batchSize, files.length)}/${files.length}`, 'info'); await Promise.all(batch.map(async (file) => { try { - if (mode === 'compress') { - await handleCompressWithFile(file); - } else { - await handleDecompressWithFile(file); - } + await handler(file); successCount++; - } catch (error) { + } catch { addLog(`处理失败: ${file}`, 'error'); failCount++; } })); - // 让出主线程,避免阻塞 UI await new Promise(resolve => setTimeout(resolve, 0)); } addLog(`批量处理完成! 成功: ${successCount} 个,失败: ${failCount} 个`, 'success', true); } -// 判断路径是否为文件夹并扫描 async function checkAndScanPath(path) { - // 尝试扫描文件夹 const files = await scanFolder(path, includeSubfoldersCheckbox.checked); - + if (files.length > 0) { - // 是文件夹,返回扫描到的文件 addLog(`扫描文件夹: ${path} (找到 ${files.length} 个文件)`, 'info'); return files; + } + + const extension = path.toLowerCase(); + if (extension.endsWith('.exe') || extension.endsWith('.dll')) { + return [path]; + } + + return []; +} + +async function setupDragAndDrop() { + await listen('tauri://drag-drop', handleDragDrop); + setupVisualFeedback(compressBtn); + setupVisualFeedback(decompressBtn); +} + +async function handleDragDrop(event) { + const { paths, position } = event.payload; + + if (!paths?.length) return; + + const allFiles = await collectFiles(paths); + + if (allFiles.length === 0) { + addLog('未找到 .exe 或 .dll 文件', 'warning'); + return; + } + + await processDropByTarget(allFiles, position); +} + +async function collectFiles(paths) { + const allFiles = []; + for (const path of paths) { + const files = await checkAndScanPath(path); + allFiles.push(...files); + } + return allFiles; +} + +async function processDropByTarget(files, position) { + const dropTarget = getDropTarget(position); + + if (dropTarget === 'compress') { + addLog('检测到拖放至加壳区域', 'info'); + await processBatchFiles(files, 'compress'); + } else if (dropTarget === 'decompress') { + addLog('检测到拖放至脱壳区域', 'info'); + await processBatchFiles(files, 'decompress'); } else { - // 不是文件夹或没有找到文件,检查是否是exe/dll文件 - if (path.toLowerCase().endsWith('.exe') || path.toLowerCase().endsWith('.dll')) { - return [path]; - } - return []; + storeFilesForLater(files); } } -// 设置文件拖放功能 -async function setupDragAndDrop() { - // 监听文件拖放释放事件 - await listen('tauri://drag-drop', async (event) => { - const paths = event.payload.paths; - const position = event.payload.position; - - if (paths && paths.length > 0) { - // 收集所有文件 - const allFiles = []; - - for (const path of paths) { - const files = await checkAndScanPath(path); - allFiles.push(...files); - } - - if (allFiles.length === 0) { - addLog('未找到 .exe 或 .dll 文件', 'warning'); - return; - } - - // 判断拖放位置,自动触发对应操作 - const dropTarget = getDropTarget(position); - - if (dropTarget === 'compress') { - addLog('检测到拖放至加壳区域', 'info'); - await processBatchFiles(allFiles, 'compress'); - } else if (dropTarget === 'decompress') { - addLog('检测到拖放至脱壳区域', 'info'); - await processBatchFiles(allFiles, 'decompress'); - } else { - // 拖放到其他位置,存储文件等待用户选择 - if (allFiles.length === 1) { - addLog('请点击"加壳压缩"或"脱壳解压"按钮', 'info'); - window.droppedFile = allFiles[0]; - } else { - addLog(`已选择 ${allFiles.length} 个文件,请点击操作按钮`, 'info'); - window.droppedFiles = allFiles; - } - } - } - }); - - // 为按钮添加视觉反馈 - setupVisualFeedback(compressBtn); - setupVisualFeedback(decompressBtn); +function storeFilesForLater(files) { + if (files.length === 1) { + addLog('请点击"加壳压缩"或"脱壳解压"按钮', 'info'); + window.droppedFile = files[0]; + } else { + addLog(`已选择 ${files.length} 个文件,请点击操作按钮`, 'info'); + window.droppedFiles = files; + } } // 缓存按钮位置信息 @@ -401,32 +354,27 @@ function updateButtonRectsCache() { }; } -// 判断拖放位置对应的目标 +function isPointInRect(x, y, rect) { + return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; +} + function getDropTarget(position) { if (!position) return null; - - // 如果缓存不存在,创建缓存 + if (!cachedButtonRects) { updateButtonRectsCache(); } - - const x = position.x; - const y = position.y; - - // 检查是否在加壳压缩按钮范围内 - const compressRect = cachedButtonRects.compress; - if (x >= compressRect.left && x <= compressRect.right && - y >= compressRect.top && y <= compressRect.bottom) { + + const { x, y } = position; + + if (isPointInRect(x, y, cachedButtonRects.compress)) { return 'compress'; } - - // 检查是否在脱壳解压按钮范围内 - const decompressRect = cachedButtonRects.decompress; - if (x >= decompressRect.left && x <= decompressRect.right && - y >= decompressRect.top && y <= decompressRect.bottom) { + + if (isPointInRect(x, y, cachedButtonRects.decompress)) { return 'decompress'; } - + return null; } @@ -448,16 +396,19 @@ function setupVisualFeedback(element) { element.addEventListener('drop', handleDragLeave, { passive: true }); } -// 处理加壳压缩 async function handleCompress() { + await handleFileSelect('compress'); +} + +async function handleDecompress() { + await handleFileSelect('decompress'); +} + +async function handleFileSelect(mode) { try { - // 选择文件(支持多选) const selected = await open({ multiple: true, - filters: [{ - name: '可执行文件', - extensions: ['exe', 'dll'] - }] + filters: [{ name: '可执行文件', extensions: ['exe', 'dll'] }] }); if (!selected || (Array.isArray(selected) && selected.length === 0)) { @@ -465,44 +416,35 @@ async function handleCompress() { return; } - // 处理选择结果(可能是单个路径或路径数组) const files = Array.isArray(selected) ? selected : [selected]; - - // 批量处理或单文件处理 + if (files.length === 1) { addLog(`选择文件: ${files[0]}`, 'info'); - await handleCompressWithFile(files[0]); + const handler = mode === 'compress' ? handleCompressWithFile : handleDecompressWithFile; + await handler(files[0]); } else { addLog(`选择了 ${files.length} 个文件`, 'info'); - await processBatchFiles(files, 'compress'); + await processBatchFiles(files, mode); } } catch (error) { addLog(`操作失败: ${error}`, 'error'); } } -// 使用指定文件处理加壳压缩 async function handleCompressWithFile(inputFile) { try { - let outputFile; - - // 根据覆盖选项决定输出文件 + if (overwriteCheckbox.checked) { outputFile = inputFile; addLog('将覆盖原文件', 'info'); } else { - // 自动生成输出文件名 const ext = inputFile.substring(inputFile.lastIndexOf('.')); const baseName = inputFile.substring(0, inputFile.lastIndexOf('.')); const defaultOutput = `${baseName}_packed${ext}`; - // 选择输出位置 outputFile = await save({ - filters: [{ - name: '可执行文件', - extensions: ['exe', 'dll'] - }], + filters: [{ name: '可执行文件', extensions: ['exe', 'dll'] }], defaultPath: defaultOutput }); @@ -514,67 +456,25 @@ async function handleCompressWithFile(inputFile) { addLog(`输出文件: ${outputFile}`, 'info'); } - // 执行压缩 await processUpx('compress', inputFile, outputFile); - } catch (error) { addLog(`操作失败: ${error}`, 'error'); } } -// 处理脱壳解压 -async function handleDecompress() { - try { - // 选择文件(支持多选) - const selected = await open({ - multiple: true, - filters: [{ - name: '可执行文件', - extensions: ['exe', 'dll'] - }] - }); - - if (!selected || (Array.isArray(selected) && selected.length === 0)) { - addLog('未选择文件', 'warning'); - return; - } - - // 处理选择结果(可能是单个路径或路径数组) - const files = Array.isArray(selected) ? selected : [selected]; - - // 批量处理或单文件处理 - if (files.length === 1) { - addLog(`选择文件: ${files[0]}`, 'info'); - await handleDecompressWithFile(files[0]); - } else { - addLog(`选择了 ${files.length} 个文件`, 'info'); - await processBatchFiles(files, 'decompress'); - } - } catch (error) { - addLog(`操作失败: ${error}`, 'error'); - } -} - -// 使用指定文件处理脱壳解压 async function handleDecompressWithFile(inputFile) { try { - // 脱壳默认覆盖原文件(直接恢复原状) - const outputFile = inputFile; addLog('将覆盖原文件', 'info'); - - // 执行解压 - await processUpx('decompress', inputFile, outputFile); - + await processUpx('decompress', inputFile, inputFile); } catch (error) { addLog(`操作失败: ${error}`, 'error'); } } -// 执行 UPX 处理 async function processUpx(mode, inputFile, outputFile) { try { const options = { - mode: mode, + mode, input_file: inputFile, output_file: outputFile, compression_level: getCompressionLevel(), @@ -591,52 +491,60 @@ async function processUpx(mode, inputFile, outputFile) { addLog('已启用强制压缩模式', 'warning'); } - addLog(`开始${mode === 'compress' ? '加壳压缩' : '脱壳解压'}...`, 'info'); + const actionName = mode === 'compress' ? '加壳压缩' : '脱壳解压'; + addLog(`开始${actionName}...`, 'info'); const result = await invoke('process_upx', { options }); - - // 分行显示结果信息 - const lines = result.split('\n'); - lines.forEach((line) => { - if (line.trim()) { - // 根据内容判断日志类型 - if (line.includes('操作成功') || line.includes('操作完成')) { - addLog(line, 'success', true); - } else if (line.includes('输出:') || line.includes('大小:') || line.includes('压缩率:')) { - addLog(line, 'success'); - } else if (line.includes('UPX 输出:')) { - addLog(line, 'info'); - } else if (line.includes('扫描') || line.includes('检测')) { - addLog(line, 'warning'); - } else { - addLog(line, 'info'); - } - } - }); + parseProcessResult(result); } catch (error) { - // 格式化错误信息,保留换行和特殊字符 - const errorMsg = String(error); - - // 分行显示错误信息,保持格式 - const lines = errorMsg.split('\n'); - lines.forEach((line, index) => { - if (line.trim()) { - // 错误标题使用 error,解决方案等使用 warning - if (line.includes('[错误]')) { - addLog(line, 'error'); - } else if (line.includes('解决方案:') || line.includes('可能原因:')) { - addLog(line, 'warning'); - } else if (line.trim().startsWith('-')) { - addLog(line, 'info'); - } else { - addLog(line, index === 0 ? 'error' : 'warning'); - } - } - }); + parseProcessError(String(error)); } } +function parseProcessResult(result) { + const LOG_PATTERNS = [ + { patterns: ['操作成功', '操作完成'], type: 'success', highlight: true }, + { patterns: ['输出:', '大小:', '压缩率:'], type: 'success', highlight: false }, + { patterns: ['UPX 输出:'], type: 'info', highlight: false }, + { patterns: ['扫描', '检测'], type: 'warning', highlight: false } + ]; + + result.split('\n').forEach((line) => { + if (!line.trim()) return; + + const match = LOG_PATTERNS.find(({ patterns }) => + patterns.some(p => line.includes(p)) + ); + + if (match) { + addLog(line, match.type, match.highlight); + } else { + addLog(line, 'info'); + } + }); +} + +function parseProcessError(errorMsg) { + const ERROR_PATTERNS = [ + { test: (s) => s.includes('[错误]'), type: 'error' }, + { test: (s) => s.includes('解决方案:') || s.includes('可能原因:'), type: 'warning' }, + { test: (s) => s.trim().startsWith('-'), type: 'info' } + ]; + + errorMsg.split('\n').forEach((line, index) => { + if (!line.trim()) return; + + const match = ERROR_PATTERNS.find(({ test }) => test(line)); + + if (match) { + addLog(line, match.type); + } else { + addLog(line, index === 0 ? 'error' : 'warning'); + } + }); +} + // 刷新图标缓存 async function handleRefreshIcon() { try { @@ -696,7 +604,6 @@ function addLog(message, type = 'info', highlight = false) { }); } -// 保存当前配置 async function saveCurrentConfig() { try { const config = { @@ -714,22 +621,20 @@ async function saveCurrentConfig() { } } -// 加载保存的配置 +function applyConfigToUI(config) { + compressionLevel.value = config.compression_level; + overwriteCheckbox.checked = config.overwrite; + backupCheckbox.checked = config.backup; + ultraBruteCheckbox.checked = config.ultra_brute; + includeSubfoldersCheckbox.checked = config.include_subfolders; + forceCompressCheckbox.checked = config.force_compress; + updateLevelDisplay(config.compression_level); +} + async function loadSavedConfig() { try { const config = await invoke('load_config'); - - // 应用配置到界面 - compressionLevel.value = config.compression_level; - overwriteCheckbox.checked = config.overwrite; - backupCheckbox.checked = config.backup; - ultraBruteCheckbox.checked = config.ultra_brute; - includeSubfoldersCheckbox.checked = config.include_subfolders; - forceCompressCheckbox.checked = config.force_compress; - - // 更新压缩级别显示(复用现有函数) - updateLevelDisplay(config.compression_level); - + applyConfigToUI(config); addLog('已加载上次保存的配置', 'info'); } catch (error) { console.error('加载配置失败:', error);