From 0abf1c6c0279708fdef5cb66b141d07d17682693 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 27 Oct 2024 16:04:30 +0100 Subject: [PATCH] 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. --- api/revanced-patcher.api | 49 ++--- docs/1_patcher_intro.md | 4 +- docs/2_1_setup.md | 4 + docs/2_2_1_fingerprinting.md | 188 ++++++++++-------- docs/2_2_patch_anatomy.md | 26 ++- docs/2_patches_intro.md | 10 +- docs/4_apis.md | 12 +- .../app/revanced/patcher/Fingerprint.kt | 140 ++++++------- .../app/revanced/patcher/PatcherConfig.kt | 4 - .../patcher/patch/BytecodePatchContext.kt | 63 ++++-- .../app/revanced/patcher/patch/Patch.kt | 89 +++------ .../app/revanced/patcher/util/ClassMerger.kt | 10 +- .../app/revanced/patcher/PatcherTest.kt | 102 +++++----- .../app/revanced/patcher/patch/PatchTest.kt | 18 -- 14 files changed, 357 insertions(+), 362 deletions(-) diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 483c7d6d..b33270ea 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -1,7 +1,4 @@ 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 { @@ -18,20 +15,17 @@ public final class app/revanced/patcher/FingerprintBuilder { 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/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 (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 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 getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; + public final fun getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public final fun getOriginalMethod ()Lcom/android/tools/smali/dexlib2/iface/Method; public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch; 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 fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (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 { @@ -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 fun getExtension ()Ljava/io/InputStream; - public final fun getFingerprints ()Ljava/util/Set; + public final fun getExtensionInputStream ()Ljava/util/function/Supplier; public fun toString ()Ljava/lang/String; } 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 extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder; - public final fun getExtension ()Ljava/io/InputStream; - public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint; - 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 fun getExtensionInputStream ()Ljava/util/function/Supplier; + public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V } 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 classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy; public fun close ()V public synthetic fun get ()Ljava/lang/Object; public fun get ()Ljava/util/Set; public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList; + public final fun getMatch (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Match; + public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match; + public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; + public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match; public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/MethodNavigator; 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 synthetic fun (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 (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 finalize (Lapp/revanced/patcher/patch/PatchContext;)V 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 ([Lkotlin/Pair;)V public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V - public final fun execute (Lkotlin/jvm/functions/Function2;)V - public final fun finalize (Lkotlin/jvm/functions/Function2;)V + public final fun execute (Lkotlin/jvm/functions/Function1;)V + public final fun finalize (Lkotlin/jvm/functions/Function1;)V protected final fun getCompatiblePackages ()Ljava/util/Set; protected final fun getDependencies ()Ljava/util/Set; protected final fun getDescription ()Ljava/lang/String; - protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2; - protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2; + protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1; + protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1; protected final fun getName ()Ljava/lang/String; protected final fun getOptions ()Ljava/util/Set; 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; protected final fun setCompatiblePackages (Ljava/util/Set;)V protected final fun setDependencies (Ljava/util/Set;)V - protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V - protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V + protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V + protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V } public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier { diff --git a/docs/1_patcher_intro.md b/docs/1_patcher_intro.md index 8664236c..e3f6872b 100644 --- a/docs/1_patcher_intro.md +++ b/docs/1_patcher_intro.md @@ -89,9 +89,9 @@ val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { pat runBlocking { patcher().collect { patchResult -> if (patchResult.exception != null) - logger.info("\"${patchResult.patch}\" failed:\n${patchResult.exception}") + logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" } else - logger.info("\"${patchResult.patch}\" succeeded") + logger.info { "\"${patchResult.patch}\" succeeded" } } } diff --git a/docs/2_1_setup.md b/docs/2_1_setup.md index b333f9d0..4cfb9b57 100644 --- a/docs/2_1_setup.md +++ b/docs/2_1_setup.md @@ -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. +> [!NOTE] +> To start a fresh project, +> you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template). + 1. Clone the repository ```bash diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md index b93372db..a9619443 100644 --- a/docs/2_2_1_fingerprinting.md +++ b/docs/2_2_1_fingerprinting.md @@ -60,14 +60,16 @@ # 🔎 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. 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 -Throughout the documentation, the following example will be used to demonstrate the concepts of fingerprints: +An example fingerprint is shown below: ```kt @@ -79,11 +81,11 @@ fingerprint { parameters("Z") opcodes(Opcode.RETURN) 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. @@ -107,27 +109,29 @@ The fingerprint contains the following information: - Package and class name: ```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: ```java - package com.some.app.ads; +package com.some.app.ads; - class AdsLoader { - public final boolean (boolean ) { - // ... + class AdsLoader { + public final boolean (boolean ) { + // ... - var userStatus = "pro"; + var userStatus = "pro"; - // ... + // ... - return ; - } + return ; } +} ``` +Using that fingerprint, this method can be matched uniquely from all other methods. + > [!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. @@ -135,8 +139,8 @@ With this information, the original code can be reconstructed: ## 🔨 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 matched by ReVanced Patcher before the patch is executed. +A fingerprint is matched to a method, +once the `match` property of the fingerprint is accessed in a patch's `execute` scope: ```kt val fingerprint = fingerprint { @@ -144,48 +148,46 @@ val fingerprint = fingerprint { } val patch = bytecodePatch { - // Directly create and add a fingerprint. - fingerprint { - // ... + execute { + val match = fingerprint.match!! } - - // Add a fingerprint manually by invoking it. - fingerprint() } ``` -> [!TIP] -> Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again. +The fingerprint won't be matched again, if it has already been matched once. +This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint: -> [!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, -> ) ->} -> ``` +```kt +// Either of these two patches will match the fingerprint first and the other patch can reuse the match: +val mainActivityPatch1 = bytecodePatch { + execute { + val match = mainActivityOnCreateFingerprint.match!! + } +} -Once the fingerprint is matched, the match can be used in the patch: +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 { - // Add a fingerprint and delegate its match to a variable. - val match by showAdsFingerprint() - val match2 by fingerprint { - // ... - } - execute { - val method = match.method - val method2 = match2.method + // 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 > 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 class Match( - val method: Method, - val classDef: ClassDef, + val originalMethod: Method, + val originalClassDef: ClassDef, val patternMatch: Match.PatternMatch?, val stringMatches: List?, // ... ) { - val mutableClass by lazy { /* ... */ } - val mutableMethod by lazy { /* ... */ } + val classDef 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. + +> [!TIP] +> If only read-only access to the class or method is needed, +> the `originalClassDef` and `originalMethod` properties can be used, +> to avoid making a mutable copy of the class or method. + +## 🏹 Manually matching fingerprints -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. +By default, a fingerprint is matched automatically against all classes when the `match` property is accessed. -You can match a fingerprint the following ways: +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 @@ -225,11 +250,9 @@ You can match a fingerprint the following ways: you can match the fingerprint on the list of classes: ```kt - execute { context -> - val match = showAdsFingerprint.apply { - match(context, context.classes) - }.match ?: throw PatchException("No match found") - } + execute { + val match = showAdsFingerprint.match(classes) ?: throw PatchException("No match found") + } ``` - 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: ```kt - execute { context -> - val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } + execute { + val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } + + val match = showAdsFingerprint.match(context, adsLoaderClass) ?: 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. - val match = showAdsFingerprint.apply { - match(context, adsLoaderClass) - }.match ?: throw PatchException("No match found") + ```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 - 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. + 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 { context -> - val proStringsFingerprint = fingerprint { - strings("free", "trial") - } + execute { + val currentPlanFingerprint = fingerprint { + strings("free", "trial") + } - 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("No match found") + currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match -> + match.stringMatches.forEach { match -> + println("The index of the string '${match.string}' is ${match.index}") + } + } ?: throw PatchException("No match found") } ``` diff --git a/docs/2_2_patch_anatomy.md b/docs/2_2_patch_anatomy.md index e6137e9c..42764f6c 100644 --- a/docs/2_2_patch_anatomy.md +++ b/docs/2_2_patch_anatomy.md @@ -76,23 +76,23 @@ val disableAdsPatch = bytecodePatch( ) { 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) - // 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") - - // 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. 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, // call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file) // to enable or disable ads. - showAdsMatch.mutableMethod.addInstructions( + showAdsMatch.method.addInstructions( 0, """ 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: ```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 val option = stringOption(key = "option") @@ -183,11 +183,9 @@ and use it in a patch: ```kt val patch = bytecodePatch(name = "Complex patch") { extendWith("complex-patch.rve") - - val match by methodFingerprint() - + execute { - match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V") + fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V") } } ``` diff --git a/docs/2_patches_intro.md b/docs/2_patches_intro.md index 062ad1e1..fe0f0d38 100644 --- a/docs/2_patches_intro.md +++ b/docs/2_patches_intro.md @@ -96,21 +96,21 @@ Example of patches: @Surpress("unused") val bytecodePatch = bytecodePatch { execute { - // TODO + // More about this on the next page of the documentation. } } @Surpress("unused") val rawResourcePatch = rawResourcePatch { - execute { - // TODO + execute { + // More about this on the next page of the documentation. } } @Surpress("unused") val resourcePatch = resourcePatch { - execute { - // TODO + execute { + // More about this on the next page of the documentation. } } ``` diff --git a/docs/4_apis.md b/docs/4_apis.md index a2368cd7..feb5bb50 100644 --- a/docs/4_apis.md +++ b/docs/4_apis.md @@ -4,13 +4,11 @@ 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)` 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) -5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)` -6. 📃 Read and write DOM files using `ResourcePatchContext.document` +1. 👹 Create mutable replacements of classes with `proxy(ClassDef)` +2. 🔍 Find and create mutable replaces with `classBy(Predicate)` +3. 🏃‍ Navigate method calls recursively by index with `navigate(Method).at(index)` +4. 💾 Read and write resource files with `get(Path, Boolean)` +5. 📃 Read and write DOM files using `document` ### 🧰 APIs diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt index 2c297e58..f1947d15 100644 --- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -4,7 +4,6 @@ package app.revanced.patcher import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.patch.* -import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps.Companion.appendParameters import app.revanced.patcher.patch.MethodClassPairs import app.revanced.patcher.util.proxy.ClassProxy 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 /** - * 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 returnType The return type. Compared using [String.startsWith]. @@ -38,13 +47,14 @@ class Fingerprint internal constructor( /** * The match for this [Fingerprint]. Null if unmatched. */ - var match: Match? = null - private set + // Backing property for "match" extension in BytecodePatchContext. + @Suppress("ktlint:standard:backing-property-naming", "PropertyName") + internal var _match: Match? = null /** * 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: * - Slowest: Specify [custom] or [opcodes] and nothing else. @@ -52,48 +62,54 @@ class Fingerprint internal constructor( * - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. * - * @param context The 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. + * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - internal fun match(context: BytecodePatchContext): Boolean { + internal fun match(context: BytecodePatchContext): Match? { + if (_match != null) return _match + val lookupMaps = context.lookupMaps - fun Fingerprint.match(methodClasses: MethodClassPairs): Boolean { + fun Fingerprint.match(methodClasses: MethodClassPairs): Match? { 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? - if (strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match) == true) { - return true - } + val match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match) + if (match != null) return match 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]. * * @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. + * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - fun match( + internal fun match( context: BytecodePatchContext, classDef: ClassDef, - ): Boolean { + ): Match? { + if (_match != null) return _match + for (method in classDef.methods) { - if (match(context, method, classDef)) { - return true - } + val match = match(context, method, classDef) + if (match != null)return match } - return false + + return null } /** @@ -101,10 +117,10 @@ class Fingerprint internal constructor( * 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. + * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - fun match( + internal fun match( context: BytecodePatchContext, method: Method, ) = 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 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. + * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ internal fun match( context: BytecodePatchContext, method: Method, classDef: ClassDef, - ): Boolean { - if (match != null) return true + ): Match? { + if (_match != null) return _match if (returnType != null && !method.returnType.startsWith(returnType)) { - return false + return null } if (accessFlags != null && accessFlags != method.accessFlags) { - return false + return null } fun parametersEqual( @@ -146,17 +162,17 @@ class Fingerprint internal constructor( // TODO: parseParameters() if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) { - return false + return null } if (custom != null && !custom.invoke(method, classDef)) { - return false + return null } val stringMatches: List? = if (strings != null) { buildList { - val instructions = method.instructionsOrNull ?: return false + val instructions = method.instructionsOrNull ?: return null val stringsList = strings.toMutableList() @@ -176,14 +192,14 @@ class Fingerprint internal constructor( stringsList.removeAt(index) } - if (stringsList.isNotEmpty()) return false + if (stringsList.isNotEmpty()) return null } } else { null } val patternMatch = if (opcodes != null) { - val instructions = method.instructionsOrNull ?: return false + val instructions = method.instructionsOrNull ?: return null fun patternScan(): Match.PatternMatch? { val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold @@ -222,54 +238,54 @@ class Fingerprint internal constructor( return null } - patternScan() ?: return false + patternScan() ?: return null } else { null } - match = Match( - method, + _match = Match( classDef, + method, patternMatch, stringMatches, context, ) - return true + return _match } } /** * A match for a [Fingerprint]. * - * @param method The matching method. - * @param classDef The class the matching method is a member of. + * @param originalClassDef The class the matching method is a member of. + * @param originalMethod The matching method. * @param 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, +class Match internal constructor( + val originalClassDef: ClassDef, + val originalMethod: Method, val patternMatch: PatternMatch?, val stringMatches: List?, internal val context: BytecodePatchContext, ) { /** - * The mutable version of [classDef]. + * The mutable version of [originalClassDef]. * * 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]. - * 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. @@ -336,7 +352,7 @@ class FingerprintBuilder internal constructor( * * @param returnType The return type compared using [String.startsWith]. */ - infix fun returns(returnType: String) { + fun returns(returnType: String) { this.returnType = returnType } @@ -427,19 +443,3 @@ 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/PatcherConfig.kt b/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt index eed317cd..7c4b6133 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt @@ -12,16 +12,12 @@ import java.util.logging.Logger * @param temporaryFilesPath A path to a folder to store temporary files in. * @param aaptBinaryPath A path to a custom aapt binary. * @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( internal val apkFile: File, private val temporaryFilesPath: File = File("revanced-temporary-files"), aaptBinaryPath: 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) diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index a5235117..a758cf77 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -1,8 +1,6 @@ package app.revanced.patcher.patch -import app.revanced.patcher.InternalApi -import app.revanced.patcher.PatcherConfig -import app.revanced.patcher.PatcherResult +import app.revanced.patcher.* import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.MethodNavigator @@ -23,6 +21,7 @@ import java.io.Closeable import java.io.FileFilter import java.util.* import java.util.logging.Logger +import kotlin.reflect.KProperty /** * A context for patches containing the current state of the bytecode. @@ -53,19 +52,52 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi ).also { opcodes = it.opcodes }.classes.toMutableList(), ) + /** + * The match for this [Fingerprint]. Null if unmatched. + */ + val Fingerprint.match get() = match(this@BytecodePatchContext) + + /** + * Match using a [ClassDef]. + * + * @param classDef The class to match against. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. + */ + fun Fingerprint.match(classDef: ClassDef) = match(this@BytecodePatchContext, classDef) + + /** + * Match using a [Method]. + * The class is retrieved from the method. + * + * @param method The method to match against. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. + */ + fun Fingerprint.match(method: Method) = match(this@BytecodePatchContext, method) + + /** + * Get the match for this [Fingerprint]. + * + * @throws IllegalStateException If the [Fingerprint] has not been matched. + */ + operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = _match + ?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".") + /** * The lookup maps for methods and the class they are a member of from the [classes]. */ 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() { - extension?.use { extensionStream -> + internal fun mergeExtension(bytecodePatch: BytecodePatch) { + bytecodePatch.extensionInputStream?.get()?.use { extensionStream -> RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef -> val existingClass = lookupMaps.classesByType[classDef.type] ?: run { - logger.fine("Adding class \"$classDef\"") + logger.fine { "Adding class \"$classDef\"" } classes += classDef lookupMaps.classesByType[classDef.type] = classDef @@ -73,7 +105,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi 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 -> // 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 } } - } ?: 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. * @@ -145,7 +168,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi }.apply { MultiDexIO.writeDexFile( true, - if (config.multithreadingDexFileWriter) -1 else 1, + -1, this, BasicDexFileNamer(), object : DexFile { @@ -155,7 +178,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi override fun getOpcodes() = this@BytecodePatchContext.opcodes }, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, - ) { _, entryName, _ -> logger.info("Compiled $entryName") } + ) { _, entryName, _ -> logger.info { "Compiled $entryName" } } }.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }.toSet() diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index d56acd3c..8f0dc838 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -2,7 +2,6 @@ package app.revanced.patcher.patch -import app.revanced.patcher.Fingerprint import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherContext import dalvik.system.DexClassLoader @@ -14,8 +13,8 @@ import java.lang.reflect.Member import java.lang.reflect.Method import java.lang.reflect.Modifier import java.net.URLClassLoader +import java.util.function.Supplier import java.util.jar.JarFile -import kotlin.reflect.KProperty typealias PackageName = String typealias VersionName = String @@ -46,10 +45,10 @@ sealed class Patch>( val dependencies: Set>, val compatiblePackages: Set?, options: Set>, - private val executeBlock: Patch.(C) -> Unit, + private val executeBlock: (C) -> Unit, // 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. - internal var finalizeBlock: (Patch.(C) -> Unit)?, + internal var finalizeBlock: ((C) -> Unit)?, ) { /** * The options of the patch. @@ -57,35 +56,35 @@ sealed class Patch>( val options = Options(options) /** - * Runs the execution block of the patch. - * Called by [Patcher]. + * Calls the execution block of the patch. + * This function is called by [Patcher.invoke]. * * @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with. */ 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. */ fun execute(context: C) = executeBlock(context) /** - * Runs the finalizing block of the patch. - * Called by [Patcher]. + * Calls the finalizing block of the patch. + * This function is called by [Patcher.invoke]. * * @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with. */ 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. */ fun finalize(context: C) { - finalizeBlock?.invoke(this, context) + finalizeBlock?.invoke(context) } override fun toString() = name ?: "Patch" @@ -127,8 +126,7 @@ internal fun Iterable>.forEachRecursively( * If null, the patch is compatible with all packages. * @param dependencies Other patches this patch depends on. * @param options The options of the patch. - * @param fingerprints The fingerprints that are resolved before the patch is executed. - * @property extension An input stream of the extension resource this patch uses. + * @property extensionInputStream Getter for the extension input stream of the patch. * 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 finalizeBlock The finalizing block of the patch. Called after all patches have been executed, @@ -143,10 +141,9 @@ class BytecodePatch internal constructor( compatiblePackages: Set?, dependencies: Set>, options: Set>, - val fingerprints: Set, - val extension: InputStream?, - executeBlock: Patch.(BytecodePatchContext) -> Unit, - finalizeBlock: (Patch.(BytecodePatchContext) -> Unit)?, + val extensionInputStream: Supplier?, + executeBlock: (BytecodePatchContext) -> Unit, + finalizeBlock: ((BytecodePatchContext) -> Unit)?, ) : Patch( name, description, @@ -158,14 +155,7 @@ class BytecodePatch internal constructor( finalizeBlock, ) { override fun execute(context: PatcherContext) = with(context.bytecodeContext) { - with(context.bytecodeContext) { - mergeExtension() - } - - fingerprints.forEach { - it.match(this) - } - + mergeExtension(this@BytecodePatch) execute(this) } @@ -198,8 +188,8 @@ class RawResourcePatch internal constructor( compatiblePackages: Set?, dependencies: Set>, options: Set>, - executeBlock: Patch.(ResourcePatchContext) -> Unit, - finalizeBlock: (Patch.(ResourcePatchContext) -> Unit)?, + executeBlock: (ResourcePatchContext) -> Unit, + finalizeBlock: ((ResourcePatchContext) -> Unit)?, ) : Patch( name, description, @@ -241,8 +231,8 @@ class ResourcePatch internal constructor( compatiblePackages: Set?, dependencies: Set>, options: Set>, - executeBlock: Patch.(ResourcePatchContext) -> Unit, - finalizeBlock: (Patch.(ResourcePatchContext) -> Unit)?, + executeBlock: (ResourcePatchContext) -> Unit, + finalizeBlock: ((ResourcePatchContext) -> Unit)?, ) : Patch( name, description, @@ -287,8 +277,8 @@ sealed class PatchBuilder>( protected var dependencies = mutableSetOf>() protected val options = mutableSetOf>() - protected var executionBlock: (Patch.(C) -> Unit) = { } - protected var finalizeBlock: (Patch.(C) -> Unit)? = null + protected var executionBlock: ((C) -> Unit) = { } + protected var finalizeBlock: ((C) -> Unit)? = null /** * Add an option to the patch. @@ -347,7 +337,7 @@ sealed class PatchBuilder>( * * @param block The execution block of the patch. */ - fun execute(block: Patch.(C) -> Unit) { + fun execute(block: C.() -> Unit) { executionBlock = block } @@ -356,7 +346,7 @@ sealed class PatchBuilder>( * * @param block The finalizing block of the patch. */ - fun finalize(block: Patch.(C) -> Unit) { + fun finalize(block: C.() -> Unit) { finalizeBlock = block } @@ -385,8 +375,7 @@ private fun > B.buildPatch(block: B.() -> Unit = {}) = apply * If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * @param description The description of the patch. * @param use Weather or not the patch should be used. - * @property fingerprints The fingerprints that are resolved before the patch is executed. - * @property extension An input stream of the extension resource this patch uses. + * @property extensionInputStream Getter for the extension input stream of the patch. * 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. @@ -396,27 +385,9 @@ class BytecodePatchBuilder internal constructor( description: String?, use: Boolean, ) : PatchBuilder(name, description, use) { - private val fingerprints = mutableSetOf() - - /** - * 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". @PublishedApi - internal var extension: InputStream? = null + internal var extensionInputStream: Supplier? = null // Inlining is necessary to get the class loader that loaded the patch // to load the extension from the resources. @@ -427,8 +398,11 @@ class BytecodePatchBuilder internal constructor( */ @Suppress("NOTHING_TO_INLINE") inline fun extendWith(extension: String) = apply { - this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension) - ?: throw PatchException("Extension \"$extension\" not found") + val classLoader = object {}.javaClass.classLoader + + extensionInputStream = Supplier { + classLoader.getResourceAsStream(extension) ?: throw PatchException("Extension \"$extension\" not found") + } } override fun build() = BytecodePatch( @@ -438,8 +412,7 @@ class BytecodePatchBuilder internal constructor( compatiblePackages, dependencies, options, - fingerprints, - extension, + extensionInputStream, executionBlock, finalizeBlock, ) diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt index 9850a4e1..d9a3a218 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt +++ b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt @@ -60,7 +60,7 @@ internal object ClassMerger { if (missingMethods.isEmpty()) return this - logger.fine("Found ${missingMethods.size} missing methods") + logger.fine { "Found ${missingMethods.size} missing methods" } return asMutableClass().apply { methods.addAll(missingMethods.map { it.toMutable() }) @@ -80,7 +80,7 @@ internal object ClassMerger { if (missingFields.isEmpty()) return this - logger.fine("Found ${missingFields.size} missing fields") + logger.fine { "Found ${missingFields.size} missing fields" } return asMutableClass().apply { fields.addAll(missingFields.map { it.toMutable() }) @@ -100,7 +100,7 @@ internal object ClassMerger { context.traverseClassHierarchy(this) { if (accessFlags.isPublic()) return@traverseClassHierarchy - logger.fine("Publicizing ${this.type}") + logger.fine { "Publicizing ${this.type}" } accessFlags = accessFlags.toPublic() } @@ -124,7 +124,7 @@ internal object ClassMerger { if (brokenFields.isEmpty()) return this - logger.fine("Found ${brokenFields.size} broken fields") + logger.fine { "Found ${brokenFields.size} broken fields" } /** * Make a field public. @@ -153,7 +153,7 @@ internal object ClassMerger { if (brokenMethods.isEmpty()) return this - logger.fine("Found ${brokenMethods.size} methods") + logger.fine { "Found ${brokenMethods.size} methods" } /** * Make a method public. diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 2fd8c88b..33c98d66 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -3,21 +3,21 @@ package app.revanced.patcher import app.revanced.patcher.patch.* import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps import app.revanced.patcher.util.ProxyClassList +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import jdk.internal.module.ModuleBootstrap.patcher import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertAll import java.util.logging.Logger -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue +import kotlin.test.* internal object PatcherTest { private lateinit var patcher: Patcher @@ -151,19 +151,15 @@ internal object PatcherTest { @Test fun `throws if unmatched fingerprint match is delegated`() { val patch = bytecodePatch { - // Fingerprint can never match. - val match by fingerprint { } - // Manually add the fingerprint. - app.revanced.patcher.fingerprint { }() - execute { + // Fingerprint can never match. + val match by fingerprint { } + // Throws, because the fingerprint can't be matched. match.patternMatch } } - assertEquals(2, patch.fingerprints.size) - assertTrue( patch().exception != null, "Expected an exception because the fingerprint can't match.", @@ -172,44 +168,6 @@ internal object PatcherTest { @Test 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>.invoke(): List { - every { patcher.context.executablePatches } returns toMutableSet() - every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes) - every { with(patcher.context.bytecodeContext) { any().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( mutableListOf( ImmutableClassDef( @@ -235,6 +193,50 @@ internal object PatcherTest { ), ), ) + every { with(patcher.context.bytecodeContext) { any().match } } answers { callOriginal() } + every { with(patcher.context.bytecodeContext) { any().match(any()) } } answers { callOriginal() } + every { with(patcher.context.bytecodeContext) { any().match(any()) } } 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>.invoke(): List { + every { patcher.context.executablePatches } returns toMutableSet() every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes) + every { with(patcher.context.bytecodeContext) { mergeExtension(any()) } } 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) + } } } diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt index 97a711c1..04989f3d 100644 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt +++ b/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt @@ -1,6 +1,5 @@ package app.revanced.patcher.patch -import app.revanced.patcher.fingerprint import kotlin.test.Test import kotlin.test.assertEquals @@ -24,23 +23,6 @@ internal object PatchTest { 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 fun `can create patch with dependencies`() { val patch = bytecodePatch(name = "Test") {