1
mirror of https://github.com/revanced/revanced-patcher synced 2025-03-21 19:04:21 +01:00

feat: Move fingerprint match members to fingerprint for ease of access by using context receivers

This commit is contained in:
oSumAtrIX 2024-11-04 02:24:16 +01:00
parent 7f55868e6f
commit 0746c22743
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
9 changed files with 324 additions and 217 deletions

@ -1,4 +1,22 @@
public final class app/revanced/patcher/Fingerprint { public final class app/revanced/patcher/Fingerprint {
public final fun getClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getOriginalMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getPatternMatch (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getPatternMatchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun getStringMatchesOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
} }
public final class app/revanced/patcher/FingerprintBuilder { public final class app/revanced/patcher/FingerprintBuilder {
@ -31,13 +49,11 @@ public final class app/revanced/patcher/Match {
} }
public final class app/revanced/patcher/Match$PatternMatch { public final class app/revanced/patcher/Match$PatternMatch {
public fun <init> (II)V
public final fun getEndIndex ()I public final fun getEndIndex ()I
public final fun getStartIndex ()I public final fun getStartIndex ()I
} }
public final class app/revanced/patcher/Match$StringMatch { public final class app/revanced/patcher/Match$StringMatch {
public fun <init> (Ljava/lang/String;I)V
public final fun getIndex ()I public final fun getIndex ()I
public final fun getString ()Ljava/lang/String; public final fun getString ()Ljava/lang/String;
} }
@ -146,10 +162,6 @@ public final class app/revanced/patcher/patch/BytecodePatchContext : app/revance
public synthetic fun get ()Ljava/lang/Object; public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/util/Set; public fun get ()Ljava/util/Set;
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList; public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
public final fun getMatch (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Match;
public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator; public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy; public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
} }
@ -468,12 +480,12 @@ public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w
} }
public final class app/revanced/patcher/util/MethodNavigator { public final class app/revanced/patcher/util/MethodNavigator {
public final fun at (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun at ([I)Lapp/revanced/patcher/util/MethodNavigator;
public static synthetic fun at$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method; public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun to (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun to ([I)Lapp/revanced/patcher/util/MethodNavigator;
public static synthetic fun to$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
} }
public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList { public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList {

@ -56,6 +56,8 @@ dependencies {
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs = listOf("-Xcontext-receivers")
} }
} }

@ -117,15 +117,19 @@ With this information, the original code can be reconstructed:
```java ```java
package com.some.app.ads; package com.some.app.ads;
<accessFlags> class AdsLoader { <accessFlags>
public final boolean <methodName>(boolean <parameter>) {
class AdsLoader {
public final boolean <methodName>(boolean <parameter>)
{
// ... // ...
var userStatus = "pro"; var userStatus = "pro";
// ... // ...
return <returnValue>; return <returnValue >;
} }
} }
``` ```
@ -134,13 +138,14 @@ Using that fingerprint, this method can be matched uniquely from all other metho
> [!TIP] > [!TIP]
> A fingerprint should contain information about a method likely to remain the same across updates. > A fingerprint should contain information about a method likely to remain the same across updates.
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app. > A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same. > app.
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
> same.
## 🔨 How to use fingerprints ## 🔨 How to use fingerprints
A fingerprint is matched to a method, After declaring a fingerprint, it can be used in a patch to find the method it matches to:
once the `match` property of the fingerprint is accessed in a patch's `execute` scope:
```kt ```kt
val fingerprint = fingerprint { val fingerprint = fingerprint {
@ -149,52 +154,34 @@ val fingerprint = fingerprint {
val patch = bytecodePatch { val patch = bytecodePatch {
execute { execute {
val match = fingerprint.match!! fingerprint.method
} }
} }
``` ```
The fingerprint won't be matched again, if it has already been matched once. The fingerprint won't be matched again, if it has already been matched once, for performance reasons.
This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint: This makes it useful, to share fingerprints between multiple patches,
and let the first executing patch match the fingerprint:
```kt ```kt
// Either of these two patches will match the fingerprint first and the other patch can reuse the match: // Either of these two patches will match the fingerprint first and the other patch can reuse the match:
val mainActivityPatch1 = bytecodePatch { val mainActivityPatch1 = bytecodePatch {
execute { execute {
val match = mainActivityOnCreateFingerprint.match!! mainActivityOnCreateFingerprint.method
} }
} }
val mainActivityPatch2 = bytecodePatch { val mainActivityPatch2 = bytecodePatch {
execute { execute {
val match = mainActivityOnCreateFingerprint.match!! mainActivityOnCreateFingerprint.method
}
}
```
A fingerprint match can also be delegated to a variable for convenience without the need to check for `null`:
```kt
val fingerprint = fingerprint {
// ...
}
val patch = bytecodePatch {
execute {
// Alternative to fingerprint.match ?: throw PatchException("No match found")
val match by fingerprint.match
try {
match.method
} catch (e: PatchException) {
// Handle the exception for example.
}
} }
} }
``` ```
> [!WARNING] > [!WARNING]
> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If such a match is delegated > If the fingerprint can not be matched to any method,
> to a variable, accessing it will raise an exception. > accessing certain properties of the fingerprint will raise an exception.
> Instead, the `orNull` properties can be used to return `null` if no match is found.
> [!TIP] > [!TIP]
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode` > If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
@ -211,47 +198,43 @@ val patch = bytecodePatch {
> ) > )
>} >}
> ``` > ```
>
The match of a fingerprint contains references to the original method and class definition of the method:
```kt The following properties can be accessed in a fingerprint:
class Match(
val originalMethod: Method,
val originalClassDef: ClassDef,
val patternMatch: Match.PatternMatch?,
val stringMatches: List<Match.StringMatch>?,
// ...
) {
val classDef by lazy { /* ... */ }
val method by lazy { /* ... */ }
// ... - `originalClassDef`: The original class definition the fingerprint matches to.
} - `originalClassDefOrNull`: The original class definition the fingerprint matches to.
``` - `originalMethod`: The original method the fingerprint matches to.
- `originalMethodOrNull`: The original method the fingerprint matches to.
- `classDef`: The class the fingerprint matches to.
- `classDefOrNull`: The class the fingerprint matches to.
- `method`: The method the fingerprint matches to. If no match is found, an exception is raised.
- `methodOrNull`: The method the fingerprint matches to.
The `classDef` and `method` properties can be used to make changes to the class or method. The difference between the `original` and non-`original` properties is that the `original` properties return the
They are lazy properties, so they are only computed original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
and will effectively replace the original method or class definition when accessed. The mutable copies can be modified. They are lazy properties, so they are only computed
and only then will effectively replace the `original` method or class definition when accessed.
> [!TIP] > [!TIP]
> If only read-only access to the class or method is needed, > If only read-only access to the class or method is needed,
> the `originalClassDef` and `originalMethod` properties can be used, > the `originalClassDef` and `originalMethod` properties should be used,
> to avoid making a mutable copy of the class or method. > to avoid making a mutable copy of the class or method.
## 🏹 Manually matching fingerprints ## 🏹 Manually matching fingerprints
By default, a fingerprint is matched automatically against all classes when the `match` property is accessed. By default, a fingerprint is matched automatically against all classes
when one of the fingerprint's properties is accessed.
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function: Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function:
- In a **list of classes**, if the fingerprint can match in a known subset of classes - In a **list of classes**, if the fingerprint can match in a known subset of classes
If you have a known list of classes you know the fingerprint can match in, If you have a known list of classes you know the fingerprint can match in,
you can match the fingerprint on the list of classes: you can match the fingerprint on the list of classes:
```kt ```kt
execute { execute {
val match = showAdsFingerprint.match(classes) ?: throw PatchException("No match found") val match = showAdsFingerprint(classes)
} }
``` ```
@ -263,23 +246,24 @@ you can match the fingerprint on the list of classes:
execute { execute {
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }
val match = showAdsFingerprint.match(context, adsLoaderClass) ?: throw PatchException("No match found") val match = showAdsFingerprint.match(adsLoaderClass)
} }
``` ```
Another common usecase is to use a fingerprint to reduce the search space of a method to a single class. Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.
```kt ```kt
execute { execute {
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint. // Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
val match by showAdsFingerprint.match(adsLoaderClassFingerprint.match!!.classDef) val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
} }
``` ```
- Match a **single method**, to extract certain information about it - Match a **single method**, to extract certain information about it
The match of a fingerprint contains useful information about the method, The match of a fingerprint contains useful information about the method,
such as the start and end index of an opcode pattern or the indices of the instructions with certain string references. such as the start and end index of an opcode pattern or the indices of the instructions with certain string
references.
A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out: A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
```kt ```kt
@ -288,14 +272,19 @@ you can match the fingerprint on the list of classes:
strings("free", "trial") strings("free", "trial")
} }
currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match -> currentPlanFingerprint.match(adsFingerprint.method).let { match ->
match.stringMatches.forEach { match -> match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}") println("The index of the string '${match.string}' is ${match.index}")
} }
} ?: throw PatchException("No match found") }
} }
``` ```
> [!WARNING]
> If the fingerprint can not be matched to any method, calling `match` will raise an
> exception.
> Instead, the `orNull` overloads can be used to return `null` if no match is found.
> [!TIP] > [!TIP]
> To see real-world examples of fingerprints, > To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). > check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).

@ -46,17 +46,17 @@ The `navigate(Method)` function allows you to navigate method calls recursively
```kt ```kt
execute { execute {
// Sequentially navigate to the instructions at index 1 within 'someMethod'. // Sequentially navigate to the instructions at index 1 within 'someMethod'.
val method = navigate(someMethod).at(1).original() // original() returns the original immutable method. val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
// Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'. // Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
// stop() returns the mutable copy of the method. // stop() returns the mutable copy of the method.
val method = navigate(someMethod).at(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop() val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
// Alternatively, to stop(), you can delegate the method to a variable. // Alternatively, to stop(), you can delegate the method to a variable.
val method by navigate(someMethod).at(1) val method by navigate(someMethod).to(1)
// You can chain multiple calls to at() to navigate deeper into the method. // You can chain multiple calls to at() to navigate deeper into the method.
val method by navigate(someMethod).at(1).at(2, 3, 4).at(5) val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
} }
``` ```
@ -85,7 +85,7 @@ execute {
The `document` function is used to read and write DOM files. The `document` function is used to read and write DOM files.
```kt ```kt
execute { execute {
document("res/values/strings.xml").use { document -> document("res/values/strings.xml").use { document ->
val element = doc.createElement("string").apply { val element = doc.createElement("string").apply {
textContent = "Hello, World!" textContent = "Hello, World!"
@ -112,5 +112,6 @@ ReVanced Patcher is a powerful library to patch Android applications, offering a
that outlive app updates. Patches make up ReVanced; without you, the community of patch developers, that outlive app updates. Patches make up ReVanced; without you, the community of patch developers,
ReVanced would not be what it is today. We hope that this documentation has been helpful to you ReVanced would not be what it is today. We hope that this documentation has been helpful to you
and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help, and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help,
talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or feature request, talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or
feature request,
ReVanced ReVanced

@ -1,3 +1,3 @@
org.gradle.parallel = true org.gradle.parallel=true
org.gradle.caching = true org.gradle.caching=true
version = 21.0.0-dev.2 version=21.0.0-dev.3

@ -3,8 +3,8 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.MethodClassPairs import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.ClassProxy import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@ -44,17 +44,21 @@ class Fingerprint internal constructor(
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?, internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int, private val fuzzyPatternScanThreshold: Int,
) { ) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/** /**
* The match for this [Fingerprint]. Null if unmatched. * The match for this [Fingerprint]. Null if unmatched.
*/ */
// Backing property for "match" extension in BytecodePatchContext. context(BytecodePatchContext)
@Suppress("ktlint:standard:backing-property-naming", "PropertyName") private val matchOrNull: Match?
internal var _match: Match? = null get() = matchOrNull()
/** /**
* Match using [BytecodePatchContext.LookupMaps]. * Match using [BytecodePatchContext.lookupMaps].
* *
* Generally faster than the other [_match] overloads when there are many methods to check for a match. * Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match.
* *
* Fingerprints can be optimized for performance: * Fingerprints can be optimized for performance:
* - Slowest: Specify [custom] or [opcodes] and nothing else. * - Slowest: Specify [custom] or [opcodes] and nothing else.
@ -62,29 +66,28 @@ class Fingerprint internal constructor(
* - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
* *
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/ */
internal fun match(context: BytecodePatchContext): Match? { context(BytecodePatchContext)
if (_match != null) return _match internal fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
val lookupMaps = context.lookupMaps val lookupMaps = lookupMaps
fun Fingerprint.match(methodClasses: MethodClassPairs): Match? { // Find the first
var match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let { methodClasses ->
methodClasses.forEach { (classDef, method) -> methodClasses.forEach { (classDef, method) ->
val match = match(context, classDef, method) val match = matchOrNull(classDef, method)
if (match != null) return match if (match != null) return@let match
} }
return null null
} }
// TODO: If only one string is necessary, why not use a single string for every fingerprint?
val match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match)
if (match != null) return match if (match != null) return match
context.classes.forEach { classDef -> classes.forEach { classDef ->
val match = match(context, classDef) match = matchOrNull(classDef)
if (match != null) return match if (match != null) return match
} }
@ -95,18 +98,17 @@ class Fingerprint internal constructor(
* Match using a [ClassDef]. * Match using a [ClassDef].
* *
* @param classDef The class to match against. * @param classDef The class to match against.
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/ */
internal fun match( context(BytecodePatchContext)
context: BytecodePatchContext, fun matchOrNull(
classDef: ClassDef, classDef: ClassDef,
): Match? { ): Match? {
if (_match != null) return _match if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) { for (method in classDef.methods) {
val match = match(context, method, classDef) val match = matchOrNull(method, classDef)
if (match != null)return match if (match != null) return match
} }
return null return null
@ -117,28 +119,26 @@ class Fingerprint internal constructor(
* The class is retrieved from the method. * The class is retrieved from the method.
* *
* @param method The method to match against. * @param method The method to match against.
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/ */
internal fun match( context(BytecodePatchContext)
context: BytecodePatchContext, fun matchOrNull(
method: Method, method: Method,
) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass) ) = matchOrNull(method, classBy { method.definingClass == it.type }!!.immutableClass)
/** /**
* Match using a [Method]. * Match using a [Method].
* *
* @param method The method to match against. * @param method The method to match against.
* @param classDef The class the method is a member of. * @param classDef The class the method is a member of.
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/ */
internal fun match( context(BytecodePatchContext)
context: BytecodePatchContext, fun matchOrNull(
method: Method, method: Method,
classDef: ClassDef, classDef: ClassDef,
): Match? { ): Match? {
if (_match != null) return _match if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) { if (returnType != null && !method.returnType.startsWith(returnType)) {
return null return null
@ -243,33 +243,189 @@ class Fingerprint internal constructor(
null null
} }
_match = Match( _matchOrNull = Match(
classDef,
method, method,
patternMatch, patternMatch,
stringMatches, stringMatches,
context, classDef,
) )
return _match return _matchOrNull
} }
private val exception get() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @throws PatchException If the [Fingerprint] has not been matched.
*/
context(BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
/**
* The class the matching method is a member of.
*/
context(BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
/**
* The matching method.
*/
context(BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val classDefOrNull
get() = matchOrNull?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
/**
* The match for the opcode pattern.
*/
context(BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
/**
* The matches for the strings.
*/
context(BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
/**
* The class the matching method is a member of.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val originalClassDef
get() = match.originalClassDef
/**
* The matching method.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val originalMethod
get() = match.originalMethod
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val classDef
get() = match.classDef
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val method
get() = match.method
/**
* The match for the opcode pattern.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val patternMatch
get() = match.patternMatch
/**
* The matches for the strings.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val stringMatches
get() = match.stringMatches
} }
/** /**
* A match for a [Fingerprint]. * A match of a [Fingerprint].
* *
* @param originalClassDef The class the matching method is a member of. * @param originalClassDef The class the matching method is a member of.
* @param originalMethod The matching method. * @param originalMethod The matching method.
* @param patternMatch The match for the opcode pattern. * @param patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings. * @param stringMatches The matches for the strings.
* @param context The context to create mutable proxies in.
*/ */
context(BytecodePatchContext)
class Match internal constructor( class Match internal constructor(
val originalClassDef: ClassDef,
val originalMethod: Method, val originalMethod: Method,
val patternMatch: PatternMatch?, val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?, val stringMatches: List<StringMatch>?,
internal val context: BytecodePatchContext, val originalClassDef: ClassDef,
) { ) {
/** /**
* The mutable version of [originalClassDef]. * The mutable version of [originalClassDef].
@ -277,7 +433,7 @@ class Match internal constructor(
* Accessing this property allocates a [ClassProxy]. * Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required. * Use [originalClassDef] if mutable access is not required.
*/ */
val classDef by lazy { context.proxy(originalClassDef).mutableClass } val classDef by lazy { proxy(originalClassDef).mutableClass }
/** /**
* The mutable version of [originalMethod]. * The mutable version of [originalMethod].
@ -292,7 +448,7 @@ class Match internal constructor(
* @param startIndex The index of the first opcode of the pattern in the method. * @param startIndex The index of the first opcode of the pattern in the method.
* @param endIndex The index of the last opcode of the pattern in the method. * @param endIndex The index of the last opcode of the pattern in the method.
*/ */
class PatternMatch( class PatternMatch internal constructor(
val startIndex: Int, val startIndex: Int,
val endIndex: Int, val endIndex: Int,
) )
@ -303,7 +459,7 @@ class Match internal constructor(
* @param string The string that matched. * @param string The string that matched.
* @param index The index of the instruction in the method. * @param index The index of the instruction in the method.
*/ */
class StringMatch(val string: String, val index: Int) class StringMatch internal constructor(val string: String, val index: Int)
} }
/** /**

@ -1,6 +1,8 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.* import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator import app.revanced.patcher.util.MethodNavigator
@ -22,7 +24,6 @@ import java.io.Closeable
import java.io.FileFilter import java.io.FileFilter
import java.util.* import java.util.*
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.reflect.KProperty
/** /**
* A context for patches containing the current state of the bytecode. * A context for patches containing the current state of the bytecode.
@ -33,7 +34,7 @@ import kotlin.reflect.KProperty
class BytecodePatchContext internal constructor(private val config: PatcherConfig) : class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>, PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable { Closeable {
private val logger = Logger.getLogger(BytecodePatchContext::class.java.name) private val logger = Logger.getLogger(this::javaClass.name)
/** /**
* [Opcodes] of the supplied [PatcherConfig.apkFile]. * [Opcodes] of the supplied [PatcherConfig.apkFile].
@ -53,36 +54,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
).also { opcodes = it.opcodes }.classes.toMutableList(), ).also { opcodes = it.opcodes }.classes.toMutableList(),
) )
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
val Fingerprint.match get() = match(this@BytecodePatchContext)
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
fun Fingerprint.match(classDef: ClassDef) = match(this@BytecodePatchContext, classDef)
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
fun Fingerprint.match(method: Method) = match(this@BytecodePatchContext, method)
/**
* Get the match for this [Fingerprint].
*
* @throws IllegalStateException If the [Fingerprint] has not been matched.
*/
operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = match
?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".")
/** /**
* The lookup maps for methods and the class they are a member of from the [classes]. * The lookup maps for methods and the class they are a member of from the [classes].
*/ */
@ -137,9 +108,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* *
* @return A proxy for the class. * @return A proxy for the class.
*/ */
fun proxy(classDef: ClassDef) = this@BytecodePatchContext.classes.proxyPool.find { fun proxy(classDef: ClassDef) = classes.proxyPool.find {
it.immutableClass.type == classDef.type it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) } } ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
/** /**
* Navigate a method. * Navigate a method.
@ -148,7 +119,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* *
* @return A [MethodNavigator] for the method. * @return A [MethodNavigator] for the method.
*/ */
fun navigate(method: MethodReference) = MethodNavigator(this@BytecodePatchContext, method) fun navigate(method: MethodReference) = MethodNavigator(method)
/** /**
* Compile bytecode from the [BytecodePatchContext]. * Compile bytecode from the [BytecodePatchContext].
@ -227,28 +198,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
} }
} }
internal companion object {
/**
* Appends a string based on the parameter reference types of this method.
*/
internal fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
// Maximum parameters to use in the signature key.
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
// To keep the signature map from becoming needlessly bloated,
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
// The value of 5 was chosen based on local performance testing and is not set in stone.
val maxSignatureParameters = 5
// Must append a unique value before the parameters to distinguish this key includes the parameters.
// If this is not appended, then methods with no parameters
// will collide with different keys that specify access/return but omit the parameters.
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
}
}
override fun close() { override fun close() {
methodsByStrings.clear() methodsByStrings.clear()
classesByType.clear() classesByType.clear()

@ -17,7 +17,6 @@ import kotlin.reflect.KProperty
/** /**
* A navigator for methods. * A navigator for methods.
* *
* @param context The [BytecodePatchContext] to use.
* @param startMethod The [Method] to start navigating from. * @param startMethod The [Method] to start navigating from.
* *
* @constructor Creates a new [MethodNavigator]. * @constructor Creates a new [MethodNavigator].
@ -25,12 +24,16 @@ import kotlin.reflect.KProperty
* @throws NavigateException If the method does not have an implementation. * @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference. * @throws NavigateException If the instruction at the specified index is not a method reference.
*/ */
class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) { context(BytecodePatchContext)
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
private var lastNavigatedMethodReference = startMethod private var lastNavigatedMethodReference = startMethod
private val lastNavigatedMethodInstructions get() = with(original()) { private val lastNavigatedMethodInstructions
instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.") get() = with(original()) {
} instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
}
/** /**
* Navigate to the method at the specified index. * Navigate to the method at the specified index.
@ -39,7 +42,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
* *
* @return This [MethodNavigator]. * @return This [MethodNavigator].
*/ */
fun at(vararg index: Int): MethodNavigator { fun to(vararg index: Int): MethodNavigator {
index.forEach { index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it) lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
} }
@ -53,7 +56,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
* @param index The index of the method to navigate to. * @param index The index of the method to navigate to.
* @param predicate The predicate to match. * @param predicate The predicate to match.
*/ */
fun at(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator { fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence() lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index) .filter(predicate).asIterable().getMethodReferenceAt(index)
@ -77,7 +80,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
* *
* @return The last navigated method mutably. * @return The last navigated method mutably.
*/ */
fun stop() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
as MutableMethod as MutableMethod
/** /**
@ -92,7 +95,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
* *
* @return The last navigated method immutably. * @return The last navigated method immutably.
*/ */
fun original() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/** /**
* Predicate to match the class defining the current method reference. * Predicate to match the class defining the current method reference.
@ -104,9 +107,10 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
/** /**
* Find the first [lastNavigatedMethodReference] in the class. * Find the first [lastNavigatedMethodReference] in the class.
*/ */
private val ClassDef.firstMethodBySignature get() = methods.first { private val ClassDef.firstMethodBySignature
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference) get() = methods.first {
} MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
}
/** /**
* An exception thrown when navigating fails. * An exception thrown when navigating fails.

@ -3,21 +3,18 @@ package app.revanced.patcher
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.ProxyClassList import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.every import io.mockk.*
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import jdk.internal.module.ModuleBootstrap.patcher
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll import org.junit.jupiter.api.assertAll
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.test.* import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
internal object PatcherTest { internal object PatcherTest {
private lateinit var patcher: Patcher private lateinit var patcher: Patcher
@ -153,10 +150,10 @@ internal object PatcherTest {
val patch = bytecodePatch { val patch = bytecodePatch {
execute { execute {
// Fingerprint can never match. // Fingerprint can never match.
val match by fingerprint { } val fingerprint = fingerprint { }
// Throws, because the fingerprint can't be matched. // Throws, because the fingerprint can't be matched.
match.patternMatch fingerprint.patternMatch
} }
} }
@ -193,11 +190,6 @@ internal object PatcherTest {
), ),
), ),
) )
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match } } answers { callOriginal() }
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<ClassDef>()) } } answers { callOriginal() }
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<Method>()) } } answers { callOriginal() }
every { patcher.context.bytecodeContext.classBy(any()) } answers { callOriginal() }
every { patcher.context.bytecodeContext.proxy(any()) } answers { callOriginal() }
val fingerprint = fingerprint { returns("V") } val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") } val fingerprint2 = fingerprint { returns("V") }
@ -208,19 +200,21 @@ internal object PatcherTest {
execute { execute {
fingerprint.match(classes.first().methods.first()) fingerprint.match(classes.first().methods.first())
fingerprint2.match(classes.first()) fingerprint2.match(classes.first())
fingerprint3.match fingerprint3.originalClassDef
} }
}, },
) )
patches() patches()
assertAll( with(patcher.context.bytecodeContext) {
"Expected fingerprints to match.", assertAll(
{ assertNotNull(fingerprint._match) }, "Expected fingerprints to match.",
{ assertNotNull(fingerprint2._match) }, { assertNotNull(fingerprint.originalClassDefOrNull) },
{ assertNotNull(fingerprint3._match) }, { assertNotNull(fingerprint2.originalClassDefOrNull) },
) { assertNotNull(fingerprint3.originalClassDefOrNull) },
)
}
} }
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> { private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {