diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 84543430..4893d844 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -46,6 +46,8 @@ dependencies { implementation(libs.okhttp) implementation(libs.androidx.documentfile) implementation(libs.rhino) + implementation(libs.kavaref.core) + implementation(libs.kavaref.extension) implementation(libs.rhino.android) { exclude(group = "org.mozilla", module = "rhino-runtime") } diff --git a/common/src/main/java/me/eternal/purrfectsnap/common/util/ktx/KavaRefFieldBridge.java b/common/src/main/java/me/eternal/purrfectsnap/common/util/ktx/KavaRefFieldBridge.java new file mode 100644 index 00000000..a132bf6d --- /dev/null +++ b/common/src/main/java/me/eternal/purrfectsnap/common/util/ktx/KavaRefFieldBridge.java @@ -0,0 +1,62 @@ +package me.eternal.purrfectsnap.common.util.ktx; + +import com.highcapable.kavaref.KavaRef; +import com.highcapable.kavaref.condition.FieldCondition; +import com.highcapable.kavaref.resolver.FieldResolver; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + +import java.util.ArrayList; +import java.util.List; + +public final class KavaRefFieldBridge { + private KavaRefFieldBridge() {} + + private static FieldResolver resolveField(Object instance, String fieldName) { + Class current = instance.getClass(); + while (current != null && current != Object.class) { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) current); + FieldResolver resolver = scope.firstFieldOrNull(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + condition.name(fieldName); + return Unit.INSTANCE; + } + }); + if (resolver != null) return resolver.of(instance); + current = current.getSuperclass(); + } + return null; + } + + public static Object getField(Object instance, String fieldName) throws NoSuchFieldException { + FieldResolver resolver = resolveField(instance, fieldName); + if (resolver == null) throw new NoSuchFieldException(instance.getClass().getName() + "#" + fieldName); + return resolver.get(); + } + + public static void setField(Object instance, String fieldName, Object value) throws NoSuchFieldException { + FieldResolver resolver = resolveField(instance, fieldName); + if (resolver == null) throw new NoSuchFieldException(instance.getClass().getName() + "#" + fieldName); + resolver.set(value); + } + + public static String[] getAllFieldNames(Object instance) { + List result = new ArrayList<>(); + Class current = instance.getClass(); + while (current != null && current != Object.class) { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) current); + List> fields = scope.field(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + return Unit.INSTANCE; + } + }); + for (FieldResolver field : fields) result.add(field.getSelf().getName()); + current = current.getSuperclass(); + } + return result.toArray(new String[0]); + } +} diff --git a/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/JSModule.kt b/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/JSModule.kt index a2d8ce2f..78caf6e9 100644 --- a/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/JSModule.kt +++ b/common/src/main/kotlin/me/eternal/purrfectsnap/common/scripting/JSModule.kt @@ -8,6 +8,7 @@ import me.eternal.purrfectsnap.common.scripting.bindings.BindingsContext import me.eternal.purrfectsnap.common.scripting.impl.JavaInterfaces import me.eternal.purrfectsnap.common.scripting.impl.Networking import me.eternal.purrfectsnap.common.scripting.impl.Protobuf +import me.eternal.purrfectsnap.common.util.ktx.KavaRefFieldBridge import me.eternal.purrfectsnap.common.scripting.ktx.contextScope import me.eternal.purrfectsnap.common.scripting.ktx.putFunction import me.eternal.purrfectsnap.common.scripting.ktx.scriptable @@ -81,18 +82,18 @@ class JSModule( val obj = args?.get(0) ?: return@putFunction Undefined.instance val name = args[1].toString() val value = args[2] - val field = obj.javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance - field.isAccessible = true - field.set(obj, value.toPrimitiveValue(lazy { field.type.name })) + runCatching { + KavaRefFieldBridge.getField(obj, name)?.javaClass?.name + }.getOrNull()?.let { typeName -> + KavaRefFieldBridge.setField(obj, name, value.toPrimitiveValue(lazy { typeName })) + } ?: return@putFunction Undefined.instance Undefined.instance } moduleObject.putFunction("getField") { args -> val obj = args?.get(0) ?: return@putFunction Undefined.instance val name = args[1].toString() - val field = obj.javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance - field.isAccessible = true - field.get(obj) + runCatching { KavaRefFieldBridge.getField(obj, name) }.getOrNull() ?: Undefined.instance } moduleObject.putFunction("sleep") { args -> @@ -141,9 +142,14 @@ class JSModule( } } + // Expose static fields if they can be read through class reflection. clazz.declaredFields.filter { Modifier.isStatic(it.modifiers) }.forEach { field -> - field.isAccessible = true - defineProperty(field.name, { field.get(null) }, { value -> field.set(null, value) }, 0) + defineProperty( + field.name, + { runCatching { field.get(null) }.getOrNull() }, + { value -> runCatching { field.set(null, value) } }, + 0 + ) } if (get("newInstance") == null) { diff --git a/core/src/main/java/me/eternal/purrfectsnap/core/util/ktx/KavaRefFieldBridge.java b/core/src/main/java/me/eternal/purrfectsnap/core/util/ktx/KavaRefFieldBridge.java new file mode 100644 index 00000000..98ec8044 --- /dev/null +++ b/core/src/main/java/me/eternal/purrfectsnap/core/util/ktx/KavaRefFieldBridge.java @@ -0,0 +1,127 @@ +package me.eternal.purrfectsnap.core.util.ktx; + +import com.highcapable.kavaref.KavaRef; +import com.highcapable.kavaref.condition.FieldCondition; +import com.highcapable.kavaref.resolver.FieldResolver; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; + +import java.util.ArrayList; +import java.util.List; + +public final class KavaRefFieldBridge { + private KavaRefFieldBridge() {} + + private static FieldResolver resolveField(Object instance, String fieldName) { + Class current = instance.getClass(); + while (current != null && current != Object.class) { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) current); + FieldResolver resolver = scope.firstFieldOrNull(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + condition.name(fieldName); + return Unit.INSTANCE; + } + }); + if (resolver != null) { + return resolver.of(instance); + } + current = current.getSuperclass(); + } + return null; + } + + public static Object getField(Object instance, String fieldName) throws NoSuchFieldException { + FieldResolver resolver = resolveField(instance, fieldName); + if (resolver == null) throw new NoSuchFieldException(instance.getClass().getName() + "#" + fieldName); + return resolver.get(); + } + + public static void setField(Object instance, String fieldName, Object value) throws NoSuchFieldException { + FieldResolver resolver = resolveField(instance, fieldName); + if (resolver == null) throw new NoSuchFieldException(instance.getClass().getName() + "#" + fieldName); + resolver.set(value); + } + + public static Class getFieldType(Object instance, String fieldName) throws NoSuchFieldException { + FieldResolver resolver = resolveField(instance, fieldName); + if (resolver == null) throw new NoSuchFieldException(instance.getClass().getName() + "#" + fieldName); + return resolver.getSelf().getType(); + } + + public static String[] findFieldNamesByType(Object instance, Class fieldType) { + List result = new ArrayList<>(); + Class current = instance.getClass(); + while (current != null && current != Object.class) { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) current); + List> fields = scope.field(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + condition.type(fieldType); + return Unit.INSTANCE; + } + }); + for (FieldResolver field : fields) { + result.add(field.getSelf().getName()); + } + current = current.getSuperclass(); + } + return result.toArray(new String[0]); + } + + public static String[] getAllFieldNames(Object instance) { + List result = new ArrayList<>(); + Class current = instance.getClass(); + while (current != null && current != Object.class) { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) current); + List> fields = scope.field(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + return Unit.INSTANCE; + } + }); + for (FieldResolver field : fields) { + result.add(field.getSelf().getName()); + } + current = current.getSuperclass(); + } + return result.toArray(new String[0]); + } + + public static Object getStaticField(Class clazz, String fieldName) throws NoSuchFieldException { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) clazz); + FieldResolver resolver = scope.firstFieldOrNull(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + condition.name(fieldName); + return Unit.INSTANCE; + } + }); + if (resolver == null) throw new NoSuchFieldException(clazz.getName() + "#" + fieldName); + return resolver.get(); + } + + public static Object findStaticFieldByType(Class clazz, Class fieldType) { + @SuppressWarnings("unchecked") + KavaRef.MemberScope scope = (KavaRef.MemberScope) KavaRef.resolveClass((Class) clazz); + List> fields = scope.field(new Function1, Unit>() { + @Override + public Unit invoke(FieldCondition condition) { + condition.type(fieldType); + return Unit.INSTANCE; + } + }); + for (FieldResolver field : fields) { + try { + Object value = field.get(); + if (value != null && value.getClass() == fieldType) return value; + } catch (Throwable ignored) {} + } + return null; + } +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt index 7b65f92b..97c2fb4d 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/BulkMessagingAction.kt @@ -67,6 +67,7 @@ import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging import me.eternal.purrfectsnap.core.ui.ViewAppearanceHelper import me.eternal.purrfectsnap.core.util.EvictingMap import me.eternal.purrfectsnap.core.util.dataBuilder +import me.eternal.purrfectsnap.core.util.ktx.findStaticObjectFieldByType import me.eternal.purrfectsnap.mapper.impl.FriendRelationshipChangerMapper import java.text.DateFormat import java.util.Date @@ -1502,11 +1503,7 @@ class BulkMessagingAction : AbstractAction() { val pageTypeClass = pageType.getAsClass() ?: return@runCatching context.log.error("Could not find page type class") val method = f9lClass.declaredMethods.firstOrNull { it.name == addFriendMethodName } ?: return@runCatching context.log.error("Could not find $addFriendMethodName method") - fun findStaticField(clazz: Class<*>): Any? = clazz.declaredFields.firstOrNull { field -> - java.lang.reflect.Modifier.isStatic(field.modifiers) && field.type == clazz - }?.let { field -> - runCatching { field.isAccessible = true; field.get(null)?.takeIf { it.javaClass == clazz } }.getOrNull() - } + fun findStaticField(clazz: Class<*>): Any? = clazz.findStaticObjectFieldByType(clazz) val enumClass = method.parameterTypes[2] val enumConstants = enumClass.enumConstants ?: enumClass.getMethod("values").invoke(null) as? Array<*> ?: return@runCatching context.log.error("Could not get enum constants") diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt index 8de58ca9..e1e335f5 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/action/impl/ManageFriendList.kt @@ -46,6 +46,7 @@ import me.eternal.purrfectsnap.core.action.AbstractAction import me.eternal.purrfectsnap.core.event.events.impl.ActivityResultEvent import me.eternal.purrfectsnap.core.features.impl.experiments.AddFriendSourceSpoof import me.eternal.purrfectsnap.core.features.impl.messaging.Messaging +import me.eternal.purrfectsnap.core.util.ktx.findStaticObjectFieldByType import me.eternal.purrfectsnap.core.util.EvictingMap import me.eternal.purrfectsnap.core.wrapper.impl.Snapchatter import me.eternal.purrfectsnap.common.util.snap.BitmojiSelfie @@ -103,15 +104,7 @@ class ManageFriendList : AbstractAction() { // Helper function to find static field by trying fallback fun findStaticField(clazz: Class<*>): Any? { - // Fallback: find any static field of the same type - return clazz.declaredFields.firstOrNull { field -> - java.lang.reflect.Modifier.isStatic(field.modifiers) && field.type == clazz - }?.let { field -> - runCatching { - field.isAccessible = true - field.get(null)?.takeIf { it.javaClass == clazz } - }.getOrNull() - } + return clazz.findStaticObjectFieldByType(clazz) } // Get enum constant for USERNAME diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt index 1b9dbbed..048e9ad1 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/SnapchatPlus.kt @@ -2,6 +2,8 @@ package me.eternal.purrfectsnap.core.features.impl.global import me.eternal.purrfectsnap.core.features.Feature import me.eternal.purrfectsnap.core.util.dataBuilder +import me.eternal.purrfectsnap.core.util.ktx.findFieldNamesByType +import me.eternal.purrfectsnap.core.util.ktx.setObjectField import me.eternal.purrfectsnap.core.util.hook.HookStage import me.eternal.purrfectsnap.core.util.hook.hook import me.eternal.purrfectsnap.core.util.hook.hookConstructor @@ -54,11 +56,10 @@ class SnapchatPlus: Feature("SnapchatPlus") { val instance = param.thisObject() val firstArg = param.argNullable(0) ?: return@hook - instance::class.java.declaredFields.filter { it.type == firstArg::class.java }.forEach { - it.isAccessible = true - it.set(instance, firstArg) + instance.findFieldNamesByType(firstArg::class.java).forEach { fieldName -> + instance.setObjectField(fieldName, firstArg) } } } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt index 3c26962f..7160b659 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/messaging/SendOverride.kt @@ -193,9 +193,7 @@ class SendOverride : Feature("Send Override") { } } runCatching { - val field = result.messageContent.instanceNonNull().javaClass.getDeclaredField("mAllowsTranscription") - field.isAccessible = true - field.set(result.messageContent.instanceNonNull(), false) + result.messageContent.instanceNonNull().setObjectField("mAllowsTranscription", false) } } }.toByteArray() @@ -508,13 +506,7 @@ class SendOverride : Feature("Send Override") { }.getOrNull() if (prohibitedEnum != null) { - val savePolicyField = localMessageContent.instanceNonNull().javaClass.declaredFields - .find { it.name == "mSavePolicy" } - - if (savePolicyField != null) { - savePolicyField.isAccessible = true - localMessageContent.instanceNonNull().setObjectField("mSavePolicy", prohibitedEnum) - } + localMessageContent.instanceNonNull().setObjectField("mSavePolicy", prohibitedEnum) } } } catch (e: Exception) { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt index 071aa70b..7cd662b5 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/spying/MessageLogger.kt @@ -24,6 +24,8 @@ import me.eternal.purrfectsnap.core.features.MessagingRuleFeature import me.eternal.purrfectsnap.core.ui.addForegroundDrawable import me.eternal.purrfectsnap.core.ui.removeForegroundDrawable import me.eternal.purrfectsnap.core.util.EvictingMap +import me.eternal.purrfectsnap.core.util.ktx.KavaRefFieldBridge +import me.eternal.purrfectsnap.core.util.ktx.setObjectField import java.util.concurrent.Executors import kotlin.system.measureTimeMillis @@ -175,11 +177,12 @@ class MessageLogger : MessagingRuleFeature("MessageLogger", MessagingRuleType.ME } //serialize all properties of messageJsonObject and put mMessageContent & mMetadata in the message object - messageInstance::class.java.declaredFields.forEach { field -> - if (field.name != "mMessageContent" && field.name != "mMetadata") return@forEach - field.isAccessible = true - deletedMessageObject[field.name]?.let { fieldValue -> - field.set(messageInstance, context.gson.fromJson(fieldValue, field.type)) + listOf("mMessageContent", "mMetadata").forEach { fieldName -> + deletedMessageObject[fieldName]?.let { fieldValue -> + runCatching { + val fieldType = KavaRefFieldBridge.getFieldType(messageInstance, fieldName) + messageInstance.setObjectField(fieldName, context.gson.fromJson(fieldValue, fieldType)) + } } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/DataClassBuilder.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/DataClassBuilder.kt index 1d6b0b7c..035fbd49 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/DataClassBuilder.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/DataClassBuilder.kt @@ -1,5 +1,8 @@ package me.eternal.purrfectsnap.core.util +import me.eternal.purrfectsnap.core.util.ktx.getObjectField +import me.eternal.purrfectsnap.core.util.ktx.setObjectField +import me.eternal.purrfectsnap.core.util.ktx.KavaRefFieldBridge import java.lang.reflect.Proxy @@ -35,28 +38,26 @@ class DataClassBuilder( private val instance: Any, ) { fun set(fieldName: String, value: Any?) { - val field = instance::class.java.declaredFields.firstOrNull { it.name == fieldName } ?: return - val fieldType = field.type ?: return - field.isAccessible = true + val fieldType = runCatching { KavaRefFieldBridge.getFieldType(instance, fieldName) }.getOrNull() ?: return when { fieldType.isEnum -> { val enumValue = fieldType.enumConstants?.firstOrNull { it.toString() == value } ?: return - field.set(instance, enumValue) + instance.setObjectField(fieldName, enumValue) } fieldType.isPrimitive -> { when (fieldType) { - Boolean::class.javaPrimitiveType -> field.setBoolean(instance, value as Boolean) - Byte::class.javaPrimitiveType -> field.setByte(instance, value as Byte) - Char::class.javaPrimitiveType -> field.setChar(instance, value as Char) - Short::class.javaPrimitiveType -> field.setShort(instance, value as Short) - Int::class.javaPrimitiveType -> field.setInt(instance, value as Int) - Long::class.javaPrimitiveType -> field.setLong(instance, value as Long) - Float::class.javaPrimitiveType -> field.setFloat(instance, value as Float) - Double::class.javaPrimitiveType -> field.setDouble(instance, value as Double) + Boolean::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Boolean) + Byte::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Byte) + Char::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Char) + Short::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Short) + Int::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Int) + Long::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Long) + Float::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Float) + Double::class.javaPrimitiveType -> instance.setObjectField(fieldName, value as Double) } } - else -> field.set(instance, value) + else -> instance.setObjectField(fieldName, value) } } @@ -64,23 +65,20 @@ class DataClassBuilder( @Suppress("UNCHECKED_CAST") fun get(fieldName: String): T? { - val field = instance::class.java.declaredFields.firstOrNull { it.name == fieldName } ?: return null - field.isAccessible = true - return field.get(instance) as? T + return instance.getObjectField(fieldName) as? T } fun from(fieldName: String, new: Boolean = false, callback: DataClassBuilder.() -> Unit) { - val field = instance::class.java.declaredFields.firstOrNull { it.name == fieldName } ?: return - field.isAccessible = true + val fieldType = runCatching { KavaRefFieldBridge.getFieldType(instance, fieldName) }.getOrNull() ?: return - val lazyInstance by lazy { CallbackBuilder.createEmptyObject(field.type.constructors.firstOrNull() ?: return@lazy null) ?: return@lazy null } + val lazyInstance by lazy { CallbackBuilder.createEmptyObject(fieldType.constructors.firstOrNull() ?: return@lazy null) ?: return@lazy null } val builderInstance = if (new) lazyInstance else { - field.get(instance).takeIf { it != null } ?: lazyInstance + instance.getObjectField(fieldName).takeIf { it != null } ?: lazyInstance } DataClassBuilder(builderInstance ?: return).apply(callback) - field.set(instance, builderInstance) + instance.setObjectField(fieldName, builderInstance) } fun cast(type: Class, callback: T.() -> Unit) { @@ -88,10 +86,8 @@ class DataClassBuilder( } fun interceptFieldInterface(fieldName: String, callback: (args: Array, originalCall: (Array) -> Any?) -> Any?) { - val field = instance.javaClass.declaredFields.firstOrNull { it.name == fieldName } ?: return - field.isAccessible = true - set(fieldName, field.get(instance)?.let { makeFunctionProxy(it, callback) } ?: return) + set(fieldName, instance.getObjectField(fieldName)?.let { makeFunctionProxy(it, callback) } ?: return) } fun build() = instance -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt index 64cd20b6..34f6bd7e 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/LSPatchUpdater.kt @@ -2,6 +2,7 @@ package me.eternal.purrfectsnap.core.util import me.eternal.purrfectsnap.common.Constants import me.eternal.purrfectsnap.core.ModContext +import me.eternal.purrfectsnap.core.util.ktx.getStaticObjectField import java.io.File import java.util.zip.ZipFile @@ -40,9 +41,9 @@ object LSPatchUpdater { val obfuscatedModulePath by lazy { (runCatching { context::class.java.classLoader?.loadClass("org.lsposed.lspatch.share.Constants") - }.getOrNull())?.declaredFields?.firstOrNull { it.name == "MANAGER_PACKAGE_NAME" }?.also { - it.isAccessible = true - }?.get(null) as? String + }.getOrNull())?.let { clazz -> + runCatching { clazz.getStaticObjectField("MANAGER_PACKAGE_NAME") as? String }.getOrNull() + } } val embeddedModule = context.androidContext.cacheDir diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt index 9fc72774..d2e2d2cb 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/util/ktx/XposedHelperExt.kt @@ -1,33 +1,34 @@ package me.eternal.purrfectsnap.core.util.ktx -private fun findDeclaredFieldRecursive(type: Class<*>, fieldName: String): java.lang.reflect.Field? { - var current: Class<*>? = type - while (current != null && current != Any::class.java) { - current.declaredFields.firstOrNull { it.name == fieldName }?.let { return it } - current = current.superclass - } - return null +fun Any.getObjectField(fieldName: String): Any? { + return KavaRefFieldBridge.getField(this, fieldName) } -fun Any.getObjectField(fieldName: String): Any? { - val field = findDeclaredFieldRecursive(this::class.java, fieldName) - ?: throw NoSuchFieldException("${this::class.java.name}#$fieldName") - field.isAccessible = true - return field.get(this) +fun Any.findFieldNamesByType(type: Class<*>): List { + return KavaRefFieldBridge.findFieldNamesByType(this, type).toList() +} + +fun Any.allFieldNames(): List { + return KavaRefFieldBridge.getAllFieldNames(this).toList() +} + +fun Class<*>.getStaticObjectField(fieldName: String): Any? { + return KavaRefFieldBridge.getStaticField(this, fieldName) +} + +fun Class<*>.findStaticObjectFieldByType(type: Class<*>): Any? { + return KavaRefFieldBridge.findStaticFieldByType(this, type) } fun Any.setEnumField(fieldName: String, value: String) { - this::class.java.getDeclaredField(fieldName) - .type.enumConstants?.firstOrNull { it.toString() == value }?.let { enum -> + val enumType = KavaRefFieldBridge.getFieldType(this, fieldName) + enumType.enumConstants?.firstOrNull { it.toString() == value }?.let { enum -> setObjectField(fieldName, enum) } } fun Any.setObjectField(fieldName: String, value: Any?) { - val field = findDeclaredFieldRecursive(this::class.java, fieldName) - ?: throw NoSuchFieldException("${this::class.java.name}#$fieldName") - field.isAccessible = true - field.set(this, value) + KavaRefFieldBridge.setField(this, fieldName, value) } fun Any.getObjectFieldOrNull(fieldName: String): Any? { diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt index e481bb02..a0b734d4 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/AbstractWrapper.kt @@ -63,7 +63,9 @@ abstract class AbstractWrapper( @Suppress("UNCHECKED_CAST") fun setEnumValue(fieldName: String, value: Enum<*>) { - val type = instance!!.javaClass.declaredFields.find { it.name == fieldName }?.type as Class> + val currentValue = instance?.getObjectField(fieldName) as? Enum<*> ?: return + @Suppress("UNCHECKED_CAST") + val type = currentValue.javaClass as Class> instance?.setObjectField(fieldName, java.lang.Enum.valueOf(type, value.name)) } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ScSize.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ScSize.kt index 1a73e709..6ef3fa06 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ScSize.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/ScSize.kt @@ -1,26 +1,26 @@ package me.eternal.purrfectsnap.core.wrapper.impl +import me.eternal.purrfectsnap.core.util.ktx.findFieldNamesByType +import me.eternal.purrfectsnap.core.util.ktx.getObjectField +import me.eternal.purrfectsnap.core.util.ktx.setObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper class ScSize( obj: Any? ) : AbstractWrapper(obj) { - private val firstField by lazy { - instanceNonNull().javaClass.declaredFields.first { it.type == Int::class.javaPrimitiveType }.also { it.isAccessible = true } + private val intFields by lazy { + instanceNonNull().findFieldNamesByType(Int::class.javaPrimitiveType ?: Int::class.java) } + private val firstFieldName by lazy { intFields.first() } + private val secondFieldName by lazy { intFields.last() } - private val secondField by lazy { - instanceNonNull().javaClass.declaredFields.last { it.type == Int::class.javaPrimitiveType }.also { it.isAccessible = true } - } - - - var first: Int get() = firstField.getInt(instanceNonNull()) + var first: Int get() = instanceNonNull().getObjectField(firstFieldName) as Int set(value) { - firstField.setInt(instanceNonNull(), value) + instanceNonNull().setObjectField(firstFieldName, value) } - var second: Int get() = secondField.getInt(instanceNonNull()) + var second: Int get() = instanceNonNull().getObjectField(secondFieldName) as Int set(value) { - secondField.setInt(instanceNonNull(), value) + instanceNonNull().setObjectField(secondFieldName, value) } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/EncryptionWrapper.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/EncryptionWrapper.kt index e8db5e19..7e0b28b5 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/EncryptionWrapper.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/EncryptionWrapper.kt @@ -3,10 +3,11 @@ package me.eternal.purrfectsnap.core.wrapper.impl.media import android.util.Base64 import android.util.Log import me.eternal.purrfectsnap.common.data.download.MediaEncryptionKeyPair +import me.eternal.purrfectsnap.core.util.ktx.findFieldNamesByType +import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper import java.io.InputStream import java.io.OutputStream -import java.lang.reflect.Field import javax.crypto.Cipher import javax.crypto.CipherInputStream import javax.crypto.CipherOutputStream @@ -44,28 +45,19 @@ class EncryptionWrapper( // Key + IV extraction val keySpec: ByteArray by lazy { - manualKey ?: searchByteArrayField(32)[instance] as ByteArray + manualKey ?: searchByteArrayField(32)?.let { instanceNonNull().getObjectField(it) as? ByteArray } + ?: throw NoSuchFieldException("Failed to find 32-byte key field") } val ivKeyParameterSpec: ByteArray by lazy { - manualIv ?: searchByteArrayField(16)[instance] as ByteArray + manualIv ?: searchByteArrayField(16)?.let { instanceNonNull().getObjectField(it) as? ByteArray } + ?: throw NoSuchFieldException("Failed to find 16-byte IV field") } - private fun searchByteArrayField(length: Int): Field { - return instanceNonNull()::class.java.declaredFields.first { field -> - try { - field.isAccessible = true - - if (!field.type.isArray || - field.type.componentType != Byte::class.javaPrimitiveType - ) return@first false - - val value = field.get(instanceNonNull()) as? ByteArray - value?.size == length - - } catch (_: Exception) { - false - } + private fun searchByteArrayField(length: Int): String? { + val fields = instanceNonNull().findFieldNamesByType(ByteArray::class.java) + return fields.firstOrNull { fieldName -> + (instanceNonNull().getObjectField(fieldName) as? ByteArray)?.size == length } } diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt index ba54018a..b30499b2 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt @@ -1,11 +1,13 @@ package me.eternal.purrfectsnap.core.wrapper.impl.media.dash +import me.eternal.purrfectsnap.core.util.ktx.findFieldNamesByType +import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper class LongformVideoPlaylistItem(obj: Any?) : AbstractWrapper(obj) { - private val chapterList by lazy { - instanceNonNull().javaClass.declaredFields.first { it.type == List::class.java } + private val chapterListField by lazy { + instanceNonNull().findFieldNamesByType(List::class.java).first() } val chapters: List - get() = (chapterList.get(instanceNonNull()) as List<*>).map { SnapChapter(it) } -} \ No newline at end of file + get() = (instanceNonNull().getObjectField(chapterListField) as List<*>).map { SnapChapter(it) } +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapChapter.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapChapter.kt index 75405d3f..e590f81e 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapChapter.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapChapter.kt @@ -1,12 +1,17 @@ package me.eternal.purrfectsnap.core.wrapper.impl.media.dash +import me.eternal.purrfectsnap.core.util.ktx.findFieldNamesByType +import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper class SnapChapter (obj: Any?) : AbstractWrapper(obj) { + private val longFields by lazy { + instanceNonNull().findFieldNamesByType(Long::class.javaPrimitiveType ?: Long::class.java) + } val snapId by lazy { - instanceNonNull().javaClass.declaredFields.first { it.type == Long::class.javaPrimitiveType }.get(instanceNonNull()) as Long + instanceNonNull().getObjectField(longFields.first()) as Long } val startTimeMs by lazy { - instanceNonNull().javaClass.declaredFields.filter { it.type == Long::class.javaPrimitiveType }[1].get(instanceNonNull()) as Long + instanceNonNull().getObjectField(longFields[1]) as Long } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapPlaylistItem.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapPlaylistItem.kt index 792bd326..a9ae53f2 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapPlaylistItem.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/wrapper/impl/media/dash/SnapPlaylistItem.kt @@ -1,9 +1,14 @@ package me.eternal.purrfectsnap.core.wrapper.impl.media.dash +import me.eternal.purrfectsnap.core.util.ktx.findFieldNamesByType +import me.eternal.purrfectsnap.core.util.ktx.getObjectField import me.eternal.purrfectsnap.core.wrapper.AbstractWrapper class SnapPlaylistItem (obj: Any?) : AbstractWrapper(obj) { - val snapId by lazy { - instanceNonNull().javaClass.declaredFields.first { it.type == Long::class.javaPrimitiveType }.get(instanceNonNull()) as Long + private val longFields by lazy { + instanceNonNull().findFieldNamesByType(Long::class.javaPrimitiveType ?: Long::class.java) } -} \ No newline at end of file + val snapId by lazy { + instanceNonNull().getObjectField(longFields.first()) as Long + } +} diff --git a/gradle.properties b/gradle.properties index e72ef751..ec537615 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,6 @@ APP_VERSION_NAME=1.3.2 APP_VERSION_CODE=272 debug_build_hash=18fe2a814d0e2eb5 psIntegrityPinnedSha256= -EXPECTED_CERT_SHA256=371cb9fbebbe9880d147f59bab36d61eee122854ef8c9ee1ecf12b82368bcf10 +EXPECTED_CERT_SHA256=0f188cb17d8ea4902ee15ce98f4928ba4226a4790fa3c52f62d776b08e46ca1c android.disallowKotlinSourceSets=false android.sourceset.disallowProvider=false