1
1
mirror of https://github.com/ytdl-org/youtube-dl synced 2024-11-27 15:16:57 +01:00

[jsinterp] Improve try/catch/finally support

This commit is contained in:
dirkf 2022-08-26 08:17:54 +01:00 committed by Bartosz Białas
parent 660cd68133
commit 8601874938
2 changed files with 61 additions and 41 deletions

View File

@ -74,6 +74,9 @@ class TestJSInterpreter(unittest.TestCase):
jsi = JSInterpreter('function f(){return 0 ?? 42;}') jsi = JSInterpreter('function f(){return 0 ?? 42;}')
self.assertEqual(jsi.call_function('f'), 0) self.assertEqual(jsi.call_function('f'), 0)
jsi = JSInterpreter('function f(){return "life, the universe and everything" < 42;}')
self.assertFalse(jsi.call_function('f'))
def test_array_access(self): def test_array_access(self):
jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}') jsi = JSInterpreter('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}')
self.assertEqual(jsi.call_function('f'), [5, 2, 7]) self.assertEqual(jsi.call_function('f'), [5, 2, 7])
@ -198,7 +201,6 @@ class TestJSInterpreter(unittest.TestCase):
''') ''')
self.assertEqual(jsi.call_function('x'), 5) self.assertEqual(jsi.call_function('x'), 5)
@unittest.expectedFailure
def test_finally(self): def test_finally(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x() { try{throw 10} finally {return 42} } function x() { try{throw 10} finally {return 42} }
@ -212,7 +214,7 @@ class TestJSInterpreter(unittest.TestCase):
def test_nested_try(self): def test_nested_try(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x() {try { function x() {try {
try{throw 10} finally {throw 42} try{throw 10} finally {throw 42}
} catch(e){return 5} } } catch(e){return 5} }
''') ''')
self.assertEqual(jsi.call_function('x'), 5) self.assertEqual(jsi.call_function('x'), 5)
@ -229,6 +231,14 @@ class TestJSInterpreter(unittest.TestCase):
''') ''')
self.assertEqual(jsi.call_function('x'), 0) self.assertEqual(jsi.call_function('x'), 0)
def test_for_loop_try(self):
jsi = JSInterpreter('''
function x() {
for (i=0; i-10; i++) { try { if (i == 5) throw i} catch {return 10} finally {break} };
return 42 }
''')
self.assertEqual(jsi.call_function('x'), 42)
def test_literal_list(self): def test_literal_list(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x() { return [1, 2, "asdf", [5, 6, 7]][3] } function x() { return [1, 2, "asdf", [5, 6, 7]][3] }

View File

@ -5,7 +5,6 @@ import json
import math import math
import operator import operator
import re import re
from collections import Counter
from .utils import ( from .utils import (
error_to_compat_str, error_to_compat_str,
@ -15,6 +14,7 @@ from .utils import (
unified_timestamp, unified_timestamp,
) )
from .compat import ( from .compat import (
compat_basestring,
compat_collections_chain_map as ChainMap, compat_collections_chain_map as ChainMap,
compat_itertools_zip_longest as zip_longest, compat_itertools_zip_longest as zip_longest,
compat_str, compat_str,
@ -76,6 +76,10 @@ def _js_comp_op(op):
def wrapped(a, b): def wrapped(a, b):
if JS_Undefined in (a, b): if JS_Undefined in (a, b):
return False return False
if isinstance(a, compat_basestring):
b = compat_str(b or 0)
elif isinstance(b, compat_basestring):
a = compat_str(a or 0)
return op(a or 0, b or 0) return op(a or 0, b or 0)
return wrapped return wrapped
@ -195,7 +199,6 @@ class JSInterpreter(object):
'y': 4096, # Perform a "sticky" search that matches starting at the current position in the target string 'y': 4096, # Perform a "sticky" search that matches starting at the current position in the target string
} }
_EXC_NAME = '__youtube_dl_exception__'
_OBJ_NAME = '__youtube_dl_jsinterp_obj' _OBJ_NAME = '__youtube_dl_jsinterp_obj'
OP_CHARS = None OP_CHARS = None
@ -242,9 +245,8 @@ class JSInterpreter(object):
def _separate(cls, expr, delim=',', max_split=None, skip_delims=None): def _separate(cls, expr, delim=',', max_split=None, skip_delims=None):
if not expr: if not expr:
return return
# collections.Counter() is ~10% slower # collections.Counter() is ~10% slower in both 2.7 and 3.9
counters = {k: 0 for k in _MATCHING_PARENS.values()} counters = {k: 0 for k in _MATCHING_PARENS.values()}
# counters = Counter()
start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1 start, splits, pos, delim_len = 0, 0, 0, len(delim) - 1
in_quote, escaping, skipping = None, False, 0 in_quote, escaping, skipping = None, False, 0
after_op, in_regex_char_group, skip_re = True, False, 0 after_op, in_regex_char_group, skip_re = True, False, 0
@ -291,7 +293,9 @@ class JSInterpreter(object):
yield expr[start:] yield expr[start:]
@classmethod @classmethod
def _separate_at_paren(cls, expr, delim): def _separate_at_paren(cls, expr, delim=None):
if delim is None:
delim = expr and _MATCHING_PARENS[expr[0]]
separated = list(cls._separate(expr, delim, 1)) separated = list(cls._separate(expr, delim, 1))
if len(separated) < 2: if len(separated) < 2:
@ -376,7 +380,7 @@ class JSInterpreter(object):
if expr.startswith('new '): if expr.startswith('new '):
obj = expr[4:] obj = expr[4:]
if obj.startswith('Date('): if obj.startswith('Date('):
left, right = self._separate_at_paren(obj[4:], ')') left, right = self._separate_at_paren(obj[4:])
expr = unified_timestamp( expr = unified_timestamp(
self.interpret_expression(left, local_vars, allow_recursion), False) self.interpret_expression(left, local_vars, allow_recursion), False)
if not expr: if not expr:
@ -390,7 +394,7 @@ class JSInterpreter(object):
return None, should_return return None, should_return
if expr.startswith('{'): if expr.startswith('{'):
inner, outer = self._separate_at_paren(expr, '}') inner, outer = self._separate_at_paren(expr)
# try for object expression (Map) # try for object expression (Map)
sub_expressions = [list(self._separate(sub_expr.strip(), ':', 1)) for sub_expr in self._separate(inner)] sub_expressions = [list(self._separate(sub_expr.strip(), ':', 1)) for sub_expr in self._separate(inner)]
if all(len(sub_expr) == 2 for sub_expr in sub_expressions): if all(len(sub_expr) == 2 for sub_expr in sub_expressions):
@ -406,7 +410,7 @@ class JSInterpreter(object):
expr = self._dump(inner, local_vars) + outer expr = self._dump(inner, local_vars) + outer
if expr.startswith('('): if expr.startswith('('):
inner, outer = self._separate_at_paren(expr, ')') inner, outer = self._separate_at_paren(expr)
inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion) inner, should_abort = self.interpret_statement(inner, local_vars, allow_recursion)
if not outer or should_abort: if not outer or should_abort:
return inner, should_abort or should_return return inner, should_abort or should_return
@ -414,57 +418,63 @@ class JSInterpreter(object):
expr = self._dump(inner, local_vars) + outer expr = self._dump(inner, local_vars) + outer
if expr.startswith('['): if expr.startswith('['):
inner, outer = self._separate_at_paren(expr, ']') inner, outer = self._separate_at_paren(expr)
name = self._named_object(local_vars, [ name = self._named_object(local_vars, [
self.interpret_expression(item, local_vars, allow_recursion) self.interpret_expression(item, local_vars, allow_recursion)
for item in self._separate(inner)]) for item in self._separate(inner)])
expr = name + outer expr = name + outer
m = re.match(r'''(?x) m = re.match(r'''(?x)
(?P<try>try|finally)\s*| (?P<try>try)\s*\{|
(?P<catch>catch\s*(?P<err>\(\s*{_NAME_RE}\s*\)))| (?P<switch>switch)\s*\(|
(?P<switch>switch)\s*\(| (?P<for>for)\s*\(
(?P<for>for)\s*\(| ''', expr)
'''.format(**globals()), expr)
md = m.groupdict() if m else {} md = m.groupdict() if m else {}
if md.get('try'): if md.get('try'):
if expr[m.end()] == '{': try_expr, expr = self._separate_at_paren(expr[m.end() - 1:])
try_expr, expr = self._separate_at_paren(expr[m.end():], '}') err = None
else:
try_expr, expr = expr[m.end() - 1:], ''
try: try:
ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion) ret, should_abort = self.interpret_statement(try_expr, local_vars, allow_recursion)
if should_abort: if should_abort:
return ret, True return ret, True
except JS_Throw as e:
local_vars[self._EXC_NAME] = e.error
except Exception as e: except Exception as e:
# XXX: This works for now, but makes debugging future issues very hard # XXX: This works for now, but makes debugging future issues very hard
local_vars[self._EXC_NAME] = e err = e
ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion)
return ret, should_abort or should_return
elif md.get('catch'): pending = (None, False)
m = re.match(r'catch\s*(?P<err>\(\s*{_NAME_RE}\s*\))?\{{'.format(**globals()), expr)
if m:
sub_expr, expr = self._separate_at_paren(expr[m.end() - 1:])
if err:
catch_vars = {}
if m.group('err'):
catch_vars[m.group('err')] = err.error if isinstance(err, JS_Throw) else err
catch_vars = local_vars.new_child(m=catch_vars)
err = None
pending = self.interpret_statement(sub_expr, catch_vars, allow_recursion)
catch_expr, expr = self._separate_at_paren(expr[m.end():], '}') m = re.match(r'finally\s*\{', expr)
if self._EXC_NAME in local_vars: if m:
catch_vars = local_vars.new_child({m.group('err'): local_vars.pop(self._EXC_NAME)}) sub_expr, expr = self._separate_at_paren(expr[m.end() - 1:])
ret, should_abort = self.interpret_statement(catch_expr, catch_vars, allow_recursion) ret, should_abort = self.interpret_statement(sub_expr, local_vars, allow_recursion)
if should_abort: if should_abort:
return ret, True return ret, True
ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) ret, should_abort = pending
if should_abort:
return ret, True
return ret, should_abort or should_return if err:
raise err
elif md.get('for'): elif md.get('for'):
constructor, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') constructor, remaining = self._separate_at_paren(expr[m.end() - 1:])
if remaining.startswith('{'): if remaining.startswith('{'):
body, expr = self._separate_at_paren(remaining, '}') body, expr = self._separate_at_paren(remaining)
else: else:
switch_m = re.match(r'switch\s*\(', remaining) # FIXME switch_m = re.match(r'switch\s*\(', remaining) # FIXME
if switch_m: if switch_m:
switch_val, remaining = self._separate_at_paren(remaining[switch_m.end() - 1:], ')') switch_val, remaining = self._separate_at_paren(remaining[switch_m.end() - 1:])
body, expr = self._separate_at_paren(remaining, '}') body, expr = self._separate_at_paren(remaining, '}')
body = 'switch(%s){%s}' % (switch_val, body) body = 'switch(%s){%s}' % (switch_val, body)
else: else:
@ -483,11 +493,9 @@ class JSInterpreter(object):
except JS_Continue: except JS_Continue:
pass pass
self.interpret_expression(increment, local_vars, allow_recursion) self.interpret_expression(increment, local_vars, allow_recursion)
ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion)
return ret, should_abort or should_return
elif md.get('switch'): elif md.get('switch'):
switch_val, remaining = self._separate_at_paren(expr[m.end() - 1:], ')') switch_val, remaining = self._separate_at_paren(expr[m.end() - 1:])
switch_val = self.interpret_expression(switch_val, local_vars, allow_recursion) switch_val = self.interpret_expression(switch_val, local_vars, allow_recursion)
body, expr = self._separate_at_paren(remaining, '}') body, expr = self._separate_at_paren(remaining, '}')
items = body.replace('default:', 'case default:').split('case ')[1:] items = body.replace('default:', 'case default:').split('case ')[1:]
@ -510,6 +518,8 @@ class JSInterpreter(object):
break break
if matched: if matched:
break break
if md:
ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion) ret, should_abort = self.interpret_statement(expr, local_vars, allow_recursion)
return ret, should_abort or should_return return ret, should_abort or should_return
@ -618,7 +628,7 @@ class JSInterpreter(object):
member = self.interpret_expression(m.group('member2'), local_vars, allow_recursion) member = self.interpret_expression(m.group('member2'), local_vars, allow_recursion)
arg_str = expr[m.end():] arg_str = expr[m.end():]
if arg_str.startswith('('): if arg_str.startswith('('):
arg_str, remaining = self._separate_at_paren(arg_str, ')') arg_str, remaining = self._separate_at_paren(arg_str)
else: else:
arg_str, remaining = None, arg_str arg_str, remaining = None, arg_str
@ -795,7 +805,7 @@ class JSInterpreter(object):
\((?P<args>[^)]*)\)\s* \((?P<args>[^)]*)\)\s*
(?P<code>{.+})''' % {'name': re.escape(funcname)}, (?P<code>{.+})''' % {'name': re.escape(funcname)},
self.code) self.code)
code, _ = self._separate_at_paren(func_m.group('code'), '}') # refine the match code, _ = self._separate_at_paren(func_m.group('code')) # refine the match
if func_m is None: if func_m is None:
raise self.Exception('Could not find JS function "{funcname}"'.format(**locals())) raise self.Exception('Could not find JS function "{funcname}"'.format(**locals()))
return self.build_arglist(func_m.group('args')), code return self.build_arglist(func_m.group('args')), code
@ -810,7 +820,7 @@ class JSInterpreter(object):
if mobj is None: if mobj is None:
break break
start, body_start = mobj.span() start, body_start = mobj.span()
body, remaining = self._separate_at_paren(code[body_start - 1:], '}') body, remaining = self._separate_at_paren(code[body_start - 1:])
name = self._named_object( name = self._named_object(
local_vars, local_vars,
self.extract_function_from_code( self.extract_function_from_code(