-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcurse.py
More file actions
200 lines (172 loc) · 6.55 KB
/
curse.py
File metadata and controls
200 lines (172 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import re
import sys
import traceback
import pdb
from multiprocessing import Process
from html.parser import HTMLParser
from utils import map_string, HTMLBuilder
from tktarget import TkinterTarget
""" References
+ <https://github.com/ajalt/fuckitpy>; very useful for learning how to get around python's
exception handling
+ <https://www.reddit.com/r/rust/comments/5penft/comment/dcsgk7n/>; some unicode characters
look like symbols but are actually letters
+ <
https://stackoverflow.com/questions/29492895/bare-words-new-keywords-in-python/29492897#29492897
>; this is where I got the `sys.excepthook` trick
"""
""" Cursed Characters Map
Open Angle '<' | \u1438 | "ᐸ"
Close Angle '>' | \u1433 | "ᐳ"
Space ' ' | \u3164 | "ㅤ"
Forward Slash '/' | \u10915 | "𐤕"
Equals Sign '=' | \uA60C | "ꘌ"
Quote '"' | \u05F2 | "ײ"
ᐸpᐳ
ㅤㅤᐸspanᐳHelloㅤWorldᐸ𐤕spanᐳ
ᐸ𐤕pᐳ
"""
class Parser(HTMLParser):
""" Build an HTML Parse Tree """
def __init__(self):
super().__init__()
self.code = ""
self.stack = []
@property
def complete(self):
""" Check whether the parse tree is complete """
return len(self.stack) == 0 and self.code != ""
@property
def top(self):
""" Get the top (latest) element of the stack """
return self.stack[-1]
def handle_starttag(self, tag, attrs):
""" Handle start tags """
self.code += f".tag(\"{tag}\""
self.parameters = []
for aname, aval in attrs:
if aval and "lambda:" in aval:
self.parameters.append((aname, aval))
else:
self.code += f", {aname}=\"{aval or True}\""
self.stack.append(tag)
self.code += ")"
for pname, pfunc in self.parameters:
self.code += f".param(\"{pname}\", {pfunc})"
def handle_endtag(self, tag):
""" Handle end tags """
try:
prev_tag = self.stack.pop()
self.code += ".pop()"
except IndexError:
raise Exception(f"Unmatched end tag: {tag}")
if prev_tag != tag:
raise Exception(f"Expected end tag {prev_tag}, got {tag}")
def handle_data(self, data):
""" Handle data """
if data.strip() == "":
return
pattern = re.compile(r"\(lambda: (.+?)\)")
for match in pattern.finditer(data):
data = data.replace(match.group(0), "{" + match.group(1) + "}")
self.code += f".text(f\"{data.strip()}\")"
def handle_startendtag(self, tag, attrs):
""" Handle self-closing tags """
self.code += f".tag(\"{tag}\""
for aname, aval in attrs:
self.code += f", {aname}=\"{aval or True}\""
self.code += ").pop()"
class ErrorHijacker:
"""
This class interrcepts errors and replaces so they can be silently
handled.
In essence, we're using NameErrors to detect our 'cursed' HTML-like
syntax and then replacing it with valid Python syntax.
"""
CHARACTER_MAP = {
"\U00001438": "<",
"\U00001433": ">",
"\U00003164": " ",
"\U00010915": "/",
"\U0000A60C": "=",
"\U000005F2": '"',
}
def __init__(self, native_hook):
self.native_hook = native_hook
def __call__(self, ex_type, value, trace):
"""
Called when an exception is raised
"""
if isinstance(value, NameError):
# Intercept NameError
# Extract the dirty file and fix the lines
stack = traceback.extract_tb(trace)
frame = stack[-1]
filename = frame.filename
lineno = frame.lineno
code = ""
with open(filename, "r") as f:
raw_lines = f.readlines()
cleaned_lines = list(self._clean_lines(raw_lines))
code = "".join(cleaned_lines)
""" This is helpful for debugging, it shows the modified source
print("".join([
f"{i+lineno:03d} {l}" for i, l in enumerate(cleaned_lines[lineno-5:])
]))
"""
# Now we have the fixed code, we can compile it and execute it
exec(
compile(code, filename, "exec"),
{**globals(), "__name__": "__main__"},
locals(),
)
# Notice! This means everything before the first instance of
# 'cursed' code will be executed twice. This is a limitation
# of the current implementation, there may be a workaround
# using the context in the traceback, but it's probably not
# worth the effort.
return
self.native_hook(ex_type, value, trace)
def _clean_lines(self, lines):
"""
Replaces (potentially) cursed characters with correct
python code
"""
grab_pattern = re.compile(r"\U00003164*(\U00001438.+\U00001433)")
parser = None
prechars = ""
for line in lines:
if self.CHARACTER_MAP.keys() & line and grab_pattern.search(line):
content = line[line.find("\U00001438"):]
html_content = map_string(content, self.CHARACTER_MAP)
evaluated_content = self._eval_expressions(html_content)
if parser is None:
prechars = line[:line.find("\U00001438")]
parser = Parser()
parser.feed(evaluated_content)
if parser is None:
yield line
elif parser.complete:
code = f"{prechars}HTMLBuilder(){parser.code}\n"
yield code
parser = None
prechars = ""
def _eval_expressions(self, line):
"""
Converts expressions to a wrapped function that evaluates to a value
in its original context.
Expressions are marked with an open parenthesis, then a python
expression that evaluates to a string or value, then a close parenthesis
and a comma.
Example: `(1 + 1),` -> `lambda: 1 + 1`
"""
pattern = re.compile(r"\((.+?)\),")
# Get a list of all the expressions in the line
expressions = pattern.findall(line)
for expr in expressions:
# Replace the expression with a lambda that evaluates to the expression
line = line.replace(f"({expr}),", f"(lambda: {expr})")
#print(line)
return line
handler = ErrorHijacker(sys.excepthook)
sys.excepthook = handler