mirror of
https://github.com/revanced/revanced-patcher
synced 2024-11-20 16:19:21 +01:00
feat: Move fingerprint match members to fingerprint for ease of access by using context receivers
This commit is contained in:
parent
7f55868e6f
commit
0746c22743
@ -1,4 +1,22 @@
|
||||
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 {
|
||||
@ -31,13 +49,11 @@ public final class app/revanced/patcher/Match {
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/Match$PatternMatch {
|
||||
public fun <init> (II)V
|
||||
public final fun getEndIndex ()I
|
||||
public final fun getStartIndex ()I
|
||||
}
|
||||
|
||||
public final class app/revanced/patcher/Match$StringMatch {
|
||||
public fun <init> (Ljava/lang/String;I)V
|
||||
public final fun getIndex ()I
|
||||
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 fun get ()Ljava/util/Set;
|
||||
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 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 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 original ()Lcom/android/tools/smali/dexlib2/iface/Method;
|
||||
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 {
|
||||
|
@ -56,6 +56,8 @@ dependencies {
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_11)
|
||||
|
||||
freeCompilerArgs = listOf("-Xcontext-receivers")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,15 +117,19 @@ With this information, the original code can be reconstructed:
|
||||
```java
|
||||
package com.some.app.ads;
|
||||
|
||||
<accessFlags> class AdsLoader {
|
||||
public final boolean <methodName>(boolean <parameter>) {
|
||||
<accessFlags>
|
||||
|
||||
class AdsLoader {
|
||||
public final boolean <methodName>(boolean <parameter>)
|
||||
|
||||
{
|
||||
// ...
|
||||
|
||||
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]
|
||||
> 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.
|
||||
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same.
|
||||
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated
|
||||
> app.
|
||||
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
|
||||
> same.
|
||||
|
||||
## 🔨 How to use fingerprints
|
||||
|
||||
A fingerprint is matched to a method,
|
||||
once the `match` property of the fingerprint is accessed in a patch's `execute` scope:
|
||||
After declaring a fingerprint, it can be used in a patch to find the method it matches to:
|
||||
|
||||
```kt
|
||||
val fingerprint = fingerprint {
|
||||
@ -149,52 +154,34 @@ val fingerprint = fingerprint {
|
||||
|
||||
val patch = bytecodePatch {
|
||||
execute {
|
||||
val match = fingerprint.match!!
|
||||
fingerprint.method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The fingerprint won't be matched again, if it has already been matched once.
|
||||
This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint:
|
||||
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 executing patch match the fingerprint:
|
||||
|
||||
```kt
|
||||
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
|
||||
val mainActivityPatch1 = bytecodePatch {
|
||||
execute {
|
||||
val match = mainActivityOnCreateFingerprint.match!!
|
||||
mainActivityOnCreateFingerprint.method
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivityPatch2 = bytecodePatch {
|
||||
execute {
|
||||
val match = mainActivityOnCreateFingerprint.match!!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
}
|
||||
mainActivityOnCreateFingerprint.method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If such a match is delegated
|
||||
> to a variable, accessing it will raise an exception.
|
||||
> If the fingerprint can not be matched to any method,
|
||||
> 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]
|
||||
> 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
|
||||
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 { /* ... */ }
|
||||
The following properties can be accessed in a fingerprint:
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
- `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.
|
||||
They are lazy properties, so they are only computed
|
||||
and will effectively replace the original method or class definition when accessed.
|
||||
The difference between the `original` and non-`original` properties is that the `original` properties return the
|
||||
original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
|
||||
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]
|
||||
> If only read-only access to the class or method is needed,
|
||||
> the `originalClassDef` and `originalMethod` properties can be used,
|
||||
> If only read-only access to the class or method is needed,
|
||||
> the `originalClassDef` and `originalMethod` properties should be used,
|
||||
> to avoid making a mutable copy of the class or method.
|
||||
|
||||
## 🏹 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:
|
||||
|
||||
- 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,
|
||||
you can match the fingerprint on the list of classes:
|
||||
you can match the fingerprint on the list of classes:
|
||||
|
||||
```kt
|
||||
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 {
|
||||
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.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
// 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
|
||||
|
||||
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:
|
||||
|
||||
```kt
|
||||
@ -288,14 +272,19 @@ you can match the fingerprint on the list of classes:
|
||||
strings("free", "trial")
|
||||
}
|
||||
|
||||
currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match ->
|
||||
currentPlanFingerprint.match(adsFingerprint.method).let { match ->
|
||||
match.stringMatches.forEach { match ->
|
||||
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]
|
||||
> To see real-world examples of fingerprints,
|
||||
> 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
|
||||
execute {
|
||||
// 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'.
|
||||
// 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
execute {
|
||||
document("res/values/strings.xml").use { document ->
|
||||
val element = doc.createElement("string").apply {
|
||||
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,
|
||||
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,
|
||||
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
|
||||
|
@ -1,3 +1,3 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
version = 21.0.0-dev.2
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
version=21.0.0-dev.3
|
||||
|
@ -3,8 +3,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.patcher.patch.MethodClassPairs
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.util.proxy.ClassProxy
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
@ -44,17 +44,21 @@ class Fingerprint internal constructor(
|
||||
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
|
||||
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.
|
||||
*/
|
||||
// Backing property for "match" extension in BytecodePatchContext.
|
||||
@Suppress("ktlint:standard:backing-property-naming", "PropertyName")
|
||||
internal var _match: Match? = null
|
||||
context(BytecodePatchContext)
|
||||
private val matchOrNull: Match?
|
||||
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:
|
||||
* - Slowest: Specify [custom] or [opcodes] and nothing else.
|
||||
@ -62,29 +66,28 @@ class Fingerprint internal constructor(
|
||||
* - Faster: Specify [accessFlags], [returnType] and [parameters].
|
||||
* - 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.
|
||||
*/
|
||||
internal fun match(context: BytecodePatchContext): Match? {
|
||||
if (_match != null) return _match
|
||||
context(BytecodePatchContext)
|
||||
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) ->
|
||||
val match = match(context, classDef, method)
|
||||
if (match != null) return match
|
||||
val match = matchOrNull(classDef, method)
|
||||
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
|
||||
|
||||
context.classes.forEach { classDef ->
|
||||
val match = match(context, classDef)
|
||||
classes.forEach { classDef ->
|
||||
match = matchOrNull(classDef)
|
||||
if (match != null) return match
|
||||
}
|
||||
|
||||
@ -95,18 +98,17 @@ class Fingerprint internal constructor(
|
||||
* Match using a [ClassDef].
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
internal fun match(
|
||||
context: BytecodePatchContext,
|
||||
context(BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
classDef: ClassDef,
|
||||
): Match? {
|
||||
if (_match != null) return _match
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
for (method in classDef.methods) {
|
||||
val match = match(context, method, classDef)
|
||||
if (match != null)return match
|
||||
val match = matchOrNull(method, classDef)
|
||||
if (match != null) return match
|
||||
}
|
||||
|
||||
return null
|
||||
@ -117,28 +119,26 @@ class Fingerprint internal constructor(
|
||||
* The class is retrieved from the method.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
internal fun match(
|
||||
context: BytecodePatchContext,
|
||||
context(BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
method: Method,
|
||||
) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass)
|
||||
) = matchOrNull(method, classBy { method.definingClass == it.type }!!.immutableClass)
|
||||
|
||||
/**
|
||||
* Match using a [Method].
|
||||
*
|
||||
* @param method The method to match against.
|
||||
* @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.
|
||||
*/
|
||||
internal fun match(
|
||||
context: BytecodePatchContext,
|
||||
context(BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
method: Method,
|
||||
classDef: ClassDef,
|
||||
): Match? {
|
||||
if (_match != null) return _match
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
||||
return null
|
||||
@ -243,33 +243,189 @@ class Fingerprint internal constructor(
|
||||
null
|
||||
}
|
||||
|
||||
_match = Match(
|
||||
classDef,
|
||||
_matchOrNull = Match(
|
||||
method,
|
||||
patternMatch,
|
||||
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 originalMethod The matching method.
|
||||
* @param patternMatch The match for the opcode pattern.
|
||||
* @param stringMatches The matches for the strings.
|
||||
* @param context The context to create mutable proxies in.
|
||||
*/
|
||||
context(BytecodePatchContext)
|
||||
class Match internal constructor(
|
||||
val originalClassDef: ClassDef,
|
||||
val originalMethod: Method,
|
||||
val patternMatch: PatternMatch?,
|
||||
val stringMatches: List<StringMatch>?,
|
||||
internal val context: BytecodePatchContext,
|
||||
val originalClassDef: ClassDef,
|
||||
) {
|
||||
/**
|
||||
* The mutable version of [originalClassDef].
|
||||
@ -277,7 +433,7 @@ class Match internal constructor(
|
||||
* Accessing this property allocates a [ClassProxy].
|
||||
* 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].
|
||||
@ -292,7 +448,7 @@ class Match internal constructor(
|
||||
* @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.
|
||||
*/
|
||||
class PatternMatch(
|
||||
class PatternMatch internal constructor(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int,
|
||||
)
|
||||
@ -303,7 +459,7 @@ class Match internal constructor(
|
||||
* @param string The string that matched.
|
||||
* @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
|
||||
|
||||
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.util.ClassMerger.merge
|
||||
import app.revanced.patcher.util.MethodNavigator
|
||||
@ -22,7 +24,6 @@ import java.io.Closeable
|
||||
import java.io.FileFilter
|
||||
import java.util.*
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* 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) :
|
||||
PatchContext<Set<PatcherResult.PatchedDexFile>>,
|
||||
Closeable {
|
||||
private val logger = Logger.getLogger(BytecodePatchContext::class.java.name)
|
||||
private val logger = Logger.getLogger(this::javaClass.name)
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherConfig.apkFile].
|
||||
@ -53,36 +54,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
||||
).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].
|
||||
*/
|
||||
@ -137,9 +108,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
||||
*
|
||||
* @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
|
||||
} ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) }
|
||||
} ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
|
||||
|
||||
/**
|
||||
* Navigate a method.
|
||||
@ -148,7 +119,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
||||
*
|
||||
* @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].
|
||||
@ -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() {
|
||||
methodsByStrings.clear()
|
||||
classesByType.clear()
|
||||
|
@ -17,7 +17,6 @@ import kotlin.reflect.KProperty
|
||||
/**
|
||||
* A navigator for methods.
|
||||
*
|
||||
* @param context The [BytecodePatchContext] to use.
|
||||
* @param startMethod The [Method] to start navigating from.
|
||||
*
|
||||
* @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 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 val lastNavigatedMethodInstructions get() = with(original()) {
|
||||
instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.")
|
||||
}
|
||||
private val lastNavigatedMethodInstructions
|
||||
get() = with(original()) {
|
||||
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the method at the specified index.
|
||||
@ -39,7 +42,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
|
||||
*
|
||||
* @return This [MethodNavigator].
|
||||
*/
|
||||
fun at(vararg index: Int): MethodNavigator {
|
||||
fun to(vararg index: Int): MethodNavigator {
|
||||
index.forEach {
|
||||
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 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()
|
||||
.filter(predicate).asIterable().getMethodReferenceAt(index)
|
||||
|
||||
@ -77,7 +80,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
|
||||
*
|
||||
* @return The last navigated method mutably.
|
||||
*/
|
||||
fun stop() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
|
||||
fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
|
||||
as MutableMethod
|
||||
|
||||
/**
|
||||
@ -92,7 +95,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
|
||||
*
|
||||
* @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.
|
||||
@ -104,9 +107,10 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
|
||||
/**
|
||||
* Find the first [lastNavigatedMethodReference] in the class.
|
||||
*/
|
||||
private val ClassDef.firstMethodBySignature get() = methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
|
||||
}
|
||||
private val ClassDef.firstMethodBySignature
|
||||
get() = methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception thrown when navigating fails.
|
||||
|
@ -3,21 +3,18 @@ package app.revanced.patcher
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
|
||||
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.ImmutableMethod
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import jdk.internal.module.ModuleBootstrap.patcher
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.assertAll
|
||||
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 {
|
||||
private lateinit var patcher: Patcher
|
||||
@ -153,10 +150,10 @@ internal object PatcherTest {
|
||||
val patch = bytecodePatch {
|
||||
execute {
|
||||
// Fingerprint can never match.
|
||||
val match by fingerprint { }
|
||||
val fingerprint = fingerprint { }
|
||||
|
||||
// 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 fingerprint2 = fingerprint { returns("V") }
|
||||
@ -208,19 +200,21 @@ internal object PatcherTest {
|
||||
execute {
|
||||
fingerprint.match(classes.first().methods.first())
|
||||
fingerprint2.match(classes.first())
|
||||
fingerprint3.match
|
||||
fingerprint3.originalClassDef
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
patches()
|
||||
|
||||
assertAll(
|
||||
"Expected fingerprints to match.",
|
||||
{ assertNotNull(fingerprint._match) },
|
||||
{ assertNotNull(fingerprint2._match) },
|
||||
{ assertNotNull(fingerprint3._match) },
|
||||
)
|
||||
with(patcher.context.bytecodeContext) {
|
||||
assertAll(
|
||||
"Expected fingerprints to match.",
|
||||
{ assertNotNull(fingerprint.originalClassDefOrNull) },
|
||||
{ assertNotNull(fingerprint2.originalClassDefOrNull) },
|
||||
{ assertNotNull(fingerprint3.originalClassDefOrNull) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
|
||||
|
Loading…
Reference in New Issue
Block a user