pyjsclear 0.1.2__tar.gz → 0.1.3__tar.gz

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 (67) hide show
  1. pyjsclear-0.1.3/NOTICE +28 -0
  2. {pyjsclear-0.1.2/pyjsclear.egg-info → pyjsclear-0.1.3}/PKG-INFO +14 -16
  3. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/README.md +13 -15
  4. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/THIRD_PARTY_LICENSES.md +27 -0
  5. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/__init__.py +1 -1
  6. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/deobfuscator.py +84 -25
  7. pyjsclear-0.1.3/pyjsclear/transforms/aa_decode.py +150 -0
  8. pyjsclear-0.1.3/pyjsclear/transforms/jj_decode.py +718 -0
  9. pyjsclear-0.1.3/pyjsclear/transforms/jsfuck_decode.py +593 -0
  10. {pyjsclear-0.1.2 → pyjsclear-0.1.3/pyjsclear.egg-info}/PKG-INFO +14 -16
  11. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/SOURCES.txt +2 -0
  12. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/tests/test_regression.py +1 -1
  13. pyjsclear-0.1.2/NOTICE +0 -19
  14. pyjsclear-0.1.2/pyjsclear/transforms/aa_decode.py +0 -83
  15. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/LICENSE +0 -0
  16. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/MANIFEST.in +0 -0
  17. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/__main__.py +0 -0
  18. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/generator.py +0 -0
  19. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/parser.py +0 -0
  20. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/scope.py +0 -0
  21. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/__init__.py +0 -0
  22. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/anti_tamper.py +0 -0
  23. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/base.py +0 -0
  24. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/class_static_resolver.py +0 -0
  25. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/class_string_decoder.py +0 -0
  26. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/cleanup.py +0 -0
  27. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/constant_prop.py +0 -0
  28. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/control_flow.py +0 -0
  29. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_branch.py +0 -0
  30. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_class_props.py +0 -0
  31. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_expressions.py +0 -0
  32. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_object_props.py +0 -0
  33. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/else_if_flatten.py +0 -0
  34. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/enum_resolver.py +0 -0
  35. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/eval_unpack.py +0 -0
  36. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/expression_simplifier.py +0 -0
  37. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/global_alias.py +0 -0
  38. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/hex_escapes.py +0 -0
  39. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/hex_numerics.py +0 -0
  40. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/logical_to_if.py +0 -0
  41. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/member_chain_resolver.py +0 -0
  42. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/noop_calls.py +0 -0
  43. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/nullish_coalescing.py +0 -0
  44. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/object_packer.py +0 -0
  45. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/object_simplifier.py +0 -0
  46. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/optional_chaining.py +0 -0
  47. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/property_simplifier.py +0 -0
  48. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/proxy_functions.py +0 -0
  49. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/reassignment.py +0 -0
  50. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/require_inliner.py +0 -0
  51. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/sequence_splitter.py +0 -0
  52. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/single_use_vars.py +0 -0
  53. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/string_revealer.py +0 -0
  54. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/unreachable_code.py +0 -0
  55. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/unused_vars.py +0 -0
  56. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/variable_renamer.py +0 -0
  57. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/xor_string_decode.py +0 -0
  58. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/traverser.py +0 -0
  59. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/utils/__init__.py +0 -0
  60. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/utils/ast_helpers.py +0 -0
  61. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/utils/string_decoders.py +0 -0
  62. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/dependency_links.txt +0 -0
  63. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/entry_points.txt +0 -0
  64. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/requires.txt +0 -0
  65. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/top_level.txt +0 -0
  66. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyproject.toml +0 -0
  67. {pyjsclear-0.1.2 → pyjsclear-0.1.3}/setup.cfg +0 -0
pyjsclear-0.1.3/NOTICE ADDED
@@ -0,0 +1,28 @@
1
+ PyJSClear
2
+ Copyright 2026 Intezer Labs
3
+
4
+ This product is a derivative work based on the following projects:
5
+
6
+ 1. obfuscator-io-deobfuscator (v1.0.6)
7
+ Copyright Ben (ben-sb)
8
+ Licensed under the Apache License, Version 2.0
9
+ https://github.com/ben-sb/obfuscator-io-deobfuscator
10
+
11
+ 2. javascript-deobfuscator
12
+ Copyright Ben (ben-sb)
13
+ Licensed under the Apache License, Version 2.0
14
+ https://github.com/ben-sb/javascript-deobfuscator
15
+
16
+ 3. webcrack (v2.14.1)
17
+ Copyright (c) 2023 j4k0xb
18
+ Licensed under the MIT License
19
+ https://github.com/j4k0xb/webcrack
20
+
21
+ This Python library re-implements the deobfuscation algorithms and transform
22
+ logic from the above projects in pure Python. No source code was directly
23
+ copied; the implementations were written from scratch following the same
24
+ algorithmic approaches.
25
+
26
+ Test dataset: obfuscated JavaScript samples from the JSIMPLIFIER dataset
27
+ (https://zenodo.org/records/17531662) are included in tests/resources/ for
28
+ evaluation purposes only. They are not part of the distributed package.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyjsclear
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Pure Python JavaScript deobfuscator
5
5
  Author-email: Intezer Labs <info@intezer.com>
6
6
  License-Expression: Apache-2.0
@@ -82,11 +82,12 @@ pyjsclear input.js --max-iterations 20
82
82
 
83
83
  ## What it does
84
84
 
85
- PyJSClear applies ~40 transforms in a multi-pass loop until the code
86
- stabilises (default limit: 50 iterations). A final one-shot pass renames
85
+ PyJSClear applies transforms in a multi-pass loop until the code
86
+ stabilizes (default limit: 50 iterations). A final one-shot pass renames
87
87
  variables and converts var/let to const.
88
88
 
89
89
  **Capabilities:**
90
+ - Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
90
91
  - String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
91
92
  - Constant propagation & reassignment elimination
92
93
  - Dead code / dead branch / unreachable code removal
@@ -99,20 +100,11 @@ variables and converts var/let to const.
99
100
  Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
100
101
  that skips expensive transforms.
101
102
 
102
- ## Testing
103
-
104
- ```bash
105
- pytest tests/ # all tests
106
- pytest tests/test_regression.py # regression suite (62 tests across 25 samples)
107
- pytest tests/ -n auto # parallel execution (requires pytest-xdist)
108
- ```
109
-
110
103
  ## Limitations
111
104
 
112
- - **Optimised for obfuscator.io output.** Other obfuscation tools may only partially deobfuscate.
105
+ - **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
113
106
  - **Large files get reduced treatment.** Files >500 KB or ASTs >50 K nodes skip expensive transforms; files >2 MB use a minimal lite mode.
114
- - **No minification reversal.** Minified-but-not-obfuscated code won't be reformatted or beautified.
115
- - **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting.
107
+ - **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting; the deobfuscator catches this and returns the best partial result.
116
108
 
117
109
  ## License
118
110
 
@@ -120,7 +112,13 @@ Apache License 2.0 — see [LICENSE](LICENSE).
120
112
 
121
113
  This project is a derivative work based on
122
114
  [obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
123
- (Apache 2.0) and
115
+ (Apache 2.0),
124
116
  [javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
125
- (Apache 2.0). See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
117
+ (Apache 2.0), and
118
+ [webcrack](https://github.com/j4k0xb/webcrack) (MIT).
119
+ See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
126
120
  [NOTICE](NOTICE) for full attribution.
121
+
122
+ Test samples include obfuscated JavaScript from the
123
+ [JSIMPLIFIER dataset](https://zenodo.org/records/17531662) (GPL-3.0),
124
+ used solely for evaluation purposes.
@@ -56,11 +56,12 @@ pyjsclear input.js --max-iterations 20
56
56
 
57
57
  ## What it does
58
58
 
59
- PyJSClear applies ~40 transforms in a multi-pass loop until the code
60
- stabilises (default limit: 50 iterations). A final one-shot pass renames
59
+ PyJSClear applies transforms in a multi-pass loop until the code
60
+ stabilizes (default limit: 50 iterations). A final one-shot pass renames
61
61
  variables and converts var/let to const.
62
62
 
63
63
  **Capabilities:**
64
+ - Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
64
65
  - String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
65
66
  - Constant propagation & reassignment elimination
66
67
  - Dead code / dead branch / unreachable code removal
@@ -73,20 +74,11 @@ variables and converts var/let to const.
73
74
  Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
74
75
  that skips expensive transforms.
75
76
 
76
- ## Testing
77
-
78
- ```bash
79
- pytest tests/ # all tests
80
- pytest tests/test_regression.py # regression suite (62 tests across 25 samples)
81
- pytest tests/ -n auto # parallel execution (requires pytest-xdist)
82
- ```
83
-
84
77
  ## Limitations
85
78
 
86
- - **Optimised for obfuscator.io output.** Other obfuscation tools may only partially deobfuscate.
79
+ - **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
87
80
  - **Large files get reduced treatment.** Files >500 KB or ASTs >50 K nodes skip expensive transforms; files >2 MB use a minimal lite mode.
88
- - **No minification reversal.** Minified-but-not-obfuscated code won't be reformatted or beautified.
89
- - **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting.
81
+ - **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting; the deobfuscator catches this and returns the best partial result.
90
82
 
91
83
  ## License
92
84
 
@@ -94,7 +86,13 @@ Apache License 2.0 — see [LICENSE](LICENSE).
94
86
 
95
87
  This project is a derivative work based on
96
88
  [obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
97
- (Apache 2.0) and
89
+ (Apache 2.0),
98
90
  [javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
99
- (Apache 2.0). See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
91
+ (Apache 2.0), and
92
+ [webcrack](https://github.com/j4k0xb/webcrack) (MIT).
93
+ See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
100
94
  [NOTICE](NOTICE) for full attribution.
95
+
96
+ Test samples include obfuscated JavaScript from the
97
+ [JSIMPLIFIER dataset](https://zenodo.org/records/17531662) (GPL-3.0),
98
+ used solely for evaluation purposes.
@@ -237,3 +237,30 @@ https://github.com/ben-sb/javascript-deobfuscator/blob/master/LICENSE).
237
237
  **Features derived from this project:** hex escape decoding (`--he`),
238
238
  static array unpacking (`--su`), property access transformation (`--tp`).
239
239
 
240
+ ---
241
+
242
+ ## webcrack
243
+
244
+ - **Version:** 2.14.1
245
+ - **Author:** j4k0xb
246
+ - **Repository:** https://github.com/j4k0xb/webcrack
247
+ - **License:** MIT
248
+
249
+ See [licenses/LICENSE-webcrack](licenses/LICENSE-webcrack) for the full license text.
250
+
251
+ **Features derived from this project:** general deobfuscation transform
252
+ patterns and architecture reference.
253
+
254
+ ---
255
+
256
+ ## Test dataset
257
+
258
+ - **JSIMPLIFIER dataset**
259
+ - **Author:** Dongchao Zhou
260
+ - **Source:** https://zenodo.org/records/17531662
261
+ - **License:** GPL-3.0
262
+
263
+ Obfuscated JavaScript samples from this dataset are included in
264
+ `tests/resources/` for evaluation purposes only. They are not part of the
265
+ distributed package and no code from JSIMPLIFIER is incorporated into
266
+ PyJSClear.
@@ -8,7 +8,7 @@ Python package.
8
8
  from .deobfuscator import Deobfuscator
9
9
 
10
10
 
11
- __version__ = '0.1.2'
11
+ __version__ = '0.1.3'
12
12
 
13
13
 
14
14
  def deobfuscate(code, max_iterations=50):
@@ -2,8 +2,6 @@
2
2
 
3
3
  from .generator import generate
4
4
  from .parser import parse
5
- from .transforms.aa_decode import aa_decode
6
- from .transforms.aa_decode import is_aa_encoded
7
5
  from .transforms.anti_tamper import AntiTamperRemover
8
6
  from .transforms.class_static_resolver import ClassStaticResolver
9
7
  from .transforms.class_string_decoder import ClassStringDecoder
@@ -28,6 +26,12 @@ from .transforms.global_alias import GlobalAliasInliner
28
26
  from .transforms.hex_escapes import HexEscapes
29
27
  from .transforms.hex_escapes import decode_hex_escapes_source
30
28
  from .transforms.hex_numerics import HexNumerics
29
+ from .transforms.aa_decode import aa_decode
30
+ from .transforms.aa_decode import is_aa_encoded
31
+ from .transforms.jj_decode import is_jj_encoded
32
+ from .transforms.jj_decode import jj_decode
33
+ from .transforms.jsfuck_decode import is_jsfuck
34
+ from .transforms.jsfuck_decode import jsfuck_decode
31
35
  from .transforms.logical_to_if import LogicalToIf
32
36
  from .transforms.member_chain_resolver import MemberChainResolver
33
37
  from .transforms.noop_calls import NoopCallRemover
@@ -127,12 +131,24 @@ class Deobfuscator:
127
131
  Returns decoded code if an encoding/packing was detected and decoded,
128
132
  or None to continue with the normal AST pipeline.
129
133
  """
134
+ # JSFuck check (must be first — these are whole-file encodings)
135
+ if is_jsfuck(code):
136
+ decoded = jsfuck_decode(code)
137
+ if decoded:
138
+ return decoded
139
+
130
140
  # AAEncode check
131
141
  if is_aa_encoded(code):
132
142
  decoded = aa_decode(code)
133
143
  if decoded:
134
144
  return decoded
135
145
 
146
+ # JJEncode check
147
+ if is_jj_encoded(code):
148
+ decoded = jj_decode(code)
149
+ if decoded:
150
+ return decoded
151
+
136
152
  # Eval packer check
137
153
  if is_eval_packed(code):
138
154
  decoded = eval_unpack(code)
@@ -141,6 +157,9 @@ class Deobfuscator:
141
157
 
142
158
  return None
143
159
 
160
+ # Maximum number of outer re-parse cycles (generate → re-parse → re-transform)
161
+ _MAX_OUTER_CYCLES = 5
162
+
144
163
  def execute(self):
145
164
  """Run all transforms and return cleaned source."""
146
165
  code = self.original_code
@@ -162,25 +181,79 @@ class Deobfuscator:
162
181
  return decoded
163
182
  return self.original_code
164
183
 
165
- # Determine optimization mode based on code size
166
- code_size = len(code)
184
+ # Outer loop: run AST transforms until generate→re-parse converges.
185
+ # Post-passes (VariableRenamer, VarToConst, LetToConst) only run on
186
+ # the final cycle to avoid interfering with subsequent transform rounds.
187
+ previous_code = code
188
+ last_changed_ast = None
189
+ try:
190
+ for _cycle in range(self._MAX_OUTER_CYCLES):
191
+ changed = self._run_ast_transforms(
192
+ ast,
193
+ code_size=len(previous_code),
194
+ )
195
+
196
+ if not changed:
197
+ break
198
+
199
+ last_changed_ast = ast
200
+
201
+ try:
202
+ generated = generate(ast)
203
+ except Exception:
204
+ break
205
+
206
+ if generated == previous_code:
207
+ break
208
+
209
+ previous_code = generated
210
+
211
+ # Re-parse for the next cycle
212
+ try:
213
+ ast = parse(generated)
214
+ except SyntaxError:
215
+ break
216
+
217
+ # Run post-passes on the final AST (always — they're cheap and handle
218
+ # cosmetic transforms like var→const even when no main transforms fired)
219
+ any_post_changed = False
220
+ for post_transform in [VariableRenamer, VarToConst, LetToConst]:
221
+ try:
222
+ if post_transform(ast).execute():
223
+ any_post_changed = True
224
+ except Exception:
225
+ pass
226
+
227
+ if last_changed_ast is None and not any_post_changed:
228
+ return self.original_code
229
+
230
+ try:
231
+ return generate(ast)
232
+ except Exception:
233
+ return previous_code
234
+ except RecursionError:
235
+ # Safety net: esprima's parser is purely recursive with no depth
236
+ # limit, so deeply nested JS hits Python's recursion limit during
237
+ # parsing or re-parsing. Our AST walkers are cheaper per level
238
+ # but also recursive. Return best result so far.
239
+ return previous_code
240
+
241
+ def _run_ast_transforms(self, ast, code_size=0):
242
+ """Run all AST transform passes. Returns True if any transform changed the AST."""
243
+ node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
244
+
167
245
  lite_mode = code_size > _MAX_CODE_SIZE
168
246
  max_iterations = self.max_iterations
169
247
  if code_size > _LARGE_FILE_SIZE:
170
248
  max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
171
249
 
172
- # Check node count for expensive transform gating
173
- node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
174
-
175
250
  # For very large ASTs, further reduce iterations
176
251
  if node_count > 100_000:
177
252
  max_iterations = min(max_iterations, 3)
178
253
 
179
254
  # Build transform list based on mode
180
255
  transform_classes = TRANSFORM_CLASSES
181
- if lite_mode:
182
- transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
183
- elif node_count > _NODE_COUNT_LIMIT:
256
+ if lite_mode or node_count > _NODE_COUNT_LIMIT:
184
257
  transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
185
258
 
186
259
  # Track which transforms are no longer productive
@@ -210,18 +283,4 @@ class Deobfuscator:
210
283
  if not modified:
211
284
  break
212
285
 
213
- # Post-passes: cosmetic transforms that run once after convergence
214
- for post_transform in [VariableRenamer, VarToConst, LetToConst]:
215
- try:
216
- if post_transform(ast).execute():
217
- any_transform_changed = True
218
- except Exception:
219
- pass
220
-
221
- if not any_transform_changed:
222
- return self.original_code
223
-
224
- try:
225
- return generate(ast)
226
- except Exception:
227
- return self.original_code
286
+ return any_transform_changed
@@ -0,0 +1,150 @@
1
+ """Pure Python AAEncode decoder.
2
+
3
+ AAEncode (by Yosuke Hasegawa) encodes JavaScript into Japanese-style
4
+ emoticon characters using fullwidth/halfwidth katakana and special symbols.
5
+ Each source character is represented as an octal or hex escape built from
6
+ emoticon digit expressions, separated by a backslash-like marker.
7
+
8
+ This decoder performs iterative string replacements to recover the digit
9
+ sequences, then converts octal/hex values back to characters.
10
+ """
11
+
12
+ import re
13
+
14
+ # Characteristic pattern present in all AAEncoded output — the execution call.
15
+ _SIGNATURE = '\uff9f\u0414\uff9f)[\uff9f\u03b5\uff9f]'
16
+
17
+ # Separator between encoded characters (represents the escape character "\").
18
+ _SEPARATOR = '(\uff9f\u0414\uff9f)[\uff9f\u03b5\uff9f]+'
19
+
20
+ # Unicode hex marker — when present before a segment, the value is hex (\uXXXX).
21
+ # Note: real AAEncode uses U+FF70 (halfwidth katakana-hiragana prolonged sound mark ー),
22
+ # NOT U+30FC (fullwidth ー).
23
+ _UNICODE_MARKER = '(o\uff9f\uff70\uff9fo)'
24
+
25
+ # Sentinel used to track unicode marker positions after replacement.
26
+ _HEX_SENTINEL = '\x01'
27
+
28
+ # Replacement rules: longer/more specific patterns first to avoid partial matches.
29
+ # All patterns use U+FF70 (ー) to match real AAEncode output.
30
+ _REPLACEMENTS = [
31
+ ('(o\uff9f\uff70\uff9fo)', _HEX_SENTINEL),
32
+ ('((\uff9f\uff70\uff9f) + (\uff9f\uff70\uff9f) + (\uff9f\u0398\uff9f))', '5'),
33
+ ('((\uff9f\uff70\uff9f) + (\uff9f\uff70\uff9f))', '4'),
34
+ ('((\uff9f\uff70\uff9f) + (o^_^o))', '3'),
35
+ ('((\uff9f\uff70\uff9f) + (\uff9f\u0398\uff9f))', '2'),
36
+ ('((o^_^o) - (\uff9f\u0398\uff9f))', '2'),
37
+ ('((o^_^o) + (o^_^o))', '6'),
38
+ ('(\uff9f\uff70\uff9f)', '1'),
39
+ ('(\uff9f\u0398\uff9f)', '1'),
40
+ ('(c^_^o)', '0'),
41
+ ('(o^_^o)', '3'),
42
+ ]
43
+
44
+
45
+ def is_aa_encoded(code):
46
+ """Check if *code* looks like AAEncoded JavaScript.
47
+
48
+ Returns True when the characteristic execution pattern is found.
49
+ """
50
+ if not isinstance(code, str):
51
+ return False
52
+ return _SIGNATURE in code
53
+
54
+
55
+ def aa_decode(code):
56
+ """Decode AAEncoded JavaScript.
57
+
58
+ Returns the decoded source string, or ``None`` on any failure.
59
+ All processing is iterative (no recursion).
60
+ """
61
+ if not isinstance(code, str) or not is_aa_encoded(code):
62
+ return None
63
+
64
+ try:
65
+ return _decode_impl(code)
66
+ except Exception:
67
+ return None
68
+
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Internal helpers
72
+ # ---------------------------------------------------------------------------
73
+
74
+
75
+ def _decode_impl(code):
76
+ """Core decoding logic."""
77
+ # 1. Isolate the data section.
78
+ # AAEncode wraps data inside an execution pattern. The encoded payload
79
+ # is the series of segments joined by the separator, ending with a
80
+ # final execution call like (゚Д゚)['_'] or )('_');
81
+ # We look for the *first* separator occurrence and take everything from
82
+ # there up to the trailing execution wrapper.
83
+
84
+ # Find the data region: everything after the initial variable setup and
85
+ # before the trailing execution portion.
86
+ # The data starts at the first separator token.
87
+ sep_idx = code.find(_SEPARATOR)
88
+ if sep_idx == -1:
89
+ return None
90
+
91
+ # The trailing execution wrapper varies but typically looks like:
92
+ # (゚Д゚)['_'](゚Θ゚) or )('_');
93
+ # We strip from the last occurrence of (゚Д゚)['_'] onward.
94
+ tail_patterns = [
95
+ "(\uff9f\u0414\uff9f)['_']",
96
+ '(\uff9f\u0414\uff9f)["_"]',
97
+ ]
98
+ data = code[sep_idx:]
99
+ for pat in tail_patterns:
100
+ tail_pos = data.rfind(pat)
101
+ if tail_pos != -1:
102
+ data = data[:tail_pos]
103
+ break
104
+
105
+ # 2. Apply emoticon-to-digit replacements.
106
+ for old, new in _REPLACEMENTS:
107
+ data = data.replace(old, new)
108
+
109
+ # 3. Split on the separator to get individual character segments.
110
+ segments = data.split(_SEPARATOR)
111
+
112
+ # The first element is the leading separator itself (empty or noise) — skip it.
113
+ # Actually, since we started data *at* the first separator, the split
114
+ # produces an empty first element. Handle gracefully.
115
+
116
+ result_chars = []
117
+ for segment in segments:
118
+ segment = segment.strip()
119
+ if not segment:
120
+ continue
121
+
122
+ # Determine hex vs octal mode.
123
+ is_hex = _HEX_SENTINEL in segment
124
+
125
+ # Remove hex sentinel and any remaining operator/whitespace noise.
126
+ cleaned = segment.replace(_HEX_SENTINEL, '')
127
+ cleaned = cleaned.replace('+', '').replace(' ', '').strip()
128
+
129
+ if not cleaned:
130
+ continue
131
+
132
+ # cleaned should now be a string of digit characters.
133
+ if not cleaned.isdigit() and not (is_hex and all(c in '0123456789abcdefABCDEF' for c in cleaned)):
134
+ # If we still have non-digit residue, try harder: keep only digits.
135
+ cleaned = re.sub(r'[^0-9a-fA-F]', '', cleaned)
136
+ if not cleaned:
137
+ continue
138
+
139
+ try:
140
+ if is_hex:
141
+ result_chars.append(chr(int(cleaned, 16)))
142
+ else:
143
+ result_chars.append(chr(int(cleaned, 8)))
144
+ except (ValueError, OverflowError):
145
+ continue
146
+
147
+ if not result_chars:
148
+ return None
149
+
150
+ return ''.join(result_chars)