pyjsclear 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. pyjsclear/__init__.py +47 -0
  2. pyjsclear/__main__.py +37 -0
  3. pyjsclear/deobfuscator.py +191 -0
  4. pyjsclear/generator.py +772 -0
  5. pyjsclear/parser.py +46 -0
  6. pyjsclear/scope.py +283 -0
  7. pyjsclear/transforms/__init__.py +0 -0
  8. pyjsclear/transforms/aa_decode.py +83 -0
  9. pyjsclear/transforms/anti_tamper.py +106 -0
  10. pyjsclear/transforms/base.py +24 -0
  11. pyjsclear/transforms/constant_prop.py +103 -0
  12. pyjsclear/transforms/control_flow.py +330 -0
  13. pyjsclear/transforms/dead_branch.py +71 -0
  14. pyjsclear/transforms/eval_unpack.py +133 -0
  15. pyjsclear/transforms/expression_simplifier.py +403 -0
  16. pyjsclear/transforms/hex_escapes.py +68 -0
  17. pyjsclear/transforms/jj_decode.py +91 -0
  18. pyjsclear/transforms/jsfuck_decode.py +93 -0
  19. pyjsclear/transforms/logical_to_if.py +173 -0
  20. pyjsclear/transforms/object_packer.py +133 -0
  21. pyjsclear/transforms/object_simplifier.py +192 -0
  22. pyjsclear/transforms/property_simplifier.py +43 -0
  23. pyjsclear/transforms/proxy_functions.py +193 -0
  24. pyjsclear/transforms/reassignment.py +183 -0
  25. pyjsclear/transforms/sequence_splitter.py +215 -0
  26. pyjsclear/transforms/string_revealer.py +1259 -0
  27. pyjsclear/transforms/unused_vars.py +111 -0
  28. pyjsclear/traverser.py +195 -0
  29. pyjsclear/utils/__init__.py +0 -0
  30. pyjsclear/utils/ast_helpers.py +257 -0
  31. pyjsclear/utils/string_decoders.py +141 -0
  32. pyjsclear-0.1.0.dist-info/METADATA +168 -0
  33. pyjsclear-0.1.0.dist-info/RECORD +38 -0
  34. pyjsclear-0.1.0.dist-info/WHEEL +5 -0
  35. pyjsclear-0.1.0.dist-info/entry_points.txt +2 -0
  36. pyjsclear-0.1.0.dist-info/licenses/LICENSE +201 -0
  37. pyjsclear-0.1.0.dist-info/licenses/NOTICE +19 -0
  38. pyjsclear-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,193 @@
1
+ """Proxy function detection and inlining.
2
+
3
+ Detects patterns like:
4
+ function _proxy(a, b) { return a + b; }
5
+ _proxy(x, y) -> x + y
6
+ """
7
+
8
+ from ..scope import build_scope_tree
9
+ from ..traverser import traverse
10
+ from ..utils.ast_helpers import deep_copy
11
+ from ..utils.ast_helpers import get_child_keys
12
+ from ..utils.ast_helpers import is_identifier
13
+ from ..utils.ast_helpers import replace_identifiers
14
+ from .base import Transform
15
+
16
+
17
+ class ProxyFunctionInliner(Transform):
18
+ """Inline proxy function calls."""
19
+
20
+ rebuild_scope = True
21
+
22
+ def execute(self):
23
+ scope_tree, node_scope = build_scope_tree(self.ast)
24
+
25
+ # Find proxy functions
26
+ proxy_fns = {} # name -> (func_node, scope, binding)
27
+ self._find_proxy_functions(scope_tree, proxy_fns)
28
+
29
+ if not proxy_fns:
30
+ return False
31
+
32
+ # Collect call sites with depth info
33
+ call_sites = [] # (call_node, parent, key, index, proxy_info, depth)
34
+ depth_counter = [0]
35
+
36
+ def enter(node, parent, key, index):
37
+ depth_counter[0] += 1
38
+ if node.get('type') != 'CallExpression':
39
+ return
40
+ callee = node.get('callee')
41
+ if not is_identifier(callee):
42
+ return
43
+ name = callee.get('name', '')
44
+ if name not in proxy_fns:
45
+ return
46
+ call_sites.append((node, parent, key, index, proxy_fns[name], depth_counter[0]))
47
+
48
+ traverse(self.ast, {'enter': enter})
49
+
50
+ # Process innermost calls first
51
+ call_sites.sort(key=lambda x: x[5], reverse=True)
52
+
53
+ for (
54
+ call_node,
55
+ parent,
56
+ key,
57
+ index,
58
+ (func_node, scope, binding),
59
+ depth,
60
+ ) in call_sites:
61
+ replacement = self._get_replacement(func_node, call_node.get('arguments', []))
62
+ if replacement is None:
63
+ continue
64
+ # Replace the call with the inlined expression
65
+ if index is not None:
66
+ parent[key][index] = replacement
67
+ else:
68
+ parent[key] = replacement
69
+ self.set_changed()
70
+
71
+ return self.has_changed()
72
+
73
+ def _find_proxy_functions(self, scope, result):
74
+ """Find all proxy function bindings in the scope tree."""
75
+ for name, binding in scope.bindings.items():
76
+ if not binding.is_constant:
77
+ continue
78
+ func_node = self._get_function_expr(binding)
79
+ if func_node and self._is_proxy_function(func_node):
80
+ result[name] = (func_node, scope, binding)
81
+
82
+ for child in scope.children:
83
+ self._find_proxy_functions(child, result)
84
+
85
+ def _get_function_expr(self, binding):
86
+ """Get the function expression from a binding."""
87
+ node = binding.node
88
+ if isinstance(node, dict):
89
+ node_type = node.get('type', '')
90
+ if node_type in (
91
+ 'FunctionDeclaration',
92
+ 'FunctionExpression',
93
+ 'ArrowFunctionExpression',
94
+ ):
95
+ return node
96
+ if node_type == 'VariableDeclarator':
97
+ init = node.get('init')
98
+ if init and init.get('type') in (
99
+ 'FunctionExpression',
100
+ 'ArrowFunctionExpression',
101
+ ):
102
+ return init
103
+ return None
104
+
105
+ def _is_proxy_function(self, func_node):
106
+ """Check if a function is a simple proxy (single return of an expression)."""
107
+ params = func_node.get('params', [])
108
+ if not all(parameter.get('type') == 'Identifier' for parameter in params):
109
+ return False
110
+
111
+ body = func_node.get('body')
112
+ if not body:
113
+ return False
114
+
115
+ # Arrow function with expression body
116
+ if func_node.get('type') == 'ArrowFunctionExpression' and body.get('type') != 'BlockStatement':
117
+ return self._is_proxy_value(body)
118
+
119
+ # Block with single return
120
+ if body.get('type') == 'BlockStatement':
121
+ stmts = body.get('body', [])
122
+ if len(stmts) != 1:
123
+ return False
124
+ stmt = stmts[0]
125
+ if stmt.get('type') != 'ReturnStatement':
126
+ return False
127
+ arg = stmt.get('argument')
128
+ if arg is None:
129
+ return True # returns undefined
130
+ return self._is_proxy_value(arg)
131
+
132
+ return False
133
+
134
+ _DISALLOWED_PROXY_TYPES = frozenset(
135
+ {
136
+ 'FunctionExpression',
137
+ 'FunctionDeclaration',
138
+ 'ArrowFunctionExpression',
139
+ 'BlockStatement',
140
+ 'SequenceExpression',
141
+ 'AssignmentExpression',
142
+ }
143
+ )
144
+
145
+ def _is_proxy_value(self, node):
146
+ """Check if an expression is a valid proxy return value (no side effects)."""
147
+ if not isinstance(node, dict) or 'type' not in node:
148
+ return False
149
+ if node.get('type', '') in self._DISALLOWED_PROXY_TYPES:
150
+ return False
151
+ for key in get_child_keys(node):
152
+ child = node.get(key)
153
+ if child is None:
154
+ continue
155
+ if isinstance(child, list):
156
+ if any(isinstance(item, dict) and item.get('type') in self._DISALLOWED_PROXY_TYPES for item in child):
157
+ return False
158
+ elif isinstance(child, dict) and child.get('type') in self._DISALLOWED_PROXY_TYPES:
159
+ return False
160
+ return True
161
+
162
+ def _get_replacement(self, func_node, args):
163
+ """Get the replacement expression for a proxy function call."""
164
+ body = func_node.get('body')
165
+ if not body:
166
+ return {'type': 'Identifier', 'name': 'undefined'}
167
+
168
+ # Arrow with expression body
169
+ if func_node.get('type') == 'ArrowFunctionExpression' and body.get('type') != 'BlockStatement':
170
+ expr = deep_copy(body)
171
+ elif body.get('type') == 'BlockStatement':
172
+ stmts = body.get('body', [])
173
+ if not stmts or stmts[0].get('type') != 'ReturnStatement':
174
+ return None
175
+ arg = stmts[0].get('argument')
176
+ if arg is None:
177
+ return {'type': 'Identifier', 'name': 'undefined'}
178
+ expr = deep_copy(arg)
179
+ else:
180
+ return None
181
+
182
+ # Build parameter map
183
+ params = func_node.get('params', [])
184
+ param_map = {}
185
+ for i, parameter in enumerate(params):
186
+ if parameter.get('type') == 'Identifier':
187
+ if i < len(args):
188
+ param_map[parameter['name']] = args[i]
189
+ else:
190
+ param_map[parameter['name']] = {'type': 'Identifier', 'name': 'undefined'}
191
+
192
+ replace_identifiers(expr, param_map)
193
+ return expr
@@ -0,0 +1,183 @@
1
+ """Remove redundant variable reassignments.
2
+
3
+ Detects: var x = y; (where y is also a variable)
4
+ And replaces all references to x with y, then removes x.
5
+ """
6
+
7
+ from ..scope import build_scope_tree
8
+ from ..traverser import REMOVE
9
+ from ..traverser import traverse
10
+ from ..utils.ast_helpers import is_identifier
11
+ from .base import Transform
12
+
13
+
14
+ class ReassignmentRemover(Transform):
15
+ """Remove redundant reassignments like x = y where y is used identically."""
16
+
17
+ # Well-known globals that are safe to inline as reassignment targets
18
+ _WELL_KNOWN_GLOBALS = frozenset(
19
+ {
20
+ 'JSON',
21
+ 'Object',
22
+ 'Array',
23
+ 'String',
24
+ 'Number',
25
+ 'Boolean',
26
+ 'Math',
27
+ 'Date',
28
+ 'RegExp',
29
+ 'Error',
30
+ 'Map',
31
+ 'Set',
32
+ 'WeakMap',
33
+ 'WeakSet',
34
+ 'Promise',
35
+ 'Symbol',
36
+ 'Proxy',
37
+ 'Reflect',
38
+ 'console',
39
+ 'parseInt',
40
+ 'parseFloat',
41
+ 'isNaN',
42
+ 'isFinite',
43
+ 'Buffer',
44
+ 'process',
45
+ 'require',
46
+ 'undefined',
47
+ 'NaN',
48
+ 'Infinity',
49
+ }
50
+ )
51
+
52
+ rebuild_scope = True
53
+
54
+ def execute(self):
55
+ scope_tree, _ = build_scope_tree(self.ast)
56
+ self._process_scope(scope_tree)
57
+ self._inline_assignment_aliases(scope_tree)
58
+ return self.has_changed()
59
+
60
+ def _process_scope(self, scope):
61
+ for name, binding in list(scope.bindings.items()):
62
+ if not binding.is_constant:
63
+ continue
64
+ if binding.kind == 'param':
65
+ continue
66
+
67
+ node = binding.node
68
+ if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
69
+ continue
70
+
71
+ initializer = node.get('init')
72
+ if not initializer or not is_identifier(initializer):
73
+ continue
74
+
75
+ target_name = initializer.get('name', '')
76
+ if target_name == name:
77
+ continue
78
+ target_binding = scope.get_binding(target_name)
79
+ # Allow inlining if target is a well-known global or a constant binding
80
+ if target_binding:
81
+ if not target_binding.is_constant:
82
+ continue
83
+ elif target_name not in self._WELL_KNOWN_GLOBALS:
84
+ continue
85
+
86
+ # Replace all references to `name` with `target_name`
87
+ for reference_node, reference_parent, reference_key, reference_index in binding.references:
88
+ if (
89
+ reference_parent
90
+ and reference_parent.get('type') == 'AssignmentExpression'
91
+ and reference_key == 'left'
92
+ ):
93
+ continue
94
+ if reference_parent and reference_parent.get('type') == 'VariableDeclarator' and reference_key == 'id':
95
+ continue
96
+ new_id = {'type': 'Identifier', 'name': target_name}
97
+ if reference_index is not None:
98
+ reference_parent[reference_key][reference_index] = new_id
99
+ else:
100
+ reference_parent[reference_key] = new_id
101
+ self.set_changed()
102
+
103
+ for child in scope.children:
104
+ self._process_scope(child)
105
+
106
+ def _inline_assignment_aliases(self, scope_tree):
107
+ """Inline aliases created by `var x; ... x = y;` patterns.
108
+
109
+ Handles the obfuscator pattern where a variable is declared without
110
+ init, then assigned once to another identifier, and only read after that.
111
+ """
112
+ self._process_assignment_aliases(scope_tree)
113
+
114
+ def _remove_assignment_statement(self, assignment_node):
115
+ """Remove the ExpressionStatement containing the given assignment expression."""
116
+
117
+ def enter(node, parent, key, index):
118
+ if node.get('type') == 'ExpressionStatement' and node.get('expression') is assignment_node:
119
+ self.set_changed()
120
+ return REMOVE
121
+
122
+ traverse(self.ast, {'enter': enter})
123
+
124
+ def _process_assignment_aliases(self, scope):
125
+ for name, binding in list(scope.bindings.items()):
126
+ if binding.is_constant or binding.kind == 'param':
127
+ continue
128
+
129
+ node = binding.node
130
+ if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
131
+ continue
132
+
133
+ # Must be declared without init: `var x;`
134
+ if node.get('init') is not None:
135
+ continue
136
+
137
+ # Look for exactly one write (assignment) in references
138
+ writes = []
139
+ reads = []
140
+ for reference_node, reference_parent, reference_key, reference_index in binding.references:
141
+ if (
142
+ reference_parent
143
+ and reference_parent.get('type') == 'AssignmentExpression'
144
+ and reference_key == 'left'
145
+ ):
146
+ writes.append((reference_node, reference_parent, reference_key, reference_index))
147
+ else:
148
+ reads.append((reference_node, reference_parent, reference_key, reference_index))
149
+
150
+ if len(writes) != 1:
151
+ continue
152
+
153
+ # The single write must be: x = <identifier>
154
+ _, write_parent, _, _ = writes[0]
155
+ right_hand_side = write_parent.get('right')
156
+ if not right_hand_side or not is_identifier(right_hand_side):
157
+ continue
158
+
159
+ target_name = right_hand_side['name']
160
+ if target_name == name:
161
+ continue
162
+
163
+ # The target must be constant or a well-known global
164
+ target_binding = scope.get_binding(target_name)
165
+ if target_binding:
166
+ if not target_binding.is_constant:
167
+ continue
168
+ elif target_name not in self._WELL_KNOWN_GLOBALS:
169
+ continue
170
+
171
+ # Replace all reads of `name` with `target_name`
172
+ for reference_node, reference_parent, reference_key, reference_index in reads:
173
+ new_id = {'type': 'Identifier', 'name': target_name}
174
+ if reference_index is not None:
175
+ reference_parent[reference_key][reference_index] = new_id
176
+ else:
177
+ reference_parent[reference_key] = new_id
178
+ self.set_changed()
179
+
180
+ self._remove_assignment_statement(write_parent)
181
+
182
+ for child in scope.children:
183
+ self._process_assignment_aliases(child)
@@ -0,0 +1,215 @@
1
+ """Split sequence expressions into individual statements.
2
+
3
+ Converts: (a(), b(), c()) in statement position → a(); b(); c();
4
+ Also splits multi-declarator var statements: var a = 1, b = 2 → var a = 1; var b = 2;
5
+ Also normalizes loop/if bodies to block statements.
6
+ """
7
+
8
+ from ..traverser import traverse
9
+ from ..utils.ast_helpers import make_block_statement
10
+ from ..utils.ast_helpers import make_expression_statement
11
+ from .base import Transform
12
+
13
+
14
+ class SequenceSplitter(Transform):
15
+ """Split sequence expressions and normalize control flow bodies."""
16
+
17
+ def execute(self):
18
+ self._normalize_bodies(self.ast)
19
+ self._split_in_body_arrays(self.ast)
20
+ return self.has_changed()
21
+
22
+ def _normalize_bodies(self, ast):
23
+ """Ensure if/while/for bodies are BlockStatements."""
24
+
25
+ def enter(node, parent, key, index):
26
+ node_type = node.get('type', '')
27
+ if node_type not in (
28
+ 'IfStatement',
29
+ 'WhileStatement',
30
+ 'DoWhileStatement',
31
+ 'ForStatement',
32
+ 'ForInStatement',
33
+ 'ForOfStatement',
34
+ ):
35
+ return
36
+ body = node.get('body')
37
+ if body and body.get('type') != 'BlockStatement':
38
+ node['body'] = make_block_statement([body])
39
+ self.set_changed()
40
+ if node_type == 'IfStatement':
41
+ self._normalize_if_branches(node)
42
+
43
+ traverse(ast, {'enter': enter})
44
+
45
+ def _normalize_if_branches(self, node):
46
+ """Wrap non-block consequent/alternate of IfStatement in BlockStatements."""
47
+ consequent = node.get('consequent')
48
+ if consequent and consequent.get('type') != 'BlockStatement':
49
+ node['consequent'] = make_block_statement([consequent])
50
+ self.set_changed()
51
+ alternate = node.get('alternate')
52
+ if alternate and alternate.get('type') not in ('BlockStatement', 'IfStatement', None):
53
+ node['alternate'] = make_block_statement([alternate])
54
+ self.set_changed()
55
+
56
+ def _split_in_body_arrays(self, node):
57
+ """Find all arrays that contain statements and split sequences + var decls in them."""
58
+ if not isinstance(node, dict):
59
+ return
60
+ for key, child in node.items():
61
+ if isinstance(child, list):
62
+ # Check if this looks like a statement array
63
+ if child and isinstance(child[0], dict) and 'type' in child[0]:
64
+ self._process_stmt_array(child)
65
+ # Recurse into items
66
+ for item in child:
67
+ self._split_in_body_arrays(item)
68
+ elif isinstance(child, dict) and 'type' in child:
69
+ self._split_in_body_arrays(child)
70
+
71
+ def _extract_indirect_call_prefixes(self, stmt):
72
+ """Extract dead prefix expressions from (0, fn)(args) patterns.
73
+
74
+ Only extracts from:
75
+ - Direct expression of ExpressionStatement
76
+ - Direct init of VariableDeclarator
77
+ - Argument of AwaitExpression in those positions
78
+ - Argument of ReturnStatement
79
+ """
80
+ prefixes = []
81
+
82
+ def extract_from_call(node):
83
+ """If node is a CallExpression with SequenceExpression callee, extract prefixes."""
84
+ if not isinstance(node, dict):
85
+ return
86
+ target = node
87
+ if target.get('type') == 'AwaitExpression' and isinstance(target.get('argument'), dict):
88
+ target = target['argument']
89
+ if target.get('type') != 'CallExpression':
90
+ return
91
+ callee = target.get('callee')
92
+ if not isinstance(callee, dict) or callee.get('type') != 'SequenceExpression':
93
+ return
94
+ exprs = callee.get('expressions', [])
95
+ if len(exprs) <= 1:
96
+ return
97
+ prefixes.extend(exprs[:-1])
98
+ target['callee'] = exprs[-1]
99
+
100
+ stype = stmt.get('type', '')
101
+ if stype == 'ExpressionStatement':
102
+ extract_from_call(stmt.get('expression'))
103
+ elif stype == 'VariableDeclaration':
104
+ for d in stmt.get('declarations', []):
105
+ extract_from_call(d.get('init'))
106
+ elif stype == 'ReturnStatement':
107
+ extract_from_call(stmt.get('argument'))
108
+ # Also check assignment expressions in ExpressionStatements:
109
+ # x = (0, fn)(args)
110
+ if stype == 'ExpressionStatement':
111
+ expr = stmt.get('expression')
112
+ if isinstance(expr, dict) and expr.get('type') == 'AssignmentExpression':
113
+ extract_from_call(expr.get('right'))
114
+
115
+ return prefixes
116
+
117
+ def _process_stmt_array(self, statements):
118
+ """Split sequence expressions and multi-var declarations in a statement array."""
119
+ i = 0
120
+ while i < len(statements):
121
+ statement = statements[i]
122
+ if not isinstance(statement, dict):
123
+ i += 1
124
+ continue
125
+
126
+ # Extract dead prefix from indirect call patterns: (0, fn)(args) → 0; fn(args);
127
+ prefixes = self._extract_indirect_call_prefixes(statement)
128
+ if prefixes:
129
+ new_stmts = [make_expression_statement(expression) for expression in prefixes]
130
+ new_stmts.append(statement)
131
+ statements[i : i + 1] = new_stmts
132
+ i += len(new_stmts)
133
+ self.set_changed()
134
+ continue
135
+
136
+ # Split SequenceExpression in ExpressionStatement
137
+ if (
138
+ statement.get('type') == 'ExpressionStatement'
139
+ and isinstance(statement.get('expression'), dict)
140
+ and statement['expression'].get('type') == 'SequenceExpression'
141
+ ):
142
+ expressions = statement['expression'].get('expressions', [])
143
+ if len(expressions) > 1:
144
+ new_stmts = [make_expression_statement(expression) for expression in expressions]
145
+ statements[i : i + 1] = new_stmts
146
+ i += len(new_stmts)
147
+ self.set_changed()
148
+ continue
149
+
150
+ # Split multi-declarator VariableDeclaration
151
+ # (but not inside for-loop init — those aren't in body arrays)
152
+ if statement.get('type') == 'VariableDeclaration':
153
+ decls = statement.get('declarations', [])
154
+ if len(decls) > 1:
155
+ kind = statement.get('kind', 'var')
156
+ new_stmts = [
157
+ {
158
+ 'type': 'VariableDeclaration',
159
+ 'kind': kind,
160
+ 'declarations': [declaration],
161
+ }
162
+ for declaration in decls
163
+ ]
164
+ statements[i : i + 1] = new_stmts
165
+ i += len(new_stmts)
166
+ self.set_changed()
167
+ continue
168
+
169
+ # Split SequenceExpression in single declarator init
170
+ if len(decls) == 1:
171
+ split_result = self._try_split_single_declarator_init(statement, decls[0])
172
+ if split_result:
173
+ statements[i : i + 1] = split_result
174
+ i += len(split_result)
175
+ self.set_changed()
176
+ continue
177
+
178
+ i += 1
179
+
180
+ @staticmethod
181
+ def _try_split_single_declarator_init(stmt, declarator):
182
+ """Split SequenceExpression from a single VariableDeclarator init.
183
+
184
+ Handles both direct sequences and sequences inside AwaitExpression.
185
+ Returns a list of replacement statements, or None.
186
+ """
187
+ init = declarator.get('init')
188
+ if not isinstance(init, dict):
189
+ return None
190
+
191
+ # Direct: const x = (a, b, expr()) → a; b; const x = expr();
192
+ if init.get('type') == 'SequenceExpression':
193
+ exprs = init.get('expressions', [])
194
+ if len(exprs) <= 1:
195
+ return None
196
+ prefix = [make_expression_statement(e) for e in exprs[:-1]]
197
+ declarator['init'] = exprs[-1]
198
+ prefix.append(stmt)
199
+ return prefix
200
+
201
+ # Await-wrapped: var x = await (a, b, expr()) → a; b; var x = await expr();
202
+ if (
203
+ init.get('type') == 'AwaitExpression'
204
+ and isinstance(init.get('argument'), dict)
205
+ and init['argument'].get('type') == 'SequenceExpression'
206
+ ):
207
+ exprs = init['argument'].get('expressions', [])
208
+ if len(exprs) <= 1:
209
+ return None
210
+ prefix = [make_expression_statement(e) for e in exprs[:-1]]
211
+ init['argument'] = exprs[-1]
212
+ prefix.append(stmt)
213
+ return prefix
214
+
215
+ return None