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 { 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 { 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())) }