Skip to main content

js_deobfuscator/transform/
global.rs

1//! Global value injection.
2//!
3//! Resolves global references from user-provided config.
4//! `window.secret` → `"abc123"` when globals has `{"window": {"secret": "abc123"}}`.
5
6use oxc::allocator::Allocator;
7use oxc::ast::ast::{Expression, Program};
8use oxc::semantic::Scoping;
9
10use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
11
12use crate::ast::{create, extract};
13use crate::engine::error::Result;
14use crate::engine::module::{Module, TransformResult};
15use crate::scope::safety;
16use crate::value::JsValue;
17
18/// Global resolver module.
19pub struct GlobalResolver {
20    globals: std::collections::HashMap<String, serde_json::Value>,
21}
22
23impl GlobalResolver {
24    pub fn new(globals: std::collections::HashMap<String, serde_json::Value>) -> Self {
25        Self { globals }
26    }
27}
28
29impl Module for GlobalResolver {
30    fn name(&self) -> &'static str {
31        "GlobalResolver"
32    }
33
34    fn transform<'a>(
35        &mut self,
36        allocator: &'a Allocator,
37        program: &mut Program<'a>,
38        scoping: Scoping,
39    ) -> Result<TransformResult> {
40        if self.globals.is_empty() {
41            return Ok(TransformResult { modifications: 0, scoping });
42        }
43        let mut visitor = GlobalVisitor { globals: &self.globals, modifications: 0 };
44        let scoping = traverse_mut(&mut visitor, allocator, program, scoping, ());
45        Ok(TransformResult { modifications: visitor.modifications, scoping })
46    }
47}
48
49struct GlobalVisitor<'g> {
50    globals: &'g std::collections::HashMap<String, serde_json::Value>,
51    modifications: usize,
52}
53
54impl<'a, 'g> Traverse<'a, ()> for GlobalVisitor<'g> {
55    fn exit_expression(
56        &mut self,
57        expr: &mut Expression<'a>,
58        ctx: &mut TraverseCtx<'a, ()>,
59    ) {
60        // Resolve: global_name → value
61        if let Expression::Identifier(ident) = &*expr {
62            if !safety::is_global(ctx.scoping(), ident) { return; }
63            let name = ident.name.as_str();
64            if let Some(val) = self.globals.get(name) {
65                if let Some(js_val) = json_to_jsvalue(val) {
66                    *expr = create::from_js_value(&js_val, &ctx.ast);
67                    self.modifications += 1;
68                    return;
69                }
70            }
71        }
72
73        // Resolve: global_name.property → value (walk property chain)
74        if let Expression::StaticMemberExpression(_) = &*expr {
75            if let Some(val) = resolve_member_chain(expr, ctx.scoping(), self.globals) {
76                *expr = create::from_js_value(&val, &ctx.ast);
77                self.modifications += 1;
78            }
79        }
80    }
81}
82
83/// Walk a member expression chain: `window.config.key` → lookup in globals.
84fn resolve_member_chain(
85    expr: &Expression,
86    scoping: &Scoping,
87    globals: &std::collections::HashMap<String, serde_json::Value>,
88) -> Option<JsValue> {
89    // Collect the property chain
90    let mut chain = Vec::new();
91    let mut current = expr;
92
93    loop {
94        match current {
95            Expression::StaticMemberExpression(m) => {
96                chain.push(m.property.name.as_str());
97                current = &m.object;
98            }
99            Expression::ComputedMemberExpression(m) => {
100                let key = extract::string(&m.expression)?;
101                chain.push(key);
102                current = &m.object;
103            }
104            Expression::Identifier(ident) => {
105                if !safety::is_global(scoping, ident) { return None; }
106                chain.push(ident.name.as_str());
107                break;
108            }
109            _ => return None,
110        }
111    }
112
113    // Reverse to get root → leaf order
114    chain.reverse();
115
116    // Walk the JSON value
117    let root_name = chain.first()?;
118    let mut val = globals.get(*root_name)?;
119
120    for key in &chain[1..] {
121        val = val.get(key)?;
122    }
123
124    json_to_jsvalue(val)
125}
126
127fn json_to_jsvalue(val: &serde_json::Value) -> Option<JsValue> {
128    match val {
129        serde_json::Value::Number(n) => n.as_f64().map(JsValue::Number),
130        serde_json::Value::String(s) => Some(JsValue::String(s.clone())),
131        serde_json::Value::Bool(b) => Some(JsValue::Boolean(*b)),
132        serde_json::Value::Null => Some(JsValue::Null),
133        _ => None, // Objects/arrays can't be represented as JsValue
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use oxc::codegen::Codegen;
141    use oxc::parser::Parser;
142    use oxc::semantic::SemanticBuilder;
143    use oxc::span::SourceType;
144
145    fn resolve_globals(source: &str, globals: serde_json::Value) -> (String, usize) {
146        let allocator = Allocator::default();
147        let mut program = Parser::new(&allocator, source, SourceType::mjs()).parse().program;
148        let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
149
150        let map: std::collections::HashMap<String, serde_json::Value> = globals
151            .as_object()
152            .unwrap()
153            .iter()
154            .map(|(k, v)| (k.clone(), v.clone()))
155            .collect();
156
157        let mut module = GlobalResolver::new(map);
158        let result = module.transform(&allocator, &mut program, scoping).unwrap();
159        (Codegen::new().build(&program).code, result.modifications)
160    }
161
162    #[test]
163    fn test_simple_global() {
164        let (code, mods) = resolve_globals(
165            "console.log(SECRET);",
166            serde_json::json!({"SECRET": "abc123"}),
167        );
168        assert!(mods > 0);
169        assert!(code.contains("\"abc123\""), "got: {code}");
170    }
171
172    #[test]
173    fn test_nested_property() {
174        let (code, mods) = resolve_globals(
175            "console.log(window.config.key);",
176            serde_json::json!({"window": {"config": {"key": "value123"}}}),
177        );
178        assert!(mods > 0);
179        assert!(code.contains("\"value123\""), "got: {code}");
180    }
181
182    #[test]
183    fn test_no_resolve_declared() {
184        // x is declared locally, not a global
185        let (_, mods) = resolve_globals(
186            "var x = 1; console.log(x);",
187            serde_json::json!({"x": 999}),
188        );
189        assert_eq!(mods, 0, "should not resolve locally declared var");
190    }
191
192    #[test]
193    fn test_number_global() {
194        let (code, mods) = resolve_globals(
195            "console.log(TIMEOUT);",
196            serde_json::json!({"TIMEOUT": 5000}),
197        );
198        assert!(mods > 0);
199        assert!(code.contains("5e3") || code.contains("5000"), "got: {code}");
200    }
201}