From eb4db5a9701e37155db9c4eded386dd6f5ceb4b9 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 17 Jun 2024 02:56:46 +0200 Subject: [PATCH] refactor fingerprint apis --- api/revanced-patcher.api | 51 +- docs/1_patcher_intro.md | 11 +- docs/2_1_setup.md | 3 +- docs/2_2_1_fingerprinting.md | 122 ++-- docs/2_2_patch_anatomy.md | 27 +- docs/2_patches_intro.md | 24 +- docs/3_structure_and_conventions.md | 17 +- docs/4_apis.md | 11 +- .../app/revanced/patcher/Fingerprint.kt | 467 ++++++++++++++++ .../kotlin/app/revanced/patcher/Patcher.kt | 20 +- .../app/revanced/patcher/PatcherContext.kt | 8 +- .../revanced/patcher/fingerprint/LookupMap.kt | 126 ----- .../patcher/fingerprint/MethodFingerprint.kt | 524 ------------------ .../patcher/patch/BytecodePatchContext.kt | 151 ++++- .../app/revanced/patcher/patch/Patch.kt | 25 +- .../app/revanced/patcher/PatcherTest.kt | 40 +- .../app/revanced/patcher/patch/PatchTest.kt | 8 +- 17 files changed, 823 insertions(+), 812 deletions(-) create mode 100644 src/main/kotlin/app/revanced/patcher/Fingerprint.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index bc69b4b9..0c090962 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -1,6 +1,53 @@ +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 fun ()V + public final fun accessFlags (I)V + public final fun accessFlags ([Lcom/android/tools/smali/dexlib2/AccessFlags;)V + public final fun custom (Lkotlin/jvm/functions/Function2;)V + public final fun opcodes (Ljava/lang/String;)V + public final fun opcodes ([Lcom/android/tools/smali/dexlib2/Opcode;)V + public final fun parameters ([Ljava/lang/String;)V + public final fun returns (Ljava/lang/String;)V + public final fun strings ([Ljava/lang/String;)V +} + +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 (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;)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/Fingerprint; +} + public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation { } +public final class app/revanced/patcher/Match { + public fun (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 ()Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method; + public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; + public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch; + public final fun getStringMatches ()Ljava/util/List; +} + +public final class app/revanced/patcher/Match$PatternMatch { + public fun (II)V + public final fun getEndIndex ()I + public final fun getStartIndex ()I +} + +public final class app/revanced/patcher/Match$StringMatch { + public fun (Ljava/lang/String;I)V + public final fun getIndex ()I + public final fun getString ()Ljava/lang/String; +} + public final class app/revanced/patcher/PackageMetadata { public final fun getPackageName ()Ljava/lang/String; public final fun getPackageVersion ()Ljava/lang/String; @@ -168,8 +215,8 @@ public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patch 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 final fun getValue (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public final fun invoke (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprint; + public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match; + public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Fingerprint; } public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext { diff --git a/docs/1_patcher_intro.md b/docs/1_patcher_intro.md index f3fee8f6..45cd084c 100644 --- a/docs/1_patcher_intro.md +++ b/docs/1_patcher_intro.md @@ -64,11 +64,14 @@ In order to create patches for Android applications, you first need to understan ## 📙 How it works -ReVanced Patcher is a library that allows you to modify Android applications by applying patches to their APKs. It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool) for resource decoding and encoding. -ReVanced Patcher accepts a list of patches and integrations, and applies them to a given APK file. It then returns the modified components of the APK file, such as modified dex files and resources, that can be repackaged into a new APK file. +ReVanced Patcher is a library that allows you to modify Android applications by applying patches to their APKs. +It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool) for resource decoding and encoding. +ReVanced Patcher accepts a list of patches and integrations, and applies them to a given APK file. +It then returns the modified components of the APK file, such as modified dex files and resources, +that can be repackaged into a new APK file. -ReVanced Patcher has a simple API that allows you to load patches and integrations from JAR files and apply them to an APK file. -Later on, you will learn how to create patches. +ReVanced Patcher has a simple API that allows you to load patches and integrations from JAR files +and apply them to an APK file. Later on, you will learn how to create patches. ```kt val patches = loadPatchesFromJar(setOf(File("revanced-patches.jar"))) diff --git a/docs/2_1_setup.md b/docs/2_1_setup.md index f4b8fb65..b333f9d0 100644 --- a/docs/2_1_setup.md +++ b/docs/2_1_setup.md @@ -98,7 +98,8 @@ Throughout the documentation, [ReVanced Patches](https://github.com/revanced/rev 3. Open the project in your IDE > [!TIP] -> It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation). +> It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches +> by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation). ## ⏭️ What's next diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md index 3b8cf3f3..c720abe8 100644 --- a/docs/2_2_1_fingerprinting.md +++ b/docs/2_2_1_fingerprinting.md @@ -60,9 +60,10 @@ # 🔎 Fingerprinting -In the context of ReVanced, fingerprinting is primarily used to resolve methods with a limited amount of known information. +In the context of ReVanced, fingerprinting is primarily 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. -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. +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. ## ⛳️ Example fingerprint @@ -72,13 +73,13 @@ Throughout the documentation, the following example will be used to demonstrate package app.revanced.patches.ads.fingerprints -methodFingerprint { +fingerprint { accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) returns("Z") parameters("Z") opcodes(Opcode.RETURN) strings("pro") - custom { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;" } + custom { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;" } } ``` @@ -106,7 +107,7 @@ The fingerprint contains the following information: - Package and class name: ```kt - custom = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;"} + custom = { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;"} ``` With this information, the original code can be reconstructed: @@ -129,20 +130,22 @@ With this information, the original code can be reconstructed: > [!TIP] > A fingerprint should contain information about a method likely to remain the same across updates. -> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app. In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same. +> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app. +> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same. ## 🔨 How to use fingerprints -Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually. Fingerprints added to a patch are resolved by ReVanced Patcher before the patch is executed. +Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually. +Fingerprints added to a patch are matched by ReVanced Patcher before the patch is executed. ```kt -val fingerprint = methodFingerprint { +val fingerprint = fingerprint { // ... } val patch = bytecodePatch { // Directly create and add a fingerprint. - methodFingerprint { + fingerprint { // ... } @@ -152,15 +155,15 @@ val patch = bytecodePatch { ``` > [!TIP] -> Multiple patches can share fingerprints. If a fingerprint is resolved once, it will not be resolved again. +> Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again. > [!TIP] -> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode` function to fuzzy match the pattern. -> Opcode pattern arrays can contain `null` values to indicate that the opcode at the index is unknown. -> Any opcode will match to a `null` value: +> 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 -> methodFingerprint(fuzzyPatternScanThreshhold = 0.5) { +> fingerprint(fuzzyPatternScanThreshhold = 2) { > opcodes( > Opcode.ICONST_0, > null, @@ -170,29 +173,31 @@ val patch = bytecodePatch { >} > ``` -Once the fingerprint is resolved, the result can be used in the patch: +Once the fingerprint is matched, the match can be used in the patch: ```kt val patch = bytecodePatch { - // Add a fingerprint and delegate its result to a variable. - val result by showAdsFingerprint() + // Add a fingerprint and delegate its match to a variable. + val match by showAdsFingerprint() execute { - val method = result.method + val method = match.method } } ``` > [!WARNING] -> If the fingerprint can not be resolved because it does not match any method, the result of a fingerprint is `null`. If the result is delegated to a variable, accessing it will raise an exception. +> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If the match is delegated +> to a variable, accessing it will raise an exception. -The result of a fingerprint that resolved successfully contains mutable and immutable references to the method and the class it is defined in. +The match of a fingerprint contains mutable and immutable references to the method and the class it is defined in. ```kt -class MethodFingerprintResult( +class Match( val method: Method, val classDef: ClassDef, - val scanResult: MethodFingerprintScanResult, + val patternMatch: Match.PatternMatch?, + val stringMatches: List?, // ... ) { val mutableClass by lazy { /* ... */ } @@ -200,73 +205,59 @@ class MethodFingerprintResult( // ... } - -class MethodFingerprintScanResult( - val patternScanResult: PatternScanResult?, - val stringsScanResult: StringsScanResult?, -) { - class StringsScanResult(val matches: List) { - class StringMatch(val string: String, val index: Int) - } - - class PatternScanResult( - val startIndex: Int, - val endIndex: Int, - // ... - ) { - // ... - } -} ``` -## 🏹 Manual resolution of fingerprints +## 🏹 Manual matching of fingerprints -Unless a fingerprint is added to a patch, the fingerprint will not be resolved automatically by ReVanced Patcher before the patch is executed. -Instead, the fingerprint can be resolved manually using various overloads of a fingerprint's `resolve` function. +Unless a fingerprint is added to a patch, the fingerprint will not be matched automatically by ReVanced Patcher +before the patch is executed. +Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function. -You can resolve a fingerprint in the following ways: +You can match a fingerprint the following ways: -- On a **list of classes**, if the fingerprint can resolve on a known subset of classes +- In a **list of classes**, if the fingerprint can match in a known subset of classes - If you have a known list of classes you know the fingerprint can resolve on, you can resolve the fingerprint on the list of classes: + If you have a known list of classes you know the fingerprint can match in, +you can match the fingerprint on the list of classes: ```kt - execute { - val result = showAdsFingerprint.also { - it.resolve(context, this.classes) - }.result ?: throw PatchException("showAdsFingerprint not found") + execute { context -> + val match = showAdsFingerprint.apply { + match(context, context.classes) + }.match ?: throw PatchException("showAdsFingerprint not found") } ``` -- On a **single class**, if the fingerprint can resolve on a single known class +- In a **single class**, if the fingerprint can match in a single known class - If you know the fingerprint can resolve to a method in a specific class, you can resolve the fingerprint on the class: + If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class: ```kt - execute { - val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } + execute { context -> + val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } - val result = showAdsFingerprint.also { - it.resolve(context, adsLoaderClass) - }.result ?: throw PatchException("showAdsFingerprint not found") + val match = showAdsFingerprint.apply { + match(context, adsLoaderClass) + }.match ?: throw PatchException("showAdsFingerprint not found") } ``` -- On a **single method**, to extract certain information about a method +- Match a **single method**, to extract certain information about it - The result of a fingerprint contains useful information about the method, such as the start and end index of an opcode pattern or the indices of the instructions with certain string references. + The match of a fingerprint contains useful information about the method, such as the start and end index of an opcode pattern +or the indices of the instructions with certain string references. A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out: ```kt - execute { - val proStringsFingerprint = methodFingerprint { + execute { context -> + val proStringsFingerprint = fingerprint { strings("free", "trial") } - proStringsFingerprint.also { - it.resolve(context, adsFingerprintResult.method) - }.result?.let { result -> - result.scanResult.stringsScanResult!!.matches.forEach { match -> + proStringsFingerprint.apply { + match(context, adsFingerprintMatch.method) + }.match?.let { match -> + match.stringMatches.forEach { match -> println("The index of the string '${match.string}' is ${match.index}") } } ?: throw PatchException("pro strings fingerprint not found") @@ -274,7 +265,8 @@ You can resolve a fingerprint in the following ways: ``` > [!TIP] -> To see real-world examples of fingerprints, check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). +> To see real-world examples of fingerprints, +> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). ## ⏭️ What's next diff --git a/docs/2_2_patch_anatomy.md b/docs/2_2_patch_anatomy.md index 0c084e70..89b94368 100644 --- a/docs/2_2_patch_anatomy.md +++ b/docs/2_2_patch_anatomy.md @@ -77,12 +77,12 @@ val disableAdsPatch = bytecodePatch( dependsOn(disableAdsResourcePatch) - val showAdsFingerprintResult by methodFingerprint { + val showAdsMatch by methodFingerprint { // ... } execute { - showAdsFingerprintResult.mutableMethod.addInstructions( + showAdsMatch.mutableMethod.addInstructions( 0, """ # Return false. @@ -99,23 +99,29 @@ val disableAdsPatch = bytecodePatch( > - Patches do not require a name, but `PatchLoader` will only load named patches. > - Patches can depend on others. Dependencies are executed first. > The dependent patch will not be executed if a dependency raises an exception. -> - A patch can declare compatibility with specific packages and versions, but patches can still be executed on any package or version. It is recommended to declare explicit compatibility to list known compatible packages. +> - A patch can declare compatibility with specific packages and versions, +> but patches can still be executed on any package or version. +> It is recommended to declare explicit compatibility to list known compatible packages. > - If `compatibleWith` is not called, the patch is compatible with any package > - If a package is specified with no versions, the patch is compatible with any version of the package -> - If an empty array of versions is specified, the patch is not compatible with any version of the package. This is useful for declaring explicit incompatibility with a specific package. +> - If an empty array of versions is specified, the patch is not compatible with any version of the package. +> This is useful for declaring explicit incompatibility with a specific package. > - This patch uses a fingerprint to find the method and replaces the method's instructions with new instructions. -> The fingerprint is resolved on the classes present in `BytecodePatchContext`. +> The fingerprint is matched in the classes present in `BytecodePatchContext`. > Fingerprints will be explained in more detail on the next page. -> - A patch can raise a `PatchException` at any time to indicate that the patch failed to execute. Any other `Exception` or `Throwable` raised will be wrapped in a `PatchException`. +> - A patch can raise a `PatchException` at any time to indicate that the patch failed to execute. +> Any other `Exception` or `Throwable` raised will be wrapped in a `PatchException`. > [!WARNING] > -> - Circular dependencies are not allowed. If a patch depends on another patch, the other patch cannot depend on the first patch. +> - Circular dependencies are not allowed. If a patch depends on another patch, +> the other patch cannot depend on the first patch. > - Dependencies inherit compatibility from dependant patches. > [!TIP] -> To see real-world examples of patches, check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). +> To see real-world examples of patches, +> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). ## 🧩 Patch API @@ -147,7 +153,10 @@ val patch = bytecodePatch(name = "Patch") { } ``` -Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`. The finalization blocks are called in reverse order of patch execution, which means, first, the finalization block of `Patch`, then the finalization block of `Dependency` is called. The output of the above patch would be `1234`. The same order is followed for multiple patches depending on the patch. +Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`. +The finalization blocks are called in reverse order of patch execution, which means, +first, the finalization block of `Patch`, then the finalization block of `Dependency` is called. +The output of the above patch would be `1234`. The same order is followed for multiple patches depending on the patch. ### ⚙️ Patch options diff --git a/docs/2_patches_intro.md b/docs/2_patches_intro.md index f3123897..c80a4696 100644 --- a/docs/2_patches_intro.md +++ b/docs/2_patches_intro.md @@ -65,19 +65,30 @@ Learn the basic concepts of ReVanced Patcher and how to create patches. ## 📙 Fundamentals A patch is a piece of code that modifies an Android application. -There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode, the APK resources, or arbitrary files in the APK: +There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode, +the APK resources, or arbitrary files in the APK: - A `BytecodePatch` modifies the Dalvik VM bytecode - A `ResourcePatch` modifies (decoded) resources - A `RawResourcePatch` modifies arbitrary files -Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way. +Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies +before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way. -The `execute` function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed. The `execute` function receives an instance of a context object that provides access to the APK. The patch can use this context to modify the APK. +The `execute` function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed. +The `execute` function receives an instance of a context object that provides access to the APK. +The patch can use this context to modify the APK. -Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources. +Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs +to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources. -The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources if it is supplied a `ResourcePatch` for execution or if any patch depends on a `ResourcePatch` and will not decode the resources before executing `RawResourcePatch`. Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK, whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case. Decoding and building resources is a time- and resource-consuming, so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`. +The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources +if it is supplied a `ResourcePatch` for execution or if any patch depends on a `ResourcePatch` +and will not decode the resources before executing `RawResourcePatch`. +Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK, +whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case. +Decoding and building resources is a time- and resource-consuming, +so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`. Example of patches: @@ -105,7 +116,8 @@ val resourcePatch = rawResourcePatch { ``` > [!TIP] -> To see real-world examples of patches, check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). +> To see real-world examples of patches, +> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). ## ⏭️ Whats next diff --git a/docs/3_structure_and_conventions.md b/docs/3_structure_and_conventions.md index 94853d4a..1c21dcc8 100644 --- a/docs/3_structure_and_conventions.md +++ b/docs/3_structure_and_conventions.md @@ -80,16 +80,23 @@ Patches are organized in a specific way. The file structure looks as follows: - 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `Remove ads`. If a patch changes the color of a button, name it `Change button color` - 🔥 Write the patch description in the third person, present tense, and end it with a period. - If a patch removes ads, the description can be omitted because of redundancy, but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._ -- 🔥 Write patches with modularity and reusability in mind. Patches can depend on each other, so it is important to write patches in a way that can be used in different contexts. + If a patch removes ads, the description can be omitted because of redundancy, +but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._ +- 🔥 Write patches with modularity and reusability in mind. Patches can depend on each other, +so it is important to write patches in a way that can be used in different contexts. - 🔥🔥 Keep patches as minimal as possible. This reduces the risk of failing patches. Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch, - you can write code in integrations. Integrations are compiled classes merged into the app before patches are executed as described in [💉 Introduction to ReVanced Patcher](1_patcher_intro). + you can write code in integrations. Integrations are compiled classes merged into the app +before patches are executed as described in [💉 Introduction to ReVanced Patcher](1_patcher_intro). Patches can then reference methods and classes from integrations. A real-world example of integrations can be found in the [ReVanced Integrations](https://github.com/ReVanced/revanced-integrations) repository - 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change. - In the example of an obfuscated method, it's better to fingerprint the method by its return type and parameters rather than its name because the name is likely to change. An intelligent selection of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates. -- 🔥🔥🔥 Document your patches. Patches are abstract, so it is important to document parts of the code that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks of instructions that are modified or added to a method + In the example of an obfuscated method, it's better to fingerprint the method by its return type +and parameters rather than its name because the name is likely to change. An intelligent selection +of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates. +- 🔥🔥🔥 Document your patches. Patches are abstract, so it is important to document parts of the code +that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks +of instructions that are modified or added to a method ## ⏭️ What's next diff --git a/docs/4_apis.md b/docs/4_apis.md index ad384dcb..a2368cd7 100644 --- a/docs/4_apis.md +++ b/docs/4_apis.md @@ -5,9 +5,10 @@ A handful of APIs are available to make patch development easier and more effici ## 📙 Overview 1. 👹 Mutate classes with `context.proxy(ClassDef)` -2. 🔍 Find and proxy existing classes with `classBy(Predicate)` +2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)` 3. 🏃‍ Easily access referenced methods recursively by index with `MethodNavigator` -4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications (Available in ReVanced Patches) +4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications +(Available in ReVanced Patches) 5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)` 6. 📃 Read and write DOM files using `ResourcePatchContext.document` @@ -18,5 +19,9 @@ A handful of APIs are available to make patch development easier and more effici ## 🎉 Afterword -ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches that outlive app updates. Patches make up ReVanced; without you, the community of patch developers, ReVanced would not be what it is today. We hope that this documentation has been helpful to you and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help, talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or feature request, +ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches +that outlive app updates. Patches make up ReVanced; without you, the community of patch developers, +ReVanced would not be what it is today. We hope that this documentation has been helpful to you +and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help, +talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or feature request, ReVanced diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt new file mode 100644 index 00000000..67ad21c5 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -0,0 +1,467 @@ +@file:Suppress("unused", "MemberVisibilityCanBePrivate") + +package app.revanced.patcher + +import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.BytecodePatchContext.MethodLookupMaps.Companion.appendParameters +import app.revanced.patcher.patch.MethodClassPairs +import app.revanced.patcher.util.proxy.ClassProxy +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +/** + * A fingerprint. + * + * @param accessFlags The exact access flags using values of [AccessFlags]. + * @param returnType The return type. Compared using [String.startsWith]. + * @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType]. + * @param opcodes A pattern of instruction opcodes. `null` can be used as a wildcard. + * @param strings A list of the strings. Compared using [String.contains]. + * @param custom A custom condition for this fingerprint. + * @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern. + */ +class Fingerprint internal constructor( + internal val accessFlags: Int?, + internal val returnType: String?, + internal val parameters: List?, + internal val opcodes: List?, + internal val strings: List?, + internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?, + private val fuzzyPatternScanThreshold: Int, +) { + /** + * The match for this [Fingerprint]. Null if unmatched. + */ + var match: Match? = null + private set + + /** + * Match using [BytecodePatchContext.MethodLookupMaps]. + * + * Generally faster than the other [match] overloads when there are many methods to check for a match. + * + * Fingerprints can be optimized for performance: + * - Slowest: Specify [custom] or [opcodes] and nothing else. + * - Fast: Specify [accessFlags], [returnType]. + * - Faster: Specify [accessFlags], [returnType] and [parameters]. + * - 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. + * @return True if a match was found or if the fingerprint is already matched to a method, false otherwise. + */ + internal fun match(context: BytecodePatchContext): Boolean { + val lookupMaps = context.methodLookupMaps + + fun Fingerprint.match(methodClasses: MethodClassPairs): Boolean { + methodClasses.forEach { (classDef, method) -> + if (match(context, classDef, method)) return true + } + return false + } + + // TODO: If only one string is necessary, why not use a single string for every fingerprint? + fun Fingerprint.lookupByStrings() = strings?.firstNotNullOfOrNull { lookupMaps.byStrings[it] } + if (lookupByStrings()?.let(::match) == true) { + return true + } + + // No strings declared or none matched (partial matches are allowed). + // Use signature matching. + fun Fingerprint.lookupBySignature(): MethodClassPairs { + if (accessFlags == null) return lookupMaps.all + + var returnTypeValue = returnType + if (returnTypeValue == null) { + if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { + // Constructors always have void return type. + returnTypeValue = "V" + } else { + return lookupMaps.all + } + } + + val signature = + buildString { + append(accessFlags) + append(returnTypeValue.first()) + appendParameters(parameters ?: return@buildString) + } + + return lookupMaps.bySignature[signature] ?: return MethodClassPairs() + } + return match(lookupBySignature()) + } + + /** + * Match using a [ClassDef]. + * + * @param classDef The class to match against. + * @param context The context to create mutable proxies for the matched method and its class. + * @return True if a match was found or if the fingerprint is already matched to a method, false otherwise. + */ + fun match( + context: BytecodePatchContext, + classDef: ClassDef, + ): Boolean { + for (method in classDef.methods) { + if (match(context, method, classDef)) { + return true + } + } + return false + } + + /** + * Match using a [Method]. + * The class is retrieved from the method. + * + * @param method The method to match against. + * @param context The context to create mutable proxies for the matched method and its class. + * @return True if a match was found or if the fingerprint is already matched to a method, false otherwise. + */ + fun match( + context: BytecodePatchContext, + method: Method, + ) = match(context, method, context.classByType(method.definingClass)!!.immutableClass) + + /** + * Match using a [Method]. + * + * @param method The method to match against. + * @param classDef The class the method is a member of. + * @param context The context to create mutable proxies for the matched method and its class. + * @return True if a match was found or if the fingerprint is already matched to a method, false otherwise. + */ + internal fun match( + context: BytecodePatchContext, + method: Method, + classDef: ClassDef, + ): Boolean { + if (match != null) return true + + if (returnType != null && !method.returnType.startsWith(returnType)) { + return false + } + + if (accessFlags != null && accessFlags != method.accessFlags) { + return false + } + + fun parametersEqual( + parameters1: Iterable, + parameters2: Iterable, + ): Boolean { + if (parameters1.count() != parameters2.count()) return false + val iterator1 = parameters1.iterator() + parameters2.forEach { + if (!it.startsWith(iterator1.next())) return false + } + return true + } + + // TODO: parseParameters() + if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) { + return false + } + + if (custom != null && !custom.invoke(method, classDef)) { + return false + } + + val stringMatches: List? = + if (strings != null) { + buildList { + val instructions = method.instructionsOrNull ?: return false + + val stringsList = strings.toMutableList() + + instructions.forEachIndexed { instructionIndex, instruction -> + if ( + instruction.opcode != Opcode.CONST_STRING && + instruction.opcode != Opcode.CONST_STRING_JUMBO + ) { + return@forEachIndexed + } + + val string = ((instruction as ReferenceInstruction).reference as StringReference).string + val index = stringsList.indexOfFirst(string::contains) + if (index == -1) return@forEachIndexed + + add(Match.StringMatch(string, instructionIndex)) + stringsList.removeAt(index) + } + + if (stringsList.isNotEmpty()) return false + } + } else { + null + } + + val patternMatch = if (opcodes != null) { + val instructions = method.instructionsOrNull ?: return false + + fun patternScan(): Match.PatternMatch? { + val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold + + val instructionLength = instructions.count() + val patternLength = opcodes.size + + for (index in 0 until instructionLength) { + var patternIndex = 0 + var threshold = fingerprintFuzzyPatternScanThreshold + + while (index + patternIndex < instructionLength) { + val originalOpcode = instructions.elementAt(index + patternIndex).opcode + val patternOpcode = opcodes.elementAt(patternIndex) + + if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { + // Reaching maximum threshold (0) means, + // the pattern does not match to the current instructions. + if (threshold-- == 0) break + } + + if (patternIndex < patternLength - 1) { + // If the entire pattern has not been scanned yet, continue the scan. + patternIndex++ + continue + } + + // The entire pattern has been scanned. + return Match.PatternMatch( + index, + index + patternIndex, + ) + } + } + + return null + } + + patternScan() ?: return false + } else { + null + } + + match = Match( + method, + classDef, + patternMatch, + stringMatches, + context, + ) + + return true + } +} + +/** + * A match for a [Fingerprint]. + * + * @param method The matching method. + * @param classDef The class the matching method is a member of. + * @param patternMatch The match for the opcode pattern. + * @param stringMatches The matches for the strings. + * @param context The context to create mutable proxies in. + */ +class Match( + val method: Method, + val classDef: ClassDef, + val patternMatch: PatternMatch?, + val stringMatches: List?, + internal val context: BytecodePatchContext, +) { + /** + * The mutable version of [classDef]. + * + * Accessing this property allocates a [ClassProxy]. + * Use [classDef] if mutable access is not required. + */ + val mutableClass by lazy { context.proxy(classDef).mutableClass } + + /** + * The mutable version of [method]. + * + * Accessing this property allocates a [ClassProxy]. + * Use [method] if mutable access is not required. + */ + val mutableMethod by lazy { mutableClass.methods.first { MethodUtil.methodSignaturesMatch(it, method) } } + + /** + * A match for an opcode pattern. + * @param startIndex The index of the first opcode of the pattern in the method. + * @param endIndex The index of the last opcode of the pattern in the method. + */ + class PatternMatch( + val startIndex: Int, + val endIndex: Int, + ) + + /** + * A match for a string. + * + * @param string The string that matched. + * @param index The index of the instruction in the method. + */ + class StringMatch(val string: String, val index: Int) +} + +/** + * A builder for [Fingerprint]. + * + * @property accessFlags The exact access flags using values of [AccessFlags]. + * @property returnType The return type compared using [String.startsWith]. + * @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. + * @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`. + * @property strings A list of the strings compared each using [String.contains]. + * @property customBlock A custom condition for this fingerprint. + * @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. + * + * @constructor Create a new [FingerprintBuilder]. + */ +class FingerprintBuilder internal constructor( + private val fuzzyPatternScanThreshold: Int = 0, +) { + private var accessFlags: Int? = null + private var returnType: String? = null + private var parameters: List? = null + private var opcodes: List? = null + private var strings: List? = null + private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null + + /** + * Set the access flags. + * + * @param accessFlags The exact access flags using values of [AccessFlags]. + */ + fun accessFlags(accessFlags: Int) { + this.accessFlags = accessFlags + } + + /** + * Set the access flags. + * + * @param accessFlags The exact access flags using values of [AccessFlags]. + */ + fun accessFlags(vararg accessFlags: AccessFlags) { + this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value } + } + + /** + * Set the return type. + * + * @param returnType The return type compared using [String.startsWith]. + */ + infix fun returns(returnType: String) { + this.returnType = returnType + } + + /** + * Set the parameters. + * + * @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. + */ + fun parameters(vararg parameters: String) { + this.parameters = parameters.toList() + } + + /** + * Set the opcodes. + * + * @param opcodes An opcode pattern of instructions. + * Wildcard or unknown opcodes can be specified by `null`. + */ + fun opcodes(vararg opcodes: Opcode?) { + this.opcodes = opcodes.toList() + } + + /** + * Set the opcodes. + * + * @param instructions A list of instructions or opcode names in SMALI format. + * - Wildcard or unknown opcodes can be specified by `null`. + * - Empty lines are ignored. + * - Each instruction must be on a new line. + * - The opcode name is enough, no need to specify the operands. + * + * @throws Exception If an unknown opcode is used. + */ + fun opcodes(instructions: String) { + this.opcodes = instructions.trimIndent().split("\n").filter { + it.isNotBlank() + }.map { + // Remove any operands. + val name = it.split(" ", limit = 1).first().trim() + if (name == "null") return@map null + + opcodesByName[name] ?: throw Exception("Unknown opcode: $name") + } + } + + /** + * Set the strings. + * + * @param strings A list of strings compared each using [String.contains]. + */ + fun strings(vararg strings: String) { + this.strings = strings.toList() + } + + /** + * Set a custom condition for this fingerprint. + * + * @param customBlock A custom condition for this fingerprint. + */ + fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) { + this.customBlock = customBlock + } + + internal fun build() = Fingerprint( + accessFlags, + returnType, + parameters, + opcodes, + strings, + customBlock, + fuzzyPatternScanThreshold, + ) + + private companion object { + val opcodesByName = Opcode.entries.associateBy { it.name } + } +} + +/** + * Create a [Fingerprint]. + * + * @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0. + * @param block The block to build the [Fingerprint]. + * + * @return The created [Fingerprint]. + */ +fun fingerprint( + fuzzyPatternScanThreshold: Int = 0, + block: FingerprintBuilder.() -> Unit, +) = 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. diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index a1e0843d..16e75ccb 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,8 +1,6 @@ package app.revanced.patcher -import app.revanced.patcher.fingerprint.LookupMap import app.revanced.patcher.patch.* -import app.revanced.patcher.patch.ResourcePatchContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.Closeable @@ -42,7 +40,7 @@ class Patcher( val context = PatcherContext(config) init { - context.resourcePatchContext.decodeResources(ResourcePatchContext.ResourceMode.NONE) + context.resourceContext.decodeResources(ResourcePatchContext.ResourceMode.NONE) } /** @@ -89,7 +87,7 @@ class Patcher( // Check, if integrations need to be merged. for (patch in patches) { if (patch.anyRecursively { it.requiresIntegrations }) { - context.bytecodePatchContext.integrations.merge = true + context.bytecodeContext.integrations.merge = true break } } @@ -99,7 +97,7 @@ class Patcher( // region Add integrations - context.bytecodePatchContext.integrations.addAll(integrations) + context.bytecodeContext.integrations.addAll(integrations) // endregion } @@ -145,13 +143,11 @@ class Patcher( }.also { executedPatches[this] = it } } - if (context.bytecodePatchContext.integrations.merge) context.bytecodePatchContext.integrations.flush() - - LookupMap.initializeLookupMaps(context.bytecodePatchContext) + if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() // Prevent from decoding the app manifest twice if it is not needed. if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) { - context.resourcePatchContext.decodeResources(config.resourceMode) + context.resourceContext.decodeResources(config.resourceMode) } logger.info("Executing patches") @@ -207,7 +203,7 @@ class Patcher( } } - override fun close() = LookupMap.clearLookupMaps() + override fun close() = context.bytecodeContext.methodLookupMaps.close() /** * Compile and save the patched APK file. @@ -217,7 +213,7 @@ class Patcher( @OptIn(InternalApi::class) override fun get() = PatcherResult( - context.bytecodePatchContext.get(), - context.resourcePatchContext.get(), + context.bytecodeContext.get(), + context.resourceContext.get(), ) } diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt index f6fbf89f..54e6ddc6 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt @@ -29,12 +29,12 @@ class PatcherContext internal constructor(config: PatcherConfig) { internal val allPatches = mutableSetOf>() /** - * A context for patches containing the current state of the resources. + * The context for patches containing the current state of the resources. */ - internal val resourcePatchContext = ResourcePatchContext(packageMetadata, config) + internal val resourceContext = ResourcePatchContext(packageMetadata, config) /** - * A context for patches containing the current state of the bytecode. + * The context for patches containing the current state of the bytecode. */ - internal val bytecodePatchContext = BytecodePatchContext(config) + internal val bytecodeContext = BytecodePatchContext(config) } diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt deleted file mode 100644 index a9b2b05b..00000000 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt +++ /dev/null @@ -1,126 +0,0 @@ -package app.revanced.patcher.fingerprint - -import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull -import app.revanced.patcher.patch.BytecodePatchContext -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.ClassDef -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.StringReference -import java.util.* - -internal typealias MethodClassPair = Pair - -/** - * Lookup map for methods. - */ -internal class LookupMap : MutableMap by mutableMapOf() { - /** - * Adds a [MethodClassPair] to the list associated with the given key. - * If the key does not exist, a new list is created and the [MethodClassPair] is added to it. - */ - fun add( - key: String, - methodClassPair: MethodClassPair, - ) { - getOrPut(key) { MethodClassList() }.add(methodClassPair) - } - - /** - * List of methods and the class they are a member of. - */ - internal class MethodClassList : LinkedList() - - companion object Maps { - /** - * A list of methods and the class they are a member of. - */ - internal val methods = MethodClassList() - - /** - * Lookup map for methods keyed to the methods access flags, return type and parameter. - */ - internal val methodSignatureLookupMap = LookupMap() - - /** - * Lookup map for methods associated by strings referenced in the method. - */ - internal val methodStringsLookupMap = LookupMap() - - /** - * Initializes lookup maps for [MethodFingerprint] resolution - * using attributes of methods such as the method signature or strings. - * - * @param context The [BytecodePatchContext] containing the classes to initialize the lookup maps with. - */ - internal fun initializeLookupMaps(context: BytecodePatchContext) { - if (methods.isNotEmpty()) clearLookupMaps() - - context.classes.forEach { classDef -> - classDef.methods.forEach { method -> - val methodClassPair = method to classDef - - // For fingerprints with no access or return type specified. - methods += methodClassPair - - val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first() - - // Add as the key. - methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair) - - // Add [parameters] as the key. - methodSignatureLookupMap.add( - buildString { - append(accessFlagsReturnKey) - appendParameters(method.parameterTypes) - }, - methodClassPair, - ) - - // Add strings contained in the method as the key. - method.instructionsOrNull?.forEach instructions@{ instruction -> - if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) { - return@instructions - } - - val string = ((instruction as ReferenceInstruction).reference as StringReference).string - - methodStringsLookupMap.add(string, methodClassPair) - } - - // In the future, the class type could be added to the lookup map. - // This would require MethodFingerprint to be changed to include the class type. - } - } - } - - /** - * Clears the internal lookup maps created in [initializeLookupMaps]. - */ - internal fun clearLookupMaps() { - methods.clear() - methodSignatureLookupMap.clear() - methodStringsLookupMap.clear() - } - - /** - * Appends a string based on the parameter reference types of this method. - */ - internal fun StringBuilder.appendParameters(parameters: Iterable) { - // Maximum parameters to use in the signature key. - // Some apps have methods with an incredible number of parameters (over 100 parameters have been seen). - // To keep the signature map from becoming needlessly bloated, - // group together in the same map entry all methods with the same access/return and 5 or more parameters. - // The value of 5 was chosen based on local performance testing and is not set in stone. - val maxSignatureParameters = 5 - // Must append a unique value before the parameters to distinguish this key includes the parameters. - // If this is not appended, then methods with no parameters - // will collide with different keys that specify access/return but omit the parameters. - append("p:") - parameters.forEachIndexed { index, parameter -> - if (index >= maxSignatureParameters) return - append(parameter.first()) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt deleted file mode 100644 index 5f750a9c..00000000 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt +++ /dev/null @@ -1,524 +0,0 @@ -@file:Suppress("unused", "MemberVisibilityCanBePrivate") - -package app.revanced.patcher.fingerprint - -import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull -import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters -import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps -import app.revanced.patcher.fingerprint.LookupMap.Maps.methodSignatureLookupMap -import app.revanced.patcher.fingerprint.LookupMap.Maps.methodStringsLookupMap -import app.revanced.patcher.fingerprint.LookupMap.Maps.methods -import app.revanced.patcher.fingerprint.MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult -import app.revanced.patcher.patch.BytecodePatchBuilder -import app.revanced.patcher.patch.BytecodePatchContext -import app.revanced.patcher.util.proxy.ClassProxy -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.ClassDef -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.StringReference -import com.android.tools.smali.dexlib2.util.MethodUtil - -/** - * A fingerprint to resolve methods. - * - * @param accessFlags The exact access flags using values of [AccessFlags]. - * @param returnType The return type compared using [String.startsWith]. - * @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. - * @param opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`. - * @param strings A list of the strings compared each using [String.contains]. - * @param custom A custom condition for this fingerprint. - * @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. - */ -@Suppress("MemberVisibilityCanBePrivate") -class MethodFingerprint internal constructor( - internal val accessFlags: Int? = null, - internal val returnType: String? = null, - internal val parameters: List? = null, - internal val opcodes: List? = null, - internal val strings: List? = null, - internal val custom: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null, - private val fuzzyPatternScanThreshold: Int = 0, -) { - /** - * The result of the [MethodFingerprint]. - */ - var result: MethodFingerprintResult? = null - private set - - /** - * Resolve a [MethodFingerprint] using the lookup map built by [initializeLookupMaps]. - * - * [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable - * amount of time because they are resolved in sequence. - * - * For apps with many fingerprints, resolving performance can be improved by: - * - Slowest: Specify [opcodes] and nothing else. - * - Fast: Specify [accessFlags], [returnType]. - * - Faster: Specify [accessFlags], [returnType] and [parameters]. - * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. - */ - internal fun resolveUsingLookupMap(context: BytecodePatchContext): Boolean { - /** - * Lookup [MethodClassPair]s that match the methods strings present in a [MethodFingerprint]. - * - * @return A list of [MethodClassPair]s that match the methods strings present in a [MethodFingerprint]. - */ - fun MethodFingerprint.methodStringsLookup(): LookupMap.MethodClassList? { - strings?.forEach { - val methods = methodStringsLookupMap[it] - if (methods != null) return methods - } - return null - } - - /** - * Lookup [MethodClassPair]s that match the method signature present in a [MethodFingerprint]. - * - * @return A list of [MethodClassPair]s that match the method signature present in a [MethodFingerprint]. - */ - fun MethodFingerprint.methodSignatureLookup(): LookupMap.MethodClassList { - if (accessFlags == null) return methods - - var returnTypeValue = returnType - if (returnTypeValue == null) { - if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { - // Constructors always have void return type - returnTypeValue = "V" - } else { - return methods - } - } - - val key = - buildString { - append(accessFlags) - append(returnTypeValue.first()) - if (parameters != null) appendParameters(parameters) - } - return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList() - } - - /** - * Resolve a [MethodFingerprint] using a list of [MethodClassPair]. - * - * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. - */ - fun MethodFingerprint.resolveUsingMethodClassPair(methodClasses: LookupMap.MethodClassList): Boolean { - methodClasses.forEach { (classDef, method) -> - if (resolve(context, classDef, method)) return true - } - return false - } - - if (methodStringsLookup()?.let(::resolveUsingMethodClassPair) == true) { - return true - } - - // No strings declared or none matched (partial matches are allowed). - // Use signature matching. - return resolveUsingMethodClassPair(methodSignatureLookup()) - } - - /** - * Resolve a [MethodFingerprint] against a [ClassDef]. - * - * @param forClass The class in which to resolve the [MethodFingerprint]. - * @param context The [BytecodePatchContext] to create mutable proxies. - * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. - */ - fun resolve( - context: BytecodePatchContext, - forClass: ClassDef, - ): Boolean { - for (method in forClass.methods) { - if (resolve(context, method, forClass)) { - return true - } - } - return false - } - - /** - * Resolve a [MethodFingerprint] against a [Method]. - * - * @param method The method in which to resolve the [MethodFingerprint]. - * @param classDef The class in which to resolve the [MethodFingerprint]. - * @param context The [BytecodePatchContext] to create mutable proxies. - * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. - */ - fun resolve( - context: BytecodePatchContext, - method: Method, - classDef: ClassDef, - ): Boolean { - if (result != null) return true - - if (returnType != null && !method.returnType.startsWith(returnType)) { - return false - } - - if (accessFlags != null && accessFlags != method.accessFlags) { - return false - } - - fun parametersEqual( - parameters1: Iterable, - parameters2: Iterable, - ): Boolean { - if (parameters1.count() != parameters2.count()) return false - val iterator1 = parameters1.iterator() - parameters2.forEach { - if (!it.startsWith(iterator1.next())) return false - } - return true - } - - // TODO: parseParameters() - if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) { - return false - } - - if (custom != null && !custom.invoke(method, classDef)) { - return false - } - - val stringsScanResult = - if (strings != null) { - StringsScanResult( - buildList { - val instructions = method.instructionsOrNull ?: return false - - val stringsList = strings.toMutableList() - - instructions.forEachIndexed { instructionIndex, instruction -> - if ( - instruction.opcode != Opcode.CONST_STRING && - instruction.opcode != Opcode.CONST_STRING_JUMBO - ) { - return@forEachIndexed - } - - val string = ((instruction as ReferenceInstruction).reference as StringReference).string - val index = stringsList.indexOfFirst(string::contains) - if (index == -1) return@forEachIndexed - - add(StringsScanResult.StringMatch(string, instructionIndex)) - stringsList.removeAt(index) - } - - if (stringsList.isNotEmpty()) return false - }, - ) - } else { - null - } - - val patternScanResult = if (opcodes != null) { - val instructions = method.instructionsOrNull ?: return false - - fun patternScan(): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { - val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold - - val instructionLength = instructions.count() - val patternLength = opcodes.size - - for (index in 0 until instructionLength) { - var patternIndex = 0 - var threshold = fingerprintFuzzyPatternScanThreshold - - while (index + patternIndex < instructionLength) { - val originalOpcode = instructions.elementAt(index + patternIndex).opcode - val patternOpcode = opcodes.elementAt(patternIndex) - - if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { - // Reaching maximum threshold (0) means, - // the pattern does not match to the current instructions. - if (threshold-- == 0) break - } - - if (patternIndex < patternLength - 1) { - // If the entire pattern has not been scanned yet, continue the scan. - patternIndex++ - continue - } - - // The entire pattern has been scanned. - return MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( - index, - index + patternIndex, - ) - } - } - - return null - } - - patternScan() ?: return false - } else { - null - } - - result = MethodFingerprintResult( - method, - classDef, - MethodFingerprintResult.MethodFingerprintScanResult( - patternScanResult, - stringsScanResult, - ), - context, - ) - - return true - } -} - -/** - * Resolve a list of [MethodFingerprint] using the lookup map built by [initializeLookupMaps]. - * - * [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable - * amount of time because they are resolved in sequence. - * - * For apps with many fingerprints, resolving performance can be improved by: - * - Slowest: Specify [MethodFingerprint.opcodes] and nothing else. - * - Fast: Specify [MethodFingerprint.accessFlags], [MethodFingerprint.returnType]. - * - Faster: Specify [MethodFingerprint.accessFlags], [MethodFingerprint.returnType] and [MethodFingerprint.parameters]. - * - Fastest: Specify [MethodFingerprint.strings], with at least one string being an exact (non-partial) match. - */ -internal fun Set.resolveUsingLookupMap(context: BytecodePatchContext) = forEach { fingerprint -> - fingerprint.resolveUsingLookupMap(context) -} - -/** - * Resolve a list of [MethodFingerprint] against a list of [ClassDef]. - * - * @param classes The classes in which to resolve the [MethodFingerprint]. - * @param context The [BytecodePatchContext] to create mutable proxies. - * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. - */ -fun Iterable.resolve( - context: BytecodePatchContext, - classes: Iterable, -) = forEach { fingerprint -> - classes.forEach { - if (fingerprint.resolve(context, it)) return@resolve - } -} - -/** - * Represents the result of a [MethodFingerprintResult]. - * - * @param method The matching method. - * @param classDef The [ClassDef] that contains the matching [method]. - * @param scanResult The result of scanning for the [MethodFingerprint]. - * @param context The [BytecodePatchContext] this [MethodFingerprintResult] is attached to, to create proxies. - */ - -class MethodFingerprintResult( - val method: Method, - val classDef: ClassDef, - val scanResult: MethodFingerprintScanResult, - internal val context: BytecodePatchContext, -) { - /** - * Returns a mutable clone of [classDef] - * - * Please note, this method allocates a [ClassProxy]. - * Use [classDef] where possible. - */ - val mutableClass by lazy { - context.proxy(classDef).mutableClass - } - - /** - * Returns a mutable clone of [method] - * - * Please note, this method allocates a [ClassProxy]. - * Use [method] where possible. - */ - val mutableMethod by lazy { - mutableClass.methods.first { - MethodUtil.methodSignaturesMatch(it, this.method) - } - } - - /** - * The result of scanning on the [MethodFingerprint]. - * @param patternScanResult The result of the pattern scan. - * @param stringsScanResult The result of the string scan. - */ - class MethodFingerprintScanResult( - val patternScanResult: PatternScanResult?, - val stringsScanResult: StringsScanResult?, - ) { - /** - * The result of scanning strings on the [MethodFingerprint]. - * @param matches The list of strings that were matched. - */ - class StringsScanResult(val matches: List) { - /** - * Represents a match for a string at an index. - * @param string The string that was matched. - * @param index The index of the string. - */ - class StringMatch(val string: String, val index: Int) - } - - /** - * The result of a pattern scan. - * @param startIndex The start index of the instructions where to which this pattern matches. - * @param endIndex The end index of the instructions where to which this pattern matches. - */ - class PatternScanResult( - val startIndex: Int, - val endIndex: Int, - ) - } -} - -/** - * A builder for [MethodFingerprint]. - * - * @property accessFlags The exact access flags using values of [AccessFlags]. - * @property returnType The return type compared using [String.startsWith]. - * @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. - * @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`. - * @property strings A list of the strings compared each using [String.contains]. - * @property customBlock A custom condition for this fingerprint. - * @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. - * - * @constructor Create a new [MethodFingerprintBuilder]. - */ -class MethodFingerprintBuilder internal constructor( - private val fuzzyPatternScanThreshold: Int = 0, -) { - private var accessFlags: Int? = null - private var returnType: String? = null - private var parameters: List? = null - private var opcodes: List? = null - private var strings: List? = null - private var customBlock: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null - - /** - * Set the access flags. - * - * @param accessFlags The exact access flags using values of [AccessFlags]. - */ - fun accessFlags(accessFlags: Int) { - this.accessFlags = accessFlags - } - - /** - * Set the access flags. - * - * @param accessFlags The exact access flags using values of [AccessFlags]. - */ - fun accessFlags(vararg accessFlags: AccessFlags) { - this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value } - } - - /** - * Set the return type. - * - * @param returnType The return type compared using [String.startsWith]. - */ - infix fun returns(returnType: String) { - this.returnType = returnType - } - - /** - * Set the parameters. - * - * @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType]. - */ - fun parameters(vararg parameters: String) { - this.parameters = parameters.toList() - } - - /** - * Set the opcodes. - * - * @param opcodes An opcode pattern of instructions. - * Wildcard or unknown opcodes can be specified by `null`. - */ - fun opcodes(vararg opcodes: Opcode?) { - this.opcodes = opcodes.toList() - } - - /** - * Set the opcodes. - * - * @param instructions A list of instructions or opcode names in SMALI format. - * - Wildcard or unknown opcodes can be specified by `null`. - * - Empty lines are ignored. - * - Each instruction must be on a new line. - * - The opcode name is enough, no need to specify the operands. - * - * @throws Exception If an unknown opcode is used. - */ - fun opcodes(instructions: String) { - this.opcodes = instructions.trimIndent().split("\n").filter { - it.isNotBlank() - }.map { - // Remove any operands. - val name = it.split(" ", limit = 1).first().trim() - if (name == "null") return@map null - - opcodesByName[name] ?: throw Exception("Unknown opcode: $name") - } - } - - /** - * Set the strings. - * - * @param strings A list of strings compared each using [String.contains]. - */ - fun strings(vararg strings: String) { - this.strings = strings.toList() - } - - /** - * Set a custom condition for this fingerprint. - * - * @param customBlock A custom condition for this fingerprint. - */ - fun custom(customBlock: (methodDef: Method, classDef: ClassDef) -> Boolean) { - this.customBlock = customBlock - } - - internal fun build() = MethodFingerprint(accessFlags, returnType, parameters, opcodes, strings, customBlock) - - private companion object { - val opcodesByName = Opcode.entries.associateBy { it.name } - } -} - -/** - * Create a [MethodFingerprint]. - * - * @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0. - * @param block The block to build the [MethodFingerprint]. - * - * @return The created [MethodFingerprint]. - */ -fun methodFingerprint( - fuzzyPatternScanThreshold: Int = 0, - block: MethodFingerprintBuilder.() -> Unit, -) = MethodFingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build() - -/** - * Create a [MethodFingerprint] 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 [MethodFingerprint]. - * - * @return The created [MethodFingerprint]. - */ -fun BytecodePatchBuilder.methodFingerprint( - fuzzyPatternScanThreshold: Int = 0, - block: MethodFingerprintBuilder.() -> Unit, -) = app.revanced.patcher.fingerprint.methodFingerprint( - fuzzyPatternScanThreshold, - block, -)() // Invoke to add to its set of fingerprints. diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index 9741bac5..767f9d7d 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -4,20 +4,26 @@ import app.revanced.patcher.InternalApi import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherContext import app.revanced.patcher.PatcherResult +import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.MethodNavigator import app.revanced.patcher.util.ProxyClassList import app.revanced.patcher.util.proxy.ClassProxy +import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcodes import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.DexFile import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.MultiDexIO +import java.io.Closeable import java.io.File import java.io.FileFilter import java.io.Flushable +import java.util.* import java.util.logging.Logger /** @@ -26,29 +32,31 @@ import java.util.logging.Logger * @param config The [PatcherConfig] used to create this context. */ @Suppress("MemberVisibilityCanBePrivate") -class BytecodePatchContext internal constructor(private val config: PatcherConfig) : - PatchContext> { +class BytecodePatchContext internal constructor(private val config: PatcherConfig) : PatchContext> { private val logger = Logger.getLogger(BytecodePatchContext::class.java.name) /** * [Opcodes] of the supplied [PatcherConfig.apkFile]. */ - internal lateinit var opcodes: Opcodes + internal val opcodes: Opcodes /** * The list of classes. */ - val classes by lazy { - ProxyClassList( - MultiDexIO.readDexFile( - true, - config.apkFile, - BasicDexFileNamer(), - null, - null, - ).also { opcodes = it.opcodes }.classes.toMutableList(), - ) - } + val classes = ProxyClassList( + MultiDexIO.readDexFile( + true, + config.apkFile, + BasicDexFileNamer(), + null, + null, + ).also { opcodes = it.opcodes }.classes.toMutableList(), + ) + + /** + * The lookup maps for methods and the class they are a member of from the [classes]. + */ + internal val methodLookupMaps by lazy { MethodLookupMaps(classes) } /** * The [Integrations] of this [PatcherContext]. @@ -131,7 +139,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi /** * The integrations of a [PatcherContext]. */ - internal inner class Integrations : MutableList by mutableListOf(), Flushable { + internal inner class Integrations : + MutableList by mutableListOf(), + Flushable { /** * Whether to merge integrations. * Set to true, if the field requiresIntegrations of any supplied [Patch] is true. @@ -178,4 +188,115 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi clear() } } + + /** + * A lookup map for methods and the class they are a member of. + * + * @param classes The list of classes to create the lookup maps from. + */ + internal class MethodLookupMaps internal constructor(classes: List) : Closeable { + /** + * All methods and the class they are a member of. + */ + internal val all = MethodClassPairs() + + /** + * Methods associated by its access flags, return type and parameter. + */ + internal val bySignature = MethodClassPairsLookupMap() + + /** + * Methods associated by strings referenced in it. + */ + internal val byStrings = MethodClassPairsLookupMap() + + init { + classes.forEach { classDef -> + classDef.methods.forEach { method -> + val methodClassPair: MethodClassPair = method to classDef + + // For fingerprints with no access or return type specified. + all += methodClassPair + + val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first() + + // Add as the key. + bySignature[accessFlagsReturnKey] = methodClassPair + + // Add [parameters] as the key. + bySignature[ + buildString { + append(accessFlagsReturnKey) + appendParameters(method.parameterTypes) + }, + ] = methodClassPair + + // Add strings contained in the method as the key. + method.instructionsOrNull?.forEach instructions@{ instruction -> + if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) { + return@instructions + } + + val string = ((instruction as ReferenceInstruction).reference as StringReference).string + + byStrings[string] = methodClassPair + } + + // In the future, the class type could be added to the lookup map. + // This would require MethodFingerprint to be changed to include the class type. + } + } + } + + internal companion object { + /** + * Appends a string based on the parameter reference types of this method. + */ + internal fun StringBuilder.appendParameters(parameters: Iterable) { + // Maximum parameters to use in the signature key. + // Some apps have methods with an incredible number of parameters (over 100 parameters have been seen). + // To keep the signature map from becoming needlessly bloated, + // group together in the same map entry all methods with the same access/return and 5 or more parameters. + // The value of 5 was chosen based on local performance testing and is not set in stone. + val maxSignatureParameters = 5 + // Must append a unique value before the parameters to distinguish this key includes the parameters. + // If this is not appended, then methods with no parameters + // will collide with different keys that specify access/return but omit the parameters. + append("p:") + parameters.forEachIndexed { index, parameter -> + if (index >= maxSignatureParameters) return + append(parameter.first()) + } + } + } + + override fun close() { + all.clear() + bySignature.clear() + byStrings.clear() + } + } +} + +/** + * A pair of a [Method] and the [ClassDef] it is a member of. + */ +internal typealias MethodClassPair = Pair + +/** + * A list of [MethodClassPair]s. + */ +internal typealias MethodClassPairs = LinkedList + +/** + * A lookup map for [MethodClassPairs]s. + * The key is a string and the value is a list of [MethodClassPair]s. + */ +internal class MethodClassPairsLookupMap : MutableMap by mutableMapOf() { + /** + * Add a [MethodClassPair] associated by any key. + * If the key does not exist, a new list is created and the [MethodClassPair] is added to it. + */ + internal operator fun set(key: String, methodClassPair: MethodClassPair) = + apply { getOrPut(key) { MethodClassPairs() }.add(methodClassPair) } } diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 9cac9aea..12a7442a 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -2,10 +2,9 @@ package app.revanced.patcher.patch +import app.revanced.patcher.Fingerprint import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherContext -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.fingerprint.resolveUsingLookupMap import dalvik.system.DexClassLoader import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.MultiDexIO @@ -110,7 +109,7 @@ class BytecodePatch internal constructor( compatiblePackages: Set?, dependencies: Set>, options: Set>, - val fingerprints: Set, + val fingerprints: Set, executeBlock: (Patch.(BytecodePatchContext) -> Unit), finalizeBlock: (Patch.(BytecodePatchContext) -> Unit), ) : Patch( @@ -125,12 +124,12 @@ class BytecodePatch internal constructor( finalizeBlock, ) { override fun execute(context: PatcherContext) { - fingerprints.resolveUsingLookupMap(context.bytecodePatchContext) + fingerprints.forEach { it.match(context.bytecodeContext) } - execute(context.bytecodePatchContext) + execute(context.bytecodeContext) } - override fun finalize(context: PatcherContext) = finalize(context.bytecodePatchContext) + override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext) override fun toString() = name ?: "BytecodePatch" } @@ -174,9 +173,9 @@ class RawResourcePatch internal constructor( executeBlock, finalizeBlock, ) { - override fun execute(context: PatcherContext) = execute(context.resourcePatchContext) + override fun execute(context: PatcherContext) = execute(context.resourceContext) - override fun finalize(context: PatcherContext) = finalize(context.resourcePatchContext) + override fun finalize(context: PatcherContext) = finalize(context.resourceContext) override fun toString() = name ?: "RawResourcePatch" } @@ -220,9 +219,9 @@ class ResourcePatch internal constructor( executeBlock, finalizeBlock, ) { - override fun execute(context: PatcherContext) = execute(context.resourcePatchContext) + override fun execute(context: PatcherContext) = execute(context.resourceContext) - override fun finalize(context: PatcherContext) = finalize(context.resourcePatchContext) + override fun finalize(context: PatcherContext) = finalize(context.resourceContext) override fun toString() = name ?: "ResourcePatch" } @@ -349,16 +348,16 @@ class BytecodePatchBuilder internal constructor( use: Boolean, requiresIntegrations: Boolean, ) : PatchBuilder(name, description, use, requiresIntegrations) { - private val fingerprints = mutableSetOf() + private val fingerprints = mutableSetOf() /** * Add the fingerprint to the patch. */ - operator fun MethodFingerprint.invoke() = apply { + operator fun Fingerprint.invoke() = apply { fingerprints.add(this) } - operator fun MethodFingerprint.getValue(nothing: Nothing?, property: KProperty<*>) = result + operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>) = match ?: throw PatchException("Cannot delegate unresolved fingerprint result to ${property.name}.") override fun build() = BytecodePatch( diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 5264d14b..3dab966c 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -1,7 +1,9 @@ package app.revanced.patcher -import app.revanced.patcher.fingerprint.methodFingerprint -import app.revanced.patcher.patch.* +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchResult +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.ProxyClassList import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef import com.android.tools.smali.dexlib2.immutable.ImmutableMethod @@ -35,8 +37,8 @@ internal object PatcherTest { Logger.getAnonymousLogger(), ) - every { context.bytecodePatchContext.classes } returns mockk(relaxed = true) - every { context.bytecodePatchContext.integrations } returns mockk(relaxed = true) + every { context.bytecodeContext.classes } returns mockk(relaxed = true) + every { context.bytecodeContext.integrations } returns mockk(relaxed = true) every { apply(false) } answers { callOriginal() } } } @@ -73,16 +75,16 @@ internal object PatcherTest { } @Test - fun `throws if unresolved fingerprint result is delegated`() { + fun `throws if unmatched fingerprint match is delegated`() { val patch = bytecodePatch { - // Fingerprint can never be resolved. - val result by methodFingerprint { } + // Fingerprint can never match. + val match by fingerprint { } // Manually add the fingerprint. - app.revanced.patcher.fingerprint.methodFingerprint { }() + app.revanced.patcher.fingerprint { }() execute { - // Throws, because the fingerprint can't be resolved. - result.scanResult + // Throws, because the fingerprint can't be matched. + match.patternMatch } } @@ -90,28 +92,28 @@ internal object PatcherTest { assertTrue( patch().exception != null, - "Expected an exception because the fingerprint can't be resolved.", + "Expected an exception because the fingerprint can't match.", ) } @Test - fun `resolves fingerprint`() { + fun `matches fingerprint`() { mockClassWithMethod() - val patches = setOf(bytecodePatch { methodFingerprint { this returns "V" }() }) + val patches = setOf(bytecodePatch { fingerprint { this returns "V" }() }) assertNull( - patches.first().fingerprints.first().result, - "Expected fingerprint to be unresolved before execution.", + patches.first().fingerprints.first().match, + "Expected fingerprint to be matched before execution.", ) patches() - assertDoesNotThrow("Expected fingerprint to be resolved.") { + assertDoesNotThrow("Expected fingerprint to be matched.") { assertEquals( "V", - patches.first().fingerprints.first().result!!.method.returnType, - "Expected fingerprint to be resolved.", + patches.first().fingerprints.first().match!!.method.returnType, + "Expected fingerprint to be matched.", ) } } @@ -131,7 +133,7 @@ internal object PatcherTest { } private fun mockClassWithMethod() { - every { patcher.context.bytecodePatchContext.classes } returns ProxyClassList( + every { patcher.context.bytecodeContext.classes } returns ProxyClassList( mutableListOf( ImmutableClassDef( "class", diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt index 7e8abfd8..7411fcee 100644 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt +++ b/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt @@ -1,6 +1,6 @@ package app.revanced.patcher.patch -import app.revanced.patcher.fingerprint.methodFingerprint +import app.revanced.patcher.fingerprint import kotlin.test.Test import kotlin.test.assertEquals @@ -26,15 +26,15 @@ internal object PatchTest { @Test fun `can create patch with fingerprints`() { - val externalFingerprint = methodFingerprint {} + val externalFingerprint = fingerprint {} val patch = bytecodePatch(name = "Test") { val result by externalFingerprint() - val internalFingerprint = methodFingerprint {} + val internalFingerprint = fingerprint {} execute { result.method - internalFingerprint.result + internalFingerprint.match } }