From aa472eb9857145b53b49f843406a9764fbb7e5ce Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 27 Oct 2024 16:00:30 +0100 Subject: [PATCH 01/10] fix: Merge extension only when patch executes (#315) --- .../kotlin/app/revanced/patcher/Patcher.kt | 12 ++--- .../patcher/patch/BytecodePatchContext.kt | 51 +++++++++---------- .../app/revanced/patcher/patch/Patch.kt | 8 ++- .../app/revanced/patcher/PatcherTest.kt | 2 +- 4 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index 50717a40..e7d49157 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -91,19 +91,15 @@ class Patcher(private val config: PatcherConfig) : Closeable { }.also { executedPatches[this] = it } } - // Prevent from decoding the app manifest twice if it is not needed. + // Prevent decoding the app manifest twice if it is not needed. if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) { context.resourceContext.decodeResources(config.resourceMode) } - logger.info("Merging extensions") + logger.info("Initializing lookup maps") - with(context.bytecodeContext) { - context.executablePatches.mergeExtensions() - - // Initialize lookup maps. - lookupMaps - } + // Accessing the lazy lookup maps to initialize them. + context.bytecodeContext.lookupMaps logger.info("Executing patches") diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index e9e881a2..a5235117 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -59,42 +59,33 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi internal val lookupMaps by lazy { LookupMaps(classes) } /** - * Merge the extensions for this set of patches. + * Merge the extension of this patch. */ - internal fun Set>.mergeExtensions() { - // Lookup map to check if a class exists by its type quickly. - val classesByType = mutableMapOf().apply { - classes.forEach { classDef -> put(classDef.type, classDef) } - } + internal fun BytecodePatch.mergeExtension() { + extension?.use { extensionStream -> + RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef -> + val existingClass = lookupMaps.classesByType[classDef.type] ?: run { + logger.fine("Adding class \"$classDef\"") - forEachRecursively { patch -> - if (patch !is BytecodePatch) return@forEachRecursively + classes += classDef + lookupMaps.classesByType[classDef.type] = classDef - patch.extension?.use { extensionStream -> - RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef -> - val existingClass = classesByType[classDef.type] ?: run { - logger.fine("Adding class \"$classDef\"") + return@forEach + } - classes += classDef - classesByType[classDef.type] = classDef + logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.") - return@forEach + existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass -> + // If the class was merged, replace the original class with the merged class. + if (mergedClass === existingClass) { + return@let } - 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. - if (mergedClass === existingClass) { - return@let - } - - classes -= existingClass - classes += mergedClass - } + classes -= existingClass + classes += mergedClass } } - } + } ?: return logger.fine("No extension to merge") } /** @@ -185,6 +176,11 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi */ internal val methodsByStrings = MethodClassPairsLookupMap() + // Lookup map for fast checking if a class exists by its type. + val classesByType = mutableMapOf().apply { + classes.forEach { classDef -> put(classDef.type, classDef) } + } + init { classes.forEach { classDef -> classDef.methods.forEach { method -> @@ -231,6 +227,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi override fun close() { methodsByStrings.clear() + classesByType.clear() } } diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 63972529..d56acd3c 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -158,7 +158,13 @@ class BytecodePatch internal constructor( finalizeBlock, ) { override fun execute(context: PatcherContext) = with(context.bytecodeContext) { - fingerprints.forEach { it.match(this) } + with(context.bytecodeContext) { + mergeExtension() + } + + fingerprints.forEach { + it.match(this) + } execute(this) } diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 7b3f2b50..2fd8c88b 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -195,7 +195,7 @@ internal object PatcherTest { 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>>().mergeExtensions() } } just runs + every { with(patcher.context.bytecodeContext) { any().mergeExtension() } } just runs return runBlocking { patcher().toList() } } From 0abf1c6c0279708fdef5cb66b141d07d17682693 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 27 Oct 2024 16:04:30 +0100 Subject: [PATCH 02/10] 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") { From b8249789df8b90129f7b7ad0e523a8d0ceaab848 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 27 Oct 2024 16:06:25 +0100 Subject: [PATCH 03/10] feat: Improve various APIs (#317) Some APIs have been slightly changed, and API docs have been added. BREAKING CHANGE: Various APIs have been changed. --- api/revanced-patcher.api | 18 ++-- docs/4_apis.md | 101 +++++++++++++++++- .../app/revanced/patcher/PatcherResult.kt | 4 +- .../patcher/patch/BytecodePatchContext.kt | 3 +- .../patcher/patch/ResourcePatchContext.kt | 23 ++-- .../revanced/patcher/util/MethodNavigator.kt | 14 ++- 6 files changed, 129 insertions(+), 34 deletions(-) diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index b33270ea..9b44089c 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -150,7 +150,7 @@ public final class app/revanced/patcher/patch/BytecodePatchContext : app/revance public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match; public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match; - public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/MethodNavigator; + public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator; public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy; } @@ -386,18 +386,13 @@ public final class app/revanced/patcher/patch/ResourcePatchBuilder : app/revance } public final class app/revanced/patcher/patch/ResourcePatchContext : app/revanced/patcher/patch/PatchContext { + public final fun delete (Ljava/lang/String;)Z + public final fun document (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document; + public final fun document (Ljava/lang/String;)Lapp/revanced/patcher/util/Document; public fun get ()Lapp/revanced/patcher/PatcherResult$PatchedResources; public synthetic fun get ()Ljava/lang/Object; public final fun get (Ljava/lang/String;Z)Ljava/io/File; public static synthetic fun get$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File; - public final fun getDocument ()Lapp/revanced/patcher/patch/ResourcePatchContext$DocumentOperatable; - public final fun stageDelete (Lkotlin/jvm/functions/Function1;)Z -} - -public final class app/revanced/patcher/patch/ResourcePatchContext$DocumentOperatable { - public fun (Lapp/revanced/patcher/patch/ResourcePatchContext;)V - public final fun get (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document; - public final fun get (Ljava/lang/String;)Lapp/revanced/patcher/util/Document; } public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document { @@ -476,8 +471,9 @@ public final class app/revanced/patcher/util/MethodNavigator { public final fun at (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator; public final fun at ([I)Lapp/revanced/patcher/util/MethodNavigator; public static synthetic fun at$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator; - public final fun immutable ()Lcom/android/tools/smali/dexlib2/iface/Method; - public final fun mutable ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method; + public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; } public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList { diff --git a/docs/4_apis.md b/docs/4_apis.md index feb5bb50..5814dc30 100644 --- a/docs/4_apis.md +++ b/docs/4_apis.md @@ -6,14 +6,105 @@ A handful of APIs are available to make patch development easier and more effici 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` +3. 🏃‍ Navigate method calls recursively by index with `navigate(Method)` +4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)` +5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)` ### 🧰 APIs -> [!WARNING] -> This section is still under construction and may be incomplete. +#### 👹 `proxy(ClassDef)` + +By default, the classes are immutable, meaning they cannot be modified. +To make a class mutable, use the `proxy(ClassDef)` function. +This function creates a lazy mutable copy of the class definition. +Accessing the property will replace the original class definition with the mutable copy, +thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy. + +```kt +execute { + val mutableClass = proxy(classDef) + mutableClass.methods.add(Method()) +} +``` + +#### 🔍 `classBy(Predicate)` + +The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate. +It automatically proxies the class definition, making it mutable. + +```kt +execute { + // Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef + val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef +} +``` + +#### 🏃‍ `navigate(Method).at(index)` + +The `navigate(Method)` function allows you to navigate method calls recursively by index. + +```kt +execute { + // Sequentially navigate to the instructions at index 1 within 'someMethod'. + val method = navigate(someMethod).at(1).original() // original() returns the original immutable method. + + // Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'. + // stop() returns the mutable copy of the method. + val method = navigate(someMethod).at(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop() + + // Alternatively, to stop(), you can delegate the method to a variable. + val method by navigate(someMethod).at(1) + + // You can chain multiple calls to at() to navigate deeper into the method. + val method by navigate(someMethod).at(1).at(2, 3, 4).at(5) +} +``` + +#### 💾 `get(String, Boolean)` and `delete(String)` + +The `get(String, Boolean)` function returns a `File` object that can be used to read and write resource files. + +```kt +execute { + val file = get("res/values/strings.xml") + val content = file.readText() + file.writeText(content) +} +``` + +The `delete` function can mark files for deletion when the APK is rebuilt. + +```kt +execute { + delete("res/values/strings.xml") +} +``` + +#### 📃 `document(String)` and `document(InputStream)` + +The `document` function is used to read and write DOM files. + +```kt +execute { + document("res/values/strings.xml").use { document -> + val element = doc.createElement("string").apply { + textContent = "Hello, World!" + } + document.documentElement.appendChild(element) + } +} +``` + +You can also read documents from an `InputStream`: + +```kt +execute { + val inputStream = classLoader.getResourceAsStream("some.xml") + document(inputStream).use { document -> + // ... + } +} +``` ## 🎉 Afterword diff --git a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt index 0334f9fb..8236cefd 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt @@ -29,12 +29,12 @@ class PatcherResult internal constructor( * @param resourcesApk The compiled resources.apk file. * @param otherResources The directory containing other resources files. * @param doNotCompress List of files that should not be compressed. - * @param deleteResources List of predicates about resources that should be deleted. + * @param deleteResources List of resources that should be deleted. */ class PatchedResources internal constructor( val resourcesApk: File?, val otherResources: File?, val doNotCompress: Set, - val deleteResources: Set<(String) -> Boolean>, + val deleteResources: Set, ) } diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index a758cf77..5508d014 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -12,6 +12,7 @@ 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.MethodReference import com.android.tools.smali.dexlib2.iface.reference.StringReference import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.DexIO @@ -147,7 +148,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi * * @return A [MethodNavigator] for the method. */ - fun navigate(method: Method) = MethodNavigator(this@BytecodePatchContext, method) + fun navigate(method: MethodReference) = MethodNavigator(this@BytecodePatchContext, method) /** * Compile bytecode from the [BytecodePatchContext]. diff --git a/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt index 5f9edfdb..f1ae3ab5 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt @@ -31,15 +31,20 @@ class ResourcePatchContext internal constructor( ) : PatchContext { private val logger = Logger.getLogger(ResourcePatchContext::class.java.name) + /** + * Read a document from an [InputStream]. + */ + fun document(inputStream: InputStream) = Document(inputStream) + /** * Read and write documents in the [PatcherConfig.apkFiles]. */ - val document = DocumentOperatable() + fun document(path: String) = Document(get(path)) /** - * Predicate to delete resources from [PatcherConfig.apkFiles]. + * Set of resources from [PatcherConfig.apkFiles] to delete. */ - private val deleteResources = mutableSetOf<(String) -> Boolean>() + private val deleteResources = mutableSetOf() /** * Decode resources of [PatcherConfig.apkFile]. @@ -201,11 +206,11 @@ class ResourcePatchContext internal constructor( } /** - * Stage a file to be deleted from [PatcherConfig.apkFile]. + * Mark a file for deletion when the APK is rebuilt. * - * @param shouldDelete The predicate to stage the file for deletion given its name. + * @param name The name of the file to delete. */ - fun stageDelete(shouldDelete: (String) -> Boolean) = deleteResources.add(shouldDelete) + fun delete(name: String) = deleteResources.add(name) /** * How to handle resources decoding and compiling. @@ -227,10 +232,4 @@ class ResourcePatchContext internal constructor( */ NONE, } - - inner class DocumentOperatable { - operator fun get(inputStream: InputStream) = Document(inputStream) - - operator fun get(path: String) = Document(this@ResourcePatchContext[path]) - } } diff --git a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt index ae780c49..f36ff092 100644 --- a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt +++ b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt @@ -12,6 +12,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.util.MethodUtil +import kotlin.reflect.KProperty /** * A navigator for methods. @@ -27,7 +28,7 @@ import com.android.tools.smali.dexlib2.util.MethodUtil class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) { private var lastNavigatedMethodReference = startMethod - private val lastNavigatedMethodInstructions get() = with(immutable()) { + private val lastNavigatedMethodInstructions get() = with(original()) { instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.") } @@ -76,15 +77,22 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon * * @return The last navigated method mutably. */ - fun mutable() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature + fun stop() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature as MutableMethod + /** + * Get the last navigated method mutably. + * + * @return The last navigated method mutably. + */ + operator fun getValue(nothing: Nothing?, property: KProperty<*>) = stop() + /** * Get the last navigated method immutably. * * @return The last navigated method immutably. */ - fun immutable() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature + fun original() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature /** * Predicate to match the class defining the current method reference. From 49f45701646d1c9e64ce04bd9e0e285b00d4e73b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 27 Oct 2024 15:08:13 +0000 Subject: [PATCH 04/10] chore: Release v21.0.0-dev.1 [skip ci] # [21.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.2...v21.0.0-dev.1) (2024-10-27) ### Bug Fixes * Merge extension only when patch executes ([#315](https://github.com/ReVanced/revanced-patcher/issues/315)) ([aa472eb](https://github.com/ReVanced/revanced-patcher/commit/aa472eb9857145b53b49f843406a9764fbb7e5ce)) ### Features * Improve Fingerprint API ([#316](https://github.com/ReVanced/revanced-patcher/issues/316)) ([0abf1c6](https://github.com/ReVanced/revanced-patcher/commit/0abf1c6c0279708fdef5cb66b141d07d17682693)) * Improve various APIs ([#317](https://github.com/ReVanced/revanced-patcher/issues/317)) ([b824978](https://github.com/ReVanced/revanced-patcher/commit/b8249789df8b90129f7b7ad0e523a8d0ceaab848)) ### BREAKING CHANGES * Various APIs have been changed. * Many APIs have been changed. --- CHANGELOG.md | 19 +++++++++++++++++++ gradle.properties | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41632183..11f19141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# [21.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.2...v21.0.0-dev.1) (2024-10-27) + + +### Bug Fixes + +* Merge extension only when patch executes ([#315](https://github.com/ReVanced/revanced-patcher/issues/315)) ([aa472eb](https://github.com/ReVanced/revanced-patcher/commit/aa472eb9857145b53b49f843406a9764fbb7e5ce)) + + +### Features + +* Improve Fingerprint API ([#316](https://github.com/ReVanced/revanced-patcher/issues/316)) ([0abf1c6](https://github.com/ReVanced/revanced-patcher/commit/0abf1c6c0279708fdef5cb66b141d07d17682693)) +* Improve various APIs ([#317](https://github.com/ReVanced/revanced-patcher/issues/317)) ([b824978](https://github.com/ReVanced/revanced-patcher/commit/b8249789df8b90129f7b7ad0e523a8d0ceaab848)) + + +### BREAKING CHANGES + +* Various APIs have been changed. +* Many APIs have been changed. + ## [20.0.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1...v20.0.2) (2024-10-17) diff --git a/gradle.properties b/gradle.properties index c31cf0b5..e690ca6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.parallel = true org.gradle.caching = true -version = 20.0.2 +version = 21.0.0-dev.1 From 5d996def4d3de4e2bfc34562e5a6c7d89a8cddf0 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Nov 2024 02:47:57 +0100 Subject: [PATCH 05/10] fix: Match fingerprint before delegating the match property --- .../kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index 5508d014..7060a6ab 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -80,7 +80,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi * * @throws IllegalStateException If the [Fingerprint] has not been matched. */ - operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = _match + operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = match ?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".") /** From 7f55868e6feec07038b3c2a466ab19106fdefb82 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 1 Nov 2024 01:49:47 +0000 Subject: [PATCH 06/10] chore: Release v21.0.0-dev.2 [skip ci] # [21.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.1...v21.0.0-dev.2) (2024-11-01) ### Bug Fixes * Match fingerprint before delegating the match property ([5d996de](https://github.com/ReVanced/revanced-patcher/commit/5d996def4d3de4e2bfc34562e5a6c7d89a8cddf0)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f19141..26823242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [21.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.1...v21.0.0-dev.2) (2024-11-01) + + +### Bug Fixes + +* Match fingerprint before delegating the match property ([5d996de](https://github.com/ReVanced/revanced-patcher/commit/5d996def4d3de4e2bfc34562e5a6c7d89a8cddf0)) + # [21.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.2...v21.0.0-dev.1) (2024-10-27) diff --git a/gradle.properties b/gradle.properties index e690ca6c..d29e422c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.parallel = true org.gradle.caching = true -version = 21.0.0-dev.1 +version = 21.0.0-dev.2 From 0746c22743a9561bae2284d234b151f2f8511ca5 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Mon, 4 Nov 2024 02:24:16 +0100 Subject: [PATCH 07/10] feat: Move fingerprint match members to fingerprint for ease of access by using context receivers --- api/revanced-patcher.api | 30 ++- build.gradle.kts | 2 + docs/2_2_1_fingerprinting.md | 115 ++++----- docs/4_apis.md | 13 +- gradle.properties | 6 +- .../app/revanced/patcher/Fingerprint.kt | 244 ++++++++++++++---- .../patcher/patch/BytecodePatchContext.kt | 65 +---- .../revanced/patcher/util/MethodNavigator.kt | 28 +- .../app/revanced/patcher/PatcherTest.kt | 38 ++- 9 files changed, 324 insertions(+), 217 deletions(-) diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 9b44089c..a242881a 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -1,4 +1,22 @@ public final class app/revanced/patcher/Fingerprint { + public final fun getClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; + public final fun getClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; + public final fun getMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun getMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun getOriginalClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public final fun getOriginalClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public final fun getOriginalMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method; + public final fun getOriginalMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method; + public final fun getPatternMatch (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch; + public final fun getPatternMatchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch; + public final fun getStringMatches (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List; + public final fun getStringMatchesOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List; + public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; + public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match; + public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; + public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; + public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match; + public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; } public final class app/revanced/patcher/FingerprintBuilder { @@ -31,13 +49,11 @@ public final class app/revanced/patcher/Match { } public final class app/revanced/patcher/Match$PatternMatch { - public fun (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; } @@ -146,10 +162,6 @@ public final class app/revanced/patcher/patch/BytecodePatchContext : app/revance public synthetic fun get ()Ljava/lang/Object; public fun get ()Ljava/util/Set; public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList; - public final fun getMatch (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Match; - public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match; - public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match; - public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match; public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator; public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy; } @@ -468,12 +480,12 @@ public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w } public final class app/revanced/patcher/util/MethodNavigator { - public final fun at (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator; - public final fun at ([I)Lapp/revanced/patcher/util/MethodNavigator; - public static synthetic fun at$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator; public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method; public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public final fun to (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator; + public final fun to ([I)Lapp/revanced/patcher/util/MethodNavigator; + public static synthetic fun to$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator; } public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList { diff --git a/build.gradle.kts b/build.gradle.kts index 61ea7e0b..08cabdca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,6 +56,8 @@ dependencies { kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) + + freeCompilerArgs = listOf("-Xcontext-receivers") } } diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md index a9619443..9260948c 100644 --- a/docs/2_2_1_fingerprinting.md +++ b/docs/2_2_1_fingerprinting.md @@ -117,15 +117,19 @@ With this information, the original code can be reconstructed: ```java package com.some.app.ads; - class AdsLoader { - public final boolean (boolean ) { + + +class AdsLoader { + public final boolean (boolean ) + + { // ... var userStatus = "pro"; // ... - return ; + return ; } } ``` @@ -134,13 +138,14 @@ Using that fingerprint, this method can be matched uniquely from all other metho > [!TIP] > A fingerprint should contain information about a method likely to remain the same across updates. -> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app. -> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same. +> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated +> app. +> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the +> same. ## 🔨 How to use fingerprints -A fingerprint is matched to a method, -once the `match` property of the fingerprint is accessed in a patch's `execute` scope: +After declaring a fingerprint, it can be used in a patch to find the method it matches to: ```kt val fingerprint = fingerprint { @@ -149,52 +154,34 @@ val fingerprint = fingerprint { val patch = bytecodePatch { execute { - val match = fingerprint.match!! + fingerprint.method } } ``` -The fingerprint won't be matched again, if it has already been matched once. -This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint: +The fingerprint won't be matched again, if it has already been matched once, for performance reasons. +This makes it useful, to share fingerprints between multiple patches, +and let the first executing patch match the fingerprint: ```kt // Either of these two patches will match the fingerprint first and the other patch can reuse the match: val mainActivityPatch1 = bytecodePatch { execute { - val match = mainActivityOnCreateFingerprint.match!! + mainActivityOnCreateFingerprint.method } } val mainActivityPatch2 = bytecodePatch { execute { - val match = mainActivityOnCreateFingerprint.match!! - } -} -``` - -A fingerprint match can also be delegated to a variable for convenience without the need to check for `null`: -```kt -val fingerprint = fingerprint { - // ... -} - -val patch = bytecodePatch { - execute { - // Alternative to fingerprint.match ?: throw PatchException("No match found") - val match by fingerprint.match - - try { - match.method - } catch (e: PatchException) { - // Handle the exception for example. - } + mainActivityOnCreateFingerprint.method } } ``` > [!WARNING] -> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If such a match is delegated -> to a variable, accessing it will raise an exception. +> If the fingerprint can not be matched to any method, +> accessing certain properties of the fingerprint will raise an exception. +> Instead, the `orNull` properties can be used to return `null` if no match is found. > [!TIP] > If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode` @@ -211,47 +198,43 @@ val patch = bytecodePatch { > ) >} > ``` -> -The match of a fingerprint contains references to the original method and class definition of the method: -```kt -class Match( - val originalMethod: Method, - val originalClassDef: ClassDef, - val patternMatch: Match.PatternMatch?, - val stringMatches: List?, - // ... -) { - val classDef by lazy { /* ... */ } - val method by lazy { /* ... */ } +The following properties can be accessed in a fingerprint: - // ... -} -``` +- `originalClassDef`: The original class definition the fingerprint matches to. +- `originalClassDefOrNull`: The original class definition the fingerprint matches to. +- `originalMethod`: The original method the fingerprint matches to. +- `originalMethodOrNull`: The original method the fingerprint matches to. +- `classDef`: The class the fingerprint matches to. +- `classDefOrNull`: The class the fingerprint matches to. +- `method`: The method the fingerprint matches to. If no match is found, an exception is raised. +- `methodOrNull`: The method the fingerprint matches to. -The `classDef` and `method` properties can be used to make changes to the class or method. -They are lazy properties, so they are only computed -and will effectively replace the original method or class definition when accessed. +The difference between the `original` and non-`original` properties is that the `original` properties return the +original class or method definition, while the non-`original` properties return a mutable copy of the class or method. +The mutable copies can be modified. They are lazy properties, so they are only computed +and only then will effectively replace the `original` method or class definition when accessed. > [!TIP] -> If only read-only access to the class or method is needed, -> the `originalClassDef` and `originalMethod` properties can be used, +> If only read-only access to the class or method is needed, +> the `originalClassDef` and `originalMethod` properties should be used, > to avoid making a mutable copy of the class or method. ## 🏹 Manually matching fingerprints -By default, a fingerprint is matched automatically against all classes when the `match` property is accessed. +By default, a fingerprint is matched automatically against all classes +when one of the fingerprint's properties is accessed. Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function: - In a **list of classes**, if the fingerprint can match in a known subset of classes If you have a known list of classes you know the fingerprint can match in, -you can match the fingerprint on the list of classes: + you can match the fingerprint on the list of classes: ```kt execute { - val match = showAdsFingerprint.match(classes) ?: throw PatchException("No match found") + val match = showAdsFingerprint(classes) } ``` @@ -263,23 +246,24 @@ you can match the fingerprint on the list of classes: execute { val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } - val match = showAdsFingerprint.match(context, adsLoaderClass) ?: throw PatchException("No match found") + val match = showAdsFingerprint.match(adsLoaderClass) } ``` - + Another common usecase is to use a fingerprint to reduce the search space of a method to a single class. ```kt execute { // Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint. - val match by showAdsFingerprint.match(adsLoaderClassFingerprint.match!!.classDef) + val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef) } ``` - Match a **single method**, to extract certain information about it The match of a fingerprint contains useful information about the method, - such as the start and end index of an opcode pattern or the indices of the instructions with certain string references. + such as the start and end index of an opcode pattern or the indices of the instructions with certain string + references. A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out: ```kt @@ -288,14 +272,19 @@ you can match the fingerprint on the list of classes: strings("free", "trial") } - currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match -> + currentPlanFingerprint.match(adsFingerprint.method).let { match -> match.stringMatches.forEach { match -> println("The index of the string '${match.string}' is ${match.index}") } - } ?: throw PatchException("No match found") + } } ``` +> [!WARNING] +> If the fingerprint can not be matched to any method, calling `match` will raise an +> exception. +> Instead, the `orNull` overloads can be used to return `null` if no match is found. + > [!TIP] > To see real-world examples of fingerprints, > check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). diff --git a/docs/4_apis.md b/docs/4_apis.md index 5814dc30..d98a5cd3 100644 --- a/docs/4_apis.md +++ b/docs/4_apis.md @@ -46,17 +46,17 @@ The `navigate(Method)` function allows you to navigate method calls recursively ```kt execute { // Sequentially navigate to the instructions at index 1 within 'someMethod'. - val method = navigate(someMethod).at(1).original() // original() returns the original immutable method. + val method = navigate(someMethod).to(1).original() // original() returns the original immutable method. // Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'. // stop() returns the mutable copy of the method. - val method = navigate(someMethod).at(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop() + val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop() // Alternatively, to stop(), you can delegate the method to a variable. - val method by navigate(someMethod).at(1) + val method by navigate(someMethod).to(1) // You can chain multiple calls to at() to navigate deeper into the method. - val method by navigate(someMethod).at(1).at(2, 3, 4).at(5) + val method by navigate(someMethod).to(1).to(2, 3, 4).to(5) } ``` @@ -85,7 +85,7 @@ execute { The `document` function is used to read and write DOM files. ```kt -execute { +execute { document("res/values/strings.xml").use { document -> val element = doc.createElement("string").apply { textContent = "Hello, World!" @@ -112,5 +112,6 @@ ReVanced Patcher is a powerful library to patch Android applications, offering a that outlive app updates. Patches make up ReVanced; without you, the community of patch developers, ReVanced would not be what it is today. We hope that this documentation has been helpful to you and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help, -talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or feature request, +talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or +feature request, ReVanced diff --git a/gradle.properties b/gradle.properties index d29e422c..76481669 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.parallel = true -org.gradle.caching = true -version = 21.0.0-dev.2 +org.gradle.parallel=true +org.gradle.caching=true +version=21.0.0-dev.3 diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt index f1947d15..94feca64 100644 --- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -3,8 +3,8 @@ package app.revanced.patcher import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull -import app.revanced.patcher.patch.* -import app.revanced.patcher.patch.MethodClassPairs +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException import app.revanced.patcher.util.proxy.ClassProxy import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -44,17 +44,21 @@ class Fingerprint internal constructor( internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?, private val fuzzyPatternScanThreshold: Int, ) { + @Suppress("ktlint:standard:backing-property-naming") + // Backing field needed for lazy initialization. + private var _matchOrNull: Match? = null + /** * The match for this [Fingerprint]. Null if unmatched. */ - // Backing property for "match" extension in BytecodePatchContext. - @Suppress("ktlint:standard:backing-property-naming", "PropertyName") - internal var _match: Match? = null + context(BytecodePatchContext) + private val matchOrNull: Match? + get() = matchOrNull() /** - * Match using [BytecodePatchContext.LookupMaps]. + * Match using [BytecodePatchContext.lookupMaps]. * - * Generally faster than the other [_match] overloads when there are many methods to check for a match. + * Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match. * * Fingerprints can be optimized for performance: * - Slowest: Specify [custom] or [opcodes] and nothing else. @@ -62,29 +66,28 @@ class Fingerprint internal constructor( * - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. * - * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - internal fun match(context: BytecodePatchContext): Match? { - if (_match != null) return _match + context(BytecodePatchContext) + internal fun matchOrNull(): Match? { + if (_matchOrNull != null) return _matchOrNull - val lookupMaps = context.lookupMaps + val lookupMaps = lookupMaps - fun Fingerprint.match(methodClasses: MethodClassPairs): Match? { + // Find the first + var match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let { methodClasses -> methodClasses.forEach { (classDef, method) -> - val match = match(context, classDef, method) - if (match != null) return match + val match = matchOrNull(classDef, method) + if (match != null) return@let match } - return null + null } - // TODO: If only one string is necessary, why not use a single string for every fingerprint? - val match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match) if (match != null) return match - context.classes.forEach { classDef -> - val match = match(context, classDef) + classes.forEach { classDef -> + match = matchOrNull(classDef) if (match != null) return match } @@ -95,18 +98,17 @@ class Fingerprint internal constructor( * Match using a [ClassDef]. * * @param classDef The class to match against. - * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - internal fun match( - context: BytecodePatchContext, + context(BytecodePatchContext) + fun matchOrNull( classDef: ClassDef, ): Match? { - if (_match != null) return _match + if (_matchOrNull != null) return _matchOrNull for (method in classDef.methods) { - val match = match(context, method, classDef) - if (match != null)return match + val match = matchOrNull(method, classDef) + if (match != null) return match } return null @@ -117,28 +119,26 @@ class Fingerprint internal constructor( * The class is retrieved from the method. * * @param method The method to match against. - * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - internal fun match( - context: BytecodePatchContext, + context(BytecodePatchContext) + fun matchOrNull( method: Method, - ) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass) + ) = matchOrNull(method, classBy { method.definingClass == it.type }!!.immutableClass) /** * Match using a [Method]. * * @param method The method to match against. * @param classDef The class the method is a member of. - * @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes]. * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. */ - internal fun match( - context: BytecodePatchContext, + context(BytecodePatchContext) + fun matchOrNull( method: Method, classDef: ClassDef, ): Match? { - if (_match != null) return _match + if (_matchOrNull != null) return _matchOrNull if (returnType != null && !method.returnType.startsWith(returnType)) { return null @@ -243,33 +243,189 @@ class Fingerprint internal constructor( null } - _match = Match( - classDef, + _matchOrNull = Match( method, patternMatch, stringMatches, - context, + classDef, ) - return _match + return _matchOrNull } + + private val exception get() = PatchException("Failed to match the fingerprint: $this") + + /** + * The match for this [Fingerprint]. + * + * @throws PatchException If the [Fingerprint] has not been matched. + */ + context(BytecodePatchContext) + private val match + get() = matchOrNull ?: throw exception + + /** + * Match using a [ClassDef]. + * + * @param classDef The class to match against. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + fun match( + classDef: ClassDef, + ) = matchOrNull(classDef) ?: throw exception + + /** + * Match using a [Method]. + * The class is retrieved from the method. + * + * @param method The method to match against. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + fun match( + method: Method, + ) = matchOrNull(method) ?: throw exception + + /** + * Match using a [Method]. + * + * @param method The method to match against. + * @param classDef The class the method is a member of. + * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + fun match( + method: Method, + classDef: ClassDef, + ) = matchOrNull(method, classDef) ?: throw exception + + /** + * The class the matching method is a member of. + */ + context(BytecodePatchContext) + val originalClassDefOrNull + get() = matchOrNull?.originalClassDef + + /** + * The matching method. + */ + context(BytecodePatchContext) + val originalMethodOrNull + get() = matchOrNull?.originalMethod + + /** + * The mutable version of [originalClassDefOrNull]. + * + * Accessing this property allocates a [ClassProxy]. + * Use [originalClassDefOrNull] if mutable access is not required. + */ + context(BytecodePatchContext) + val classDefOrNull + get() = matchOrNull?.classDef + + /** + * The mutable version of [originalMethodOrNull]. + * + * Accessing this property allocates a [ClassProxy]. + * Use [originalMethodOrNull] if mutable access is not required. + */ + context(BytecodePatchContext) + val methodOrNull + get() = matchOrNull?.method + + /** + * The match for the opcode pattern. + */ + context(BytecodePatchContext) + val patternMatchOrNull + get() = matchOrNull?.patternMatch + + /** + * The matches for the strings. + */ + context(BytecodePatchContext) + val stringMatchesOrNull + get() = matchOrNull?.stringMatches + + /** + * The class the matching method is a member of. + * + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + val originalClassDef + get() = match.originalClassDef + + /** + * The matching method. + * + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + val originalMethod + get() = match.originalMethod + + /** + * The mutable version of [originalClassDef]. + * + * Accessing this property allocates a [ClassProxy]. + * Use [originalClassDef] if mutable access is not required. + * + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + val classDef + get() = match.classDef + + /** + * The mutable version of [originalMethod]. + * + * Accessing this property allocates a [ClassProxy]. + * Use [originalMethod] if mutable access is not required. + * + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + val method + get() = match.method + + /** + * The match for the opcode pattern. + * + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + val patternMatch + get() = match.patternMatch + + /** + * The matches for the strings. + * + * @throws PatchException If the fingerprint has not been matched. + */ + context(BytecodePatchContext) + val stringMatches + get() = match.stringMatches } /** - * A match for a [Fingerprint]. + * A match of a [Fingerprint]. * * @param originalClassDef The class the matching method is a member of. * @param originalMethod The matching method. * @param patternMatch The match for the opcode pattern. * @param stringMatches The matches for the strings. - * @param context The context to create mutable proxies in. */ +context(BytecodePatchContext) class Match internal constructor( - val originalClassDef: ClassDef, val originalMethod: Method, val patternMatch: PatternMatch?, val stringMatches: List?, - internal val context: BytecodePatchContext, + val originalClassDef: ClassDef, ) { /** * The mutable version of [originalClassDef]. @@ -277,7 +433,7 @@ class Match internal constructor( * Accessing this property allocates a [ClassProxy]. * Use [originalClassDef] if mutable access is not required. */ - val classDef by lazy { context.proxy(originalClassDef).mutableClass } + val classDef by lazy { proxy(originalClassDef).mutableClass } /** * The mutable version of [originalMethod]. @@ -292,7 +448,7 @@ class Match internal constructor( * @param startIndex The index of the first opcode of the pattern in the method. * @param endIndex The index of the last opcode of the pattern in the method. */ - class PatternMatch( + class PatternMatch internal constructor( val startIndex: Int, val endIndex: Int, ) @@ -303,7 +459,7 @@ class Match internal constructor( * @param string The string that matched. * @param index The index of the instruction in the method. */ - class StringMatch(val string: String, val index: Int) + class StringMatch internal constructor(val string: String, val index: Int) } /** diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index 7060a6ab..b5a14ac7 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -1,6 +1,8 @@ package app.revanced.patcher.patch -import app.revanced.patcher.* +import app.revanced.patcher.InternalApi +import app.revanced.patcher.PatcherConfig +import app.revanced.patcher.PatcherResult import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.MethodNavigator @@ -22,7 +24,6 @@ import java.io.Closeable import java.io.FileFilter import java.util.* import java.util.logging.Logger -import kotlin.reflect.KProperty /** * A context for patches containing the current state of the bytecode. @@ -33,7 +34,7 @@ import kotlin.reflect.KProperty class BytecodePatchContext internal constructor(private val config: PatcherConfig) : PatchContext>, Closeable { - private val logger = Logger.getLogger(BytecodePatchContext::class.java.name) + private val logger = Logger.getLogger(this::javaClass.name) /** * [Opcodes] of the supplied [PatcherConfig.apkFile]. @@ -53,36 +54,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi ).also { opcodes = it.opcodes }.classes.toMutableList(), ) - /** - * The match for this [Fingerprint]. Null if unmatched. - */ - val Fingerprint.match get() = match(this@BytecodePatchContext) - - /** - * Match using a [ClassDef]. - * - * @param classDef The class to match against. - * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. - */ - fun Fingerprint.match(classDef: ClassDef) = match(this@BytecodePatchContext, classDef) - - /** - * Match using a [Method]. - * The class is retrieved from the method. - * - * @param method The method to match against. - * @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. - */ - fun Fingerprint.match(method: Method) = match(this@BytecodePatchContext, method) - - /** - * Get the match for this [Fingerprint]. - * - * @throws IllegalStateException If the [Fingerprint] has not been matched. - */ - operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = match - ?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".") - /** * The lookup maps for methods and the class they are a member of from the [classes]. */ @@ -137,9 +108,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi * * @return A proxy for the class. */ - fun proxy(classDef: ClassDef) = this@BytecodePatchContext.classes.proxyPool.find { + fun proxy(classDef: ClassDef) = classes.proxyPool.find { it.immutableClass.type == classDef.type - } ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) } + } ?: ClassProxy(classDef).also { classes.proxyPool.add(it) } /** * Navigate a method. @@ -148,7 +119,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi * * @return A [MethodNavigator] for the method. */ - fun navigate(method: MethodReference) = MethodNavigator(this@BytecodePatchContext, method) + fun navigate(method: MethodReference) = MethodNavigator(method) /** * Compile bytecode from the [BytecodePatchContext]. @@ -227,28 +198,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi } } - internal companion object { - /** - * Appends a string based on the parameter reference types of this method. - */ - internal fun StringBuilder.appendParameters(parameters: Iterable) { - // Maximum parameters to use in the signature key. - // Some apps have methods with an incredible number of parameters (over 100 parameters have been seen). - // To keep the signature map from becoming needlessly bloated, - // group together in the same map entry all methods with the same access/return and 5 or more parameters. - // The value of 5 was chosen based on local performance testing and is not set in stone. - val maxSignatureParameters = 5 - // Must append a unique value before the parameters to distinguish this key includes the parameters. - // If this is not appended, then methods with no parameters - // will collide with different keys that specify access/return but omit the parameters. - append("p:") - parameters.forEachIndexed { index, parameter -> - if (index >= maxSignatureParameters) return - append(parameter.first()) - } - } - } - override fun close() { methodsByStrings.clear() classesByType.clear() diff --git a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt index f36ff092..d894e9e7 100644 --- a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt +++ b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt @@ -17,7 +17,6 @@ import kotlin.reflect.KProperty /** * A navigator for methods. * - * @param context The [BytecodePatchContext] to use. * @param startMethod The [Method] to start navigating from. * * @constructor Creates a new [MethodNavigator]. @@ -25,12 +24,16 @@ import kotlin.reflect.KProperty * @throws NavigateException If the method does not have an implementation. * @throws NavigateException If the instruction at the specified index is not a method reference. */ -class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) { +context(BytecodePatchContext) +class MethodNavigator internal constructor( + private var startMethod: MethodReference, +) { private var lastNavigatedMethodReference = startMethod - private val lastNavigatedMethodInstructions get() = with(original()) { - instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.") - } + private val lastNavigatedMethodInstructions + get() = with(original()) { + instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.") + } /** * Navigate to the method at the specified index. @@ -39,7 +42,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon * * @return This [MethodNavigator]. */ - fun at(vararg index: Int): MethodNavigator { + fun to(vararg index: Int): MethodNavigator { index.forEach { lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it) } @@ -53,7 +56,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon * @param index The index of the method to navigate to. * @param predicate The predicate to match. */ - fun at(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator { + fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator { lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence() .filter(predicate).asIterable().getMethodReferenceAt(index) @@ -77,7 +80,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon * * @return The last navigated method mutably. */ - fun stop() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature + fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature as MutableMethod /** @@ -92,7 +95,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon * * @return The last navigated method immutably. */ - fun original() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature + fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature /** * Predicate to match the class defining the current method reference. @@ -104,9 +107,10 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon /** * Find the first [lastNavigatedMethodReference] in the class. */ - private val ClassDef.firstMethodBySignature get() = methods.first { - MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference) - } + private val ClassDef.firstMethodBySignature + get() = methods.first { + MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference) + } /** * An exception thrown when navigating fails. diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 33c98d66..bf4aab27 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -3,21 +3,18 @@ package app.revanced.patcher import app.revanced.patcher.patch.* import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps import app.revanced.patcher.util.ProxyClassList -import com.android.tools.smali.dexlib2.iface.ClassDef -import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.runs -import jdk.internal.module.ModuleBootstrap.patcher +import io.mockk.* import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.assertAll import java.util.logging.Logger -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue internal object PatcherTest { private lateinit var patcher: Patcher @@ -153,10 +150,10 @@ internal object PatcherTest { val patch = bytecodePatch { execute { // Fingerprint can never match. - val match by fingerprint { } + val fingerprint = fingerprint { } // Throws, because the fingerprint can't be matched. - match.patternMatch + fingerprint.patternMatch } } @@ -193,11 +190,6 @@ internal object PatcherTest { ), ), ) - every { with(patcher.context.bytecodeContext) { any().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") } @@ -208,19 +200,21 @@ internal object PatcherTest { execute { fingerprint.match(classes.first().methods.first()) fingerprint2.match(classes.first()) - fingerprint3.match + fingerprint3.originalClassDef } }, ) patches() - assertAll( - "Expected fingerprints to match.", - { assertNotNull(fingerprint._match) }, - { assertNotNull(fingerprint2._match) }, - { assertNotNull(fingerprint3._match) }, - ) + with(patcher.context.bytecodeContext) { + assertAll( + "Expected fingerprints to match.", + { assertNotNull(fingerprint.originalClassDefOrNull) }, + { assertNotNull(fingerprint2.originalClassDefOrNull) }, + { assertNotNull(fingerprint3.originalClassDefOrNull) }, + ) + } } private operator fun Set>.invoke(): List { From 6712f0ea7292c85211c4857dbc76046d9d6fcd71 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 5 Nov 2024 13:25:22 +0000 Subject: [PATCH 08/10] chore: Release v21.0.0-dev.3 [skip ci] # [21.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.2...v21.0.0-dev.3) (2024-11-05) ### Features * Move fingerprint match members to fingerprint for ease of access by using context receivers ([0746c22](https://github.com/ReVanced/revanced-patcher/commit/0746c22743a9561bae2284d234b151f2f8511ca5)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26823242..befe6c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [21.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.2...v21.0.0-dev.3) (2024-11-05) + + +### Features + +* Move fingerprint match members to fingerprint for ease of access by using context receivers ([0746c22](https://github.com/ReVanced/revanced-patcher/commit/0746c22743a9561bae2284d234b151f2f8511ca5)) + # [21.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.1...v21.0.0-dev.2) (2024-11-01) diff --git a/gradle.properties b/gradle.properties index 76481669..80fa915a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.parallel=true -org.gradle.caching=true -version=21.0.0-dev.3 +org.gradle.parallel = true +org.gradle.caching = true +version = 21.0.0-dev.3 From 1358d3fa10cb8ba011b6b89cfe3684ecf9849d2f Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 5 Nov 2024 14:39:06 +0100 Subject: [PATCH 09/10] perf: Use smallest lookup map for strings --- src/main/kotlin/app/revanced/patcher/Fingerprint.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt index 94feca64..a44c00ba 100644 --- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -72,10 +72,9 @@ class Fingerprint internal constructor( internal fun matchOrNull(): Match? { if (_matchOrNull != null) return _matchOrNull - val lookupMaps = lookupMaps - - // Find the first - var match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let { methodClasses -> + var match = strings?.mapNotNull { + lookupMaps.methodsByStrings[it] + }?.minByOrNull { it.size }?.let { methodClasses -> methodClasses.forEach { (classDef, method) -> val match = matchOrNull(classDef, method) if (match != null) return@let match @@ -83,7 +82,6 @@ class Fingerprint internal constructor( null } - if (match != null) return match classes.forEach { classDef -> From 62191e3c4a7b8ad09e2bb433f205b04ee8bf8a02 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 5 Nov 2024 13:41:06 +0000 Subject: [PATCH 10/10] chore: Release v21.0.0-dev.4 [skip ci] # [21.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.3...v21.0.0-dev.4) (2024-11-05) ### Performance Improvements * Use smallest lookup map for strings ([1358d3f](https://github.com/ReVanced/revanced-patcher/commit/1358d3fa10cb8ba011b6b89cfe3684ecf9849d2f)) --- CHANGELOG.md | 7 +++++++ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index befe6c99..0180d7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [21.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.3...v21.0.0-dev.4) (2024-11-05) + + +### Performance Improvements + +* Use smallest lookup map for strings ([1358d3f](https://github.com/ReVanced/revanced-patcher/commit/1358d3fa10cb8ba011b6b89cfe3684ecf9849d2f)) + # [21.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.2...v21.0.0-dev.3) (2024-11-05) diff --git a/gradle.properties b/gradle.properties index 80fa915a..706a690d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ org.gradle.parallel = true org.gradle.caching = true -version = 21.0.0-dev.3 +version = 21.0.0-dev.4