You've already forked revanced-patcher
							
							
				mirror of
				https://github.com/revanced/revanced-patcher
				synced 2025-10-29 18:30:51 +01:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
			v21.1.0-de
			...
			feat/kmp
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | c2e0f57579 | ||
|   | 242e244b18 | ||
|   | f338ebff6c | ||
|   | 00e5950cf2 | ||
|   | 5a0e3841ff | ||
|   | cfb459b832 | ||
|   | 2096306107 | ||
|   | 92eaba8081 | ||
|   | 7be0cd8548 | 
| @@ -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 <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/Match$PatternMatch;Ljava/util/List;Lapp/revanced/patcher/patch/BytecodePatchContext;)V | ||||
| 	public final fun getClassDef ()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 <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V | ||||
| 	public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V | ||||
| 	public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V | ||||
| 	public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V | ||||
| } | ||||
|  | ||||
| public final class app/revanced/patcher/PatcherContext : java/io/Closeable { | ||||
| @@ -135,31 +129,28 @@ 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 navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/MethodNavigator; | ||||
| 	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; | ||||
| } | ||||
|  | ||||
| @@ -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 <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V | ||||
| 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V | ||||
| 	public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V | ||||
| 	public final fun 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 { | ||||
| @@ -395,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 <init> (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 { | ||||
| @@ -485,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 { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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") | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|   | ||||
| @@ -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. | ||||
|     } | ||||
| } | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										107
									
								
								docs/4_apis.md
									
									
									
									
									
								
							
							
						
						
									
										107
									
								
								docs/4_apis.md
									
									
									
									
									
								
							| @@ -4,18 +4,107 @@ 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)` | ||||
| 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 | ||||
|  | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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<String>, | ||||
|         val deleteResources: Set<(String) -> Boolean>, | ||||
|         val deleteResources: Set<String>, | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| @@ -14,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 | ||||
| @@ -23,6 +22,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,59 +53,74 @@ 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 extensions for this set of patches. | ||||
|      * 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 Set<Patch<*>>.mergeExtensions() { | ||||
|         // Lookup map to check if a class exists by its type quickly. | ||||
|         val classesByType = mutableMapOf<String, ClassDef>().apply { | ||||
|             classes.forEach { classDef -> put(classDef.type, classDef) } | ||||
|         } | ||||
|     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\"") | ||||
|  | ||||
|         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 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         } ?: 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. | ||||
|      * | ||||
| @@ -133,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]. | ||||
| @@ -154,7 +169,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi | ||||
|             }.apply { | ||||
|                 MultiDexIO.writeDexFile( | ||||
|                     true, | ||||
|                     if (config.multithreadingDexFileWriter) -1 else 1, | ||||
|                     -1, | ||||
|                     this, | ||||
|                     BasicDexFileNamer(), | ||||
|                     object : DexFile { | ||||
| @@ -185,6 +200,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<String, ClassDef>().apply { | ||||
|             classes.forEach { classDef -> put(classDef.type, classDef) } | ||||
|         } | ||||
|  | ||||
|         init { | ||||
|             classes.forEach { classDef -> | ||||
|                 classDef.methods.forEach { method -> | ||||
| @@ -231,6 +251,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi | ||||
|  | ||||
|         override fun close() { | ||||
|             methodsByStrings.clear() | ||||
|             classesByType.clear() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<C : PatchContext<*>>( | ||||
|     val dependencies: Set<Patch<*>>, | ||||
|     val compatiblePackages: Set<Package>?, | ||||
|     options: Set<Option<*>>, | ||||
|     private val executeBlock: Patch<C>.(C) -> Unit, | ||||
|     private val executeBlock: (C) -> Unit, | ||||
|     // Must be internal and nullable, so that Patcher.invoke can check, | ||||
|     // if a patch has a finalizing block in order to not emit it twice. | ||||
|     internal var finalizeBlock: (Patch<C>.(C) -> Unit)?, | ||||
|     internal var finalizeBlock: ((C) -> Unit)?, | ||||
| ) { | ||||
|     /** | ||||
|      * The options of the patch. | ||||
| @@ -57,35 +56,35 @@ sealed class Patch<C : PatchContext<*>>( | ||||
|     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<Patch<*>>.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<Package>?, | ||||
|     dependencies: Set<Patch<*>>, | ||||
|     options: Set<Option<*>>, | ||||
|     val fingerprints: Set<Fingerprint>, | ||||
|     val extension: InputStream?, | ||||
|     executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit, | ||||
|     finalizeBlock: (Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit)?, | ||||
|     val extensionInputStream: Supplier<InputStream>?, | ||||
|     executeBlock: (BytecodePatchContext) -> Unit, | ||||
|     finalizeBlock: ((BytecodePatchContext) -> Unit)?, | ||||
| ) : Patch<BytecodePatchContext>( | ||||
|     name, | ||||
|     description, | ||||
| @@ -158,8 +155,7 @@ class BytecodePatch internal constructor( | ||||
|     finalizeBlock, | ||||
| ) { | ||||
|     override fun execute(context: PatcherContext) = with(context.bytecodeContext) { | ||||
|         fingerprints.forEach { it.match(this) } | ||||
|  | ||||
|         mergeExtension(this@BytecodePatch) | ||||
|         execute(this) | ||||
|     } | ||||
|  | ||||
| @@ -192,8 +188,8 @@ class RawResourcePatch internal constructor( | ||||
|     compatiblePackages: Set<Package>?, | ||||
|     dependencies: Set<Patch<*>>, | ||||
|     options: Set<Option<*>>, | ||||
|     executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit, | ||||
|     finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?, | ||||
|     executeBlock: (ResourcePatchContext) -> Unit, | ||||
|     finalizeBlock: ((ResourcePatchContext) -> Unit)?, | ||||
| ) : Patch<ResourcePatchContext>( | ||||
|     name, | ||||
|     description, | ||||
| @@ -235,8 +231,8 @@ class ResourcePatch internal constructor( | ||||
|     compatiblePackages: Set<Package>?, | ||||
|     dependencies: Set<Patch<*>>, | ||||
|     options: Set<Option<*>>, | ||||
|     executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit, | ||||
|     finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?, | ||||
|     executeBlock: (ResourcePatchContext) -> Unit, | ||||
|     finalizeBlock: ((ResourcePatchContext) -> Unit)?, | ||||
| ) : Patch<ResourcePatchContext>( | ||||
|     name, | ||||
|     description, | ||||
| @@ -281,8 +277,8 @@ sealed class PatchBuilder<C : PatchContext<*>>( | ||||
|     protected var dependencies = mutableSetOf<Patch<*>>() | ||||
|     protected val options = mutableSetOf<Option<*>>() | ||||
|  | ||||
|     protected var executionBlock: (Patch<C>.(C) -> Unit) = { } | ||||
|     protected var finalizeBlock: (Patch<C>.(C) -> Unit)? = null | ||||
|     protected var executionBlock: ((C) -> Unit) = { } | ||||
|     protected var finalizeBlock: ((C) -> Unit)? = null | ||||
|  | ||||
|     /** | ||||
|      * Add an option to the patch. | ||||
| @@ -341,7 +337,7 @@ sealed class PatchBuilder<C : PatchContext<*>>( | ||||
|      * | ||||
|      * @param block The execution block of the patch. | ||||
|      */ | ||||
|     fun execute(block: Patch<C>.(C) -> Unit) { | ||||
|     fun execute(block: C.() -> Unit) { | ||||
|         executionBlock = block | ||||
|     } | ||||
|  | ||||
| @@ -350,7 +346,7 @@ sealed class PatchBuilder<C : PatchContext<*>>( | ||||
|      * | ||||
|      * @param block The finalizing block of the patch. | ||||
|      */ | ||||
|     fun finalize(block: Patch<C>.(C) -> Unit) { | ||||
|     fun finalize(block: C.() -> Unit) { | ||||
|         finalizeBlock = block | ||||
|     } | ||||
|  | ||||
| @@ -379,8 +375,7 @@ private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply | ||||
|  * If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. | ||||
|  * @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. | ||||
| @@ -390,27 +385,9 @@ class BytecodePatchBuilder internal constructor( | ||||
|     description: String?, | ||||
|     use: Boolean, | ||||
| ) : PatchBuilder<BytecodePatchContext>(name, description, use) { | ||||
|     private val fingerprints = mutableSetOf<Fingerprint>() | ||||
|  | ||||
|     /** | ||||
|      * Add the fingerprint to the patch. | ||||
|      * | ||||
|      * @return A wrapper for the fingerprint with the ability to delegate the match to the fingerprint. | ||||
|      */ | ||||
|     operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) }) | ||||
|  | ||||
|     class InvokedFingerprint internal constructor(private val fingerprint: Fingerprint) { | ||||
|         // The reason getValue isn't extending the Fingerprint class is | ||||
|         // because delegating makes only sense if the fingerprint was previously added to the patch by invoking it. | ||||
|         // It may be likely to forget invoking it. By wrapping the fingerprint into this class, | ||||
|         // the compiler will throw an error if the fingerprint was not invoked if attempting to delegate the match. | ||||
|         operator fun getValue(nothing: Nothing?, property: KProperty<*>) = fingerprint.match | ||||
|             ?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".") | ||||
|     } | ||||
|  | ||||
|     // Must be internal for the inlined function "extendWith". | ||||
|     @PublishedApi | ||||
|     internal var extension: InputStream? = null | ||||
|     internal var extensionInputStream: Supplier<InputStream>? = null | ||||
|  | ||||
|     // Inlining is necessary to get the class loader that loaded the patch | ||||
|     // to load the extension from the resources. | ||||
| @@ -421,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( | ||||
| @@ -432,8 +412,7 @@ class BytecodePatchBuilder internal constructor( | ||||
|         compatiblePackages, | ||||
|         dependencies, | ||||
|         options, | ||||
|         fingerprints, | ||||
|         extension, | ||||
|         extensionInputStream, | ||||
|         executionBlock, | ||||
|         finalizeBlock, | ||||
|     ) | ||||
|   | ||||
| @@ -32,14 +32,19 @@ class ResourcePatchContext internal constructor( | ||||
|     private val logger = Logger.getLogger(ResourcePatchContext::class.java.name) | ||||
|  | ||||
|     /** | ||||
|      * Read and write documents in the [PatcherConfig.apkFiles]. | ||||
|      * Read a document from an [InputStream]. | ||||
|      */ | ||||
|     val document = DocumentOperatable() | ||||
|     fun document(inputStream: InputStream) = Document(inputStream) | ||||
|  | ||||
|     /** | ||||
|      * Predicate to delete resources from [PatcherConfig.apkFiles]. | ||||
|      * Read and write documents in the [PatcherConfig.apkFiles]. | ||||
|      */ | ||||
|     private val deleteResources = mutableSetOf<(String) -> Boolean>() | ||||
|     fun document(path: String) = Document(get(path)) | ||||
|  | ||||
|     /** | ||||
|      * Set of resources from [PatcherConfig.apkFiles] to delete. | ||||
|      */ | ||||
|     private val deleteResources = mutableSetOf<String>() | ||||
|  | ||||
|     /** | ||||
|      * 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]) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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<Patch<*>>.invoke(): List<PatchResult> { | ||||
|         every { patcher.context.executablePatches } returns toMutableSet() | ||||
|         every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes) | ||||
|         every { with(patcher.context.bytecodeContext) { any<Set<Patch<*>>>().mergeExtensions() } } 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<Fingerprint>().match } } answers { callOriginal() } | ||||
|         every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<ClassDef>()) } } answers { callOriginal() } | ||||
|         every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<Method>()) } } answers { callOriginal() } | ||||
|         every { patcher.context.bytecodeContext.classBy(any()) } answers { callOriginal() } | ||||
|         every { patcher.context.bytecodeContext.proxy(any()) } answers { callOriginal() } | ||||
|  | ||||
|         val fingerprint = fingerprint { returns("V") } | ||||
|         val fingerprint2 = fingerprint { returns("V") } | ||||
|         val fingerprint3 = fingerprint { returns("V") } | ||||
|  | ||||
|         val patches = setOf( | ||||
|             bytecodePatch { | ||||
|                 execute { | ||||
|                     fingerprint.match(classes.first().methods.first()) | ||||
|                     fingerprint2.match(classes.first()) | ||||
|                     fingerprint3.match | ||||
|                 } | ||||
|             }, | ||||
|         ) | ||||
|  | ||||
|         patches() | ||||
|  | ||||
|         assertAll( | ||||
|             "Expected fingerprints to match.", | ||||
|             { assertNotNull(fingerprint._match) }, | ||||
|             { assertNotNull(fingerprint2._match) }, | ||||
|             { assertNotNull(fingerprint3._match) }, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     private operator fun Set<Patch<*>>.invoke(): List<PatchResult> { | ||||
|         every { patcher.context.executablePatches } returns toMutableSet() | ||||
|         every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes) | ||||
|         every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs | ||||
|  | ||||
|         return runBlocking { patcher().toList() } | ||||
|     } | ||||
|  | ||||
|     private operator fun Patch<*>.invoke() = setOf(this)().first() | ||||
|  | ||||
|     private fun Any.setPrivateField(field: String, value: Any) { | ||||
|         this::class.java.getDeclaredField(field).apply { | ||||
|             this.isAccessible = true | ||||
|             set(this@setPrivateField, value) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| package app.revanced.patcher.patch | ||||
|  | ||||
| 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") { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user