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,173 @@
1
+ """Convert logical expressions in statement position to if-statements.
2
+
3
+ Converts:
4
+ a && b() → if (a) { b(); }
5
+ a || b() → if (!a) { b(); }
6
+ a && (b(), c()) → if (a) { b(); c(); }
7
+ return a || 0, b(), c → if (!a) { 0; b(); } return c;
8
+ return await x(), y → await x(); return y;
9
+ """
10
+
11
+ from ..utils.ast_helpers import make_block_statement
12
+ from ..utils.ast_helpers import make_expression_statement
13
+ from .base import Transform
14
+
15
+
16
+ def _negate(expr):
17
+ """Wrap an expression in a logical NOT."""
18
+ return {
19
+ 'type': 'UnaryExpression',
20
+ 'operator': '!',
21
+ 'prefix': True,
22
+ 'argument': expr,
23
+ }
24
+
25
+
26
+ class LogicalToIf(Transform):
27
+ """Convert logical/comma expressions in statement position to if-statements."""
28
+
29
+ def execute(self):
30
+ self._transform_bodies(self.ast)
31
+ return self.has_changed()
32
+
33
+ def _transform_bodies(self, node):
34
+ """Walk all statement arrays and apply transforms."""
35
+ if not isinstance(node, dict):
36
+ return
37
+ for key, child in node.items():
38
+ if isinstance(child, list):
39
+ if child and isinstance(child[0], dict) and 'type' in child[0]:
40
+ self._process_stmt_array(child)
41
+ for item in child:
42
+ self._transform_bodies(item)
43
+ elif isinstance(child, dict) and 'type' in child:
44
+ self._transform_bodies(child)
45
+
46
+ def _process_stmt_array(self, stmts):
47
+ i = 0
48
+ while i < len(stmts):
49
+ stmt = stmts[i]
50
+ if not isinstance(stmt, dict):
51
+ i += 1
52
+ continue
53
+
54
+ replacement = self._try_convert_stmt(stmt)
55
+ if replacement is not None:
56
+ stmts[i : i + 1] = replacement
57
+ self.set_changed()
58
+ i += len(replacement)
59
+ continue
60
+
61
+ i += 1
62
+
63
+ def _try_convert_stmt(self, stmt):
64
+ """Try to convert a statement. Returns replacement list or None."""
65
+ match stmt.get('type'):
66
+ case 'ExpressionStatement':
67
+ return self._handle_expression_stmt(stmt)
68
+ case 'ReturnStatement':
69
+ return self._handle_return_stmt(stmt)
70
+ return None
71
+
72
+ def _handle_expression_stmt(self, stmt):
73
+ """Handle ExpressionStatement with logical or conditional."""
74
+ expression = stmt.get('expression')
75
+ if not isinstance(expression, dict):
76
+ return None
77
+ match expression.get('type'):
78
+ case 'LogicalExpression':
79
+ return self._logical_to_if(expression)
80
+ case 'ConditionalExpression':
81
+ return self._ternary_to_if(expression)
82
+ return None
83
+
84
+ def _handle_return_stmt(self, stmt):
85
+ """Handle ReturnStatement with sequence or logical expressions."""
86
+ argument = stmt.get('argument')
87
+ if not isinstance(argument, dict):
88
+ return None
89
+
90
+ # return a, b, c → a; b; return c;
91
+ if argument.get('type') == 'SequenceExpression':
92
+ return self._split_return_sequence(argument)
93
+
94
+ # return a || (b(), c) → if (!a) { b(); } return c;
95
+ if argument.get('type') == 'LogicalExpression':
96
+ return self._split_return_logical(argument)
97
+
98
+ return None
99
+
100
+ def _split_return_sequence(self, seq):
101
+ """Split return (a, b, c) into a; b; return c."""
102
+ exprs = seq.get('expressions', [])
103
+ if len(exprs) <= 1:
104
+ return None
105
+ new_stmts = []
106
+ for expression in exprs[:-1]:
107
+ if isinstance(expression, dict) and expression.get('type') == 'LogicalExpression':
108
+ converted = self._logical_to_if(expression)
109
+ if converted:
110
+ new_stmts.extend(converted)
111
+ continue
112
+ new_stmts.append(make_expression_statement(expression))
113
+ new_stmts.append({'type': 'ReturnStatement', 'argument': exprs[-1]})
114
+ return new_stmts
115
+
116
+ def _split_return_logical(self, logical):
117
+ """Split return a || (b(), c) into if (!a) { b(); } return c."""
118
+ right = logical.get('right')
119
+ if not (isinstance(right, dict) and right.get('type') == 'SequenceExpression'):
120
+ return None
121
+ exprs = right.get('expressions', [])
122
+ if len(exprs) <= 1:
123
+ return None
124
+
125
+ test = logical.get('left')
126
+ if logical.get('operator') == '||':
127
+ test = _negate(test)
128
+
129
+ body_stmts = [make_expression_statement(e) for e in exprs[:-1]]
130
+ if_stmt = {
131
+ 'type': 'IfStatement',
132
+ 'test': test,
133
+ 'consequent': make_block_statement(body_stmts),
134
+ 'alternate': None,
135
+ }
136
+ ret = {'type': 'ReturnStatement', 'argument': exprs[-1]}
137
+ return [if_stmt, ret]
138
+
139
+ def _logical_to_if(self, expr):
140
+ """Convert a LogicalExpression to if-statement(s). Returns list of stmts or None."""
141
+ left = expr.get('left')
142
+ match expr.get('operator'):
143
+ case '&&':
144
+ test = left
145
+ case '||':
146
+ test = _negate(left)
147
+ case _:
148
+ return None
149
+
150
+ body_stmts = self._expr_to_stmts(expr.get('right'))
151
+ if_stmt = {
152
+ 'type': 'IfStatement',
153
+ 'test': test,
154
+ 'consequent': make_block_statement(body_stmts),
155
+ 'alternate': None,
156
+ }
157
+ return [if_stmt]
158
+
159
+ def _ternary_to_if(self, expr):
160
+ """Convert a ConditionalExpression to if-else. Returns list of stmts or None."""
161
+ if_stmt = {
162
+ 'type': 'IfStatement',
163
+ 'test': expr.get('test'),
164
+ 'consequent': make_block_statement(self._expr_to_stmts(expr.get('consequent'))),
165
+ 'alternate': make_block_statement(self._expr_to_stmts(expr.get('alternate'))),
166
+ }
167
+ return [if_stmt]
168
+
169
+ def _expr_to_stmts(self, expr):
170
+ """Convert an expression to a list of statements."""
171
+ if isinstance(expr, dict) and expr.get('type') == 'SequenceExpression':
172
+ return [make_expression_statement(e) for e in expr.get('expressions', [])]
173
+ return [make_expression_statement(expr)]
@@ -0,0 +1,133 @@
1
+ """Consolidate sequential obj.x = ... assignments into object literal.
2
+
3
+ Detects: var o = {}; o.x = 1; o.y = 2;
4
+ Replaces: var o = {x: 1, y: 2};
5
+ """
6
+
7
+ from ..utils.ast_helpers import get_child_keys
8
+ from ..utils.ast_helpers import is_identifier
9
+ from .base import Transform
10
+
11
+
12
+ class ObjectPacker(Transform):
13
+ """Pack sequential property assignments into object initializers."""
14
+
15
+ def execute(self):
16
+ self._process_bodies(self.ast)
17
+ return self.has_changed()
18
+
19
+ def _process_bodies(self, node):
20
+ """Recursively find body arrays and try packing."""
21
+ if not isinstance(node, dict):
22
+ return
23
+ for key, child in node.items():
24
+ if isinstance(child, list):
25
+ if child and isinstance(child[0], dict) and 'type' in child[0]:
26
+ self._try_pack_body(child)
27
+ for item in child:
28
+ self._process_bodies(item)
29
+ elif isinstance(child, dict) and 'type' in child:
30
+ self._process_bodies(child)
31
+
32
+ @staticmethod
33
+ def _find_empty_object_declaration(stmt):
34
+ """Find an empty object literal in a VariableDeclaration.
35
+
36
+ Returns (name, declarator, object_expression) or None.
37
+ """
38
+ if stmt.get('type') != 'VariableDeclaration':
39
+ return None
40
+ for declaration in stmt.get('declarations', []):
41
+ initializer = declaration.get('init')
42
+ if (
43
+ initializer
44
+ and initializer.get('type') == 'ObjectExpression'
45
+ and len(initializer.get('properties', [])) == 0
46
+ and declaration.get('id', {}).get('type') == 'Identifier'
47
+ ):
48
+ return declaration['id']['name'], declaration, initializer
49
+ return None
50
+
51
+ def _try_pack_body(self, body):
52
+ """Find empty object declarations followed by property assignments and pack them."""
53
+ i = 0
54
+ while i < len(body):
55
+ stmt = body[i]
56
+ if not isinstance(stmt, dict):
57
+ i += 1
58
+ continue
59
+
60
+ found = self._find_empty_object_declaration(stmt)
61
+ if not found:
62
+ i += 1
63
+ continue
64
+
65
+ obj_name, obj_decl, obj_expr = found
66
+
67
+ # Collect consecutive property assignments
68
+ assignments = []
69
+ j = i + 1
70
+ while j < len(body):
71
+ statement = body[j]
72
+ if not isinstance(statement, dict) or statement.get('type') != 'ExpressionStatement':
73
+ break
74
+ expr = statement.get('expression')
75
+ if not expr or expr.get('type') != 'AssignmentExpression' or expr.get('operator') != '=':
76
+ break
77
+ left = expr.get('left')
78
+ if not left or left.get('type') != 'MemberExpression':
79
+ break
80
+ object_reference = left.get('object')
81
+ if not is_identifier(object_reference) or object_reference.get('name') != obj_name:
82
+ break
83
+ property_node = left.get('property')
84
+ right = expr.get('right')
85
+ # Get property key
86
+ if property_node is None:
87
+ break
88
+ # Support both computed and non-computed property keys
89
+ property_key = property_node
90
+
91
+ # Don't pack self-referential assignments (o.x = o.y)
92
+ if self._references_name(right, obj_name):
93
+ break
94
+
95
+ computed = left.get('computed', False)
96
+ assignments.append((property_key, right, computed))
97
+ j += 1
98
+
99
+ if assignments:
100
+ # Pack into the object literal
101
+ for property_key, value, computed in assignments:
102
+ property_node = {
103
+ 'type': 'Property',
104
+ 'key': property_key,
105
+ 'value': value,
106
+ 'kind': 'init',
107
+ 'method': False,
108
+ 'shorthand': False,
109
+ 'computed': computed,
110
+ }
111
+ obj_expr['properties'].append(property_node)
112
+ # Remove the assignment statements
113
+ del body[i + 1 : j]
114
+ self.set_changed()
115
+
116
+ i += 1
117
+
118
+ def _references_name(self, node, name):
119
+ """Check if a node references a given identifier name."""
120
+ if not isinstance(node, dict) or 'type' not in node:
121
+ return False
122
+ if node.get('type') == 'Identifier' and node.get('name') == name:
123
+ return True
124
+ for key in get_child_keys(node):
125
+ child = node.get(key)
126
+ if child is None:
127
+ continue
128
+ if isinstance(child, list):
129
+ if any(self._references_name(item, name) for item in child):
130
+ return True
131
+ elif isinstance(child, dict) and self._references_name(child, name):
132
+ return True
133
+ return False
@@ -0,0 +1,192 @@
1
+ """Inline proxy object property accesses.
2
+
3
+ Detects: const o = {x: 1, y: "hello"}; ... o.x ... o.y ...
4
+ Replaces: ... 1 ... "hello" ...
5
+ """
6
+
7
+ from ..scope import build_scope_tree
8
+ from ..traverser import find_parent
9
+ from ..utils.ast_helpers import deep_copy
10
+ from ..utils.ast_helpers import is_literal
11
+ from ..utils.ast_helpers import is_string_literal
12
+ from ..utils.ast_helpers import replace_identifiers
13
+ from .base import Transform
14
+
15
+
16
+ class ObjectSimplifier(Transform):
17
+ """Replace proxy object property accesses with their literal values."""
18
+
19
+ rebuild_scope = True
20
+
21
+ def execute(self):
22
+ scope_tree, _ = build_scope_tree(self.ast)
23
+ self._process_scope(scope_tree)
24
+ return self.has_changed()
25
+
26
+ def _process_scope(self, scope):
27
+ for name, binding in list(scope.bindings.items()):
28
+ if not binding.is_constant:
29
+ continue
30
+ node = binding.node
31
+ if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
32
+ continue
33
+ initializer = node.get('init')
34
+ if not initializer or initializer.get('type') != 'ObjectExpression':
35
+ continue
36
+
37
+ # Build property map (only literals and simple function expressions)
38
+ properties = initializer.get('properties', [])
39
+ if not self._is_proxy_object(properties):
40
+ continue
41
+
42
+ prop_map = {}
43
+ for property_node in properties:
44
+ key = self._get_property_key(property_node)
45
+ if key is None:
46
+ continue
47
+ value = property_node.get('value')
48
+ if is_literal(value):
49
+ prop_map[key] = value
50
+ elif value and value.get('type') in (
51
+ 'FunctionExpression',
52
+ 'ArrowFunctionExpression',
53
+ ):
54
+ prop_map[key] = value
55
+
56
+ if not prop_map:
57
+ continue
58
+
59
+ if self._has_property_assignment(binding):
60
+ continue
61
+
62
+ # Replace property accesses
63
+ for reference_node, ref_parent, ref_key, ref_index in binding.references:
64
+ if not ref_parent or ref_parent.get('type') != 'MemberExpression':
65
+ continue
66
+ if ref_key != 'object':
67
+ continue
68
+
69
+ member_expression = ref_parent
70
+ property_name = self._get_member_prop_name(member_expression)
71
+ if property_name is None or property_name not in prop_map:
72
+ continue
73
+
74
+ value = prop_map[property_name]
75
+ if is_literal(value):
76
+ self._replace_node(member_expression, deep_copy(value))
77
+ self.set_changed()
78
+ continue
79
+
80
+ if value.get('type') not in (
81
+ 'FunctionExpression',
82
+ 'ArrowFunctionExpression',
83
+ ):
84
+ continue
85
+ self._try_inline_function_call(member_expression, value)
86
+
87
+ for child in scope.children:
88
+ self._process_scope(child)
89
+
90
+ def _has_property_assignment(self, binding):
91
+ """Check if any reference to the binding is a property assignment target."""
92
+ for reference_node, reference_parent, ref_key, ref_index in binding.references:
93
+ if not (reference_parent and reference_parent.get('type') == 'MemberExpression' and ref_key == 'object'):
94
+ continue
95
+ me_parent_info = find_parent(self.ast, reference_parent)
96
+ if not me_parent_info:
97
+ continue
98
+ parent, key, _ = me_parent_info
99
+ if parent and parent.get('type') == 'AssignmentExpression' and key == 'left':
100
+ return True
101
+ return False
102
+
103
+ def _try_inline_function_call(self, member_expression, function_value):
104
+ """Try to inline a function call at a MemberExpression site."""
105
+ me_parent_info = find_parent(self.ast, member_expression)
106
+ if not me_parent_info:
107
+ return
108
+ parent, key, _ = me_parent_info
109
+ if not (parent and parent.get('type') == 'CallExpression' and key == 'callee'):
110
+ return
111
+ replacement = self._inline_func(function_value, parent.get('arguments', []))
112
+ if not replacement:
113
+ return
114
+ self._replace_node(parent, replacement)
115
+ self.set_changed()
116
+
117
+ def _is_proxy_object(self, properties):
118
+ """Check if all properties are literals or simple functions."""
119
+ for p in properties:
120
+ if p.get('type') != 'Property':
121
+ return False
122
+ val = p.get('value')
123
+ if not val:
124
+ return False
125
+ if is_literal(val):
126
+ continue
127
+ if val.get('type') in ('FunctionExpression', 'ArrowFunctionExpression'):
128
+ continue
129
+ return False
130
+ return True
131
+
132
+ def _get_property_key(self, prop):
133
+ """Get the string key of a property."""
134
+ key = prop.get('key')
135
+ if not key:
136
+ return None
137
+ match key.get('type'):
138
+ case 'Identifier':
139
+ return key['name']
140
+ case 'Literal' if is_string_literal(key):
141
+ return key['value']
142
+ return None
143
+
144
+ def _get_member_prop_name(self, member_expr):
145
+ """Get property name from a member expression."""
146
+ prop = member_expr.get('property')
147
+ if not prop:
148
+ return None
149
+ if member_expr.get('computed'):
150
+ if is_string_literal(prop):
151
+ return prop['value']
152
+ return None
153
+ if prop.get('type') == 'Identifier':
154
+ return prop['name']
155
+ return None
156
+
157
+ def _replace_node(self, target, replacement):
158
+ """Replace target node in the AST."""
159
+ result = find_parent(self.ast, target)
160
+ if result:
161
+ parent, key, index = result
162
+ if index is not None:
163
+ parent[key][index] = replacement
164
+ else:
165
+ parent[key] = replacement
166
+
167
+ def _inline_func(self, func, args):
168
+ """Inline a simple function call."""
169
+ body = func.get('body')
170
+ if not body:
171
+ return None
172
+ if func.get('type') == 'ArrowFunctionExpression' and body.get('type') != 'BlockStatement':
173
+ expr = deep_copy(body)
174
+ elif body.get('type') == 'BlockStatement':
175
+ stmts = body.get('body', [])
176
+ if len(stmts) != 1 or stmts[0].get('type') != 'ReturnStatement':
177
+ return None
178
+ argument = stmts[0].get('argument')
179
+ if not argument:
180
+ return None
181
+ expr = deep_copy(argument)
182
+ else:
183
+ return None
184
+
185
+ params = func.get('params', [])
186
+ param_map = {}
187
+ for i, parameter in enumerate(params):
188
+ if parameter.get('type') == 'Identifier':
189
+ param_map[parameter['name']] = args[i] if i < len(args) else {'type': 'Identifier', 'name': 'undefined'}
190
+
191
+ replace_identifiers(expr, param_map)
192
+ return expr
@@ -0,0 +1,43 @@
1
+ """Convert computed property access to dot notation: obj["x"] -> obj.x"""
2
+
3
+ from ..traverser import traverse
4
+ from ..utils.ast_helpers import is_string_literal
5
+ from ..utils.ast_helpers import is_valid_identifier
6
+ from .base import Transform
7
+
8
+
9
+ class PropertySimplifier(Transform):
10
+ """Simplify obj["prop"] to obj.prop when prop is a valid identifier."""
11
+
12
+ def execute(self):
13
+ def enter(node, parent, key, index):
14
+ if node.get('type') != 'MemberExpression':
15
+ return
16
+ if not node.get('computed'):
17
+ return
18
+ property_node = node.get('property')
19
+ if not is_string_literal(property_node):
20
+ return
21
+ name = property_node.get('value', '')
22
+ if not is_valid_identifier(name):
23
+ return
24
+ # Convert to dot notation
25
+ node['computed'] = False
26
+ node['property'] = {'type': 'Identifier', 'name': name}
27
+ self.set_changed()
28
+
29
+ traverse(self.ast, {'enter': enter})
30
+
31
+ # Also simplify computed property keys in object literals
32
+ def enter_obj(node, parent, key, index):
33
+ if node.get('type') != 'Property':
34
+ return
35
+ if not node.get('computed'):
36
+ # Check string literal keys
37
+ key_node = node.get('key')
38
+ if is_string_literal(key_node) and is_valid_identifier(key_node.get('value', '')):
39
+ node['key'] = {'type': 'Identifier', 'name': key_node['value']}
40
+ self.set_changed()
41
+
42
+ traverse(self.ast, {'enter': enter_obj})
43
+ return self.has_changed()