243 lines
8.5 KiB
Rust
243 lines
8.5 KiB
Rust
use std::env;
|
||
use std::fs;
|
||
use std::io::Write;
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use base64::engine::general_purpose::STANDARD;
|
||
use base64::Engine;
|
||
use eframe::egui;
|
||
|
||
const MAGIC: &[u8] = b"LSWRAP1";
|
||
|
||
fn main() -> eframe::Result<()> {
|
||
let options = eframe::NativeOptions::default();
|
||
eframe::run_native(
|
||
"授权打包器",
|
||
options,
|
||
Box::new(|_| Box::new(LauncherGui::new())),
|
||
)
|
||
}
|
||
|
||
struct LauncherGui {
|
||
api_base: String,
|
||
integration_code: String,
|
||
input_path: String,
|
||
output_path: String,
|
||
extract_to_self: bool,
|
||
status: String,
|
||
}
|
||
|
||
impl LauncherGui {
|
||
fn new() -> Self {
|
||
Self {
|
||
api_base: String::new(),
|
||
integration_code: String::new(),
|
||
input_path: String::new(),
|
||
output_path: String::new(),
|
||
extract_to_self: false,
|
||
status: String::new(),
|
||
}
|
||
}
|
||
|
||
fn pack(&mut self) -> Result<(), String> {
|
||
let api_base = normalize_api_base(&self.api_base)?;
|
||
let (project_id, project_key) = decode_integration_code(&self.integration_code)?;
|
||
|
||
let input = PathBuf::from(self.input_path.trim());
|
||
if !input.exists() {
|
||
return Err("未选择可执行文件".to_string());
|
||
}
|
||
|
||
let output = if self.output_path.trim().is_empty() {
|
||
derive_output_path(&input)?
|
||
} else {
|
||
PathBuf::from(self.output_path.trim())
|
||
};
|
||
|
||
if input == output {
|
||
return Err("输出文件不能与输入文件相同".to_string());
|
||
}
|
||
|
||
let exe_dir = env::current_exe()
|
||
.map_err(|e| format!("无法定位程序目录: {e}"))?
|
||
.parent()
|
||
.unwrap_or(Path::new("."))
|
||
.to_path_buf();
|
||
let stub_path = exe_dir.join("launcher_stub.exe");
|
||
if !stub_path.exists() {
|
||
return Err("未找到 launcher_stub.exe,请放在同目录".to_string());
|
||
}
|
||
|
||
let payload_name = input
|
||
.file_name()
|
||
.and_then(|name| name.to_str())
|
||
.unwrap_or("payload.exe")
|
||
.to_string();
|
||
|
||
let extract_to = if self.extract_to_self { "self" } else { "temp" };
|
||
|
||
let config = serde_json::json!({
|
||
"api_base": api_base,
|
||
"project_id": project_id,
|
||
"project_secret": project_key,
|
||
"license_file": "license.key",
|
||
"request_timeout_sec": 10,
|
||
"heartbeat_retries": 2,
|
||
"heartbeat_retry_delay_sec": 5,
|
||
"extract_to": extract_to,
|
||
"keep_payload": false,
|
||
"payload_name": payload_name
|
||
});
|
||
let config_bytes = serde_json::to_vec(&config)
|
||
.map_err(|e| format!("配置序列化失败: {e}"))?;
|
||
|
||
pack_files(&stub_path, &input, &output, &config_bytes)?;
|
||
self.output_path = output.to_string_lossy().to_string();
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl eframe::App for LauncherGui {
|
||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||
handle_drop(ctx, self);
|
||
|
||
egui::CentralPanel::default().show(ctx, |ui| {
|
||
ui.heading("授权打包器");
|
||
ui.add_space(8.0);
|
||
|
||
ui.label("授权系统地址");
|
||
ui.add(egui::TextEdit::singleline(&mut self.api_base).hint_text("http://118.145.218.2:39256"));
|
||
|
||
ui.add_space(6.0);
|
||
ui.label("项目对接码");
|
||
ui.add(egui::TextEdit::singleline(&mut self.integration_code).hint_text("LSC1.xxxxx"));
|
||
|
||
ui.add_space(6.0);
|
||
ui.label("选择要打包的 EXE");
|
||
ui.horizontal(|ui| {
|
||
ui.add(egui::TextEdit::singleline(&mut self.input_path).desired_width(360.0));
|
||
if ui.button("浏览").clicked() {
|
||
if let Some(file) = rfd::FileDialog::new().add_filter("EXE", &["exe"]).pick_file() {
|
||
self.input_path = file.to_string_lossy().to_string();
|
||
if self.output_path.trim().is_empty() {
|
||
if let Ok(path) = derive_output_path(&file) {
|
||
self.output_path = path.to_string_lossy().to_string();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
ui.add_space(6.0);
|
||
ui.label("输出文件");
|
||
ui.horizontal(|ui| {
|
||
ui.add(egui::TextEdit::singleline(&mut self.output_path).desired_width(360.0));
|
||
if ui.button("另存为").clicked() {
|
||
if let Some(file) = rfd::FileDialog::new().add_filter("EXE", &["exe"]).set_file_name("packed.exe").save_file() {
|
||
self.output_path = file.to_string_lossy().to_string();
|
||
}
|
||
}
|
||
});
|
||
|
||
ui.add_space(6.0);
|
||
ui.checkbox(&mut self.extract_to_self, "解压到程序目录(需要本地资源时勾选)");
|
||
|
||
ui.add_space(10.0);
|
||
if ui.button("开始打包").clicked() {
|
||
match self.pack() {
|
||
Ok(_) => self.status = "打包完成".to_string(),
|
||
Err(err) => self.status = format!("打包失败: {err}"),
|
||
}
|
||
}
|
||
|
||
if !self.status.is_empty() {
|
||
ui.add_space(8.0);
|
||
ui.label(&self.status);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
fn handle_drop(ctx: &egui::Context, app: &mut LauncherGui) {
|
||
let dropped = ctx.input(|i| i.raw.dropped_files.clone());
|
||
for file in dropped {
|
||
if let Some(path) = file.path {
|
||
if path.extension().and_then(|s| s.to_str()).map(|s| s.eq_ignore_ascii_case("exe")).unwrap_or(false) {
|
||
app.input_path = path.to_string_lossy().to_string();
|
||
if app.output_path.trim().is_empty() {
|
||
if let Ok(out) = derive_output_path(&path) {
|
||
app.output_path = out.to_string_lossy().to_string();
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn pack_files(stub_path: &Path, payload_path: &Path, output_path: &Path, config: &[u8]) -> Result<(), String> {
|
||
let stub = fs::read(stub_path).map_err(|e| format!("读取 stub 失败: {e}"))?;
|
||
let payload = fs::read(payload_path).map_err(|e| format!("读取 EXE 失败: {e}"))?;
|
||
|
||
let mut out = fs::File::create(output_path).map_err(|e| format!("创建输出失败: {e}"))?;
|
||
out.write_all(&stub).map_err(|e| format!("写入 stub 失败: {e}"))?;
|
||
out.write_all(&payload).map_err(|e| format!("写入 EXE 失败: {e}"))?;
|
||
out.write_all(config).map_err(|e| format!("写入配置失败: {e}"))?;
|
||
out.write_all(MAGIC).map_err(|e| format!("写入标识失败: {e}"))?;
|
||
out.write_all(&(payload.len() as u64).to_le_bytes())
|
||
.map_err(|e| format!("写入长度失败: {e}"))?;
|
||
out.write_all(&(config.len() as u64).to_le_bytes())
|
||
.map_err(|e| format!("写入长度失败: {e}"))?;
|
||
out.flush().map_err(|e| format!("写入失败: {e}"))?;
|
||
Ok(())
|
||
}
|
||
|
||
fn normalize_api_base(value: &str) -> Result<String, String> {
|
||
let trimmed = value.trim().trim_end_matches('/');
|
||
if trimmed.is_empty() {
|
||
return Err("请输入授权系统地址".to_string());
|
||
}
|
||
Ok(trimmed.to_string())
|
||
}
|
||
|
||
fn derive_output_path(input: &Path) -> Result<PathBuf, String> {
|
||
let parent = input.parent().unwrap_or(Path::new("."));
|
||
let stem = input
|
||
.file_stem()
|
||
.and_then(|s| s.to_str())
|
||
.unwrap_or("app");
|
||
let ext = input
|
||
.extension()
|
||
.and_then(|s| s.to_str())
|
||
.unwrap_or("exe");
|
||
let name = format!("{stem}_packed.{ext}");
|
||
Ok(parent.join(name))
|
||
}
|
||
|
||
fn decode_integration_code(code: &str) -> Result<(String, String), String> {
|
||
let trimmed = code.trim();
|
||
if trimmed.is_empty() {
|
||
return Err("请输入对接码".to_string());
|
||
}
|
||
|
||
if trimmed.contains('|') {
|
||
return split_code(trimmed);
|
||
}
|
||
|
||
let raw = trimmed.strip_prefix("LSC1.").unwrap_or(trimmed);
|
||
let decoded = STANDARD.decode(raw).map_err(|_| "对接码格式错误".to_string())?;
|
||
let decoded_str = String::from_utf8(decoded).map_err(|_| "对接码解析失败".to_string())?;
|
||
split_code(&decoded_str)
|
||
}
|
||
|
||
fn split_code(value: &str) -> Result<(String, String), String> {
|
||
let mut parts = value.split('|');
|
||
let project_id = parts.next().unwrap_or("").trim();
|
||
let project_key = parts.next().unwrap_or("").trim();
|
||
if project_id.is_empty() || project_key.is_empty() {
|
||
return Err("对接码内容不完整".to_string());
|
||
}
|
||
Ok((project_id.to_string(), project_key.to_string()))
|
||
}
|