Skip to content

Commit

Permalink
Merge branch 'refs/heads/feat/integrations-merge' into feat/dsl-api
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Jun 18, 2024
2 parents e473e59 + 44fa0ce commit d79012d
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 274 deletions.
27 changes: 16 additions & 11 deletions api/revanced-patcher.api
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public final class app/revanced/patcher/Patcher : java/io/Closeable {
public final fun get ()Lapp/revanced/patcher/PatcherResult;
public final fun getContext ()Lapp/revanced/patcher/PatcherContext;
public final fun invoke ()Lkotlinx/coroutines/flow/Flow;
public final fun plusAssign (Lkotlin/Pair;)V
public final fun plusAssign (Ljava/util/Set;)V
}

public final class app/revanced/patcher/PatcherConfig {
Expand Down Expand Up @@ -193,14 +193,21 @@ public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$Meth
}

public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public final fun getClassLoader ()Ljava/lang/ClassLoader;
public final fun getExtension ()Ljava/lang/String;
public final fun getFingerprints ()Ljava/util/Set;
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 extendedBy (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public final fun getClassLoader ()Ljava/lang/ClassLoader;
public final fun getExtension ()Ljava/lang/String;
public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Fingerprint;
public final fun setClassLoader (Ljava/lang/ClassLoader;)V
public final fun setExtension (Ljava/lang/String;)V
}

public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext {
Expand Down Expand Up @@ -318,21 +325,20 @@ 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;ZZLjava/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/Function2;Lkotlin/jvm/functions/Function2;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;
public final fun getDependencies ()Ljava/util/Set;
public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String;
public final fun getOptions ()Lapp/revanced/patcher/patch/Options;
public final fun getRequiresIntegrations ()Z
public final fun getUse ()Z
public fun toString ()Ljava/lang/String;
}

public abstract class app/revanced/patcher/patch/PatchBuilder {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
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
Expand All @@ -345,7 +351,6 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set;
protected final fun getRequiresIntegrations ()Z
protected final fun getUse ()Z
public final fun invoke (Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
Expand All @@ -365,15 +370,15 @@ public final class app/revanced/patcher/patch/PatchException : java/lang/Excepti
}

public final class app/revanced/patcher/patch/PatchKt {
public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;)Lapp/revanced/patcher/patch/PatchLoader$Dex;
public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/patcher/patch/PatchLoader$Dex;
public static final fun loadPatchesFromJar (Ljava/util/Set;)Lapp/revanced/patcher/patch/PatchLoader$Jar;
public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun rawResourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun resourcePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/ResourcePatch;
public static synthetic fun resourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun rawResourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun resourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/ResourcePatch;
public static synthetic fun resourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
}

public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
Expand Down
9 changes: 4 additions & 5 deletions docs/1_patcher_intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,22 @@ ReVanced Patcher is a library that allows modifying Android apps by applying pat
It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool)
for resource decoding and encoding.

ReVanced Patcher accepts a list of patches and integrations, and applies them to a given APK file.
ReVanced Patcher receives a list of patches and applies them to a given APK file.
It then returns the modified components of the APK file, such as modified dex files and resources,
that can be repackaged into a new APK file.

ReVanced Patcher has a simple API that allows you to load patches and integrations from JAR files
ReVanced Patcher has a simple API that allows you to load patches from JAR files
and apply them to an APK file. Later on, you will learn how to create patches.

```kt
val patches = loadPatchesFromJar(setOf(File("revanced-patches.jar")))
val integrations = setOf(File("integrations.apk"))

val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { patcher ->
// Here you can access metadata about the APK file through patcher.context.packageMetadata
// such as package name, version code, version name, etc.

// Add patches and integrations.
patcher += patches to integrations
// Add patches.
patcher += patches

// Execute the patches.
runBlocking {
Expand Down
195 changes: 123 additions & 72 deletions docs/2_2_patch_anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,70 +64,141 @@ Learn the API to create patches using ReVanced Patcher.

## 鉀筹笍 Example patch

The following example patch disables ads in an app.
In the following sections, each part of the patch will be explained in detail.

```kt
package app.revanced.patches.ads

val disableAdsPatch = bytecodePatch(
name = "Disable ads",
description = "Disable ads in the app.",
) {
compatibleWith(
"com.some.app"("1.0.0")
)

dependsOn(disableAdsResourcePatch)
compatibleWith("com.some.app"("1.0.0"))

val showAdsMatch by methodFingerprint {
// ...
// Resource patch disables ads by patching resource files.
dependsOn(disableAdsResourcePatch)

// Precompiled DEX file to be merged into the patched app.
extendedBy("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 {
// In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads.
showAdsMatch.mutableMethod.addInstructions(
0,
"""
# Return false.
const/4 v0, 0x0
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
move-result v0
return v0
"""
)
}
}
```

> [!NOTE]
>
> - Patches do not require a name, but `PatchLoader` will only load named patches.
> - Patches can depend on others. Dependencies are executed first.
> The dependent patch will not be executed if a dependency raises an exception.
> - A patch can declare compatibility with specific packages and versions,
> but patches can still be executed on any package or version.
> It is recommended to declare explicit compatibility to list known compatible packages.
> - If `compatibleWith` is not called, the patch is compatible with any package
> - If a package is specified with no versions, the patch is compatible with any version of the package
> - If an empty array of versions is specified, the patch is not compatible with any version of the package.
> This is useful for declaring explicit incompatibility with a specific package.
> - This patch uses a fingerprint to find the method and replaces the method's instructions with new instructions.
> The fingerprint is matched in the classes present in `BytecodePatchContext`.
> Fingerprints will be explained in more detail on the next page.
> - A patch can raise a `PatchException` at any time to indicate that the patch failed to execute.
> Any other `Exception` or `Throwable` raised will be wrapped in a `PatchException`.
> [!WARNING]
>
> - Circular dependencies are not allowed. If a patch depends on another patch,
> the other patch cannot depend on the first patch.
> - Dependencies inherit compatibility from dependant patches.

> [!TIP]
> To see real-world examples of patches,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
## 馃З Patch API

### 鈿欙笍 Patch options

Patches can have options to get and set before a patch is executed.
Options are useful for making patches configurable.
After loading the patches using `PatchLoader`, options can be set for a patch.
Multiple types are already inbuilt in ReVanced Patcher and are supported by any application that uses ReVanced Patcher.

To define an option, use available `option` functions:

```kt
val patch = bytecodePatch(name = "Patch") {
// Add an inbuilt option and delegate it to a property.
val value by stringOption(key = "option")

// Add an option with a custom type and delegate it to a property.
val string by option<String>(key = "string")

execute {
println(value)
println(string)
}
}
```

Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch:

```kt
loadPatchesJar(patches).apply {
// Type is checked at runtime.
first { it.name == "Patch" }.options["option"] = "Value"
}
```

The type of an option can be obtained from the `type` property of the option:

```kt
option.type // The KType of the option.
```

### 馃З Extensions

An extension is a precompiled DEX file that is merged into the patched app before a patch is executed.
While patches are compile-time constructs, extensions are runtime constructs
that extend the patched app with additional classes.

Assume you want to add a complex feature to an app that would need multiple classes and methods:

```java
public class ComplexPatch {
public static void doSomething() {
// ...
}
}
```

After compiling the above code as a DEX file, you can add the DEX file as a resource in the patches file
and use it in a patch:

```kt
val patch = bytecodePatch(name = "Complex patch") {
extendedBy("complex-patch.rve")

val match by methodFingerprint()

execute {
match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
}
}
```

ReVanced Patcher merges the classes from the extension into `context.classes` before executing the patch.
When the patch is executed, it can reference the classes and methods from the extension.

> [!NOTE]
>
> The [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template) repository
> is a template project to create patches and extensions.
> [!TIP]
> To see real-world examples of extensions,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
### 鈾伙笍 Finalization

Patches can have a finalization block called after all patches have been executed, in reverse order of patch execution.
The finalization block is called after all patches that depend on the patch have been executed.
This is useful for doing post-processing tasks.
A simple real-world example would be a patch that opens a resource file of the app for writing.
Other patches that depend on this patch can write to the file, and the finalization block can close the file.

```kt
val patch = bytecodePatch(name = "Patch") {
Expand All @@ -154,45 +225,25 @@ val patch = bytecodePatch(name = "Patch") {
```

Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`.
The finalization blocks are called in reverse order of patch execution, which means,
Finalization blocks are called in reverse order of patch execution, which means,
first, the finalization block of `Patch`, then the finalization block of `Dependency` is called.
The output of the above patch would be `1234`. The same order is followed for multiple patches depending on the patch.

### 鈿欙笍 Patch options

Patches can have options to get and set before a patch is executed. Multiple inbuilt types can be used as options.

To define an option, use available `option` functions:

```kt
val patch = bytecodePatch(name = "Patch") {
// Add an option with a custom type and delegate it's value to a variable.
val string by option<String>(key = "string")

// Add an inbuilt option and delegate it's value to a variable.
val value by stringOption(key = "option")

execute {
println(string)
println(value)
}
}
```

Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch:

```kt
loadPatchesJar(patchesJarFile).apply {
// Type is checked at runtime.
first { it.name == "Patch" }.options["option"] = "Value"
}
```

The type of an option can be obtained from the `type` property of the option:

```kt
option.type // The KType of the option.
```
The output after executing the patch above would be `1234`.
The same order is followed for multiple patches depending on the patch.

## 馃挕 Additional tips

- When using 麓PatchLoader` to load patches, only patches with a name are loaded.
Refer to the inline documentation of `PatchLoader` for detailed information.
- Patches can depend on others. Dependencies are executed first.
The dependent patch will not be executed if a dependency raises an exception while executing.
- A patch can declare compatibility with specific packages and versions,
but patches can still be executed on any package or version.
It is recommended to declare compatibility to present known compatible packages and versions.
- If `compatibleWith` is not used, the patch is treated as compatible with any package
- If a package is specified with no versions, the patch is compatible with any version of the package
- If an empty array of versions is specified, the patch is not compatible with any version of the package.
This is useful for declaring incompatibility with a specific package.
- A patch can raise a `PatchException` at any time of execution to indicate that the patch failed to execute.

## 鈴笍 What's next

Expand Down
18 changes: 9 additions & 9 deletions docs/3_structure_and_conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,22 +81,22 @@ Patches are organized in a specific way. The file structure looks as follows:
If a patch changes the color of a button, name it `Change button color`
- 馃敟 Write the patch description in the third person, present tense, and end it with a period.
If a patch removes ads, the description can be omitted because of redundancy,
but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
- 馃敟 Write patches with modularity and reusability in mind. Patches can depend on each other,
so it is important to write patches in a way that can be used in different contexts.
so it is important to write patches in a way that can be used in different contexts.
- 馃敟馃敟 Keep patches as minimal as possible. This reduces the risk of failing patches.
Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch,
you can write code in integrations. Integrations are compiled classes merged into the app
before patches are executed as described in [馃拤 Introduction to ReVanced Patcher](1_patcher_intro.md).
Patches can then reference methods and classes from integrations.
you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app
before this patch is executed.
Patches can then reference methods and classes from extensions.
A real-world example of integrations can be found in the [ReVanced Integrations](https://github.com/ReVanced/revanced-integrations) repository
- 馃敟馃敟馃敟 Do not overload a fingerprint with information about a method that's likely to change.
In the example of an obfuscated method, it's better to fingerprint the method by its return type
and parameters rather than its name because the name is likely to change. An intelligent selection
of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates.
and parameters rather than its name because the name is likely to change. An intelligent selection
of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates.
- 馃敟馃敟馃敟 Document your patches. Patches are abstract, so it is important to document parts of the code
that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks
of instructions that are modified or added to a method
that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks
of instructions that are modified or added to a method

## 鈴笍 What's next

Expand Down
Loading

0 comments on commit d79012d

Please sign in to comment.