chore: 代码优化.
This commit is contained in:
1 parent
38939c9b80
commit
d585477def
4 files changed
+595
-597
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user