gbf_stats/
main.rs

1#![feature(backtrace_frames)]
2
3use std::{
4    collections::HashMap,
5    env,
6    fmt::Write,
7    fs::{self},
8    path, process,
9    sync::Mutex,
10};
11
12use consts::{ExitCode, GBF_SUITE_INPUT_DIR_ENV_VAR};
13use dotenv::dotenv;
14use gbf_core::{
15    decompiler::{
16        ast::visitors::emit_context::EmitContextBuilder,
17        function_decompiler::FunctionDecompilerBuilder,
18    },
19    module::ModuleBuilder,
20};
21
22use rayon::prelude::*;
23use std::sync::LazyLock;
24
25pub mod consts;
26
27static STATS: LazyLock<Mutex<DecompStats>> = LazyLock::new(|| Mutex::new(DecompStats::new()));
28
29struct DecompStats {
30    total_scripts: usize,
31    total_functions: usize,
32    successful_functions: usize,
33    error_counts: HashMap<String, usize>,
34}
35
36impl DecompStats {
37    fn new() -> Self {
38        Self {
39            total_scripts: 0,
40            total_functions: 0,
41            successful_functions: 0,
42            error_counts: HashMap::new(),
43        }
44    }
45
46    fn add_script(&mut self, function_count: usize) {
47        self.total_scripts += 1;
48        self.total_functions += function_count;
49    }
50
51    fn add_success(&mut self) {
52        self.successful_functions += 1;
53    }
54
55    fn add_error(&mut self, error: String) {
56        *self.error_counts.entry(error).or_insert(0) += 1;
57    }
58
59    fn log_stats(&mut self) {
60        let coverage = if self.total_functions > 0 {
61            (self.successful_functions as f32 / self.total_functions as f32) * 100.0
62        } else {
63            0.0
64        };
65
66        // Get top 5 errors
67        let mut errors: Vec<_> = self.error_counts.iter().collect();
68        errors.sort_by(|a, b| b.1.cmp(a.1));
69        let top_errors: Vec<_> = errors.iter().take(10).collect();
70
71        log::info!(
72            "Decompilation Statistics:\n\
73            Total Scripts: {}\n\
74            Total Functions: {}\n\
75            Total Coverage: {:.1}%\n\
76            Top 10 most common errors:{}",
77            self.total_scripts,
78            self.total_functions,
79            coverage,
80            if top_errors.is_empty() {
81                "\nNone".to_string()
82            } else {
83                top_errors.iter().enumerate().fold(
84                    String::new(),
85                    |mut output, (i, (err, count))| {
86                        let _ = write!(output, "\n{}. {} ({})", i + 1, err, count);
87                        output
88                    },
89                )
90            }
91        );
92    }
93}
94
95fn main() {
96    // Load .env file if it exists
97    dotenv().ok();
98
99    let config_path = path::Path::new(env!("CARGO_MANIFEST_DIR")).join("logging_config.yaml");
100    log4rs::init_file(config_path, Default::default()).unwrap();
101
102    // Attempt to unwrap the environment variable. If it fails, exit with an error code.
103    let value = env::var(GBF_SUITE_INPUT_DIR_ENV_VAR).unwrap_or_else(|_| {
104        log::error!(
105            "Environment variable {} not found",
106            GBF_SUITE_INPUT_DIR_ENV_VAR
107        );
108        process::exit(ExitCode::EnvVarNotFound.into());
109    });
110
111    // Attempt to open the directory. If it fails, exit with an error code.
112    let dir = std::fs::read_dir(value).unwrap_or_else(|_| {
113        log::error!("Invalid directory");
114        process::exit(ExitCode::InvalidDir.into());
115    });
116
117    // Process each file in the directory (each file is considered a module).
118    dir.par_bridge().for_each(|entry| {
119        let entry = match entry {
120            Ok(entry) => entry,
121            Err(err) => {
122                log::error!("Failed to get the directory entry: {}", err);
123                return;
124            }
125        };
126
127        let result = process_module(entry.path().as_ref());
128        if let Err(e) = result {
129            log::error!("Failed to process module: {}", e);
130            std::process::exit(ExitCode::UnexpectedError.into());
131        }
132    });
133
134    // After processing all modules, log the final statistics.
135    STATS.lock().unwrap().log_stats();
136}
137
138/// Processes a single module (file), building it and then decompiling each function.
139/// Decompilation is performed in parallel for each function.
140fn process_module(path: &path::Path) -> Result<(), Box<dyn std::error::Error>> {
141    let module_name = path
142        .file_name()
143        .ok_or("Failed to get file name")?
144        .to_str()
145        .ok_or("Failed to convert file name to string")?
146        .to_string();
147
148    let reader = fs::File::open(path)?;
149
150    let module = match ModuleBuilder::new()
151        .name(module_name.clone())
152        .reader(Box::new(reader))
153        .build()
154    {
155        Ok(module) => module,
156        Err(e) => {
157            log::error!("Failed to build module {}: {:?}", module_name, e);
158            return Ok(()); // Skip this module rather than returning an error
159        }
160    };
161
162    // We add the script to our stats (one script is the entire module).
163    // Also note how many functions are in this module.
164    STATS.lock().unwrap().add_script(module.len());
165
166    // Decompile each function in parallel using rayon
167    module.par_iter().for_each(|func| {
168        let func_name = func.id.name.clone().unwrap_or_else(|| "entry".to_string());
169        let func_insts_count = func.iter().map(|bb| bb.len()).sum::<usize>();
170        log::info!(
171            "Decompiling function {} in module {} ({} instructions)",
172            func_name,
173            module_name,
174            func_insts_count
175        );
176
177        let mut decompiler = FunctionDecompilerBuilder::new(func)
178            .structure_analysis_max_iterations(1000)
179            .structure_debug_mode(false)
180            .build();
181
182        let res = decompiler.decompile(
183            EmitContextBuilder::default()
184                .include_ssa_versions(true)
185                .build(),
186        );
187
188        match res {
189            Ok(_) => {
190                STATS.lock().unwrap().add_success();
191                log::info!("Decompiled function {}", func_name);
192            }
193            Err(e) => {
194                STATS.lock().unwrap().add_error(e.to_string());
195                log::error!(
196                    "Error decompiling function {} in module {} ({} instructions): {}",
197                    func_name,
198                    module_name,
199                    func_insts_count,
200                    e
201                );
202            }
203        }
204    });
205
206    Ok(())
207}