diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index 82050d3..3d03ba5 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -4,6 +4,7 @@ import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.PatchMetadata import app.revanced.patcher.patch.PatchResultSuccess import app.revanced.patcher.proxy.ClassProxy +import app.revanced.patcher.signature.MethodSignature import app.revanced.patcher.signature.resolver.SignatureResolver import app.revanced.patcher.util.ListBackedSet import lanchon.multidexlib2.BasicDexFileNamer @@ -116,22 +117,37 @@ class Patcher( patcherData.patches.addAll(patches) } + /** + * Resolves all signatures. + * @throws IllegalStateException if no patches were added or signatures have already been resolved. + */ + fun resolveSignatures(): List { + if (signaturesResolved) { + throw IllegalStateException("Signatures have already been resolved.") + } + val signatures = patcherData.patches.flatMap { it.signatures } + if (signatures.isEmpty()) { + throw IllegalStateException("No signatures found to resolve.") + } + SignatureResolver(patcherData.classes, signatures).resolve() + signaturesResolved = true + return signatures + } + /** * Apply patches loaded into the patcher. * @param stopOnError If true, the patches will stop on the first error. * @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. + * @throws IllegalStateException if signatures have not been resolved. */ fun applyPatches( stopOnError: Boolean = false, callback: (String) -> Unit = {} ): Map> { - if (!signaturesResolved) { - val signatures = patcherData.patches.flatMap { it.signatures } - SignatureResolver(patcherData.classes, signatures).resolve() - signaturesResolved = true + throw IllegalStateException("Signatures not yet resolved, please invoke Patcher#resolveSignatures() first.") } return buildMap { for (patch in patcherData.patches) { diff --git a/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt index 59b5c25..4ef7272 100644 --- a/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt +++ b/src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt @@ -5,14 +5,14 @@ import org.jf.dexlib2.Opcode /** * Represents the [MethodSignature] for a method. - * @param methodSignatureMetadata Metadata for this [MethodSignature]. + * @param metadata 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 The list of opcodes of the method. */ class MethodSignature( - val methodSignatureMetadata: MethodSignatureMetadata, + val metadata: MethodSignatureMetadata, internal val returnType: String?, internal val accessFlags: Int?, internal val methodParameters: Iterable?, @@ -24,9 +24,13 @@ class MethodSignature( var result: SignatureResolverResult? = null // TODO: figure out how to get rid of nullable get() { return field ?: throw MethodNotFoundException( - "Could not resolve required signature ${methodSignatureMetadata.name}" + "Could not resolve required signature ${metadata.name}" ) } + val resolved: Boolean + get() { + return result != null + } } /** @@ -70,5 +74,29 @@ interface PatternScanMethod { /** * When comparing the signature, if [threshold] or more of the opcodes do not match, skip. */ - class Fuzzy(internal val threshold: Int) : PatternScanMethod + class Fuzzy(internal val threshold: Int) : PatternScanMethod { + /** + * A list of warnings the resolver found. + * + * This list will be allocated when the signature has been found. + * Meaning, if the signature was not found, + * or the signature was not yet resolved, + * the list will be null. + */ + lateinit var warnings: List + + /** + * Represents a resolver warning. + * @param expected The opcode the signature expected it to be. + * @param actual The actual opcode it was. Always different from [expected]. + * @param expectedIndex The index for [expected]. Relative to the instruction list. + * @param actualIndex The index for [actual]. Relative to the pattern list from the signature. + */ + data class Warning( + val expected: Opcode, + val actual: Opcode, + val expectedIndex: Int, + val actualIndex: Int, + ) + } } \ 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 a12628c..f541535 100644 --- a/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt +++ b/src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt @@ -83,24 +83,26 @@ internal class SignatureResolver( val count = instructions.count() val pattern = signature.opcodes!! val size = pattern.count() - var threshold = 0 - if (signature.methodSignatureMetadata.patternScanMethod is PatternScanMethod.Fuzzy) { - threshold = signature.methodSignatureMetadata.patternScanMethod.threshold - } + val method = signature.metadata.patternScanMethod + val threshold = if (method is PatternScanMethod.Fuzzy) + method.threshold else 0 for (instructionIndex in 0 until count) { var patternIndex = 0 var currentThreshold = threshold while (instructionIndex + patternIndex < count) { - if ( - instructions.elementAt( - instructionIndex + patternIndex - ).opcode != pattern.elementAt(patternIndex) - && currentThreshold-- == 0 - ) break + val originalOpcode = instructions.elementAt(instructionIndex + patternIndex).opcode + val patternOpcode = pattern.elementAt(patternIndex) + if (originalOpcode != patternOpcode && currentThreshold-- == 0) break if (++patternIndex < size) continue - return PatternScanResult(instructionIndex, instructionIndex + patternIndex) + val result = PatternScanResult(instructionIndex, instructionIndex + patternIndex) + if (method is PatternScanMethod.Fuzzy) { + method.warnings = generateWarnings( + signature, instructions, result + ) + } + return result } } @@ -113,6 +115,24 @@ internal class SignatureResolver( ): Boolean { return signature.count() != original.size || !(signature.all { a -> original.any { it.startsWith(a) } }) } + + private fun generateWarnings( + signature: MethodSignature, + instructions: Iterable, + scanResult: PatternScanResult, + ) = buildList { + val pattern = signature.opcodes!! + for ((patternIndex, originalIndex) in (scanResult.startIndex until scanResult.endIndex).withIndex()) { + val originalOpcode = instructions.elementAt(originalIndex).opcode + val patternOpcode = pattern.elementAt(patternIndex) + if (originalOpcode != patternOpcode) { + this.add(PatternScanMethod.Fuzzy.Warning( + originalOpcode, patternOpcode, + originalIndex, patternIndex + )) + } + } + } } } 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 0000000..5b56ece --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -0,0 +1,37 @@ +package app.revanced.patcher + +import app.revanced.patcher.signature.PatternScanMethod +import app.revanced.patcher.usage.ExamplePatch +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.test.assertTrue + +internal class PatcherTest { + @Test + fun testPatcher() { + val patcher = Patcher(File(PatcherTest::class.java.getResource("/test1.dex")!!.toURI())) + patcher.addPatches(listOf(ExamplePatch())) + for (signature in patcher.resolveSignatures()) { + if (!signature.resolved) { + throw Exception("Signature ${signature.metadata.name} was not resolved!") + } + val patternScanMethod = signature.metadata.patternScanMethod + if (patternScanMethod is PatternScanMethod.Fuzzy) { + val warnings = patternScanMethod.warnings + println("Signature ${signature.metadata.name} had ${warnings.size} warnings!") + for (warning in warnings) { + println(warning.toString()) + } + } + } + for ((metadata, result) in patcher.applyPatches()) { + if (result.isFailure) { + throw Exception("Patch ${metadata.shortName} failed", result.exceptionOrNull()!!) + } else { + println("Patch ${metadata.shortName} applied successfully!") + } + } + val out = patcher.save() + assertTrue(out.isNotEmpty(), "Expected the output of Patcher#save() to not be empty.") + } +} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt index 8b45a15..504a55e 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt +++ b/src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt @@ -31,7 +31,6 @@ import org.jf.dexlib2.immutable.reference.ImmutableStringReference import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue import org.jf.dexlib2.util.Preconditions -@Suppress("unused") // TODO: Add tests class ExamplePatch : Patch( metadata = PatchMetadata( shortName = "example-patch", @@ -48,7 +47,7 @@ class ExamplePatch : Patch( definingClass = "TestClass", name = "main", ), - patternScanMethod = PatternScanMethod.Fuzzy(2), + patternScanMethod = PatternScanMethod.Fuzzy(1), compatiblePackages = listOf("com.example.examplePackage"), description = "The main method of TestClass", version = "1.0.0" @@ -67,7 +66,6 @@ class ExamplePatch : Patch( // 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!!