aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authordirkf <[email protected]>2024-12-12 00:00:32 +0000
committerdirkf <[email protected]>2024-12-16 12:38:51 +0000
commitc1a03b1ac3a453508b358b6d1bc5d158cc80a0ce (patch)
treeba7a589341adfc3202421c2f375d20d0e8b50b31
parent118c6d7a17c61f1a1031e5530ca2854a526834d5 (diff)
downloadyoutube-dl-c1a03b1ac3a453508b358b6d1bc5d158cc80a0ce.tar.gz
youtube-dl-c1a03b1ac3a453508b358b6d1bc5d158cc80a0ce.zip
[jsinterp] Fix and improve loose and strict equality operations
* reimplement loose equality according to MDN (eg, 1 == "1") * improve strict equality (eg, "abc" === "abc" but 'abc' is not 'abc') * add tests for above
-rw-r--r--test/test_jsinterp.py35
-rw-r--r--youtube_dl/jsinterp.py80
2 files changed, 101 insertions, 14 deletions
diff --git a/test/test_jsinterp.py b/test/test_jsinterp.py
index d063bbd36..86137d8e5 100644
--- a/test/test_jsinterp.py
+++ b/test/test_jsinterp.py
@@ -1,4 +1,5 @@
#!/usr/bin/env python
+# coding: utf-8
from __future__ import unicode_literals
@@ -11,7 +12,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import math
import re
-from youtube_dl.compat import compat_str
+from youtube_dl.compat import compat_str as str
from youtube_dl.jsinterp import JS_Undefined, JSInterpreter
NaN = object()
@@ -19,7 +20,7 @@ NaN = object()
class TestJSInterpreter(unittest.TestCase):
def _test(self, jsi_or_code, expected, func='f', args=()):
- if isinstance(jsi_or_code, compat_str):
+ if isinstance(jsi_or_code, str):
jsi_or_code = JSInterpreter(jsi_or_code)
got = jsi_or_code.call_function(func, *args)
if expected is NaN:
@@ -89,7 +90,35 @@ class TestJSInterpreter(unittest.TestCase):
self._test('function f(){return 19 & 21;}', 17)
self._test('function f(){return 11 >> 2;}', 2)
self._test('function f(){return []? 2+3: 4;}', 5)
+ # equality
+ self._test('function f(){return 1 == 1}', True)
+ self._test('function f(){return 1 == 1.0}', True)
+ self._test('function f(){return 1 == "1"}', True)
self._test('function f(){return 1 == 2}', False)
+ self._test('function f(){return 1 != "1"}', False)
+ self._test('function f(){return 1 != 2}', True)
+ self._test('function f(){var x = {a: 1}; var y = x; return x == y}', True)
+ self._test('function f(){var x = {a: 1}; return x == {a: 1}}', False)
+ self._test('function f(){return NaN == NaN}', False)
+ self._test('function f(){return null == undefined}', True)
+ self._test('function f(){return "spam, eggs" == "spam, eggs"}', True)
+ # strict equality
+ self._test('function f(){return 1 === 1}', True)
+ self._test('function f(){return 1 === 1.0}', True)
+ self._test('function f(){return 1 === "1"}', False)
+ self._test('function f(){return 1 === 2}', False)
+ self._test('function f(){var x = {a: 1}; var y = x; return x === y}', True)
+ self._test('function f(){var x = {a: 1}; return x === {a: 1}}', False)
+ self._test('function f(){return NaN === NaN}', False)
+ self._test('function f(){return null === undefined}', False)
+ self._test('function f(){return null === null}', True)
+ self._test('function f(){return undefined === undefined}', True)
+ self._test('function f(){return "uninterned" === "uninterned"}', True)
+ self._test('function f(){return 1 === 1}', True)
+ self._test('function f(){return 1 === "1"}', False)
+ self._test('function f(){return 1 !== 1}', False)
+ self._test('function f(){return 1 !== "1"}', True)
+ # expressions
self._test('function f(){return 0 && 1 || 2;}', 2)
self._test('function f(){return 0 ?? 42;}', 0)
self._test('function f(){return "life, the universe and everything" < 42;}', False)
@@ -296,7 +325,7 @@ class TestJSInterpreter(unittest.TestCase):
def test_undefined(self):
self._test('function f() { return undefined === undefined; }', True)
self._test('function f() { return undefined; }', JS_Undefined)
- self._test('function f() {return undefined ?? 42; }', 42)
+ self._test('function f() { return undefined ?? 42; }', 42)
self._test('function f() { let v; return v; }', JS_Undefined)
self._test('function f() { let v; return v**0; }', 1)
self._test('function f() { let v; return [v>42, v<=42, v&&42, 42&&v]; }',
diff --git a/youtube_dl/jsinterp.py b/youtube_dl/jsinterp.py
index acc68c533..b7a3ef69a 100644
--- a/youtube_dl/jsinterp.py
+++ b/youtube_dl/jsinterp.py
@@ -1,3 +1,4 @@
+# coding: utf-8
from __future__ import unicode_literals
import itertools
@@ -64,6 +65,10 @@ _NaN = float('nan')
_Infinity = float('inf')
+class JS_Undefined(object):
+ pass
+
+
def _js_bit_op(op):
def zeroise(x):
@@ -107,12 +112,69 @@ def _js_exp(a, b):
return (a or 0) ** b
-def _js_eq_op(op):
+def _js_to_primitive(v):
+ return (
+ ','.join(map(_js_toString, v)) if isinstance(v, list)
+ else '[object Object]' if isinstance(v, dict)
+ else compat_str(v) if not isinstance(v, (
+ compat_numeric_types, compat_basestring, bool))
+ else v
+ )
+
+
+def _js_toString(v):
+ return (
+ 'undefined' if v is JS_Undefined
+ else 'Infinity' if v == _Infinity
+ else 'NaN' if v is _NaN
+ else 'null' if v is None
+ else compat_str(v) if isinstance(v, compat_numeric_types)
+ else _js_to_primitive(v))
+
+
+_nullish = frozenset((None, JS_Undefined))
+
+
+def _js_eq(a, b):
+ # NaN != any
+ if _NaN in (a, b):
+ return False
+ # Object is Object
+ if isinstance(a, type(b)) and isinstance(b, (dict, list)):
+ return operator.is_(a, b)
+ # general case
+ if a == b:
+ return True
+ # null == undefined
+ a_b = set((a, b))
+ if a_b & _nullish:
+ return a_b <= _nullish
+ a, b = _js_to_primitive(a), _js_to_primitive(b)
+ if not isinstance(a, compat_basestring):
+ a, b = b, a
+ # Number to String: convert the string to a number
+ # Conversion failure results in ... false
+ if isinstance(a, compat_basestring):
+ return float_or_none(a) == b
+ return a == b
+
+
+def _js_neq(a, b):
+ return not _js_eq(a, b)
+
+
+def _js_id_op(op):
@wraps_op(op)
def wrapped(a, b):
- if set((a, b)) <= set((None, JS_Undefined)):
- return op(a, a)
+ if _NaN in (a, b):
+ return op(_NaN, None)
+ if not isinstance(a, (compat_basestring, compat_numeric_types)):
+ a, b = b, a
+ # strings are === if ==
+ # why 'a' is not 'a': https://stackoverflow.com/a/1504848
+ if isinstance(a, (compat_basestring, compat_numeric_types)):
+ return a == b if op(0, 0) else a != b
return op(a, b)
return wrapped
@@ -187,10 +249,10 @@ _OPERATORS = (
)
_COMP_OPERATORS = (
- ('===', operator.is_),
- ('!==', operator.is_not),
- ('==', _js_eq_op(operator.eq)),
- ('!=', _js_eq_op(operator.ne)),
+ ('===', _js_id_op(operator.is_)),
+ ('!==', _js_id_op(operator.is_not)),
+ ('==', _js_eq),
+ ('!=', _js_neq),
('<=', _js_comp_op(operator.le)),
('>=', _js_comp_op(operator.ge)),
('<', _js_comp_op(operator.lt)),
@@ -222,10 +284,6 @@ _MATCHING_PARENS = dict(zip(*zip('()', '{}', '[]')))
_QUOTES = '\'"/'
-class JS_Undefined(object):
- pass
-
-
class JS_Break(ExtractorError):
def __init__(self):
ExtractorError.__init__(self, 'Invalid break')