From 1f08da8b2aa811074cfdf23ea7d752152e06c8e6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 14 Apr 2022 08:48:05 +0200 Subject: [PATCH] refact: include each signature in its corresponding patch Signed-off-by: oSumAtrIX --- .../kotlin/app/revanced/patcher/Patcher.kt | 64 +++--- .../{cache/Cache.kt => PatcherData.kt} | 35 ++-- .../revanced/patcher/extensions/Extensions.kt | 2 +- .../app/revanced/patcher/patch/Patch.kt | 31 ++- .../patcher/signature/MethodSignature.kt | 79 ++++---- .../signature/resolver/SignatureResolver.kt | 15 +- .../app/revanced/patcher/PatcherTest.kt | 190 ------------------ .../revanced/patcher/usage/ExamplePatch.kt | 190 ++++++++++++++++++ 8 files changed, 316 insertions(+), 290 deletions(-) rename src/main/kotlin/app/revanced/patcher/{cache/Cache.kt => PatcherData.kt} (59%) delete mode 100644 src/test/kotlin/app/revanced/patcher/PatcherTest.kt create mode 100644 src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index fc2d33f..c2cbcd8 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,11 +1,9 @@ package app.revanced.patcher -import app.revanced.patcher.cache.Cache -import app.revanced.patcher.cache.findIndexed import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.PatchMetadata import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.signature.MethodSignature +import app.revanced.patcher.proxy.ClassProxy import app.revanced.patcher.signature.resolver.SignatureResolver import app.revanced.patcher.util.ListBackedSet import lanchon.multidexlib2.BasicDexFileNamer @@ -22,21 +20,18 @@ val NAMER = BasicDexFileNamer() /** * ReVanced Patcher. * @param input The input file (an apk or any other multi dex container). - * @param signatures A list of method signatures for the patches. */ class Patcher( input: File, - private val signatures: Iterable, ) { - private val cache: Cache - private val patches = mutableSetOf() + private val patcherData: PatcherData private val opcodes: Opcodes - private var sigsResolved = false + private var signaturesResolved = false init { val dexFile = MultiDexIO.readDexFile(true, input, NAMER, null, null) opcodes = dexFile.opcodes - cache = Cache(dexFile.classes.toMutableList()) + patcherData = PatcherData(dexFile.classes.toMutableList()) } /** @@ -53,18 +48,18 @@ class Patcher( for (file in files) { val dexFile = MultiDexIO.readDexFile(true, file, NAMER, null, null) for (classDef in dexFile.classes) { - val e = cache.classes.findIndexed { it.type == classDef.type } + val e = patcherData.classes.findIndexed { it.type == classDef.type } if (e != null) { if (throwOnDuplicates) { throw Exception("Class ${classDef.type} has already been added to the patcher.") } val (_, idx) = e if (allowedOverwrites.contains(classDef.type)) { - cache.classes[idx] = classDef + patcherData.classes[idx] = classDef } continue } - cache.classes.add(classDef) + patcherData.classes.add(classDef) } } } @@ -74,16 +69,27 @@ class Patcher( */ fun save(): Map { val newDexFile = object : DexFile { + private fun MutableList.replaceWith(proxy: ClassProxy) { + if (proxy.proxyUsed) return + this[proxy.originalIndex] = proxy.mutatedClass + } + override fun getClasses(): Set { - cache.methodMap.values.forEach { - if (it.definingClassProxy.proxyUsed) { - cache.classes[it.definingClassProxy.originalIndex] = it.definingClassProxy.mutatedClass + for (proxy in patcherData.classProxies) { + patcherData.classes.replaceWith(proxy) + } + for (patch in patcherData.patches) { + for (signature in patch.signatures) { + val result = signature.result + result ?: continue + + val proxy = result.definingClassProxy + if (!proxy.proxyUsed) continue + + patcherData.classes.replaceWith(proxy) } } - cache.classProxy.filter { it.proxyUsed }.forEach { proxy -> - cache.classes[proxy.originalIndex] = proxy.mutatedClass - } - return ListBackedSet(cache.classes) + return ListBackedSet(patcherData.classes) } override fun getOpcodes(): Opcodes { @@ -106,29 +112,31 @@ class Patcher( * @param patches The patches to add. */ fun addPatches(patches: Iterable) { - this.patches.addAll(patches) + patcherData.patches.addAll(patches) } /** * Apply patches loaded into the patcher. * @param stopOnError If true, the patches will stop on the first error. - * @return A map of results. If the patch was successfully applied, - * PatchResultSuccess will always be returned in the wrapping Result object. - * If the patch failed to apply, an Exception will always be returned in the wrapping Result object. + * @return A map of [PatchResultSuccess]. If the [Patch] was successfully applied, + * [PatchResultSuccess] will always be returned to the wrapping Result object. + * If the [Patch] failed to apply, an Exception will always be returned to the wrapping Result object. */ fun applyPatches( stopOnError: Boolean = false, callback: (String) -> Unit = {} ): Map> { - if (!sigsResolved) { - SignatureResolver(cache.classes, signatures).resolve(cache.methodMap) - sigsResolved = true + + if (!signaturesResolved) { + val signatures = patcherData.patches.flatMap { it.signatures } + SignatureResolver(patcherData.classes, signatures).resolve() + signaturesResolved = true } return buildMap { - for (patch in patches) { + for (patch in patcherData.patches) { callback(patch.metadata.shortName) val result: Result = try { - val pr = patch.execute(cache) + val pr = patch.execute(patcherData) if (pr.isSuccess()) { Result.success(pr.success()!!) } else { diff --git a/src/main/kotlin/app/revanced/patcher/cache/Cache.kt b/src/main/kotlin/app/revanced/patcher/PatcherData.kt similarity index 59% rename from src/main/kotlin/app/revanced/patcher/cache/Cache.kt rename to src/main/kotlin/app/revanced/patcher/PatcherData.kt index cf8a30f..f28a9b2 100644 --- a/src/main/kotlin/app/revanced/patcher/cache/Cache.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherData.kt @@ -1,18 +1,15 @@ -package app.revanced.patcher.cache +package app.revanced.patcher +import app.revanced.patcher.patch.Patch import app.revanced.patcher.proxy.ClassProxy import app.revanced.patcher.signature.SignatureResolverResult import org.jf.dexlib2.iface.ClassDef -class Cache( +class PatcherData( internal val classes: MutableList, - val methodMap: MethodMap = MethodMap() ) { - // TODO: currently we create ClassProxies at multiple places, which is why we could have merge conflicts - // this can be solved by creating a dedicated method for creating class proxies, - // if the class proxy already exists in the cached proxy list below. - // The to-do in the method findClass is related - internal val classProxy = mutableSetOf() + internal val classProxies = mutableSetOf() + internal val patches = mutableSetOf() /** * Find a class by a given class name @@ -25,23 +22,23 @@ class Cache( * @return A proxy for the first class that matches the predicate */ fun findClass(predicate: (ClassDef) -> Boolean): ClassProxy? { - // TODO: find a cleaner way to store all proxied classes. - // Currently we have to search the method map as well as the class proxy list which is not elegant + // if we already proxied the class matching the predicate... + for (patch in patches) { + for (signature in patch.signatures) { + val result = signature.result + result ?: continue - // if we already proxied the class matching the predicate, - val proxiedClass = classProxy.find { predicate(it.immutableClass) } - // return that proxy - if (proxiedClass != null) return proxiedClass - // if we already have the class matching the predicate in the method map, - val result = methodMap.entries.find { predicate(it.value.definingClassProxy.immutableClass) }?.value - if (result != null) return result.definingClassProxy + if (predicate(result.definingClassProxy.immutableClass)) + return result.definingClassProxy // ...then return that proxy + } + } // else search the original class list val (foundClass, index) = classes.findIndexed(predicate) ?: return null // create a class proxy with the index of the class in the classes list val classProxy = ClassProxy(foundClass, index) // add it to the cache and - this.classProxy.add(classProxy) + this.classProxies.add(classProxy) // return the proxy class return classProxy } @@ -55,7 +52,7 @@ class MethodMap : LinkedHashMap() { internal class MethodNotFoundException(s: String) : Exception(s) -internal inline fun Iterable.find(predicate: (T) -> Boolean): T? { +internal inline fun Iterable.find(predicate: (T) -> Boolean): T? { for (element in this) { if (predicate(element)) { return element diff --git a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt index d50b2ac..07c20d3 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt @@ -15,4 +15,4 @@ fun MutableMethodImplementation.addInstructions(index: Int, instructions: List +) { + + /** + * The main function of the [Patch] which the patcher will call. + */ + abstract fun execute(patcherData: PatcherData): PatchResult } +/** + * Metadata about a [Patch]. + * @param shortName A suggestive short name for the [Patch]. + * @param name A suggestive name for the [Patch]. + * @param description A description for the [Patch]. + * @param compatiblePackages A list of packages this [Patch] is compatible with. + * @param version The version of the [Patch]. + */ data class PatchMetadata( val shortName: String, - val fullName: String, + val name: String, val description: String, + @Suppress("ArrayInDataClass") val compatiblePackages: Array, + val version: String, ) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt index 1becbaa..2d3810f 100644 --- a/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt +++ b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt @@ -3,65 +3,66 @@ package app.revanced.patcher.signature import org.jf.dexlib2.Opcode /** - * Represents a method signature. - * @param name A suggestive name for the method which the signature was created for. - * @param metadata Metadata about this signature. + * Represents the [MethodSignature] for a method. + * @param methodSignatureMetadata Metadata for this [MethodSignature]. * @param returnType The return type of the method. * @param accessFlags The access flags of the method. * @param methodParameters The parameters of the method. - * @param opcodes A list of opcodes of the method. + * @param opcodes The list of opcodes of the method. */ -data class MethodSignature( +class MethodSignature( + val methodSignatureMetadata: MethodSignatureMetadata, + internal val returnType: String?, + internal val accessFlags: Int?, + internal val methodParameters: Iterable?, + internal val opcodes: Iterable? +) { + /** + * The result of the signature + */ + var result: SignatureResolverResult? = null // TODO: figure out how to get rid of nullable +} + +/** + * Metadata about a [MethodSignature]. + * @param name A suggestive name for the [MethodSignature]. + * @param methodMetadata Metadata about the method for the [MethodSignature]. + * @param patternScanMethod The pattern scanning method the pattern scanner should rely on. + * Can either be [PatternScanMethod.Fuzzy] or [PatternScanMethod.Direct]. + * @param description An optional description of the [MethodSignature]. + * @param compatiblePackages The list of packages the [MethodSignature] is compatible with. + * @param version The version of this signature. + */ +data class MethodSignatureMetadata( val name: String, - val metadata: SignatureMetadata, - val returnType: String?, - val accessFlags: Int?, - val methodParameters: Iterable?, - val opcodes: Iterable? + val methodMetadata: MethodMetadata, + val patternScanMethod: PatternScanMethod, + @Suppress("ArrayInDataClass") val compatiblePackages: Array, + val description: String?, + val version: String ) /** - * Metadata about the signature. - * @param method Metadata about the method for this signature. - * @param patcher Metadata for the Patcher, this contains things like how the Patcher should interpret this signature. - */ -data class SignatureMetadata( - val method: MethodMetadata, - val patcher: PatcherMetadata -) - -/** - * Metadata about the method for this signature. - * @param definingClass The defining class name of the original method. - * @param methodName The name of the original method. - * @param comment A comment about this method and the data above. - * For example, the version this signature was originally made for. + * Metadata about the method for a [MethodSignature]. + * @param definingClass The defining class name of the method. + * @param name A suggestive name for the method which the [MethodSignature] was created for. */ data class MethodMetadata( val definingClass: String?, - val methodName: String?, - val comment: String + val name: String? ) /** - * Metadata for the Patcher, this contains things like how the Patcher should interpret this signature. - * @param resolverMethod The method the resolver should use to resolve the signature. + * The method, the patcher should rely on when scanning the opcode pattern of a [MethodSignature] */ -data class PatcherMetadata( - val resolverMethod: ResolverMethod -) - -/** - * The method the resolver should use to resolve the signature. - */ -interface ResolverMethod { +interface PatternScanMethod { /** * When comparing the signature, if one or more of the opcodes do not match, skip. */ - class Direct : ResolverMethod + class Direct : PatternScanMethod /** * When comparing the signature, if [threshold] or more of the opcodes do not match, skip. */ - class Fuzzy(val threshold: Int) : ResolverMethod + class Fuzzy(internal val threshold: Int) : PatternScanMethod } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt b/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt index e30c3fd..7d09396 100644 --- a/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt +++ b/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt @@ -1,9 +1,8 @@ package app.revanced.patcher.signature.resolver -import app.revanced.patcher.cache.MethodMap import app.revanced.patcher.proxy.ClassProxy import app.revanced.patcher.signature.MethodSignature -import app.revanced.patcher.signature.ResolverMethod +import app.revanced.patcher.signature.PatternScanMethod import app.revanced.patcher.signature.PatternScanResult import app.revanced.patcher.signature.SignatureResolverResult import org.jf.dexlib2.iface.ClassDef @@ -14,19 +13,17 @@ internal class SignatureResolver( private val classes: List, private val methodSignatures: Iterable ) { - fun resolve(methodMap: MethodMap) { + fun resolve() { for ((index, classDef) in classes.withIndex()) { for (signature in methodSignatures) { - if (methodMap.containsKey(signature.name)) { - continue - } + if (signature.result != null) continue for (method in classDef.methods) { val patternScanData = compareSignatureToMethod(signature, method) ?: continue // create class proxy, in case a patch needs mutability val classProxy = ClassProxy(classDef, index) - methodMap[signature.name] = SignatureResolverResult( + signature.result = SignatureResolverResult( classProxy, patternScanData, method.name, @@ -89,8 +86,8 @@ internal class SignatureResolver( val pattern = signature.opcodes!! val size = pattern.count() var threshold = 0 - if (signature.metadata.patcher.resolverMethod is ResolverMethod.Fuzzy) { - threshold = signature.metadata.patcher.resolverMethod.threshold + if (signature.methodSignatureMetadata.patternScanMethod is PatternScanMethod.Fuzzy) { + threshold = signature.methodSignatureMetadata.patternScanMethod.threshold } for (instructionIndex in 0 until count) { diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt deleted file mode 100644 index 2729860..0000000 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ /dev/null @@ -1,190 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.cache.Cache -import app.revanced.patcher.extensions.AccessFlagExtensions.Companion.or -import app.revanced.patcher.extensions.addInstructions -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchMetadata -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.proxy.mutableTypes.MutableField.Companion.toMutable -import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.revanced.patcher.signature.* -import app.revanced.patcher.smali.asInstruction -import app.revanced.patcher.smali.asInstructions -import com.google.common.collect.ImmutableList -import org.jf.dexlib2.AccessFlags -import org.jf.dexlib2.Opcode -import org.jf.dexlib2.builder.instruction.BuilderInstruction11x -import org.jf.dexlib2.builder.instruction.BuilderInstruction21c -import org.jf.dexlib2.immutable.ImmutableField -import org.jf.dexlib2.immutable.ImmutableMethod -import org.jf.dexlib2.immutable.ImmutableMethodImplementation -import org.jf.dexlib2.immutable.reference.ImmutableFieldReference -import org.jf.dexlib2.immutable.reference.ImmutableStringReference -import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue -import org.junit.jupiter.api.Test -import java.io.File -import kotlin.test.assertTrue - -internal class PatcherTest { - companion object { - val testSignatures = listOf( - MethodSignature( - "main-method", - SignatureMetadata( - method = MethodMetadata( - definingClass = "TestClass", - methodName = "main", - comment = "Main method of TestClass. Version 1.0.0" - ), - patcher = PatcherMetadata( - resolverMethod = ResolverMethod.Fuzzy(2) - ) - ), - "V", - AccessFlags.PUBLIC or AccessFlags.STATIC or AccessFlags.STATIC, - listOf("[L"), - listOf( - Opcode.CONST_STRING, - Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver. - Opcode.RETURN_VOID - ) - ) - ) - } - - @Test - fun testPatcher() { - val patcher = Patcher( - File(PatcherTest::class.java.getResource("/test1.dex")!!.toURI()), - testSignatures - ) - - patcher.addPatches(listOf( - object : Patch(PatchMetadata( - "test-patch", - "My Test Patch", - "A very good description." - )) { - override fun execute(cache: Cache): PatchResult { - // Get the result from the resolver cache - val result = cache.methodMap["main-method"] - // Get the implementation for the resolved method - val implementation = result.method.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 - - // Replace the instruction at index startIndex with a new instruction. - // The instruction format can be found in 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! Adding bytecode.". - // The format is 21c, so we create a new BuilderInstruction21c - // This instruction will hold the string reference constant in the virtual register 1. - // For that a reference to the string is needed. It can be created with an ImmutableStringReference. - // At last, use the method replaceInstruction to replace it at the given index startIndex. - implementation.replaceInstruction( - startIndex, - BuilderInstruction21c( - Opcode.CONST_STRING, - 1, - ImmutableStringReference("Hello, ReVanced! Editing bytecode.") - ) - ) - - // Get the class in which the method matching our signature is defined in. - val mainClass = cache.findClass { - it.type == result.definingClassProxy.immutableClass.type - }!!.resolve() - - // Add a new method returning a string - mainClass.methods.add( - ImmutableMethod( - result.definingClassProxy.immutableClass.type, - "returnHello", - null, - "Ljava/lang/String;", - AccessFlags.PRIVATE or AccessFlags.STATIC, - null, - null, - ImmutableMethodImplementation( - 1, - ImmutableList.of( - BuilderInstruction21c( - Opcode.CONST_STRING, - 0, - ImmutableStringReference("Hello, ReVanced! Adding bytecode.") - ), - BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) - ), - null, - null - ) - ).toMutable() - ) - - // Add a field in the main class - // We will use this field in our method below to call println on - // The field holds the Ljava/io/PrintStream->out; field - mainClass.fields.add( - ImmutableField( - mainClass.type, - "dummyField", - "Ljava/io/PrintStream;", - AccessFlags.PRIVATE or AccessFlags.STATIC, - ImmutableFieldEncodedValue( - ImmutableFieldReference( - "Ljava/lang/System;", - "out", - "Ljava/io/PrintStream;" - ) - ), - null, - null - ).toMutable() - ) - - // store the fields initial value into the first virtual register - implementation.replaceInstruction( - 0, - "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;".asInstruction() - ) - - // Now let's create a new call to our method and print the return value! - // You can also use the smali compiler to create instructions. - // For this sake of example I reuse the TestClass field dummyField inside the virtual register 0. - // - // Control flow instructions are not supported as of now. - val instructions = """ - invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; - move-result-object v1 - invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V - """.trimIndent().asInstructions() - implementation.addInstructions(startIndex + 2, instructions) - - // 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()!!) - } - } - - val out = patcher.save() - assertTrue(out.isNotEmpty(), "Expected the output of Patcher#save() to not be empty.") - } -} diff --git a/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt new file mode 100644 index 0000000..29db4cd --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt @@ -0,0 +1,190 @@ +package app.revanced.patcher.usage + +import app.revanced.patcher.PatcherData +import app.revanced.patcher.extensions.AccessFlagExtensions.Companion.or +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchMetadata +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.signature.MethodMetadata +import app.revanced.patcher.signature.MethodSignature +import app.revanced.patcher.signature.MethodSignatureMetadata +import app.revanced.patcher.signature.PatternScanMethod +import app.revanced.patcher.smali.asInstruction +import app.revanced.patcher.smali.asInstructions +import com.google.common.collect.ImmutableList +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.Format +import org.jf.dexlib2.Opcode +import org.jf.dexlib2.builder.MutableMethodImplementation +import org.jf.dexlib2.builder.instruction.BuilderInstruction11x +import org.jf.dexlib2.builder.instruction.BuilderInstruction21c +import org.jf.dexlib2.iface.instruction.formats.Instruction21c +import org.jf.dexlib2.immutable.ImmutableField +import org.jf.dexlib2.immutable.ImmutableMethod +import org.jf.dexlib2.immutable.ImmutableMethodImplementation +import org.jf.dexlib2.immutable.reference.ImmutableFieldReference +import org.jf.dexlib2.immutable.reference.ImmutableStringReference +import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue +import org.jf.dexlib2.util.Preconditions + +class ExamplePatch : Patch( + metadata = PatchMetadata( + shortName = "example-patch", + name = "ReVanced example patch", + description = "A demonstrative patch to feature the core features of the ReVanced patcher", + compatiblePackages = arrayOf("com.example.examplePackage"), + version = "0.0.1" + ), + signatures = setOf( + MethodSignature( + MethodSignatureMetadata( + name = "Example signature", + methodMetadata = MethodMetadata( + definingClass = "TestClass", + name = "main", + ), + patternScanMethod = PatternScanMethod.Fuzzy(2), + compatiblePackages = arrayOf("com.example.examplePackage"), + description = "The main method of TestClass", + version = "1.0.0" + ), + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC or AccessFlags.STATIC, + methodParameters = listOf("[L"), + opcodes = listOf( + Opcode.CONST_STRING, + Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver. + Opcode.RETURN_VOID + ) + ) + ) +) { + // This function will be executed by the patcher. + // You can treat it as a constructor + override fun execute(patcherData: PatcherData): PatchResult { + + // Get the resolved method for the signature from the resolver cache + val result = signatures.first().result!! + + // Get the implementation for the resolved method + val implementation = result.method.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 + + implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") + + // Get the class in which the method matching our signature is defined in. + val mainClass = patcherData.findClass { + it.type == result.definingClassProxy.immutableClass.type + }!!.resolve() + + // Add a new method returning a string + mainClass.methods.add( + ImmutableMethod( + result.definingClassProxy.immutableClass.type, + "returnHello", + null, + "Ljava/lang/String;", + AccessFlags.PRIVATE or AccessFlags.STATIC, + null, + null, + ImmutableMethodImplementation( + 1, + ImmutableList.of( + BuilderInstruction21c( + Opcode.CONST_STRING, + 0, + ImmutableStringReference("Hello, ReVanced! Adding bytecode.") + ), + BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) + ), + null, + null + ) + ).toMutable() + ) + + // Add a field in the main class + // We will use this field in our method below to call println on + // The field holds the Ljava/io/PrintStream->out; field + mainClass.fields.add( + ImmutableField( + mainClass.type, + "dummyField", + "Ljava/io/PrintStream;", + AccessFlags.PRIVATE or AccessFlags.STATIC, + ImmutableFieldEncodedValue( + ImmutableFieldReference( + "Ljava/lang/System;", + "out", + "Ljava/io/PrintStream;" + ) + ), + null, + null + ).toMutable() + ) + + // store the fields initial value into the first virtual register + implementation.replaceInstruction( + 0, + "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;".asInstruction() + ) + + // Now let's create a new call to our method and print the return value! + // You can also use the smali compiler to create instructions. + // For this sake of example I reuse the TestClass field dummyField inside the virtual register 0. + // + // Control flow instructions are not supported as of now. + val instructions = """ + invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; + move-result-object v1 + invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + """.trimIndent().asInstructions() + implementation.addInstructions(startIndex + 2, instructions) + + // 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() + } + + /** + * Replace the string for an instruction at the given index with a new one. + * @param index The index of the instruction to replace the string for + * @param string The replacing string + */ + private fun MutableMethodImplementation.replaceStringAt(index: Int, string: String) { + val instruction = this.instructions[index] + + // Utility method of dexlib2 + Preconditions.checkFormat(instruction.opcode, Format.Format21c) + + // Cast this to an instruction of the format 21c + // The instruction format can be found in the docs at + // https://source.android.com/devices/tech/dalvik/dalvik-bytecode + val strInstruction = instruction as Instruction21c + + // In our case we want an instruction with the opcode CONST_STRING + // The format is 21c, so we create a new BuilderInstruction21c + // This instruction will hold the string reference constant in the virtual register of the original instruction + // For that a reference to the string is needed. It can be created with an ImmutableStringReference. + // At last, use the method replaceInstruction to replace it at the given index startIndex. + this.replaceInstruction( + index, + BuilderInstruction21c( + Opcode.CONST_STRING, + strInstruction.registerA, + ImmutableStringReference(string) + ) + ) + } +}