js_deobfuscator/transform/
object.rs1use 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
23pub 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 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 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 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 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}