Skip to main content

js_deobfuscator/engine/
pipeline.rs

1//! Two-phase convergence loop.
2//!
3//! Phase 1 (Common): Run modules in order, repeat until 0 modifications.
4//! Phase 2 (Locked): Run once after common stabilizes. If changes, restart Phase 1.
5//! Outer loop: Repeat until locked makes 0 changes, or max iterations reached.
6
7use std::time::{Duration, Instant};
8
9use oxc::allocator::Allocator;
10use oxc::ast::ast::Program;
11use oxc::semantic::{SemanticBuilder, Scoping};
12
13use tracing::{debug, info};
14
15use super::error::Result;
16use super::module::Module;
17
18/// Statistics from an engine run.
19#[derive(Debug)]
20pub struct EngineResult {
21    /// Number of convergence iterations executed.
22    pub iterations: usize,
23    /// Total modifications across all iterations.
24    pub total_modifications: usize,
25    /// Whether the engine reached a fixed point (0 changes).
26    pub converged: bool,
27    /// Wall-clock time for the entire run.
28    pub elapsed: Duration,
29}
30
31/// The convergence engine.
32pub struct Engine {
33    /// Common modules — converge to 0 changes before locked modules run.
34    common: Vec<Box<dyn Module>>,
35    /// Locked modules — run once after common stabilizes.
36    locked: Vec<Box<dyn Module>>,
37    max_iterations: usize,
38}
39
40impl Engine {
41    pub fn new(
42        common: Vec<Box<dyn Module>>,
43        locked: Vec<Box<dyn Module>>,
44        max_iterations: usize,
45    ) -> Self {
46        Self { common, locked, max_iterations }
47    }
48
49    /// Run the two-phase convergence loop.
50    pub fn run<'a>(
51        &mut self,
52        allocator: &'a Allocator,
53        program: &mut Program<'a>,
54    ) -> Result<EngineResult> {
55        let start = Instant::now();
56        let mut scoping = build_scoping(program);
57        let mut total_modifications = 0;
58        let mut iterations = 0;
59        let mut converged = false;
60
61        for _outer in 0..self.max_iterations {
62            // Phase 1: Common convergence
63            for _inner in 0..self.max_iterations {
64                let mut iteration_mods = 0;
65
66                for module in &mut self.common {
67                    let result = module.transform(allocator, program, scoping)?;
68                    iteration_mods += result.modifications;
69                    scoping = result.scoping;
70
71                    if module.changes_symbols() && result.modifications > 0 {
72                        scoping = build_scoping(program);
73                    }
74                }
75
76                total_modifications += iteration_mods;
77                iterations += 1;
78
79                debug!(iteration = iterations, mods = iteration_mods, "common iteration");
80
81                if iteration_mods == 0 {
82                    break;
83                }
84            }
85
86            // Phase 2: Locked modules (run once)
87            let mut locked_mods = 0;
88            for module in &mut self.locked {
89                let result = module.transform(allocator, program, scoping)?;
90                locked_mods += result.modifications;
91                scoping = result.scoping;
92
93                if module.changes_symbols() && result.modifications > 0 {
94                    scoping = build_scoping(program);
95                }
96            }
97
98            total_modifications += locked_mods;
99            if locked_mods > 0 {
100                iterations += 1;
101                debug!(locked_mods, "locked modules changed, restarting common");
102            }
103
104            if locked_mods == 0 {
105                converged = true;
106                break;
107            }
108        }
109
110        info!(
111            iterations,
112            total_modifications,
113            converged,
114            elapsed_ms = start.elapsed().as_millis(),
115            "engine run complete"
116        );
117
118        Ok(EngineResult {
119            iterations,
120            total_modifications,
121            converged,
122            elapsed: start.elapsed(),
123        })
124    }
125}
126
127fn build_scoping(program: &Program) -> Scoping {
128    SemanticBuilder::new().build(program).semantic.into_scoping()
129}