From c2fcf2797b571bc33fc276c2e2122fb01853bc4f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sat, 20 Jan 2024 22:53:40 -0700 Subject: [PATCH] Export function to decomp.me scratch (beta) --- Cargo.lock | 11 +++ Cargo.toml | 4 +- src/app.rs | 8 ++- src/app_config.rs | 1 + src/config.rs | 20 +++++- src/jobs/create_scratch.rs | 135 +++++++++++++++++++++++++++++++++++++ src/jobs/mod.rs | 8 ++- src/jobs/objdiff.rs | 47 +++++++++---- src/views/config.rs | 3 + src/views/data_diff.rs | 2 +- src/views/function_diff.rs | 20 +++++- src/views/symbol_diff.rs | 48 +++++++++++-- 12 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 src/jobs/create_scratch.rs diff --git a/Cargo.lock b/Cargo.lock index 71c21ee..47067bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2537,6 +2537,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mime_guess2" version = "2.0.5" @@ -3403,6 +3413,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index e7cf41c..d2253b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,12 +64,12 @@ twox-hash = "1.6.3" # For Linux static binaries, use rustls [target.'cfg(target_os = "linux")'.dependencies] -reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "rustls"] } +reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "rustls"] } self_update = { version = "0.39.0", default-features = false, features = ["rustls"] } # For all other platforms, use native TLS [target.'cfg(not(target_os = "linux"))'.dependencies] -reqwest = "0.11.23" +reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "json", "multipart", "default-tls"] } self_update = "0.39.0" [target.'cfg(windows)'.dependencies] diff --git a/src/app.rs b/src/app.rs index 9c33651..194dd23 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,7 +16,7 @@ use time::UtcOffset; use crate::{ app_config::{deserialize_config, AppConfigVersion}, - config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode}, + config::{build_globset, load_project_config, ProjectObject, ProjectObjectNode, ScratchConfig}, diff::DiffAlg, jobs::{ objdiff::{start_build, ObjDiffConfig}, @@ -60,6 +60,7 @@ pub struct ObjectConfig { pub base_path: Option, pub reverse_fn_order: Option, pub complete: Option, + pub scratch: Option, } #[derive(Clone, Eq, PartialEq)] @@ -128,6 +129,8 @@ pub struct AppConfig { #[serde(skip)] pub queue_reload: bool, #[serde(skip)] + pub queue_scratch: bool, + #[serde(skip)] pub project_config_info: Option, } @@ -157,6 +160,7 @@ impl Default for AppConfig { obj_change: false, queue_build: false, queue_reload: false, + queue_scratch: false, project_config_info: None, } } @@ -314,7 +318,7 @@ impl App { let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; config_state.post_update(ctx, jobs, &self.config); - diff_state.post_update(&self.config); + diff_state.post_update(ctx, jobs, &self.config); let Ok(mut config) = self.config.write() else { return; diff --git a/src/app_config.rs b/src/app_config.rs index e7df92b..86ddf87 100644 --- a/src/app_config.rs +++ b/src/app_config.rs @@ -60,6 +60,7 @@ impl ObjectConfigV0 { base_path: Some(self.base_path), reverse_fn_order: self.reverse_fn_order, complete: None, + scratch: None, } } } diff --git a/src/config.rs b/src/config.rs index 9a514a3..47d33d2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,6 +50,22 @@ pub struct ProjectObject { pub reverse_fn_order: Option, #[serde(default)] pub complete: Option, + #[serde(default)] + pub scratch: Option, +} + +#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +pub struct ScratchConfig { + #[serde(default)] + pub platform: Option, + #[serde(default)] + pub compiler: Option, + #[serde(default)] + pub c_flags: Option, + #[serde(default)] + pub ctx_path: Option, + #[serde(default)] + pub build_ctx: bool, } impl ProjectObject { @@ -66,7 +82,7 @@ impl ProjectObject { #[derive(Clone)] pub enum ProjectObjectNode { - File(String, ProjectObject), + File(String, Box), Dir(String, Vec), } @@ -114,7 +130,7 @@ fn build_nodes( } } } - let mut object = object.clone(); + let mut object = Box::new(object.clone()); if let (Some(target_obj_dir), Some(path), None) = (target_obj_dir, &object.path, &object.target_path) { diff --git a/src/jobs/create_scratch.rs b/src/jobs/create_scratch.rs new file mode 100644 index 0000000..50d721f --- /dev/null +++ b/src/jobs/create_scratch.rs @@ -0,0 +1,135 @@ +use std::{fs, path::PathBuf, sync::mpsc::Receiver}; + +use anyhow::{anyhow, bail, Context, Result}; +use const_format::formatcp; + +use crate::{ + app::AppConfig, + jobs::{ + objdiff::{run_make, BuildConfig, BuildStatus}, + start_job, update_status, Job, JobContext, JobResult, JobState, + }, +}; + +#[derive(Debug, Clone)] +pub struct CreateScratchConfig { + pub build_config: BuildConfig, + pub context_path: Option, + pub build_context: bool, + + // Scratch fields + pub compiler: String, + pub platform: String, + pub compiler_flags: String, + pub function_name: String, + pub target_obj: PathBuf, +} + +impl CreateScratchConfig { + pub(crate) fn from_config(config: &AppConfig, function_name: String) -> Result { + let Some(selected_obj) = &config.selected_obj else { + bail!("No object selected"); + }; + let Some(target_path) = &selected_obj.target_path else { + bail!("No target path for {}", selected_obj.name); + }; + let Some(scratch_config) = &selected_obj.scratch else { + bail!("No scratch configuration for {}", selected_obj.name); + }; + Ok(Self { + build_config: BuildConfig::from_config(config), + context_path: scratch_config.ctx_path.clone(), + build_context: scratch_config.build_ctx, + compiler: scratch_config.compiler.clone().unwrap_or_default(), + platform: scratch_config.platform.clone().unwrap_or_default(), + compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), + function_name, + target_obj: target_path.to_path_buf(), + }) + } + + pub fn is_available(config: &AppConfig) -> bool { + let Some(selected_obj) = &config.selected_obj else { + return false; + }; + selected_obj.target_path.is_some() && selected_obj.scratch.is_some() + } +} + +#[derive(Default, Debug, Clone)] +pub struct CreateScratchResult { + pub scratch_url: String, +} + +#[derive(Debug, Default, Clone, serde::Deserialize)] +struct CreateScratchResponse { + pub slug: String, + pub claim_token: String, +} + +const API_HOST: &str = "http://127.0.0.1:8000"; +const WEB_HOST: &str = "http://localhost:8080"; + +fn run_create_scratch( + status: &JobContext, + cancel: Receiver<()>, + config: CreateScratchConfig, +) -> Result> { + let project_dir = + config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?; + + let mut context = None; + if let Some(context_path) = &config.context_path { + if config.build_context { + update_status(status, "Building context".to_string(), 0, 2, &cancel)?; + match run_make(&config.build_config, context_path) { + BuildStatus { success: true, .. } => {} + BuildStatus { success: false, stdout, stderr, .. } => { + bail!("Failed to build context:\n{stdout}\n{stderr}") + } + } + } + let context_path = project_dir.join(context_path); + context = Some( + fs::read_to_string(&context_path) + .map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?, + ); + } + + update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?; + let diff_flags = [format!("--disassemble={}", config.function_name)]; + let diff_flags = serde_json::to_string(&diff_flags).unwrap(); + let obj_path = project_dir.join(&config.target_obj); + let file = reqwest::blocking::multipart::Part::file(&obj_path) + .with_context(|| format!("Failed to open {}", obj_path.display()))?; + let form = reqwest::blocking::multipart::Form::new() + .text("compiler", config.compiler.clone()) + .text("platform", config.platform.clone()) + .text("compiler_flags", config.compiler_flags.clone()) + .text("diff_label", config.function_name.clone()) + .text("diff_flags", diff_flags) + .text("context", context.unwrap_or_default()) + .text("source_code", "// Move related code from Context tab to here") + .part("target_obj", file); + let client = reqwest::blocking::Client::new(); + let response = client + .post(formatcp!("{API_HOST}/api/scratch")) + .multipart(form) + .send() + .map_err(|e| anyhow!("Failed to send request: {}", e))?; + if !response.status().is_success() { + return Err(anyhow!("Failed to create scratch: {}", response.text()?)); + } + let body: CreateScratchResponse = response.json().context("Failed to parse response")?; + let scratch_url = format!("{WEB_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token); + + update_status(status, "Complete".to_string(), 2, 2, &cancel)?; + Ok(Box::from(CreateScratchResult { scratch_url })) +} + +pub fn start_create_scratch(ctx: &egui::Context, config: CreateScratchConfig) -> JobState { + start_job(ctx, "Create scratch", Job::CreateScratch, move |context, cancel| { + run_create_scratch(&context, cancel, config) + .map(|result| JobResult::CreateScratch(Some(result))) + }) +} diff --git a/src/jobs/mod.rs b/src/jobs/mod.rs index 34ba02e..f339eff 100644 --- a/src/jobs/mod.rs +++ b/src/jobs/mod.rs @@ -9,9 +9,13 @@ use std::{ use anyhow::Result; -use crate::jobs::{check_update::CheckUpdateResult, objdiff::ObjDiffResult, update::UpdateResult}; +use crate::jobs::{ + check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult, + update::UpdateResult, +}; pub mod check_update; +pub mod create_scratch; pub mod objdiff; pub mod update; @@ -20,6 +24,7 @@ pub enum Job { ObjDiff, CheckUpdate, Update, + CreateScratch, } pub static JOB_ID: AtomicUsize = AtomicUsize::new(0); @@ -119,6 +124,7 @@ pub enum JobResult { ObjDiff(Option>), CheckUpdate(Option>), Update(Box), + CreateScratch(Option>), } fn should_cancel(rx: &Receiver<()>) -> bool { diff --git a/src/jobs/objdiff.rs b/src/jobs/objdiff.rs index c19d48d..e6d45fb 100644 --- a/src/jobs/objdiff.rs +++ b/src/jobs/objdiff.rs @@ -33,13 +33,28 @@ impl Default for BuildStatus { } } +#[derive(Debug, Clone)] +pub struct BuildConfig { + pub project_dir: Option, + pub custom_make: Option, + pub selected_wsl_distro: Option, +} + +impl BuildConfig { + pub(crate) fn from_config(config: &AppConfig) -> Self { + Self { + project_dir: config.project_dir.clone(), + custom_make: config.custom_make.clone(), + selected_wsl_distro: config.selected_wsl_distro.clone(), + } + } +} + pub struct ObjDiffConfig { + pub build_config: BuildConfig, pub build_base: bool, pub build_target: bool, - pub custom_make: Option, - pub project_dir: Option, pub selected_obj: Option, - pub selected_wsl_distro: Option, pub code_alg: DiffAlg, pub data_alg: DiffAlg, pub relax_reloc_diffs: bool, @@ -47,13 +62,11 @@ pub struct ObjDiffConfig { impl ObjDiffConfig { pub(crate) fn from_config(config: &AppConfig) -> Self { - ObjDiffConfig { + Self { + build_config: BuildConfig::from_config(config), build_base: config.build_base, build_target: config.build_target, - custom_make: config.custom_make.clone(), - project_dir: config.project_dir.clone(), selected_obj: config.selected_obj.clone(), - selected_wsl_distro: config.selected_wsl_distro.clone(), code_alg: config.code_alg, data_alg: config.data_alg, relax_reloc_diffs: config.relax_reloc_diffs, @@ -69,7 +82,14 @@ pub struct ObjDiffResult { pub time: OffsetDateTime, } -fn run_make(cwd: &Path, arg: &Path, config: &ObjDiffConfig) -> BuildStatus { +pub(crate) fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus { + let Some(cwd) = &config.project_dir else { + return BuildStatus { + success: false, + stderr: "Missing project dir".to_string(), + ..Default::default() + }; + }; match (|| -> Result { let make = config.custom_make.as_deref().unwrap_or("make"); #[cfg(not(windows))] @@ -130,8 +150,11 @@ fn run_build( config: ObjDiffConfig, ) -> Result> { let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; - let project_dir = - config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?; + let project_dir = config + .build_config + .project_dir + .as_ref() + .ok_or_else(|| Error::msg("Missing project dir"))?; let target_path_rel = if let Some(target_path) = &obj_config.target_path { Some(target_path.strip_prefix(project_dir).map_err(|_| { anyhow!( @@ -171,7 +194,7 @@ fn run_build( total, &cancel, )?; - run_make(project_dir, target_path_rel, &config) + run_make(&config.build_config, target_path_rel) } _ => BuildStatus::default(), }; @@ -185,7 +208,7 @@ fn run_build( total, &cancel, )?; - run_make(project_dir, base_path_rel, &config) + run_make(&config.build_config, base_path_rel) } _ => BuildStatus::default(), }; diff --git a/src/views/config.rs b/src/views/config.rs index e698163..63f1271 100644 --- a/src/views/config.rs +++ b/src/views/config.rs @@ -93,6 +93,7 @@ impl ConfigViewState { base_path: Some(path), reverse_fn_order: None, complete: None, + scratch: None, }); } else if let Ok(obj_path) = path.strip_prefix(target_dir) { let base_path = base_dir.join(obj_path); @@ -102,6 +103,7 @@ impl ConfigViewState { base_path: Some(base_path), reverse_fn_order: None, complete: None, + scratch: None, }); } } @@ -393,6 +395,7 @@ fn display_object( base_path: object.base_path.clone(), reverse_fn_order: object.reverse_fn_order, complete: object.complete, + scratch: object.scratch.clone(), }); } } diff --git a/src/views/data_diff.rs b/src/views/data_diff.rs index 8f89679..5f45d52 100644 --- a/src/views/data_diff.rs +++ b/src/views/data_diff.rs @@ -192,7 +192,7 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A |ui| { ui.set_width(column_width); - if ui.button("Back").clicked() { + if ui.button("⏴ Back").clicked() { state.current_view = View::SymbolDiff; } diff --git a/src/views/function_diff.rs b/src/views/function_diff.rs index 0f5ac00..99fe983 100644 --- a/src/views/function_diff.rs +++ b/src/views/function_diff.rs @@ -586,9 +586,23 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance |ui| { ui.set_width(column_width); - if ui.button("Back").clicked() { - state.current_view = View::SymbolDiff; - } + ui.horizontal(|ui| { + if ui.button("⏴ Back").clicked() { + state.current_view = View::SymbolDiff; + } + ui.separator(); + if ui + .add_enabled( + !state.scratch_running && state.scratch_available, + egui::Button::new("📲 decomp.me"), + ) + .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") + .on_disabled_hover_text("Scratch configuration missing") + .clicked() + { + state.queue_scratch = true; + } + }); let demangled = demangle(&selected_symbol.symbol_name, &Default::default()); let name = demangled.as_deref().unwrap_or(&selected_symbol.symbol_name); diff --git a/src/views/symbol_diff.rs b/src/views/symbol_diff.rs index 451898e..02c837f 100644 --- a/src/views/symbol_diff.rs +++ b/src/views/symbol_diff.rs @@ -1,14 +1,15 @@ use std::mem::take; use egui::{ - text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, ScrollArea, SelectableLabel, - TextEdit, Ui, Vec2, Widget, + text::LayoutJob, Align, CollapsingHeader, Color32, Id, Layout, OpenUrl, ScrollArea, + SelectableLabel, TextEdit, Ui, Vec2, Widget, }; use egui_extras::{Size, StripBuilder}; use crate::{ app::AppConfigRef, jobs::{ + create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult}, objdiff::{BuildStatus, ObjDiffResult}, Job, JobQueue, JobResult, }, @@ -33,12 +34,16 @@ pub enum View { #[derive(Default)] pub struct DiffViewState { pub build: Option>, + pub scratch: Option>, pub current_view: View, pub symbol_state: SymbolViewState, pub function_state: FunctionViewState, pub search: String, pub queue_build: bool, pub build_running: bool, + pub scratch_available: bool, + pub queue_scratch: bool, + pub scratch_running: bool, } #[derive(Default)] @@ -52,15 +57,19 @@ pub struct SymbolViewState { impl DiffViewState { pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) { - jobs.results.retain_mut(|result| { - if let JobResult::ObjDiff(result) = result { + jobs.results.retain_mut(|result| match result { + JobResult::ObjDiff(result) => { self.build = take(result); false - } else { - true } + JobResult::CreateScratch(result) => { + self.scratch = take(result); + false + } + _ => true, }); self.build_running = jobs.is_running(Job::ObjDiff); + self.scratch_running = jobs.is_running(Job::CreateScratch); self.symbol_state.disable_reverse_fn_order = false; if let Ok(config) = config.read() { @@ -70,16 +79,41 @@ impl DiffViewState { self.symbol_state.disable_reverse_fn_order = true; } } + self.scratch_available = CreateScratchConfig::is_available(&config); } } - pub fn post_update(&mut self, config: &AppConfigRef) { + pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) { + if let Some(result) = take(&mut self.scratch) { + ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url))); + } + if self.queue_build { self.queue_build = false; if let Ok(mut config) = config.write() { config.queue_build = true; } } + + if self.queue_scratch { + self.queue_scratch = false; + if let Some(function_name) = + self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone()) + { + if let Ok(config) = config.read() { + match CreateScratchConfig::from_config(&config, function_name) { + Ok(config) => { + jobs.push_once(Job::CreateScratch, || { + start_create_scratch(ctx, config) + }); + } + Err(err) => { + log::error!("Failed to create scratch config: {err}"); + } + } + } + } + } } }