Skip to main content

objdiff_core/jobs/
create_scratch.rs

1use std::{fs, sync::mpsc::Receiver, task::Waker};
2
3use anyhow::{Context, Result, anyhow, bail};
4use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
5
6use crate::{
7    build::{BuildConfig, BuildStatus, run_make},
8    diff::Demangler,
9    jobs::{Job, JobContext, JobResult, JobState, start_job, update_status},
10};
11
12#[derive(Debug, Clone)]
13pub struct CreateScratchConfig {
14    pub build_config: BuildConfig,
15    pub context_path: Option<Utf8UnixPathBuf>,
16    pub build_context: bool,
17
18    // Scratch fields
19    pub compiler: String,
20    pub platform: String,
21    pub compiler_flags: String,
22    pub function_name: String,
23    pub target_obj: Utf8PlatformPathBuf,
24    pub preset_id: Option<u32>,
25    pub demangler: Demangler,
26}
27
28#[derive(Default, Debug, Clone)]
29pub struct CreateScratchResult {
30    pub scratch_url: String,
31}
32
33#[derive(Debug, Default, Clone, serde::Deserialize)]
34struct CreateScratchResponse {
35    pub slug: String,
36    pub claim_token: String,
37}
38
39const API_HOST: &str = "https://decomp.me";
40
41fn run_create_scratch(
42    status: &JobContext,
43    cancel: Receiver<()>,
44    config: CreateScratchConfig,
45) -> Result<Box<CreateScratchResult>> {
46    let project_dir =
47        config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?;
48
49    let mut context = None;
50    if let Some(context_path) = &config.context_path {
51        if config.build_context {
52            update_status(status, "Building context".to_string(), 0, 2, &cancel)?;
53            match run_make(&config.build_config, context_path.as_ref()) {
54                BuildStatus { success: true, .. } => {}
55                BuildStatus { success: false, stdout, stderr, .. } => {
56                    bail!("Failed to build context:\n{stdout}\n{stderr}")
57                }
58            }
59        }
60        let context_path = project_dir.join(context_path.with_platform_encoding());
61        context = Some(
62            fs::read_to_string(&context_path)
63                .map_err(|e| anyhow!("Failed to read {}: {}", context_path, e))?,
64        );
65    }
66
67    update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
68    let diff_flags = [format!("--disassemble={}", config.function_name)];
69    let diff_flags = serde_json::to_string(&diff_flags)?;
70    let file = reqwest::blocking::multipart::Part::file(&config.target_obj)
71        .with_context(|| format!("Failed to open {}", config.target_obj))?;
72    let mut form = reqwest::blocking::multipart::Form::new()
73        .text("compiler", config.compiler.clone())
74        .text("platform", config.platform.clone())
75        .text("compiler_flags", config.compiler_flags.clone())
76        .text("diff_label", config.function_name.clone())
77        .text("diff_flags", diff_flags)
78        .text("context", context.unwrap_or_default())
79        .text("source_code", "// Move related code from Context tab to here")
80        .text(
81            "name",
82            config
83                .demangler
84                .demangle(&config.function_name)
85                .unwrap_or_else(|| config.function_name.clone()),
86        );
87    if let Some(preset) = config.preset_id {
88        form = form.text("preset", preset.to_string());
89    }
90    form = form.part("target_obj", file);
91    let client = reqwest::blocking::Client::new();
92    let response = client
93        .post(format!("{API_HOST}/api/scratch"))
94        .multipart(form)
95        .send()
96        .map_err(|e| anyhow!("Failed to send request: {}", e))?;
97    if !response.status().is_success() {
98        return Err(anyhow!("Failed to create scratch: {}", response.text()?));
99    }
100    let body: CreateScratchResponse = response.json().context("Failed to parse response")?;
101    let scratch_url = format!("{API_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token);
102
103    update_status(status, "Complete".to_string(), 2, 2, &cancel)?;
104    Ok(Box::from(CreateScratchResult { scratch_url }))
105}
106
107pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState {
108    start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| {
109        run_create_scratch(&context, cancel, config)
110            .map(|result| JobResult::CreateScratch(Some(result)))
111    })
112}