mirror of
https://github.com/ytdl-org/youtube-dl
synced 2024-12-29 18:45:51 +01:00
[jsinterp] Fix and improve arithmetic operations
* addition becomes concat with a string operand * improve handling of edgier cases * arithmetic in float like JS (more places need cast to int?) * increase test coverage
This commit is contained in:
parent
81e64cacf2
commit
5dee6213ed
@ -41,16 +41,27 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
self._test('function f(){return 42 + 7;}', 49)
|
self._test('function f(){return 42 + 7;}', 49)
|
||||||
self._test('function f(){return 42 + undefined;}', NaN)
|
self._test('function f(){return 42 + undefined;}', NaN)
|
||||||
self._test('function f(){return 42 + null;}', 42)
|
self._test('function f(){return 42 + null;}', 42)
|
||||||
|
self._test('function f(){return 1 + "";}', '1')
|
||||||
|
self._test('function f(){return 42 + "7";}', '427')
|
||||||
|
self._test('function f(){return false + true;}', 1)
|
||||||
|
self._test('function f(){return "false" + true;}', 'falsetrue')
|
||||||
|
self._test('function f(){return '
|
||||||
|
'1 + "2" + [3,4] + {k: 56} + null + undefined + Infinity;}',
|
||||||
|
'123,4[object Object]nullundefinedInfinity')
|
||||||
|
|
||||||
def test_sub(self):
|
def test_sub(self):
|
||||||
self._test('function f(){return 42 - 7;}', 35)
|
self._test('function f(){return 42 - 7;}', 35)
|
||||||
self._test('function f(){return 42 - undefined;}', NaN)
|
self._test('function f(){return 42 - undefined;}', NaN)
|
||||||
self._test('function f(){return 42 - null;}', 42)
|
self._test('function f(){return 42 - null;}', 42)
|
||||||
|
self._test('function f(){return 42 - "7";}', 35)
|
||||||
|
self._test('function f(){return 42 - "spam";}', NaN)
|
||||||
|
|
||||||
def test_mul(self):
|
def test_mul(self):
|
||||||
self._test('function f(){return 42 * 7;}', 294)
|
self._test('function f(){return 42 * 7;}', 294)
|
||||||
self._test('function f(){return 42 * undefined;}', NaN)
|
self._test('function f(){return 42 * undefined;}', NaN)
|
||||||
self._test('function f(){return 42 * null;}', 0)
|
self._test('function f(){return 42 * null;}', 0)
|
||||||
|
self._test('function f(){return 42 * "7";}', 294)
|
||||||
|
self._test('function f(){return 42 * "eggs";}', NaN)
|
||||||
|
|
||||||
def test_div(self):
|
def test_div(self):
|
||||||
jsi = JSInterpreter('function f(a, b){return a / b;}')
|
jsi = JSInterpreter('function f(a, b){return a / b;}')
|
||||||
@ -58,17 +69,26 @@ class TestJSInterpreter(unittest.TestCase):
|
|||||||
self._test(jsi, NaN, args=(JS_Undefined, 1))
|
self._test(jsi, NaN, args=(JS_Undefined, 1))
|
||||||
self._test(jsi, float('inf'), args=(2, 0))
|
self._test(jsi, float('inf'), args=(2, 0))
|
||||||
self._test(jsi, 0, args=(0, 3))
|
self._test(jsi, 0, args=(0, 3))
|
||||||
|
self._test(jsi, 6, args=(42, 7))
|
||||||
|
self._test(jsi, 0, args=(42, float('inf')))
|
||||||
|
self._test(jsi, 6, args=("42", 7))
|
||||||
|
self._test(jsi, NaN, args=("spam", 7))
|
||||||
|
|
||||||
def test_mod(self):
|
def test_mod(self):
|
||||||
self._test('function f(){return 42 % 7;}', 0)
|
self._test('function f(){return 42 % 7;}', 0)
|
||||||
self._test('function f(){return 42 % 0;}', NaN)
|
self._test('function f(){return 42 % 0;}', NaN)
|
||||||
self._test('function f(){return 42 % undefined;}', NaN)
|
self._test('function f(){return 42 % undefined;}', NaN)
|
||||||
|
self._test('function f(){return 42 % "7";}', 0)
|
||||||
|
self._test('function f(){return 42 % "beans";}', NaN)
|
||||||
|
|
||||||
def test_exp(self):
|
def test_exp(self):
|
||||||
self._test('function f(){return 42 ** 2;}', 1764)
|
self._test('function f(){return 42 ** 2;}', 1764)
|
||||||
self._test('function f(){return 42 ** undefined;}', NaN)
|
self._test('function f(){return 42 ** undefined;}', NaN)
|
||||||
self._test('function f(){return 42 ** null;}', 1)
|
self._test('function f(){return 42 ** null;}', 1)
|
||||||
|
self._test('function f(){return undefined ** 0;}', 1)
|
||||||
self._test('function f(){return undefined ** 42;}', NaN)
|
self._test('function f(){return undefined ** 42;}', NaN)
|
||||||
|
self._test('function f(){return 42 ** "2";}', 1764)
|
||||||
|
self._test('function f(){return 42 ** "spam";}', NaN)
|
||||||
|
|
||||||
def test_calc(self):
|
def test_calc(self):
|
||||||
self._test('function f(a){return 2*a+1;}', 7, args=[3])
|
self._test('function f(a){return 2*a+1;}', 7, args=[3])
|
||||||
|
@ -11,6 +11,7 @@ from functools import update_wrapper, wraps
|
|||||||
from .utils import (
|
from .utils import (
|
||||||
error_to_compat_str,
|
error_to_compat_str,
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
|
float_or_none,
|
||||||
js_to_json,
|
js_to_json,
|
||||||
remove_quotes,
|
remove_quotes,
|
||||||
unified_timestamp,
|
unified_timestamp,
|
||||||
@ -81,35 +82,47 @@ def _js_bit_op(op):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def _js_arith_op(op):
|
def _js_arith_op(op, div=False):
|
||||||
|
|
||||||
@wraps_op(op)
|
@wraps_op(op)
|
||||||
def wrapped(a, b):
|
def wrapped(a, b):
|
||||||
if JS_Undefined in (a, b):
|
if JS_Undefined in (a, b):
|
||||||
return _NaN
|
return _NaN
|
||||||
return op(a or 0, b or 0)
|
# null, "" --> 0
|
||||||
|
a, b = (float_or_none(
|
||||||
|
(x.strip() if isinstance(x, compat_basestring) else x) or 0,
|
||||||
|
default=_NaN) for x in (a, b))
|
||||||
|
if _NaN in (a, b):
|
||||||
|
return _NaN
|
||||||
|
try:
|
||||||
|
return op(a, b)
|
||||||
|
except ZeroDivisionError:
|
||||||
|
return _NaN if not (div and (a or b)) else _Infinity
|
||||||
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def _js_div(a, b):
|
_js_arith_add = _js_arith_op(operator.add)
|
||||||
if JS_Undefined in (a, b) or not (a or b):
|
|
||||||
return _NaN
|
|
||||||
return operator.truediv(a or 0, b) if b else _Infinity
|
|
||||||
|
|
||||||
|
|
||||||
def _js_mod(a, b):
|
def _js_add(a, b):
|
||||||
if JS_Undefined in (a, b) or not b:
|
if not (isinstance(a, compat_basestring) or isinstance(b, compat_basestring)):
|
||||||
return _NaN
|
return _js_arith_add(a, b)
|
||||||
return (a or 0) % b
|
if not isinstance(a, compat_basestring):
|
||||||
|
a = _js_toString(a)
|
||||||
|
elif not isinstance(b, compat_basestring):
|
||||||
|
b = _js_toString(b)
|
||||||
|
return operator.concat(a, b)
|
||||||
|
|
||||||
|
|
||||||
|
_js_mod = _js_arith_op(operator.mod)
|
||||||
|
__js_exp = _js_arith_op(operator.pow)
|
||||||
|
|
||||||
|
|
||||||
def _js_exp(a, b):
|
def _js_exp(a, b):
|
||||||
if not b:
|
if not b:
|
||||||
return 1 # even 0 ** 0 !!
|
return 1 # even 0 ** 0 !!
|
||||||
elif JS_Undefined in (a, b):
|
return __js_exp(a, b)
|
||||||
return _NaN
|
|
||||||
return (a or 0) ** b
|
|
||||||
|
|
||||||
|
|
||||||
def _js_to_primitive(v):
|
def _js_to_primitive(v):
|
||||||
@ -117,7 +130,7 @@ def _js_to_primitive(v):
|
|||||||
','.join(map(_js_toString, v)) if isinstance(v, list)
|
','.join(map(_js_toString, v)) if isinstance(v, list)
|
||||||
else '[object Object]' if isinstance(v, dict)
|
else '[object Object]' if isinstance(v, dict)
|
||||||
else compat_str(v) if not isinstance(v, (
|
else compat_str(v) if not isinstance(v, (
|
||||||
compat_numeric_types, compat_basestring, bool))
|
compat_numeric_types, compat_basestring))
|
||||||
else v
|
else v
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -128,7 +141,9 @@ def _js_toString(v):
|
|||||||
else 'Infinity' if v == _Infinity
|
else 'Infinity' if v == _Infinity
|
||||||
else 'NaN' if v is _NaN
|
else 'NaN' if v is _NaN
|
||||||
else 'null' if v is None
|
else 'null' if v is None
|
||||||
else compat_str(v) if isinstance(v, compat_numeric_types)
|
# bool <= int: do this first
|
||||||
|
else ('false', 'true')[v] if isinstance(v, bool)
|
||||||
|
else '{0:.7f}'.format(v).rstrip('.0') if isinstance(v, compat_numeric_types)
|
||||||
else _js_to_primitive(v))
|
else _js_to_primitive(v))
|
||||||
|
|
||||||
|
|
||||||
@ -240,11 +255,11 @@ def _js_typeof(expr):
|
|||||||
_OPERATORS = (
|
_OPERATORS = (
|
||||||
('>>', _js_bit_op(operator.rshift)),
|
('>>', _js_bit_op(operator.rshift)),
|
||||||
('<<', _js_bit_op(operator.lshift)),
|
('<<', _js_bit_op(operator.lshift)),
|
||||||
('+', _js_arith_op(operator.add)),
|
('+', _js_add),
|
||||||
('-', _js_arith_op(operator.sub)),
|
('-', _js_arith_op(operator.sub)),
|
||||||
('*', _js_arith_op(operator.mul)),
|
('*', _js_arith_op(operator.mul)),
|
||||||
('%', _js_mod),
|
('%', _js_mod),
|
||||||
('/', _js_div),
|
('/', _js_arith_op(operator.truediv, div=True)),
|
||||||
('**', _js_exp),
|
('**', _js_exp),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -873,7 +888,7 @@ class JSInterpreter(object):
|
|||||||
start, end = m.span()
|
start, end = m.span()
|
||||||
sign = m.group('pre_sign') or m.group('post_sign')
|
sign = m.group('pre_sign') or m.group('post_sign')
|
||||||
ret = local_vars[var]
|
ret = local_vars[var]
|
||||||
local_vars[var] += 1 if sign[0] == '+' else -1
|
local_vars[var] = _js_add(ret, 1 if sign[0] == '+' else -1)
|
||||||
if m.group('pre_sign'):
|
if m.group('pre_sign'):
|
||||||
ret = local_vars[var]
|
ret = local_vars[var]
|
||||||
expr = expr[:start] + self._dump(ret, local_vars) + expr[end:]
|
expr = expr[:start] + self._dump(ret, local_vars) + expr[end:]
|
||||||
@ -1023,7 +1038,7 @@ class JSInterpreter(object):
|
|||||||
if obj is compat_str:
|
if obj is compat_str:
|
||||||
if member == 'fromCharCode':
|
if member == 'fromCharCode':
|
||||||
assertion(argvals, 'takes one or more arguments')
|
assertion(argvals, 'takes one or more arguments')
|
||||||
return ''.join(map(compat_chr, argvals))
|
return ''.join(compat_chr(int(n)) for n in argvals)
|
||||||
raise self.Exception('Unsupported string method ' + member, expr=expr)
|
raise self.Exception('Unsupported string method ' + member, expr=expr)
|
||||||
elif obj is float:
|
elif obj is float:
|
||||||
if member == 'pow':
|
if member == 'pow':
|
||||||
|
Loading…
Reference in New Issue
Block a user