js-deobfuscator 2.0.0

Universal JavaScript deobfuscator built on OXC
Documentation
//! Layer 4: Dynamic evaluation (Node.js subprocess).
//!
//! Fallback for expressions fold/ can't handle statically.
//! Spawns a persistent `node` process, sends expressions via stdin,
//! receives results via stdout JSON. Cached — same expression never evaluated twice.

pub mod safety;
pub mod node;

use oxc::allocator::Allocator;
use oxc::ast::ast::{Expression, Program};
use oxc::semantic::Scoping;

use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};

use crate::ast::{codegen, create, extract, query};
use crate::engine::error::Result;
use crate::engine::module::{Module, TransformResult};
use crate::value::JsValue;

/// Dynamic evaluator module.
#[derive(Default)]
pub struct DynamicEvaluator {
    node: Option<node::NodeProcess>,
}

impl DynamicEvaluator {
    pub fn new() -> Self {
        Self::default()
    }

    fn ensure_node(&mut self) -> Option<&mut node::NodeProcess> {
        if self.node.is_none() {
            self.node = node::NodeProcess::spawn().ok();
        }
        self.node.as_mut()
    }
}

impl Module for DynamicEvaluator {
    fn name(&self) -> &'static str {
        "DynamicEvaluator"
    }

    fn transform<'a>(
        &mut self,
        allocator: &'a Allocator,
        program: &mut Program<'a>,
        scoping: Scoping,
    ) -> Result<TransformResult> {
        let mut visitor = EvalVisitor {
            evaluator: self,
            modifications: 0,
        };
        let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
        Ok(TransformResult { modifications: visitor.modifications, scoping })
    }
}

struct EvalVisitor<'e> {
    evaluator: &'e mut DynamicEvaluator,
    modifications: usize,
}

impl<'a, 'e> Traverse<'a, ()> for EvalVisitor<'e> {
    fn exit_expression(
        &mut self,
        expr: &mut Expression<'a>,
        ctx: &mut TraverseCtx<'a, ()>,
    ) {
        // Skip if already a literal
        if extract::js_value(expr).is_some() {
            return;
        }

        // Must be side-effect-free
        if !query::is_side_effect_free(expr) {
            return;
        }

        // Must pass safety check
        if !safety::is_safe_expr(expr) {
            return;
        }

        // Generate code for the expression
        let code = codegen::expr_to_code(expr);

        // Evaluate via Node.js
        let Some(node) = self.evaluator.ensure_node() else {
            return;
        };
        let Some(result) = node.eval(&code) else {
            return;
        };

        // Convert JSON result to JsValue
        let Some(val) = json_to_jsvalue(&result) else {
            return;
        };

        *expr = create::from_js_value(&val, &ctx.ast);
        self.modifications += 1;
    }
}

fn json_to_jsvalue(val: &serde_json::Value) -> Option<JsValue> {
    match val {
        serde_json::Value::Number(n) => n.as_f64().map(JsValue::Number),
        serde_json::Value::String(s) => Some(JsValue::String(s.clone())),
        serde_json::Value::Bool(b) => Some(JsValue::Boolean(*b)),
        serde_json::Value::Null => Some(JsValue::Null),
        _ => None,
    }
}