Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve various APIs #317

Merged
merged 15 commits into from
Oct 27, 2024
18 changes: 7 additions & 11 deletions api/revanced-patcher.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 <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 {
Expand Down Expand Up @@ -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 {
Expand Down
101 changes: 96 additions & 5 deletions docs/4_apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/app/revanced/patcher/PatcherResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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].
Expand Down
23 changes: 11 additions & 12 deletions src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ class ResourcePatchContext internal constructor(
) : PatchContext<PatcherResult.PatchedResources?> {
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<String>()

/**
* Decode resources of [PatcherConfig.apkFile].
Expand Down Expand Up @@ -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.
Expand All @@ -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])
}
}
14 changes: 11 additions & 3 deletions src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.")
}

Expand Down Expand Up @@ -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.
Expand Down
Loading