1
1
mirror of https://github.com/ytdl-org/youtube-dl synced 2024-11-09 07:47:06 +01:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Allan Daemon
55e466b153
Merge f8703c4291 into e1b3fa242c 2024-07-27 20:18:42 +09:00
dirkf
e1b3fa242c [Youtube] Find n function name in player 3400486c
Fixes #32877
2024-07-25 00:16:00 +01:00
dirkf
451046d62a [Youtube] Make n-sig throttling diagnostic up-to-date 2024-07-24 14:33:34 +01:00
dirkf
16f5bbc464 [YouTube] Fix nsig processing for player b22ef6e7
* improve extraction of function name (like yt-dlp/yt-dlp#10390)
* always use JSInterp to extract function code (yt-dlp/yt-dlp#10396, thx seproDev, pukkandan)
2024-07-11 00:50:46 +01:00
dirkf
d35ce6ce95 [jsinterp] Support functionality for player b22ef6e7
* support `prototype` for call() and apply() (yt-dlp/yt-dlp#10392, thx Grub4k)
* map JS `Array` to `list`
2024-07-11 00:50:46 +01:00
dirkf
76ac69917e [jsinterp] Further improve expression parsing (fix fd8242e)
Passes tests from yt-dlp
2024-07-11 00:50:46 +01:00
dirkf
756f6b45c7 [jsinterp] Re-align JSInterp and tests (esp.) with yt-dlp
Thx: various yt-dlp authors
2024-07-11 00:50:46 +01:00
bashonly
43a74c5fa5 [core] Address gaps in allowed extensions
Adds some extensions missing in 4652109643
(from yt-dlp/yt-dlp#10362)

Authored by: bashonly
Co-authored by: dirkf
2024-07-11 00:50:46 +01:00
5 changed files with 384 additions and 485 deletions

View File

@ -11,194 +11,146 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import math import math
import re import re
from youtube_dl.compat import compat_str
from youtube_dl.jsinterp import JS_Undefined, JSInterpreter from youtube_dl.jsinterp import JS_Undefined, JSInterpreter
NaN = object()
class TestJSInterpreter(unittest.TestCase): class TestJSInterpreter(unittest.TestCase):
def _test(self, jsi_or_code, expected, func='f', args=()):
if isinstance(jsi_or_code, compat_str):
jsi_or_code = JSInterpreter(jsi_or_code)
got = jsi_or_code.call_function(func, *args)
if expected is NaN:
self.assertTrue(math.isnan(got), '{0} is not NaN'.format(got))
else:
self.assertEqual(got, expected)
def test_basic(self): def test_basic(self):
jsi = JSInterpreter('function x(){;}') jsi = JSInterpreter('function f(){;}')
self.assertEqual(jsi.call_function('x'), None) self.assertEqual(repr(jsi.extract_function('f')), 'F<f>')
self.assertEqual(repr(jsi.extract_function('x')), 'F<x>') self._test(jsi, None)
jsi = JSInterpreter('function x3(){return 42;}') self._test('function f(){return 42;}', 42)
self.assertEqual(jsi.call_function('x3'), 42) self._test('function f(){42}', None)
self._test('var f = function(){return 42;}', 42)
jsi = JSInterpreter('function x3(){42}')
self.assertEqual(jsi.call_function('x3'), None)
jsi = JSInterpreter('var x5 = function(){return 42;}')
self.assertEqual(jsi.call_function('x5'), 42)
def test_calc(self):
jsi = JSInterpreter('function x4(a){return 2*a+1;}')
self.assertEqual(jsi.call_function('x4', 3), 7)
def test_add(self): def test_add(self):
jsi = JSInterpreter('function f(){return 42 + 7;}') self._test('function f(){return 42 + 7;}', 49)
self.assertEqual(jsi.call_function('f'), 49) self._test('function f(){return 42 + undefined;}', NaN)
jsi = JSInterpreter('function f(){return 42 + undefined;}') self._test('function f(){return 42 + null;}', 42)
self.assertTrue(math.isnan(jsi.call_function('f')))
jsi = JSInterpreter('function f(){return 42 + null;}')
self.assertEqual(jsi.call_function('f'), 42)
def test_sub(self): def test_sub(self):
jsi = JSInterpreter('function f(){return 42 - 7;}') self._test('function f(){return 42 - 7;}', 35)
self.assertEqual(jsi.call_function('f'), 35) self._test('function f(){return 42 - undefined;}', NaN)
jsi = JSInterpreter('function f(){return 42 - undefined;}') self._test('function f(){return 42 - null;}', 42)
self.assertTrue(math.isnan(jsi.call_function('f')))
jsi = JSInterpreter('function f(){return 42 - null;}')
self.assertEqual(jsi.call_function('f'), 42)
def test_mul(self): def test_mul(self):
jsi = JSInterpreter('function f(){return 42 * 7;}') self._test('function f(){return 42 * 7;}', 294)
self.assertEqual(jsi.call_function('f'), 294) self._test('function f(){return 42 * undefined;}', NaN)
jsi = JSInterpreter('function f(){return 42 * undefined;}') self._test('function f(){return 42 * null;}', 0)
self.assertTrue(math.isnan(jsi.call_function('f')))
jsi = JSInterpreter('function f(){return 42 * null;}')
self.assertEqual(jsi.call_function('f'), 0)
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;}')
self.assertTrue(math.isnan(jsi.call_function('f', 0, 0))) self._test(jsi, NaN, args=(0, 0))
self.assertTrue(math.isnan(jsi.call_function('f', JS_Undefined, 1))) self._test(jsi, NaN, args=(JS_Undefined, 1))
self.assertTrue(math.isinf(jsi.call_function('f', 2, 0))) self._test(jsi, float('inf'), args=(2, 0))
self.assertEqual(jsi.call_function('f', 0, 3), 0) self._test(jsi, 0, args=(0, 3))
def test_mod(self): def test_mod(self):
jsi = JSInterpreter('function f(){return 42 % 7;}') self._test('function f(){return 42 % 7;}', 0)
self.assertEqual(jsi.call_function('f'), 0) self._test('function f(){return 42 % 0;}', NaN)
jsi = JSInterpreter('function f(){return 42 % 0;}') self._test('function f(){return 42 % undefined;}', NaN)
self.assertTrue(math.isnan(jsi.call_function('f')))
jsi = JSInterpreter('function f(){return 42 % undefined;}')
self.assertTrue(math.isnan(jsi.call_function('f')))
def test_exp(self): def test_exp(self):
jsi = JSInterpreter('function f(){return 42 ** 2;}') self._test('function f(){return 42 ** 2;}', 1764)
self.assertEqual(jsi.call_function('f'), 1764) self._test('function f(){return 42 ** undefined;}', NaN)
jsi = JSInterpreter('function f(){return 42 ** undefined;}') self._test('function f(){return 42 ** null;}', 1)
self.assertTrue(math.isnan(jsi.call_function('f'))) self._test('function f(){return undefined ** 42;}', NaN)
jsi = JSInterpreter('function f(){return 42 ** null;}')
self.assertEqual(jsi.call_function('f'), 1) def test_calc(self):
jsi = JSInterpreter('function f(){return undefined ** 42;}') self._test('function f(a){return 2*a+1;}', 7, args=[3])
self.assertTrue(math.isnan(jsi.call_function('f')))
def test_empty_return(self): def test_empty_return(self):
jsi = JSInterpreter('function f(){return; y()}') self._test('function f(){return; y()}', None)
self.assertEqual(jsi.call_function('f'), None)
def test_morespace(self): def test_morespace(self):
jsi = JSInterpreter('function x (a) { return 2 * a + 1 ; }') self._test('function f (a) { return 2 * a + 1 ; }', 7, args=[3])
self.assertEqual(jsi.call_function('x', 3), 7) self._test('function f () { x = 2 ; return x; }', 2)
jsi = JSInterpreter('function f () { x = 2 ; return x; }')
self.assertEqual(jsi.call_function('f'), 2)
def test_strange_chars(self): def test_strange_chars(self):
jsi = JSInterpreter('function $_xY1 ($_axY1) { var $_axY2 = $_axY1 + 1; return $_axY2; }') self._test('function $_xY1 ($_axY1) { var $_axY2 = $_axY1 + 1; return $_axY2; }',
self.assertEqual(jsi.call_function('$_xY1', 20), 21) 21, args=[20], func='$_xY1')
def test_operators(self): def test_operators(self):
jsi = JSInterpreter('function f(){return 1 << 5;}') self._test('function f(){return 1 << 5;}', 32)
self.assertEqual(jsi.call_function('f'), 32) self._test('function f(){return 2 ** 5}', 32)
self._test('function f(){return 19 & 21;}', 17)
jsi = JSInterpreter('function f(){return 2 ** 5}') self._test('function f(){return 11 >> 2;}', 2)
self.assertEqual(jsi.call_function('f'), 32) self._test('function f(){return []? 2+3: 4;}', 5)
self._test('function f(){return 1 == 2}', False)
jsi = JSInterpreter('function f(){return 19 & 21;}') self._test('function f(){return 0 && 1 || 2;}', 2)
self.assertEqual(jsi.call_function('f'), 17) self._test('function f(){return 0 ?? 42;}', 0)
self._test('function f(){return "life, the universe and everything" < 42;}', False)
jsi = JSInterpreter('function f(){return 11 >> 2;}') # https://github.com/ytdl-org/youtube-dl/issues/32815
self.assertEqual(jsi.call_function('f'), 2) self._test('function f(){return 0 - 7 * - 6;}', 42)
jsi = JSInterpreter('function f(){return []? 2+3: 4;}')
self.assertEqual(jsi.call_function('f'), 5)
jsi = JSInterpreter('function f(){return 1 == 2}')
self.assertEqual(jsi.call_function('f'), False)
jsi = JSInterpreter('function f(){return 0 && 1 || 2;}')
self.assertEqual(jsi.call_function('f'), 2)
jsi = JSInterpreter('function f(){return 0 ?? 42;}')
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;}') self._test('function f(){var x = [1,2,3]; x[0] = 4; x[0] = 5; x[2.0] = 7; return x;}', [5, 2, 7])
self.assertEqual(jsi.call_function('f'), [5, 2, 7])
def test_parens(self): def test_parens(self):
jsi = JSInterpreter('function f(){return (1) + (2) * ((( (( (((((3)))))) )) ));}') self._test('function f(){return (1) + (2) * ((( (( (((((3)))))) )) ));}', 7)
self.assertEqual(jsi.call_function('f'), 7) self._test('function f(){return (1 + 2) * 3;}', 9)
jsi = JSInterpreter('function f(){return (1 + 2) * 3;}')
self.assertEqual(jsi.call_function('f'), 9)
def test_quotes(self): def test_quotes(self):
jsi = JSInterpreter(r'function f(){return "a\"\\("}') self._test(r'function f(){return "a\"\\("}', r'a"\(')
self.assertEqual(jsi.call_function('f'), r'a"\(')
def test_assignments(self): def test_assignments(self):
jsi = JSInterpreter('function f(){var x = 20; x = 30 + 1; return x;}') self._test('function f(){var x = 20; x = 30 + 1; return x;}', 31)
self.assertEqual(jsi.call_function('f'), 31) self._test('function f(){var x = 20; x += 30 + 1; return x;}', 51)
self._test('function f(){var x = 20; x -= 30 + 1; return x;}', -11)
jsi = JSInterpreter('function f(){var x = 20; x += 30 + 1; return x;}')
self.assertEqual(jsi.call_function('f'), 51)
jsi = JSInterpreter('function f(){var x = 20; x -= 30 + 1; return x;}')
self.assertEqual(jsi.call_function('f'), -11)
@unittest.skip('Not yet fully implemented')
def test_comments(self): def test_comments(self):
'Skipping: Not yet fully implemented' self._test('''
return function f() {
jsi = JSInterpreter(''' var x = /* 1 + */ 2;
function x() { var y = /* 30
var x = /* 1 + */ 2; * 40 */ 50;
var y = /* 30 return x + y;
* 40 */ 50; }
return x + y; ''', 52)
}
''')
self.assertEqual(jsi.call_function('x'), 52)
jsi = JSInterpreter(''' self._test('''
function f() { function f() {
var x = "/*"; var x = "/*";
var y = 1 /* comment */ + 2; var y = 1 /* comment */ + 2;
return y; return y;
} }
''') ''', 3)
self.assertEqual(jsi.call_function('f'), 3)
def test_precedence(self): def test_precedence(self):
jsi = JSInterpreter(''' self._test('''
function x() { function f() {
var a = [10, 20, 30, 40, 50]; var a = [10, 20, 30, 40, 50];
var b = 6; var b = 6;
a[0]=a[b%a.length]; a[0]=a[b%a.length];
return a; return a;
}''') }
self.assertEqual(jsi.call_function('x'), [20, 20, 30, 40, 50]) ''', [20, 20, 30, 40, 50])
def test_builtins(self): def test_builtins(self):
jsi = JSInterpreter(''' self._test('function f() { return NaN }', NaN)
function x() { return NaN }
''')
self.assertTrue(math.isnan(jsi.call_function('x')))
def test_Date(self): def test_Date(self):
jsi = JSInterpreter(''' self._test('function f() { return new Date("Wednesday 31 December 1969 18:01:26 MDT") - 0; }', 86000)
function x(dt) { return new Date(dt) - 0; }
''')
self.assertEqual(jsi.call_function('x', 'Wednesday 31 December 1969 18:01:26 MDT'), 86000)
jsi = JSInterpreter('function f(dt) { return new Date(dt) - 0; }')
# date format m/d/y # date format m/d/y
self.assertEqual(jsi.call_function('x', '12/31/1969 18:01:26 MDT'), 86000) self._test(jsi, 86000, args=['12/31/1969 18:01:26 MDT'])
# epoch 0 # epoch 0
self.assertEqual(jsi.call_function('x', '1 January 1970 00:00:00 UTC'), 0) self._test(jsi, 0, args=['1 January 1970 00:00:00 UTC'])
def test_call(self): def test_call(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
@ -206,179 +158,115 @@ class TestJSInterpreter(unittest.TestCase):
function y(a) { return x() + (a?a:0); } function y(a) { return x() + (a?a:0); }
function z() { return y(3); } function z() { return y(3); }
''') ''')
self.assertEqual(jsi.call_function('z'), 5) self._test(jsi, 5, func='z')
self.assertEqual(jsi.call_function('y'), 2) self._test(jsi, 2, func='y')
def test_if(self): def test_if(self):
jsi = JSInterpreter(''' self._test('''
function x() { function f() {
let a = 9; let a = 9;
if (0==0) {a++} if (0==0) {a++}
return a return a
}''') }
self.assertEqual(jsi.call_function('x'), 10) ''', 10)
jsi = JSInterpreter(''' self._test('''
function x() { function f() {
if (0==0) {return 10} if (0==0) {return 10}
}''') }
self.assertEqual(jsi.call_function('x'), 10) ''', 10)
jsi = JSInterpreter(''' self._test('''
function x() { function f() {
if (0!=0) {return 1} if (0!=0) {return 1}
else {return 10} else {return 10}
}''') }
self.assertEqual(jsi.call_function('x'), 10) ''', 10)
""" # Unsupported
jsi = JSInterpreter('''
function x() {
if (0!=0) return 1;
else {return 10}
}''')
self.assertEqual(jsi.call_function('x'), 10)
"""
def test_elseif(self): def test_elseif(self):
jsi = JSInterpreter(''' self._test('''
function x() { function f() {
if (0!=0) {return 1} if (0!=0) {return 1}
else if (1==0) {return 2} else if (1==0) {return 2}
else {return 10} else {return 10}
}''') }
self.assertEqual(jsi.call_function('x'), 10) ''', 10)
""" # Unsupported
jsi = JSInterpreter('''
function x() {
if (0!=0) return 1;
else if (1==0) {return 2}
else {return 10}
}''')
self.assertEqual(jsi.call_function('x'), 10)
# etc
"""
def test_for_loop(self): def test_for_loop(self):
# function x() { a=0; for (i=0; i-10; i++) {a++} a } self._test('function f() { a=0; for (i=0; i-10; i++) {a++} return a }', 10)
jsi = JSInterpreter('''
function x() { a=0; for (i=0; i-10; i++) {a++} return a }
''')
self.assertEqual(jsi.call_function('x'), 10)
def test_while_loop(self): def test_while_loop(self):
# function x() { a=0; while (a<10) {a++} a } self._test('function f() { a=0; while (a<10) {a++} return a }', 10)
jsi = JSInterpreter('''
function x() { a=0; while (a<10) {a++} return a }
''')
self.assertEqual(jsi.call_function('x'), 10)
def test_switch(self): def test_switch(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x(f) { switch(f){ function f(x) { switch(x){
case 1:f+=1; case 1:x+=1;
case 2:f+=2; case 2:x+=2;
case 3:f+=3;break; case 3:x+=3;break;
case 4:f+=4; case 4:x+=4;
default:f=0; default:x=0;
} return f } } return x }
''') ''')
self.assertEqual(jsi.call_function('x', 1), 7) self._test(jsi, 7, args=[1])
self.assertEqual(jsi.call_function('x', 3), 6) self._test(jsi, 6, args=[3])
self.assertEqual(jsi.call_function('x', 5), 0) self._test(jsi, 0, args=[5])
def test_switch_default(self): def test_switch_default(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x(f) { switch(f){ function f(x) { switch(x){
case 2: f+=2; case 2: x+=2;
default: f-=1; default: x-=1;
case 5: case 5:
case 6: f+=6; case 6: x+=6;
case 0: break; case 0: break;
case 1: f+=1; case 1: x+=1;
} return f } } return x }
''') ''')
self.assertEqual(jsi.call_function('x', 1), 2) self._test(jsi, 2, args=[1])
self.assertEqual(jsi.call_function('x', 5), 11) self._test(jsi, 11, args=[5])
self.assertEqual(jsi.call_function('x', 9), 14) self._test(jsi, 14, args=[9])
def test_try(self): def test_try(self):
jsi = JSInterpreter(''' self._test('function f() { try{return 10} catch(e){return 5} }', 10)
function x() { try{return 10} catch(e){return 5} }
''')
self.assertEqual(jsi.call_function('x'), 10)
def test_catch(self): def test_catch(self):
jsi = JSInterpreter(''' self._test('function f() { try{throw 10} catch(e){return 5} }', 5)
function x() { try{throw 10} catch(e){return 5} }
''')
self.assertEqual(jsi.call_function('x'), 5)
def test_finally(self): def test_finally(self):
jsi = JSInterpreter(''' self._test('function f() { try{throw 10} finally {return 42} }', 42)
function x() { try{throw 10} finally {return 42} } self._test('function f() { try{throw 10} catch(e){return 5} finally {return 42} }', 42)
''')
self.assertEqual(jsi.call_function('x'), 42)
jsi = JSInterpreter('''
function x() { try{throw 10} catch(e){return 5} finally {return 42} }
''')
self.assertEqual(jsi.call_function('x'), 42)
def test_nested_try(self): def test_nested_try(self):
jsi = JSInterpreter(''' self._test('''
function x() {try { function f() {try {
try{throw 10} finally {throw 42} try{throw 10} finally {throw 42}
} catch(e){return 5} } } catch(e){return 5} }
''') ''', 5)
self.assertEqual(jsi.call_function('x'), 5)
def test_for_loop_continue(self): def test_for_loop_continue(self):
jsi = JSInterpreter(''' self._test('function f() { a=0; for (i=0; i-10; i++) { continue; a++ } return a }', 0)
function x() { a=0; for (i=0; i-10; i++) { continue; a++ } return a }
''')
self.assertEqual(jsi.call_function('x'), 0)
def test_for_loop_break(self): def test_for_loop_break(self):
jsi = JSInterpreter(''' self._test('function f() { a=0; for (i=0; i-10; i++) { break; a++ } return a }', 0)
function x() { a=0; for (i=0; i-10; i++) { break; a++ } return a }
''')
self.assertEqual(jsi.call_function('x'), 0)
def test_for_loop_try(self): def test_for_loop_try(self):
jsi = JSInterpreter(''' self._test('''
function x() { function f() {
for (i=0; i-10; i++) { try { if (i == 5) throw i} catch {return 10} finally {break} }; for (i=0; i-10; i++) { try { if (i == 5) throw i} catch {return 10} finally {break} };
return 42 } return 42 }
''') ''', 42)
self.assertEqual(jsi.call_function('x'), 42)
def test_literal_list(self): def test_literal_list(self):
jsi = JSInterpreter(''' self._test('function f() { return [1, 2, "asdf", [5, 6, 7]][3] }', [5, 6, 7])
function x() { return [1, 2, "asdf", [5, 6, 7]][3] }
''')
self.assertEqual(jsi.call_function('x'), [5, 6, 7])
def test_comma(self): def test_comma(self):
jsi = JSInterpreter(''' self._test('function f() { a=5; a -= 1, a+=3; return a }', 7)
function x() { a=5; a -= 1, a+=3; return a } self._test('function f() { a=5; return (a -= 1, a+=3, a); }', 7)
''') self._test('function f() { return (l=[0,1,2,3], function(a, b){return a+b})((l[1], l[2]), l[3]) }', 5)
self.assertEqual(jsi.call_function('x'), 7)
jsi = JSInterpreter('''
function x() { a=5; return (a -= 1, a+=3, a); }
''')
self.assertEqual(jsi.call_function('x'), 7)
jsi = JSInterpreter('''
function x() { return (l=[0,1,2,3], function(a, b){return a+b})((l[1], l[2]), l[3]) }
''')
self.assertEqual(jsi.call_function('x'), 5)
def test_void(self): def test_void(self):
jsi = JSInterpreter(''' self._test('function f() { return void 42; }', None)
function x() { return void 42; }
''')
self.assertEqual(jsi.call_function('x'), None)
def test_return_function(self): def test_return_function(self):
jsi = JSInterpreter(''' jsi = JSInterpreter('''
@ -387,110 +275,60 @@ class TestJSInterpreter(unittest.TestCase):
self.assertEqual(jsi.call_function('x')([]), 1) self.assertEqual(jsi.call_function('x')([]), 1)
def test_null(self): def test_null(self):
jsi = JSInterpreter(''' self._test('function f() { return null; }', None)
function x() { return null; } self._test('function f() { return [null > 0, null < 0, null == 0, null === 0]; }',
''') [False, False, False, False])
self.assertIs(jsi.call_function('x'), None) self._test('function f() { return [null >= 0, null <= 0]; }', [True, True])
jsi = JSInterpreter('''
function x() { return [null > 0, null < 0, null == 0, null === 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, False, False])
jsi = JSInterpreter('''
function x() { return [null >= 0, null <= 0]; }
''')
self.assertEqual(jsi.call_function('x'), [True, True])
def test_undefined(self): def test_undefined(self):
jsi = JSInterpreter(''' self._test('function f() { return undefined === undefined; }', True)
function x() { return undefined === undefined; } self._test('function f() { return undefined; }', JS_Undefined)
''') self._test('function f() {return undefined ?? 42; }', 42)
self.assertTrue(jsi.call_function('x')) 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]; }',
[False, False, JS_Undefined, JS_Undefined])
self._test('''
function f() { return [
undefined === undefined,
undefined == undefined,
undefined == null
]; }
''', [True] * 3)
self._test('''
function f() { return [
undefined < undefined,
undefined > undefined,
undefined === 0,
undefined == 0,
undefined < 0,
undefined > 0,
undefined >= 0,
undefined <= 0,
undefined > null,
undefined < null,
undefined === null
]; }
''', [False] * 11)
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x() { return undefined; } function x() { let v; return [42+v, v+42, v**42, 42**v, 0**v]; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)
jsi = JSInterpreter('''
function x() { let v; return v; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)
jsi = JSInterpreter('''
function x() { return [undefined === undefined, undefined == undefined, undefined < undefined, undefined > undefined]; }
''')
self.assertEqual(jsi.call_function('x'), [True, True, False, False])
jsi = JSInterpreter('''
function x() { return [undefined === 0, undefined == 0, undefined < 0, undefined > 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, False, False])
jsi = JSInterpreter('''
function x() { return [undefined >= 0, undefined <= 0]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False])
jsi = JSInterpreter('''
function x() { return [undefined > null, undefined < null, undefined == null, undefined === null]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, True, False])
jsi = JSInterpreter('''
function x() { return [undefined === null, undefined == null, undefined < null, undefined > null]; }
''')
self.assertEqual(jsi.call_function('x'), [False, True, False, False])
jsi = JSInterpreter('''
function x() { let v; return [42+v, v+42, v**42, 42**v, 0**v]; }
''') ''')
for y in jsi.call_function('x'): for y in jsi.call_function('x'):
self.assertTrue(math.isnan(y)) self.assertTrue(math.isnan(y))
jsi = JSInterpreter('''
function x() { let v; return v**0; }
''')
self.assertEqual(jsi.call_function('x'), 1)
jsi = JSInterpreter('''
function x() { let v; return [v>42, v<=42, v&&42, 42&&v]; }
''')
self.assertEqual(jsi.call_function('x'), [False, False, JS_Undefined, JS_Undefined])
jsi = JSInterpreter('function x(){return undefined ?? 42; }')
self.assertEqual(jsi.call_function('x'), 42)
def test_object(self): def test_object(self):
jsi = JSInterpreter(''' self._test('function f() { return {}; }', {})
function x() { return {}; } self._test('function f() { let a = {m1: 42, m2: 0 }; return [a["m1"], a.m2]; }', [42, 0])
''') self._test('function f() { let a; return a?.qq; }', JS_Undefined)
self.assertEqual(jsi.call_function('x'), {}) self._test('function f() { let a = {m1: 42, m2: 0 }; return a?.qq; }', JS_Undefined)
jsi = JSInterpreter('''
function x() { let a = {m1: 42, m2: 0 }; return [a["m1"], a.m2]; }
''')
self.assertEqual(jsi.call_function('x'), [42, 0])
jsi = JSInterpreter('''
function x() { let a; return a?.qq; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)
jsi = JSInterpreter('''
function x() { let a = {m1: 42, m2: 0 }; return a?.qq; }
''')
self.assertIs(jsi.call_function('x'), JS_Undefined)
def test_regex(self): def test_regex(self):
jsi = JSInterpreter(''' self._test('function f() { let a=/,,[/,913,/](,)}/; }', None)
function x() { let a=/,,[/,913,/](,)}/; }
''')
self.assertIs(jsi.call_function('x'), None)
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/; "".replace(a, ""); return a; } function x() { let a=/,,[/,913,/](,)}/; "".replace(a, ""); return a; }
''') ''')
attrs = set(('findall', 'finditer', 'match', 'scanner', 'search', attrs = set(('findall', 'finditer', 'match', 'scanner', 'search',
'split', 'sub', 'subn')) 'split', 'sub', 'subn'))
@ -500,94 +338,92 @@ class TestJSInterpreter(unittest.TestCase):
self.assertSetEqual(set(dir(jsi.call_function('x'))) & attrs, attrs) self.assertSetEqual(set(dir(jsi.call_function('x'))) & attrs, attrs)
jsi = JSInterpreter(''' jsi = JSInterpreter('''
function x() { let a=/,,[/,913,/](,)}/i; return a; } function x() { let a=/,,[/,913,/](,)}/i; return a; }
''') ''')
self.assertEqual(jsi.call_function('x').flags & ~re.U, re.I) self.assertEqual(jsi.call_function('x').flags & ~re.U, re.I)
jsi = JSInterpreter(r''' jsi = JSInterpreter(r'function f() { let a=/,][}",],()}(\[)/; return a; }')
function x() { let a="data-name".replace("data-", ""); return a } self.assertEqual(jsi.call_function('f').pattern, r',][}",],()}(\[)')
''')
self.assertEqual(jsi.call_function('x'), 'name')
jsi = JSInterpreter(r''' jsi = JSInterpreter(r'function f() { let a=[/[)\\]/]; return a[0]; }')
function x() { let a="data-name".replace(new RegExp("^.+-"), ""); return a; } self.assertEqual(jsi.call_function('f').pattern, r'[)\\]')
''')
self.assertEqual(jsi.call_function('x'), 'name')
jsi = JSInterpreter(r''' def test_replace(self):
function x() { let a="data-name".replace(/^.+-/, ""); return a; } self._test('function f() { let a="data-name".replace("data-", ""); return a }',
''') 'name')
self.assertEqual(jsi.call_function('x'), 'name') self._test('function f() { let a="data-name".replace(new RegExp("^.+-"), ""); return a; }',
'name')
jsi = JSInterpreter(r''' self._test('function f() { let a="data-name".replace(/^.+-/, ""); return a; }',
function x() { let a="data-name".replace(/a/g, "o"); return a; } 'name')
''') self._test('function f() { let a="data-name".replace(/a/g, "o"); return a; }',
self.assertEqual(jsi.call_function('x'), 'doto-nome') 'doto-nome')
self._test('function f() { let a="data-name".replaceAll("a", "o"); return a; }',
jsi = JSInterpreter(r''' 'doto-nome')
function x() { let a="data-name".replaceAll("a", "o"); return a; }
''')
self.assertEqual(jsi.call_function('x'), 'doto-nome')
jsi = JSInterpreter(r'''
function x() { let a=[/[)\\]/]; return a[0]; }
''')
self.assertEqual(jsi.call_function('x').pattern, r'[)\\]')
""" # fails
jsi = JSInterpreter(r'''
function x() { let a=100; a/=/[0-9]+/.exec('divide by 20 today')[0]; }
''')
self.assertEqual(jsi.call_function('x'), 5)
"""
def test_char_code_at(self): def test_char_code_at(self):
jsi = JSInterpreter('function x(i){return "test".charCodeAt(i)}') jsi = JSInterpreter('function f(i){return "test".charCodeAt(i)}')
self.assertEqual(jsi.call_function('x', 0), 116) self._test(jsi, 116, args=[0])
self.assertEqual(jsi.call_function('x', 1), 101) self._test(jsi, 101, args=[1])
self.assertEqual(jsi.call_function('x', 2), 115) self._test(jsi, 115, args=[2])
self.assertEqual(jsi.call_function('x', 3), 116) self._test(jsi, 116, args=[3])
self.assertEqual(jsi.call_function('x', 4), None) self._test(jsi, None, args=[4])
self.assertEqual(jsi.call_function('x', 'not_a_number'), 116) self._test(jsi, 116, args=['not_a_number'])
def test_bitwise_operators_overflow(self): def test_bitwise_operators_overflow(self):
jsi = JSInterpreter('function x(){return -524999584 << 5}') self._test('function f(){return -524999584 << 5}', 379882496)
self.assertEqual(jsi.call_function('x'), 379882496) self._test('function f(){return 1236566549 << 5}', 915423904)
jsi = JSInterpreter('function x(){return 1236566549 << 5}') def test_bitwise_operators_typecast(self):
self.assertEqual(jsi.call_function('x'), 915423904) # madness
self._test('function f(){return null << 5}', 0)
self._test('function f(){return undefined >> 5}', 0)
self._test('function f(){return 42 << NaN}', 42)
self._test('function f(){return 42 << Infinity}', 42)
def test_bitwise_operators_madness(self): def test_negative(self):
jsi = JSInterpreter('function x(){return null << 5}') self._test('function f(){return 2 * -2.0 ;}', -4)
self.assertEqual(jsi.call_function('x'), 0) self._test('function f(){return 2 - - -2 ;}', 0)
self._test('function f(){return 2 - - - -2 ;}', 4)
jsi = JSInterpreter('function x(){return undefined >> 5}') self._test('function f(){return 2 - + + - -2;}', 0)
self.assertEqual(jsi.call_function('x'), 0) self._test('function f(){return 2 + - + - -2;}', 0)
jsi = JSInterpreter('function x(){return 42 << NaN}')
self.assertEqual(jsi.call_function('x'), 42)
jsi = JSInterpreter('function x(){return 42 << Infinity}')
self.assertEqual(jsi.call_function('x'), 42)
def test_32066(self): def test_32066(self):
jsi = JSInterpreter("function x(){return Math.pow(3, 5) + new Date('1970-01-01T08:01:42.000+08:00') / 1000 * -239 - -24205;}") self._test(
self.assertEqual(jsi.call_function('x'), 70) "function f(){return Math.pow(3, 5) + new Date('1970-01-01T08:01:42.000+08:00') / 1000 * -239 - -24205;}",
70)
def test_unary_operators(self): @unittest.skip('Not yet working')
jsi = JSInterpreter('function f(){return 2 - - - 2;}')
self.assertEqual(jsi.call_function('f'), 0)
jsi = JSInterpreter('function f(){return 2 + - + - - 2;}')
self.assertEqual(jsi.call_function('f'), 0)
# https://github.com/ytdl-org/youtube-dl/issues/32815
jsi = JSInterpreter('function f(){return 0 - 7 * - 6;}')
self.assertEqual(jsi.call_function('f'), 42)
""" # fails so far
def test_packed(self): def test_packed(self):
jsi = JSInterpreter('''function x(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''') self._test(
self.assertEqual(jsi.call_function('x', '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|'))) '''function f(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}''',
""" '''h 7=g("1j");7.7h({7g:[{33:"w://7f-7e-7d-7c.v.7b/7a/79/78/77/76.74?t=73&s=2s&e=72&f=2t&71=70.0.0.1&6z=6y&6x=6w"}],6v:"w://32.v.u/6u.31",16:"r%",15:"r%",6t:"6s",6r:"",6q:"l",6p:"l",6o:"6n",6m:\'6l\',6k:"6j",9:[{33:"/2u?b=6i&n=50&6h=w://32.v.u/6g.31",6f:"6e"}],1y:{6d:1,6c:\'#6b\',6a:\'#69\',68:"67",66:30,65:r,},"64":{63:"%62 2m%m%61%5z%5y%5x.u%5w%5v%5u.2y%22 2k%m%1o%22 5t%m%1o%22 5s%m%1o%22 2j%m%5r%22 16%m%5q%22 15%m%5p%22 5o%2z%5n%5m%2z",5l:"w://v.u/d/1k/5k.2y",5j:[]},\'5i\':{"5h":"5g"},5f:"5e",5d:"w://v.u",5c:{},5b:l,1x:[0.25,0.50,0.75,1,1.25,1.5,2]});h 1m,1n,5a;h 59=0,58=0;h 7=g("1j");h 2x=0,57=0,56=0;$.55({54:{\'53-52\':\'2i-51\'}});7.j(\'4z\',6(x){c(5>0&&x.1l>=5&&1n!=1){1n=1;$(\'q.4y\').4x(\'4w\')}});7.j(\'13\',6(x){2x=x.1l});7.j(\'2g\',6(x){2w(x)});7.j(\'4v\',6(){$(\'q.2v\').4u()});6 2w(x){$(\'q.2v\').4t();c(1m)19;1m=1;17=0;c(4s.4r===l){17=1}$.4q(\'/2u?b=4p&2l=1k&4o=2t-4n-4m-2s-4l&4k=&4j=&4i=&17=\'+17,6(2r){$(\'#4h\').4g(2r)});$(\'.3-8-4f-4e:4d("4c")\').2h(6(e){2q();g().4b(0);g().4a(l)});6 2q(){h $14=$("<q />").2p({1l:"49",16:"r%",15:"r%",48:0,2n:0,2o:47,46:"45(10%, 10%, 10%, 0.4)","44-43":"42"});$("<41 />").2p({16:"60%",15:"60%",2o:40,"3z-2n":"3y"}).3x({\'2m\':\'/?b=3w&2l=1k\',\'2k\':\'0\',\'2j\':\'2i\'}).2f($14);$14.2h(6(){$(3v).3u();g().2g()});$14.2f($(\'#1j\'))}g().13(0);}6 3t(){h 9=7.1b(2e);2d.2c(9);c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==2e){2d.2c(\'!!=\'+i);7.1p(i)}}}}7.j(\'3s\',6(){g().1h("/2a/3r.29","3q 10 28",6(){g().13(g().27()+10)},"2b");$("q[26=2b]").23().21(\'.3-20-1z\');g().1h("/2a/3p.29","3o 10 28",6(){h 12=g().27()-10;c(12<0)12=0;g().13(12)},"24");$("q[26=24]").23().21(\'.3-20-1z\');});6 1i(){}7.j(\'3n\',6(){1i()});7.j(\'3m\',6(){1i()});7.j("k",6(y){h 9=7.1b();c(9.n<2)19;$(\'.3-8-3l-3k\').3j(6(){$(\'#3-8-a-k\').1e(\'3-8-a-z\');$(\'.3-a-k\').p(\'o-1f\',\'11\')});7.1h("/3i/3h.3g","3f 3e",6(){$(\'.3-1w\').3d(\'3-8-1v\');$(\'.3-8-1y, .3-8-1x\').p(\'o-1g\',\'11\');c($(\'.3-1w\').3c(\'3-8-1v\')){$(\'.3-a-k\').p(\'o-1g\',\'l\');$(\'.3-a-k\').p(\'o-1f\',\'l\');$(\'.3-8-a\').1e(\'3-8-a-z\');$(\'.3-8-a:1u\').3b(\'3-8-a-z\')}3a{$(\'.3-a-k\').p(\'o-1g\',\'11\');$(\'.3-a-k\').p(\'o-1f\',\'11\');$(\'.3-8-a:1u\').1e(\'3-8-a-z\')}},"39");7.j("38",6(y){1d.37(\'1c\',y.9[y.36].1a)});c(1d.1t(\'1c\')){35("1s(1d.1t(\'1c\'));",34)}});h 18;6 1s(1q){h 9=7.1b();c(9.n>1){1r(i=0;i<9.n;i++){c(9[i].1a==1q){c(i==18){19}18=i;7.1p(i)}}}}',36,270,'|||jw|||function|player|settings|tracks|submenu||if||||jwplayer|var||on|audioTracks|true|3D|length|aria|attr|div|100|||sx|filemoon|https||event|active||false|tt|seek|dd|height|width|adb|current_audio|return|name|getAudioTracks|default_audio|localStorage|removeClass|expanded|checked|addButton|callMeMaybe|vplayer|0fxcyc2ajhp1|position|vvplay|vvad|220|setCurrentAudioTrack|audio_name|for|audio_set|getItem|last|open|controls|playbackRates|captions|rewind|icon|insertAfter||detach|ff00||button|getPosition|sec|png|player8|ff11|log|console|track_name|appendTo|play|click|no|scrolling|frameborder|file_code|src|top|zIndex|css|showCCform|data|1662367683|383371|dl|video_ad|doPlay|prevt|mp4|3E||jpg|thumbs|file|300|setTimeout|currentTrack|setItem|audioTrackChanged|dualSound|else|addClass|hasClass|toggleClass|Track|Audio|svg|dualy|images|mousedown|buttons|topbar|playAttemptFailed|beforePlay|Rewind|fr|Forward|ff|ready|set_audio_track|remove|this|upload_srt|prop|50px|margin|1000001|iframe|center|align|text|rgba|background|1000000|left|absolute|pause|setCurrentCaptions|Upload|contains|item|content|html|fviews|referer|prem|embed|3e57249ef633e0d03bf76ceb8d8a4b65|216|83|hash|view|get|TokenZir|window|hide|show|complete|slow|fadeIn|video_ad_fadein|time||cache|Cache|Content|headers|ajaxSetup|v2done|tott|vastdone2|vastdone1|vvbefore|playbackRateControls|cast|aboutlink|FileMoon|abouttext|UHD|1870|qualityLabels|sites|GNOME_POWER|link|2Fiframe|3C|allowfullscreen|22360|22640|22no|marginheight|marginwidth|2FGNOME_POWER|2F0fxcyc2ajhp1|2Fe|2Ffilemoon|2F|3A||22https|3Ciframe|code|sharing|fontOpacity|backgroundOpacity|Tahoma|fontFamily|303030|backgroundColor|FFFFFF|color|userFontScale|thumbnails|kind|0fxcyc2ajhp10000|url|get_slides|start|startparam|none|preload|html5|primary|hlshtml|androidhls|duration|uniform|stretching|0fxcyc2ajhp1_xt|image|2048|sp|6871|asn|127|srv|43200|_g3XlBcu2lmD9oDexD2NLWSmah2Nu3XcDrl93m9PwXY|m3u8||master|0fxcyc2ajhp1_x|00076|01|hls2|to|s01|delivery|storage|moon|sources|setup'''.split('|'))
def test_join(self):
test_input = list('test')
tests = [
'function f(a, b){return a.join(b)}',
'function f(a, b){return Array.prototype.join.call(a, b)}',
'function f(a, b){return Array.prototype.join.apply(a, [b])}',
]
for test in tests:
jsi = JSInterpreter(test)
self._test(jsi, 'test', args=[test_input, ''])
self._test(jsi, 't-e-s-t', args=[test_input, '-'])
self._test(jsi, '', args=[[], '-'])
def test_split(self):
test_result = list('test')
tests = [
'function f(a, b){return a.split(b)}',
'function f(a, b){return String.prototype.split.call(a, b)}',
'function f(a, b){return String.prototype.split.apply(a, [b])}',
]
for test in tests:
jsi = JSInterpreter(test)
self._test(jsi, test_result, args=['test', ''])
self._test(jsi, test_result, args=['t-e-s-t', '-'])
self._test(jsi, [''], args=['', '-'])
self._test(jsi, [], args=['', ''])
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -162,6 +162,18 @@ _NSIG_TESTS = [
'https://www.youtube.com/s/player/590f65a6/player_ias.vflset/en_US/base.js', 'https://www.youtube.com/s/player/590f65a6/player_ias.vflset/en_US/base.js',
'1tm7-g_A9zsI8_Lay_', 'xI4Vem4Put_rOg', '1tm7-g_A9zsI8_Lay_', 'xI4Vem4Put_rOg',
), ),
(
'https://www.youtube.com/s/player/b22ef6e7/player_ias.vflset/en_US/base.js',
'b6HcntHGkvBLk_FRf', 'kNPW6A7FyP2l8A',
),
(
'https://www.youtube.com/s/player/3400486c/player_ias.vflset/en_US/base.js',
'lL46g3XifCKUZn1Xfw', 'z767lhet6V2Skl',
),
(
'https://www.youtube.com/s/player/5604538d/player_ias.vflset/en_US/base.js',
'7X-he4jjvMx7BCX', 'sViSydX8IHtdWA',
),
] ]

View File

@ -1636,7 +1636,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
try: try:
jsi, player_id, func_code = self._extract_n_function_code(video_id, player_url) jsi, player_id, func_code = self._extract_n_function_code(video_id, player_url)
except ExtractorError as e: except ExtractorError as e:
raise ExtractorError('Unable to extract nsig jsi, player_id, func_codefunction code', cause=e) raise ExtractorError('Unable to extract nsig function code', cause=e)
if self.get_param('youtube_print_sig_code'): if self.get_param('youtube_print_sig_code'):
self.to_screen('Extracted nsig function from {0}:\n{1}\n'.format( self.to_screen('Extracted nsig function from {0}:\n{1}\n'.format(
player_id, func_code[1])) player_id, func_code[1]))
@ -1647,7 +1647,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
except JSInterpreter.Exception as e: except JSInterpreter.Exception as e:
self.report_warning( self.report_warning(
'%s (%s %s)' % ( '%s (%s %s)' % (
'Unable to decode n-parameter: download likely to be throttled', 'Unable to decode n-parameter: expect download to be blocked or throttled',
error_to_compat_str(e), error_to_compat_str(e),
traceback.format_exc()), traceback.format_exc()),
video_id=video_id) video_id=video_id)
@ -1658,13 +1658,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
def _extract_n_function_name(self, jscode): def _extract_n_function_name(self, jscode):
func_name, idx = self._search_regex( func_name, idx = self._search_regex(
r'\.get\("n"\)\)&&\(b=(?P<nfunc>[a-zA-Z_$][\w$]*)(?:\[(?P<idx>\d+)\])?\([\w$]+\)', # new: (b=String.fromCharCode(110),c=a.get(b))&&c=nfunc[idx](c)
jscode, 'Initial JS player n function name', group=('nfunc', 'idx')) # or: (b="nn"[+a.D],c=a.get(b))&&(c=nfunc[idx](c)s
# old: .get("n"))&&(b=nfunc[idx](b)
# older: .get("n"))&&(b=nfunc(b)
r'''(?x)
(?:\(\s*(?P<b>[a-z])\s*=\s*(?:
String\s*\.\s*fromCharCode\s*\(\s*110\s*\)|
"n+"\[\s*\+?s*[\w$.]+\s*]
)\s*,(?P<c>[a-z])\s*=\s*[a-z]\s*)?
\.\s*get\s*\(\s*(?(b)(?P=b)|"n{1,2}")(?:\s*\)){2}\s*&&\s*\(\s*(?(c)(?P=c)|b)\s*=\s*
(?P<nfunc>[a-zA-Z_$][\w$]*)(?:\s*\[(?P<idx>\d+)\])?\s*\(\s*[\w$]+\s*\)
''', jscode, 'Initial JS player n function name', group=('nfunc', 'idx'))
if not idx: if not idx:
return func_name return func_name
return self._parse_json(self._search_regex( return self._parse_json(self._search_regex(
r'var {0}\s*=\s*(\[.+?\])\s*[,;]'.format(re.escape(func_name)), jscode, r'var\s+{0}\s*=\s*(\[.+?\])\s*[,;]'.format(re.escape(func_name)), jscode,
'Initial JS player n function list ({0}.{1})'.format(func_name, idx)), 'Initial JS player n function list ({0}.{1})'.format(func_name, idx)),
func_name, transform_source=js_to_json)[int(idx)] func_name, transform_source=js_to_json)[int(idx)]
@ -1679,17 +1689,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor):
func_name = self._extract_n_function_name(jscode) func_name = self._extract_n_function_name(jscode)
# For redundancy func_code = jsi.extract_function_code(func_name)
func_code = self._search_regex(
r'''(?xs)%s\s*=\s*function\s*\((?P<var>[\w$]+)\)\s*
# NB: The end of the regex is intentionally kept strict
{(?P<code>.+?}\s*return\ [\w$]+.join\(""\))};''' % func_name,
jscode, 'nsig function', group=('var', 'code'), default=None)
if func_code:
func_code = ([func_code[0]], func_code[1])
else:
self.write_debug('Extracting nsig function with jsinterp')
func_code = jsi.extract_function_code(func_name)
self.cache.store('youtube-nsig', player_id, func_code) self.cache.store('youtube-nsig', player_id, func_code)
return jsi, player_id, func_code return jsi, player_id, func_code

View File

@ -20,7 +20,9 @@ from .compat import (
compat_basestring, compat_basestring,
compat_chr, compat_chr,
compat_collections_chain_map as ChainMap, compat_collections_chain_map as ChainMap,
compat_filter as filter,
compat_itertools_zip_longest as zip_longest, compat_itertools_zip_longest as zip_longest,
compat_map as map,
compat_str, compat_str,
) )
@ -252,7 +254,7 @@ class Debugger(object):
cls.write('=> Raises:', e, '<-|', stmt, level=allow_recursion) cls.write('=> Raises:', e, '<-|', stmt, level=allow_recursion)
raise raise
if cls.ENABLED and stmt.strip(): if cls.ENABLED and stmt.strip():
if should_ret or not repr(ret) == stmt: if should_ret or repr(ret) != stmt:
cls.write(['->', '=>'][should_ret], repr(ret), '<-|', stmt, level=allow_recursion) cls.write(['->', '=>'][should_ret], repr(ret), '<-|', stmt, level=allow_recursion)
return ret, should_ret return ret, should_ret
return interpret_statement return interpret_statement
@ -365,6 +367,8 @@ class JSInterpreter(object):
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, after_op, in_regex_char_group = None, False, True, False in_quote, escaping, after_op, in_regex_char_group = None, False, True, False
skipping = 0 skipping = 0
if skip_delims:
skip_delims = variadic(skip_delims)
for idx, char in enumerate(expr): for idx, char in enumerate(expr):
paren_delta = 0 paren_delta = 0
if not in_quote: if not in_quote:
@ -391,7 +395,7 @@ class JSInterpreter(object):
continue continue
elif pos == 0 and skip_delims: elif pos == 0 and skip_delims:
here = expr[idx:] here = expr[idx:]
for s in variadic(skip_delims): for s in skip_delims:
if here.startswith(s) and s: if here.startswith(s) and s:
skipping = len(s) - 1 skipping = len(s) - 1
break break
@ -412,7 +416,6 @@ class JSInterpreter(object):
if delim is None: if delim is None:
delim = expr and _MATCHING_PARENS[expr[0]] 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:
raise cls.Exception('No terminating paren {delim} in {expr!r:.5500}'.format(**locals())) raise cls.Exception('No terminating paren {delim} in {expr!r:.5500}'.format(**locals()))
return separated[0][1:].strip(), separated[1].strip() return separated[0][1:].strip(), separated[1].strip()
@ -487,6 +490,7 @@ class JSInterpreter(object):
# fails on (eg) if (...) stmt1; else stmt2; # fails on (eg) if (...) stmt1; else stmt2;
sub_statements = list(self._separate(stmt, ';')) or [''] sub_statements = list(self._separate(stmt, ';')) or ['']
expr = stmt = sub_statements.pop().strip() expr = stmt = sub_statements.pop().strip()
for sub_stmt in sub_statements: for sub_stmt in sub_statements:
ret, should_return = self.interpret_statement(sub_stmt, local_vars, allow_recursion) ret, should_return = self.interpret_statement(sub_stmt, local_vars, allow_recursion)
if should_return: if should_return:
@ -626,8 +630,7 @@ class JSInterpreter(object):
if m.group('err'): if m.group('err'):
catch_vars[m.group('err')] = err.error if isinstance(err, JS_Throw) else err catch_vars[m.group('err')] = err.error if isinstance(err, JS_Throw) else err
catch_vars = local_vars.new_child(m=catch_vars) catch_vars = local_vars.new_child(m=catch_vars)
err = None err, pending = None, self.interpret_statement(sub_expr, catch_vars, allow_recursion)
pending = self.interpret_statement(sub_expr, catch_vars, allow_recursion)
m = self._FINALLY_RE.match(expr) m = self._FINALLY_RE.match(expr)
if m: if m:
@ -801,16 +804,19 @@ class JSInterpreter(object):
if op in ('+', '-'): if op in ('+', '-'):
# simplify/adjust consecutive instances of these operators # simplify/adjust consecutive instances of these operators
undone = 0 undone = 0
while len(separated) > 1 and not separated[-1].strip(): separated = [s.strip() for s in separated]
while len(separated) > 1 and not separated[-1]:
undone += 1 undone += 1
separated.pop() separated.pop()
if op == '-' and undone % 2 != 0: if op == '-' and undone % 2 != 0:
right_expr = op + right_expr right_expr = op + right_expr
elif op == '+': elif op == '+':
while len(separated) > 1 and separated[-1].strip() in self.OP_CHARS: while len(separated) > 1 and set(separated[-1]) <= self.OP_CHARS:
right_expr = separated.pop() + right_expr
if separated[-1][-1:] in self.OP_CHARS:
right_expr = separated.pop() + right_expr right_expr = separated.pop() + right_expr
# hanging op at end of left => unary + (strip) or - (push right) # hanging op at end of left => unary + (strip) or - (push right)
left_val = separated[-1] left_val = separated[-1] if separated else ''
for dm_op in ('*', '%', '/', '**'): for dm_op in ('*', '%', '/', '**'):
bodmas = tuple(self._separate(left_val, dm_op, skip_delims=skip_delim)) bodmas = tuple(self._separate(left_val, dm_op, skip_delims=skip_delim))
if len(bodmas) > 1 and not bodmas[-1].strip(): if len(bodmas) > 1 and not bodmas[-1].strip():
@ -844,7 +850,7 @@ class JSInterpreter(object):
memb = member memb = member
raise self.Exception('{memb} {msg}'.format(**locals()), expr=expr) raise self.Exception('{memb} {msg}'.format(**locals()), expr=expr)
def eval_method(): def eval_method(variable, member):
if (variable, member) == ('console', 'debug'): if (variable, member) == ('console', 'debug'):
if Debugger.ENABLED: if Debugger.ENABLED:
Debugger.write(self.interpret_expression('[{}]'.format(arg_str), local_vars, allow_recursion)) Debugger.write(self.interpret_expression('[{}]'.format(arg_str), local_vars, allow_recursion))
@ -852,6 +858,7 @@ class JSInterpreter(object):
types = { types = {
'String': compat_str, 'String': compat_str,
'Math': float, 'Math': float,
'Array': list,
} }
obj = local_vars.get(variable) obj = local_vars.get(variable)
if obj in (JS_Undefined, None): if obj in (JS_Undefined, None):
@ -877,12 +884,29 @@ class JSInterpreter(object):
self.interpret_expression(v, local_vars, allow_recursion) self.interpret_expression(v, local_vars, allow_recursion)
for v in self._separate(arg_str)] for v in self._separate(arg_str)]
if obj == compat_str: # Fixup prototype call
if isinstance(obj, type):
new_member, rest = member.partition('.')[0::2]
if new_member == 'prototype':
new_member, func_prototype = rest.partition('.')[0::2]
assertion(argvals, 'takes one or more arguments')
assertion(isinstance(argvals[0], obj), 'must bind to type {0}'.format(obj))
if func_prototype == 'call':
obj = argvals.pop(0)
elif func_prototype == 'apply':
assertion(len(argvals) == 2, 'takes two arguments')
obj, argvals = argvals
assertion(isinstance(argvals, list), 'second argument must be a list')
else:
raise self.Exception('Unsupported Function method ' + func_prototype, expr)
member = new_member
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(map(compat_chr, argvals))
raise self.Exception('Unsupported string method ' + member, expr=expr) raise self.Exception('Unsupported string method ' + member, expr=expr)
elif obj == float: elif obj is float:
if member == 'pow': if member == 'pow':
assertion(len(argvals) == 2, 'takes two arguments') assertion(len(argvals) == 2, 'takes two arguments')
return argvals[0] ** argvals[1] return argvals[0] ** argvals[1]
@ -907,12 +931,12 @@ class JSInterpreter(object):
elif member == 'splice': elif member == 'splice':
assertion(isinstance(obj, list), 'must be applied on a list') assertion(isinstance(obj, list), 'must be applied on a list')
assertion(argvals, 'takes one or more arguments') assertion(argvals, 'takes one or more arguments')
index, howMany = map(int, (argvals + [len(obj)])[:2]) index, how_many = map(int, (argvals + [len(obj)])[:2])
if index < 0: if index < 0:
index += len(obj) index += len(obj)
add_items = argvals[2:] add_items = argvals[2:]
res = [] res = []
for i in range(index, min(index + howMany, len(obj))): for _ in range(index, min(index + how_many, len(obj))):
res.append(obj.pop(index)) res.append(obj.pop(index))
for i, item in enumerate(add_items): for i, item in enumerate(add_items):
obj.insert(index + i, item) obj.insert(index + i, item)
@ -970,11 +994,11 @@ class JSInterpreter(object):
if remaining: if remaining:
ret, should_abort = self.interpret_statement( ret, should_abort = self.interpret_statement(
self._named_object(local_vars, eval_method()) + remaining, self._named_object(local_vars, eval_method(variable, member)) + remaining,
local_vars, allow_recursion) local_vars, allow_recursion)
return ret, should_return or should_abort return ret, should_return or should_abort
else: else:
return eval_method(), should_return return eval_method(variable, member), should_return
elif md.get('function'): elif md.get('function'):
fname = m.group('fname') fname = m.group('fname')
@ -1002,28 +1026,25 @@ class JSInterpreter(object):
def extract_object(self, objname): def extract_object(self, objname):
_FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')''' _FUNC_NAME_RE = r'''(?:[a-zA-Z$0-9]+|"[a-zA-Z$0-9]+"|'[a-zA-Z$0-9]+')'''
obj = {} obj = {}
fields = None fields = next(filter(None, (
for obj_m in re.finditer( obj_m.group('fields') for obj_m in re.finditer(
r'''(?xs) r'''(?xs)
{0}\s*\.\s*{1}|{1}\s*=\s*\{{\s* {0}\s*\.\s*{1}|{1}\s*=\s*\{{\s*
(?P<fields>({2}\s*:\s*function\s*\(.*?\)\s*\{{.*?}}(?:,\s*)?)*) (?P<fields>({2}\s*:\s*function\s*\(.*?\)\s*\{{.*?}}(?:,\s*)?)*)
}}\s*; }}\s*;
'''.format(_NAME_RE, re.escape(objname), _FUNC_NAME_RE), '''.format(_NAME_RE, re.escape(objname), _FUNC_NAME_RE),
self.code): self.code))), None)
fields = obj_m.group('fields') if not fields:
if fields:
break
else:
raise self.Exception('Could not find object ' + objname) raise self.Exception('Could not find object ' + objname)
# Currently, it only supports function definitions # Currently, it only supports function definitions
fields_m = re.finditer( for f in re.finditer(
r'''(?x) r'''(?x)
(?P<key>%s)\s*:\s*function\s*\((?P<args>(?:%s|,)*)\){(?P<code>[^}]+)} (?P<key>%s)\s*:\s*function\s*\((?P<args>(?:%s|,)*)\){(?P<code>[^}]+)}
''' % (_FUNC_NAME_RE, _NAME_RE), ''' % (_FUNC_NAME_RE, _NAME_RE),
fields) fields):
for f in fields_m:
argnames = self.build_arglist(f.group('args')) argnames = self.build_arglist(f.group('args'))
obj[remove_quotes(f.group('key'))] = self.build_function(argnames, f.group('code')) name = remove_quotes(f.group('key'))
obj[name] = function_with_repr(self.build_function(argnames, f.group('code')), 'F<{0}>'.format(name))
return obj return obj
@ -1058,7 +1079,7 @@ class JSInterpreter(object):
def extract_function(self, funcname): def extract_function(self, funcname):
return function_with_repr( return function_with_repr(
self.extract_function_from_code(*self.extract_function_code(funcname)), self.extract_function_from_code(*self.extract_function_code(funcname)),
'F<%s>' % (funcname, )) 'F<%s>' % (funcname,))
def extract_function_from_code(self, argnames, code, *global_stack): def extract_function_from_code(self, argnames, code, *global_stack):
local_vars = {} local_vars = {}
@ -1067,7 +1088,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(local_vars, self.extract_function_from_code( name = self._named_object(local_vars, self.extract_function_from_code(
[x.strip() for x in mobj.group('args').split(',')], [x.strip() for x in mobj.group('args').split(',')],
body, local_vars, *global_stack)) body, local_vars, *global_stack))
@ -1095,8 +1116,7 @@ class JSInterpreter(object):
argnames = tuple(argnames) argnames = tuple(argnames)
def resf(args, kwargs={}, allow_recursion=100): def resf(args, kwargs={}, allow_recursion=100):
global_stack[0].update( global_stack[0].update(zip_longest(argnames, args, fillvalue=None))
zip_longest(argnames, args, fillvalue=None))
global_stack[0].update(kwargs) global_stack[0].update(kwargs)
var_stack = LocalNameSpace(*global_stack) var_stack = LocalNameSpace(*global_stack)
ret, should_abort = self.interpret_statement(code.replace('\n', ' '), var_stack, allow_recursion - 1) ret, should_abort = self.interpret_statement(code.replace('\n', ' '), var_stack, allow_recursion - 1)

View File

@ -6604,27 +6604,53 @@ class _UnsafeExtensionError(Exception):
), ),
# video # video
MEDIA_EXTENSIONS.video, ( MEDIA_EXTENSIONS.video, (
'avif', 'asx',
'ismv', 'ismv',
'm2t',
'm2ts', 'm2ts',
'm2v',
'm4s', 'm4s',
'mng', 'mng',
'mp2v',
'mp4v',
'mpe',
'mpeg', 'mpeg',
'mpeg1',
'mpeg2',
'mpeg4',
'mxf',
'ogm',
'qt', 'qt',
'rm',
'swf', 'swf',
'ts', 'ts',
'vob',
'vp9', 'vp9',
'wvm',
), ),
# audio # audio
MEDIA_EXTENSIONS.audio, ( MEDIA_EXTENSIONS.audio, (
'3ga',
'ac3',
'adts',
'aif',
'au',
'dts',
'isma', 'isma',
'it',
'mid', 'mid',
'mod',
'mpga', 'mpga',
'mp1',
'mp2',
'mp4a',
'mpa',
'ra', 'ra',
'shn',
'xm',
), ),
# image # image
MEDIA_EXTENSIONS.thumbnails, ( MEDIA_EXTENSIONS.thumbnails, (
'avif',
'bmp', 'bmp',
'gif', 'gif',
'ico', 'ico',
@ -6634,6 +6660,7 @@ class _UnsafeExtensionError(Exception):
'jxl', 'jxl',
'svg', 'svg',
'tif', 'tif',
'tiff',
'wbmp', 'wbmp',
), ),
# subtitle # subtitle
@ -6641,10 +6668,15 @@ class _UnsafeExtensionError(Exception):
'dfxp', 'dfxp',
'fs', 'fs',
'ismt', 'ismt',
'json3',
'sami', 'sami',
'scc', 'scc',
'srv1',
'srv2',
'srv3',
'ssa', 'ssa',
'tt', 'tt',
'xml',
), ),
# others # others
MEDIA_EXTENSIONS.manifests, MEDIA_EXTENSIONS.manifests,
@ -6658,7 +6690,6 @@ class _UnsafeExtensionError(Exception):
# 'swp', # 'swp',
# 'url', # 'url',
# 'webloc', # 'webloc',
# 'xml',
))) )))
def __init__(self, extension): def __init__(self, extension):