From 2503283941ba540cf609618cd64f377a4d00b077 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 28 Apr 2024 02:47:58 +0200 Subject: [PATCH] docs: Update docs to match new changes --- docs/1_patcher_intro.md | 29 ++-- docs/2_2_1_fingerprinting.md | 124 +++++++++-------- docs/2_2_patch_anatomy.md | 201 +++++++++++++--------------- docs/2_patches_intro.md | 42 +++--- docs/3_structure_and_conventions.md | 4 +- docs/4_apis.md | 11 +- 6 files changed, 197 insertions(+), 214 deletions(-) diff --git a/docs/1_patcher_intro.md b/docs/1_patcher_intro.md index 983d7582..58823578 100644 --- a/docs/1_patcher_intro.md +++ b/docs/1_patcher_intro.md @@ -71,29 +71,26 @@ ReVanced Patcher has a simple API that allows you to load patches and integratio Later on, you will learn how to create patches. ```kt - // Executed patches do not necessarily reset their state. - // For that reason it is important to create a new instance of the PatchBundleLoader - // once the patches are executed instead of reusing the same instance of patches loaded by PatchBundleLoader. -val patches: PatchSet /* = Set> */ = PatchBundleLoader.Jar(File("revanced-patches.jar")) +val patches = loadPatchesFromJar(File("revanced-patches.jar")) val integrations = setOf(File("integrations.apk")) // Instantiating the patcher will decode the manifest of the APK file to read the package and version name. val patcherConfig = PatcherConfig(apkFile = File("some.apk")) val patcherResult = Patcher(patcherConfig).use { patcher -> - patcher.apply { - acceptIntegrations(integrations) - acceptPatches(patches) + patcher.accept(patches, integrations) - // Execute patches. - runBlocking { - patcher.apply(returnOnError = false).collect { patchResult -> - if (patchResult.exception != null) - println("${patchResult.patchName} failed:\n${patchResult.exception}") - else - println("${patchResult.patchName} succeeded") - } + // Execute patches. + patcher.runBlocking { + patcher.apply(returnOnError = false).collect { patchResult -> + if (patchResult.exception != null) + println("${patchResult.patchName} failed:\n${patchResult.exception}") + else + println("${patchResult.patchName} succeeded") } - }.get() + } + + // Compile and save the patched APK file. + patcher.get() } // The result of the patcher contains the modified components of the APK file that can be repackaged into a new APK file. diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md index 5ce35068..80d7b0b4 100644 --- a/docs/2_2_1_fingerprinting.md +++ b/docs/2_2_1_fingerprinting.md @@ -72,14 +72,19 @@ Throughout the documentation, the following example will be used to demonstrate package app.revanced.patches.ads.fingerprints -object ShowAdsFingerprint : MethodFingerprint( - returnType = "Z", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Z"), - opcodes = listOf(Opcode.RETURN), - strings = listOf("pro"), - customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;" } -) +methodFingerprint { + returns("Z") + + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + + parameters("Z") + + opcodes(Opcode.RETURN) + + strings("pro") + + custom { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;" } +} ``` ## 🔎 Reconstructing the original code from a fingerprint @@ -91,22 +96,22 @@ The fingerprint contains the following information: - Method signature: ```kt - returnType = "Z", - access = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Z"), + returns("Z") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("Z") ``` - Method implementation: ```kt - opcodes = listOf(Opcode.RETURN) - strings = listOf("pro"), + opcodes(Opcode.RETURN) + strings("pro"), ``` - Package and class name: ```kt - customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;"} + custom = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;"} ``` With this information, the original code can be reconstructed: @@ -133,45 +138,59 @@ With this information, the original code can be reconstructed: ## 🔨 How to use fingerprints -After creating a fingerprint, add it to the constructor of a `BytecodePatch`: +Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually. Fingerprints added to a patch are resolved by ReVanced Patcher before the patch is executed. ```kt -object DisableAdsPatch : BytecodePatch( - setOf(ShowAdsFingerprint) -) { +val fingerprint = methodFingerprint { // ... - } -``` +} + +val patch = bytecodePatch { + // Directly create and add a fingerprint. + methodFingerprint { + // ... + } -> [!NOTE] -> Fingerprints passed to the constructor of `BytecodePatch` are resolved by ReVanced Patcher before the patch is executed. + // Add a fingerprint manually by invoking it. + fingerprint() +} +``` > [!TIP] > Multiple patches can share fingerprints. If a fingerprint is resolved once, it will not be resolved again. > [!TIP] -> If a fingerprint has an opcode pattern, you can use the `FuzzyPatternScanMethod` annotation to fuzzy match the pattern. +> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode` function to fuzzy match the pattern. > Opcode pattern arrays can contain `null` values to indicate that the opcode at the index is unknown. -> Any opcode will match to a `null` value. - -> [!WARNING] -> If the fingerprint can not be resolved because it does not match any method, the result of a fingerprint is `null`. +> Any opcode will match to a `null` value: +> +> ```kt +> methodFingerprint(fuzzyPatternScanThreshhold = 0.5) { +> opcodes( +> Opcode.ICONST_0, +> null, +> Opcode.ICONST_1, +> Opcode.IRETURN, +> ) +>} +> ``` Once the fingerprint is resolved, the result can be used in the patch: ```kt -object DisableAdsPatch : BytecodePatch( - setOf(ShowAdsFingerprint) -) { - override fun execute(context: BytecodeContext) { - val result = ShowAdsFingerprint.result - ?: throw PatchException("ShowAdsFingerprint not found") +val patch = bytecodePatch { + // Add a fingerprint and delegate it's result to a variable. + val result by showAdsFingerprint - // ... + execute { + val method = result.method } } ``` +> [!WARNING] +> If the fingerprint can not be resolved because it does not match any method, the result of a fingerprint is `null`. If the result is delegated to a variable, accessing it will raise an exception. + The result of a fingerprint that resolved successfully contains mutable and immutable references to the method and the class it is defined in. ```kt @@ -207,7 +226,7 @@ class MethodFingerprintScanResult( ## 🏹 Manual resolution of fingerprints -Unless a fingerprint is added to the constructor of `BytecodePatch`, the fingerprint will not be resolved automatically by ReVanced Patcher before the patch is executed. +Unless a fingerprint is added to a patch, the fingerprint will not be resolved automatically by ReVanced Patcher before the patch is executed. Instead, the fingerprint can be resolved manually using various overloads of the `resolve` function of a fingerprint. You can resolve a fingerprint in the following ways: @@ -217,12 +236,11 @@ You can resolve a fingerprint in the following ways: If you have a known list of classes you know the fingerprint can resolve on, you can resolve the fingerprint on the list of classes: ```kt - override fun execute(context: BytecodeContext) { - val result = ShowAdsFingerprint.also { it.resolve(context, context.classes) }.result - ?: throw PatchException("ShowAdsFingerprint not found") - - // ... - } + execute { + val result = showAdsFingerprint.also { + it.resolve(context, this.classes) + }.result ?: throw PatchException("showAdsFingerprint not found") + } ``` - On a **single class**, if the fingerprint can resolve on a single known class @@ -230,13 +248,12 @@ You can resolve a fingerprint in the following ways: If you know the fingerprint can resolve to a method in a specific class, you can resolve the fingerprint on the class: ```kt - override fun execute(context: BytecodeContext) { - val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } + execute { + val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } - val result = ShowAdsFingerprint.also { it.resolve(context, adsLoaderClass) }.result - ?: throw PatchException("ShowAdsFingerprint not found") - - // ... + val result = showAdsFingerprint.also { + it.resolve(context, adsLoaderClass) + }.result ?: throw PatchException("showAdsFingerprint not found") } ``` @@ -246,13 +263,13 @@ You can resolve a fingerprint in the following ways: A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out: ```kt - override fun execute(context: BytecodeContext) { - val adsFingerprintResult = ShowAdsFingerprint.result - ?: throw PatchException("ShowAdsFingerprint not found") + execute { + val adsFingerprintResult = showAdsFingerprint.result + ?: throw PatchException("showAdsFingerprint not found") - val proStringsFingerprint = object : MethodFingerprint( - strings = listOf("free", "trial") - ) {} + val proStringsFingerprint = methodFingerprint { + strings("free", "trial") + } proStringsFingerprint.also { it.resolve(context, adsFingerprintResult.method) @@ -260,7 +277,6 @@ You can resolve a fingerprint in the following ways: result.scanResult.stringsScanResult!!.matches.forEach { match -> println("The index of the string '${match.string}' is ${match.index}") } - } ?: throw PatchException("pro strings fingerprint not found") } ``` diff --git a/docs/2_2_patch_anatomy.md b/docs/2_2_patch_anatomy.md index 9b941f3f..75a4bfbf 100644 --- a/docs/2_2_patch_anatomy.md +++ b/docs/2_2_patch_anatomy.md @@ -64,145 +64,128 @@ Learn the API to create patches using ReVanced Patcher. ## ⛳️ Example patch -Throughout the documentation, the following example will be used to demonstrate the concepts of patches: - ```kt package app.revanced.patches.ads -@Patch( +val disableAdsPatch = bytecodePatch( name = "Disable ads", description = "Disable ads in the app.", - dependencies = [DisableAdsResourcePatch::class], - compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] -) -object DisableAdsPatch : BytecodePatch( - setOf(ShowAdsFingerprint) -) { - override fun execute(context: BytecodeContext) { - ShowAdsFingerprint.result?.let { result -> - result.mutableMethod.addInstructions( - 0, - """ - # Return false. - const/4 v0, 0x0 - return v0 - """ - ) - } ?: throw PatchException("ShowAdsFingerprint not found") +) { + compatibleWith { + "com.some.app"("1.0.0") } -} -``` - -## 🔎 Breakdown - -The example patch consists of the following parts: - -### 📝 Patch annotation -```kt -@Patch( - name = "Disable ads", - description = "Disable ads in the app.", - dependencies = [DisableAdsResourcePatch::class], - compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] -) + dependsOn { + disableAdsResourcePatch() + } + + val showAdsFingerprintResult by methodFingerprint { + // ... + } + + execute { + showAdsFingerprintResult.mutableMethod.addInstructions( + 0, + """ + # Return false. + const/4 v0, 0x0 + return v0 + """ + ) + } +} ``` -The `@Patch` annotation is used to provide metadata about the patch. - -Notable annotation parameters are: - -- `name`: The name of the patch. This is used as an identifier for the patch. - If this parameter is not set, `PatchBundleLoader` will not load the patch. - Other patches can still use this patch as a dependency -- `description`: A description of the patch. Can be unset if the name is descriptive enough -- `dependencies`: A set of patches which the patch depends on. The patches in this set will be executed before this patch. If a dependency patch raises an exception, this patch will not be executed; subsquently, other patches that depend on this patch will not be executed. -- `compatiblePackages`: A set of `CompatiblePackage` objects. Each `CompatiblePackage` object contains the package name and a set of compatible version names. This parameter can specify the packages and versions the patch is compatible with. Patches can still execute on incompatible packages, but it is recommended to use this parameter to list known compatible packages - - If unset, it is implied that the patch is compatible with all packages - - If the set of versions is unset, it is implied that the patch is compatible with all versions of the package - - If the set of versions is empty, it is implied that the patch is not compatible with any version of the package. This can be useful, for example, to prevent a patch from executing on specific packages that are known to be incompatible +> [!NOTE] +> +> - Patches do not require a name but `PatchLoader` will only load patches that are named. +> - Patches can depend on others. Dependencies are executed first. +If a dependency raises an exception, the dependent patch will not be executed. +> - 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 to declare 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 resolved on 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. +> +> - Circular dependencies are not allowed. If a patch depends on another patch, the other patch cannot depend on the first patch. +> - Dependencies inerhit compatibility from dependant patches. -> [!NOTE] -> The `@Patch` annotation is optional. If the patch does not require any metadata, it can be omitted. -> If the patch is only used as a dependency, the metadata, such as the `compatiblePackages` parameter, has no effect, as every dependency patch inherits the compatible packages of the patches that depend on it. > [!TIP] -> An abstract patch class can be annotated with `@Patch`. -> Patches extending off the abstract patch class will inherit the metadata of the abstract patch class. +> To see real-world examples of patches, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository. -> [!TIP] -> Instead of the `@Patch` annotation, the superclass's constructor can be used. This is useful in the example scenario where you want to create an abstract patch class. -> -> Example: -> -> ```kt -> abstract class AbstractDisableAdsPatch( -> fingerprints: Set -> ) : BytecodePatch( -> name = "Disable ads", -> description = "Disable ads in the app.", -> fingerprints -> ) { -> // ... -> } -> ``` -> -> Remember that this constructor has precedence over the `@Patch` annotation. +## 🧩 Patch API + +### ♻️ Finalization -### 🏗️ Patch class +Patches can have a finalization block that is called after all patches have been executed, in reverse order of patch execution. ```kt -object DisableAdsPatch : BytecodePatch( /* Parameters */ ) { - // ... +val patch = bytecodePatch(name = "Patch") { + dependsOn { + bytecodePatch(name = "Dependency") { + execute { + print("1") + } + + finalize { + print("4") + } + } + } + + execute { + print("2") + } + + finalize { + print("3") + } } ``` -Each patch class extends off a base class that implements the `Patch` interface. -The interface requires the `execute` method to be implemented. -Depending on which base class is extended, the patch can modify different parts of the APK as described in [🧩 Introduction to ReVanced Patches](2_introduction_to_patches.md). - -> [!TIP] -> A patch is usually a singleton object, meaning only one patch instance exists in the JVM. -> Because dependencies are executed before the patch itself, a patch can rely on the state of the dependency patch. -> This is useful in the example scenario, where the `DisableAdsPatch` depends on the `DisableAdsResourcePatch`. -> The `DisableAdsResourcePatch` can, for example, be used to read the decoded resources of the app and provide the `DisableAdsPatch` with the necessary information to disable ads because the `DisableAdsResourcePatch` is executed before the `DisableAdsPatch` and is a singleton object. +Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`. The 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 a patch. -### 🏁 The `execute` function +### ⚙️ Patch options -The `execute` function is declared in the `Patch` interface and needs to be implemented. -The `execute` function receives an instance of a context object that provides access to the APK. The patch can use this context to modify the APK as described in [🧩 Introduction to ReVanced Patches](2_introduction_to_patches.md). +Patches can have options that can be get and set before a patch is executed. There are multiple inbuilt types that can be used as options. -In the current example, the patch adds instructions at the beginning of a method implementation in the Dalvik VM bytecode. The added instructions return `false` to disable ads in the current example: +To define an option, use available `option` functions: ```kt -val result = LoadAdsFingerprint.result - ?: throw PatchException("LoadAdsFingerprint not found") - -result.mutableMethod.addInstructions( - 0, - """ - # Return false. - const/4 v0, 0x0 - return v0 - """ -) +val patch = bytecodePatch(name = "Patch") { + // Add an option with a custom type and delegate it's value to a variable. + val string by option(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) + } +} ``` -> [!NOTE] -> This patch uses a fingerprint to find the method and replaces the method's instructions with new instructions. -> The fingerprint is resolved on the classes present in `BytecodeContext`. -> Fingerprints will be explained in more detail on the next page. +Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch: -> [!TIP] -> The patch can also raise any `Exception` or `Throwable` at any time to indicate that the patch failed to execute. A `PatchException` is recommended to be raised if the patch fails to execute. -> If any patch depends on this patch, the dependent patch will not be executed, whereas other patches that do not depend on this patch can still be executed. -> ReVanced Patcher will handle any exception raised by a patch. +```kt +loadPatchesJar(patchesJarFile).apply { + // Type is checked at runtime. + first { it.name == "Patch" }.options["option"] = "Value" +} +``` -> [!TIP] -> To see real-world examples of patches, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository. +The type of an option can be obtained from the `type` property of the option: + +```kt +option.type // The KType of the option. +``` ## ⏭️ What's next diff --git a/docs/2_patches_intro.md b/docs/2_patches_intro.md index bcb3ab22..5975617e 100644 --- a/docs/2_patches_intro.md +++ b/docs/2_patches_intro.md @@ -73,47 +73,35 @@ There are multiple types of patches. Each type can modify a different part of th Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way. -A patch class can be annotated with `@Patch` to provide metadata about and dependencies of the patch. -Alternatively, a constructor of the superclass can be used. This is useful in the example scenario where you want to create an abstract patch class. +The `execute` function of is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed. The `execute` function receives an instance of a context object that provides access to the APK. The patch can use this context to modify the APK. -The entry point of a patch is the `execute` function. This function is called by ReVanced Patcher when the patch is executed. The `execute` function receives an instance of the context object that provides access to the APK. The patch can use this context to modify the APK. - -Each type of context provides different APIs to modify the APK. For example, the `BytecodeContext` provides APIs to modify the Dalvik VM bytecode, while the `ResourceContext` provides APIs to modify resources. +Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources. The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources if it is supplied a `ResourcePatch` for execution or if any kind of patch depends on a `ResourcePatch` and will not decode the resources before executing `RawResourcePatch`. Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK, whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case. Decoding and building resources is a time- and resource-consuming process, so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`. -Example of a `BytecodePatch`: +Example of patches: ```kt @Surpress("unused") -object MyPatch : BytecodePatch() { - override fun execute(context: BytecodeContext) { - // Your patch code here - } +val bytecodePatch = bytecodePatch { + execute { + // TODO + } } -``` - -Example of a `ResourcePatch`: -```kt @Surpress("unused") -object MyPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { - // Your patch code here - } +val rawResourcePatch = rawResourcePatch { + execute { + // TODO + } } -``` -Example of a `RawResourcePatch`: - -```kt @Surpress("unused") -object MyPatch : RawResourcePatch() { - override fun execute(context: ResourceContext) { - // Your patch code here - } +val resourcePatch = rawResourcePatch { + execute { + // TODO + } } -``` > [!TIP] > To see real-world examples of patches, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository. diff --git a/docs/3_structure_and_conventions.md b/docs/3_structure_and_conventions.md index 03ad9fa9..b0eb695f 100644 --- a/docs/3_structure_and_conventions.md +++ b/docs/3_structure_and_conventions.md @@ -76,8 +76,8 @@ Patches are organized in a specific file structure. The file structure is as fol ## 📙 Conventions -- 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `RemoveAdsPatch`. - If a patch changes the color of a button, name it `ChangeButtonColorPatch` +- 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `Remove ads`. + 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._ - 🔥 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. diff --git a/docs/4_apis.md b/docs/4_apis.md index 21c1cf7a..41fcc3f3 100644 --- a/docs/4_apis.md +++ b/docs/4_apis.md @@ -4,13 +4,12 @@ A handful of APIs are available to make patch development easier and more effici ## 📙 Overview -1. 👹 Create new mutable classes with `context.proxy(ClassDef)` -2. 🔍 Find and proxy existing classes with `BytecodeContext.findClass(Predicate)` -3. 🏃‍ Easily access referenced methods recursively by index with `BytecodeContext.toMethodWalker(Method)` +1. 👹 Mutate classes with `classDex.proxy()` +2. 🔍 Find and proxy existing classes with `classBy(Predicate)` +3. 🏃‍ Easily access referenced methods recursively by index with `method.navigate()` 4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications (Available in ReVanced Patches) -5. 💾 Read and write (decoded) resources with `ResourceContext.get(Path, Boolean) ` -6. 📃 Read and write DOM files using `ResourceContext.document` -7. 🔧 Equip patches with configurable options using `Patch.options` +5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)` +6. 📃 Read and write DOM files using `ResourcePatchContext.document` ### 🧰 APIs