mirror of
https://github.com/revanced/revanced-patcher
synced 2024-12-11 22:03:54 +01:00
feat: Improve Fingerprint API (#316)
Fingerprints can now be matched easily without adding them to a patch first. BREAKING CHANGE: Many APIs have been changed.
This commit is contained in:
parent
aa472eb985
commit
0abf1c6c02
@ -1,7 +1,4 @@
|
|||||||
public final class app/revanced/patcher/Fingerprint {
|
public final class app/revanced/patcher/Fingerprint {
|
||||||
public final fun getMatch ()Lapp/revanced/patcher/Match;
|
|
||||||
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
|
|
||||||
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/FingerprintBuilder {
|
public final class app/revanced/patcher/FingerprintBuilder {
|
||||||
@ -18,20 +15,17 @@ public final class app/revanced/patcher/FingerprintBuilder {
|
|||||||
|
|
||||||
public final class app/revanced/patcher/FingerprintKt {
|
public final class app/revanced/patcher/FingerprintKt {
|
||||||
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
|
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
|
||||||
public static final fun fingerprint (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
|
|
||||||
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
|
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
|
||||||
public static synthetic fun fingerprint$default (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
|
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/Match {
|
public final class app/revanced/patcher/Match {
|
||||||
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/Match$PatternMatch;Ljava/util/List;Lapp/revanced/patcher/patch/BytecodePatchContext;)V
|
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
|
||||||
public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
|
public final fun getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
|
||||||
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
|
public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
|
||||||
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
|
public final fun getOriginalMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
|
||||||
public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
|
|
||||||
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
|
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
|
||||||
public final fun getStringMatches ()Ljava/util/List;
|
public final fun getStringMatches ()Ljava/util/List;
|
||||||
}
|
}
|
||||||
@ -63,8 +57,8 @@ public final class app/revanced/patcher/Patcher : java/io/Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/PatcherConfig {
|
public final class app/revanced/patcher/PatcherConfig {
|
||||||
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
|
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
|
||||||
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
|
public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
|
||||||
@ -135,30 +129,27 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
|
public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
|
||||||
public final fun getExtension ()Ljava/io/InputStream;
|
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
|
||||||
public final fun getFingerprints ()Ljava/util/Set;
|
|
||||||
public fun toString ()Ljava/lang/String;
|
public fun toString ()Ljava/lang/String;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
|
public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
|
||||||
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
|
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
|
||||||
public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
|
public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
|
||||||
public final fun getExtension ()Ljava/io/InputStream;
|
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
|
||||||
public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
|
public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V
|
||||||
public final fun setExtension (Ljava/io/InputStream;)V
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint {
|
|
||||||
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
|
public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
|
||||||
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
|
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
|
||||||
public final fun classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
|
|
||||||
public fun close ()V
|
public fun close ()V
|
||||||
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/Method;)Lapp/revanced/patcher/util/MethodNavigator;
|
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)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;
|
||||||
}
|
}
|
||||||
@ -286,7 +277,7 @@ public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jv
|
|||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/patcher/patch/Patch {
|
public abstract class app/revanced/patcher/patch/Patch {
|
||||||
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
|
public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
|
||||||
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
|
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
|
||||||
public final fun getCompatiblePackages ()Ljava/util/Set;
|
public final fun getCompatiblePackages ()Ljava/util/Set;
|
||||||
@ -303,13 +294,13 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
|
|||||||
public final fun compatibleWith ([Ljava/lang/String;)V
|
public final fun compatibleWith ([Ljava/lang/String;)V
|
||||||
public final fun compatibleWith ([Lkotlin/Pair;)V
|
public final fun compatibleWith ([Lkotlin/Pair;)V
|
||||||
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
|
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
|
||||||
public final fun execute (Lkotlin/jvm/functions/Function2;)V
|
public final fun execute (Lkotlin/jvm/functions/Function1;)V
|
||||||
public final fun finalize (Lkotlin/jvm/functions/Function2;)V
|
public final fun finalize (Lkotlin/jvm/functions/Function1;)V
|
||||||
protected final fun getCompatiblePackages ()Ljava/util/Set;
|
protected final fun getCompatiblePackages ()Ljava/util/Set;
|
||||||
protected final fun getDependencies ()Ljava/util/Set;
|
protected final fun getDependencies ()Ljava/util/Set;
|
||||||
protected final fun getDescription ()Ljava/lang/String;
|
protected final fun getDescription ()Ljava/lang/String;
|
||||||
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2;
|
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1;
|
||||||
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
|
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1;
|
||||||
protected final fun getName ()Ljava/lang/String;
|
protected final fun getName ()Ljava/lang/String;
|
||||||
protected final fun getOptions ()Ljava/util/Set;
|
protected final fun getOptions ()Ljava/util/Set;
|
||||||
protected final fun getUse ()Z
|
protected final fun getUse ()Z
|
||||||
@ -317,8 +308,8 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
|
|||||||
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
|
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
|
||||||
protected final fun setCompatiblePackages (Ljava/util/Set;)V
|
protected final fun setCompatiblePackages (Ljava/util/Set;)V
|
||||||
protected final fun setDependencies (Ljava/util/Set;)V
|
protected final fun setDependencies (Ljava/util/Set;)V
|
||||||
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V
|
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V
|
||||||
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V
|
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
|
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
|
||||||
|
@ -89,9 +89,9 @@ val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { pat
|
|||||||
runBlocking {
|
runBlocking {
|
||||||
patcher().collect { patchResult ->
|
patcher().collect { patchResult ->
|
||||||
if (patchResult.exception != null)
|
if (patchResult.exception != null)
|
||||||
logger.info("\"${patchResult.patch}\" failed:\n${patchResult.exception}")
|
logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" }
|
||||||
else
|
else
|
||||||
logger.info("\"${patchResult.patch}\" succeeded")
|
logger.info { "\"${patchResult.patch}\" succeeded" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,10 @@ To start developing patches with ReVanced Patcher, you must prepare a developmen
|
|||||||
|
|
||||||
Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.
|
Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> To start a fresh project,
|
||||||
|
> you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template).
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -60,14 +60,16 @@
|
|||||||
|
|
||||||
# 🔎 Fingerprinting
|
# 🔎 Fingerprinting
|
||||||
|
|
||||||
In the context of ReVanced, fingerprinting is primarily used to match methods with a limited amount of known information.
|
In the context of ReVanced, a fingerprint is a partial description of a method.
|
||||||
|
It is used to uniquely match a method by its characteristics.
|
||||||
|
Fingerprinting is used to match methods with a limited amount of known information.
|
||||||
Methods with obfuscated names that change with each update are primary candidates for fingerprinting.
|
Methods with obfuscated names that change with each update are primary candidates for fingerprinting.
|
||||||
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
|
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
|
||||||
access flags, an opcode pattern, strings, and more.
|
access flags, an opcode pattern, strings, and more.
|
||||||
|
|
||||||
## ⛳️ Example fingerprint
|
## ⛳️ Example fingerprint
|
||||||
|
|
||||||
Throughout the documentation, the following example will be used to demonstrate the concepts of fingerprints:
|
An example fingerprint is shown below:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
|
|
||||||
@ -79,11 +81,11 @@ fingerprint {
|
|||||||
parameters("Z")
|
parameters("Z")
|
||||||
opcodes(Opcode.RETURN)
|
opcodes(Opcode.RETURN)
|
||||||
strings("pro")
|
strings("pro")
|
||||||
custom { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;" }
|
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔎 Reconstructing the original code from a fingerprint
|
## 🔎 Reconstructing the original code from the example fingerprint from above
|
||||||
|
|
||||||
The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
|
The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
|
||||||
|
|
||||||
@ -107,27 +109,29 @@ The fingerprint contains the following information:
|
|||||||
- Package and class name:
|
- Package and class name:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
custom = { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;"}
|
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
|
||||||
```
|
```
|
||||||
|
|
||||||
With this information, the original code can be reconstructed:
|
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> class AdsLoader {
|
||||||
public final boolean <methodName>(boolean <parameter>) {
|
public final boolean <methodName>(boolean <parameter>) {
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
var userStatus = "pro";
|
var userStatus = "pro";
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
return <returnValue>;
|
return <returnValue>;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Using that fingerprint, this method can be matched uniquely from all other methods.
|
||||||
|
|
||||||
> [!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 app.
|
||||||
@ -135,8 +139,8 @@ With this information, the original code can be reconstructed:
|
|||||||
|
|
||||||
## 🔨 How to use fingerprints
|
## 🔨 How to use fingerprints
|
||||||
|
|
||||||
Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually.
|
A fingerprint is matched to a method,
|
||||||
Fingerprints added to a patch are matched by ReVanced Patcher before the patch is executed.
|
once the `match` property of the fingerprint is accessed in a patch's `execute` scope:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
val fingerprint = fingerprint {
|
val fingerprint = fingerprint {
|
||||||
@ -144,48 +148,46 @@ val fingerprint = fingerprint {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val patch = bytecodePatch {
|
val patch = bytecodePatch {
|
||||||
// Directly create and add a fingerprint.
|
execute {
|
||||||
fingerprint {
|
val match = fingerprint.match!!
|
||||||
// ...
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a fingerprint manually by invoking it.
|
|
||||||
fingerprint()
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
The fingerprint won't be matched again, if it has already been matched once.
|
||||||
> Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again.
|
This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint:
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
|
|
||||||
> function to fuzzy match the pattern.
|
|
||||||
> `null` can be used as a wildcard to match any opcode:
|
|
||||||
>
|
|
||||||
> ```kt
|
|
||||||
> fingerprint(fuzzyPatternScanThreshhold = 2) {
|
|
||||||
> opcodes(
|
|
||||||
> Opcode.ICONST_0,
|
|
||||||
> null,
|
|
||||||
> Opcode.ICONST_1,
|
|
||||||
> Opcode.IRETURN,
|
|
||||||
> )
|
|
||||||
>}
|
|
||||||
> ```
|
|
||||||
|
|
||||||
Once the fingerprint is matched, the match can be used in the patch:
|
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
val patch = bytecodePatch {
|
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
|
||||||
// Add a fingerprint and delegate its match to a variable.
|
val mainActivityPatch1 = bytecodePatch {
|
||||||
val match by showAdsFingerprint()
|
|
||||||
val match2 by fingerprint {
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
val method = match.method
|
val match = mainActivityOnCreateFingerprint.match!!
|
||||||
val method2 = match2.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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -194,30 +196,53 @@ val patch = bytecodePatch {
|
|||||||
> 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, the match of a fingerprint is `null`. If such a match is delegated
|
||||||
> to a variable, accessing it will raise an exception.
|
> to a variable, accessing it will raise an exception.
|
||||||
|
|
||||||
The match of a fingerprint contains mutable and immutable references to the method and the class it matches to.
|
> [!TIP]
|
||||||
|
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
|
||||||
|
> function to fuzzy match the pattern.
|
||||||
|
> `null` can be used as a wildcard to match any opcode:
|
||||||
|
>
|
||||||
|
> ```kt
|
||||||
|
> fingerprint(fuzzyPatternScanThreshhold = 2) {
|
||||||
|
> opcodes(
|
||||||
|
> Opcode.ICONST_0,
|
||||||
|
> null,
|
||||||
|
> Opcode.ICONST_1,
|
||||||
|
> Opcode.IRETURN,
|
||||||
|
> )
|
||||||
|
>}
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
The match of a fingerprint contains references to the original method and class definition of the method:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
class Match(
|
class Match(
|
||||||
val method: Method,
|
val originalMethod: Method,
|
||||||
val classDef: ClassDef,
|
val originalClassDef: ClassDef,
|
||||||
val patternMatch: Match.PatternMatch?,
|
val patternMatch: Match.PatternMatch?,
|
||||||
val stringMatches: List<Match.StringMatch>?,
|
val stringMatches: List<Match.StringMatch>?,
|
||||||
// ...
|
// ...
|
||||||
) {
|
) {
|
||||||
val mutableClass by lazy { /* ... */ }
|
val classDef by lazy { /* ... */ }
|
||||||
val mutableMethod by lazy { /* ... */ }
|
val method by lazy { /* ... */ }
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏹 Manual matching of fingerprints
|
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.
|
||||||
|
|
||||||
Unless a fingerprint is added to a patch, the fingerprint will not be matched automatically by ReVanced Patcher
|
> [!TIP]
|
||||||
before the patch is executed.
|
> If only read-only access to the class or method is needed,
|
||||||
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function.
|
> the `originalClassDef` and `originalMethod` properties can be used,
|
||||||
|
> to avoid making a mutable copy of the class or method.
|
||||||
|
|
||||||
You can match a fingerprint the following ways:
|
## 🏹 Manually matching fingerprints
|
||||||
|
|
||||||
|
By default, a fingerprint is matched automatically against all classes when the `match` property 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
|
- In a **list of classes**, if the fingerprint can match in a known subset of classes
|
||||||
|
|
||||||
@ -225,11 +250,9 @@ You can match a fingerprint the following ways:
|
|||||||
you can match the fingerprint on the list of classes:
|
you can match the fingerprint on the list of classes:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
execute { context ->
|
execute {
|
||||||
val match = showAdsFingerprint.apply {
|
val match = showAdsFingerprint.match(classes) ?: throw PatchException("No match found")
|
||||||
match(context, context.classes)
|
}
|
||||||
}.match ?: throw PatchException("No match found")
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- In a **single class**, if the fingerprint can match in a single known class
|
- In a **single class**, if the fingerprint can match in a single known class
|
||||||
@ -237,34 +260,39 @@ you can match the fingerprint on the list of classes:
|
|||||||
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
|
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
execute { context ->
|
execute {
|
||||||
val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" }
|
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }
|
||||||
|
|
||||||
val match = showAdsFingerprint.apply {
|
val match = showAdsFingerprint.match(context, adsLoaderClass) ?: throw PatchException("No match found")
|
||||||
match(context, adsLoaderClass)
|
}
|
||||||
}.match ?: throw PatchException("No match found")
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- 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, such as the start and end index of an opcode pattern
|
The match of a fingerprint contains useful information about the method,
|
||||||
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
|
||||||
execute { context ->
|
execute {
|
||||||
val proStringsFingerprint = fingerprint {
|
val currentPlanFingerprint = fingerprint {
|
||||||
strings("free", "trial")
|
strings("free", "trial")
|
||||||
}
|
}
|
||||||
|
|
||||||
proStringsFingerprint.apply {
|
currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match ->
|
||||||
match(context, adsFingerprintMatch.method)
|
match.stringMatches.forEach { match ->
|
||||||
}.match?.let { match ->
|
println("The index of the string '${match.string}' is ${match.index}")
|
||||||
match.stringMatches.forEach { match ->
|
}
|
||||||
println("The index of the string '${match.string}' is ${match.index}")
|
} ?: throw PatchException("No match found")
|
||||||
}
|
|
||||||
} ?: throw PatchException("No match found")
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -76,23 +76,23 @@ val disableAdsPatch = bytecodePatch(
|
|||||||
) {
|
) {
|
||||||
compatibleWith("com.some.app"("1.0.0"))
|
compatibleWith("com.some.app"("1.0.0"))
|
||||||
|
|
||||||
// Resource patch disables ads by patching resource files.
|
// Patches can depend on other patches, executing them first.
|
||||||
dependsOn(disableAdsResourcePatch)
|
dependsOn(disableAdsResourcePatch)
|
||||||
|
|
||||||
// Precompiled DEX file to be merged into the patched app.
|
// Merge precompiled DEX files into the patched app, before the patch is executed.
|
||||||
extendWith("disable-ads.rve")
|
extendWith("disable-ads.rve")
|
||||||
|
|
||||||
// Fingerprint to find the method to patch.
|
|
||||||
val showAdsMatch by showAdsFingerprint {
|
|
||||||
// More about fingerprints on the next page of the documentation.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Business logic of the patch to disable ads in the app.
|
// Business logic of the patch to disable ads in the app.
|
||||||
execute {
|
execute {
|
||||||
|
// Fingerprint to find the method to patch.
|
||||||
|
val showAdsMatch by showAdsFingerprint {
|
||||||
|
// More about fingerprints on the next page of the documentation.
|
||||||
|
}
|
||||||
|
|
||||||
// In the method that shows ads,
|
// In the method that shows ads,
|
||||||
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
|
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
|
||||||
// to enable or disable ads.
|
// to enable or disable ads.
|
||||||
showAdsMatch.mutableMethod.addInstructions(
|
showAdsMatch.method.addInstructions(
|
||||||
0,
|
0,
|
||||||
"""
|
"""
|
||||||
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
|
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
|
||||||
@ -146,10 +146,10 @@ loadPatchesJar(patches).apply {
|
|||||||
The type of an option can be obtained from the `type` property of the option:
|
The type of an option can be obtained from the `type` property of the option:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
option.type // The KType of the option.
|
option.type // The KType of the option. Captures the full type information of the option.
|
||||||
```
|
```
|
||||||
|
|
||||||
Options can be declared outside of a patch and added to a patch manually:
|
Options can be declared outside a patch and added to a patch manually:
|
||||||
|
|
||||||
```kt
|
```kt
|
||||||
val option = stringOption(key = "option")
|
val option = stringOption(key = "option")
|
||||||
@ -183,11 +183,9 @@ and use it in a patch:
|
|||||||
```kt
|
```kt
|
||||||
val patch = bytecodePatch(name = "Complex patch") {
|
val patch = bytecodePatch(name = "Complex patch") {
|
||||||
extendWith("complex-patch.rve")
|
extendWith("complex-patch.rve")
|
||||||
|
|
||||||
val match by methodFingerprint()
|
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
|
fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -96,21 +96,21 @@ Example of patches:
|
|||||||
@Surpress("unused")
|
@Surpress("unused")
|
||||||
val bytecodePatch = bytecodePatch {
|
val bytecodePatch = bytecodePatch {
|
||||||
execute {
|
execute {
|
||||||
// TODO
|
// More about this on the next page of the documentation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Surpress("unused")
|
@Surpress("unused")
|
||||||
val rawResourcePatch = rawResourcePatch {
|
val rawResourcePatch = rawResourcePatch {
|
||||||
execute {
|
execute {
|
||||||
// TODO
|
// More about this on the next page of the documentation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Surpress("unused")
|
@Surpress("unused")
|
||||||
val resourcePatch = resourcePatch {
|
val resourcePatch = resourcePatch {
|
||||||
execute {
|
execute {
|
||||||
// TODO
|
// More about this on the next page of the documentation.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -4,13 +4,11 @@ A handful of APIs are available to make patch development easier and more effici
|
|||||||
|
|
||||||
## 📙 Overview
|
## 📙 Overview
|
||||||
|
|
||||||
1. 👹 Mutate classes with `context.proxy(ClassDef)`
|
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
|
||||||
2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)`
|
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
|
||||||
3. 🏃 Easily access referenced methods recursively by index with `MethodNavigator`
|
3. 🏃 Navigate method calls recursively by index with `navigate(Method).at(index)`
|
||||||
4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications
|
4. 💾 Read and write resource files with `get(Path, Boolean)`
|
||||||
(Available in ReVanced Patches)
|
5. 📃 Read and write DOM files using `document`
|
||||||
5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)`
|
|
||||||
6. 📃 Read and write DOM files using `ResourcePatchContext.document`
|
|
||||||
|
|
||||||
### 🧰 APIs
|
### 🧰 APIs
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ 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.*
|
||||||
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps.Companion.appendParameters
|
|
||||||
import app.revanced.patcher.patch.MethodClassPairs
|
import app.revanced.patcher.patch.MethodClassPairs
|
||||||
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
|
||||||
@ -16,7 +15,17 @@ import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
|||||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A fingerprint.
|
* A fingerprint for a method. A fingerprint is a partial description of a method.
|
||||||
|
* It is used to uniquely match a method by its characteristics.
|
||||||
|
*
|
||||||
|
* An example fingerprint for a public method that takes a single string parameter and returns void:
|
||||||
|
* ```
|
||||||
|
* fingerprint {
|
||||||
|
* accessFlags(AccessFlags.PUBLIC)
|
||||||
|
* returns("V")
|
||||||
|
* parameters("Ljava/lang/String;")
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*
|
*
|
||||||
* @param accessFlags The exact access flags using values of [AccessFlags].
|
* @param accessFlags The exact access flags using values of [AccessFlags].
|
||||||
* @param returnType The return type. Compared using [String.startsWith].
|
* @param returnType The return type. Compared using [String.startsWith].
|
||||||
@ -38,13 +47,14 @@ class Fingerprint internal constructor(
|
|||||||
/**
|
/**
|
||||||
* The match for this [Fingerprint]. Null if unmatched.
|
* The match for this [Fingerprint]. Null if unmatched.
|
||||||
*/
|
*/
|
||||||
var match: Match? = null
|
// Backing property for "match" extension in BytecodePatchContext.
|
||||||
private set
|
@Suppress("ktlint:standard:backing-property-naming", "PropertyName")
|
||||||
|
internal var _match: Match? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 [_match] 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.
|
||||||
@ -52,48 +62,54 @@ 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 context to create mutable proxies for the matched method and its class.
|
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
|
||||||
* @return True if a match was found or if the fingerprint is already matched to a method, false 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): Boolean {
|
internal fun match(context: BytecodePatchContext): Match? {
|
||||||
|
if (_match != null) return _match
|
||||||
|
|
||||||
val lookupMaps = context.lookupMaps
|
val lookupMaps = context.lookupMaps
|
||||||
|
|
||||||
fun Fingerprint.match(methodClasses: MethodClassPairs): Boolean {
|
fun Fingerprint.match(methodClasses: MethodClassPairs): Match? {
|
||||||
methodClasses.forEach { (classDef, method) ->
|
methodClasses.forEach { (classDef, method) ->
|
||||||
if (match(context, classDef, method)) return true
|
val match = match(context, classDef, method)
|
||||||
|
if (match != null) return match
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: If only one string is necessary, why not use a single string for every fingerprint?
|
// TODO: If only one string is necessary, why not use a single string for every fingerprint?
|
||||||
if (strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match) == true) {
|
val match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match)
|
||||||
return true
|
if (match != null) return match
|
||||||
}
|
|
||||||
|
|
||||||
context.classes.forEach { classDef ->
|
context.classes.forEach { classDef ->
|
||||||
if (match(context, classDef)) return true
|
val match = match(context, classDef)
|
||||||
|
if (match != null) return match
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 context to create mutable proxies for the matched method and its class.
|
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
|
||||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
|
||||||
*/
|
*/
|
||||||
fun match(
|
internal fun match(
|
||||||
context: BytecodePatchContext,
|
context: BytecodePatchContext,
|
||||||
classDef: ClassDef,
|
classDef: ClassDef,
|
||||||
): Boolean {
|
): Match? {
|
||||||
|
if (_match != null) return _match
|
||||||
|
|
||||||
for (method in classDef.methods) {
|
for (method in classDef.methods) {
|
||||||
if (match(context, method, classDef)) {
|
val match = match(context, method, classDef)
|
||||||
return true
|
if (match != null)return match
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -101,10 +117,10 @@ 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 context to create mutable proxies for the matched method and its class.
|
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
|
||||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
|
||||||
*/
|
*/
|
||||||
fun match(
|
internal fun match(
|
||||||
context: BytecodePatchContext,
|
context: BytecodePatchContext,
|
||||||
method: Method,
|
method: Method,
|
||||||
) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass)
|
) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass)
|
||||||
@ -114,22 +130,22 @@ class Fingerprint internal constructor(
|
|||||||
*
|
*
|
||||||
* @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 context to create mutable proxies for the matched method and its class.
|
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
|
||||||
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
|
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
|
||||||
*/
|
*/
|
||||||
internal fun match(
|
internal fun match(
|
||||||
context: BytecodePatchContext,
|
context: BytecodePatchContext,
|
||||||
method: Method,
|
method: Method,
|
||||||
classDef: ClassDef,
|
classDef: ClassDef,
|
||||||
): Boolean {
|
): Match? {
|
||||||
if (match != null) return true
|
if (_match != null) return _match
|
||||||
|
|
||||||
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accessFlags != null && accessFlags != method.accessFlags) {
|
if (accessFlags != null && accessFlags != method.accessFlags) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parametersEqual(
|
fun parametersEqual(
|
||||||
@ -146,17 +162,17 @@ class Fingerprint internal constructor(
|
|||||||
|
|
||||||
// TODO: parseParameters()
|
// TODO: parseParameters()
|
||||||
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
|
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (custom != null && !custom.invoke(method, classDef)) {
|
if (custom != null && !custom.invoke(method, classDef)) {
|
||||||
return false
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val stringMatches: List<Match.StringMatch>? =
|
val stringMatches: List<Match.StringMatch>? =
|
||||||
if (strings != null) {
|
if (strings != null) {
|
||||||
buildList {
|
buildList {
|
||||||
val instructions = method.instructionsOrNull ?: return false
|
val instructions = method.instructionsOrNull ?: return null
|
||||||
|
|
||||||
val stringsList = strings.toMutableList()
|
val stringsList = strings.toMutableList()
|
||||||
|
|
||||||
@ -176,14 +192,14 @@ class Fingerprint internal constructor(
|
|||||||
stringsList.removeAt(index)
|
stringsList.removeAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringsList.isNotEmpty()) return false
|
if (stringsList.isNotEmpty()) return null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val patternMatch = if (opcodes != null) {
|
val patternMatch = if (opcodes != null) {
|
||||||
val instructions = method.instructionsOrNull ?: return false
|
val instructions = method.instructionsOrNull ?: return null
|
||||||
|
|
||||||
fun patternScan(): Match.PatternMatch? {
|
fun patternScan(): Match.PatternMatch? {
|
||||||
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
|
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
|
||||||
@ -222,54 +238,54 @@ class Fingerprint internal constructor(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
patternScan() ?: return false
|
patternScan() ?: return null
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
match = Match(
|
_match = Match(
|
||||||
method,
|
|
||||||
classDef,
|
classDef,
|
||||||
|
method,
|
||||||
patternMatch,
|
patternMatch,
|
||||||
stringMatches,
|
stringMatches,
|
||||||
context,
|
context,
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return _match
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A match for a [Fingerprint].
|
* A match for a [Fingerprint].
|
||||||
*
|
*
|
||||||
* @param method The matching method.
|
* @param originalClassDef The class the matching method is a member of.
|
||||||
* @param classDef The class the matching method is a member of.
|
* @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.
|
* @param context The context to create mutable proxies in.
|
||||||
*/
|
*/
|
||||||
class Match(
|
class Match internal constructor(
|
||||||
val method: Method,
|
val originalClassDef: ClassDef,
|
||||||
val classDef: ClassDef,
|
val originalMethod: Method,
|
||||||
val patternMatch: PatternMatch?,
|
val patternMatch: PatternMatch?,
|
||||||
val stringMatches: List<StringMatch>?,
|
val stringMatches: List<StringMatch>?,
|
||||||
internal val context: BytecodePatchContext,
|
internal val context: BytecodePatchContext,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* The mutable version of [classDef].
|
* The mutable version of [originalClassDef].
|
||||||
*
|
*
|
||||||
* Accessing this property allocates a [ClassProxy].
|
* Accessing this property allocates a [ClassProxy].
|
||||||
* Use [classDef] if mutable access is not required.
|
* Use [originalClassDef] if mutable access is not required.
|
||||||
*/
|
*/
|
||||||
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
val classDef by lazy { context.proxy(originalClassDef).mutableClass }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mutable version of [method].
|
* The mutable version of [originalMethod].
|
||||||
*
|
*
|
||||||
* Accessing this property allocates a [ClassProxy].
|
* Accessing this property allocates a [ClassProxy].
|
||||||
* Use [method] if mutable access is not required.
|
* Use [originalMethod] if mutable access is not required.
|
||||||
*/
|
*/
|
||||||
val mutableMethod by lazy { mutableClass.methods.first { MethodUtil.methodSignaturesMatch(it, method) } }
|
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A match for an opcode pattern.
|
* A match for an opcode pattern.
|
||||||
@ -336,7 +352,7 @@ class FingerprintBuilder internal constructor(
|
|||||||
*
|
*
|
||||||
* @param returnType The return type compared using [String.startsWith].
|
* @param returnType The return type compared using [String.startsWith].
|
||||||
*/
|
*/
|
||||||
infix fun returns(returnType: String) {
|
fun returns(returnType: String) {
|
||||||
this.returnType = returnType
|
this.returnType = returnType
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -427,19 +443,3 @@ fun fingerprint(
|
|||||||
fuzzyPatternScanThreshold: Int = 0,
|
fuzzyPatternScanThreshold: Int = 0,
|
||||||
block: FingerprintBuilder.() -> Unit,
|
block: FingerprintBuilder.() -> Unit,
|
||||||
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()
|
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [Fingerprint] and add it to the set of fingerprints.
|
|
||||||
*
|
|
||||||
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
|
|
||||||
* @param block The block to build the [Fingerprint].
|
|
||||||
*
|
|
||||||
* @return The created [Fingerprint].
|
|
||||||
*/
|
|
||||||
fun BytecodePatchBuilder.fingerprint(
|
|
||||||
fuzzyPatternScanThreshold: Int = 0,
|
|
||||||
block: FingerprintBuilder.() -> Unit,
|
|
||||||
) = app.revanced.patcher.fingerprint(
|
|
||||||
fuzzyPatternScanThreshold,
|
|
||||||
block,
|
|
||||||
)() // Invoke to add it.
|
|
||||||
|
@ -12,16 +12,12 @@ import java.util.logging.Logger
|
|||||||
* @param temporaryFilesPath A path to a folder to store temporary files in.
|
* @param temporaryFilesPath A path to a folder to store temporary files in.
|
||||||
* @param aaptBinaryPath A path to a custom aapt binary.
|
* @param aaptBinaryPath A path to a custom aapt binary.
|
||||||
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
|
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
|
||||||
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
|
||||||
* This has impact on memory usage and performance.
|
|
||||||
*/
|
*/
|
||||||
class PatcherConfig(
|
class PatcherConfig(
|
||||||
internal val apkFile: File,
|
internal val apkFile: File,
|
||||||
private val temporaryFilesPath: File = File("revanced-temporary-files"),
|
private val temporaryFilesPath: File = File("revanced-temporary-files"),
|
||||||
aaptBinaryPath: String? = null,
|
aaptBinaryPath: String? = null,
|
||||||
frameworkFileDirectory: String? = null,
|
frameworkFileDirectory: String? = null,
|
||||||
@Deprecated("This is going to be removed in the future because it is not needed anymore.")
|
|
||||||
internal val multithreadingDexFileWriter: Boolean = false,
|
|
||||||
) {
|
) {
|
||||||
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
|
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.InternalApi
|
import app.revanced.patcher.*
|
||||||
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
|
||||||
@ -23,6 +21,7 @@ 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.
|
||||||
@ -53,19 +52,52 @@ 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].
|
||||||
*/
|
*/
|
||||||
internal val lookupMaps by lazy { LookupMaps(classes) }
|
internal val lookupMaps by lazy { LookupMaps(classes) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge the extension of this patch.
|
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
|
||||||
|
* If no extension is present, the function will return early.
|
||||||
|
*
|
||||||
|
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
|
||||||
*/
|
*/
|
||||||
internal fun BytecodePatch.mergeExtension() {
|
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
|
||||||
extension?.use { extensionStream ->
|
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
|
||||||
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
|
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
|
||||||
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
|
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
|
||||||
logger.fine("Adding class \"$classDef\"")
|
logger.fine { "Adding class \"$classDef\"" }
|
||||||
|
|
||||||
classes += classDef
|
classes += classDef
|
||||||
lookupMaps.classesByType[classDef.type] = classDef
|
lookupMaps.classesByType[classDef.type] = classDef
|
||||||
@ -73,7 +105,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
|||||||
return@forEach
|
return@forEach
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.")
|
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
|
||||||
|
|
||||||
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
|
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
|
||||||
// If the class was merged, replace the original class with the merged class.
|
// If the class was merged, replace the original class with the merged class.
|
||||||
@ -85,18 +117,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
|||||||
classes += mergedClass
|
classes += mergedClass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} ?: return logger.fine("No extension to merge")
|
} ?: logger.fine("No extension to merge")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a class by its type using a contains check.
|
|
||||||
*
|
|
||||||
* @param type The type of the class.
|
|
||||||
* @return A proxy for the first class that matches the type.
|
|
||||||
*/
|
|
||||||
@Deprecated("Use classBy { type in it.type } instead.", ReplaceWith("classBy { type in it.type }"))
|
|
||||||
fun classByType(type: String) = classBy { type in it.type }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a class with a predicate.
|
* Find a class with a predicate.
|
||||||
*
|
*
|
||||||
@ -145,7 +168,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
|||||||
}.apply {
|
}.apply {
|
||||||
MultiDexIO.writeDexFile(
|
MultiDexIO.writeDexFile(
|
||||||
true,
|
true,
|
||||||
if (config.multithreadingDexFileWriter) -1 else 1,
|
-1,
|
||||||
this,
|
this,
|
||||||
BasicDexFileNamer(),
|
BasicDexFileNamer(),
|
||||||
object : DexFile {
|
object : DexFile {
|
||||||
@ -155,7 +178,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
|
|||||||
override fun getOpcodes() = this@BytecodePatchContext.opcodes
|
override fun getOpcodes() = this@BytecodePatchContext.opcodes
|
||||||
},
|
},
|
||||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
|
||||||
}.listFiles(FileFilter { it.isFile })!!.map {
|
}.listFiles(FileFilter { it.isFile })!!.map {
|
||||||
PatcherResult.PatchedDexFile(it.name, it.inputStream())
|
PatcherResult.PatchedDexFile(it.name, it.inputStream())
|
||||||
}.toSet()
|
}.toSet()
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.Fingerprint
|
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherContext
|
import app.revanced.patcher.PatcherContext
|
||||||
import dalvik.system.DexClassLoader
|
import dalvik.system.DexClassLoader
|
||||||
@ -14,8 +13,8 @@ import java.lang.reflect.Member
|
|||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
|
import java.util.function.Supplier
|
||||||
import java.util.jar.JarFile
|
import java.util.jar.JarFile
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
typealias PackageName = String
|
typealias PackageName = String
|
||||||
typealias VersionName = String
|
typealias VersionName = String
|
||||||
@ -46,10 +45,10 @@ sealed class Patch<C : PatchContext<*>>(
|
|||||||
val dependencies: Set<Patch<*>>,
|
val dependencies: Set<Patch<*>>,
|
||||||
val compatiblePackages: Set<Package>?,
|
val compatiblePackages: Set<Package>?,
|
||||||
options: Set<Option<*>>,
|
options: Set<Option<*>>,
|
||||||
private val executeBlock: Patch<C>.(C) -> Unit,
|
private val executeBlock: (C) -> Unit,
|
||||||
// Must be internal and nullable, so that Patcher.invoke can check,
|
// Must be internal and nullable, so that Patcher.invoke can check,
|
||||||
// if a patch has a finalizing block in order to not emit it twice.
|
// if a patch has a finalizing block in order to not emit it twice.
|
||||||
internal var finalizeBlock: (Patch<C>.(C) -> Unit)?,
|
internal var finalizeBlock: ((C) -> Unit)?,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* The options of the patch.
|
* The options of the patch.
|
||||||
@ -57,35 +56,35 @@ sealed class Patch<C : PatchContext<*>>(
|
|||||||
val options = Options(options)
|
val options = Options(options)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the execution block of the patch.
|
* Calls the execution block of the patch.
|
||||||
* Called by [Patcher].
|
* This function is called by [Patcher.invoke].
|
||||||
*
|
*
|
||||||
* @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
|
* @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
|
||||||
*/
|
*/
|
||||||
internal abstract fun execute(context: PatcherContext)
|
internal abstract fun execute(context: PatcherContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the execution block of the patch.
|
* Calls the execution block of the patch.
|
||||||
*
|
*
|
||||||
* @param context The [PatchContext] to execute the patch with.
|
* @param context The [PatchContext] to execute the patch with.
|
||||||
*/
|
*/
|
||||||
fun execute(context: C) = executeBlock(context)
|
fun execute(context: C) = executeBlock(context)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the finalizing block of the patch.
|
* Calls the finalizing block of the patch.
|
||||||
* Called by [Patcher].
|
* This function is called by [Patcher.invoke].
|
||||||
*
|
*
|
||||||
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
|
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
|
||||||
*/
|
*/
|
||||||
internal abstract fun finalize(context: PatcherContext)
|
internal abstract fun finalize(context: PatcherContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the finalizing block of the patch.
|
* Calls the finalizing block of the patch.
|
||||||
*
|
*
|
||||||
* @param context The [PatchContext] to finalize the patch with.
|
* @param context The [PatchContext] to finalize the patch with.
|
||||||
*/
|
*/
|
||||||
fun finalize(context: C) {
|
fun finalize(context: C) {
|
||||||
finalizeBlock?.invoke(this, context)
|
finalizeBlock?.invoke(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString() = name ?: "Patch"
|
override fun toString() = name ?: "Patch"
|
||||||
@ -127,8 +126,7 @@ internal fun Iterable<Patch<*>>.forEachRecursively(
|
|||||||
* If null, the patch is compatible with all packages.
|
* If null, the patch is compatible with all packages.
|
||||||
* @param dependencies Other patches this patch depends on.
|
* @param dependencies Other patches this patch depends on.
|
||||||
* @param options The options of the patch.
|
* @param options The options of the patch.
|
||||||
* @param fingerprints The fingerprints that are resolved before the patch is executed.
|
* @property extensionInputStream Getter for the extension input stream of the patch.
|
||||||
* @property extension An input stream of the extension resource this patch uses.
|
|
||||||
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
|
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
|
||||||
* @param executeBlock The execution block of the patch.
|
* @param executeBlock The execution block of the patch.
|
||||||
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
|
||||||
@ -143,10 +141,9 @@ class BytecodePatch internal constructor(
|
|||||||
compatiblePackages: Set<Package>?,
|
compatiblePackages: Set<Package>?,
|
||||||
dependencies: Set<Patch<*>>,
|
dependencies: Set<Patch<*>>,
|
||||||
options: Set<Option<*>>,
|
options: Set<Option<*>>,
|
||||||
val fingerprints: Set<Fingerprint>,
|
val extensionInputStream: Supplier<InputStream>?,
|
||||||
val extension: InputStream?,
|
executeBlock: (BytecodePatchContext) -> Unit,
|
||||||
executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
|
finalizeBlock: ((BytecodePatchContext) -> Unit)?,
|
||||||
finalizeBlock: (Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit)?,
|
|
||||||
) : Patch<BytecodePatchContext>(
|
) : Patch<BytecodePatchContext>(
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@ -158,14 +155,7 @@ class BytecodePatch internal constructor(
|
|||||||
finalizeBlock,
|
finalizeBlock,
|
||||||
) {
|
) {
|
||||||
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
|
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
|
||||||
with(context.bytecodeContext) {
|
mergeExtension(this@BytecodePatch)
|
||||||
mergeExtension()
|
|
||||||
}
|
|
||||||
|
|
||||||
fingerprints.forEach {
|
|
||||||
it.match(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(this)
|
execute(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,8 +188,8 @@ class RawResourcePatch internal constructor(
|
|||||||
compatiblePackages: Set<Package>?,
|
compatiblePackages: Set<Package>?,
|
||||||
dependencies: Set<Patch<*>>,
|
dependencies: Set<Patch<*>>,
|
||||||
options: Set<Option<*>>,
|
options: Set<Option<*>>,
|
||||||
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
|
executeBlock: (ResourcePatchContext) -> Unit,
|
||||||
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
|
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
|
||||||
) : Patch<ResourcePatchContext>(
|
) : Patch<ResourcePatchContext>(
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@ -241,8 +231,8 @@ class ResourcePatch internal constructor(
|
|||||||
compatiblePackages: Set<Package>?,
|
compatiblePackages: Set<Package>?,
|
||||||
dependencies: Set<Patch<*>>,
|
dependencies: Set<Patch<*>>,
|
||||||
options: Set<Option<*>>,
|
options: Set<Option<*>>,
|
||||||
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
|
executeBlock: (ResourcePatchContext) -> Unit,
|
||||||
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
|
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
|
||||||
) : Patch<ResourcePatchContext>(
|
) : Patch<ResourcePatchContext>(
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
@ -287,8 +277,8 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||||||
protected var dependencies = mutableSetOf<Patch<*>>()
|
protected var dependencies = mutableSetOf<Patch<*>>()
|
||||||
protected val options = mutableSetOf<Option<*>>()
|
protected val options = mutableSetOf<Option<*>>()
|
||||||
|
|
||||||
protected var executionBlock: (Patch<C>.(C) -> Unit) = { }
|
protected var executionBlock: ((C) -> Unit) = { }
|
||||||
protected var finalizeBlock: (Patch<C>.(C) -> Unit)? = null
|
protected var finalizeBlock: ((C) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an option to the patch.
|
* Add an option to the patch.
|
||||||
@ -347,7 +337,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||||||
*
|
*
|
||||||
* @param block The execution block of the patch.
|
* @param block The execution block of the patch.
|
||||||
*/
|
*/
|
||||||
fun execute(block: Patch<C>.(C) -> Unit) {
|
fun execute(block: C.() -> Unit) {
|
||||||
executionBlock = block
|
executionBlock = block
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +346,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
|
|||||||
*
|
*
|
||||||
* @param block The finalizing block of the patch.
|
* @param block The finalizing block of the patch.
|
||||||
*/
|
*/
|
||||||
fun finalize(block: Patch<C>.(C) -> Unit) {
|
fun finalize(block: C.() -> Unit) {
|
||||||
finalizeBlock = block
|
finalizeBlock = block
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -385,8 +375,7 @@ private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply
|
|||||||
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
|
||||||
* @param description The description of the patch.
|
* @param description The description of the patch.
|
||||||
* @param use Weather or not the patch should be used.
|
* @param use Weather or not the patch should be used.
|
||||||
* @property fingerprints The fingerprints that are resolved before the patch is executed.
|
* @property extensionInputStream Getter for the extension input stream of the patch.
|
||||||
* @property extension An input stream of the extension resource this patch uses.
|
|
||||||
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
|
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
|
||||||
*
|
*
|
||||||
* @constructor Create a new [BytecodePatchBuilder] builder.
|
* @constructor Create a new [BytecodePatchBuilder] builder.
|
||||||
@ -396,27 +385,9 @@ class BytecodePatchBuilder internal constructor(
|
|||||||
description: String?,
|
description: String?,
|
||||||
use: Boolean,
|
use: Boolean,
|
||||||
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
|
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
|
||||||
private val fingerprints = mutableSetOf<Fingerprint>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the fingerprint to the patch.
|
|
||||||
*
|
|
||||||
* @return A wrapper for the fingerprint with the ability to delegate the match to the fingerprint.
|
|
||||||
*/
|
|
||||||
operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) })
|
|
||||||
|
|
||||||
class InvokedFingerprint internal constructor(private val fingerprint: Fingerprint) {
|
|
||||||
// The reason getValue isn't extending the Fingerprint class is
|
|
||||||
// because delegating makes only sense if the fingerprint was previously added to the patch by invoking it.
|
|
||||||
// It may be likely to forget invoking it. By wrapping the fingerprint into this class,
|
|
||||||
// the compiler will throw an error if the fingerprint was not invoked if attempting to delegate the match.
|
|
||||||
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = fingerprint.match
|
|
||||||
?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must be internal for the inlined function "extendWith".
|
// Must be internal for the inlined function "extendWith".
|
||||||
@PublishedApi
|
@PublishedApi
|
||||||
internal var extension: InputStream? = null
|
internal var extensionInputStream: Supplier<InputStream>? = null
|
||||||
|
|
||||||
// Inlining is necessary to get the class loader that loaded the patch
|
// Inlining is necessary to get the class loader that loaded the patch
|
||||||
// to load the extension from the resources.
|
// to load the extension from the resources.
|
||||||
@ -427,8 +398,11 @@ class BytecodePatchBuilder internal constructor(
|
|||||||
*/
|
*/
|
||||||
@Suppress("NOTHING_TO_INLINE")
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
inline fun extendWith(extension: String) = apply {
|
inline fun extendWith(extension: String) = apply {
|
||||||
this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension)
|
val classLoader = object {}.javaClass.classLoader
|
||||||
?: throw PatchException("Extension \"$extension\" not found")
|
|
||||||
|
extensionInputStream = Supplier {
|
||||||
|
classLoader.getResourceAsStream(extension) ?: throw PatchException("Extension \"$extension\" not found")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun build() = BytecodePatch(
|
override fun build() = BytecodePatch(
|
||||||
@ -438,8 +412,7 @@ class BytecodePatchBuilder internal constructor(
|
|||||||
compatiblePackages,
|
compatiblePackages,
|
||||||
dependencies,
|
dependencies,
|
||||||
options,
|
options,
|
||||||
fingerprints,
|
extensionInputStream,
|
||||||
extension,
|
|
||||||
executionBlock,
|
executionBlock,
|
||||||
finalizeBlock,
|
finalizeBlock,
|
||||||
)
|
)
|
||||||
|
@ -60,7 +60,7 @@ internal object ClassMerger {
|
|||||||
|
|
||||||
if (missingMethods.isEmpty()) return this
|
if (missingMethods.isEmpty()) return this
|
||||||
|
|
||||||
logger.fine("Found ${missingMethods.size} missing methods")
|
logger.fine { "Found ${missingMethods.size} missing methods" }
|
||||||
|
|
||||||
return asMutableClass().apply {
|
return asMutableClass().apply {
|
||||||
methods.addAll(missingMethods.map { it.toMutable() })
|
methods.addAll(missingMethods.map { it.toMutable() })
|
||||||
@ -80,7 +80,7 @@ internal object ClassMerger {
|
|||||||
|
|
||||||
if (missingFields.isEmpty()) return this
|
if (missingFields.isEmpty()) return this
|
||||||
|
|
||||||
logger.fine("Found ${missingFields.size} missing fields")
|
logger.fine { "Found ${missingFields.size} missing fields" }
|
||||||
|
|
||||||
return asMutableClass().apply {
|
return asMutableClass().apply {
|
||||||
fields.addAll(missingFields.map { it.toMutable() })
|
fields.addAll(missingFields.map { it.toMutable() })
|
||||||
@ -100,7 +100,7 @@ internal object ClassMerger {
|
|||||||
context.traverseClassHierarchy(this) {
|
context.traverseClassHierarchy(this) {
|
||||||
if (accessFlags.isPublic()) return@traverseClassHierarchy
|
if (accessFlags.isPublic()) return@traverseClassHierarchy
|
||||||
|
|
||||||
logger.fine("Publicizing ${this.type}")
|
logger.fine { "Publicizing ${this.type}" }
|
||||||
|
|
||||||
accessFlags = accessFlags.toPublic()
|
accessFlags = accessFlags.toPublic()
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ internal object ClassMerger {
|
|||||||
|
|
||||||
if (brokenFields.isEmpty()) return this
|
if (brokenFields.isEmpty()) return this
|
||||||
|
|
||||||
logger.fine("Found ${brokenFields.size} broken fields")
|
logger.fine { "Found ${brokenFields.size} broken fields" }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a field public.
|
* Make a field public.
|
||||||
@ -153,7 +153,7 @@ internal object ClassMerger {
|
|||||||
|
|
||||||
if (brokenMethods.isEmpty()) return this
|
if (brokenMethods.isEmpty()) return this
|
||||||
|
|
||||||
logger.fine("Found ${brokenMethods.size} methods")
|
logger.fine { "Found ${brokenMethods.size} methods" }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make a method public.
|
* Make a method public.
|
||||||
|
@ -3,21 +3,21 @@ 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.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
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.assertDoesNotThrow
|
import org.junit.jupiter.api.assertAll
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
import kotlin.test.Test
|
import kotlin.test.*
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
import kotlin.test.assertTrue
|
|
||||||
|
|
||||||
internal object PatcherTest {
|
internal object PatcherTest {
|
||||||
private lateinit var patcher: Patcher
|
private lateinit var patcher: Patcher
|
||||||
@ -151,19 +151,15 @@ internal object PatcherTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `throws if unmatched fingerprint match is delegated`() {
|
fun `throws if unmatched fingerprint match is delegated`() {
|
||||||
val patch = bytecodePatch {
|
val patch = bytecodePatch {
|
||||||
// Fingerprint can never match.
|
|
||||||
val match by fingerprint { }
|
|
||||||
// Manually add the fingerprint.
|
|
||||||
app.revanced.patcher.fingerprint { }()
|
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
|
// Fingerprint can never match.
|
||||||
|
val match by fingerprint { }
|
||||||
|
|
||||||
// Throws, because the fingerprint can't be matched.
|
// Throws, because the fingerprint can't be matched.
|
||||||
match.patternMatch
|
match.patternMatch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(2, patch.fingerprints.size)
|
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
patch().exception != null,
|
patch().exception != null,
|
||||||
"Expected an exception because the fingerprint can't match.",
|
"Expected an exception because the fingerprint can't match.",
|
||||||
@ -172,44 +168,6 @@ internal object PatcherTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `matches fingerprint`() {
|
fun `matches fingerprint`() {
|
||||||
mockClassWithMethod()
|
|
||||||
|
|
||||||
val patches = setOf(bytecodePatch { fingerprint { this returns "V" } })
|
|
||||||
|
|
||||||
assertNull(
|
|
||||||
patches.first().fingerprints.first().match,
|
|
||||||
"Expected fingerprint to be matched before execution.",
|
|
||||||
)
|
|
||||||
|
|
||||||
patches()
|
|
||||||
|
|
||||||
assertDoesNotThrow("Expected fingerprint to be matched.") {
|
|
||||||
assertEquals(
|
|
||||||
"V",
|
|
||||||
patches.first().fingerprints.first().match!!.method.returnType,
|
|
||||||
"Expected fingerprint to be matched.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
|
|
||||||
every { patcher.context.executablePatches } returns toMutableSet()
|
|
||||||
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
|
|
||||||
every { with(patcher.context.bytecodeContext) { any<BytecodePatch>().mergeExtension() } } just runs
|
|
||||||
|
|
||||||
return runBlocking { patcher().toList() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private operator fun Patch<*>.invoke() = setOf(this)().first()
|
|
||||||
|
|
||||||
private fun Any.setPrivateField(field: String, value: Any) {
|
|
||||||
this::class.java.getDeclaredField(field).apply {
|
|
||||||
this.isAccessible = true
|
|
||||||
set(this@setPrivateField, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mockClassWithMethod() {
|
|
||||||
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
|
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
|
||||||
mutableListOf(
|
mutableListOf(
|
||||||
ImmutableClassDef(
|
ImmutableClassDef(
|
||||||
@ -235,6 +193,50 @@ 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") }
|
||||||
|
val fingerprint3 = fingerprint { returns("V") }
|
||||||
|
|
||||||
|
val patches = setOf(
|
||||||
|
bytecodePatch {
|
||||||
|
execute {
|
||||||
|
fingerprint.match(classes.first().methods.first())
|
||||||
|
fingerprint2.match(classes.first())
|
||||||
|
fingerprint3.match
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
patches()
|
||||||
|
|
||||||
|
assertAll(
|
||||||
|
"Expected fingerprints to match.",
|
||||||
|
{ assertNotNull(fingerprint._match) },
|
||||||
|
{ assertNotNull(fingerprint2._match) },
|
||||||
|
{ assertNotNull(fingerprint3._match) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
|
||||||
|
every { patcher.context.executablePatches } returns toMutableSet()
|
||||||
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
|
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
|
||||||
|
every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs
|
||||||
|
|
||||||
|
return runBlocking { patcher().toList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private operator fun Patch<*>.invoke() = setOf(this)().first()
|
||||||
|
|
||||||
|
private fun Any.setPrivateField(field: String, value: Any) {
|
||||||
|
this::class.java.getDeclaredField(field).apply {
|
||||||
|
this.isAccessible = true
|
||||||
|
set(this@setPrivateField, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint
|
|
||||||
import kotlin.test.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@ -24,23 +23,6 @@ internal object PatchTest {
|
|||||||
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
|
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `can create patch with fingerprints`() {
|
|
||||||
val externalFingerprint = fingerprint {}
|
|
||||||
|
|
||||||
val patch = bytecodePatch(name = "Test") {
|
|
||||||
val externalFingerprintMatch by externalFingerprint()
|
|
||||||
val internalFingerprintMatch by fingerprint {}
|
|
||||||
|
|
||||||
execute {
|
|
||||||
externalFingerprintMatch.method
|
|
||||||
internalFingerprintMatch.method
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEquals(2, patch.fingerprints.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `can create patch with dependencies`() {
|
fun `can create patch with dependencies`() {
|
||||||
val patch = bytecodePatch(name = "Test") {
|
val patch = bytecodePatch(name = "Test") {
|
||||||
|
Loading…
Reference in New Issue
Block a user