gbf_driver/
main.rs

1#![feature(backtrace_frames)]
2
3use clap::{Parser, ValueEnum};
4use gbf_core::{
5    cfg_dot::{CfgDotConfig, DotRenderableGraph},
6    decompiler::{
7        ast::visitors::emit_context::{
8            EmitContext, EmitContextBuilder, EmitVerbosity, IndentStyle,
9        },
10        function_decompiler::FunctionDecompilerBuilder,
11    },
12    function::Function,
13    module::{Module, ModuleBuilder},
14};
15use log::{error, info};
16use rayon::prelude::*;
17use std::{
18    fs,
19    io::Read,
20    path::{Path, PathBuf},
21};
22
23/// GBF decompilation driver
24#[derive(Parser, Debug)]
25#[clap(
26    name = "gbf_driver",
27    version,
28    about = "Driver for decompiling GBF modules and functions"
29)]
30struct Cli {
31    /// Input file or directory containing GBF bytecode
32    #[clap(value_parser)]
33    input: PathBuf,
34
35    /// Optional output directory for decompiled output (if not provided, prints to stdout)
36    #[clap(long, value_parser)]
37    output: Option<PathBuf>,
38
39    /// Decompilation mode: decompile an entire module or one or all functions.
40    #[clap(short, long, value_enum, default_value_t = Mode::Module)]
41    mode: Mode,
42
43    /// If mode is 'function', the name of the function to decompile.
44    /// If omitted, decompiles all functions concurrently.
45    #[clap(short, long)]
46    function: Option<String>,
47
48    /// Minify the decompiled output (instead of pretty printing)
49    #[clap(long)]
50    minified: bool,
51
52    /// Do not include SSA versions (by default SSA versions are included)
53    #[clap(long)]
54    no_ssa: bool,
55
56    /// Indent style: Allman or KAndR (default: Allman)
57    #[clap(long, value_enum, default_value_t = Indent::Allman)]
58    indent: Indent,
59
60    /// Do not format numbers as hexadecimal (by default hex formatting is enabled)
61    #[clap(long)]
62    no_hex: bool,
63
64    /// Output Graphviz CFG dot files for each function in the module
65    #[clap(long)]
66    graphviz: bool,
67
68    /// Directory where Graphviz dot files will be written (if not provided, prints to stdout)
69    #[clap(long, value_parser)]
70    graphviz_output: Option<PathBuf>,
71}
72
73/// Decompilation mode: either an entire module or function(s).
74#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
75enum Mode {
76    Module,
77    Function,
78}
79
80/// Indent style for decompiled output.
81#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
82enum Indent {
83    Allman,
84    KAndR,
85}
86
87fn main() {
88    // Initialize log4rs using the logging_config.yaml file in the crate root.
89    let config_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("logging_config.yaml");
90    if let Err(e) = log4rs::init_file(config_path, Default::default()) {
91        eprintln!("Failed to initialize logging: {}", e);
92        std::process::exit(1);
93    }
94
95    let args = Cli::parse();
96
97    // Build the EmitContext based on command-line options.
98    let emit_context: EmitContext = EmitContextBuilder::default()
99        .verbosity(if args.minified {
100            EmitVerbosity::Minified
101        } else {
102            EmitVerbosity::Pretty
103        })
104        .include_ssa_versions(!args.no_ssa)
105        .indent_style(match args.indent {
106            Indent::Allman => IndentStyle::Allman,
107            Indent::KAndR => IndentStyle::KAndR,
108        })
109        .format_number_hex(!args.no_hex)
110        .build();
111
112    // If the input is a directory, process each file in parallel.
113    if args.input.is_dir() {
114        fs::read_dir(&args.input)
115            .unwrap_or_else(|e| {
116                error!("Failed to read directory {}: {}", args.input.display(), e);
117                std::process::exit(1);
118            })
119            .par_bridge()
120            .for_each(|entry| {
121                let entry = match entry {
122                    Ok(ent) => ent,
123                    Err(e) => {
124                        error!("Failed to get directory entry: {}", e);
125                        return;
126                    }
127                };
128
129                let path = entry.path();
130                if path.is_file() {
131                    process_file(&path, &args, &emit_context);
132                }
133            });
134    } else if args.input.is_file() {
135        process_file(&args.input, &args, &emit_context);
136    } else {
137        error!(
138            "Input path {} is not a valid file or directory",
139            args.input.display()
140        );
141        std::process::exit(1);
142    }
143}
144
145/// Process a single file (module) using the given options and context.
146fn process_file(path: &Path, args: &Cli, context: &EmitContext) {
147    info!("Processing file: {}", path.display());
148
149    let file = match fs::File::open(path) {
150        Ok(f) => f,
151        Err(e) => {
152            error!("Failed to open file {}: {}", path.display(), e);
153            return;
154        }
155    };
156
157    let module = match ModuleBuilder::new()
158        .name(
159            path.file_name()
160                .map(|s| s.to_string_lossy().into_owned())
161                .unwrap_or_else(|| "unknown_module".to_string()),
162        )
163        .reader(Box::new(file) as Box<dyn Read>)
164        .build()
165    {
166        Ok(m) => m,
167        Err(e) => {
168            error!("Failed to build module from {}: {:?}", path.display(), e);
169            return;
170        }
171    };
172
173    match args.mode {
174        Mode::Module => {
175            // Decompile the entire module.
176            match module.decompile(*context) {
177                Ok(decompiled) => {
178                    info!("--- Decompiled module from {} ---", path.display());
179                    if let Some(ref output_dir) = args.output {
180                        let base = path
181                            .file_stem()
182                            .map(|s| s.to_string_lossy())
183                            .unwrap_or_else(|| "module".into());
184                        let output_path = output_dir.join(format!("{}.gs2", base));
185                        if let Err(e) = fs::write(&output_path, &decompiled) {
186                            error!(
187                                "Failed to write decompiled module to {}: {}",
188                                output_path.display(),
189                                e
190                            );
191                        } else {
192                            info!("Decompiled module written to {}", output_path.display());
193                        }
194                    } else {
195                        println!("{}", decompiled);
196                    }
197                    if args.graphviz {
198                        output_module_graphviz(&module, path, args);
199                    }
200                }
201                Err(e) => {
202                    error!(
203                        "Module decompilation failed for {}: {:?}",
204                        path.display(),
205                        e
206                    );
207                }
208            }
209        }
210        Mode::Function => {
211            if let Some(func_name) = &args.function {
212                // Decompile a single, specified function.
213                let func = match module.get_function_by_name(func_name).ok() {
214                    Some(f) => f,
215                    None => {
216                        error!(
217                            "Function '{}' not found in module {}",
218                            func_name,
219                            path.display()
220                        );
221                        return;
222                    }
223                };
224                decompile_and_output_function(func, &module, path, args, context);
225            } else {
226                // Decompile all functions concurrently.
227                let funcs: Vec<_> = module.iter().collect();
228                funcs.par_iter().for_each(|func| {
229                    let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
230                    let func_insts_count = func.iter().map(|bb| bb.len()).sum::<usize>();
231                    info!(
232                        "Decompiling function {} in module {} ({} instructions)",
233                        func_name,
234                        module.name.clone().unwrap_or_else(|| "unknown".to_string()),
235                        func_insts_count
236                    );
237                    // Create a function-specific EmitContext (customize as needed).
238                    let func_context = EmitContextBuilder::default()
239                        .include_ssa_versions(true)
240                        .build();
241                    let mut decompiler = FunctionDecompilerBuilder::new(func)
242                        .structure_analysis_max_iterations(1000)
243                        .structure_debug_mode(false)
244                        .build();
245                    let res = decompiler.decompile(func_context);
246                    match res {
247                        Ok(result) => {
248                            info!("Decompiled function {}", func_name);
249                            if let Some(ref output_dir) = args.output {
250                                let output_path = output_dir.join(format!(
251                                    "{}_{}.gs2",
252                                    module.name.clone().unwrap_or_else(|| "unknown".to_string()),
253                                    func_name
254                                ));
255                                if let Err(e) = fs::write(&output_path, &result) {
256                                    error!(
257                                        "Failed to write decompiled function to {}: {}",
258                                        output_path.display(),
259                                        e
260                                    );
261                                } else {
262                                    info!(
263                                        "Decompiled function written to {}",
264                                        output_path.display()
265                                    );
266                                }
267                            } else {
268                                println!("--- Decompiled function '{}' ---\n{}", func_name, result);
269                            }
270                        }
271                        Err(e) => {
272                            error!(
273                                "Error decompiling function {} in module {} ({} instructions): {}",
274                                func_name,
275                                module.name.clone().unwrap_or_else(|| "unknown".to_string()),
276                                func_insts_count,
277                                e
278                            );
279                        }
280                    }
281                    if args.graphviz {
282                        output_function_graphviz(func, path, args);
283                    }
284                });
285            }
286        }
287    }
288}
289
290/// Helper to decompile a single function and output its result.
291fn decompile_and_output_function(
292    func: &Function,
293    module: &Module,
294    path: &Path,
295    args: &Cli,
296    context: &EmitContext,
297) {
298    let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
299    let mut decompiler = FunctionDecompilerBuilder::new(func).build();
300    match decompiler.decompile(*context) {
301        Ok(result) => {
302            info!(
303                "--- Decompiled function '{}' from {} ---",
304                func_name,
305                path.display()
306            );
307            if let Some(ref output_dir) = args.output {
308                let output_path = output_dir.join(format!(
309                    "{}_{}.gs2",
310                    module.name.clone().unwrap_or_else(|| "unknown".to_string()),
311                    func_name
312                ));
313                if let Err(e) = fs::write(&output_path, &result) {
314                    error!(
315                        "Failed to write decompiled function to {}: {}",
316                        output_path.display(),
317                        e
318                    );
319                } else {
320                    info!("Decompiled function written to {}", output_path.display());
321                }
322            } else {
323                println!("{}", result);
324            }
325        }
326        Err(e) => {
327            error!(
328                "Function decompilation failed for {} in {}: {:?}",
329                func_name,
330                path.display(),
331                e
332            );
333        }
334    }
335}
336
337/// Output Graphviz CFG dot files for every function in the module.
338fn output_module_graphviz(module: &Module, input_path: &Path, args: &Cli) {
339    if let Some(ref output_dir) = args.graphviz_output {
340        // Write each function's dot file to the specified directory.
341        for func in module.iter() {
342            let dot = func.render_dot(CfgDotConfig::default());
343            let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
344            let filename = format!(
345                "{}_{}_cfg.dot",
346                module.name.clone().unwrap_or_else(|| "unknown".to_string()),
347                func_name
348            );
349            let dot_path = output_dir.join(filename);
350            if let Err(e) = fs::write(&dot_path, &dot) {
351                error!(
352                    "Failed to write Graphviz CFG to {}: {}",
353                    dot_path.display(),
354                    e
355                );
356            } else {
357                info!("Graphviz CFG written to {}", dot_path.display());
358            }
359        }
360    } else {
361        // Print dot files to stdout if no output directory is specified.
362        for func in module.iter() {
363            let dot = func.render_dot(CfgDotConfig::default());
364            let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
365            println!(
366                "\n--- Graphviz CFG for function '{}' in module '{}' ---\n{}\n",
367                func_name,
368                input_path.display(),
369                dot
370            );
371        }
372    }
373}
374
375/// Output a Graphviz CFG dot file for a single function.
376fn output_function_graphviz(func: &Function, input_path: &Path, args: &Cli) {
377    if let Some(ref output_dir) = args.graphviz_output {
378        let dot = func.render_dot(CfgDotConfig::default());
379        let base = input_path
380            .file_stem()
381            .map(|s| s.to_string_lossy())
382            .unwrap_or_else(|| "module".into());
383        let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
384        let filename = format!("{}_{}_cfg.dot", base, func_name);
385        let dot_path = output_dir.join(filename);
386        if let Err(e) = fs::write(&dot_path, &dot) {
387            error!(
388                "Failed to write Graphviz CFG to {}: {}",
389                dot_path.display(),
390                e
391            );
392        } else {
393            info!("Graphviz CFG written to {}", dot_path.display());
394        }
395    } else {
396        let dot = func.render_dot(CfgDotConfig::default());
397        let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
398        println!(
399            "\n--- Graphviz CFG for function '{}' in module '{}' ---\n{}\n",
400            func_name,
401            input_path.display(),
402            dot
403        );
404    }
405}