Skip to main content

js_deobfuscator/eval/
mod.rs

1//! Layer 4: Dynamic evaluation (Node.js subprocess).
2//!
3//! Fallback for expressions fold/ can't handle statically.
4//! Spawns a persistent `node` process, sends expressions via stdin,
5//! receives results via stdout JSON. Cached — same expression never evaluated twice.
6
7pub mod safety;
8pub mod node;
9
10use oxc::allocator::Allocator;
11use oxc::ast::ast::{Expression, Program};
12use oxc::semantic::Scoping;
13
14use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
15
16use crate::ast::{codegen, create, extract, query};
17use crate::engine::error::Result;
18use crate::engine::module::{Module, TransformResult};
19use crate::value::JsValue;
20
21/// Dynamic evaluator module.
22#[derive(Default)]
23pub struct DynamicEvaluator {
24    node: Option<node::NodeProcess>,
25}
26
27impl DynamicEvaluator {
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    fn ensure_node(&mut self) -> Option<&mut node::NodeProcess> {
33        if self.node.is_none() {
34            self.node = node::NodeProcess::spawn().ok();
35        }
36        self.node.as_mut()
37    }
38}
39
40impl Module for DynamicEvaluator {
41    fn name(&self) -> &'static str {
42        "DynamicEvaluator"
43    }
44
45    fn transform<'a>(
46        &mut self,
47        allocator: &'a Allocator,
48        program: &mut Program<'a>,
49        scoping: Scoping,
50    ) -> Result<TransformResult> {
51        let mut visitor = EvalVisitor {
52            evaluator: self,
53            modifications: 0,
54        };
55        let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
56        Ok(TransformResult { modifications: visitor.modifications, scoping })
57    }
58}
59
60struct EvalVisitor<'e> {
61    evaluator: &'e mut DynamicEvaluator,
62    modifications: usize,
63}
64
65impl<'a, 'e> Traverse<'a, ()> for EvalVisitor<'e> {
66    fn exit_expression(
67        &mut self,
68        expr: &mut Expression<'a>,
69        ctx: &mut TraverseCtx<'a, ()>,
70    ) {
71        // Skip if already a literal
72        if extract::js_value(expr).is_some() {
73            return;
74        }
75
76        // Must be side-effect-free
77        if !query::is_side_effect_free(expr) {
78            return;
79        }
80
81        // Must pass safety check
82        if !safety::is_safe_expr(expr) {
83            return;
84        }
85
86        // Generate code for the expression
87        let code = codegen::expr_to_code(expr);
88
89        // Evaluate via Node.js
90        let Some(node) = self.evaluator.ensure_node() else {
91            return;
92        };
93        let Some(result) = node.eval(&code) else {
94            return;
95        };
96
97        // Convert JSON result to JsValue
98        let Some(val) = json_to_jsvalue(&result) else {
99            return;
100        };
101
102        *expr = create::from_js_value(&val, &ctx.ast);
103        self.modifications += 1;
104    }
105}
106
107fn json_to_jsvalue(val: &serde_json::Value) -> Option<JsValue> {
108    match val {
109        serde_json::Value::Number(n) => n.as_f64().map(JsValue::Number),
110        serde_json::Value::String(s) => Some(JsValue::String(s.clone())),
111        serde_json::Value::Bool(b) => Some(JsValue::Boolean(*b)),
112        serde_json::Value::Null => Some(JsValue::Null),
113        _ => None,
114    }
115}