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.