Skip to content

Commit

Permalink
feat: Add warnings for Fuzzy resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
Sculas authored and oSumAtrIX committed Jun 5, 2022
1 parent 9889ec9 commit 715a2ad
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 22 deletions.
24 changes: 20 additions & 4 deletions src/main/kotlin/app/revanced/patcher/Patcher.kt
Expand Up @@ -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
Expand Down Expand Up @@ -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<MethodSignature> {
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<PatchMetadata, Result<PatchResultSuccess>> {

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) {
Expand Down
36 changes: 32 additions & 4 deletions src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt
Expand Up @@ -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<String>?,
Expand All @@ -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
}
}

/**
Expand Down Expand Up @@ -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<Warning>

/**
* 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,
)
}
}
Expand Up @@ -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
}
}

Expand All @@ -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<Instruction>,
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
))
}
}
}
}
}

Expand Down
37 changes: 37 additions & 0 deletions 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.")
}
}
4 changes: 1 addition & 3 deletions src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt
Expand Up @@ -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",
Expand All @@ -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"
Expand All @@ -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!!

Expand Down

0 comments on commit 715a2ad

Please sign in to comment.