[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:
dirkf 2024-12-12 00:26:00 +00:00
parent 81e64cacf2
commit 5dee6213ed
2 changed files with 54 additions and 19 deletions

View File

@ -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])

View File

@ -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':