1use oxc::allocator::Allocator;
4use oxc::codegen::Codegen;
5use oxc::parser::Parser;
6use oxc::span::SourceType;
7
8use super::config::Config;
9use super::error::{DeobError, Result};
10use super::module::Module;
11use super::pipeline::{Engine, EngineResult};
12use crate::targets::Target;
13
14pub struct DeobfuscateResult {
16 pub code: String,
18 pub iterations: usize,
20 pub modifications: usize,
22 pub converged: bool,
24}
25
26pub struct JSDeobfuscator {
36 config: Config,
37 target: Option<Target>,
38 custom_common: Vec<Box<dyn Module>>,
39 custom_locked: Vec<Box<dyn Module>>,
40}
41
42impl Default for JSDeobfuscator {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl JSDeobfuscator {
49 #[must_use]
51 pub fn new() -> Self {
52 Self {
53 config: Config::default(),
54 target: None,
55 custom_common: Vec::new(),
56 custom_locked: Vec::new(),
57 }
58 }
59
60 #[must_use]
62 pub fn with_config(config: Config) -> Self {
63 Self {
64 config,
65 target: None,
66 custom_common: Vec::new(),
67 custom_locked: Vec::new(),
68 }
69 }
70
71 #[must_use]
81 pub fn target(mut self, target: Target) -> Self {
82 self.target = Some(target);
83 self
84 }
85
86 #[must_use]
88 pub fn max_iterations(mut self, n: usize) -> Self {
89 self.config.max_iterations = n;
90 self
91 }
92
93 #[must_use]
95 pub fn static_eval(mut self, enabled: bool) -> Self {
96 self.config.static_eval = enabled;
97 self
98 }
99
100 #[must_use]
102 pub fn dynamic_eval(mut self, enabled: bool) -> Self {
103 self.config.dynamic_eval = enabled;
104 self
105 }
106
107 #[must_use]
109 pub fn transforms(mut self, enabled: bool) -> Self {
110 self.config.transforms = enabled;
111 self
112 }
113
114 #[must_use]
116 pub fn global(mut self, name: &str, value: serde_json::Value) -> Self {
117 self.config.globals.insert(name.to_string(), value);
118 self
119 }
120
121 #[must_use]
123 pub fn add_common(mut self, module: Box<dyn Module>) -> Self {
124 self.custom_common.push(module);
125 self
126 }
127
128 #[must_use]
130 pub fn add_locked(mut self, module: Box<dyn Module>) -> Self {
131 self.custom_locked.push(module);
132 self
133 }
134
135 pub fn deobfuscate(self, source: &str) -> Result<DeobfuscateResult> {
137 let allocator = Allocator::default();
138 let ret = Parser::new(&allocator, source, SourceType::mjs()).parse();
139
140 if !ret.errors.is_empty() {
141 let msg = ret.errors.iter()
142 .map(|e| e.to_string())
143 .collect::<Vec<_>>()
144 .join("; ");
145 return Err(DeobError::Parse(msg));
146 }
147
148 let mut program = ret.program;
149
150 let mut common: Vec<Box<dyn Module>> = Vec::new();
152 let mut locked: Vec<Box<dyn Module>> = Vec::new();
153
154 if self.config.transforms {
157 if !self.config.globals.is_empty() {
158 common.push(Box::new(crate::transform::global::GlobalResolver::new(
159 self.config.globals.clone(),
160 )));
161 }
162 common.push(Box::new(crate::transform::object::ObjectPropResolver));
163 common.push(Box::new(crate::transform::constant::ConstantPropagator));
164 common.push(Box::new(crate::transform::alias::AliasInliner));
165 common.push(Box::new(crate::transform::proxy::ProxyInliner));
166 }
167 if self.config.static_eval {
168 common.push(Box::new(crate::fold::StaticFolder));
169 }
170 if self.config.transforms {
171 common.push(Box::new(crate::transform::member::MemberSimplifier));
172 common.push(Box::new(crate::transform::dead::DeadCodeEliminator));
173 }
174
175 if let Some(target) = self.target {
177 locked.extend(target.modules());
178 }
179
180 common.extend(self.custom_common);
182 locked.extend(self.custom_locked);
183
184 let mut engine = Engine::new(common, locked, self.config.max_iterations);
186 let EngineResult {
187 iterations,
188 total_modifications,
189 converged,
190 ..
191 } = engine.run(&allocator, &mut program)?;
192
193 let code = Codegen::new().build(&program).code;
195
196 Ok(DeobfuscateResult {
197 code,
198 iterations,
199 modifications: total_modifications,
200 converged,
201 })
202 }
203}
204
205#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_simple_fold() {
215 let result = JSDeobfuscator::new()
216 .deobfuscate("console.log(1 + 2);")
217 .unwrap();
218 assert!(result.code.contains("console.log(3)"), "got: {}", result.code);
219 assert!(result.converged);
220 }
221
222 #[test]
223 fn test_string_concat() {
224 let result = JSDeobfuscator::new()
225 .deobfuscate("console.log(\"hello\" + \" world\");")
226 .unwrap();
227 assert!(result.code.contains("\"hello world\""), "got: {}", result.code);
228 }
229
230 #[test]
231 fn test_math_fold() {
232 let result = JSDeobfuscator::new()
233 .deobfuscate("console.log(Math.floor(1.7));")
234 .unwrap();
235 assert!(result.code.contains("console.log(1)"), "got: {}", result.code);
236 }
237
238 #[test]
239 fn test_atob_fold() {
240 let result = JSDeobfuscator::new()
241 .deobfuscate("console.log(atob(\"SGVsbG8=\"));")
242 .unwrap();
243 assert!(result.code.contains("\"Hello\""), "got: {}", result.code);
244 }
245
246 #[test]
247 fn test_ternary_fold() {
248 let result = JSDeobfuscator::new()
249 .deobfuscate("console.log(true ? 42 : 0);")
250 .unwrap();
251 assert!(result.code.contains("console.log(42)"), "got: {}", result.code);
252 }
253
254 #[test]
255 fn test_dead_code_removal() {
256 let result = JSDeobfuscator::new()
257 .deobfuscate("if (false) { var dead = 1; } console.log(2);")
258 .unwrap();
259 assert!(!result.code.contains("dead"), "got: {}", result.code);
260 assert!(result.code.contains("console.log(2)"), "got: {}", result.code);
261 }
262
263 #[test]
264 fn test_chained_fold() {
265 let result = JSDeobfuscator::new()
266 .deobfuscate("console.log(String(1 + 2));")
267 .unwrap();
268 assert!(result.code.contains("\"3\""), "got: {}", result.code);
269 }
270
271 #[test]
272 fn test_complex_chain() {
273 let result = JSDeobfuscator::new()
274 .deobfuscate("console.log(atob(\"dGVzdA==\").toUpperCase());")
275 .unwrap();
276 assert!(result.code.contains("\"TEST\""), "got: {}", result.code);
277 }
278
279 #[test]
280 fn test_convergence_with_nested() {
281 let result = JSDeobfuscator::new()
282 .deobfuscate("console.log(parseInt(\"0xff\") + 1);")
283 .unwrap();
284 assert!(result.code.contains("256"), "got: {}", result.code);
285 }
286
287 #[test]
288 fn test_parse_error() {
289 let result = JSDeobfuscator::new()
290 .deobfuscate("var x = ;");
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn test_no_modifications_converges() {
296 let result = JSDeobfuscator::new()
297 .deobfuscate("console.log(y);")
298 .unwrap();
299 assert!(result.converged);
300 }
301
302 #[test]
303 fn test_disabled_static_eval() {
304 let result = JSDeobfuscator::new()
305 .static_eval(false)
306 .transforms(false)
307 .deobfuscate("var x = 1 + 2;")
308 .unwrap();
309 assert!(result.code.contains("1 + 2"), "got: {}", result.code);
310 }
311
312 #[test]
313 fn test_constant_propagation() {
314 let result = JSDeobfuscator::new()
315 .deobfuscate("var key = 42; console.log(key);")
316 .unwrap();
317 assert!(result.code.contains("console.log(42)"), "got: {}", result.code);
319 }
320
321 #[test]
322 fn test_dead_var_removed_function_scope() {
323 let result = JSDeobfuscator::new()
326 .deobfuscate("function f() { var unused = 1; console.log(2); } f();")
327 .unwrap();
328 assert!(!result.code.contains("unused"), "got: {}", result.code);
329 assert!(result.code.contains("console.log(2)"), "got: {}", result.code);
330 }
331
332 #[test]
333 fn test_module_scope_var_preserved() {
334 let result = JSDeobfuscator::new()
337 .deobfuscate("var unused = 1; console.log(2);")
338 .unwrap();
339 assert!(result.code.contains("unused"), "module-scope var must be preserved: {}", result.code);
340 }
341
342 #[test]
343 fn test_member_simplification() {
344 let result = JSDeobfuscator::new()
345 .deobfuscate("console.log(obj[\"property\"]);")
346 .unwrap();
347 assert!(result.code.contains("obj.property"), "got: {}", result.code);
348 }
349
350 #[test]
351 fn test_full_pipeline() {
352 let result = JSDeobfuscator::new()
356 .deobfuscate(r#"
357 function main() {
358 var _0x1 = "Hello";
359 var _0x2 = " ";
360 var _0x3 = "World";
361 var _unused = 999;
362 console.log(_0x1 + _0x2 + _0x3);
363 }
364 main();
365 "#)
366 .unwrap();
367 assert!(result.code.contains("\"Hello World\""), "got: {}", result.code);
368 assert!(!result.code.contains("_unused"), "got: {}", result.code);
369 assert!(!result.code.contains("999"), "got: {}", result.code);
370 }
371
372 #[test]
373 fn test_alias_inlining() {
374 let result = JSDeobfuscator::new()
375 .deobfuscate("var e = Yp; console.log(e(445));")
376 .unwrap();
377 assert!(result.code.contains("Yp(445)"), "alias not inlined: {}", result.code);
378 }
379
380 #[test]
381 fn test_object_prop_resolution() {
382 let result = JSDeobfuscator::new()
383 .deobfuscate("var t = {F: 445, H: 417}; console.log(t.F + t.H);")
384 .unwrap();
385 assert!(result.code.contains("862"), "got: {}", result.code);
386 }
387
388 #[test]
389 fn test_global_injection() {
390 let result = JSDeobfuscator::new()
391 .global("window", serde_json::json!({"secret": "abc123"}))
392 .deobfuscate("console.log(window.secret);")
393 .unwrap();
394 assert!(result.code.contains("\"abc123\""), "got: {}", result.code);
395 }
396
397 #[test]
398 fn test_obfuscated_pattern_full() {
399 let result = JSDeobfuscator::new()
403 .deobfuscate(r#"
404 function main() {
405 var _0x4e = {a: "log", b: "Hello"};
406 var _0xc = console;
407 var _unused = 42;
408 _0xc[_0x4e["a"]](_0x4e["b"]);
409 }
410 main();
411 "#)
412 .unwrap();
413 assert!(result.code.contains("\"Hello\""), "got: {}", result.code);
415 assert!(!result.code.contains("_unused"), "dead code not removed: {}", result.code);
416 }
417
418 #[test]
419 fn test_target_api() {
420 let result = JSDeobfuscator::new()
422 .target(Target::ObfuscatorIO)
423 .deobfuscate("console.log(1 + 2);")
424 .unwrap();
425 assert!(result.code.contains("3"), "got: {}", result.code);
426 }
427}