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,111 @@
1
+ """Remove unreferenced variables."""
2
+
3
+ from ..scope import build_scope_tree
4
+ from ..traverser import REMOVE
5
+ from ..traverser import traverse
6
+ from ..utils.ast_helpers import get_child_keys
7
+ from .base import Transform
8
+
9
+
10
+ _SIDE_EFFECT_TYPES = frozenset(
11
+ {
12
+ 'CallExpression',
13
+ 'NewExpression',
14
+ 'AssignmentExpression',
15
+ 'UpdateExpression',
16
+ }
17
+ )
18
+ _PURE_TYPES = frozenset(
19
+ {
20
+ 'Literal',
21
+ 'Identifier',
22
+ 'ThisExpression',
23
+ 'ArrayExpression',
24
+ 'ObjectExpression',
25
+ 'FunctionExpression',
26
+ 'ArrowFunctionExpression',
27
+ }
28
+ )
29
+
30
+
31
+ class UnusedVariableRemover(Transform):
32
+ """Remove variables with 0 references after other transforms."""
33
+
34
+ rebuild_scope = True
35
+
36
+ def execute(self):
37
+ scope_tree, _ = build_scope_tree(self.ast)
38
+ declarators_to_remove = set()
39
+ functions_to_remove = set()
40
+ self._collect_unused(scope_tree, declarators_to_remove, functions_to_remove)
41
+ if not declarators_to_remove and not functions_to_remove:
42
+ return False
43
+ self._batch_remove(declarators_to_remove, functions_to_remove)
44
+ return self.has_changed()
45
+
46
+ def _collect_unused(self, scope, declarators, functions):
47
+ skip_global = scope.parent is None
48
+
49
+ for name, binding in scope.bindings.items():
50
+ if binding.references or binding.kind == 'param':
51
+ continue
52
+ if skip_global and not name.startswith('_0x'):
53
+ continue
54
+ node = binding.node
55
+ if not isinstance(node, dict):
56
+ continue
57
+
58
+ node_type = node.get('type')
59
+ if node_type == 'VariableDeclarator':
60
+ init = node.get('init')
61
+ if not init or not self._has_side_effects(init):
62
+ declarators.add(id(node))
63
+ elif node_type == 'FunctionDeclaration':
64
+ functions.add(id(node))
65
+
66
+ for child in scope.children:
67
+ self._collect_unused(child, declarators, functions)
68
+
69
+ def _batch_remove(self, declarators_to_remove, functions_to_remove):
70
+ """Remove all collected unused declarations in a single traversal."""
71
+
72
+ def enter(node, parent, key, index):
73
+ node_type = node.get('type')
74
+ if node_type == 'FunctionDeclaration' and id(node) in functions_to_remove:
75
+ self.set_changed()
76
+ return REMOVE
77
+ if node_type != 'VariableDeclaration':
78
+ return
79
+ decls = node.get('declarations')
80
+ if not decls:
81
+ return
82
+ new_decls = [d for d in decls if id(d) not in declarators_to_remove]
83
+ if len(new_decls) == len(decls):
84
+ return
85
+ self.set_changed()
86
+ if not new_decls:
87
+ return REMOVE
88
+ node['declarations'] = new_decls
89
+
90
+ traverse(self.ast, {'enter': enter})
91
+
92
+ def _has_side_effects(self, node):
93
+ """Conservative check for side effects in an expression."""
94
+ if not isinstance(node, dict):
95
+ return False
96
+ node_type = node.get('type', '')
97
+ if node_type in _SIDE_EFFECT_TYPES:
98
+ return True
99
+ if node_type in _PURE_TYPES:
100
+ return False
101
+ # For binary/unary/etc, recurse into children
102
+ for key in get_child_keys(node):
103
+ child = node.get(key)
104
+ if child is None:
105
+ continue
106
+ if isinstance(child, list):
107
+ if any(isinstance(item, dict) and self._has_side_effects(item) for item in child):
108
+ return True
109
+ elif isinstance(child, dict) and self._has_side_effects(child):
110
+ return True
111
+ return False
pyjsclear/traverser.py ADDED
@@ -0,0 +1,195 @@
1
+ """ESTree AST traversal with visitor pattern."""
2
+
3
+ from .utils.ast_helpers import _CHILD_KEYS
4
+ from .utils.ast_helpers import get_child_keys
5
+
6
+
7
+ # Sentinel to signal node removal
8
+ REMOVE = object()
9
+ # Sentinel to skip traversing children
10
+ SKIP = object()
11
+
12
+ # Local aliases for hot-path performance (~15% faster traversal)
13
+ _dict = dict
14
+ _list = list
15
+ _isinstance = isinstance
16
+
17
+
18
+ def traverse(node, visitor):
19
+ """Traverse an ESTree AST calling visitor callbacks.
20
+
21
+ visitor should be a dict or object with optional 'enter' and 'exit' callables.
22
+ Each callback receives (node, parent, key, index) and can return:
23
+ - None: continue normally
24
+ - REMOVE: remove this node from parent
25
+ - SKIP: (enter only) skip traversing children
26
+ - a dict (node): replace this node with the returned node
27
+ """
28
+ if _isinstance(visitor, _dict):
29
+ enter_fn = visitor.get('enter')
30
+ exit_fn = visitor.get('exit')
31
+ else:
32
+ enter_fn = getattr(visitor, 'enter', None)
33
+ exit_fn = getattr(visitor, 'exit', None)
34
+
35
+ child_keys_map = _CHILD_KEYS
36
+ _REMOVE = REMOVE
37
+ _SKIP = SKIP
38
+
39
+ def _visit(current_node, parent, key, index):
40
+ node_type = current_node.get('type')
41
+ if node_type is None:
42
+ return current_node
43
+
44
+ # Enter
45
+ if enter_fn:
46
+ result = enter_fn(current_node, parent, key, index)
47
+ if result is _REMOVE:
48
+ return _REMOVE
49
+ if result is _SKIP:
50
+ if not exit_fn:
51
+ return current_node
52
+ exit_result = exit_fn(current_node, parent, key, index)
53
+ if exit_result is _REMOVE:
54
+ return _REMOVE
55
+ if _isinstance(exit_result, _dict) and 'type' in exit_result:
56
+ return exit_result
57
+ return current_node
58
+ if _isinstance(result, _dict) and 'type' in result:
59
+ current_node = result
60
+ if parent is not None:
61
+ if index is not None:
62
+ parent[key][index] = current_node
63
+ else:
64
+ parent[key] = current_node
65
+
66
+ # Visit children
67
+ child_keys = child_keys_map.get(current_node.get('type'))
68
+ if child_keys is None:
69
+ child_keys = get_child_keys(current_node)
70
+ for child_key in child_keys:
71
+ child = current_node.get(child_key)
72
+ if child is None:
73
+ continue
74
+ if _isinstance(child, _list):
75
+ i = 0
76
+ while i < len(child):
77
+ item = child[i]
78
+ if _isinstance(item, _dict) and 'type' in item:
79
+ result = _visit(item, current_node, child_key, i)
80
+ if result is _REMOVE:
81
+ child.pop(i)
82
+ continue
83
+ elif result is not item:
84
+ child[i] = result
85
+ i += 1
86
+ elif _isinstance(child, _dict) and 'type' in child:
87
+ result = _visit(child, current_node, child_key, None)
88
+ if result is _REMOVE:
89
+ current_node[child_key] = None
90
+ elif result is not child:
91
+ current_node[child_key] = result
92
+
93
+ # Exit
94
+ if exit_fn:
95
+ result = exit_fn(current_node, parent, key, index)
96
+ if result is _REMOVE:
97
+ return _REMOVE
98
+ if _isinstance(result, _dict) and 'type' in result:
99
+ return result
100
+
101
+ return current_node
102
+
103
+ _visit(node, None, None, None)
104
+
105
+
106
+ def simple_traverse(node, callback):
107
+ """Simple traversal that calls callback(node, parent) for every node.
108
+ No replacement support - just visiting.
109
+ """
110
+ child_keys_map = _CHILD_KEYS
111
+
112
+ def _visit(current_node, parent):
113
+ node_type = current_node.get('type')
114
+ if node_type is None:
115
+ return
116
+ callback(current_node, parent)
117
+ child_keys = child_keys_map.get(node_type)
118
+ if child_keys is None:
119
+ child_keys = get_child_keys(current_node)
120
+ for key in child_keys:
121
+ child = current_node.get(key)
122
+ if child is None:
123
+ continue
124
+ if _isinstance(child, _list):
125
+ for item in child:
126
+ if _isinstance(item, _dict) and 'type' in item:
127
+ _visit(item, current_node)
128
+ elif _isinstance(child, _dict) and 'type' in child:
129
+ _visit(child, current_node)
130
+
131
+ _visit(node, None)
132
+
133
+
134
+ def collect_nodes(ast, node_type):
135
+ """Collect all nodes of a given type."""
136
+ result = []
137
+
138
+ def cb(node, parent):
139
+ if node.get('type') == node_type:
140
+ result.append(node)
141
+
142
+ simple_traverse(ast, cb)
143
+ return result
144
+
145
+
146
+ class _FoundParent(Exception):
147
+ """Raised to short-circuit find_parent search."""
148
+
149
+ __slots__ = ('value',)
150
+
151
+ def __init__(self, value):
152
+ self.value = value
153
+
154
+
155
+ def find_parent(ast, target_node):
156
+ """Find the parent of a node in the AST. Returns (parent, key, index) or None."""
157
+
158
+ def _visit(node):
159
+ if not isinstance(node, dict) or 'type' not in node:
160
+ return
161
+ for child_key in get_child_keys(node):
162
+ child = node.get(child_key)
163
+ if child is None:
164
+ continue
165
+ if isinstance(child, list):
166
+ for i, item in enumerate(child):
167
+ if item is target_node:
168
+ raise _FoundParent((node, child_key, i))
169
+ _visit(item)
170
+ elif isinstance(child, dict):
171
+ if child is target_node:
172
+ raise _FoundParent((node, child_key, None))
173
+ _visit(child)
174
+
175
+ try:
176
+ _visit(ast)
177
+ except _FoundParent as found:
178
+ return found.value
179
+ return None
180
+
181
+
182
+ def replace_in_parent(parent, key, index, new_node):
183
+ """Replace a node within its parent."""
184
+ if index is not None:
185
+ parent[key][index] = new_node
186
+ else:
187
+ parent[key] = new_node
188
+
189
+
190
+ def remove_from_parent(parent, key, index):
191
+ """Remove a node from its parent."""
192
+ if index is not None:
193
+ parent[key].pop(index)
194
+ else:
195
+ parent[key] = None
File without changes
@@ -0,0 +1,257 @@
1
+ """AST helper utilities for ESTree nodes."""
2
+
3
+ import copy
4
+ import re
5
+
6
+
7
+ def deep_copy(node):
8
+ """Deep copy an AST node."""
9
+ return copy.deepcopy(node)
10
+
11
+
12
+ def is_literal(node):
13
+ """Check if node is a Literal."""
14
+ return isinstance(node, dict) and node.get('type') == 'Literal'
15
+
16
+
17
+ def is_identifier(node):
18
+ """Check if node is an Identifier."""
19
+ return isinstance(node, dict) and node.get('type') == 'Identifier'
20
+
21
+
22
+ def is_string_literal(node):
23
+ """Check if node is a string Literal."""
24
+ return is_literal(node) and isinstance(node.get('value'), str)
25
+
26
+
27
+ def is_numeric_literal(node):
28
+ """Check if node is a numeric Literal."""
29
+ return is_literal(node) and isinstance(node.get('value'), (int, float))
30
+
31
+
32
+ def is_boolean_literal(node):
33
+ """Check if node is a boolean-ish literal (true/false or !0/!1)."""
34
+ return is_literal(node) and isinstance(node.get('value'), bool)
35
+
36
+
37
+ def is_null_literal(node):
38
+ """Check if node is null literal."""
39
+ return is_literal(node) and node.get('value') is None and node.get('raw') == 'null'
40
+
41
+
42
+ def is_undefined(node):
43
+ """Check if node represents undefined."""
44
+ return is_identifier(node) and node.get('name') == 'undefined'
45
+
46
+
47
+ def get_literal_value(node):
48
+ """Extract the value from a literal node. Returns (value, True) or (None, False)."""
49
+ if not is_literal(node):
50
+ return None, False
51
+ return node.get('value'), True
52
+
53
+
54
+ def make_literal(value, raw=None):
55
+ """Create a Literal AST node."""
56
+ if raw is not None:
57
+ return {'type': 'Literal', 'value': value, 'raw': raw}
58
+
59
+ match value:
60
+ case str():
61
+ escaped = value.replace('\\', '\\\\')
62
+ escaped = escaped.replace('"', '\\"')
63
+ escaped = escaped.replace('\n', '\\n')
64
+ escaped = escaped.replace('\r', '\\r')
65
+ escaped = escaped.replace('\t', '\\t')
66
+ escaped = escaped.replace('\0', '\\0')
67
+ raw = f'"{escaped}"'
68
+ case bool():
69
+ raw = 'true' if value else 'false'
70
+ case int() | float():
71
+ if isinstance(value, float) and value == int(value) and not (value == 0 and str(value).startswith('-')):
72
+ raw = str(int(value))
73
+ else:
74
+ raw = str(value)
75
+ case None:
76
+ raw = 'null'
77
+ case _:
78
+ raw = str(value)
79
+ return {'type': 'Literal', 'value': value, 'raw': raw}
80
+
81
+
82
+ def make_identifier(name):
83
+ """Create an Identifier AST node."""
84
+ return {'type': 'Identifier', 'name': name}
85
+
86
+
87
+ def make_expression_statement(expr):
88
+ """Wrap an expression in an ExpressionStatement."""
89
+ return {'type': 'ExpressionStatement', 'expression': expr}
90
+
91
+
92
+ def make_block_statement(body):
93
+ """Create a BlockStatement."""
94
+ return {'type': 'BlockStatement', 'body': body}
95
+
96
+
97
+ def make_var_declaration(name, init=None, kind='var'):
98
+ """Create a VariableDeclaration with a single declarator."""
99
+ return {
100
+ 'type': 'VariableDeclaration',
101
+ 'declarations': [{'type': 'VariableDeclarator', 'id': make_identifier(name), 'init': init}],
102
+ 'kind': kind,
103
+ }
104
+
105
+
106
+ _IDENT_RE = re.compile(r'^[a-zA-Z_$][a-zA-Z0-9_$]*$')
107
+
108
+
109
+ def is_valid_identifier(name):
110
+ """Check if a string is a valid JS identifier (for obj.prop access)."""
111
+ if not isinstance(name, str) or not name:
112
+ return False
113
+ return bool(_IDENT_RE.match(name))
114
+
115
+
116
+ _CHILD_KEYS = {
117
+ 'Program': ('body',),
118
+ 'ExpressionStatement': ('expression',),
119
+ 'BlockStatement': ('body',),
120
+ 'VariableDeclaration': ('declarations',),
121
+ 'VariableDeclarator': ('id', 'init'),
122
+ 'FunctionDeclaration': ('id', 'params', 'body'),
123
+ 'FunctionExpression': ('id', 'params', 'body'),
124
+ 'ArrowFunctionExpression': ('params', 'body'),
125
+ 'ReturnStatement': ('argument',),
126
+ 'IfStatement': ('test', 'consequent', 'alternate'),
127
+ 'WhileStatement': ('test', 'body'),
128
+ 'DoWhileStatement': ('test', 'body'),
129
+ 'ForStatement': ('init', 'test', 'update', 'body'),
130
+ 'ForInStatement': ('left', 'right', 'body'),
131
+ 'ForOfStatement': ('left', 'right', 'body'),
132
+ 'SwitchStatement': ('discriminant', 'cases'),
133
+ 'SwitchCase': ('test', 'consequent'),
134
+ 'BreakStatement': ('label',),
135
+ 'ContinueStatement': ('label',),
136
+ 'LabeledStatement': ('label', 'body'),
137
+ 'ThrowStatement': ('argument',),
138
+ 'TryStatement': ('block', 'handler', 'finalizer'),
139
+ 'CatchClause': ('param', 'body'),
140
+ 'BinaryExpression': ('left', 'right'),
141
+ 'LogicalExpression': ('left', 'right'),
142
+ 'UnaryExpression': ('argument',),
143
+ 'UpdateExpression': ('argument',),
144
+ 'AssignmentExpression': ('left', 'right'),
145
+ 'MemberExpression': ('object', 'property'),
146
+ 'CallExpression': ('callee', 'arguments'),
147
+ 'NewExpression': ('callee', 'arguments'),
148
+ 'ConditionalExpression': ('test', 'consequent', 'alternate'),
149
+ 'SequenceExpression': ('expressions',),
150
+ 'ArrayExpression': ('elements',),
151
+ 'ObjectExpression': ('properties',),
152
+ 'Property': ('key', 'value'),
153
+ 'SpreadElement': ('argument',),
154
+ 'TemplateLiteral': ('quasis', 'expressions'),
155
+ 'TaggedTemplateExpression': ('tag', 'quasi'),
156
+ 'TemplateElement': (),
157
+ 'AssignmentPattern': ('left', 'right'),
158
+ 'ArrayPattern': ('elements',),
159
+ 'ObjectPattern': ('properties',),
160
+ 'RestElement': ('argument',),
161
+ 'ClassDeclaration': ('id', 'superClass', 'body'),
162
+ 'ClassExpression': ('id', 'superClass', 'body'),
163
+ 'ClassBody': ('body',),
164
+ 'MethodDefinition': ('key', 'value'),
165
+ 'YieldExpression': ('argument',),
166
+ 'AwaitExpression': ('argument',),
167
+ 'EmptyStatement': (),
168
+ 'Literal': (),
169
+ 'Identifier': (),
170
+ 'ThisExpression': (),
171
+ }
172
+
173
+ _SKIP_KEYS = frozenset(
174
+ (
175
+ 'type',
176
+ 'raw',
177
+ 'value',
178
+ 'name',
179
+ 'operator',
180
+ 'kind',
181
+ 'computed',
182
+ 'method',
183
+ 'shorthand',
184
+ 'prefix',
185
+ 'async',
186
+ 'generator',
187
+ 'static',
188
+ 'sourceType',
189
+ 'start',
190
+ 'end',
191
+ 'loc',
192
+ 'range',
193
+ 'directive',
194
+ 'regex',
195
+ )
196
+ )
197
+
198
+
199
+ def get_child_keys(node):
200
+ """Get keys of a node that may contain child nodes/arrays."""
201
+ if not isinstance(node, dict) or 'type' not in node:
202
+ return ()
203
+ node_type = node['type']
204
+ keys = _CHILD_KEYS.get(node_type)
205
+ if keys is not None:
206
+ return keys
207
+ # Fallback: return all keys that look like they might contain nodes
208
+ return [
209
+ k
210
+ for k, v in node.items()
211
+ if k not in _SKIP_KEYS
212
+ and not (k == 'expression' and node_type != 'ExpressionStatement')
213
+ and isinstance(v, (dict, list))
214
+ ]
215
+
216
+
217
+ def replace_identifiers(node, param_map):
218
+ """Replace Identifier nodes whose names are in param_map with deep copies.
219
+
220
+ Skips non-computed property names in MemberExpressions.
221
+ """
222
+ if not isinstance(node, dict) or 'type' not in node:
223
+ return
224
+ for key in get_child_keys(node):
225
+ child = node.get(key)
226
+ if child is None:
227
+ continue
228
+ is_noncomputed_prop = key == 'property' and node.get('type') == 'MemberExpression' and not node.get('computed')
229
+ if isinstance(child, list):
230
+ for i, item in enumerate(child):
231
+ if isinstance(item, dict) and item.get('type') == 'Identifier':
232
+ if not is_noncomputed_prop and item.get('name', '') in param_map:
233
+ child[i] = copy.deepcopy(param_map[item['name']])
234
+ elif isinstance(item, dict) and 'type' in item:
235
+ replace_identifiers(item, param_map)
236
+ elif isinstance(child, dict):
237
+ if child.get('type') == 'Identifier':
238
+ if not is_noncomputed_prop and child.get('name', '') in param_map:
239
+ node[key] = copy.deepcopy(param_map[child['name']])
240
+ elif 'type' in child:
241
+ replace_identifiers(child, param_map)
242
+
243
+
244
+ def nodes_equal(a, b):
245
+ """Check if two AST nodes are structurally equal (ignoring position info)."""
246
+ if type(a) != type(b):
247
+ return False
248
+ match a:
249
+ case dict():
250
+ keys_a = {k for k in a if k not in ('start', 'end', 'loc', 'range')}
251
+ keys_b = {k for k in b if k not in ('start', 'end', 'loc', 'range')}
252
+ if keys_a != keys_b:
253
+ return False
254
+ return all(nodes_equal(a[k], b[k]) for k in keys_a)
255
+ case list():
256
+ return len(a) == len(b) and all(nodes_equal(x, y) for x, y in zip(a, b))
257
+ return a == b