]>
Commit | Line | Data |
---|---|---|
1 | import collections | |
2 | import contextlib | |
3 | import itertools | |
4 | import json | |
5 | import math | |
6 | import operator | |
7 | import re | |
8 | ||
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 | ) | |
18 | ||
19 | ||
20 | def _js_bit_op(op): | |
21 | def wrapped(a, b): | |
22 | def zeroise(x): | |
23 | return 0 if x in (None, JS_Undefined) else x | |
24 | return op(zeroise(a), zeroise(b)) | |
25 | ||
26 | return wrapped | |
27 | ||
28 | ||
29 | def _js_arith_op(op): | |
30 | ||
31 | def wrapped(a, b): | |
32 | if JS_Undefined in (a, b): | |
33 | return float('nan') | |
34 | return op(a or 0, b or 0) | |
35 | ||
36 | return wrapped | |
37 | ||
38 | ||
39 | def _js_div(a, b): | |
40 | if JS_Undefined in (a, b) or not (a and b): | |
41 | return float('nan') | |
42 | return (a or 0) / b if b else float('inf') | |
43 | ||
44 | ||
45 | def _js_mod(a, b): | |
46 | if JS_Undefined in (a, b) or not b: | |
47 | return float('nan') | |
48 | return (a or 0) % b | |
49 | ||
50 | ||
51 | def _js_exp(a, b): | |
52 | if not b: | |
53 | return 1 # even 0 ** 0 !! | |
54 | elif JS_Undefined in (a, b): | |
55 | return float('nan') | |
56 | return (a or 0) ** b | |
57 | ||
58 | ||
59 | def _js_eq_op(op): | |
60 | ||
61 | def wrapped(a, b): | |
62 | if {a, b} <= {None, JS_Undefined}: | |
63 | return op(a, a) | |
64 | return op(a, b) | |
65 | ||
66 | return wrapped | |
67 | ||
68 | ||
69 | def _js_comp_op(op): | |
70 | ||
71 | def wrapped(a, b): | |
72 | if JS_Undefined in (a, b): | |
73 | return False | |
74 | return op(a or 0, b or 0) | |
75 | ||
76 | return wrapped | |
77 | ||
78 | ||
79 | def _js_ternary(cndn, if_true=True, if_false=False): | |
80 | """Simulate JS's ternary operator (cndn?if_true:if_false)""" | |
81 | if cndn in (False, None, 0, '', JS_Undefined): | |
82 | return if_false | |
83 | with contextlib.suppress(TypeError): | |
84 | if math.isnan(cndn): # NB: NaN cannot be checked by membership | |
85 | return if_false | |
86 | return if_true | |
87 | ||
88 | ||
89 | # Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence | |
90 | _OPERATORS = { # None => Defined in JSInterpreter._operator | |
91 | '?': None, | |
92 | '??': None, | |
93 | '||': None, | |
94 | '&&': None, | |
95 | ||
96 | '|': _js_bit_op(operator.or_), | |
97 | '^': _js_bit_op(operator.xor), | |
98 | '&': _js_bit_op(operator.and_), | |
99 | ||
100 | '===': operator.is_, | |
101 | '!==': operator.is_not, | |
102 | '==': _js_eq_op(operator.eq), | |
103 | '!=': _js_eq_op(operator.ne), | |
104 | ||
105 | '<=': _js_comp_op(operator.le), | |
106 | '>=': _js_comp_op(operator.ge), | |
107 | '<': _js_comp_op(operator.lt), | |
108 | '>': _js_comp_op(operator.gt), | |
109 | ||
110 | '>>': _js_bit_op(operator.rshift), | |
111 | '<<': _js_bit_op(operator.lshift), | |
112 | ||
113 | '+': _js_arith_op(operator.add), | |
114 | '-': _js_arith_op(operator.sub), | |
115 | ||
116 | '*': _js_arith_op(operator.mul), | |
117 | '/': _js_div, | |
118 | '%': _js_mod, | |
119 | '**': _js_exp, | |
120 | } | |
121 | ||
122 | _COMP_OPERATORS = {'===', '!==', '==', '!=', '<=', '>=', '<', '>'} | |
123 | ||
124 | _NAME_RE = r'[a-zA-Z_$][\w$]*' | |
125 | _MATCHING_PARENS = dict(zip(*zip('()', '{}', '[]'))) | |
126 | _QUOTES = '\'"/' | |
127 | ||
128 | ||
129 | class JS_Undefined: | |
130 | pass | |
131 | ||
132 | ||
133 | class JS_Break(ExtractorError): | |
134 | def __init__(self): | |
135 | ExtractorError.__init__(self, 'Invalid break') | |
136 | ||
137 | ||
138 | class JS_Continue(ExtractorError): | |
139 | def __init__(self): | |
140 | ExtractorError.__init__(self, 'Invalid continue') | |
141 | ||
142 | ||
143 | class JS_Throw(ExtractorError): | |
144 | def __init__(self, e): | |
145 | self.error = e | |
146 | ExtractorError.__init__(self, f'Uncaught exception {e}') | |
147 | ||
148 | ||
149 | class LocalNameSpace(collections.ChainMap): | |
150 | def __setitem__(self, key, value): | |
151 | for scope in self.maps: | |
152 | if key in scope: | |
153 | scope[key] = value | |
154 | return | |
155 | self.maps[0][key] = value | |
156 | ||
157 | def __delitem__(self, key): | |
158 | raise NotImplementedError('Deleting is not supported') | |
159 | ||
160 | ||
161 | class Debugger: | |
162 | import sys | |
163 | ENABLED = False and 'pytest' in sys.modules | |
164 | ||
165 | @staticmethod | |
166 | def write(*args, level=100): | |
167 | write_string(f'[debug] JS: {" " * (100 - level)}' | |
168 | f'{" ".join(truncate_string(str(x), 50, 50) for x in args)}\n') | |
169 | ||
170 | @classmethod | |
171 | def wrap_interpreter(cls, f): | |
172 | def interpret_statement(self, stmt, local_vars, allow_recursion, *args, **kwargs): | |
173 | if cls.ENABLED and stmt.strip(): | |
174 | cls.write(stmt, level=allow_recursion) | |
175 | try: | |
176 | ret, should_ret = f(self, stmt, local_vars, allow_recursion, *args, **kwargs) | |
177 | except Exception as e: | |
178 | if cls.ENABLED: | |
179 | if isinstance(e, ExtractorError): | |
180 | e = e.orig_msg | |
181 | cls.write('=> Raises:', e, '<-|', stmt, level=allow_recursion) | |
182 | raise | |
183 | if cls.ENABLED and stmt.strip(): | |
184 | cls.write(['->', '=>'][should_ret], repr(ret), '<-|', stmt, level=allow_recursion) | |
185 | return ret, should_ret | |
186 | return interpret_statement | |
187 | ||
188 | ||
189 | class JSInterpreter: | |
190 | __named_object_counter = 0 | |
191 | ||
192 | _RE_FLAGS = { | |
193 | # special knowledge: Python's re flags are bitmask values, current max 128 | |
194 | # invent new bitmask values well above that for literal parsing | |
195 | # TODO: new pattern class to execute matches with these flags | |
196 | 'd': 1024, # Generate indices for substring matches | |
197 | 'g': 2048, # Global search | |
198 | 'i': re.I, # Case-insensitive search | |
199 | 'm': re.M, # Multi-line search | |
200 | 's': re.S, # Allows . to match newline characters | |
201 | 'u': re.U, # Treat a pattern as a sequence of unicode code points | |
202 | 'y': 4096, # Perform a "sticky" search that matches starting at the current position in the target string | |
203 | } | |
204 | ||
205 | _EXC_NAME = '__yt_dlp_exception__' | |
206 | ||
207 | def __init__(self, code, objects=None): | |
208 | self.code, self._functions = code, {} | |
209 | self._objects = {} if objects is None else objects | |
210 | ||
211 | class Exception(ExtractorError): | |
212 | def __init__(self, msg, expr=None, *args, **kwargs): | |
213 | if expr is not None: | |
214 | msg = f'{msg.rstrip()} in: {truncate_string(expr, 50, 50)}' | |
215 | super().__init__(msg, *args, **kwargs) | |
216 | ||
217 | def _named_object(self, namespace, obj): | |
218 | self.__named_object_counter += 1 | |
219 | name = f'__yt_dlp_jsinterp_obj{self.__named_object_counter}' | |
220 | namespace[name] = obj | |
221 | return name | |
222 | ||
223 | @classmethod | |
224 | def _regex_flags(cls, expr): | |
225 | flags = 0 | |
226 | if not expr: | |
227 | return flags, expr | |
228 | for idx, ch in enumerate(expr): | |
229 | if ch not in cls._RE_FLAGS: | |
230 | break | |
231 | flags |= cls._RE_FLAGS[ch] | |
232 | return flags, expr[idx + 1:] | |
233 | ||
234 | @staticmethod | |
235 | def _separate(expr, delim=',', max_split=None): | |
236 | OP_CHARS = '+-*/%&|^=<>!,;{}:' | |
237 | if not expr: | |
238 | return | |
239 | counters = {k: 0 for k in _MATCHING_PARENS.values()} | |
240 | start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1 | |
241 | in_quote, escaping, after_op, in_regex_char_group = None, False, True, False | |
242 | for idx, char in enumerate(expr): | |
243 | if not in_quote and char in _MATCHING_PARENS: | |
244 | counters[_MATCHING_PARENS[char]] += 1 | |
245 | elif not in_quote and char in counters: | |
246 | counters[char] -= 1 | |
247 | elif not escaping and char in _QUOTES and in_quote in (char, None): | |
248 | if in_quote or after_op or char != '/': | |
249 | in_quote = None if in_quote and not in_regex_char_group else char | |
250 | elif in_quote == '/' and char in '[]': | |
251 | in_regex_char_group = char == '[' | |
252 | escaping = not escaping and in_quote and char == '\\' | |
253 | after_op = not in_quote and char in OP_CHARS or (char.isspace() and after_op) | |
254 | ||
255 | if char != delim[pos] or any(counters.values()) or in_quote: | |
256 | pos = 0 | |
257 | continue | |
258 | elif pos != delim_len: | |
259 | pos += 1 | |
260 | continue | |
261 | yield expr[start: idx - delim_len] | |
262 | start, pos = idx + 1, 0 | |
263 | splits += 1 | |
264 | if max_split and splits >= max_split: | |
265 | break | |
266 | yield expr[start:] | |
267 | ||
268 | @classmethod | |
269 | def _separate_at_paren(cls, expr, delim): | |
270 | separated = list(cls._separate(expr, delim, 1)) | |
271 | if len(separated) < 2: | |
272 | raise cls.Exception(f'No terminating paren {delim}', expr) | |
273 | return separated[0][1:].strip(), separated[1].strip() | |
274 | ||
275 | def _operator(self, op, left_val, right_expr, expr, local_vars, allow_recursion): | |
276 | if op in ('||', '&&'): | |
277 | if (op == '&&') ^ _js_ternary(left_val): | |
278 | return left_val # short circuiting | |
279 | elif op == '??': | |
280 | if left_val not in (None, JS_Undefined): | |
281 | return left_val | |
282 | elif op == '?': | |
283 | right_expr = _js_ternary(left_val, *self._separate(right_expr, ':', 1)) | |
284 | ||
285 | right_val = self.interpret_expression(right_expr, local_vars, allow_recursion) | |
286 | if not _OPERATORS.get(op): | |
287 | return right_val | |
288 | ||
289 | try: | |
290 | return _OPERATORS[op](left_val, right_val) | |
291 | except Exception as e: | |
292 | raise self.Exception(f'Failed to evaluate {left_val!r} {op} {right_val!r}', expr, cause=e) | |
293 | ||
294 | def _index(self, obj, idx, allow_undefined=False): | |
295 | if idx == 'length': | |
296 | return len(obj) | |
297 | try: | |
298 | return obj[int(idx)] if isinstance(obj, list) else obj[idx] | |
299 | except Exception as e: | |
300 | if allow_undefined: | |
301 | return JS_Undefined | |
302 | raise self.Exception(f'Cannot get index {idx}', repr(obj), cause=e) | |
303 | ||
304 | def _dump(self, obj, namespace): | |
305 | try: | |
306 | return json.dumps(obj) | |
307 | except TypeError: | |
308 | return self._named_object(namespace, obj) | |
309 | ||
310 | @Debugger.wrap_interpreter | |
311 | def interpret_statement(self, stmt, local_vars, allow_recursion=100): | |
312 | if allow_recursion < 0: | |
313 | raise self.Exception('Recursion limit reached') | |
314 | allow_recursion -= 1 | |
315 | ||
316 | should_return = False | |
317 | sub_statements = list(self._separate(stmt, ';')) or [''] | |
318 | expr = stmt = sub_statements.pop().strip() | |
319 | ||
320 | for sub_stmt in sub_statements: | |
321 | ret, should_return = self.interpret_statement(sub_stmt, local_vars, allow_recursion) | |
322 | if should_return: | |
323 | return ret, should_return | |
324 | ||
325 | m = re.match(r'(?P<var>(?:var|const|let)\s)|return(?:\s+|(?=["\'])|$)|(?P<throw>throw\s+)', stmt) | |
326 | if m: | |
327 | expr = stmt[len(m.group(0)):].strip() | |
328 | if m.group('throw'): | |
329 | raise JS_Throw(self.interpret_expression(expr, local_vars, allow_recursion)) | |
330 | should_return = not m.group('var') | |
331 | if not expr: | |
332 | return None, should_return | |
333 | ||
334 | if expr[0] in _QUOTES: | |
335 | inner, outer = self._separate(expr, expr[0], 1) | |
336 | if expr[0] == '/': | |
337 | flags, outer = self._regex_flags(outer) | |
338 | inner = re.compile(inner[1:], flags=flags) | |
339 | else: | |
340 | inner = json.loads(js_to_json(f'{inner}{expr[0]}', strict=True)) | |
341 | if not outer: | |
342 | return inner, should_return | |
343 | expr = self._named_object(local_vars, inner) + outer | |
344 | ||
345 | if expr.startswith('new '): | |
346 | obj = expr[4:] | |
347 | if obj.startswith('Date('): | |
348 | left, right = self._separate_at_paren(obj[4:], ')') | |
349 | expr = unified_timestamp( | |
350 | self.interpret_expression(left, local_vars, allow_recursion), False) | |
351 | if not expr: | |
352 | raise self.Exception(f'Failed to parse date {left!r}', expr) | |
353 | expr = self._dump(int(expr * 1000), local_vars) + right | |
354 | else: | |
355 | raise self.Exception(f'Unsupported object {obj}', expr) | |
356 | ||
357 | if expr.startswith('void '): | |
358 | left = self.interpret_expression(expr[5:], local_vars, allow_recursion) | |
359 | return None, should_return | |
360 | ||
361 | if expr.startswith('{'): | |
362 | inner, outer = self._separate_at_paren(expr, '}') | |
363 | # Look for Map first | |
364 | sub_expressions = [list(self._separate(sub_expr.strip(), ':', 1)) for sub_expr in self._separate(inner)] | |
365 | if all(len(sub_expr) == 2 for sub_expr in sub_expressions): | |
366 | def dict_item(key, val): | |
367 | val = self.interpret_expression(val, local_vars, allow_recursion) | |
368 | if re.match(_NAME_RE, key): | |
369 | return key, val | |
370 | return self.interpret_expression(key, local_vars, allow_recursion), val | |
371 | ||
372 | return dict(dict_item(k, v) for k, v in sub_expressions), should_return | |
373 | ||
374 | inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion) | |
375 | if not outer or should_abort: | |
376 | return inner, should_abort or should_return | |
377 | else: | |
378 | expr = self._dump(inner, local_vars) + outer | |
379 | ||
380 | if expr.startswith('('): | |
381 | inner, outer = self._separate_at_paren(expr, ')') | |
382 | inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion) | |
383 | if not outer or should_abort: | |
384 | return inner, should_abort or should_return | |
385 | else: | |
386 | expr = self._dump(inner, local_vars) + outer | |
387 | ||
388 | if expr.startswith('['): | |
389 | inner, outer = self._separate_at_paren(expr, ']') | |
390 | name = self._named_object(local_vars, [ | |
391 | self.interpret_expression(item, local_vars, allow_recursion) | |
392 | for item in self._separate(inner)]) | |
393 | expr = name + outer | |
394 | ||
395 | m = re.match(rf'''(?x) | |
396 | (?P<try>try|finally)\s*| | |
397 | (?P<catch>catch\s*(?P<err>\(\s*{_NAME_RE}\s*\)))| | |
398 | (?P<switch>switch)\s*\(| | |
399 | (?P<for>for)\s*\(|''', expr) | |
400 | if m and m.group('try'): | |
401 | if expr[m.end()] == '{': | |
402 | try_expr, expr = self._separate_at_paren(expr[m.end():], '}') | |
403 | else: | |
404 | try_expr, expr = expr[m.end() - 1:], '' | |
405 | try: | |
406 | ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion) | |
407 | if should_abort: | |
408 | return ret, True | |
409 | except JS_Throw as e: | |
410 | local_vars[self._EXC_NAME] = e.error | |
411 | except Exception as e: | |
412 | # XXX: This works for now, but makes debugging future issues very hard | |
413 | local_vars[self._EXC_NAME] = e | |
414 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) | |
415 | return ret, should_abort or should_return | |
416 | ||
417 | elif m and m.group('catch'): | |
418 | catch_expr, expr = self._separate_at_paren(expr[m.end():], '}') | |
419 | if self._EXC_NAME in local_vars: | |
420 | catch_vars = local_vars.new_child({m.group('err'): local_vars.pop(self._EXC_NAME)}) | |
421 | ret, should_abort = self.interpret_statement(catch_expr, catch_vars, allow_recursion) | |
422 | if should_abort: | |
423 | return ret, True | |
424 | ||
425 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) | |
426 | return ret, should_abort or should_return | |
427 | ||
428 | elif m and m.group('for'): | |
429 | constructor, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') | |
430 | if remaining.startswith('{'): | |
431 | body, expr = self._separate_at_paren(remaining, '}') | |
432 | else: | |
433 | switch_m = re.match(r'switch\s*\(', remaining) # FIXME | |
434 | if switch_m: | |
435 | switch_val, remaining = self._separate_at_paren(remaining[switch_m.end() - 1:], ')') | |
436 | body, expr = self._separate_at_paren(remaining, '}') | |
437 | body = 'switch(%s){%s}' % (switch_val, body) | |
438 | else: | |
439 | body, expr = remaining, '' | |
440 | start, cndn, increment = self._separate(constructor, ';') | |
441 | self.interpret_expression(start, local_vars, allow_recursion) | |
442 | while True: | |
443 | if not _js_ternary(self.interpret_expression(cndn, local_vars, allow_recursion)): | |
444 | break | |
445 | try: | |
446 | ret, should_abort = self.interpret_statement(body, local_vars, allow_recursion) | |
447 | if should_abort: | |
448 | return ret, True | |
449 | except JS_Break: | |
450 | break | |
451 | except JS_Continue: | |
452 | pass | |
453 | self.interpret_expression(increment, local_vars, allow_recursion) | |
454 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) | |
455 | return ret, should_abort or should_return | |
456 | ||
457 | elif m and m.group('switch'): | |
458 | switch_val, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') | |
459 | switch_val = self.interpret_expression(switch_val, local_vars, allow_recursion) | |
460 | body, expr = self._separate_at_paren(remaining, '}') | |
461 | items = body.replace('default:', 'case default:').split('case ')[1:] | |
462 | for default in (False, True): | |
463 | matched = False | |
464 | for item in items: | |
465 | case, stmt = (i.strip() for i in self._separate(item, ':', 1)) | |
466 | if default: | |
467 | matched = matched or case == 'default' | |
468 | elif not matched: | |
469 | matched = (case != 'default' | |
470 | and switch_val == self.interpret_expression(case, local_vars, allow_recursion)) | |
471 | if not matched: | |
472 | continue | |
473 | try: | |
474 | ret, should_abort = self.interpret_statement(stmt, local_vars, allow_recursion) | |
475 | if should_abort: | |
476 | return ret | |
477 | except JS_Break: | |
478 | break | |
479 | if matched: | |
480 | break | |
481 | ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) | |
482 | return ret, should_abort or should_return | |
483 | ||
484 | # Comma separated statements | |
485 | sub_expressions = list(self._separate(expr)) | |
486 | if len(sub_expressions) > 1: | |
487 | for sub_expr in sub_expressions: | |
488 | ret, should_abort = self.interpret_statement(sub_expr, local_vars, allow_recursion) | |
489 | if should_abort: | |
490 | return ret, True | |
491 | return ret, False | |
492 | ||
493 | for m in re.finditer(rf'''(?x) | |
494 | (?P<pre_sign>\+\+|--)(?P<var1>{_NAME_RE})| | |
495 | (?P<var2>{_NAME_RE})(?P<post_sign>\+\+|--)''', expr): | |
496 | var = m.group('var1') or m.group('var2') | |
497 | start, end = m.span() | |
498 | sign = m.group('pre_sign') or m.group('post_sign') | |
499 | ret = local_vars[var] | |
500 | local_vars[var] += 1 if sign[0] == '+' else -1 | |
501 | if m.group('pre_sign'): | |
502 | ret = local_vars[var] | |
503 | expr = expr[:start] + self._dump(ret, local_vars) + expr[end:] | |
504 | ||
505 | if not expr: | |
506 | return None, should_return | |
507 | ||
508 | m = re.match(fr'''(?x) | |
509 | (?P<assign> | |
510 | (?P<out>{_NAME_RE})(?:\[(?P<index>[^\]]+?)\])?\s* | |
511 | (?P<op>{"|".join(map(re.escape, set(_OPERATORS) - _COMP_OPERATORS))})? | |
512 | =(?!=)(?P<expr>.*)$ | |
513 | )|(?P<return> | |
514 | (?!if|return|true|false|null|undefined|NaN)(?P<name>{_NAME_RE})$ | |
515 | )|(?P<indexing> | |
516 | (?P<in>{_NAME_RE})\[(?P<idx>.+)\]$ | |
517 | )|(?P<attribute> | |
518 | (?P<var>{_NAME_RE})(?:(?P<nullish>\?)?\.(?P<member>[^(]+)|\[(?P<member2>[^\]]+)\])\s* | |
519 | )|(?P<function> | |
520 | (?P<fname>{_NAME_RE})\((?P<args>.*)\)$ | |
521 | )''', expr) | |
522 | if m and m.group('assign'): | |
523 | left_val = local_vars.get(m.group('out')) | |
524 | ||
525 | if not m.group('index'): | |
526 | local_vars[m.group('out')] = self._operator( | |
527 | m.group('op'), left_val, m.group('expr'), expr, local_vars, allow_recursion) | |
528 | return local_vars[m.group('out')], should_return | |
529 | elif left_val in (None, JS_Undefined): | |
530 | raise self.Exception(f'Cannot index undefined variable {m.group("out")}', expr) | |
531 | ||
532 | idx = self.interpret_expression(m.group('index'), local_vars, allow_recursion) | |
533 | if not isinstance(idx, (int, float)): | |
534 | raise self.Exception(f'List index {idx} must be integer', expr) | |
535 | idx = int(idx) | |
536 | left_val[idx] = self._operator( | |
537 | m.group('op'), self._index(left_val, idx), m.group('expr'), expr, local_vars, allow_recursion) | |
538 | return left_val[idx], should_return | |
539 | ||
540 | elif expr.isdigit(): | |
541 | return int(expr), should_return | |
542 | ||
543 | elif expr == 'break': | |
544 | raise JS_Break() | |
545 | elif expr == 'continue': | |
546 | raise JS_Continue() | |
547 | elif expr == 'undefined': | |
548 | return JS_Undefined, should_return | |
549 | elif expr == 'NaN': | |
550 | return float('NaN'), should_return | |
551 | ||
552 | elif m and m.group('return'): | |
553 | return local_vars.get(m.group('name'), JS_Undefined), should_return | |
554 | ||
555 | with contextlib.suppress(ValueError): | |
556 | return json.loads(js_to_json(expr, strict=True)), should_return | |
557 | ||
558 | if m and m.group('indexing'): | |
559 | val = local_vars[m.group('in')] | |
560 | idx = self.interpret_expression(m.group('idx'), local_vars, allow_recursion) | |
561 | return self._index(val, idx), should_return | |
562 | ||
563 | for op in _OPERATORS: | |
564 | separated = list(self._separate(expr, op)) | |
565 | right_expr = separated.pop() | |
566 | while True: | |
567 | if op in '?<>*-' and len(separated) > 1 and not separated[-1].strip(): | |
568 | separated.pop() | |
569 | elif not (separated and op == '?' and right_expr.startswith('.')): | |
570 | break | |
571 | right_expr = f'{op}{right_expr}' | |
572 | if op != '-': | |
573 | right_expr = f'{separated.pop()}{op}{right_expr}' | |
574 | if not separated: | |
575 | continue | |
576 | left_val = self.interpret_expression(op.join(separated), local_vars, allow_recursion) | |
577 | return self._operator(op, left_val, right_expr, expr, local_vars, allow_recursion), should_return | |
578 | ||
579 | if m and m.group('attribute'): | |
580 | variable, member, nullish = m.group('var', 'member', 'nullish') | |
581 | if not member: | |
582 | member = self.interpret_expression(m.group('member2'), local_vars, allow_recursion) | |
583 | arg_str = expr[m.end():] | |
584 | if arg_str.startswith('('): | |
585 | arg_str, remaining = self._separate_at_paren(arg_str, ')') | |
586 | else: | |
587 | arg_str, remaining = None, arg_str | |
588 | ||
589 | def assertion(cndn, msg): | |
590 | """ assert, but without risk of getting optimized out """ | |
591 | if not cndn: | |
592 | raise self.Exception(f'{member} {msg}', expr) | |
593 | ||
594 | def eval_method(): | |
595 | if (variable, member) == ('console', 'debug'): | |
596 | if Debugger.ENABLED: | |
597 | Debugger.write(self.interpret_expression(f'[{arg_str}]', local_vars, allow_recursion)) | |
598 | return | |
599 | ||
600 | types = { | |
601 | 'String': str, | |
602 | 'Math': float, | |
603 | } | |
604 | obj = local_vars.get(variable, types.get(variable, NO_DEFAULT)) | |
605 | if obj is NO_DEFAULT: | |
606 | if variable not in self._objects: | |
607 | try: | |
608 | self._objects[variable] = self.extract_object(variable) | |
609 | except self.Exception: | |
610 | if not nullish: | |
611 | raise | |
612 | obj = self._objects.get(variable, JS_Undefined) | |
613 | ||
614 | if nullish and obj is JS_Undefined: | |
615 | return JS_Undefined | |
616 | ||
617 | # Member access | |
618 | if arg_str is None: | |
619 | return self._index(obj, member, nullish) | |
620 | ||
621 | # Function call | |
622 | argvals = [ | |
623 | self.interpret_expression(v, local_vars, allow_recursion) | |
624 | for v in self._separate(arg_str)] | |
625 | ||
626 | if obj == str: | |
627 | if member == 'fromCharCode': | |
628 | assertion(argvals, 'takes one or more arguments') | |
629 | return ''.join(map(chr, argvals)) | |
630 | raise self.Exception(f'Unsupported String method {member}', expr) | |
631 | elif obj == float: | |
632 | if member == 'pow': | |
633 | assertion(len(argvals) == 2, 'takes two arguments') | |
634 | return argvals[0] ** argvals[1] | |
635 | raise self.Exception(f'Unsupported Math method {member}', expr) | |
636 | ||
637 | if member == 'split': | |
638 | assertion(argvals, 'takes one or more arguments') | |
639 | assertion(len(argvals) == 1, 'with limit argument is not implemented') | |
640 | return obj.split(argvals[0]) if argvals[0] else list(obj) | |
641 | elif member == 'join': | |
642 | assertion(isinstance(obj, list), 'must be applied on a list') | |
643 | assertion(len(argvals) == 1, 'takes exactly one argument') | |
644 | return argvals[0].join(obj) | |
645 | elif member == 'reverse': | |
646 | assertion(not argvals, 'does not take any arguments') | |
647 | obj.reverse() | |
648 | return obj | |
649 | elif member == 'slice': | |
650 | assertion(isinstance(obj, list), 'must be applied on a list') | |
651 | assertion(len(argvals) == 1, 'takes exactly one argument') | |
652 | return obj[argvals[0]:] | |
653 | elif member == 'splice': | |
654 | assertion(isinstance(obj, list), 'must be applied on a list') | |
655 | assertion(argvals, 'takes one or more arguments') | |
656 | index, howMany = map(int, (argvals + [len(obj)])[:2]) | |
657 | if index < 0: | |
658 | index += len(obj) | |
659 | add_items = argvals[2:] | |
660 | res = [] | |
661 | for i in range(index, min(index + howMany, len(obj))): | |
662 | res.append(obj.pop(index)) | |
663 | for i, item in enumerate(add_items): | |
664 | obj.insert(index + i, item) | |
665 | return res | |
666 | elif member == 'unshift': | |
667 | assertion(isinstance(obj, list), 'must be applied on a list') | |
668 | assertion(argvals, 'takes one or more arguments') | |
669 | for item in reversed(argvals): | |
670 | obj.insert(0, item) | |
671 | return obj | |
672 | elif member == 'pop': | |
673 | assertion(isinstance(obj, list), 'must be applied on a list') | |
674 | assertion(not argvals, 'does not take any arguments') | |
675 | if not obj: | |
676 | return | |
677 | return obj.pop() | |
678 | elif member == 'push': | |
679 | assertion(argvals, 'takes one or more arguments') | |
680 | obj.extend(argvals) | |
681 | return obj | |
682 | elif member == 'forEach': | |
683 | assertion(argvals, 'takes one or more arguments') | |
684 | assertion(len(argvals) <= 2, 'takes at-most 2 arguments') | |
685 | f, this = (argvals + [''])[:2] | |
686 | return [f((item, idx, obj), {'this': this}, allow_recursion) for idx, item in enumerate(obj)] | |
687 | elif member == 'indexOf': | |
688 | assertion(argvals, 'takes one or more arguments') | |
689 | assertion(len(argvals) <= 2, 'takes at-most 2 arguments') | |
690 | idx, start = (argvals + [0])[:2] | |
691 | try: | |
692 | return obj.index(idx, start) | |
693 | except ValueError: | |
694 | return -1 | |
695 | ||
696 | idx = int(member) if isinstance(obj, list) else member | |
697 | return obj[idx](argvals, allow_recursion=allow_recursion) | |
698 | ||
699 | if remaining: | |
700 | ret, should_abort = self.interpret_statement( | |
701 | self._named_object(local_vars, eval_method()) + remaining, | |
702 | local_vars, allow_recursion) | |
703 | return ret, should_return or should_abort | |
704 | else: | |
705 | return eval_method(), should_return | |
706 | ||
707 | elif m and m.group('function'): | |
708 | fname = m.group('fname') | |
709 | argvals = [self.interpret_expression(v, local_vars, allow_recursion) | |
710 | for v in self._separate(m.group('args'))] | |
711 | if fname in local_vars: | |
712 | return local_vars[fname](argvals, allow_recursion=allow_recursion), should_return | |
713 | elif fname not in self._functions: | |
714 | self._functions[fname] = self.extract_function(fname) | |
715 | return self._functions[fname](argvals, allow_recursion=allow_recursion), should_return | |
716 | ||
717 | raise self.Exception( | |
718 | f'Unsupported JS expression {truncate_string(expr, 20, 20) if expr != stmt else ""}', stmt) | |
719 | ||
720 | def interpret_expression(self, expr, local_vars, allow_recursion): | |
721 | ret, should_return = self.interpret_statement(expr, local_vars, allow_recursion) | |
722 | if should_return: | |
723 | raise self.Exception('Cannot return from an expression', expr) | |
724 | return ret | |
725 | ||
726 | def extract_object(self, objname): | |
727 | _FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')''' | |
728 | obj = {} | |
729 | obj_m = re.search( | |
730 | r'''(?x) | |
731 | (?<!this\.)%s\s*=\s*{\s* | |
732 | (?P<fields>(%s\s*:\s*function\s*\(.*?\)\s*{.*?}(?:,\s*)?)*) | |
733 | }\s*; | |
734 | ''' % (re.escape(objname), _FUNC_NAME_RE), | |
735 | self.code) | |
736 | if not obj_m: | |
737 | raise self.Exception(f'Could not find object {objname}') | |
738 | fields = obj_m.group('fields') | |
739 | # Currently, it only supports function definitions | |
740 | fields_m = re.finditer( | |
741 | r'''(?x) | |
742 | (?P<key>%s)\s*:\s*function\s*\((?P<args>(?:%s|,)*)\){(?P<code>[^}]+)} | |
743 | ''' % (_FUNC_NAME_RE, _NAME_RE), | |
744 | fields) | |
745 | for f in fields_m: | |
746 | argnames = f.group('args').split(',') | |
747 | obj[remove_quotes(f.group('key'))] = self.build_function(argnames, f.group('code')) | |
748 | ||
749 | return obj | |
750 | ||
751 | def extract_function_code(self, funcname): | |
752 | """ @returns argnames, code """ | |
753 | func_m = re.search( | |
754 | r'''(?xs) | |
755 | (?: | |
756 | function\s+%(name)s| | |
757 | [{;,]\s*%(name)s\s*=\s*function| | |
758 | (?:var|const|let)\s+%(name)s\s*=\s*function | |
759 | )\s* | |
760 | \((?P<args>[^)]*)\)\s* | |
761 | (?P<code>{.+})''' % {'name': re.escape(funcname)}, | |
762 | self.code) | |
763 | code, _ = self._separate_at_paren(func_m.group('code'), '}') | |
764 | if func_m is None: | |
765 | raise self.Exception(f'Could not find JS function "{funcname}"') | |
766 | return [x.strip() for x in func_m.group('args').split(',')], code | |
767 | ||
768 | def extract_function(self, funcname): | |
769 | return self.extract_function_from_code(*self.extract_function_code(funcname)) | |
770 | ||
771 | def extract_function_from_code(self, argnames, code, *global_stack): | |
772 | local_vars = {} | |
773 | while True: | |
774 | mobj = re.search(r'function\((?P<args>[^)]*)\)\s*{', code) | |
775 | if mobj is None: | |
776 | break | |
777 | start, body_start = mobj.span() | |
778 | body, remaining = self._separate_at_paren(code[body_start - 1:], '}') | |
779 | name = self._named_object(local_vars, self.extract_function_from_code( | |
780 | [x.strip() for x in mobj.group('args').split(',')], | |
781 | body, local_vars, *global_stack)) | |
782 | code = code[:start] + name + remaining | |
783 | return self.build_function(argnames, code, local_vars, *global_stack) | |
784 | ||
785 | def call_function(self, funcname, *args): | |
786 | return self.extract_function(funcname)(args) | |
787 | ||
788 | def build_function(self, argnames, code, *global_stack): | |
789 | global_stack = list(global_stack) or [{}] | |
790 | argnames = tuple(argnames) | |
791 | ||
792 | def resf(args, kwargs={}, allow_recursion=100): | |
793 | global_stack[0].update(itertools.zip_longest(argnames, args, fillvalue=None)) | |
794 | global_stack[0].update(kwargs) | |
795 | var_stack = LocalNameSpace(*global_stack) | |
796 | ret, should_abort = self.interpret_statement(code.replace('\n', ' '), var_stack, allow_recursion - 1) | |
797 | if should_abort: | |
798 | return ret | |
799 | return resf |