diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt index f0ddd54c965..8ac07bba312 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt @@ -780,6 +780,11 @@ fun fixTitle(str: String): String { * * Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null) **/ +// Deprecate after next stable +/* @Deprecated( + message = "Use JsContext or evalJs instead.", + level = DeprecationLevel.WARNING, +) */ suspend fun getRhinoContext(): org.mozilla.javascript.Context { return Coroutines.mainWork { val rhino = org.mozilla.javascript.Context.enter() diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt index 211b5ecf946..fb11e334de7 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt @@ -3,9 +3,9 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.JsContext import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.newExtractorLink -import org.mozilla.javascript.Context class Watchadsontape : StreamTape() { override var mainUrl = "https://watchadsontape.com" @@ -30,24 +30,17 @@ open class StreamTape : ExtractorApi() { override suspend fun getUrl(url: String, referer: String?): List? { with(app.get(url)) { - var result = + val result = this.document.select("script").firstOrNull { it.html().contains("botlink').innerHTML") } - ?.html()?.lines()?.firstOrNull{ it.contains("botlink').innerHTML") }?.let { + ?.html()?.lines()?.firstOrNull { it.contains("botlink').innerHTML") }?.let { val scriptContent = it.substringAfter(").innerHTML").replaceFirst("=", "var url =") - val rhino = Context.enter() - rhino.setInterpretedMode(true) - val scope = rhino.initStandardObjects() - var result = "" - try { - rhino.evaluateString(scope, scriptContent, "url", 1, null) - result = scope.get("url", scope).toString() - }finally { - rhino.close() - } - result + val ctx = JsContext() + ctx.eval(scriptContent) + ctx["url"]?.toString() ?: "" } - if(!result.isNullOrEmpty()){ + + if (!result.isNullOrEmpty()) { val extractedUrl = "https:${result}&stream=1" return listOf( newExtractorLink( diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt index fad6e8571b3..5068b169b2d 100644 --- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt @@ -3,9 +3,6 @@ package com.lagradost.cloudstream3.extractors import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.* -import org.mozilla.javascript.Context -import org.mozilla.javascript.EvaluatorException -import org.mozilla.javascript.Scriptable open class Userload : ExtractorApi() { override var name = "Userload" @@ -32,17 +29,8 @@ open class Userload : ExtractorApi() { return array } - private fun evaluateMath(mathExpression : String): String { - val rhino = Context.enter() - rhino.initStandardObjects() - rhino.setInterpretedMode(true) - val scope: Scriptable = rhino.initStandardObjects() - return try { - rhino.evaluateString(scope, "eval($mathExpression)", "JavaScript", 1, null).toString() - } - catch (e: EvaluatorException){ - "" - } + private fun evaluateMath(mathExpression: String): String { + return jsValueToString(evalJs("eval($mathExpression)")) } private fun decodeVideoJs(text: String): List { @@ -68,22 +56,16 @@ open class Userload : ExtractorApi() { subchar.add(splitInput(v).map { evaluateMath(it).substringBefore(".") }.toString().filter { it.isDigit() }) } var txtresult = "" - subchar.forEach{ + subchar.forEach { txtresult = txtresult.plus(Char(it.toInt(8))) } val val1 = Regex(""""morocco="((.|\\n)*?)"&mycountry="""").find(txtresult)?.groups?.get(1)?.value.toString().drop(1).dropLast(1) val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")") - return listOf( - val1, - val2 - ) - - + return listOf(val1, val2) } override suspend fun getUrl(url: String, referer: String?): List? { - val extractedLinksList: MutableList = mutableListOf() val response = app.get(url).text @@ -113,4 +95,4 @@ open class Userload : ExtractorApi() { return extractedLinksList } -} \ No newline at end of file +} diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsInterpreter.kt b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsInterpreter.kt new file mode 100644 index 00000000000..e20dd00beb6 --- /dev/null +++ b/library/src/commonMain/kotlin/com/lagradost/cloudstream3/utils/JsInterpreter.kt @@ -0,0 +1,1228 @@ +package com.lagradost.cloudstream3.utils + +import com.lagradost.cloudstream3.Prerelease +import com.lagradost.cloudstream3.mvvm.logError +import kotlin.math.* + +/** + * Lightweight pure-Kotlin JavaScript interpreter designed to replace Rhino for + * our own deobfuscation use-cases. + * + * Supports the subset of JS that appears in obfuscated video-hosting scripts: + * - Variable declarations (var / let / const) + * - String, number and boolean literals + * - Arithmetic / bitwise / comparison / logical operators + * - String concatenation with + + * - String methods: split, join, reverse, replace, charAt, charCodeAt, + * fromCharCode, substr, substring, slice, indexOf, + * trim, toLowerCase, toUpperCase, toString, length + * - Array literals and methods: join, reverse, split, push, pop, + * map, filter, forEach, length + * - parseInt / parseFloat / isNaN / isFinite + * - Math.* (sin, cos, floor, ceil, round, abs, pow, sqrt, log, max, min, random) + * - String.fromCharCode + * - Ternary operator (a ? b : c) + * - if / else / while / for statements + * - Function declarations and calls (named and anonymous) + * - return / break / continue + * - Template literals (back-tick strings) + * - typeof + * + * Usage: + * val result = JsInterpreter().eval("var x = 1+2; x") + * // result == 3.0 (numbers are always Double internally) + * + * // Drop-in replacement for the old Rhino-based evaluateString pattern: + * val ctx = JsContext() + * ctx.eval("var url = 'https:' + computeSuffix()") + * val url = ctx["url"] // retrieves the variable as a String + */ + +/** + * Convert any JS runtime value to its JavaScript string representation. + * Mirrors what JS `String(value)` would produce. + */ +@Prerelease +fun jsValueToString(v: Any?): String = toJsString(v) + +/** + * Stateful JS execution context. Keeps variables alive between [eval] calls, + * mimicking the Rhino "scope" object that extensions used to hold on to. + */ +@Prerelease +class JsContext { + private val interpreter = JsInterpreter() + + /** Evaluate [code] in this context. Returns the last expression value. */ + fun eval(code: String): Any? = interpreter.eval(code) + + /** Retrieve a variable set by previously evaluated code. */ + operator fun get(name: String): Any? = interpreter.getVar(name) + + /** Expose a Kotlin value to subsequently evaluated JS code. */ + operator fun set(name: String, value: Any?) = interpreter.setVar(name, value) +} + +/** + * Evaluate [js] and return its last value. Convenience wrapper for one-shot + * evaluations, equivalent to the old `rhino.evaluateString(scope, js, ...)`. + */ +@Prerelease +fun evalJs(js: String): Any? = JsInterpreter().eval(js) + +private enum class TT { + NUMBER, STRING, IDENT, PLUS, MINUS, STAR, SLASH, PERCENT, EQ, EQEQ, EQEQEQ, + NEQ, NEQEQ, LT, LTEQ, GT, GTEQ, AND, OR, NOT, AMP, PIPE, CARET, TILDE, + LSHIFT, RSHIFT, URSHIFT, PLUSEQ, MINUSEQ, STAREQ, SLASHEQ, PERCENTEQ, + PLUSPLUS, MINUSMINUS, DOT, COMMA, SEMI, COLON, QUESTION, LPAREN, RPAREN, + LBRACE, RBRACE, LBRACKET, RBRACKET, EOF +} + +private data class Token(val type: TT, val raw: String, val pos: Int) + +private class Lexer(private val src: String) { + var pos = 0 + private val tokens = mutableListOf() + private var idx = 0 + + init { tokenize() } + + private fun tokenize() { + while (pos < src.length) { + skipWhitespaceAndComments() + if (pos >= src.length) break + val c = src[pos] + when { + c.isDigit() || (c == '.' && pos + 1 < src.length && src[pos + 1].isDigit()) -> readNumber() + c == '"' || c == '\'' || c == '`' -> readString(c) + c.isLetter() || c == '_' || c == '$' -> readIdent() + else -> readOp() + } + } + tokens.add(Token(TT.EOF, "", pos)) + } + + private fun skipWhitespaceAndComments() { + while (pos < src.length) { + val c = src[pos] + if (c.isWhitespace()) { pos++; continue } + if (c == '/' && pos + 1 < src.length) { + when (src[pos + 1]) { + '/' -> { pos = src.indexOf('\n', pos).let { if (it < 0) src.length else it + 1 }; continue } + '*' -> { + val end = src.indexOf("*/", pos + 2) + pos = if (end < 0) src.length else end + 2; continue + } + else -> {} + } + } + break + } + } + + private fun readNumber() { + val start = pos + if (pos + 1 < src.length && src[pos] == '0' && + (src[pos + 1] == 'x' || src[pos + 1] == 'X') + ) { + pos += 2 + while (pos < src.length && src[pos].isLetterOrDigit()) pos++ + } else { + while (pos < src.length && (src[pos].isDigit() || src[pos] == '.')) pos++ + if (pos < src.length && (src[pos] == 'e' || src[pos] == 'E')) { + pos++ + if (pos < src.length && (src[pos] == '+' || src[pos] == '-')) pos++ + while (pos < src.length && src[pos].isDigit()) pos++ + } + } + tokens.add(Token(TT.NUMBER, src.substring(start, pos), start)) + } + + private fun readString(quote: Char) { + val start = pos++ + val sb = StringBuilder() + if (quote == '`') { + // template literal. We flatten it to a plain string (no interpolation yet) + while (pos < src.length && src[pos] != '`') { + if (src[pos] == '\\' && pos + 1 < src.length) { + sb.append(unescape(src[++pos])); pos++ + } else sb.append(src[pos++]) + } + if (pos < src.length) pos++ // consume closing ` + } else { + while (pos < src.length && src[pos] != quote) { + if (src[pos] == '\\' && pos + 1 < src.length) { + sb.append(unescape(src[++pos])); pos++ + } else sb.append(src[pos++]) + } + if (pos < src.length) pos++ + } + tokens.add(Token(TT.STRING, sb.toString(), start)) + } + + private fun unescape(c: Char) = when (c) { + 'n' -> '\n'; 'r' -> '\r'; 't' -> '\t'; 'b' -> '\b' + '\'' -> '\''; '"' -> '"'; '\\' -> '\\'; '`' -> '`' + else -> c + } + + private fun readIdent() { + val start = pos + while (pos < src.length && (src[pos].isLetterOrDigit() || src[pos] == '_' || src[pos] == '$')) pos++ + tokens.add(Token(TT.IDENT, src.substring(start, pos), start)) + } + + private fun readOp() { + val start = pos + val c = src[pos++] + val next = if (pos < src.length) src[pos] else '\u0000' + val nn = if (pos + 1 < src.length) src[pos + 1] else '\u0000' + fun adv(t: TT, n: Int = 0): Token { pos += n; return Token(t, "", start) } + val tok = when (c) { + '+' -> when (next) { '+' -> adv(TT.PLUSPLUS, 1); '=' -> adv(TT.PLUSEQ, 1); else -> Token(TT.PLUS, "+", start) } + '-' -> when (next) { '-' -> adv(TT.MINUSMINUS, 1); '=' -> adv(TT.MINUSEQ, 1); else -> Token(TT.MINUS, "-", start) } + '*' -> if (next == '=') adv(TT.STAREQ, 1) else Token(TT.STAR, "*", start) + '/' -> if (next == '=') adv(TT.SLASHEQ, 1) else Token(TT.SLASH, "/", start) + '%' -> if (next == '=') adv(TT.PERCENTEQ, 1) else Token(TT.PERCENT, "%", start) + '=' -> when { next == '=' && nn == '=' -> adv(TT.EQEQEQ, 2); next == '=' -> adv(TT.EQEQ, 1); else -> Token(TT.EQ, "=", start) } + '!' -> when { next == '=' && nn == '=' -> adv(TT.NEQEQ, 2); next == '=' -> adv(TT.NEQ, 1); else -> Token(TT.NOT, "!", start) } + '<' -> when { next == '<' -> adv(TT.LSHIFT, 1); next == '=' -> adv(TT.LTEQ, 1); else -> Token(TT.LT, "<", start) } + '>' -> when { next == '>' && nn == '>' -> adv(TT.URSHIFT, 2); next == '>' -> adv(TT.RSHIFT, 1); next == '=' -> adv(TT.GTEQ, 1); else -> Token(TT.GT, ">", start) } + '&' -> if (next == '&') adv(TT.AND, 1) else Token(TT.AMP, "&", start) + '|' -> if (next == '|') adv(TT.OR, 1) else Token(TT.PIPE, "|", start) + '^' -> Token(TT.CARET, "^", start) + '~' -> Token(TT.TILDE, "~", start) + '.' -> Token(TT.DOT, ".", start) + ',' -> Token(TT.COMMA, ",", start) + ';' -> Token(TT.SEMI, ";", start) + ':' -> Token(TT.COLON, ":", start) + '?' -> Token(TT.QUESTION, "?", start) + '(' -> Token(TT.LPAREN, "(", start) + ')' -> Token(TT.RPAREN, ")", start) + '{' -> Token(TT.LBRACE, "{", start) + '}' -> Token(TT.RBRACE, "}", start) + '[' -> Token(TT.LBRACKET, "[", start) + ']' -> Token(TT.RBRACKET, "]", start) + else -> Token(TT.SEMI, "", start) // swallow unknown + } + tokens.add(tok) + } + + fun peek(offset: Int = 0): Token = tokens.getOrElse(idx + offset) { tokens.last() } + fun consume(): Token = tokens[idx++] + fun expect(t: TT): Token { + val tok = consume() + if (tok.type != t) throw RuntimeException("Expected $t got ${tok.type} ('${tok.raw}') at ${tok.pos}") + return tok + } + fun matchIf(t: TT): Boolean = if (peek().type == t) { consume(); true } else false +} + +private sealed class Node +private data class NumLit(val v: Double) : Node() +private data class StrLit(val v: String) : Node() +private data class BoolLit(val v: Boolean) : Node() +private object NullLit : Node() +private object UndefinedLit : Node() +private data class Ident(val name: String) : Node() +private data class ArrayLit(val elems: List) : Node() +private data class ObjLit(val pairs: List>) : Node() +private data class MemberExpr(val obj: Node, val prop: Node, val computed: Boolean) : Node() +private data class CallExpr(val callee: Node, val args: List) : Node() +private data class NewExpr(val callee: Node, val args: List) : Node() +private data class UnaryExpr(val op: String, val expr: Node, val prefix: Boolean) : Node() +private data class BinExpr(val op: String, val left: Node, val right: Node) : Node() +private data class AssignExpr(val op: String, val left: Node, val right: Node) : Node() +private data class CondExpr(val test: Node, val cons: Node, val alt: Node) : Node() +private data class TypeofExpr(val expr: Node) : Node() +private data class SeqExpr(val exprs: List) : Node() +private data class FuncExpr(val name: String?, val params: List, val body: List) : Node() +private data class VarDecl(val decls: List>) : Node() +private data class ExprStmt(val expr: Node) : Node() +private data class BlockStmt(val stmts: List) : Node() +private data class ReturnStmt(val expr: Node?) : Node() +private data class IfStmt(val test: Node, val cons: Node, val alt: Node?) : Node() +private data class WhileStmt(val test: Node, val body: Node) : Node() +private data class ForStmt(val init: Node?, val test: Node?, val update: Node?, val body: Node) : Node() +private data class ForInStmt(val decl: String, val obj: Node, val body: Node) : Node() +private object BreakStmt : Node() +private object ContinueStmt : Node() +private data class TryCatch(val body: List, val catchParam: String?, val catchBody: List?, val finallyBody: List?) : Node() +private data class ThrowStmt(val expr: Node) : Node() + +private class Parser(private val lex: Lexer) { + + fun parseProgram(): List { + val stmts = mutableListOf() + while (lex.peek().type != TT.EOF) stmts.add(parseStatement()) + return stmts + } + + private fun parseStatement(): Node { + return when (lex.peek().type) { + TT.LBRACE -> parseBlock() + TT.SEMI -> { lex.consume(); BlockStmt(emptyList()) } + TT.IDENT -> when (lex.peek().raw) { + "var", "let", "const" -> parseVarDecl() + "function" -> parseFunctionDecl() + "return" -> parseReturn() + "if" -> parseIf() + "while" -> parseWhile() + "for" -> parseFor() + "break" -> { lex.consume(); lex.matchIf(TT.SEMI); BreakStmt } + "continue" -> { lex.consume(); lex.matchIf(TT.SEMI); ContinueStmt } + "try" -> parseTryCatch() + "throw" -> parseThrow() + "typeof" -> parseExprStmt() + else -> parseExprStmt() + } + else -> parseExprStmt() + } + } + + private fun parseBlock(): BlockStmt { + lex.expect(TT.LBRACE) + val stmts = mutableListOf() + while (lex.peek().type != TT.RBRACE && lex.peek().type != TT.EOF) stmts.add(parseStatement()) + lex.expect(TT.RBRACE) + return BlockStmt(stmts) + } + + private fun parseVarDecl(): VarDecl { + lex.consume() // var / let / const + val decls = mutableListOf>() + do { + val name = lex.expect(TT.IDENT).raw + val init = if (lex.matchIf(TT.EQ)) parseAssign() else null + decls.add(name to init) + } while (lex.matchIf(TT.COMMA)) + lex.matchIf(TT.SEMI) + return VarDecl(decls) + } + + private fun parseFunctionDecl(): Node { + lex.consume() // "function" + val name = if (lex.peek().type == TT.IDENT) lex.consume().raw else null + val params = parseParams() + val body = parseBlock().stmts + return if (name != null) VarDecl(listOf(name to FuncExpr(name, params, body))) else FuncExpr(name, params, body) + } + + private fun parseParams(): List { + lex.expect(TT.LPAREN) + val params = mutableListOf() + while (lex.peek().type != TT.RPAREN && lex.peek().type != TT.EOF) { + params.add(lex.expect(TT.IDENT).raw) + if (!lex.matchIf(TT.COMMA)) break + } + lex.expect(TT.RPAREN) + return params + } + + private fun parseReturn(): ReturnStmt { + lex.consume() + val expr = if (lex.peek().type == TT.SEMI || lex.peek().type == TT.RBRACE || lex.peek().type == TT.EOF) null else parseAssign() + lex.matchIf(TT.SEMI) + return ReturnStmt(expr) + } + + private fun parseIf(): IfStmt { + lex.consume() + lex.expect(TT.LPAREN) + val test = parseAssign() + lex.expect(TT.RPAREN) + val cons = parseStatement() + val alt = if (lex.peek().type == TT.IDENT && lex.peek().raw == "else") { lex.consume(); parseStatement() } else null + return IfStmt(test, cons, alt) + } + + private fun parseWhile(): WhileStmt { + lex.consume() + lex.expect(TT.LPAREN) + val test = parseAssign() + lex.expect(TT.RPAREN) + return WhileStmt(test, parseStatement()) + } + + private fun parseFor(): Node { + lex.consume() + lex.expect(TT.LPAREN) + // for..in check + if (lex.peek().type == TT.IDENT && (lex.peek().raw == "var" || lex.peek().raw == "let" || lex.peek().raw == "const")) { + val savedIdx = lex.peek() + lex.consume() + val varName = lex.expect(TT.IDENT).raw + if (lex.peek().type == TT.IDENT && lex.peek().raw == "in") { + lex.consume() + val obj = parseAssign() + lex.expect(TT.RPAREN) + return ForInStmt(varName, obj, parseStatement()) + } + // backtrack is hard so we just reconstruct as normal for + val init: Node? = if (lex.peek().type != TT.SEMI) { + val initExpr = if (lex.matchIf(TT.EQ)) parseAssign() else null + VarDecl(listOf(varName to initExpr)) + } else VarDecl(listOf(varName to null)) + lex.matchIf(TT.SEMI) + val test = if (lex.peek().type != TT.SEMI) parseAssign() else null + lex.matchIf(TT.SEMI) + val update = if (lex.peek().type != TT.RPAREN) parseAssign() else null + lex.expect(TT.RPAREN) + return ForStmt(init, test, update, parseStatement()) + } + val init = if (lex.peek().type != TT.SEMI) parseAssign() else null + lex.matchIf(TT.SEMI) + val test = if (lex.peek().type != TT.SEMI) parseAssign() else null + lex.matchIf(TT.SEMI) + val update = if (lex.peek().type != TT.RPAREN) parseAssign() else null + lex.expect(TT.RPAREN) + return ForStmt(init, test, update, parseStatement()) + } + + private fun parseTryCatch(): TryCatch { + lex.consume() // "try" + val body = parseBlock().stmts + var catchParam: String? = null + var catchBody: List? = null + if (lex.peek().type == TT.IDENT && lex.peek().raw == "catch") { + lex.consume() + if (lex.matchIf(TT.LPAREN)) { + catchParam = lex.expect(TT.IDENT).raw + lex.expect(TT.RPAREN) + } + catchBody = parseBlock().stmts + } + val finallyBody = if (lex.peek().type == TT.IDENT && lex.peek().raw == "finally") { + lex.consume(); parseBlock().stmts + } else null + return TryCatch(body, catchParam, catchBody, finallyBody) + } + + private fun parseThrow(): ThrowStmt { + lex.consume() + val expr = parseAssign() + lex.matchIf(TT.SEMI) + return ThrowStmt(expr) + } + + private fun parseExprStmt(): Node { + val expr = parseSeq() + lex.matchIf(TT.SEMI) + return ExprStmt(expr) + } + + private fun parseSeq(): Node { + val first = parseAssign() + if (lex.peek().type != TT.COMMA) return first + val exprs = mutableListOf(first) + while (lex.matchIf(TT.COMMA)) exprs.add(parseAssign()) + return SeqExpr(exprs) + } + + private fun parseAssign(): Node { + val left = parseTernary() + val op = when (lex.peek().type) { + TT.EQ -> "="; TT.PLUSEQ -> "+="; TT.MINUSEQ -> "-="; TT.STAREQ -> "*="; TT.SLASHEQ -> "/="; TT.PERCENTEQ -> "%=" + else -> return left + } + lex.consume() + return AssignExpr(op, left, parseAssign()) + } + + private fun parseTernary(): Node { + val test = parseOr() + if (!lex.matchIf(TT.QUESTION)) return test + val cons = parseAssign() + lex.expect(TT.COLON) + return CondExpr(test, cons, parseAssign()) + } + + private fun parseOr() = parseBin(::parseAnd, TT.OR to "||") + private fun parseAnd() = parseBin(::parseBitor, TT.AND to "&&") + private fun parseBitor() = parseBin(::parseBitxor, TT.PIPE to "|") + private fun parseBitxor() = parseBin(::parseBitand, TT.CARET to "^") + private fun parseBitand() = parseBin(::parseEq, TT.AMP to "&") + private fun parseEq() = parseBin(::parseRel, TT.EQEQ to "==", TT.EQEQEQ to "===", TT.NEQ to "!=", TT.NEQEQ to "!==") + private fun parseRel() = parseBin(::parseShift, TT.LT to "<", TT.LTEQ to "<=", TT.GT to ">", TT.GTEQ to ">=") + private fun parseShift() = parseBin(::parseAdd, TT.LSHIFT to "<<", TT.RSHIFT to ">>", TT.URSHIFT to ">>>") + private fun parseAdd() = parseBin(::parseMul, TT.PLUS to "+", TT.MINUS to "-") + private fun parseMul() = parseBin(::parseUnary, TT.STAR to "*", TT.SLASH to "/", TT.PERCENT to "%") + + private fun parseBin(next: () -> Node, vararg ops: Pair): Node { + var left = next() + while (true) { + val op = ops.firstOrNull { it.first == lex.peek().type } ?: break + lex.consume() + left = BinExpr(op.second, left, next()) + } + return left + } + + private fun parseUnary(): Node { + return when (lex.peek().type) { + TT.MINUS -> { lex.consume(); UnaryExpr("-", parseUnary(), true) } + TT.PLUS -> { lex.consume(); UnaryExpr("+", parseUnary(), true) } + TT.NOT -> { lex.consume(); UnaryExpr("!", parseUnary(), true) } + TT.TILDE -> { lex.consume(); UnaryExpr("~", parseUnary(), true) } + TT.PLUSPLUS -> { lex.consume(); UnaryExpr("++", parsePostfix(), true) } + TT.MINUSMINUS -> { lex.consume(); UnaryExpr("--", parsePostfix(), true) } + TT.IDENT -> when (lex.peek().raw) { + "typeof" -> { lex.consume(); TypeofExpr(parseUnary()) } + "void" -> { lex.consume(); parseUnary(); UndefinedLit } + "new" -> parseNew() + else -> parsePostfix() + } + else -> parsePostfix() + } + } + + private fun parseNew(): Node { + lex.consume() // "new" + val callee = parsePrimary() + val args = if (lex.peek().type == TT.LPAREN) parseArgs() else emptyList() + return NewExpr(callee, args) + } + + private fun parsePostfix(): Node { + var expr = parseCall() + while (true) { + expr = when (lex.peek().type) { + TT.PLUSPLUS -> { lex.consume(); UnaryExpr("++", expr, false) } + TT.MINUSMINUS -> { lex.consume(); UnaryExpr("--", expr, false) } + else -> break + } + } + return expr + } + + private fun parseCall(): Node { + var expr = parsePrimary() + while (true) { + expr = when (lex.peek().type) { + TT.LPAREN -> CallExpr(expr, parseArgs()) + TT.DOT -> { + lex.consume() + val prop = lex.expect(TT.IDENT).raw + MemberExpr(expr, StrLit(prop), false) + } + TT.LBRACKET -> { + lex.consume() + val prop = parseAssign() + lex.expect(TT.RBRACKET) + MemberExpr(expr, prop, true) + } + else -> break + } + } + return expr + } + + private fun parseArgs(): List { + lex.expect(TT.LPAREN) + val args = mutableListOf() + while (lex.peek().type != TT.RPAREN && lex.peek().type != TT.EOF) { + args.add(parseAssign()) + if (!lex.matchIf(TT.COMMA)) break + } + lex.expect(TT.RPAREN) + return args + } + + private fun parsePrimary(): Node { + val tok = lex.peek() + return when (tok.type) { + TT.NUMBER -> { + lex.consume() + NumLit( + tok.raw.toDoubleOrNull() + ?: if (tok.raw.startsWith("0x") || tok.raw.startsWith("0X")) + tok.raw.drop(2).toLong(16).toDouble() + else Double.NaN + ) + } + TT.STRING -> { lex.consume(); StrLit(tok.raw) } + TT.LPAREN -> { + lex.consume() + val expr = parseSeq() + lex.expect(TT.RPAREN) + expr + } + TT.LBRACKET -> { + lex.consume() + val elems = mutableListOf() + while (lex.peek().type != TT.RBRACKET && lex.peek().type != TT.EOF) { + elems.add(parseAssign()); lex.matchIf(TT.COMMA) + } + lex.expect(TT.RBRACKET) + ArrayLit(elems) + } + TT.LBRACE -> { + lex.consume() + val pairs = mutableListOf>() + while (lex.peek().type != TT.RBRACE && lex.peek().type != TT.EOF) { + val key = when (lex.peek().type) { + TT.IDENT, TT.STRING -> lex.consume().raw + TT.NUMBER -> lex.consume().raw + else -> lex.consume().raw + } + lex.expect(TT.COLON) + pairs.add(key to parseAssign()) + lex.matchIf(TT.COMMA) + } + lex.expect(TT.RBRACE) + ObjLit(pairs) + } + TT.IDENT -> { + val name = tok.raw + lex.consume() + when (name) { + "true" -> BoolLit(true) + "false" -> BoolLit(false) + "null" -> NullLit + "undefined" -> UndefinedLit + "function" -> { + val fname = if (lex.peek().type == TT.IDENT) lex.consume().raw else null + val params = parseParams() + FuncExpr(fname, params, parseBlock().stmts) + } + else -> Ident(name) + } + } + else -> { lex.consume(); UndefinedLit } + } + } +} + +private class ReturnSignal(val value: Any?) : Throwable() +private object BreakSignal : Throwable() +private object ContinueSignal : Throwable() +private class ThrowSignal(val value: Any?) : Throwable() + +private fun toNumber(v: Any?): Double = when (v) { + null, is Unit -> 0.0 + is Double -> v + is Boolean -> if (v) 1.0 else 0.0 + is String -> v.trim().toDoubleOrNull() ?: if (v.startsWith("0x") || v.startsWith("0X")) v.drop(2).toLongOrNull(16)?.toDouble() ?: Double.NaN else Double.NaN + is JsList -> Double.NaN + else -> Double.NaN +} + +private fun toBoolean(v: Any?): Boolean = when (v) { + null, is Unit -> false + is Boolean -> v + is Double -> v != 0.0 && !v.isNaN() + is String -> v.isNotEmpty() + is JsList -> true + is JsObject -> true + else -> true +} + +private fun toJsString(v: Any?): String = when (v) { + null -> "null" + is Unit -> "undefined" + is Boolean -> v.toString() + is Double -> if (v == floor(v) && !v.isInfinite()) v.toLong().toString() else v.toString() + is String -> v + is JsList -> v.elements.joinToString(",") { toJsString(it) } + is JsObject -> "[object Object]" + else -> v.toString() +} + +private fun strictEq(a: Any?, b: Any?): Boolean = when { + a is Double && b is Double -> a == b + a is String && b is String -> a == b + a is Boolean && b is Boolean -> a == b + a == null && b == null -> true + a is Unit && b is Unit -> true + else -> a === b +} + +private fun looseEq(a: Any?, b: Any?): Boolean { + if (strictEq(a, b)) return true + // number coercion + val an = a is Double || a is Boolean + val bn = b is Double || b is Boolean + if (an || bn) return toNumber(a) == toNumber(b) + if (a is String || b is String) return toJsString(a) == toJsString(b) + return false +} + +private class JsList(val elements: MutableList = mutableListOf()) { + var length: Int + get() = elements.size + set(v) { while (elements.size < v) elements.add(Unit); while (elements.size > v) elements.removeAt(elements.size - 1) } + + operator fun get(idx: Int): Any? = elements.getOrElse(idx) { Unit } + operator fun set(idx: Int, v: Any?) { while (elements.size <= idx) elements.add(Unit); elements[idx] = v } +} + +private class JsObject(val props: MutableMap = mutableMapOf()) + +private class JsFunction( + val name: String?, + val params: List, + val body: List, + val closure: Scope +) + +private class Scope(val parent: Scope? = null) { + val vars = mutableMapOf() + + fun get(name: String): Any? { + if (vars.containsKey(name)) return vars[name] + return parent?.get(name) ?: Unit + } + + fun set(name: String, value: Any?) { + val scope = findOwner(name) + if (scope != null) scope.vars[name] = value else vars[name] = value + } + + fun define(name: String, value: Any?) { vars[name] = value } + + private fun findOwner(name: String): Scope? = + if (vars.containsKey(name)) this else parent?.findOwner(name) +} + +class JsInterpreter { + private val globalScope = Scope() + + init { installGlobals() } + + private fun installGlobals() { + val mathObj = JsObject(mutableMapOf( + "PI" to Math.PI, "E" to Math.E, + "floor" to nativeFn { args -> floor(toNumber(args.getOrNull(0))) }, + "ceil" to nativeFn { args -> ceil(toNumber(args.getOrNull(0))) }, + "round" to nativeFn { args -> round(toNumber(args.getOrNull(0))) }, + "abs" to nativeFn { args -> abs(toNumber(args.getOrNull(0))) }, + "sqrt" to nativeFn { args -> sqrt(toNumber(args.getOrNull(0))) }, + "pow" to nativeFn { args -> toNumber(args.getOrNull(0)).pow(toNumber(args.getOrNull(1))) }, + "log" to nativeFn { args -> ln(toNumber(args.getOrNull(0))) }, + "sin" to nativeFn { args -> sin(toNumber(args.getOrNull(0))) }, + "cos" to nativeFn { args -> cos(toNumber(args.getOrNull(0))) }, + "tan" to nativeFn { args -> tan(toNumber(args.getOrNull(0))) }, + "max" to nativeFn { args -> args.maxOfOrNull { toNumber(it) } ?: Double.NEGATIVE_INFINITY }, + "min" to nativeFn { args -> args.minOfOrNull { toNumber(it) } ?: Double.POSITIVE_INFINITY }, + "random" to nativeFn { _ -> Math.random() }, + "trunc" to nativeFn { args -> truncate(toNumber(args.getOrNull(0))) }, + "log2" to nativeFn { args -> log2(toNumber(args.getOrNull(0))) }, + "log10" to nativeFn { args -> log10(toNumber(args.getOrNull(0))) }, + )) + globalScope.define("Math", mathObj) + + // for String.fromCharCode + val stringObj = JsObject(mutableMapOf( + "fromCharCode" to nativeFn { args -> args.joinToString("") { toNumber(it).toInt().toChar().toString() } } + )) + globalScope.define("String", stringObj) + + globalScope.define("parseInt", nativeFn { args -> + val s = toJsString(args.getOrNull(0)).trim() + val radix = args.getOrNull(1)?.let { toNumber(it).toInt() }?.takeIf { it in 2..36 } ?: 10 + try { s.toLong(radix).toDouble() } catch (_: Exception) { Double.NaN } + }) + globalScope.define("parseFloat", nativeFn { args -> toNumber(args.getOrNull(0)) }) + globalScope.define("isNaN", nativeFn { args -> toNumber(args.getOrNull(0)).isNaN() }) + globalScope.define("isFinite", nativeFn { args -> toNumber(args.getOrNull(0)).isFinite() }) + globalScope.define("decodeURIComponent", nativeFn { args -> java.net.URLDecoder.decode(toJsString(args.getOrNull(0)), "UTF-8") }) + globalScope.define("encodeURIComponent", nativeFn { args -> java.net.URLEncoder.encode(toJsString(args.getOrNull(0)), "UTF-8") }) + globalScope.define("escape", nativeFn { args -> java.net.URLEncoder.encode(toJsString(args.getOrNull(0)), "UTF-8") }) + globalScope.define("unescape", nativeFn { args -> java.net.URLDecoder.decode(toJsString(args.getOrNull(0)), "UTF-8") }) + globalScope.define("eval", nativeFn { args -> eval(toJsString(args.getOrNull(0))) }) + globalScope.define("undefined", Unit) + globalScope.define("NaN", Double.NaN) + globalScope.define("Infinity", Double.POSITIVE_INFINITY) + + globalScope.define("Array", nativeFn { args -> + if (args.size == 1 && args[0] is Double) JsList(MutableList((args[0] as Double).toInt()) { Unit }) + else JsList(args.toMutableList()) + }) + + globalScope.define("Object", JsObject(mutableMapOf( + "keys" to nativeFn { args -> + when (val o = args.getOrNull(0)) { + is JsObject -> JsList(o.props.keys.map { it as Any? }.toMutableList()) + else -> JsList() + } + }, + "values" to nativeFn { args -> + when (val o = args.getOrNull(0)) { + is JsObject -> JsList(o.props.values.toMutableList()) + else -> JsList() + } + } + ))) + + // console.log (no-op for silence, but avoids errors) + val consoleObj = JsObject(mutableMapOf( + "log" to nativeFn { _ -> Unit }, + "error" to nativeFn { _ -> Unit }, + "warn" to nativeFn { _ -> Unit }, + )) + globalScope.define("console", consoleObj) + } + + private fun nativeFn(fn: (List) -> Any?): Any? = NativeFn(fn) + + fun eval(code: String): Any? { + return try { + val lexer = Lexer(code) + val parser = Parser(lexer) + val stmts = parser.parseProgram() + var last: Any? = Unit + for (stmt in stmts) last = execNode(stmt, globalScope) + last + } catch (r: ReturnSignal) { + r.value + } catch (e: Exception) { + logError(e) + Unit + } + } + + fun getVar(name: String): Any? = globalScope.get(name).let { if (it is Unit) null else it } + fun setVar(name: String, value: Any?) = globalScope.define(name, value) + + private fun execNode(node: Node, scope: Scope): Any? = when (node) { + is VarDecl -> { + for ((name, init) in node.decls) scope.define(name, init?.let { evalExpr(it, scope) }) + Unit + } + is ExprStmt -> evalExpr(node.expr, scope) + is BlockStmt -> { + val inner = Scope(scope) + var last: Any? = Unit + for (s in node.stmts) last = execNode(s, inner) + last + } + is ReturnStmt -> throw ReturnSignal(node.expr?.let { evalExpr(it, scope) }) + is IfStmt -> { + if (toBoolean(evalExpr(node.test, scope))) execNode(node.cons, scope) + else node.alt?.let { execNode(it, scope) } + } + is WhileStmt -> { + try { + while (toBoolean(evalExpr(node.test, scope))) { + try { execNode(node.body, scope) } catch (_: ContinueSignal) {} + } + } catch (_: BreakSignal) {} + Unit + } + is ForStmt -> { + val inner = Scope(scope) + node.init?.let { execNode(it, inner) } + try { + while (node.test == null || toBoolean(evalExpr(node.test, inner))) { + try { execNode(node.body, inner) } catch (_: ContinueSignal) {} + node.update?.let { evalExpr(it, inner) } + } + } catch (_: BreakSignal) {} + Unit + } + is ForInStmt -> { + val obj = evalExpr(node.obj, scope) + val inner = Scope(scope) + try { + when (obj) { + is JsObject -> for (key in obj.props.keys) { + inner.define(node.decl, key) + try { execNode(node.body, inner) } catch (_: ContinueSignal) {} + } + is JsList -> for (i in obj.elements.indices) { + inner.define(node.decl, i.toDouble()) + try { execNode(node.body, inner) } catch (_: ContinueSignal) {} + } + else -> {} + } + } catch (_: BreakSignal) {} + Unit + } + is TryCatch -> { + try { + for (s in node.body) execNode(s, scope) + } catch (e: ThrowSignal) { + if (node.catchBody != null) { + val inner = Scope(scope) + if (node.catchParam != null) inner.define(node.catchParam, e.value) + for (s in node.catchBody) execNode(s, inner) + } + } catch (_: Exception) { + // swallow other exceptions inside try (e.g. runtime errors) + } finally { + node.finallyBody?.forEach { execNode(it, scope) } + } + Unit + } + is ThrowStmt -> throw ThrowSignal(evalExpr(node.expr, scope)) + is BreakStmt -> throw BreakSignal + is ContinueStmt -> throw ContinueSignal + else -> evalExpr(node, scope) + } + + private fun evalExpr(node: Node, scope: Scope): Any? = when (node) { + is NumLit -> node.v + is StrLit -> node.v + is BoolLit -> node.v + NullLit -> null + UndefinedLit -> Unit + is Ident -> scope.get(node.name) + is ArrayLit -> JsList(node.elems.map { evalExpr(it, scope) }.toMutableList()) + is ObjLit -> JsObject(node.pairs.associate { (k, v) -> k to evalExpr(v, scope) }.toMutableMap()) + is FuncExpr -> { + val fn = JsFunction(node.name, node.params, node.body, scope) + if (node.name != null) scope.define(node.name, fn) + fn + } + is SeqExpr -> node.exprs.fold(Unit as Any?) { _, e -> evalExpr(e, scope) } + is TypeofExpr -> { + val v = try { evalExpr(node.expr, scope) } catch (_: Exception) { Unit } + when (v) { + is Unit -> "undefined" + null -> "object" + is Double -> "number" + is Boolean -> "boolean" + is String -> "string" + is JsFunction, is NativeFn -> "function" + else -> "object" + } + } + is UnaryExpr -> evalUnary(node, scope) + is BinExpr -> evalBinary(node, scope) + is AssignExpr -> evalAssign(node, scope) + is CondExpr -> if (toBoolean(evalExpr(node.test, scope))) evalExpr(node.cons, scope) else evalExpr(node.alt, scope) + is MemberExpr -> evalMember(node, scope) + is CallExpr -> evalCall(node, scope) + is NewExpr -> evalNew(node, scope) + else -> Unit + } + + private fun evalUnary(node: UnaryExpr, scope: Scope): Any? { + if (node.op == "++" || node.op == "--") { + val delta = if (node.op == "++") 1.0 else -1.0 + val old = toNumber(evalExpr(node.expr, scope)) + val newVal = old + delta + assignTo(node.expr, newVal, scope) + return if (node.prefix) newVal else old + } + val v = evalExpr(node.expr, scope) + return when (node.op) { + "-" -> -toNumber(v) + "+" -> toNumber(v) + "!" -> !toBoolean(v) + "~" -> toNumber(v).toLong().inv().toDouble() + else -> Unit + } + } + + private fun evalBinary(node: BinExpr, scope: Scope): Any? { + // Short-circuit + if (node.op == "&&") { + val l = evalExpr(node.left, scope) + return if (!toBoolean(l)) l else evalExpr(node.right, scope) + } + if (node.op == "||") { + val l = evalExpr(node.left, scope) + return if (toBoolean(l)) l else evalExpr(node.right, scope) + } + val l = evalExpr(node.left, scope) + val r = evalExpr(node.right, scope) + return when (node.op) { + "+" -> { + if (l is String || r is String) toJsString(l) + toJsString(r) + else if (l is JsList || r is JsList) toJsString(l) + toJsString(r) + else toNumber(l) + toNumber(r) + } + "-" -> toNumber(l) - toNumber(r) + "*" -> toNumber(l) * toNumber(r) + "/" -> toNumber(l) / toNumber(r) + "%" -> toNumber(l) % toNumber(r) + "<" -> toNumber(l) < toNumber(r) + "<=" -> toNumber(l) <= toNumber(r) + ">" -> toNumber(l) > toNumber(r) + ">=" -> toNumber(l) >= toNumber(r) + "==" -> looseEq(l, r) + "!=" -> !looseEq(l, r) + "===" -> strictEq(l, r) + "!==" -> !strictEq(l, r) + "&" -> (toNumber(l).toLong() and toNumber(r).toLong()).toDouble() + "|" -> (toNumber(l).toLong() or toNumber(r).toLong()).toDouble() + "^" -> (toNumber(l).toLong() xor toNumber(r).toLong()).toDouble() + "<<" -> (toNumber(l).toLong() shl toNumber(r).toInt()).toDouble() + ">>" -> (toNumber(l).toLong() shr toNumber(r).toInt()).toDouble() + ">>>" -> (toNumber(l).toLong() ushr toNumber(r).toInt()).toDouble() + "instanceof" -> false + "in" -> when (r) { + is JsObject -> toJsString(l) in r.props + is JsList -> toNumber(l).toInt().let { it >= 0 && it < r.elements.size } + else -> false + } + else -> Unit + } + } + + private fun evalAssign(node: AssignExpr, scope: Scope): Any? { + val right = evalExpr(node.right, scope) + val value = if (node.op == "=") right else { + val left = evalExpr(node.left, scope) + when (node.op) { + "+=" -> if (left is String || right is String) toJsString(left) + toJsString(right) else toNumber(left) + toNumber(right) + "-=" -> toNumber(left) - toNumber(right) + "*=" -> toNumber(left) * toNumber(right) + "/=" -> toNumber(left) / toNumber(right) + "%=" -> toNumber(left) % toNumber(right) + else -> right + } + } + assignTo(node.left, value, scope) + return value + } + + private fun assignTo(target: Node, value: Any?, scope: Scope) { + when (target) { + is Ident -> scope.set(target.name, value) + is MemberExpr -> { + val obj = evalExpr(target.obj, scope) + val key = if (target.computed) toJsString(evalExpr(target.prop, scope)) else (target.prop as StrLit).v + when (obj) { + is JsObject -> obj.props[key] = value + is JsList -> { + val idx = key.toIntOrNull() + if (idx != null) obj[idx] = value + else if (key == "length") obj.length = toNumber(value).toInt() + } + else -> {} + } + } + else -> {} + } + } + + private fun evalMember(node: MemberExpr, scope: Scope): Any? { + val obj = evalExpr(node.obj, scope) + val key = if (node.computed) toJsString(evalExpr(node.prop, scope)) else (node.prop as StrLit).v + return getMember(obj, key) + } + + private fun getMember(obj: Any?, key: String): Any? = when (obj) { + is JsObject -> obj.props[key] ?: Unit + is JsList -> when (key) { + "length" -> obj.length.toDouble() + "join" -> nativeFn { args -> obj.elements.joinToString(args.getOrNull(0)?.let { toJsString(it) } ?: ",") { toJsString(it) } } + "reverse" -> nativeFn { _ -> obj.elements.reverse(); obj } + "push" -> nativeFn { args -> args.forEach { obj.elements.add(it) }; obj.elements.size.toDouble() } + "pop" -> nativeFn { _ -> if (obj.elements.isEmpty()) Unit else obj.elements.removeAt(obj.elements.size - 1) } + "shift" -> nativeFn { _ -> if (obj.elements.isEmpty()) Unit else obj.elements.removeAt(0) } + "unshift" -> nativeFn { args -> args.reversed().forEach { obj.elements.add(0, it) }; obj.elements.size.toDouble() } + "slice" -> nativeFn { args -> + val start = args.getOrNull(0)?.let { toNumber(it).toInt() } ?: 0 + val end = args.getOrNull(1)?.let { toNumber(it).toInt() } ?: obj.elements.size + val s = if (start < 0) maxOf(0, obj.elements.size + start) else minOf(start, obj.elements.size) + val e = if (end < 0) maxOf(0, obj.elements.size + end) else minOf(end, obj.elements.size) + JsList(obj.elements.subList(maxOf(0, s), maxOf(s, e)).toMutableList()) + } + "splice" -> nativeFn { args -> + val start = args.getOrNull(0)?.let { toNumber(it).toInt() }?.let { if (it < 0) maxOf(0, obj.elements.size + it) else minOf(it, obj.elements.size) } ?: 0 + val deleteCount = args.getOrNull(1)?.let { toNumber(it).toInt() }?.coerceIn(0, obj.elements.size - start) ?: (obj.elements.size - start) + val removed = JsList(obj.elements.subList(start, start + deleteCount).toMutableList()) + repeat(deleteCount) { obj.elements.removeAt(start) } + args.drop(2).forEachIndexed { i, v -> obj.elements.add(start + i, v) } + removed + } + "indexOf" -> nativeFn { args -> + val v = args.getOrNull(0); val start = args.getOrNull(1)?.let { toNumber(it).toInt() } ?: 0 + obj.elements.indexOfFirst { strictEq(it, v) }.let { if (it < start) -1.0 else it.toDouble() } + } + "map" -> nativeFn { args -> + val fn = args.getOrNull(0) + JsList(obj.elements.mapIndexed { i, v -> callAny(fn, listOf(v, i.toDouble(), obj), null) }.toMutableList()) + } + "filter" -> nativeFn { args -> + val fn = args.getOrNull(0) + JsList(obj.elements.filterIndexed { i, v -> toBoolean(callAny(fn, listOf(v, i.toDouble(), obj), null)) }.toMutableList()) + } + "forEach" -> nativeFn { args -> + val fn = args.getOrNull(0) + obj.elements.forEachIndexed { i, v -> callAny(fn, listOf(v, i.toDouble(), obj), null) } + Unit + } + "reduce" -> nativeFn { args -> + val fn = args.getOrNull(0) + var acc: Any? = if (args.size > 1) args[1] else obj.elements.firstOrNull() ?: Unit + val startIdx = if (args.size > 1) 0 else 1 + for (i in startIdx until obj.elements.size) acc = callAny(fn, listOf(acc, obj.elements[i], i.toDouble(), obj), null) + acc + } + "concat" -> nativeFn { args -> + val result = JsList(obj.elements.toMutableList()) + args.forEach { a -> when (a) { is JsList -> result.elements.addAll(a.elements); else -> result.elements.add(a) } } + result + } + "find" -> nativeFn { args -> + val fn = args.getOrNull(0) + obj.elements.firstOrNull { toBoolean(callAny(fn, listOf(it), null)) } ?: Unit + } + "some" -> nativeFn { args -> + val fn = args.getOrNull(0) + obj.elements.any { toBoolean(callAny(fn, listOf(it), null)) } + } + "every" -> nativeFn { args -> + val fn = args.getOrNull(0) + obj.elements.all { toBoolean(callAny(fn, listOf(it), null)) } + } + "sort" -> nativeFn { args -> + val fn = args.getOrNull(0) + if (fn == null) obj.elements.sortWith { a, b -> toJsString(a).compareTo(toJsString(b)) } + else obj.elements.sortWith { a, b -> toNumber(callAny(fn, listOf(a, b), null)).toInt() } + obj + } + "includes" -> nativeFn { args -> obj.elements.any { looseEq(it, args.getOrNull(0)) } } + "toString" -> nativeFn { _ -> obj.elements.joinToString(",") { toJsString(it) } } + "flat" -> nativeFn { _ -> + val result = JsList() + obj.elements.forEach { if (it is JsList) result.elements.addAll(it.elements) else result.elements.add(it) } + result + } + else -> key.toIntOrNull()?.let { obj[it] } ?: Unit + } + is String -> when (key) { + "length" -> obj.length.toDouble() + "split" -> nativeFn { args -> + val sep = args.getOrNull(0) + when { + sep == null || sep is Unit -> JsList(mutableListOf(obj)) + sep is String && sep.isEmpty() -> JsList(obj.map { it.toString() as Any? }.toMutableList()) + sep is String -> JsList(obj.split(sep).map { it as Any? }.toMutableList()) + else -> JsList(obj.split(toJsString(sep)).map { it as Any? }.toMutableList()) + } + } + "join" -> nativeFn { args -> obj } // strings don't have join but just in case + "replace" -> nativeFn { args -> + val from = args.getOrNull(0); val to = toJsString(args.getOrNull(1)) + when (from) { + is String -> obj.replaceFirst(from, to) + else -> obj.replace(toJsString(from), to) + } + } + "replaceAll" -> nativeFn { args -> + val from = args.getOrNull(0); val to = toJsString(args.getOrNull(1)) + obj.replace(toJsString(from), to) + } + "indexOf" -> nativeFn { args -> obj.indexOf(toJsString(args.getOrNull(0))).toDouble() } + "lastIndexOf" -> nativeFn { args -> obj.lastIndexOf(toJsString(args.getOrNull(0))).toDouble() } + "includes" -> nativeFn { args -> obj.contains(toJsString(args.getOrNull(0))) } + "startsWith" -> nativeFn { args -> obj.startsWith(toJsString(args.getOrNull(0))) } + "endsWith" -> nativeFn { args -> obj.endsWith(toJsString(args.getOrNull(0))) } + "slice" -> nativeFn { args -> + val start = args.getOrNull(0)?.let { toNumber(it).toInt() }?.let { if (it < 0) maxOf(0, obj.length + it) else minOf(it, obj.length) } ?: 0 + val end = args.getOrNull(1)?.let { toNumber(it).toInt() }?.let { if (it < 0) maxOf(0, obj.length + it) else minOf(it, obj.length) } ?: obj.length + if (end <= start) "" else obj.substring(start, end) + } + "substr" -> nativeFn { args -> + val start = args.getOrNull(0)?.let { toNumber(it).toInt() }?.let { if (it < 0) maxOf(0, obj.length + it) else minOf(it, obj.length) } ?: 0 + val len = args.getOrNull(1)?.let { toNumber(it).toInt() } ?: (obj.length - start) + if (len <= 0) "" else obj.substring(start, minOf(start + len, obj.length)) + } + "substring" -> nativeFn { args -> + val a = args.getOrNull(0)?.let { toNumber(it).toInt().coerceIn(0, obj.length) } ?: 0 + val b = args.getOrNull(1)?.let { toNumber(it).toInt().coerceIn(0, obj.length) } ?: obj.length + obj.substring(minOf(a, b), maxOf(a, b)) + } + "charAt" -> nativeFn { args -> + val i = args.getOrNull(0)?.let { toNumber(it).toInt() } ?: 0 + if (i < 0 || i >= obj.length) "" else obj[i].toString() + } + "charCodeAt" -> nativeFn { args -> + val i = args.getOrNull(0)?.let { toNumber(it).toInt() } ?: 0 + if (i < 0 || i >= obj.length) Double.NaN else obj[i].code.toDouble() + } + "codePointAt" -> nativeFn { args -> + val i = args.getOrNull(0)?.let { toNumber(it).toInt() } ?: 0 + if (i < 0 || i >= obj.length) Double.NaN else obj[i].code.toDouble() + } + "toUpperCase", "toLocaleUpperCase" -> nativeFn { _ -> obj.uppercase() } + "toLowerCase", "toLocaleLowerCase" -> nativeFn { _ -> obj.lowercase() } + "trim" -> nativeFn { _ -> obj.trim() } + "trimStart", "trimLeft" -> nativeFn { _ -> obj.trimStart() } + "trimEnd", "trimRight" -> nativeFn { _ -> obj.trimEnd() } + "repeat" -> nativeFn { args -> obj.repeat(toNumber(args.getOrNull(0)).toInt().coerceAtLeast(0)) } + "padStart" -> nativeFn { args -> + val len = toNumber(args.getOrNull(0)).toInt(); val pad = args.getOrNull(1)?.let { toJsString(it) } ?: " " + if (obj.length >= len) obj else (pad.repeat(len) + obj).takeLast(len) + } + "padEnd" -> nativeFn { args -> + val len = toNumber(args.getOrNull(0)).toInt(); val pad = args.getOrNull(1)?.let { toJsString(it) } ?: " " + if (obj.length >= len) obj else (obj + pad.repeat(len)).take(len) + } + "toString" -> nativeFn { _ -> obj } + "valueOf" -> nativeFn { _ -> obj } + "match" -> nativeFn { args -> + val pattern = toJsString(args.getOrNull(0)) + try { + val result = Regex(pattern).find(obj) + if (result == null) null + else JsList(result.groupValues.map { it as Any? }.toMutableList()) + } catch (_: Exception) { null } + } + else -> key.toIntOrNull()?.let { + if (it >= 0 && it < obj.length) obj[it].toString() else Unit + } ?: Unit + } + is Double -> when (key) { + "toString" -> nativeFn { args -> + val radix = args.getOrNull(0)?.let { toNumber(it).toInt() } ?: 10 + if (radix == 10) toJsString(obj) else obj.toLong().toString(radix) + } + "toFixed" -> nativeFn { args -> + val digits = args.getOrNull(0)?.let { toNumber(it).toInt() } ?: 0 + "%.${digits}f".format(obj) + } + else -> Unit + } + else -> Unit + } + + private fun evalCall(node: CallExpr, scope: Scope): Any? { + val (callee, thisVal) = resolveCallee(node.callee, scope) + val args = node.args.map { evalExpr(it, scope) } + return callAny(callee, args, thisVal) + } + + private fun resolveCallee(calleeNode: Node, scope: Scope): Pair { + return if (calleeNode is MemberExpr) { + val obj = evalExpr(calleeNode.obj, scope) + val key = if (calleeNode.computed) toJsString(evalExpr(calleeNode.prop, scope)) else (calleeNode.prop as StrLit).v + getMember(obj, key) to obj + } else { + evalExpr(calleeNode, scope) to null + } + } + + private fun evalNew(node: NewExpr, scope: Scope): Any? { + val callee = evalExpr(node.callee, scope) + val args = node.args.map { evalExpr(it, scope) } + // Just call it + return callAny(callee, args, JsObject()) + } + + private fun callAny(callee: Any?, args: List, thisVal: Any?): Any? = when (callee) { + is NativeFn -> callee.fn(args) + is JsFunction -> { + val fnScope = Scope(callee.closure) + fnScope.define("this", thisVal) + fnScope.define("arguments", JsList(args.toMutableList())) + callee.params.forEachIndexed { i, p -> fnScope.define(p, args.getOrElse(i) { Unit }) } + try { + var last: Any? = Unit + for (s in callee.body) last = execNode(s, fnScope) + last + } catch (r: ReturnSignal) { + r.value + } + } + else -> Unit + } +} + +// Wrapper so we can store Kotlin lambdas as "callable" values +private class NativeFn(val fn: (List) -> Any?) diff --git a/library/src/commonTest/kotlin/com/lagradost/cloudstream3/utils/JsInterpreterTest.kt b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/utils/JsInterpreterTest.kt new file mode 100644 index 00000000000..f4ff6c274a7 --- /dev/null +++ b/library/src/commonTest/kotlin/com/lagradost/cloudstream3/utils/JsInterpreterTest.kt @@ -0,0 +1,846 @@ +package com.lagradost.cloudstream3.utils + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.assertFalse + +class JsInterpreterTest { + + private fun num(code: String) = (evalJs(code) as? Double) ?: Double.NaN + private fun str(code: String) = jsValueToString(evalJs(code)) + private fun bool(code: String) = evalJs(code) as? Boolean ?: false + + private fun assertApprox(expected: Double, actual: Double, tol: Double = 1e-9) { + assertTrue(kotlin.math.abs(actual - expected) <= tol, "Expected $expected ± $tol but was $actual") + } + + @Test + fun integerLiteral() { + assertEquals(42.0, num("42")) + } + + @Test + fun negativeLiteral() { + assertEquals(-7.0, num("-7")) + } + + @Test + fun floatLiteral() { + assertApprox(3.14, num("3.14")) + } + + @Test + fun hexLiteral() { + assertEquals(255.0, num("0xff")) + } + + @Test + fun stringLiteralDouble() { + assertEquals("hi", str("\"hi\"")) + } + + @Test + fun stringLiteralSingle() { + assertEquals("hi", str("'hi'")) + } + + @Test + fun templateLiteral() { + assertEquals("hi", str("`hi`")) + } + + @Test + fun booleanTrue() { + assertTrue(bool("true")) + } + + @Test + fun booleanFalse() { + assertFalse(bool("false")) + } + + @Test + fun nullLiteral() { + assertNull(evalJs("null")) + } + + @Test + fun undefinedLiteral() { + assertEquals(Unit, evalJs("undefined")) + } + + @Test + fun addition() { + assertEquals(5.0, num("2+3")) + } + + @Test + fun subtraction() { + assertEquals(1.0, num("3-2")) + } + + @Test + fun multiplication() { + assertEquals(6.0, num("2*3")) + } + + @Test + fun division() { + assertEquals(2.5, num("5/2")) + } + + @Test + fun modulo() { + assertEquals(1.0, num("7%3")) + } + + @Test + fun operatorPrecedence() { + assertEquals(7.0, num("1+2*3")) + } + + @Test + fun parenthesesOverride() { + assertEquals(9.0, num("(1+2)*3")) + } + + @Test + fun unaryMinus() { + assertEquals(-5.0, num("-(2+3)")) + } + + @Test + fun unaryPlusCoerces() { + assertEquals(3.0, num("+'3'")) + } + + @Test + fun bitwiseAnd() { + assertEquals(2.0, num("6&3")) + } + + @Test + fun bitwiseOr() { + assertEquals(7.0, num("5|3")) + } + + @Test + fun bitwiseXor() { + assertEquals(6.0, num("5^3")) + } + + @Test + fun bitwiseNot() { + assertEquals(-6.0, num("~5")) + } + + @Test + fun leftShift() { + assertEquals(20.0, num("5<<2")) + } + + @Test + fun rightShift() { + assertEquals(1.0, num("5>>2")) + } + + @Test + fun unsignedRightShift() { + assertEquals(1.0, num("5>>>2")) + } + + @Test + fun lessThanTrue() { + assertTrue(bool("1<2")) + } + + @Test + fun lessThanFalse() { + assertFalse(bool("2<1")) + } + + @Test + fun looseEqNumberString() { + assertTrue(bool("1=='1'")) + } + + @Test + fun strictEqSameType() { + assertTrue(bool("1===1")) + } + + @Test + fun strictEqDifferentType() { + assertFalse(bool("1==='1'")) + } + + @Test + fun strictNeq() { + assertTrue(bool("1!=='1'")) + } + + @Test + fun logicalAndRightWhenLeftTruthy() { + assertEquals(2.0, num("1&&2")) + } + + @Test + fun logicalAndLeftWhenLeftFalsy() { + assertEquals(0.0, num("0&&2")) + } + + @Test + fun logicalOrLeftWhenTruthy() { + assertEquals(1.0, num("1||2")) + } + + @Test + fun logicalOrRightWhenLeftFalsy() { + assertEquals(2.0, num("0||2")) + } + + @Test + fun logicalNot() { + assertTrue(bool("!false")) + } + + @Test + fun ternaryTrueBranch() { + assertEquals(1.0, num("true?1:2")) + } + + @Test + fun ternaryFalseBranch() { + assertEquals(2.0, num("false?1:2")) + } + + @Test + fun varDeclarationAndRead() { + assertEquals(10.0, num("var x=10; x")) + } + + @Test + fun letDeclaration() { + assertEquals("hello", str("let s='hello'; s")) + } + + @Test + fun constDeclaration() { + assertApprox(3.14, num("const PI=3.14; PI")) + } + + @Test + fun assignmentPlusEquals() { + assertEquals(15.0, num("var x=10; x+=5; x")) + } + + @Test + fun assignmentMinusEquals() { + assertEquals(5.0, num("var x=10; x-=5; x")) + } + + @Test + fun assignmentTimesEquals() { + assertEquals(20.0, num("var x=4; x*=5; x")) + } + + @Test + fun prefixIncrement() { + assertEquals(6.0, num("var x=5; ++x")) + } + + @Test + fun postfixIncrementReturnsOldValue() { + assertEquals(5.0, num("var x=5; x++")) + } + + @Test + fun postfixIncrementMutatesVariable() { + assertEquals(6.0, num("var x=5; x++; x")) + } + + @Test + fun typeofNumber() { + assertEquals("number", str("typeof 42")) + } + + @Test + fun typeofString() { + assertEquals("string", str("typeof 'x'")) + } + + @Test + fun typeofBoolean() { + assertEquals("boolean", str("typeof true")) + } + + @Test + fun typeofUndefined() { + assertEquals("undefined", str("typeof undefined")) + } + + @Test + fun typeofFunction() { + assertEquals("function", str("typeof function(){}")) + } + + @Test + fun stringConcatenation() { + assertEquals("ab", str("'a'+'b'")) + } + + @Test + fun numberPlusStringCoercesToString() { + assertEquals("1x", str("1+'x'")) + } + + @Test + fun stringLength() { + assertEquals(5.0, num("'hello'.length")) + } + + @Test + fun stringCharAt() { + assertEquals("e", str("'hello'.charAt(1)")) + } + + @Test + fun stringCharCodeAt() { + assertEquals(104.0, num("'hello'.charCodeAt(0)")) + } + + @Test + fun stringIndexOfFound() { + assertEquals(1.0, num("'hello'.indexOf('e')")) + } + + @Test + fun stringIndexOfNotFound() { + assertEquals(-1.0, num("'hello'.indexOf('z')")) + } + + @Test + fun stringSlice() { + assertEquals("ell", str("'hello'.slice(1,4)")) + } + + @Test + fun stringSliceNegativeIndex() { + assertEquals("lo", str("'hello'.slice(-2)")) + } + + @Test + fun stringSubstr() { + assertEquals("ell", str("'hello'.substr(1,3)")) + } + + @Test + fun stringSubstring() { + assertEquals("ell", str("'hello'.substring(1,4)")) + } + + @Test + fun stringSplitAndJoin() { + assertEquals("a-b-c", str("'a|b|c'.split('|').join('-')")) + } + + @Test + fun stringSplitEmptySepGivesChars() { + assertEquals("h,e,l,l,o", str("'hello'.split('').join(',')")) + } + + @Test + fun stringReplaceFirstOccurrence() { + assertEquals("xbc", str("'abc'.replace('a','x')")) + } + + @Test + fun stringToUpperCase() { + assertEquals("HELLO", str("'hello'.toUpperCase()")) + } + + @Test + fun stringToLowerCase() { + assertEquals("hello", str("'HELLO'.toLowerCase()")) + } + + @Test + fun stringTrim() { + assertEquals("hi", str("' hi '.trim()")) + } + + @Test + fun stringRepeat() { + assertEquals("aaa", str("'a'.repeat(3)")) + } + + @Test + fun stringPadStart() { + assertEquals("005", str("'5'.padStart(3,'0')")) + } + + @Test + fun stringPadEnd() { + assertEquals("500", str("'5'.padEnd(3,'0')")) + } + + @Test + fun stringIncludes() { + assertTrue(bool("'hello'.includes('ell')")) + } + + @Test + fun stringStartsWith() { + assertTrue(bool("'hello'.startsWith('hel')")) + } + + @Test + fun stringEndsWith() { + assertTrue(bool("'hello'.endsWith('llo')")) + } + + @Test + fun stringFromCharCode() { + assertEquals("A", str("String.fromCharCode(65)")) + } + + @Test + fun stringFromCharCodeMultiple() { + assertEquals("Hi", str("String.fromCharCode(72,105)")) + } + + @Test + fun numberToStringRadix16() { + assertEquals("ff", str("(255).toString(16)")) + } + + @Test + fun numberToFixed() { + assertEquals("3.14", str("(3.14159).toFixed(2)")) + } + + @Test + fun arrayLiteralAndLength() { + assertEquals(3.0, num("[1,2,3].length")) + } + + @Test + fun arrayIndexAccess() { + assertEquals(2.0, num("[1,2,3][1]")) + } + + @Test + fun arrayJoin() { + assertEquals("1,2,3", str("[1,2,3].join(',')")) + } + + @Test + fun arrayReverse() { + assertEquals("3,2,1", str("[1,2,3].reverse().join(',')")) + } + + @Test + fun arrayPushReturnsNewLength() { + assertEquals(4.0, num("var a=[1,2,3]; a.push(4)")) + } + + @Test + fun arrayPopRemovesLastElement() { + assertEquals(3.0, num("var a=[1,2,3]; a.pop()")) + } + + @Test + fun arraySlice() { + assertEquals("2,3", str("[1,2,3,4].slice(1,3).join(',')")) + } + + @Test + fun arrayMap() { + assertEquals("2,4,6", str("[1,2,3].map(function(x){return x*2}).join(',')")) + } + + @Test + fun arrayFilter() { + assertEquals("2,4", str("[1,2,3,4].filter(function(x){return x%2===0}).join(',')")) + } + + @Test + fun arrayReduce() { + assertEquals(10.0, num("[1,2,3,4].reduce(function(acc,x){return acc+x},0)")) + } + + @Test + fun arrayIndexOf() { + assertEquals(2.0, num("[10,20,30].indexOf(30)")) + } + + @Test + fun arrayIncludes() { + assertTrue(bool("[1,2,3].includes(2)")) + } + + @Test + fun arrayConcat() { + assertEquals("1,2,3,4", str("[1,2].concat([3,4]).join(',')")) + } + + @Test + fun arraySome() { + assertTrue(bool("[1,2,3].some(function(x){return x>2})")) + } + + @Test + fun arrayEvery() { + assertFalse(bool("[1,2,3].every(function(x){return x>2})")) + } + + @Test + fun arraySortWithComparator() { + assertEquals("1,2,10", str("[10,1,2].sort(function(a,b){return a-b}).join(',')")) + } + + @Test + fun objectPropertyAccessWithDot() { + assertEquals(1.0, num("var o={a:1}; o.a")) + } + + @Test + fun objectPropertyAccessWithBracket() { + assertEquals(2.0, num("var o={b:2}; o['b']")) + } + + @Test + fun objectPropertyAssignment() { + assertEquals(99.0, num("var o={}; o.x=99; o.x")) + } + + @Test + fun ifTrueBranch() { + assertEquals(1.0, num("var r=0; if(true){r=1} r")) + } + + @Test + fun ifFalseUsesElse() { + assertEquals(2.0, num("var r=0; if(false){r=1}else{r=2} r")) + } + + @Test + fun whileLoop() { + assertEquals(10.0, num("var i=0; while(i<10){i++} i")) + } + + @Test + fun whileBreak() { + assertEquals(5.0, num("var i=0; while(true){if(i===5)break; i++} i")) + } + + @Test + fun whileContinue() { + assertEquals(25.0, num("var i=0; var s=0; while(i<10){i++; if(i%2===0)continue; s+=i} s")) + } + + @Test + fun forLoop() { + assertEquals(10.0, num("var s=0; for(var i=1;i<=4;i++){s+=i} s")) + } + + @Test + fun forLoopWithBreak() { + assertEquals(3.0, num("var i; for(i=0;i<10;i++){if(i===3)break} i")) + } + + @Test + fun forInOverObjectKeys() { + assertEquals("a,b,c", str("var o={a:1,b:2,c:3}; var keys=[]; for(var k in o){keys.push(k)} keys.sort().join(',')")) + } + + @Test + fun namedFunctionDeclarationAndCall() { + assertEquals(7.0, num("function add(a,b){return a+b} add(3,4)")) + } + + @Test + fun anonymousFunctionExpression() { + assertEquals(12.0, num("var mul=function(a,b){return a*b}; mul(3,4)")) + } + + @Test + fun recursiveFunction() { + assertEquals(120.0, num("function fact(n){if(n<=1)return 1; return n*fact(n-1)} fact(5)")) + } + + @Test + fun closureCapturesOuterVariable() { + assertEquals(3.0, num("var c=0; function inc(){c+=1} inc();inc();inc(); c")) + } + + @Test + fun immediatelyInvokedFunctionExpression() { + assertEquals(9.0, num("(function(x){return x*x})(3)")) + } + + @Test + fun functionAsArgument() { + assertEquals(6.0, num("function apply(f,x){return f(x)} apply(function(n){return n+1},5)")) + } + + @Test + fun mathFloor() { + assertEquals(3.0, num("Math.floor(3.9)")) + } + + @Test + fun mathCeil() { + assertEquals(4.0, num("Math.ceil(3.1)")) + } + + @Test + fun mathRound() { + assertEquals(4.0, num("Math.round(3.6)")) + } + + @Test + fun mathAbs() { + assertEquals(5.0, num("Math.abs(-5)")) + } + + @Test + fun mathSqrt() { + assertApprox(3.0, num("Math.sqrt(9)")) + } + + @Test + fun mathPow() { + assertEquals(8.0, num("Math.pow(2,3)")) + } + + @Test + fun mathMax() { + assertEquals(9.0, num("Math.max(1,9,3)")) + } + + @Test + fun mathMin() { + assertEquals(1.0, num("Math.min(1,9,3)")) + } + + @Test + fun mathPi() { + assertApprox(Math.PI, num("Math.PI")) + } + + @Test + fun mathLog() { + assertApprox(0.0, num("Math.log(1)")) + } + + @Test + fun parseIntDecimal() { + assertEquals(42.0, num("parseInt('42')")) + } + + @Test + fun parseIntHex() { + assertEquals(255.0, num("parseInt('ff',16)")) + } + + @Test + fun parseIntBinary() { + assertEquals(5.0, num("parseInt('101',2)")) + } + + @Test + fun parseIntInvalid() { + assertTrue(num("parseInt('abc')").isNaN()) + } + + @Test + fun parseFloat() { + assertApprox(3.14, num("parseFloat('3.14')")) + } + + @Test + fun isNanTrue() { + assertTrue(bool("isNaN(NaN)")) + } + + @Test + fun isNanFalse() { + assertFalse(bool("isNaN(1)")) + } + + @Test + fun isFiniteFalse() { + assertFalse(bool("isFinite(Infinity)")) + } + + @Test + fun isFiniteTrue() { + assertTrue(bool("isFinite(1)")) + } + + @Test + fun tryCatchSwallowsThrownValue() { + assertEquals(42.0, num("var r=0; try{throw 42}catch(e){r=e} r")) + } + + @Test + fun finallyAlwaysRuns() { + assertEquals(99.0, num("var r=0; try{throw 1}catch(e){}finally{r=99} r")) + } + + @Test + fun tryWithoutThrowSkipsCatch() { + assertEquals(1.0, num("var r=0; try{r=1}catch(e){r=99} r")) + } + + @Test + fun jsContextPersistsVariablesAcrossEvals() { + val ctx = JsContext() + ctx.eval("var x = 10") + ctx.eval("x += 5") + assertEquals(15.0, ctx["x"] as? Double ?: 0.0) + } + + @Test + fun jsContextGetReturnsNullForUndefined() { + val ctx = JsContext() + assertNull(ctx["nope"]) + } + + @Test + fun jsContextSetExposesValueToEval() { + val ctx = JsContext() + ctx["base"] = 100.0 + ctx.eval("var result = base + 1") + assertEquals(101.0, ctx["result"] as? Double ?: 0.0) + } + + @Test + fun jsContextEvalReturnsLastExpression() { + val ctx = JsContext() + val result = ctx.eval("1+2") + assertEquals(3.0, result as? Double ?: 0.0) + } + + @Test + fun urlExtraction() { + val scriptContent = "var url = '/e/abc123?t=' + (1000+337) + '&s=xyz'" + val ctx = JsContext() + ctx.eval(scriptContent) + assertEquals("/e/abc123?t=1337&s=xyz", ctx["url"]?.toString()) + } + + @Test + fun evaluateMathSimpleAddition() { + assertEquals("5", jsValueToString(evalJs("eval(2+3)"))) + } + + @Test + fun evaluateMathNestedParens() { + assertEquals("12", jsValueToString(evalJs("eval((2+4)*2)"))) + } + + @Test + fun evaluateMathProducesCharCode() { + val code = "eval(1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1)" + assertEquals(65.0, (evalJs(code) as? Double) ?: 0.0) + } + + @Test + fun hexEncodedStringDecoding() { + val code = """ + var encoded = '48|65|6c|6c|6f'; + var decoded = encoded.split('|').map(function(h){ + return String.fromCharCode(parseInt(h, 16)); + }).join(''); + decoded + """.trimIndent() + assertEquals("Hello", str(code)) + } + + @Test + fun charCodeArrayToString() { + val code = """ + var codes = [72, 101, 108, 108, 111]; + var s = ''; + for(var i=0; i 0) { + r = alpha[n % 16] + r; + n = Math.floor(n / 16); + } + return r || '0'; + } + toBase16(255) + """.trimIndent() + assertEquals("ff", str(code)) + } + + @Test + fun xorDeobfuscation() { + val code = """ + var _0x1 = function(s) { + return s.split('').map(function(c) { + return String.fromCharCode(c.charCodeAt(0) ^ 1); + }).join(''); + }; + _0x1('ifmmp') + """.trimIndent() + val expected = "ifmmp".map { (it.code xor 1).toChar() }.joinToString("") + assertEquals(expected, str(code)) + } + + @Test + fun symtabLookupPattern() { + val code = """ + var symtab = ['hello', '', 'world', 'foo']; + var tokens = '0 2'.split(' '); + var result = tokens.map(function(w){ + var idx = parseInt(w,10); + var v = symtab[idx]; + return (v !== undefined && v !== '') ? v : w; + }).join(' '); + result + """.trimIndent() + assertEquals("hello world", str(code)) + } + + @Test + fun hunterDecoderDufHelper() { + val code = """ + function duf(d, e) { + var str = '0123456789abcdefghijklmnopqrstuvwxyz'; + var h = str.substring(0, e); + var j = 0.0; + var rev = d.split('').reverse().join(''); + for(var c=0; c= 0) j += idx * Math.pow(e, c); + } + return Math.floor(j); + } + duf('z', 36) + """.trimIndent() + assertEquals(35.0, num(code)) + } +}