1
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:
oSumAtrIX 2024-10-27 16:04:30 +01:00 committed by GitHub
parent aa472eb985
commit 0abf1c6c02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 360 additions and 365 deletions

View File

@ -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 {

View File

@ -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" }
} }
} }

View File

@ -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

View File

@ -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")
} }
``` ```

View File

@ -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")
} }
} }
``` ```

View File

@ -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.
} }
} }
``` ```

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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()

View File

@ -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,
) )

View File

@ -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.

View File

@ -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)
}
} }
} }

View File

@ -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") {