JSModule.kt (13474B) - raw


      1 package me.rhunk.snapenhance.common.scripting
      2 
      3 import android.os.Handler
      4 import android.widget.Toast
      5 import kotlinx.coroutines.*
      6 import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
      7 import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext
      8 import me.rhunk.snapenhance.common.scripting.impl.JavaInterfaces
      9 import me.rhunk.snapenhance.common.scripting.impl.Networking
     10 import me.rhunk.snapenhance.common.scripting.impl.Protobuf
     11 import me.rhunk.snapenhance.common.scripting.ktx.contextScope
     12 import me.rhunk.snapenhance.common.scripting.ktx.putFunction
     13 import me.rhunk.snapenhance.common.scripting.ktx.scriptable
     14 import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject
     15 import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
     16 import me.rhunk.snapenhance.common.scripting.type.Permissions
     17 import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager
     18 import org.mozilla.javascript.Function
     19 import org.mozilla.javascript.ScriptableObject
     20 import org.mozilla.javascript.Undefined
     21 import org.mozilla.javascript.Wrapper
     22 import java.io.Reader
     23 import java.lang.reflect.Modifier
     24 import kotlin.reflect.KClass
     25 
     26 class JSModule(
     27     private val scriptRuntime: ScriptRuntime,
     28     val moduleInfo: ModuleInfo,
     29     private val reader: Reader,
     30 ) {
     31     val coroutineScope = CoroutineScope(Dispatchers.IO)
     32     private val moduleBindings = mutableMapOf<String, AbstractBinding>()
     33     private lateinit var moduleObject: ScriptableObject
     34 
     35     private val moduleBindingContext by lazy {
     36         BindingsContext(
     37             moduleInfo = moduleInfo,
     38             runtime = scriptRuntime,
     39             module = this,
     40         )
     41     }
     42 
     43     fun load(block: ScriptableObject.() -> Unit) {
     44         contextScope {
     45             val classLoader = scriptRuntime.androidContext.classLoader
     46             moduleObject = initSafeStandardObjects()
     47             moduleObject.putConst("module", moduleObject, scriptableObject {
     48                 putConst("info", this, scriptableObject {
     49                     putConst("name", this, moduleInfo.name)
     50                     putConst("version", this, moduleInfo.version)
     51                     putConst("displayName", this, moduleInfo.displayName)
     52                     putConst("description", this, moduleInfo.description)
     53                     putConst("author", this, moduleInfo.author)
     54                     putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion)
     55                     putConst("minSEVersion", this, moduleInfo.minSEVersion)
     56                     putConst("grantedPermissions", this, moduleInfo.grantedPermissions)
     57                 })
     58             })
     59 
     60             scriptRuntime.logger.apply {
     61                 moduleObject.putConst("console", moduleObject, scriptableObject {
     62                     putFunction("log") { info(argsToString(it)) }
     63                     putFunction("warn") { warn(argsToString(it)) }
     64                     putFunction("error") { error(argsToString(it)) }
     65                     putFunction("debug") { debug(argsToString(it)) }
     66                     putFunction("info") { info(argsToString(it)) }
     67                     putFunction("trace") { verbose(argsToString(it)) }
     68                     putFunction("verbose") { verbose(argsToString(it)) }
     69                 })
     70             }
     71 
     72             registerBindings(
     73                 JavaInterfaces(),
     74                 InterfaceManager(),
     75                 Networking(),
     76                 Protobuf()
     77             )
     78 
     79             moduleObject.putFunction("setField") { args ->
     80                 val obj = args?.get(0) ?: return@putFunction Undefined.instance
     81                 val name = args[1].toString()
     82                 val value = args[2]
     83                 val field = obj.javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance
     84                 field.isAccessible = true
     85                 field.set(obj, value.toPrimitiveValue(lazy { field.type.name }))
     86                 Undefined.instance
     87             }
     88 
     89             moduleObject.putFunction("getField") { args ->
     90                 val obj = args?.get(0) ?: return@putFunction Undefined.instance
     91                 val name = args[1].toString()
     92                 val field = obj.javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance
     93                 field.isAccessible = true
     94                 field.get(obj)
     95             }
     96 
     97             moduleObject.putFunction("sleep") { args ->
     98                 val time = args?.get(0) as? Number ?: return@putFunction Undefined.instance
     99                 Thread.sleep(time.toLong())
    100                 Undefined.instance
    101             }
    102 
    103             moduleObject.putFunction("findClass") {
    104                 val className = it?.get(0).toString()
    105                 val useModClassLoader = it?.getOrNull(1) as? Boolean ?: false
    106                 if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER)
    107 
    108                 runCatching {
    109                     if (useModClassLoader) this::class.java.classLoader?.loadClass(className)
    110                     else classLoader.loadClass(className)
    111                 }.onFailure { throwable ->
    112                     scriptRuntime.logger.error("Failed to load class $className", throwable)
    113                 }.getOrNull()
    114             }
    115 
    116             moduleObject.putFunction("type") { args ->
    117                 val className = args?.get(0).toString()
    118                 val useModClassLoader = args?.getOrNull(1) as? Boolean ?: false
    119                 if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER)
    120 
    121                 val clazz = runCatching {
    122                     if (useModClassLoader) this::class.java.classLoader?.loadClass(className) else classLoader.loadClass(className)
    123                 }.getOrNull() ?: return@putFunction Undefined.instance
    124 
    125                 scriptableObject("JavaClassWrapper") {
    126                     val newInstance: (Array<out Any?>?) -> Any? = { args ->
    127                         val constructor = clazz.declaredConstructors.find {
    128                             (args ?: emptyArray()).isSameParameters(it.parameterTypes)
    129                         }?.also { it.isAccessible = true } ?: throw IllegalArgumentException("Constructor not found with args ${argsToString(args)}")
    130                         constructor.newInstance(*args ?: emptyArray())
    131                     }
    132                     putFunction("__new__") { newInstance(it) }
    133 
    134                     clazz.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.forEach { method ->
    135                         putFunction(method.name) { args ->
    136                             val declaredMethod = clazz.declaredMethods.find {
    137                                 it.name == method.name && (args ?: emptyArray()).isSameParameters(it.parameterTypes)
    138                             }?.also { it.isAccessible = true } ?: throw IllegalArgumentException("Method ${method.name} not found with args ${argsToString(args)}")
    139                             declaredMethod.invoke(null, *args ?: emptyArray())
    140                         }
    141                     }
    142 
    143                     clazz.declaredFields.filter { Modifier.isStatic(it.modifiers) }.forEach { field ->
    144                         field.isAccessible = true
    145                         defineProperty(field.name, { field.get(null) }, { value -> field.set(null, value) }, 0)
    146                     }
    147 
    148                     if (get("newInstance") == null) {
    149                         putFunction("newInstance") { newInstance(it) }
    150                     }
    151                 }
    152             }
    153 
    154             moduleObject.putFunction("logInfo") { args ->
    155                 scriptRuntime.logger.info(argsToString(args))
    156                 Undefined.instance
    157             }
    158 
    159             moduleObject.putFunction("logError") { args ->
    160                 scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.getOrNull(1) as? Throwable ?: Throwable())
    161                 Undefined.instance
    162             }
    163 
    164             moduleObject.putFunction("setTimeout") {
    165                 val function = it?.get(0) as? Function ?: return@putFunction Undefined.instance
    166                 val time = it[1] as? Number ?: 0
    167 
    168                 return@putFunction coroutineScope.launch {
    169                     delay(time.toLong())
    170                     contextScope {
    171                         function.call(this, this@putFunction, this@putFunction, emptyArray())
    172                     }
    173                 }
    174             }
    175 
    176             moduleObject.putFunction("setInterval") {
    177                 val function = it?.get(0) as? Function ?: return@putFunction Undefined.instance
    178                 val time = it[1] as? Number ?: 0
    179 
    180                 return@putFunction coroutineScope.launch {
    181                     while (true) {
    182                         delay(time.toLong())
    183                         contextScope {
    184                             function.call(this, this@putFunction, this@putFunction, emptyArray())
    185                         }
    186                     }
    187                 }
    188             }
    189 
    190             arrayOf("clearInterval", "clearTimeout").forEach {
    191                 moduleObject.putFunction(it) { args ->
    192                     val job = args?.get(0) as? Job ?: return@putFunction Undefined.instance
    193                     runCatching {
    194                         job.cancel()
    195                     }
    196                     Undefined.instance
    197                 }
    198             }
    199 
    200             for (toastFunc in listOf("longToast", "shortToast")) {
    201                 moduleObject.putFunction(toastFunc) { args ->
    202                     Handler(scriptRuntime.androidContext.mainLooper).post {
    203                         Toast.makeText(
    204                             scriptRuntime.androidContext,
    205                             args?.joinToString(" ") ?: "",
    206                             if (toastFunc == "longToast") Toast.LENGTH_LONG else Toast.LENGTH_SHORT
    207                         ).show()
    208                     }
    209                     Undefined.instance
    210                 }
    211             }
    212 
    213             block(moduleObject)
    214 
    215             moduleBindings.forEach { (_, instance) ->
    216                 instance.context = moduleBindingContext
    217 
    218                 runCatching {
    219                     instance.onInit()
    220                 }.onFailure {
    221                     scriptRuntime.logger.error("Failed to init binding ${instance.name}", it)
    222                 }
    223             }
    224 
    225             moduleObject.putFunction("require") { args ->
    226                 val bindingName = args?.get(0).toString()
    227                 val (namespace, path) = bindingName.takeIf {
    228                     it.startsWith("@") && it.contains("/")
    229                 }?.let {
    230                     it.substring(1).substringBefore("/") to it.substringAfter("/")
    231                 } ?: (null to "")
    232 
    233                 when (namespace) {
    234                     "modules" -> scriptRuntime.getModuleByName(path)?.moduleObject?.scriptable("module")?.scriptable("exports")
    235                     else -> moduleBindings[bindingName]?.getObject()
    236                 }
    237             }
    238         }
    239 
    240         contextScope(shouldOptimize = scriptRuntime.config().scripting.disableOptimization.getNullable() != true) {
    241             evaluateReader(moduleObject, reader, moduleInfo.name, 1, null)
    242         }
    243     }
    244 
    245     fun unload() {
    246         callFunction("module.onUnload")
    247         runCatching {
    248             coroutineScope.cancel("Module unloaded")
    249         }
    250         moduleBindings.entries.removeIf { (name, binding) ->
    251             runCatching {
    252                 binding.onDispose()
    253             }.onFailure {
    254                 scriptRuntime.logger.error("Failed to dispose binding $name", it)
    255             }
    256             true
    257         }
    258     }
    259 
    260     fun callFunction(name: String, vararg args: Any?) {
    261         contextScope {
    262             name.split(".").also { split ->
    263                 val function = split.dropLast(1).fold(moduleObject) { obj, key ->
    264                     obj.get(key, obj) as? ScriptableObject ?: return@contextScope Unit
    265                 }.get(split.last(), moduleObject) as? Function ?: return@contextScope Unit
    266 
    267                 runCatching {
    268                     function.call(this, moduleObject, moduleObject, args)
    269                 }.onFailure {
    270                     scriptRuntime.logger.error("Error while calling function $name", it)
    271                 }
    272             }
    273         }
    274     }
    275 
    276     fun registerBindings(vararg bindings: AbstractBinding) {
    277         bindings.forEach {
    278             moduleBindings[it.name] = it.apply {
    279                 context = moduleBindingContext
    280             }
    281         }
    282     }
    283 
    284     fun onBridgeConnected(reloaded: Boolean = false) {
    285         if (reloaded) {
    286             moduleBindings.values.forEach { binding ->
    287                 runCatching {
    288                     binding.onBridgeReloaded()
    289                 }.onFailure {
    290                     scriptRuntime.logger.error("Failed to call onBridgeConnected for binding ${binding.name}", it)
    291                 }
    292             }
    293         }
    294 
    295         callFunction("module.onBridgeConnected", reloaded)
    296     }
    297 
    298     @Suppress("UNCHECKED_CAST")
    299     fun <T : Any> getBinding(clazz: KClass<T>): T? {
    300         return moduleBindings.values.find { clazz.isInstance(it) } as? T
    301     }
    302 
    303     private fun argsToString(args: Array<out Any?>?): String {
    304         return args?.joinToString(" ") {
    305             when (it) {
    306                 is Wrapper -> it.unwrap().let { value ->
    307                     if (value is Throwable) value.message + "\n" + value.stackTraceToString()
    308                     else value.toString()
    309                 }
    310                 else -> it.toString()
    311             }
    312         } ?: "null"
    313     }
    314 }