js_deobfuscator/core/
engine.rs

1//! Deobfuscation engine with convergence loop.
2
3use std::time::{Duration, Instant};
4
5use oxc::allocator::Allocator;
6use oxc::ast::ast::Program;
7use oxc::semantic::SemanticBuilder;
8use oxc_traverse::traverse_mut;
9
10use super::config::EngineConfig;
11use super::error::{DeobError, PassError};
12use crate::ecma::EcmaTransformer;
13use crate::extensions::string_rotator::StringRotator;
14use crate::runtime::RuntimeTransformer;
15
16// ============================================================================
17// EngineResult
18// ============================================================================
19
20/// Result of running the engine.
21#[derive(Debug)]
22pub struct EngineResult {
23    /// Number of iterations executed.
24    pub iterations: usize,
25
26    /// Total modifications across all iterations.
27    pub total_modifications: usize,
28
29    /// Modifications per iteration.
30    pub modifications_per_iteration: Vec<usize>,
31
32    /// Whether convergence was reached.
33    pub converged: bool,
34
35    /// Time elapsed.
36    pub elapsed: Duration,
37
38    /// Errors encountered (non-fatal).
39    pub errors: Vec<PassError>,
40}
41
42impl EngineResult {
43    /// Check if successful (converged).
44    pub fn is_success(&self) -> bool {
45        self.converged
46    }
47}
48
49// ============================================================================
50// Engine
51// ============================================================================
52
53/// The deobfuscation engine.
54///
55/// Runs layers in a convergence loop until no modifications.
56///
57/// # Layers
58///
59/// 1. **Extensions** (string rotator, etc.) - runs first to decode strings
60/// 2. **ECMA** (operators, string methods, etc.) - constant folding
61/// 3. **Runtime** (atob, btoa, escape, unescape) - runtime API evaluation
62///
63/// # Example
64///
65/// ```ignore
66/// use js_deobfuscator::{Engine, EngineConfig};
67/// use oxc::allocator::Allocator;
68/// use oxc::codegen::Codegen;
69/// use oxc::parser::Parser;
70/// use oxc::span::SourceType;
71///
72/// let allocator = Allocator::default();
73/// let mut program = Parser::new(&allocator, source, SourceType::mjs()).parse().program;
74///
75/// let engine = Engine::with_config(EngineConfig::full());
76/// let result = engine.run(&allocator, &mut program)?;
77///
78/// let output = Codegen::new().build(&program).code;
79/// ```
80#[derive(Default)]
81pub struct Engine {
82    config: EngineConfig,
83}
84
85impl Engine {
86    /// Create engine with default configuration.
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Create engine with custom configuration.
92    pub fn with_config(config: EngineConfig) -> Self {
93        Self { config }
94    }
95
96    /// Get the configuration.
97    pub fn config(&self) -> &EngineConfig {
98        &self.config
99    }
100
101    /// Run deobfuscation on program.
102    #[tracing::instrument(
103        skip(self, allocator, program),
104        fields(max_iterations = self.config.max_iterations)
105    )]
106    pub fn run<'a>(
107        &self,
108        allocator: &'a Allocator,
109        program: &mut Program<'a>,
110    ) -> Result<EngineResult, DeobError> {
111        let start = Instant::now();
112        let mut total_modifications = 0;
113        let mut modifications_per_iteration = Vec::new();
114        let mut converged = false;
115        let errors = Vec::new();
116
117        tracing::info!(target: "deob::engine", "starting deobfuscation");
118
119        for iteration in 0..self.config.max_iterations {
120            let iter_span = tracing::info_span!(target: "deob::engine", "iteration", n = iteration);
121            let _guard = iter_span.enter();
122
123            let mut iter_mods = 0;
124
125            // 1. Run Extensions layer (string rotator, etc.) - FIRST to decode strings
126            if self.config.layers.extensions && self.config.layers.extensions_config.string_array {
127                match StringRotator::transform(allocator, program) {
128                    Ok(result) => {
129                        let ext_mods = result.strings_decoded + result.functions_removed;
130                        tracing::debug!(
131                            target: "deob::engine",
132                            systems = result.systems_detected,
133                            decoded = result.strings_decoded,
134                            inlined = result.calls_inlined,
135                            removed = result.functions_removed,
136                            "Extensions layer complete"
137                        );
138                        iter_mods += ext_mods;
139                    }
140                    Err(e) => {
141                        tracing::warn!(target: "deob::engine", error = %e, "Extensions layer error");
142                    }
143                }
144            }
145
146            // 2. Rebuild scoping (fresh symbol/reference info after extensions)
147            tracing::debug!(target: "deob::engine", "building scoping");
148            let semantic = SemanticBuilder::new().build(program).semantic;
149            let scoping = semantic.into_scoping();
150
151            // 3. Run ECMA layer
152            if self.config.layers.ecma {
153                let mut ecma = EcmaTransformer::new(&self.config.layers.ecma_config);
154                traverse_mut(&mut ecma, allocator, program, scoping, ());
155                let ecma_mods = ecma.modifications();
156                tracing::debug!(target: "deob::engine", ecma_mods, "ECMA layer complete");
157                iter_mods += ecma_mods;
158            }
159
160            // 4. Rebuild scoping for Runtime layer
161            let semantic = SemanticBuilder::new().build(program).semantic;
162            let scoping = semantic.into_scoping();
163
164            // 5. Run Runtime layer
165            if self.config.layers.runtime {
166                let mut runtime = RuntimeTransformer::new(&self.config.layers.runtime_config);
167                traverse_mut(&mut runtime, allocator, program, scoping, ());
168                let runtime_mods = runtime.modifications();
169                tracing::debug!(target: "deob::engine", runtime_mods, "Runtime layer complete");
170                iter_mods += runtime_mods;
171            }
172
173            tracing::info!(
174                target: "deob::engine",
175                modifications = iter_mods,
176                "iteration complete"
177            );
178
179            total_modifications += iter_mods;
180            modifications_per_iteration.push(iter_mods);
181
182            // 6. Check convergence
183            if iter_mods == 0 {
184                tracing::info!(target: "deob::engine", iterations = iteration + 1, "converged");
185                converged = true;
186                break;
187            }
188        }
189
190        if !converged {
191            tracing::warn!(
192                target: "deob::engine",
193                max = self.config.max_iterations,
194                "max iterations reached without convergence"
195            );
196        }
197
198        let elapsed = start.elapsed();
199        tracing::info!(
200            target: "deob::engine",
201            total_modifications,
202            iterations = modifications_per_iteration.len(),
203            elapsed_ms = elapsed.as_millis(),
204            "deobfuscation complete"
205        );
206
207        Ok(EngineResult {
208            iterations: modifications_per_iteration.len(),
209            total_modifications,
210            modifications_per_iteration,
211            converged,
212            elapsed,
213            errors,
214        })
215    }
216}