diff --git a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt index df5876d4..af5928f4 100644 --- a/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt +++ b/src/main/kotlin/app/revanced/patcher/proxy/mutableTypes/MutableClass.kt @@ -29,7 +29,7 @@ class MutableClass(classDef: ClassDef) : ClassDef, BaseTypeReference() { // Fields private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() } private val _staticFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_STATIC).toMutableSet() } - private val _instanceFields by lazy {Iterables.filter(_fields, FieldUtil.FIELD_IS_INSTANCE) .toMutableSet() } + private val _instanceFields by lazy { Iterables.filter(_fields, FieldUtil.FIELD_IS_INSTANCE).toMutableSet() } fun setType(type: String) { this.type = type diff --git a/src/main/kotlin/app/revanced/patcher/resolver/SignatureResolver.kt b/src/main/kotlin/app/revanced/patcher/resolver/SignatureResolver.kt index dde362fb..dad6340a 100644 --- a/src/main/kotlin/app/revanced/patcher/resolver/SignatureResolver.kt +++ b/src/main/kotlin/app/revanced/patcher/resolver/SignatureResolver.kt @@ -31,8 +31,8 @@ internal class SignatureResolver( val classProxy = ClassProxy(classDef, index) methodMap[signature.name] = SignatureResolverResult( classProxy, + patternScanData, method.name, - patternScanData ) } } @@ -53,8 +53,8 @@ internal class SignatureResolver( val result = compareSignatureToMethod(signature, method) ?: continue return SignatureResolverResult( classProxy, + result, method.name, - result ) } return null diff --git a/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt b/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt index a8a8dee1..0d9fd574 100644 --- a/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt +++ b/src/main/kotlin/app/revanced/patcher/signature/SignatureResolverResult.kt @@ -1,6 +1,7 @@ package app.revanced.patcher.signature import app.revanced.patcher.proxy.ClassProxy +import app.revanced.patcher.proxy.mutableTypes.MutableMethod import app.revanced.patcher.resolver.SignatureResolver /** @@ -11,9 +12,14 @@ import app.revanced.patcher.resolver.SignatureResolver */ data class SignatureResolverResult( val definingClassProxy: ClassProxy, - val resolvedMethodName: String, - val scanData: PatternScanResult? + val scanData: PatternScanResult, + private val resolvedMethodName: String, ) { + + fun resolveAndGetMethod(): MutableMethod { + return definingClassProxy.resolve().methods.single { it.name == resolvedMethodName } + } + @Suppress("Unused") // TODO(Sculas): remove this when we have coverage for this method. fun findParentMethod(signature: MethodSignature): SignatureResolverResult? { return SignatureResolver.resolveFromProxy(definingClassProxy, signature) diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt new file mode 100644 index 00000000..99870859 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -0,0 +1,128 @@ +package app.revanced.patcher + +import app.revanced.patcher.cache.Cache +import app.revanced.patcher.extensions.or +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.signature.MethodSignature +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.builder.instruction.BuilderInstruction21c +import org.jf.dexlib2.builder.instruction.BuilderInstruction35c +import org.jf.dexlib2.immutable.reference.ImmutableMethodReference +import org.jf.dexlib2.immutable.reference.ImmutableStringReference +import org.junit.jupiter.api.Test +import java.io.File + +internal class PatcherTest { + companion object { + val testSignatures: Array = arrayOf( + MethodSignature( + "main-method", + "V", + AccessFlags.PUBLIC or AccessFlags.STATIC, + setOf("[L"), + arrayOf( + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ) + ) + ) + } + + @Test + fun testPatcher() { + val patcher = Patcher( + File(PatcherTest::class.java.getResource("/test1.apk")!!.toURI()), + File("/"), + testSignatures + ) + + patcher.addPatches( + object : Patch("TestPatch") { + override fun execute(cache: Cache): PatchResult { + // Get the result from the resolver cache + val result = cache.resolvedMethods["main-method"] + // Get the implementation for the resolved method + val implementation = result.resolveAndGetMethod().implementation!! + // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." + // Get the start index of our opcode pattern. + // This will be the index of the instruction with the opcode CONST_STRING. + val startIndex = result.scanData.startIndex + + // the instruction format can be found via the docs at https://source.android.com/devices/tech/dalvik/dalvik-bytecode + // in our case we want an instruction with the opcode CONST_STRING and the string "Hello, ReVanced! Editing bytecode." + // the format is 21c, so we create a new BuilderInstruction21c + // with the opcode CONST_STRING and the string "Hello, ReVanced! Editing bytecode." + // This instruction will store the constant string reference in the register v1 + // For that a reference to the string is needed. It can be created by creating a ImmutableStringReference + val stringInstruction1 = BuilderInstruction21c( + Opcode.CONST_STRING, + 1, + ImmutableStringReference("Hello, ReVanced! Editing bytecode.") + ) + + // Replace the instruction at index startIndex with a new instruction + // We make sure to use this method to handle references to it via labels in any case + // If we are sure that the instruction is not referenced by any label, we can use the index operator overload + // of the instruction list: + // implementation.instructions[startIndex] = instruction + implementation.replaceInstruction(startIndex, stringInstruction1) + + // Now lets print our string twice! + + // Create the necessary instructions (we could also clone the existing ones) + val stringInstruction2 = BuilderInstruction21c( + Opcode.CONST_STRING, + 1, + ImmutableStringReference("Hello, ReVanced! Adding bytecode.") + ) + val invokeInstruction = BuilderInstruction35c( + Opcode.INVOKE_VIRTUAL, + 2, 0, 1, 0, 0, 0, + ImmutableMethodReference( + "Ljava.io.PrintStream;", + "println", + setOf("Ljava/lang/String;"), + "V" + ) + ) + + // Insert our instructions after the second instruction by our pattern. + implementation.addInstruction(startIndex + 1, stringInstruction2) + implementation.addInstruction(startIndex + 3, invokeInstruction) + + // Finally, tell the patcher that this patch was a success. + // You can also return PatchResultError with a message. + // If an exception is thrown inside this function, + // a PatchResultError will be returned with the error message. + return PatchResultSuccess() + } + } + ) + + // Apply all patches loaded in the patcher + val patchResult = patcher.applyPatches() + // You can check if an error occurred + for ((patchName, result) in patchResult) { + if (result.isFailure) { + throw Exception("Patch $patchName failed", result.exceptionOrNull()!!) + } + } + + patcher.save() + } + + @Test + fun `test patcher with no changes`() { + Patcher( + File(PatcherTest::class.java.getResource("/test1.apk")!!.toURI()), + File("/no-changes-test"), + testSignatures + ).save() + // FIXME(Sculas): There seems to be a 1-byte difference, not sure what it is. + // assertEquals(available, out.size()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/patcher/PatcherTest.kt b/src/test/kotlin/patcher/PatcherTest.kt deleted file mode 100644 index 77aab4c5..00000000 --- a/src/test/kotlin/patcher/PatcherTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -package patcher - -import app.revanced.patcher.Patcher -import app.revanced.patcher.cache.Cache -import app.revanced.patcher.extensions.or -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultError -import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.signature.MethodSignature -import app.revanced.patcher.smali.asInstruction -import org.jf.dexlib2.AccessFlags -import org.jf.dexlib2.Opcode -import org.jf.dexlib2.builder.instruction.BuilderInstruction21t -import org.jf.dexlib2.builder.instruction.BuilderInstruction35c -import org.jf.dexlib2.iface.reference.MethodReference -import org.jf.dexlib2.immutable.reference.ImmutableMethodReference -import java.io.File -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals - -fun main() { - val signatures = arrayOf( - MethodSignature( - "main-method", - "V", - AccessFlags.STATIC or AccessFlags.PUBLIC, - listOf("[O"), - arrayOf( - Opcode.SGET_OBJECT, - Opcode.CONST_STRING, - Opcode.INVOKE_VIRTUAL, - Opcode.RETURN_VOID - ) - ) - ) - - val patcher = Patcher( - File("black.apk"), - File("./"), - signatures - ) - - patcher.addPatches( - object : Patch("main-method-patch-via-proxy") { - override fun execute(cache: Cache): PatchResult { - val proxy = cache.findClass("XAdRemover") - ?: return PatchResultError("Class 'XAdRemover' could not be found") - - val xAdRemoverClass = proxy.resolve() - val hideReelMethod = xAdRemoverClass.methods.find { - it.name.contains("HideReel") - }!! - - val implementation = hideReelMethod.implementation!! - - val readSettingsInstructionCompiled = - "invoke-static { }, Lfi/razerman/youtube/XGlobals;->ReadSettings()V" - .asInstruction() as BuilderInstruction35c - val readSettingsInstructionAssembled = BuilderInstruction35c( - Opcode.INVOKE_STATIC, - 0, 0, 0, 0, 0, 0, - ImmutableMethodReference( - "Lfi/razerman/youtube/XGlobals;", - "ReadSettings", - emptyList(), - "V" - ) - ) - - assertEquals(readSettingsInstructionAssembled.opcode, readSettingsInstructionCompiled.opcode) - assertEquals( - readSettingsInstructionAssembled.referenceType, - readSettingsInstructionCompiled.referenceType - ) - assertEquals( - readSettingsInstructionAssembled.registerCount, - readSettingsInstructionCompiled.registerCount - ) - assertEquals(readSettingsInstructionAssembled.registerC, readSettingsInstructionCompiled.registerC) - assertEquals(readSettingsInstructionAssembled.registerD, readSettingsInstructionCompiled.registerD) - assertEquals(readSettingsInstructionAssembled.registerE, readSettingsInstructionCompiled.registerE) - assertEquals(readSettingsInstructionAssembled.registerF, readSettingsInstructionCompiled.registerF) - assertEquals(readSettingsInstructionAssembled.registerG, readSettingsInstructionCompiled.registerG) - run { - val compiledRef = readSettingsInstructionCompiled.reference as MethodReference - val assembledRef = readSettingsInstructionAssembled.reference as MethodReference - - assertEquals(assembledRef.name, compiledRef.name) - assertEquals(assembledRef.definingClass, compiledRef.definingClass) - assertEquals(assembledRef.returnType, compiledRef.returnType) - assertContentEquals(assembledRef.parameterTypes, compiledRef.parameterTypes) - } - - implementation.addInstruction( - 21, - readSettingsInstructionCompiled - ) - - // fix labels - // create a new label for the instruction we want to jump to - val newLabel = implementation.newLabelForIndex(21) - // replace all instances of the old label with the new one - implementation.replaceInstruction(4, BuilderInstruction21t(Opcode.IF_NEZ, 0, newLabel)) - return PatchResultSuccess() - } - }, - object : Patch("main-method-patch-via-signature") { - override fun execute(cache: Cache): PatchResult { - val mainMethodMap = cache.resolvedMethods["main-method"] - mainMethodMap.definingClassProxy.immutableClass.methods.single { method -> - method.name == mainMethodMap.resolvedMethodName - } - return PatchResultSuccess() - } - } - ) - - patcher.applyPatches() - patcher.save() -}