]>
Commit | Line | Data |
---|---|---|
19a03940 | 1 | import collections |
2 | import contextlib | |
8f53dc44 | 3 | import itertools |
825abb81 | 4 | import json |
8f53dc44 | 5 | import math |
9e3f1991 | 6 | import operator |
2b25cb5d PH |
7 | import re |
8 | ||
8f53dc44 | 9 | from .utils import ( |
10 | NO_DEFAULT, | |
11 | ExtractorError, | |
12 | js_to_json, | |
13 | remove_quotes, | |
14 | truncate_string, | |
15 | unified_timestamp, | |
16 | write_string, | |
17 | ) | |
2b25cb5d | 18 | |
230d5c82 | 19 | _NAME_RE = r'[a-zA-Z_$][\w$]*' |
49b4ceae | 20 | |
21 | # Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence | |
8f53dc44 | 22 | _OPERATORS = { # None => Defined in JSInterpreter._operator |
23 | '?': None, | |
24 | ||
25 | '||': None, | |
26 | '&&': None, | |
27 | '&': operator.and_, | |
230d5c82 | 28 | '|': operator.or_, |
29 | '^': operator.xor, | |
8f53dc44 | 30 | |
49b4ceae | 31 | '===': operator.is_, |
32 | '!==': operator.is_not, | |
33 | '==': operator.eq, | |
34 | '!=': operator.ne, | |
8f53dc44 | 35 | |
36 | '<=': operator.le, | |
37 | '>=': operator.ge, | |
38 | '<': operator.lt, | |
39 | '>': operator.gt, | |
40 | ||
49b4ceae | 41 | '>>': operator.rshift, |
42 | '<<': operator.lshift, | |
43 | ||
230d5c82 | 44 | '+': operator.add, |
8f53dc44 | 45 | '-': operator.sub, |
46 | ||
230d5c82 | 47 | '*': operator.mul, |
8f53dc44 | 48 | '/': operator.truediv, |
49 | '%': operator.mod, | |
49b4ceae | 50 | |
51 | '**': operator.pow, | |
230d5c82 | 52 | } |
9e3f1991 | 53 | |
49b4ceae | 54 | _COMP_OPERATORS = {'===', '!==', '==', '!=', '<=', '>=', '<', '>'} |
55 | ||
06dfe0a0 | 56 | _MATCHING_PARENS = dict(zip('({[', ')}]')) |
64fa820c | 57 | _QUOTES = '\'"' |
06dfe0a0 | 58 | |
2b25cb5d | 59 | |
8f53dc44 | 60 | def _ternary(cndn, if_true=True, if_false=False): |
61 | """Simulate JS's ternary operator (cndn?if_true:if_false)""" | |
62 | if cndn in (False, None, 0, ''): | |
63 | return if_false | |
64 | with contextlib.suppress(TypeError): | |
65 | if math.isnan(cndn): # NB: NaN cannot be checked by membership | |
66 | return if_false | |
67 | return if_true | |
68 | ||
69 | ||
404f611f | 70 | class JS_Break(ExtractorError): |
71 | def __init__(self): | |
72 | ExtractorError.__init__(self, 'Invalid break') | |
73 | ||
74 | ||
75 | class JS_Continue(ExtractorError): | |
76 | def __init__(self): | |
77 | ExtractorError.__init__(self, 'Invalid continue') | |
78 | ||
79 | ||
19a03940 | 80 | class LocalNameSpace(collections.ChainMap): |
404f611f | 81 | def __setitem__(self, key, value): |
19a03940 | 82 | for scope in self.maps: |
404f611f | 83 | if key in scope: |
84 | scope[key] = value | |
19a03940 | 85 | return |
86 | self.maps[0][key] = value | |
404f611f | 87 | |
88 | def __delitem__(self, key): | |
89 | raise NotImplementedError('Deleting is not supported') | |
90 | ||
404f611f | 91 | |
8f53dc44 | 92 | class Debugger: |
93 | import sys | |
49b4ceae | 94 | ENABLED = False and 'pytest' in sys.modules |
8f53dc44 | 95 | |
96 | @staticmethod | |
97 | def write(*args, level=100): | |
98 | write_string(f'[debug] JS: {" " * (100 - level)}' | |
99 | f'{" ".join(truncate_string(str(x), 50, 50) for x in args)}\n') | |
100 | ||
101 | @classmethod | |
102 | def wrap_interpreter(cls, f): | |
103 | def interpret_statement(self, stmt, local_vars, allow_recursion, *args, **kwargs): | |
104 | if cls.ENABLED and stmt.strip(): | |
105 | cls.write(stmt, level=allow_recursion) | |
106 | ret, should_ret = f(self, stmt, local_vars, allow_recursion, *args, **kwargs) | |
107 | if cls.ENABLED and stmt.strip(): | |
108 | cls.write(['->', '=>'][should_ret], repr(ret), '<-|', stmt, level=allow_recursion) | |
109 | return ret, should_ret | |
110 | return interpret_statement | |
111 | ||
112 | ||
86e5f3ed | 113 | class JSInterpreter: |
230d5c82 | 114 | __named_object_counter = 0 |
115 | ||
9e3f1991 | 116 | def __init__(self, code, objects=None): |
230d5c82 | 117 | self.code, self._functions = code, {} |
118 | self._objects = {} if objects is None else objects | |
404f611f | 119 | |
a1c5bd82 | 120 | class Exception(ExtractorError): |
121 | def __init__(self, msg, expr=None, *args, **kwargs): | |
122 | if expr is not None: | |
8f53dc44 | 123 | msg = f'{msg.rstrip()} in: {truncate_string(expr, 50, 50)}' |
a1c5bd82 | 124 | super().__init__(msg, *args, **kwargs) |
125 | ||
404f611f | 126 | def _named_object(self, namespace, obj): |
127 | self.__named_object_counter += 1 | |
128 | name = f'__yt_dlp_jsinterp_obj{self.__named_object_counter}' | |
129 | namespace[name] = obj | |
130 | return name | |
131 | ||
132 | @staticmethod | |
e75bb0d6 | 133 | def _separate(expr, delim=',', max_split=None): |
404f611f | 134 | if not expr: |
135 | return | |
06dfe0a0 | 136 | counters = {k: 0 for k in _MATCHING_PARENS.values()} |
137 | start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1 | |
64fa820c | 138 | in_quote, escaping = None, False |
404f611f | 139 | for idx, char in enumerate(expr): |
8f53dc44 | 140 | if not in_quote and char in _MATCHING_PARENS: |
06dfe0a0 | 141 | counters[_MATCHING_PARENS[char]] += 1 |
8f53dc44 | 142 | elif not in_quote and char in counters: |
06dfe0a0 | 143 | counters[char] -= 1 |
64fa820c | 144 | elif not escaping and char in _QUOTES and in_quote in (char, None): |
145 | in_quote = None if in_quote else char | |
146 | escaping = not escaping and in_quote and char == '\\' | |
147 | ||
148 | if char != delim[pos] or any(counters.values()) or in_quote: | |
404f611f | 149 | pos = 0 |
06dfe0a0 | 150 | continue |
151 | elif pos != delim_len: | |
152 | pos += 1 | |
153 | continue | |
154 | yield expr[start: idx - delim_len] | |
155 | start, pos = idx + 1, 0 | |
156 | splits += 1 | |
157 | if max_split and splits >= max_split: | |
158 | break | |
404f611f | 159 | yield expr[start:] |
160 | ||
230d5c82 | 161 | @classmethod |
162 | def _separate_at_paren(cls, expr, delim): | |
163 | separated = list(cls._separate(expr, delim, 1)) | |
e75bb0d6 | 164 | if len(separated) < 2: |
a1c5bd82 | 165 | raise cls.Exception(f'No terminating paren {delim}', expr) |
e75bb0d6 | 166 | return separated[0][1:].strip(), separated[1].strip() |
9e3f1991 | 167 | |
8f53dc44 | 168 | def _operator(self, op, left_val, right_expr, expr, local_vars, allow_recursion): |
169 | if op in ('||', '&&'): | |
170 | if (op == '&&') ^ _ternary(left_val): | |
171 | return left_val # short circuiting | |
172 | elif op == '?': | |
173 | right_expr = _ternary(left_val, *self._separate(right_expr, ':', 1)) | |
174 | ||
175 | right_val = self.interpret_expression(right_expr, local_vars, allow_recursion) | |
176 | if not _OPERATORS.get(op): | |
177 | return right_val | |
178 | ||
179 | try: | |
180 | return _OPERATORS[op](left_val, right_val) | |
181 | except Exception as e: | |
182 | raise self.Exception(f'Failed to evaluate {left_val!r} {op} {right_val!r}', expr, cause=e) | |
183 | ||
184 | def _index(self, obj, idx): | |
185 | if idx == 'length': | |
186 | return len(obj) | |
187 | try: | |
188 | return obj[int(idx)] if isinstance(obj, list) else obj[idx] | |
189 | except Exception as e: | |
190 | raise self.Exception(f'Cannot get index {idx}', repr(obj), cause=e) | |
191 | ||
192 | def _dump(self, obj, namespace): | |
193 | try: | |
194 | return json.dumps(obj) | |
195 | except TypeError: | |
196 | return self._named_object(namespace, obj) | |
197 | ||
198 | @Debugger.wrap_interpreter | |
9e3f1991 | 199 | def interpret_statement(self, stmt, local_vars, allow_recursion=100): |
2b25cb5d | 200 | if allow_recursion < 0: |
a1c5bd82 | 201 | raise self.Exception('Recursion limit reached') |
8f53dc44 | 202 | allow_recursion -= 1 |
2b25cb5d | 203 | |
8f53dc44 | 204 | should_return = False |
230d5c82 | 205 | sub_statements = list(self._separate(stmt, ';')) or [''] |
8f53dc44 | 206 | expr = stmt = sub_statements.pop().strip() |
230d5c82 | 207 | |
404f611f | 208 | for sub_stmt in sub_statements: |
8f53dc44 | 209 | ret, should_return = self.interpret_statement(sub_stmt, local_vars, allow_recursion) |
210 | if should_return: | |
211 | return ret, should_return | |
404f611f | 212 | |
49b4ceae | 213 | m = re.match(r'(?P<var>(?:var|const|let)\s)|return(?:\s+|$)', stmt) |
8f53dc44 | 214 | if m: |
215 | expr = stmt[len(m.group(0)):].strip() | |
216 | should_return = not m.group('var') | |
230d5c82 | 217 | if not expr: |
8f53dc44 | 218 | return None, should_return |
219 | ||
220 | if expr[0] in _QUOTES: | |
221 | inner, outer = self._separate(expr, expr[0], 1) | |
222 | inner = json.loads(js_to_json(f'{inner}{expr[0]}', strict=True)) | |
223 | if not outer: | |
224 | return inner, should_return | |
225 | expr = self._named_object(local_vars, inner) + outer | |
226 | ||
227 | if expr.startswith('new '): | |
228 | obj = expr[4:] | |
229 | if obj.startswith('Date('): | |
230 | left, right = self._separate_at_paren(obj[4:], ')') | |
49b4ceae | 231 | expr = unified_timestamp( |
232 | self.interpret_expression(left, local_vars, allow_recursion), False) | |
8f53dc44 | 233 | if not expr: |
234 | raise self.Exception(f'Failed to parse date {left!r}', expr) | |
235 | expr = self._dump(int(expr * 1000), local_vars) + right | |
236 | else: | |
237 | raise self.Exception(f'Unsupported object {obj}', expr) | |
9e3f1991 | 238 | |
49b4ceae | 239 | if expr.startswith('void '): |
240 | left = self.interpret_expression(expr[5:], local_vars, allow_recursion) | |
241 | return None, should_return | |
242 | ||
404f611f | 243 | if expr.startswith('{'): |
e75bb0d6 | 244 | inner, outer = self._separate_at_paren(expr, '}') |
8f53dc44 | 245 | inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion) |
404f611f | 246 | if not outer or should_abort: |
8f53dc44 | 247 | return inner, should_abort or should_return |
404f611f | 248 | else: |
8f53dc44 | 249 | expr = self._dump(inner, local_vars) + outer |
404f611f | 250 | |
9e3f1991 | 251 | if expr.startswith('('): |
e75bb0d6 | 252 | inner, outer = self._separate_at_paren(expr, ')') |
8f53dc44 | 253 | inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion) |
254 | if not outer or should_abort: | |
255 | return inner, should_abort or should_return | |
404f611f | 256 | else: |
8f53dc44 | 257 | expr = self._dump(inner, local_vars) + outer |
404f611f | 258 | |
259 | if expr.startswith('['): | |
e75bb0d6 | 260 | inner, outer = self._separate_at_paren(expr, ']') |
404f611f | 261 | name = self._named_object(local_vars, [ |
262 | self.interpret_expression(item, local_vars, allow_recursion) | |
e75bb0d6 | 263 | for item in self._separate(inner)]) |
404f611f | 264 | expr = name + outer |
265 | ||
8f53dc44 | 266 | m = re.match(r'(?P<try>try|finally)\s*|(?:(?P<catch>catch)|(?P<for>for)|(?P<switch>switch))\s*\(', expr) |
230d5c82 | 267 | if m and m.group('try'): |
404f611f | 268 | if expr[m.end()] == '{': |
e75bb0d6 | 269 | try_expr, expr = self._separate_at_paren(expr[m.end():], '}') |
404f611f | 270 | else: |
271 | try_expr, expr = expr[m.end() - 1:], '' | |
8f53dc44 | 272 | ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion) |
404f611f | 273 | if should_abort: |
8f53dc44 | 274 | return ret, True |
275 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) | |
276 | return ret, should_abort or should_return | |
404f611f | 277 | |
230d5c82 | 278 | elif m and m.group('catch'): |
404f611f | 279 | # We ignore the catch block |
e75bb0d6 | 280 | _, expr = self._separate_at_paren(expr, '}') |
8f53dc44 | 281 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) |
282 | return ret, should_abort or should_return | |
404f611f | 283 | |
230d5c82 | 284 | elif m and m.group('for'): |
e75bb0d6 | 285 | constructor, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') |
404f611f | 286 | if remaining.startswith('{'): |
e75bb0d6 | 287 | body, expr = self._separate_at_paren(remaining, '}') |
404f611f | 288 | else: |
230d5c82 | 289 | switch_m = re.match(r'switch\s*\(', remaining) # FIXME |
290 | if switch_m: | |
291 | switch_val, remaining = self._separate_at_paren(remaining[switch_m.end() - 1:], ')') | |
e75bb0d6 | 292 | body, expr = self._separate_at_paren(remaining, '}') |
404f611f | 293 | body = 'switch(%s){%s}' % (switch_val, body) |
9e3f1991 | 294 | else: |
404f611f | 295 | body, expr = remaining, '' |
e75bb0d6 | 296 | start, cndn, increment = self._separate(constructor, ';') |
8f53dc44 | 297 | self.interpret_expression(start, local_vars, allow_recursion) |
404f611f | 298 | while True: |
8f53dc44 | 299 | if not _ternary(self.interpret_expression(cndn, local_vars, allow_recursion)): |
404f611f | 300 | break |
301 | try: | |
8f53dc44 | 302 | ret, should_abort = self.interpret_statement(body, local_vars, allow_recursion) |
404f611f | 303 | if should_abort: |
8f53dc44 | 304 | return ret, True |
404f611f | 305 | except JS_Break: |
306 | break | |
307 | except JS_Continue: | |
308 | pass | |
8f53dc44 | 309 | self.interpret_expression(increment, local_vars, allow_recursion) |
310 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) | |
311 | return ret, should_abort or should_return | |
404f611f | 312 | |
230d5c82 | 313 | elif m and m.group('switch'): |
e75bb0d6 | 314 | switch_val, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') |
404f611f | 315 | switch_val = self.interpret_expression(switch_val, local_vars, allow_recursion) |
e75bb0d6 | 316 | body, expr = self._separate_at_paren(remaining, '}') |
a1fc7ca0 | 317 | items = body.replace('default:', 'case default:').split('case ')[1:] |
318 | for default in (False, True): | |
319 | matched = False | |
320 | for item in items: | |
86e5f3ed | 321 | case, stmt = (i.strip() for i in self._separate(item, ':', 1)) |
a1fc7ca0 | 322 | if default: |
323 | matched = matched or case == 'default' | |
324 | elif not matched: | |
49b4ceae | 325 | matched = (case != 'default' |
326 | and switch_val == self.interpret_expression(case, local_vars, allow_recursion)) | |
a1fc7ca0 | 327 | if not matched: |
328 | continue | |
404f611f | 329 | try: |
8f53dc44 | 330 | ret, should_abort = self.interpret_statement(stmt, local_vars, allow_recursion) |
404f611f | 331 | if should_abort: |
332 | return ret | |
333 | except JS_Break: | |
9e3f1991 | 334 | break |
a1fc7ca0 | 335 | if matched: |
336 | break | |
8f53dc44 | 337 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) |
338 | return ret, should_abort or should_return | |
404f611f | 339 | |
e75bb0d6 U |
340 | # Comma separated statements |
341 | sub_expressions = list(self._separate(expr)) | |
404f611f | 342 | expr = sub_expressions.pop().strip() if sub_expressions else '' |
343 | for sub_expr in sub_expressions: | |
8f53dc44 | 344 | ret, should_abort = self.interpret_statement(sub_expr, local_vars, allow_recursion) |
345 | if should_abort: | |
346 | return ret, True | |
404f611f | 347 | |
348 | for m in re.finditer(rf'''(?x) | |
349 | (?P<pre_sign>\+\+|--)(?P<var1>{_NAME_RE})| | |
350 | (?P<var2>{_NAME_RE})(?P<post_sign>\+\+|--)''', expr): | |
351 | var = m.group('var1') or m.group('var2') | |
352 | start, end = m.span() | |
353 | sign = m.group('pre_sign') or m.group('post_sign') | |
354 | ret = local_vars[var] | |
355 | local_vars[var] += 1 if sign[0] == '+' else -1 | |
356 | if m.group('pre_sign'): | |
357 | ret = local_vars[var] | |
8f53dc44 | 358 | expr = expr[:start] + self._dump(ret, local_vars) + expr[end:] |
9e3f1991 | 359 | |
230d5c82 | 360 | if not expr: |
8f53dc44 | 361 | return None, should_return |
9e3f1991 | 362 | |
230d5c82 | 363 | m = re.match(fr'''(?x) |
364 | (?P<assign> | |
365 | (?P<out>{_NAME_RE})(?:\[(?P<index>[^\]]+?)\])?\s* | |
49b4ceae | 366 | (?P<op>{"|".join(map(re.escape, set(_OPERATORS) - _COMP_OPERATORS))})? |
230d5c82 | 367 | =(?P<expr>.*)$ |
368 | )|(?P<return> | |
8f53dc44 | 369 | (?!if|return|true|false|null|undefined)(?P<name>{_NAME_RE})$ |
230d5c82 | 370 | )|(?P<indexing> |
371 | (?P<in>{_NAME_RE})\[(?P<idx>.+)\]$ | |
372 | )|(?P<attribute> | |
373 | (?P<var>{_NAME_RE})(?:\.(?P<member>[^(]+)|\[(?P<member2>[^\]]+)\])\s* | |
374 | )|(?P<function> | |
8f53dc44 | 375 | (?P<fname>{_NAME_RE})\((?P<args>.*)\)$ |
230d5c82 | 376 | )''', expr) |
377 | if m and m.group('assign'): | |
230d5c82 | 378 | left_val = local_vars.get(m.group('out')) |
379 | ||
380 | if not m.group('index'): | |
8f53dc44 | 381 | local_vars[m.group('out')] = self._operator( |
382 | m.group('op'), left_val, m.group('expr'), expr, local_vars, allow_recursion) | |
383 | return local_vars[m.group('out')], should_return | |
230d5c82 | 384 | elif left_val is None: |
a1c5bd82 | 385 | raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr) |
230d5c82 | 386 | |
387 | idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion) | |
8f53dc44 | 388 | if not isinstance(idx, (int, float)): |
a1c5bd82 | 389 | raise self.Exception(f'List index {idx} must be integer', expr) |
8f53dc44 | 390 | idx = int(idx) |
391 | left_val[idx] = self._operator( | |
392 | m.group('op'), left_val[idx], m.group('expr'), expr, local_vars, allow_recursion) | |
393 | return left_val[idx], should_return | |
9e3f1991 | 394 | |
230d5c82 | 395 | elif expr.isdigit(): |
8f53dc44 | 396 | return int(expr), should_return |
2b25cb5d | 397 | |
230d5c82 | 398 | elif expr == 'break': |
404f611f | 399 | raise JS_Break() |
400 | elif expr == 'continue': | |
401 | raise JS_Continue() | |
402 | ||
230d5c82 | 403 | elif m and m.group('return'): |
8f53dc44 | 404 | return local_vars[m.group('name')], should_return |
2b25cb5d | 405 | |
19a03940 | 406 | with contextlib.suppress(ValueError): |
8f53dc44 | 407 | return json.loads(js_to_json(expr, strict=True)), should_return |
825abb81 | 408 | |
230d5c82 | 409 | if m and m.group('indexing'): |
7769f837 | 410 | val = local_vars[m.group('in')] |
404f611f | 411 | idx = self.interpret_expression(m.group('idx'), local_vars, allow_recursion) |
8f53dc44 | 412 | return self._index(val, idx), should_return |
7769f837 | 413 | |
8f53dc44 | 414 | for op in _OPERATORS: |
e75bb0d6 | 415 | separated = list(self._separate(expr, op)) |
8f53dc44 | 416 | right_expr = separated.pop() |
49b4ceae | 417 | while op in '<>*-' and len(separated) > 1 and not separated[-1].strip(): |
8f53dc44 | 418 | separated.pop() |
49b4ceae | 419 | right_expr = f'{op}{right_expr}' |
420 | if op != '-': | |
421 | right_expr = f'{separated.pop()}{op}{right_expr}' | |
422 | if not separated: | |
423 | continue | |
8f53dc44 | 424 | left_val = self.interpret_expression(op.join(separated), local_vars, allow_recursion) |
425 | return self._operator(op, 0 if left_val is None else left_val, | |
426 | right_expr, expr, local_vars, allow_recursion), should_return | |
404f611f | 427 | |
230d5c82 | 428 | if m and m.group('attribute'): |
825abb81 | 429 | variable = m.group('var') |
8f53dc44 | 430 | member = m.group('member') |
431 | if not member: | |
432 | member = self.interpret_expression(m.group('member2'), local_vars, allow_recursion) | |
404f611f | 433 | arg_str = expr[m.end():] |
434 | if arg_str.startswith('('): | |
e75bb0d6 | 435 | arg_str, remaining = self._separate_at_paren(arg_str, ')') |
825abb81 | 436 | else: |
404f611f | 437 | arg_str, remaining = None, arg_str |
438 | ||
439 | def assertion(cndn, msg): | |
440 | """ assert, but without risk of getting optimized out """ | |
441 | if not cndn: | |
a1c5bd82 | 442 | raise self.Exception(f'{member} {msg}', expr) |
404f611f | 443 | |
444 | def eval_method(): | |
8f53dc44 | 445 | if (variable, member) == ('console', 'debug'): |
446 | if Debugger.ENABLED: | |
447 | Debugger.write(self.interpret_expression(f'[{arg_str}]', local_vars, allow_recursion)) | |
448 | return | |
449 | ||
450 | types = { | |
451 | 'String': str, | |
452 | 'Math': float, | |
453 | } | |
454 | obj = local_vars.get(variable, types.get(variable, NO_DEFAULT)) | |
455 | if obj is NO_DEFAULT: | |
404f611f | 456 | if variable not in self._objects: |
457 | self._objects[variable] = self.extract_object(variable) | |
458 | obj = self._objects[variable] | |
459 | ||
230d5c82 | 460 | # Member access |
404f611f | 461 | if arg_str is None: |
8f53dc44 | 462 | return self._index(obj, member) |
404f611f | 463 | |
464 | # Function call | |
465 | argvals = [ | |
825abb81 | 466 | self.interpret_expression(v, local_vars, allow_recursion) |
e75bb0d6 | 467 | for v in self._separate(arg_str)] |
404f611f | 468 | |
469 | if obj == str: | |
470 | if member == 'fromCharCode': | |
471 | assertion(argvals, 'takes one or more arguments') | |
472 | return ''.join(map(chr, argvals)) | |
8f53dc44 | 473 | raise self.Exception(f'Unsupported String method {member}', expr) |
474 | elif obj == float: | |
475 | if member == 'pow': | |
476 | assertion(len(argvals) == 2, 'takes two arguments') | |
477 | return argvals[0] ** argvals[1] | |
478 | raise self.Exception(f'Unsupported Math method {member}', expr) | |
404f611f | 479 | |
480 | if member == 'split': | |
481 | assertion(argvals, 'takes one or more arguments') | |
8f53dc44 | 482 | assertion(len(argvals) == 1, 'with limit argument is not implemented') |
483 | return obj.split(argvals[0]) if argvals[0] else list(obj) | |
404f611f | 484 | elif member == 'join': |
485 | assertion(isinstance(obj, list), 'must be applied on a list') | |
486 | assertion(len(argvals) == 1, 'takes exactly one argument') | |
487 | return argvals[0].join(obj) | |
488 | elif member == 'reverse': | |
489 | assertion(not argvals, 'does not take any arguments') | |
490 | obj.reverse() | |
491 | return obj | |
492 | elif member == 'slice': | |
493 | assertion(isinstance(obj, list), 'must be applied on a list') | |
494 | assertion(len(argvals) == 1, 'takes exactly one argument') | |
495 | return obj[argvals[0]:] | |
496 | elif member == 'splice': | |
497 | assertion(isinstance(obj, list), 'must be applied on a list') | |
498 | assertion(argvals, 'takes one or more arguments') | |
57dbe807 | 499 | index, howMany = map(int, (argvals + [len(obj)])[:2]) |
404f611f | 500 | if index < 0: |
501 | index += len(obj) | |
502 | add_items = argvals[2:] | |
503 | res = [] | |
504 | for i in range(index, min(index + howMany, len(obj))): | |
505 | res.append(obj.pop(index)) | |
506 | for i, item in enumerate(add_items): | |
507 | obj.insert(index + i, item) | |
508 | return res | |
509 | elif member == 'unshift': | |
510 | assertion(isinstance(obj, list), 'must be applied on a list') | |
511 | assertion(argvals, 'takes one or more arguments') | |
512 | for item in reversed(argvals): | |
513 | obj.insert(0, item) | |
514 | return obj | |
515 | elif member == 'pop': | |
516 | assertion(isinstance(obj, list), 'must be applied on a list') | |
517 | assertion(not argvals, 'does not take any arguments') | |
518 | if not obj: | |
519 | return | |
520 | return obj.pop() | |
521 | elif member == 'push': | |
522 | assertion(argvals, 'takes one or more arguments') | |
523 | obj.extend(argvals) | |
524 | return obj | |
525 | elif member == 'forEach': | |
526 | assertion(argvals, 'takes one or more arguments') | |
527 | assertion(len(argvals) <= 2, 'takes at-most 2 arguments') | |
528 | f, this = (argvals + [''])[:2] | |
8f53dc44 | 529 | return [f((item, idx, obj), {'this': this}, allow_recursion) for idx, item in enumerate(obj)] |
404f611f | 530 | elif member == 'indexOf': |
531 | assertion(argvals, 'takes one or more arguments') | |
532 | assertion(len(argvals) <= 2, 'takes at-most 2 arguments') | |
533 | idx, start = (argvals + [0])[:2] | |
534 | try: | |
535 | return obj.index(idx, start) | |
536 | except ValueError: | |
537 | return -1 | |
538 | ||
8f53dc44 | 539 | idx = int(member) if isinstance(obj, list) else member |
540 | return obj[idx](argvals, allow_recursion=allow_recursion) | |
404f611f | 541 | |
542 | if remaining: | |
8f53dc44 | 543 | ret, should_abort = self.interpret_statement( |
404f611f | 544 | self._named_object(local_vars, eval_method()) + remaining, |
545 | local_vars, allow_recursion) | |
8f53dc44 | 546 | return ret, should_return or should_abort |
404f611f | 547 | else: |
8f53dc44 | 548 | return eval_method(), should_return |
2b25cb5d | 549 | |
230d5c82 | 550 | elif m and m.group('function'): |
551 | fname = m.group('fname') | |
8f53dc44 | 552 | argvals = [self.interpret_expression(v, local_vars, allow_recursion) |
553 | for v in self._separate(m.group('args'))] | |
404f611f | 554 | if fname in local_vars: |
8f53dc44 | 555 | return local_vars[fname](argvals, allow_recursion=allow_recursion), should_return |
404f611f | 556 | elif fname not in self._functions: |
1f749b66 | 557 | self._functions[fname] = self.extract_function(fname) |
8f53dc44 | 558 | return self._functions[fname](argvals, allow_recursion=allow_recursion), should_return |
559 | ||
560 | raise self.Exception( | |
561 | f'Unsupported JS expression {truncate_string(expr, 20, 20) if expr != stmt else ""}', stmt) | |
9e3f1991 | 562 | |
8f53dc44 | 563 | def interpret_expression(self, expr, local_vars, allow_recursion): |
564 | ret, should_return = self.interpret_statement(expr, local_vars, allow_recursion) | |
565 | if should_return: | |
566 | raise self.Exception('Cannot return from an expression', expr) | |
567 | return ret | |
2b25cb5d | 568 | |
ad25aee2 | 569 | def extract_object(self, objname): |
7769f837 | 570 | _FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')''' |
ad25aee2 JMF |
571 | obj = {} |
572 | obj_m = re.search( | |
0e2d626d S |
573 | r'''(?x) |
574 | (?<!this\.)%s\s*=\s*{\s* | |
575 | (?P<fields>(%s\s*:\s*function\s*\(.*?\)\s*{.*?}(?:,\s*)?)*) | |
576 | }\s*; | |
577 | ''' % (re.escape(objname), _FUNC_NAME_RE), | |
ad25aee2 | 578 | self.code) |
8f53dc44 | 579 | if not obj_m: |
580 | raise self.Exception(f'Could not find object {objname}') | |
ad25aee2 JMF |
581 | fields = obj_m.group('fields') |
582 | # Currently, it only supports function definitions | |
583 | fields_m = re.finditer( | |
0e2d626d | 584 | r'''(?x) |
49b4ceae | 585 | (?P<key>%s)\s*:\s*function\s*\((?P<args>(?:%s|,)*)\){(?P<code>[^}]+)} |
586 | ''' % (_FUNC_NAME_RE, _NAME_RE), | |
ad25aee2 JMF |
587 | fields) |
588 | for f in fields_m: | |
589 | argnames = f.group('args').split(',') | |
7769f837 | 590 | obj[remove_quotes(f.group('key'))] = self.build_function(argnames, f.group('code')) |
ad25aee2 JMF |
591 | |
592 | return obj | |
593 | ||
404f611f | 594 | def extract_function_code(self, funcname): |
595 | """ @returns argnames, code """ | |
2b25cb5d | 596 | func_m = re.search( |
8f53dc44 | 597 | r'''(?xs) |
230d5c82 | 598 | (?: |
599 | function\s+%(name)s| | |
600 | [{;,]\s*%(name)s\s*=\s*function| | |
49b4ceae | 601 | (?:var|const|let)\s+%(name)s\s*=\s*function |
230d5c82 | 602 | )\s* |
9e3f1991 | 603 | \((?P<args>[^)]*)\)\s* |
8f53dc44 | 604 | (?P<code>{.+})''' % {'name': re.escape(funcname)}, |
2b25cb5d | 605 | self.code) |
8f53dc44 | 606 | code, _ = self._separate_at_paren(func_m.group('code'), '}') |
77ffa957 | 607 | if func_m is None: |
a1c5bd82 | 608 | raise self.Exception(f'Could not find JS function "{funcname}"') |
8f53dc44 | 609 | return [x.strip() for x in func_m.group('args').split(',')], code |
2b25cb5d | 610 | |
404f611f | 611 | def extract_function(self, funcname): |
612 | return self.extract_function_from_code(*self.extract_function_code(funcname)) | |
613 | ||
614 | def extract_function_from_code(self, argnames, code, *global_stack): | |
615 | local_vars = {} | |
616 | while True: | |
617 | mobj = re.search(r'function\((?P<args>[^)]*)\)\s*{', code) | |
618 | if mobj is None: | |
619 | break | |
620 | start, body_start = mobj.span() | |
e75bb0d6 | 621 | body, remaining = self._separate_at_paren(code[body_start - 1:], '}') |
230d5c82 | 622 | name = self._named_object(local_vars, self.extract_function_from_code( |
623 | [x.strip() for x in mobj.group('args').split(',')], | |
624 | body, local_vars, *global_stack)) | |
404f611f | 625 | code = code[:start] + name + remaining |
626 | return self.build_function(argnames, code, local_vars, *global_stack) | |
ad25aee2 | 627 | |
9e3f1991 | 628 | def call_function(self, funcname, *args): |
404f611f | 629 | return self.extract_function(funcname)(args) |
630 | ||
631 | def build_function(self, argnames, code, *global_stack): | |
632 | global_stack = list(global_stack) or [{}] | |
8f53dc44 | 633 | argnames = tuple(argnames) |
404f611f | 634 | |
8f53dc44 | 635 | def resf(args, kwargs={}, allow_recursion=100): |
49b4ceae | 636 | global_stack[0].update(itertools.zip_longest(argnames, args, fillvalue=None)) |
637 | global_stack[0].update(kwargs) | |
19a03940 | 638 | var_stack = LocalNameSpace(*global_stack) |
8f53dc44 | 639 | ret, should_abort = self.interpret_statement(code.replace('\n', ''), var_stack, allow_recursion - 1) |
640 | if should_abort: | |
641 | return ret | |
2b25cb5d | 642 | return resf |