chore: 代码优化.

This commit is contained in:
ASLant committed 2026-01-13 12:42:09 +08:00
1 parent 38939c9b80
commit d585477def
4 files changed
+595 -597

No files matched your search

+4
View File
@@ -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
+72
View File
@@ -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)`
+317 -300
View File
@@ -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<PathBuf> {
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<PathBuf> {
// 打包后的位置
if let Ok(exe_path) = std::env::current_exe() {
@@ -77,252 +81,140 @@ fn get_upx_path() -> Option<PathBuf> {
// 开发环境
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<String, String> {
// 获取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<Item = String> + '_ {
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<Item = String> + '_ {
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<String, String> {
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<String, String> {
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<String> {
let mut files = Vec::new();
@@ -348,13 +353,11 @@ fn scan_folder_recursive(folder_path: &Path, include_subfolders: bool) -> Vec<St
let path = entry.path();
if path.is_dir() && include_subfolders {
// 递归扫描子文件夹
let sub_files = scan_folder_recursive(&path, include_subfolders);
files.extend(sub_files);
files.extend(scan_folder_recursive(&path, true));
} else if path.is_file() {
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
if ext == "exe" || ext == "dll" {
if let Some(ext) = path.extension() {
let ext_lower = ext.to_string_lossy().to_lowercase();
if SUPPORTED_EXTENSIONS.contains(&ext_lower.as_str()) {
if let Some(path_str) = path.to_str() {
files.push(path_str.to_string());
}
@@ -382,82 +385,93 @@ fn scan_folder(options: ScanFolderOptions) -> Result<Vec<String>, String> {
Ok(scan_folder_recursive(path, options.include_subfolders))
}
// 获取UPX版本
// ============================================================================
// UPX 版本查询
// ============================================================================
#[tauri::command]
fn get_upx_version() -> Result<String, String> {
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<AppConfig, String> {
let config_path = get_config_path().ok_or("无法获取配置文件路径")?;
@@ -480,6 +493,10 @@ fn load_config() -> Result<AppConfig, String> {
serde_json::from_str(&json).map_err(|e| format!("解析配置文件失败: {}", e))
}
// ============================================================================
// Tauri 应用入口
// ============================================================================
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
+202 -297
View File
@@ -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);