Skip to content

Commit 715a2ad

Browse files
SculasoSumAtrIX
authored andcommittedJun 5, 2022
feat: Add warnings for Fuzzy resolver
1 parent 9889ec9 commit 715a2ad

File tree

5 files changed

+121
-22
lines changed

5 files changed

+121
-22
lines changed
 

‎src/main/kotlin/app/revanced/patcher/Patcher.kt

+20-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import app.revanced.patcher.patch.Patch
44
import app.revanced.patcher.patch.PatchMetadata
55
import app.revanced.patcher.patch.PatchResultSuccess
66
import app.revanced.patcher.proxy.ClassProxy
7+
import app.revanced.patcher.signature.MethodSignature
78
import app.revanced.patcher.signature.resolver.SignatureResolver
89
import app.revanced.patcher.util.ListBackedSet
910
import lanchon.multidexlib2.BasicDexFileNamer
@@ -116,22 +117,37 @@ class Patcher(
116117
patcherData.patches.addAll(patches)
117118
}
118119

120+
/**
121+
* Resolves all signatures.
122+
* @throws IllegalStateException if no patches were added or signatures have already been resolved.
123+
*/
124+
fun resolveSignatures(): List<MethodSignature> {
125+
if (signaturesResolved) {
126+
throw IllegalStateException("Signatures have already been resolved.")
127+
}
128+
val signatures = patcherData.patches.flatMap { it.signatures }
129+
if (signatures.isEmpty()) {
130+
throw IllegalStateException("No signatures found to resolve.")
131+
}
132+
SignatureResolver(patcherData.classes, signatures).resolve()
133+
signaturesResolved = true
134+
return signatures
135+
}
136+
119137
/**
120138
* Apply patches loaded into the patcher.
121139
* @param stopOnError If true, the patches will stop on the first error.
122140
* @return A map of [PatchResultSuccess]. If the [Patch] was successfully applied,
123141
* [PatchResultSuccess] will always be returned to the wrapping Result object.
124142
* If the [Patch] failed to apply, an Exception will always be returned to the wrapping Result object.
143+
* @throws IllegalStateException if signatures have not been resolved.
125144
*/
126145
fun applyPatches(
127146
stopOnError: Boolean = false,
128147
callback: (String) -> Unit = {}
129148
): Map<PatchMetadata, Result<PatchResultSuccess>> {
130-
131149
if (!signaturesResolved) {
132-
val signatures = patcherData.patches.flatMap { it.signatures }
133-
SignatureResolver(patcherData.classes, signatures).resolve()
134-
signaturesResolved = true
150+
throw IllegalStateException("Signatures not yet resolved, please invoke Patcher#resolveSignatures() first.")
135151
}
136152
return buildMap {
137153
for (patch in patcherData.patches) {

‎src/main/kotlin/app/revanced/patcher/signature/MethodSignature.kt

+32-4
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ import org.jf.dexlib2.Opcode
55

66
/**
77
* Represents the [MethodSignature] for a method.
8-
* @param methodSignatureMetadata Metadata for this [MethodSignature].
8+
* @param metadata Metadata for this [MethodSignature].
99
* @param returnType The return type of the method.
1010
* @param accessFlags The access flags of the method.
1111
* @param methodParameters The parameters of the method.
1212
* @param opcodes The list of opcodes of the method.
1313
*/
1414
class MethodSignature(
15-
val methodSignatureMetadata: MethodSignatureMetadata,
15+
val metadata: MethodSignatureMetadata,
1616
internal val returnType: String?,
1717
internal val accessFlags: Int?,
1818
internal val methodParameters: Iterable<String>?,
@@ -24,9 +24,13 @@ class MethodSignature(
2424
var result: SignatureResolverResult? = null // TODO: figure out how to get rid of nullable
2525
get() {
2626
return field ?: throw MethodNotFoundException(
27-
"Could not resolve required signature ${methodSignatureMetadata.name}"
27+
"Could not resolve required signature ${metadata.name}"
2828
)
2929
}
30+
val resolved: Boolean
31+
get() {
32+
return result != null
33+
}
3034
}
3135

3236
/**
@@ -70,5 +74,29 @@ interface PatternScanMethod {
7074
/**
7175
* When comparing the signature, if [threshold] or more of the opcodes do not match, skip.
7276
*/
73-
class Fuzzy(internal val threshold: Int) : PatternScanMethod
77+
class Fuzzy(internal val threshold: Int) : PatternScanMethod {
78+
/**
79+
* A list of warnings the resolver found.
80+
*
81+
* This list will be allocated when the signature has been found.
82+
* Meaning, if the signature was not found,
83+
* or the signature was not yet resolved,
84+
* the list will be null.
85+
*/
86+
lateinit var warnings: List<Warning>
87+
88+
/**
89+
* Represents a resolver warning.
90+
* @param expected The opcode the signature expected it to be.
91+
* @param actual The actual opcode it was. Always different from [expected].
92+
* @param expectedIndex The index for [expected]. Relative to the instruction list.
93+
* @param actualIndex The index for [actual]. Relative to the pattern list from the signature.
94+
*/
95+
data class Warning(
96+
val expected: Opcode,
97+
val actual: Opcode,
98+
val expectedIndex: Int,
99+
val actualIndex: Int,
100+
)
101+
}
74102
}

‎src/main/kotlin/app/revanced/patcher/signature/resolver/SignatureResolver.kt

+31-11
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,26 @@ internal class SignatureResolver(
8383
val count = instructions.count()
8484
val pattern = signature.opcodes!!
8585
val size = pattern.count()
86-
var threshold = 0
87-
if (signature.methodSignatureMetadata.patternScanMethod is PatternScanMethod.Fuzzy) {
88-
threshold = signature.methodSignatureMetadata.patternScanMethod.threshold
89-
}
86+
val method = signature.metadata.patternScanMethod
87+
val threshold = if (method is PatternScanMethod.Fuzzy)
88+
method.threshold else 0
9089

9190
for (instructionIndex in 0 until count) {
9291
var patternIndex = 0
9392
var currentThreshold = threshold
9493
while (instructionIndex + patternIndex < count) {
95-
if (
96-
instructions.elementAt(
97-
instructionIndex + patternIndex
98-
).opcode != pattern.elementAt(patternIndex)
99-
&& currentThreshold-- == 0
100-
) break
94+
val originalOpcode = instructions.elementAt(instructionIndex + patternIndex).opcode
95+
val patternOpcode = pattern.elementAt(patternIndex)
96+
if (originalOpcode != patternOpcode && currentThreshold-- == 0) break
10197
if (++patternIndex < size) continue
10298

103-
return PatternScanResult(instructionIndex, instructionIndex + patternIndex)
99+
val result = PatternScanResult(instructionIndex, instructionIndex + patternIndex)
100+
if (method is PatternScanMethod.Fuzzy) {
101+
method.warnings = generateWarnings(
102+
signature, instructions, result
103+
)
104+
}
105+
return result
104106
}
105107
}
106108

@@ -113,6 +115,24 @@ internal class SignatureResolver(
113115
): Boolean {
114116
return signature.count() != original.size || !(signature.all { a -> original.any { it.startsWith(a) } })
115117
}
118+
119+
private fun generateWarnings(
120+
signature: MethodSignature,
121+
instructions: Iterable<Instruction>,
122+
scanResult: PatternScanResult,
123+
) = buildList {
124+
val pattern = signature.opcodes!!
125+
for ((patternIndex, originalIndex) in (scanResult.startIndex until scanResult.endIndex).withIndex()) {
126+
val originalOpcode = instructions.elementAt(originalIndex).opcode
127+
val patternOpcode = pattern.elementAt(patternIndex)
128+
if (originalOpcode != patternOpcode) {
129+
this.add(PatternScanMethod.Fuzzy.Warning(
130+
originalOpcode, patternOpcode,
131+
originalIndex, patternIndex
132+
))
133+
}
134+
}
135+
}
116136
}
117137
}
118138

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package app.revanced.patcher
2+
3+
import app.revanced.patcher.signature.PatternScanMethod
4+
import app.revanced.patcher.usage.ExamplePatch
5+
import org.junit.jupiter.api.Test
6+
import java.io.File
7+
import kotlin.test.assertTrue
8+
9+
internal class PatcherTest {
10+
@Test
11+
fun testPatcher() {
12+
val patcher = Patcher(File(PatcherTest::class.java.getResource("/test1.dex")!!.toURI()))
13+
patcher.addPatches(listOf(ExamplePatch()))
14+
for (signature in patcher.resolveSignatures()) {
15+
if (!signature.resolved) {
16+
throw Exception("Signature ${signature.metadata.name} was not resolved!")
17+
}
18+
val patternScanMethod = signature.metadata.patternScanMethod
19+
if (patternScanMethod is PatternScanMethod.Fuzzy) {
20+
val warnings = patternScanMethod.warnings
21+
println("Signature ${signature.metadata.name} had ${warnings.size} warnings!")
22+
for (warning in warnings) {
23+
println(warning.toString())
24+
}
25+
}
26+
}
27+
for ((metadata, result) in patcher.applyPatches()) {
28+
if (result.isFailure) {
29+
throw Exception("Patch ${metadata.shortName} failed", result.exceptionOrNull()!!)
30+
} else {
31+
println("Patch ${metadata.shortName} applied successfully!")
32+
}
33+
}
34+
val out = patcher.save()
35+
assertTrue(out.isNotEmpty(), "Expected the output of Patcher#save() to not be empty.")
36+
}
37+
}

‎src/test/kotlin/app/revanced/patcher/usage/ExamplePatch.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import org.jf.dexlib2.immutable.reference.ImmutableStringReference
3131
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
3232
import org.jf.dexlib2.util.Preconditions
3333

34-
@Suppress("unused") // TODO: Add tests
3534
class ExamplePatch : Patch(
3635
metadata = PatchMetadata(
3736
shortName = "example-patch",
@@ -48,7 +47,7 @@ class ExamplePatch : Patch(
4847
definingClass = "TestClass",
4948
name = "main",
5049
),
51-
patternScanMethod = PatternScanMethod.Fuzzy(2),
50+
patternScanMethod = PatternScanMethod.Fuzzy(1),
5251
compatiblePackages = listOf("com.example.examplePackage"),
5352
description = "The main method of TestClass",
5453
version = "1.0.0"
@@ -67,7 +66,6 @@ class ExamplePatch : Patch(
6766
// This function will be executed by the patcher.
6867
// You can treat it as a constructor
6968
override fun execute(patcherData: PatcherData): PatchResult {
70-
7169
// Get the resolved method for the signature from the resolver cache
7270
val result = signatures.first().result!!
7371

0 commit comments

Comments
 (0)
Please sign in to comment.