js_deobfuscator/core/
deobfuscator.rs

1//! High-level deobfuscation API.
2//!
3//! # Example
4//!
5//! ```ignore
6//! use js_deobfuscator::{JSDeobfuscator, Extension};
7//!
8//! let output = JSDeobfuscator::new()
9//!     .ecma(true)
10//!     .runtime(true)
11//!     .extension(Extension::StringRotator, true)
12//!     .deobfuscate(source)?;
13//! ```
14
15use oxc::allocator::Allocator;
16use oxc::codegen::Codegen;
17use oxc::parser::Parser;
18use oxc::span::SourceType;
19
20use super::config::EngineConfig;
21use super::engine::Engine;
22use super::error::DeobError;
23
24// ============================================================================
25// Extension enum
26// ============================================================================
27
28/// Available extensions for obfuscator-specific patterns.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum Extension {
31    /// String array/rotator decoder (javascript-obfuscator style).
32    StringRotator,
33    /// Control flow deflattener.
34    ControlFlow,
35    /// Proxy function inliner.
36    Proxy,
37}
38
39// ============================================================================
40// DeobfuscateResult
41// ============================================================================
42
43/// Result of deobfuscation.
44#[derive(Debug)]
45pub struct DeobfuscateResult {
46    /// Deobfuscated source code.
47    pub code: String,
48    /// Number of iterations executed.
49    pub iterations: usize,
50    /// Total modifications made.
51    pub modifications: usize,
52    /// Whether convergence was reached.
53    pub converged: bool,
54}
55
56// ============================================================================
57// JSDeobfuscator
58// ============================================================================
59
60/// High-level JavaScript deobfuscator.
61///
62/// # Example
63///
64/// ```ignore
65/// use js_deobfuscator::{JSDeobfuscator, Extension};
66///
67/// // Standard (ECMA + Runtime)
68/// let output = JSDeobfuscator::new()
69///     .deobfuscate(source)?;
70///
71/// // Full with string rotator
72/// let output = JSDeobfuscator::new()
73///     .ecma(true)
74///     .runtime(true)
75///     .extension(Extension::StringRotator, true)
76///     .deobfuscate(source)?;
77///
78/// // ECMA only
79/// let output = JSDeobfuscator::new()
80///     .ecma(true)
81///     .runtime(false)
82///     .deobfuscate(source)?;
83/// ```
84#[derive(Debug, Clone)]
85pub struct JSDeobfuscator {
86    config: EngineConfig,
87}
88
89impl Default for JSDeobfuscator {
90    fn default() -> Self {
91        Self::new()
92    }
93}
94
95impl JSDeobfuscator {
96    /// Create deobfuscator with standard config (ECMA + Runtime enabled).
97    pub fn new() -> Self {
98        Self {
99            config: EngineConfig::standard(),
100        }
101    }
102
103    /// Set max iterations.
104    #[must_use]
105    pub fn max_iterations(mut self, max: usize) -> Self {
106        self.config.max_iterations = max;
107        self
108    }
109
110    /// Enable/disable ECMA layer (constant folding, dead code, etc.).
111    #[must_use]
112    pub fn ecma(mut self, enabled: bool) -> Self {
113        self.config.layers.ecma = enabled;
114        self
115    }
116
117    /// Enable/disable Runtime layer (atob, btoa, escape, unescape).
118    #[must_use]
119    pub fn runtime(mut self, enabled: bool) -> Self {
120        self.config.layers.runtime = enabled;
121        self
122    }
123
124    /// Enable specific extensions.
125    ///
126    /// ```ignore
127    /// .extensions([Extension::StringRotator])
128    /// .extensions([Extension::StringRotator, Extension::ControlFlow])
129    /// ```
130    #[must_use]
131    pub fn extensions<I>(mut self, exts: I) -> Self
132    where
133        I: IntoIterator<Item = Extension>,
134    {
135        // Reset all extensions first
136        self.config.layers.extensions_config.string_array = false;
137        self.config.layers.extensions_config.control_flow = false;
138        self.config.layers.extensions_config.proxy = false;
139
140        for ext in exts {
141            match ext {
142                Extension::StringRotator => {
143                    self.config.layers.extensions_config.string_array = true;
144                }
145                Extension::ControlFlow => {
146                    self.config.layers.extensions_config.control_flow = true;
147                }
148                Extension::Proxy => {
149                    self.config.layers.extensions_config.proxy = true;
150                }
151            }
152        }
153
154        // Enable extensions layer if any extension is enabled
155        self.config.layers.extensions = self.config.layers.extensions_config.string_array
156            || self.config.layers.extensions_config.control_flow
157            || self.config.layers.extensions_config.proxy;
158
159        self
160    }
161
162    /// Deobfuscate JavaScript source code.
163    pub fn deobfuscate(&self, source: &str) -> Result<String, DeobError> {
164        Ok(self.deobfuscate_with_result(source)?.code)
165    }
166
167    /// Deobfuscate JavaScript source code with detailed result.
168    pub fn deobfuscate_with_result(&self, source: &str) -> Result<DeobfuscateResult, DeobError> {
169        let allocator = Allocator::default();
170        let ret = Parser::new(&allocator, source, SourceType::mjs()).parse();
171
172        if !ret.errors.is_empty() {
173            return Err(DeobError::Parse(
174                ret.errors
175                    .iter()
176                    .map(|e| e.to_string())
177                    .collect::<Vec<_>>()
178                    .join("; "),
179            ));
180        }
181
182        let mut program = ret.program;
183        let engine = Engine::with_config(self.config.clone());
184        let result = engine.run(&allocator, &mut program)?;
185
186        Ok(DeobfuscateResult {
187            code: Codegen::new().build(&program).code,
188            iterations: result.iterations,
189            modifications: result.total_modifications,
190            converged: result.converged,
191        })
192    }
193}
194
195// ============================================================================
196// Convenience functions
197// ============================================================================
198
199/// Quick deobfuscation with standard config (ECMA + Runtime).
200pub fn deobfuscate(source: &str) -> Result<String, DeobError> {
201    JSDeobfuscator::new().deobfuscate(source)
202}
203
204/// Full deobfuscation with all extensions enabled.
205pub fn deobfuscate_full(source: &str) -> Result<String, DeobError> {
206    JSDeobfuscator::new()
207        .extensions([Extension::StringRotator, Extension::ControlFlow, Extension::Proxy])
208        .deobfuscate(source)
209}
210
211// ============================================================================
212// Tests
213// ============================================================================
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_simple() {
221        let result = deobfuscate("var a = 1 + 2;").unwrap();
222        assert!(result.contains("3"));
223    }
224
225    #[test]
226    fn test_builder() {
227        let result = JSDeobfuscator::new()
228            .ecma(true)
229            .runtime(true)
230            .max_iterations(10)
231            .deobfuscate("var a = 1 + 2;")
232            .unwrap();
233        assert!(result.contains("3"));
234    }
235
236    #[test]
237    fn test_extensions() {
238        let result = JSDeobfuscator::new()
239            .extensions([Extension::StringRotator])
240            .deobfuscate("var a = 1 + 2;")
241            .unwrap();
242        assert!(result.contains("3"));
243    }
244}