Skip to main content

js_deobfuscator/transform/
object.rs

1//! Object/array property resolution.
2//!
3//! `var t = {F: 445}; t.F` → `445`
4//! `var a = [10, 20]; a[1]` → `20`
5//!
6//! Two-pass: collect constant objects/arrays, resolve property access.
7
8use rustc_hash::{FxHashMap, FxHashSet};
9
10use oxc::allocator::Allocator;
11use oxc::ast::ast::{Expression, ObjectPropertyKind, Program, PropertyKey};
12use oxc::semantic::{Scoping, SymbolId};
13
14use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
15
16use crate::ast::{create, extract};
17use crate::engine::error::Result;
18use crate::engine::module::{Module, TransformResult};
19use crate::scope::{query, resolve};
20use crate::value::JsValue;
21use crate::value::coerce::number_to_string;
22
23/// Object property resolution module.
24pub struct ObjectPropResolver;
25
26impl Module for ObjectPropResolver {
27    fn name(&self) -> &'static str {
28        "ObjectPropResolver"
29    }
30
31    fn transform<'a>(
32        &mut self,
33        allocator: &'a Allocator,
34        program: &mut Program<'a>,
35        scoping: Scoping,
36    ) -> Result<TransformResult> {
37        let mut collector = Collector::default();
38        let scoping = traverse_mut(&mut collector, allocator, program, scoping, ());
39
40        // Remove mutated symbols
41        for sym in &collector.mutated {
42            collector.objects.remove(sym);
43            collector.arrays.remove(sym);
44        }
45
46        if collector.objects.is_empty() && collector.arrays.is_empty() {
47            return Ok(TransformResult { modifications: 0, scoping });
48        }
49
50        let mut resolver = Resolver {
51            objects: collector.objects,
52            arrays: collector.arrays,
53            modifications: 0,
54        };
55        let scoping = traverse_mut(&mut resolver, allocator, program, scoping, ());
56
57        Ok(TransformResult { modifications: resolver.modifications, scoping })
58    }
59}
60
61#[derive(Default)]
62struct Collector {
63    objects: FxHashMap<SymbolId, FxHashMap<String, JsValue>>,
64    arrays: FxHashMap<SymbolId, Vec<JsValue>>,
65    mutated: FxHashSet<SymbolId>,
66}
67
68impl<'a> Traverse<'a, ()> for Collector {
69    fn enter_assignment_expression(
70        &mut self,
71        node: &mut oxc::ast::ast::AssignmentExpression<'a>,
72        ctx: &mut TraverseCtx<'a, ()>,
73    ) {
74        let target_obj = match &node.left {
75            oxc::ast::ast::AssignmentTarget::StaticMemberExpression(m) => Some(&m.object),
76            oxc::ast::ast::AssignmentTarget::ComputedMemberExpression(m) => Some(&m.object),
77            _ => None,
78        };
79        if let Some(Expression::Identifier(id)) = target_obj {
80            if let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) {
81                self.mutated.insert(sym);
82            }
83        }
84    }
85
86    fn enter_update_expression(
87        &mut self,
88        node: &mut oxc::ast::ast::UpdateExpression<'a>,
89        ctx: &mut TraverseCtx<'a, ()>,
90    ) {
91        // Catch ++obj.x, obj.x--, etc.
92        let target_obj = match &node.argument {
93            oxc::ast::ast::SimpleAssignmentTarget::StaticMemberExpression(m) => Some(&m.object),
94            oxc::ast::ast::SimpleAssignmentTarget::ComputedMemberExpression(m) => Some(&m.object),
95            _ => None,
96        };
97        if let Some(Expression::Identifier(id)) = target_obj {
98            if let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) {
99                self.mutated.insert(sym);
100            }
101        }
102    }
103
104    fn enter_unary_expression(
105        &mut self,
106        node: &mut oxc::ast::ast::UnaryExpression<'a>,
107        ctx: &mut TraverseCtx<'a, ()>,
108    ) {
109        // Catch delete obj.x
110        if node.operator != oxc::ast::ast::UnaryOperator::Delete {
111            return;
112        }
113        let target_obj = match &node.argument {
114            Expression::StaticMemberExpression(m) => Some(&m.object),
115            Expression::ComputedMemberExpression(m) => Some(&m.object),
116            _ => None,
117        };
118        if let Some(Expression::Identifier(id)) = target_obj {
119            if let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) {
120                self.mutated.insert(sym);
121            }
122        }
123    }
124
125    fn enter_call_expression(
126        &mut self,
127        node: &mut oxc::ast::ast::CallExpression<'a>,
128        ctx: &mut TraverseCtx<'a, ()>,
129    ) {
130        // Catch mutating method calls: arr.push(), arr.splice(), obj.assign(), etc.
131        const MUTATING_METHODS: &[&str] = &[
132            "push", "pop", "shift", "unshift", "splice", "sort", "reverse", "fill", "copyWithin",
133        ];
134        if let Expression::StaticMemberExpression(m) = &node.callee {
135            if MUTATING_METHODS.contains(&m.property.name.as_str()) {
136                if let Expression::Identifier(id) = &m.object {
137                    if let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) {
138                        self.mutated.insert(sym);
139                    }
140                }
141            }
142        }
143    }
144
145    fn enter_variable_declarator(
146        &mut self,
147        node: &mut oxc::ast::ast::VariableDeclarator<'a>,
148        ctx: &mut TraverseCtx<'a, ()>,
149    ) {
150        let Some(init) = &node.init else { return };
151        let Some(symbol_id) = resolve::get_declarator_symbol(node) else { return };
152
153        if query::has_writes(ctx.scoping(), symbol_id) {
154            return;
155        }
156
157        match init {
158            Expression::ObjectExpression(obj) => {
159                let mut props = FxHashMap::default();
160                for prop in &obj.properties {
161                    let ObjectPropertyKind::ObjectProperty(p) = prop else { return; };
162                    let Some(key) = get_key_str(&p.key) else { return; };
163                    let Some(val) = extract::js_value(&p.value) else { return; };
164                    props.insert(key, val);
165                }
166                if !props.is_empty() {
167                    self.objects.insert(symbol_id, props);
168                }
169            }
170            Expression::ArrayExpression(arr) => {
171                let elements: Option<Vec<JsValue>> = arr.elements.iter()
172                    .map(|el| el.as_expression().and_then(extract::js_value))
173                    .collect();
174                if let Some(elems) = elements {
175                    if !elems.is_empty() {
176                        self.arrays.insert(symbol_id, elems);
177                    }
178                }
179            }
180            _ => {}
181        }
182    }
183}
184
185struct Resolver {
186    objects: FxHashMap<SymbolId, FxHashMap<String, JsValue>>,
187    arrays: FxHashMap<SymbolId, Vec<JsValue>>,
188    modifications: usize,
189}
190
191impl<'a> Traverse<'a, ()> for Resolver {
192    fn exit_expression(
193        &mut self,
194        expr: &mut Expression<'a>,
195        ctx: &mut TraverseCtx<'a, ()>,
196    ) {
197        match &*expr {
198            Expression::StaticMemberExpression(_) => self.try_resolve_static(expr, ctx),
199            Expression::ComputedMemberExpression(_) => self.try_resolve_computed(expr, ctx),
200            _ => {}
201        }
202    }
203}
204
205impl Resolver {
206    fn try_resolve_static<'a>(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a, ()>) {
207        let val = {
208            let Expression::StaticMemberExpression(m) = &*expr else { return; };
209            let Expression::Identifier(id) = &m.object else { return; };
210            let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) else { return; };
211            let prop = m.property.name.as_str();
212
213            if let Some(props) = self.objects.get(&sym) {
214                props.get(prop).cloned()
215            } else if let Some(elems) = self.arrays.get(&sym) {
216                if prop == "length" { Some(JsValue::Number(elems.len() as f64)) } else { None }
217            } else {
218                None
219            }
220        };
221        if let Some(val) = val {
222            *expr = create::from_js_value(&val, &ctx.ast);
223            self.modifications += 1;
224        }
225    }
226
227    fn try_resolve_computed<'a>(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a, ()>) {
228        let val = {
229            let Expression::ComputedMemberExpression(m) = &*expr else { return; };
230            let Expression::Identifier(id) = &m.object else { return; };
231            let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) else { return; };
232
233            if let Some(props) = self.objects.get(&sym) {
234                extract::string(&m.expression).and_then(|k| props.get(k).cloned())
235            } else if let Some(elems) = self.arrays.get(&sym) {
236                extract::number(&m.expression).and_then(|idx| elems.get(idx as usize).cloned())
237            } else {
238                None
239            }
240        };
241        if let Some(val) = val {
242            *expr = create::from_js_value(&val, &ctx.ast);
243            self.modifications += 1;
244        }
245    }
246}
247
248fn get_key_str(key: &PropertyKey) -> Option<String> {
249    match key {
250        PropertyKey::StaticIdentifier(id) => Some(id.name.to_string()),
251        PropertyKey::StringLiteral(s) => Some(s.value.to_string()),
252        PropertyKey::NumericLiteral(n) => Some(number_to_string(n.value)),
253        _ => None,
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use oxc::codegen::Codegen;
261    use oxc::parser::Parser;
262    use oxc::semantic::SemanticBuilder;
263    use oxc::span::SourceType;
264
265    fn resolve_obj(source: &str) -> (String, usize) {
266        let allocator = Allocator::default();
267        let mut program = Parser::new(&allocator, source, SourceType::mjs()).parse().program;
268        let scoping = SemanticBuilder::new().build(&program).semantic.into_scoping();
269        let mut module = ObjectPropResolver;
270        let result = module.transform(&allocator, &mut program, scoping).unwrap();
271        (Codegen::new().build(&program).code, result.modifications)
272    }
273
274    #[test]
275    fn test_object_static() {
276        let (code, mods) = resolve_obj("var t = {F: 445, H: 417}; console.log(t.F);");
277        assert!(mods > 0);
278        assert!(code.contains("445"), "t.F → 445: {code}");
279    }
280
281    #[test]
282    fn test_object_computed() {
283        let (code, mods) = resolve_obj("var t = {F: 445}; console.log(t[\"F\"]);");
284        assert!(mods > 0);
285        assert!(code.contains("445"), "got: {code}");
286    }
287
288    #[test]
289    fn test_array_index() {
290        let (code, mods) = resolve_obj("var a = [10, 20, 30]; console.log(a[1]);");
291        assert!(mods > 0);
292        assert!(code.contains("20"), "a[1] → 20: {code}");
293    }
294
295    #[test]
296    fn test_array_length() {
297        let (code, mods) = resolve_obj("var a = [1, 2, 3]; console.log(a.length);");
298        assert!(mods > 0);
299        assert!(code.contains("3"), "got: {code}");
300    }
301
302    #[test]
303    fn test_no_resolve_mutated() {
304        let (code, mods) = resolve_obj("var t = {x: 1}; t.x = 2; console.log(t.x);");
305        assert_eq!(mods, 0);
306        assert!(code.contains("t.x"), "got: {code}");
307    }
308}