From 4c66709131eb28e79f7d81aeb4a41dae5a17bbe1 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 19 Aug 2023 02:48:21 +0200 Subject: [PATCH 1/8] docs: init --- .github/workflows/update-documentation.yml | 19 ++ docs/0_preparation.md | 31 +++ docs/1_introduction.md | 43 ++++ docs/2_skeleton.md | 167 ++++++++++++++ docs/3_fingerprinting.md | 246 +++++++++++++++++++++ docs/4_structure_and_conventions.md | 48 ++++ docs/5_apis.md | 65 ++++++ docs/README.md | 14 ++ 8 files changed, 633 insertions(+) create mode 100644 .github/workflows/update-documentation.yml create mode 100644 docs/0_preparation.md create mode 100644 docs/1_introduction.md create mode 100644 docs/2_skeleton.md create mode 100644 docs/3_fingerprinting.md create mode 100644 docs/4_structure_and_conventions.md create mode 100644 docs/5_apis.md create mode 100644 docs/README.md diff --git a/.github/workflows/update-documentation.yml b/.github/workflows/update-documentation.yml new file mode 100644 index 0000000000..77097e2fe6 --- /dev/null +++ b/.github/workflows/update-documentation.yml @@ -0,0 +1,19 @@ +name: Update documentation + +on: + push: + paths: + - docs/** + +jobs: + trigger: + runs-on: ubuntu-latest + name: Dispatch event to documentation repository + if: github.ref == 'refs/heads/main' + steps: + - uses: peter-evans/repository-dispatch@v2 + with: + token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }} + repository: revanced/revanced-documentation + event-type: update-documentation + client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}' diff --git a/docs/0_preparation.md b/docs/0_preparation.md new file mode 100644 index 0000000000..d52a910ac3 --- /dev/null +++ b/docs/0_preparation.md @@ -0,0 +1,31 @@ +# 👶 Preparing a development environment + +To develop ReVanced patches, a certain development environment is required. + +## 📝 Prerequisites + +- A Java IDE supporting Kotlin such as [IntelliJ IDEA](https://www.jetbrains.com/idea/) +- Knowledge of Java, [Kotlin](https://kotlinlang.org) and [Dalvik bytecode](https://source.android.com/docs/core/runtime/dalvik-bytecode) +- Android reverse engineering tools such as [jadx](https://github.com/skylot/jadx) + +## 🏃 Prepare the environment + +For this guide, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as a base. + +1. Clone the repository + + ```bash + git clone https://github.com/revanced/revanced-patches && cd revanced-patches + ``` + +2. Build the patches + + ```bash + ./gradlew build + ``` + +## ⏭️ Whats next + +The following section will give you a basic understanding of [ReVanced Patcher](https://github.com/revanced/revanced-patcher). + +Continue: [💉 Introduction to ReVanced Patcher](1_introduction.md) diff --git a/docs/1_introduction.md b/docs/1_introduction.md new file mode 100644 index 0000000000..d5dfc99c57 --- /dev/null +++ b/docs/1_introduction.md @@ -0,0 +1,43 @@ +# 💉 Introduction to [ReVanced Patcher](https://github.com/revanced/revanced-patcher) + +Familiarize yourself with [ReVanced Patcher](https://github.com/revanced/revanced-patcher). + +## 📙 How it works + +```kt +// Prepare patches to apply and files to merge + +val patches = PatchBundle.Jar("revanced-patches.jar").loadPatches() +val mergeList = listOf("integrations.apk") + +// Create the options for the patcher + +val options = PatcherOptions( + inputFile = File("some.apk"), + resourceCacheDirectory = File("cache"), +) + +// Create the patcher and add the prepared patches and files + +val patcher = Patcher(options) + .also { it.addPatches(patches) } + .also { it.addFiles(mergeList) } + +// Execute and save the patched files + +patcher.executePatches().forEach { (patch, result) -> + val log = if (!result.isSuccess) + "failed" + else + "succeeded" + println("$patch $log") +} + +val result = patcher.save() +``` + +## ⏭️ Whats next + +The next section will give you an understanding of a patch. + +Continue: [🧩 Skeleton of a Patch](2_skeleton.md) diff --git a/docs/2_skeleton.md b/docs/2_skeleton.md new file mode 100644 index 0000000000..983de64ff6 --- /dev/null +++ b/docs/2_skeleton.md @@ -0,0 +1,167 @@ +# 🧩 Skeleton of a Patch + +Patches are what make ReVanced, ReVanced. On the following page the basic structure of a patch will be explained. + +## ⛳️ Example patch + +This page works with the following patch as an example: + +```kt +package app.revanced.patches.ads.patch + +// Imports + +@Patch +@Name("Disable ads") +@Description("Disables ads.") +@DependsOn([DisableAdResourcePatch:class]) +@Compatibility([Package("com.some.app", arrayOf("1.3.0"))]) +class DisableAdsPatch : BytecodePatch( + listOf(LoadAdsFingerprint) +) { + override fun execute(context: BytecodeContext): PatchResult { + val result = LoadAdsFingerprint.result + ?: return PatchResultError("LoadAdsFingerprint not found") + + result.mutableMethod.replaceInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """ + ) + + return PatchResultSuccess() + } +} +``` + +## 🔎 Dissecting the example patch + +Lets start with understanding, how a patch is structured. A patch is mainly built out of three components: + +1. 📝 Patch annotations + + ```kt + @Patch + @Name("Disable Ads") + @Description("Disables ads.") + @DependsOn([DisableAdResourcePatch:class]) + @Compatibility([Package("com.some.app", arrayOf("1.3.0"))]) + ``` + + To give context about the patch, annotations are used. They serve different but important purposes: + + - Every visible patch **should** be annotated with `@Patch` to be picked up by `PatchBundle` from the [introduction](1_introduction.md). Patches which are not annotated with `@Patch` can be referenced by other patches. We refer to those as _patch dependencies_. Patch dependencies are useful to structure multiple patches. + + Example: _To add settings switches to an app, first, a patch is required that can provide a basic framework for other patches to add their toggles to that app. Those patches refer to the dependency patch and use its framework to add their toggles to an app. [ReVanced Patcher](https://github.com/revanced/revanced-patcher) will execute the dependency and then the patch itself. The dependency can prepare a preference screen when executed and then initialize itself for further use by other patches._ + + - Visible patches **should** be annotated with `@Name`. This annotation does not serve any functional purpose. Instead, it allows referring to the patch with a name. [ReVanced Patches](https://github.com/revanced/revanced-patches) use _Sentence casing_ by convention, but any name can be used for patches. Patches with no `@Patch` annotation do not require the `@Name` annotation, because they are only useable as dependencies for other patches, and therefore are not visible through `PatchBundle`. + + - Visible patches should be annotated with `@Description`. This annotation serves the same purpose as the annotation `@Name`. It is used to give the patch a short description. + + - Patches can be annotated with `@DependsOn`. If the current patch depends on other patches, it can declare them as dependencies. + + Example: _The patch to remove ads needs to patch the bytecode. Additionally it makes use of a second patch, to get rid of resource files in the app which show ads in the app._ + + - **All patches** should be annotated with `@Compatibility`. This annotation is the most complex, but **most important** one and serves the purpose of constraining a patch to a package. Every patch is compatible with usually one or more packages. Additionally, the constraint can optionally be extended to versions of the package to discourage the use of the patch with versions outside of the constraint. + + Example: _The patch disables ads for an app. The app regularly updates and the code of the app mutates heavily. In that case the patch might not be compatible for future, untested versions of the app. To discourage the use of the app with other versions than the versions, this patch was confirmed to work on, it is constrained to those versions only._ + + - Annotate a patch with `@RequiresIntegrations` if it depends on additional integrations to be merged by [ReVanced Patcher](https://github.com/revanced/revanced-patcher). + + > Integrations are precompiled classes which are useful to off-load and useful for developing complex patches. Details of integrations and what exactly integrations are will be introduced properly on another page. + +2. 🏗️ Patch class + + ```kt + class DisableAdsPatch : BytecodePatch( /* Parameters */ ) { + // ... + } + ``` + + Usually, patches consist out of a single class. The class can be used to create methods and fields for the patch, or provide a framework for other patches, in case it is meant to be used as a dependency patch. + + [ReVanced Patches](https://github.com/revanced/revanced-patches) follow a convention to name the class of patches: + + Example: _The class for a patch which disables ads should be called `DisableAdsPatch`, for a patch which adds a new download feature it should be called `DownloadsPatch`._ + + Each patch implicitly implements the [Patch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L15) interface when extending off [ResourcePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L35) or [BytecodePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L42). The current example extends off `BytecodePatch`: + + ```kt + class DisableAdsPatch : BytecodePatch( /* Parameters */ ) { + // ... + } + ``` + + If the patch extends off `ResourcePatch`, it is able to **patch resources** such as `XML`, `PNG` or similar files. On the other hand, if the patche extends off `BytecodePatch`, it is able to **patch the bytecode** of an app. If a patch needs access to the resources and the bytecode at the same time. Either can use the other as a dependency. **Circular dependencies are unhandled.** + +3. 🏁 The `execute` method + + ```kt + override fun execute(context: BytecodeContext): PatchResult { + // ... + } + ``` + + The `execute` method is declared in the `Patch` interface and therefore part of any patch: + + ```kt + fun execute(context: /* Omitted */ T): PatchResult + ``` + + It is the **first** method executed when running the patch. The current example extends off `BytecodePatch`. Since patches that extend on it can interact with the bytecode, the signature for the execute method when implemented requires a [BytecodeContext](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/data/Context.kt#L23) as a parameter: + + ```kt + override fun execute(context: BytecodeContext): PatchResult { + // ... + } + ``` + + The `BytecodeContext` contains everything necessary related to bytecode for patches, including every class of the app on which the patch will be applied. Likewise, a `ResourcePatch` will require a [ResourceContext](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/data/Context.kt#L89) parameter and provide the patch with everything necessary to patch resources. + + The `execute` method has to be returned with [PatchResult](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt#L3). Patches may return early with `PatchResultError` if something went wrong. If this patch is used as a dependency for other patches, those patches will not execute subsequently. If a patch succeeds, `PatchResultSuccess` must be returned. + + In the current example the `execute` method runs the following code to replace instructions at the index `0` of the methods instruction list: + + ```kt + val result = LoadAdsFingerprint.result + ?: return PatchResultError("LoadAdsFingerprint not found") + + result.mutableMethod.replaceInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """ + ) + return PatchResultSuccess() + ``` + +> **Note**: Details of this implementation and what exactly `Fingerprints` are will be introduced properly on another page. + +## 🤏 Minimal template for a bytecode patch + +```kt +package app.revanced.patches.examples.minimal.patch + +// Imports + +@Patch +@Name("Minimal Demonstration") +@Description("Demonstrates a minimal implementation of a patch.") +@Compatibility([Package("com.some.app")]) +class MinimalExamplePatch : BytecodePatch() { + override fun execute(context: BytecodeContext) { + println("${MinimalExamplePatch::class.patchName} is being executed." ) + + return PatchResultSuccess() + } +} +``` + +## ⏭️ Whats next + +The next section will explain how fingerprinting works. + +Continue: [🔎 Fingerprinting](3_fingerprinting.md) diff --git a/docs/3_fingerprinting.md b/docs/3_fingerprinting.md new file mode 100644 index 0000000000..b5769da3d5 --- /dev/null +++ b/docs/3_fingerprinting.md @@ -0,0 +1,246 @@ +# 🔎 Fingerprinting + +Fingerprinting is the process of creating uniquely identifyable data about something arbitrarily large. In the context of ReVanced, fingerprinting is essential to be able to find classes, methods and fields without knowing their original names or certain other attributes, which would be used to identify them under normal circumstances. + +## ⛳️ Example fingerprint + +This page works with the following fingerprint as an example: + +```kt + +package app.revanced.patches.ads.fingerprints + +// Imports + +object LoadAdsFingerprint : MethodFingerprint( + returnType = "Z", + access = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z"), + opcodes = listOf(Opcode.RETURN), + strings = listOf("pro"), + customFingerprint = { it.definingClass == "Lcom/some/app/ads/Loader;"} +) +``` + +## 🆗 Understanding the example fingerprint + +The example fingerprint called `LoadAdsFingerprint` which extends on [`MethodFingerprint`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L28) is made to uniquely identify a certain method by capturing various attributes of the method such as the return type, access flags, an opcode pattern and more. The following code can be inferred just from the fingerprint: + +```kt + package com.some.app.ads + + // Imports + + 4 class Loader { + 5 public final Boolean (: Boolean) { + // ... + + 8 val userStatus = "pro"; + + // ... + +12 return + } + } +``` + +## 🚀 How it works + +Each attribute of the fingerprint is responsible to describe a specific but distinct part of the method. The combination out of those should be and ideally remain unique to all methods in all classes. In the case of the example fingerprint, the `customFingerprint` attribute is responsible to find the class the method is defined in. This greatly increases the uniqueness of the fingerprint, because now the possible methods reduce down to that class. Adding the signature of the method and a string the method implementation refers to in combination now creates a unique fingerprint in the current example: + +- Package & class (Line 4) + + ```kt + customFingerprint = { it.definingClass == "Lcom/some/app/ads/Loader;"} + ``` + +- Method signature (Line 5) + + ```kt + returnType = "Z", + access = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z"), + ``` + +- Method implementation (Line 8 & 12) + + ```kt + strings = listOf("pro"), + opcodes = listOf(Opcode.RETURN) + ``` + +## 🔨 How to use fingerprints + +After creating a fingerprint, add it to the constructor of the `BytecodePatch`: + +```kt +class DisableAdsPatch : BytecodePatch( + listOf(LoadAdsFingerprint) +) { /* .. */ } +``` + +ReVanced Patcher will try to [resolve the fingerprint](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L63) **before** it calls the `execute` method of the patch. + +The fingerprint can now be used in the patch by accessing [`MethodFingerprint.result`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L227): + +```kt +class DisableAdsPatch : BytecodePatch( + listOf(LoadAdsFingerprint) +) { + override fun execute(context: BytecodeContext): PatchResult { + val result = LoadAdsFingerprint.result + ?: return PatchResultError("LoadAdsFingerprint not found") + + // ... + } +} +``` + +> **Note**: `MethodFingerprint.result` **can be null** if the fingerprint does not match any method. In such case, the fingerprint needs to be fixed and made more resilient if the error is caused by a later version of an app which the fingerprint was not tested on. A fingerprint is good, if it is _light_, but still resilient - like Carbon fiber-reinforced polymers. + +If the fingerprint resolved to a method, the following properties are now available: + +```kt +data class MethodFingerprintResult( + val method: Method, + val classDef: ClassDef, + val scanResult: MethodFingerprintScanResult, + // ... +) { + val mutableClass + val mutableMethod + + // ... +} +``` + +> Details on how to use them in a patch and what exactly these are will be introduced properly later on this page. + +## 🏹 Different ways to resolve a fingerprint + +Usually, fingerprints are mostly resolved by the patcher, but it is also possible to manually resolve a fingerprint in a patch. This can be quite useful in lots of situations. To resolve a fingerprint you need a `BytecodeContext` to resolve it on. This context contains classes and thus methods to which the fingerprint can be resolved against. Example: _You have a fingerprint which you manually want to resolve **without** the help of the patcher._ + +> **Note**: A fingerprint should not be added to the constructor of `BytecodePatch` if manual resolution is intended, because the patcher would try resolve it before manual resolution. + +- On a **list of classes** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L49) + + This can be useful, if a fingerprint should be resolved to a smaller subset of classes, otherwise the fingerprint can be resolved by the patcher automatically. + + ```kt + class DisableAdsPatch : BytecodePatch( + /* listOf(LoadAdsFingerprint) */ + ) { + override fun execute(context: BytecodeContext): PatchResult { + val result = LoadAdsFingerprint.also { it.resolve(context, context.classes) }.result + ?: return PatchResultError("LoadAdsFingerprint not found") + + // ... + } + } + ``` + +- On a **single class** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L63) + + Sometimes you know a class but you need certain methods. In such case, you can resolve fingerprints on a class. + + ```kt + class DisableAdsPatch : BytecodePatch( + listOf(LoadAdsFingerprint) + ) { + override fun execute(context: BytecodeContext): PatchResult { + val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } + + val result = LoadAdsFingerprint.also { it.resolve(context, adsLoaderClass) }.result + ?: return PatchResultError("LoadAdsFingerprint not found") + + // ... + } + } + ``` + +- On a **method** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L78) + + Resolving a fingerprint on a method is mostly only useful if the fingerprint is used to resolve certain information about a method such as `MethodFingerprintResult.scanResult`. Example: _A fingerprint should be used to resolve the method which loads ads. For that the fingerprint is added to the constructor of `BytecodePatch`. An additional fingerprint is responsible for finding the indices of the instructions with certain string references in the implementation of the method the first fingerprint resolved to._ + + ```kt + class DisableAdsPatch : BytecodePatch( + /* listOf(LoadAdsFingerprint) */ + ) { + override fun execute(context: BytecodeContext): PatchResult { + // Make sure this fingerprint succeeds as the result is required + val adsFingerprintResult = LoadAdsFingerprint.result + ?: return PatchResultError("LoadAdsFingerprint not found") + + // Additional fingerprint to get the indices of two strings + val proStringsFingerprint = object : MethodFingerprint( + strings = listOf("free", "trial") + ) {} + + proStringsFingerprint.also { + // Resolve the fingerprint on the first fingerprints method + it.resolve(context, adsFingerprintResult.method) + }.result?.let { result -> + // Use the fingerprints result + result.scanResult.stringsScanResult!!.matches.forEach { match -> + println("The index of the string '${match.string}' is ${match.index}") + } + + } ?: return PatchResultError("pro strings fingerprint not found") + + return PatchResultSuccess + } + } + ``` + +## 🎯 The result of a fingerprint + +After a `MethodFingerprint` resolves successfully, its result can be used. The result contains mutable and immutable references to the method and the class it is defined in. + +> **Warning**: By default the immutable references **should be used** to prevent a mutable copy of the immutable references. For a patch to properly use a fingerprint though, usually write access is required. For that the mutable references can be used. + +Among them, the result also contains [MethodFingerprintResult.scanResult](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L239) which contains additional useful properties: + +```kt +data class MethodFingerprintScanResult( + val patternScanResult: PatternScanResult?, + val stringsScanResult: StringsScanResult? +) { + data class PatternScanResult( + val startIndex: Int, + val endIndex: Int, + var warnings: List? = null + ) + + data class StringsScanResult(val matches: List){ + data class StringMatch(val string: String, val index: Int) + } + + // ... +} +``` + +The following properties are utilized by bytecode patches: + +- The `MethodFingerprint.strings` allows patches to know the indices of the instructions which hold references to the strings. + +- If a fingerprint defines `MethodFingerprint.opcodes`, the start and end index of the first instructions matching that pattern will be available. These are useful to patch the implementation of methods relative to the pattern. Ideally the pattern contains the instructions opcodes pattern which is to be patched, in order to guarantee a successfull patch. + + > **Note**: Sometimes long patterns might be necessary, but the bigger the pattern list, the higher the chance it mutates if the app updates. For that reason the annotation `FuzzyPatternScanMethod` can be used on a fingerprint. The `FuzzyPatternScanMethod.threshold` will define, how many opcodes can remain unmatched. `PatternScanResult.warnings` can then be used, if is necessary to know where pattern missmatches occured. + +## ⭐ Closely related code examples + +### 🧩 Patches + +- [CommentsPatch](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/comments/bytecode/patch/CommentsPatch.kt) +- [MusicVideoAdsPatch](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/music/ad/video/patch/MusicVideoAdsPatch.kt) + +### 🔍 Fingerprints + +- [LoadVideoAdsFingerprint](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/ad/video/fingerprints/LoadVideoAdsFingerprint.kt) +- [SeekbarTappingParentFingerprint](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/fingerprints/SeekbarTappingParentFingerprint.kt) + +## ⏭️ Whats next + +The next section will give a suggestion on coding conventions and on the file structure of a patch. + +Continue: [📜 Patch file structure and conventions](4_structure_and_conventions.md) diff --git a/docs/4_structure_and_conventions.md b/docs/4_structure_and_conventions.md new file mode 100644 index 0000000000..145a65d6e2 --- /dev/null +++ b/docs/4_structure_and_conventions.md @@ -0,0 +1,48 @@ +# 📜 Patch file structure and conventions + +ReVanced follows a couple of conventions when creating patches which can be found in [ReVanced Patches](https://github.com/revanced/revanced-patches). + +## 📁 File structure + +Each patch is structured the following way: + +```text +📦your.patches.app.category.patch + ├ 📂annotations + ├ └ ⚙️SomePatchCompatibility.kt + ├ 📂fingerprints + ├ ├ 🔍SomeFingerprintA.kt + ├ └ 🔍SomeFingerprintB.kt + ├ 📂patch + └ └ 🧩SomePatch.kt +``` + +### 🆗 Example + +As an example the structure of [`VideoAdsPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/ad/video) can be used as a reference: + +```text +📦app.revanced.patches.youtube.ad.video + ├ 📂annotations + ├ └ ⚙️VideoAdsCompatibility.kt + ├ 📂fingerprints + ├ └ 🔍LoadVideoAdsFingerprint.kt + ├ 📂patch + └ └ 🧩VideoAdsPatch.kt +``` + +## 📙 Conventions + +> **Note**: More ⭐ equals more importance + +- ⭐⭐ **`@Patch` should be named by what they accomplish**. Example: _To patch ads on videos, the patch should be called `HideVideoAdsPatch`._ + +- ⭐⭐ **`@Description` should be written in third person and end with punctuation**. Example: _Removes ads in the video player._ + +- ⭐ **Resource and bytecode patches should be properly separated**. That means, bytecode patches handle patching bytecode, while resource patches handle resources. As an example, [`SponsorBlockPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock) can be used. + +- ⭐⭐⭐ **Allocate as little code as possible in patches**. This reduces the risk of failing patches. In the example of [`SponsorBlockPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock), most of the code logic is written in [revanced-integrations](https://github.com/revanced/revanced-integrations). The patches now only insert references to public methods from the integrations which are merged into the app which is far better than writing huge bytecode patches. + +- ⭐⭐⭐ **Create small but strong fingerprints**. This is essential for patches to last long, because fingerprints create the foundation for patches to find the places where patches need to be done. A small fingerprint guarantees that it remains in tact in case the app updates and code mutates, but can also can cause problems if it is not unique enough and for example resolve to a wrong method or give the wrong indices of instructions if a pattern is used. A fingerprint consisting out of couple distinct strings is a small but strong fingerprint, on the other hand, a fingerprint which contains a huge list of opcodes can be strong, but is likely fail to resolve in the future because the instructions could mutate with an update of the app. + +- ⭐⭐⭐ **Document patches**. This is essential as a future reference when reading the code. Explaining what certain patches do and accomplish guarantees, that the code can be understood in the future in the case it needs to be updated. Example code comment: _Patch the return value to true in order to spoof the pro status of the user. This turns off ads._ diff --git a/docs/5_apis.md b/docs/5_apis.md new file mode 100644 index 0000000000..84fee68d07 --- /dev/null +++ b/docs/5_apis.md @@ -0,0 +1,65 @@ +# 💪 Advanced APIs + +[ReVanced](https://github.com/revanced/) comes with APIs which assist with the development of patches. + +## 📙 Overview + +1. 👹 Create mutable classes with `context.proxy(classDef)` +2. 🔍 Find mutable classes with `BytecodeContext.findClass(predicate)` +3. 🏃‍ Walk through the method call hierarchy with `BytecodeContext.toMethodWalker(startMethod)` +4. 🔨 Work with resources from patches with `ResourceUtils` +5. 💾 Read and write resources with `ResourceContext.get(path)` +6. 📃 Edit xml files with `DomFileEditor` +7. 🔧 Implement settings with `app.revanced.patches.shared.settings` + +### 🧰 APIs + +- #### 👹 Create mutable classes with `context.proxy(classDef)` + + To be able to make changes to classes, it is necessary to work on a mutable clone of that class. + For that, the `BytecodeContext` allows to create mutable instances of classes with `context.proxy(classDef)`. + + Example: + + ```kt + override fun execute(context: BytecodeContext) { + // Code + + val classProxy = context.proxy(someClass) + + // From now on, this class is shadowed over the original class. + // The original class can still be found in context.classes. + val proxy = classProxy.mutableClass + + // Code + + classProxy.mutableClass.fields.add(someField) + + return PatchResultSuccess() + } + ``` + > **Note**: The mutable clone will now be used for every future modification on the class, even in other patches, + if `ClassProxy.mutableClass` is accessed. This means, if you try to proxy the same class twice, you will get the same + instance of the mutable clone. + + > **Note**: On the page [🔎 Fingerprinting](3_fingerprinting.md) the result of fingerprints were introduced. + Accessing [`MethodFingerprint.mutableClass`](https://github.com/revanced/revanced-patcher/blob/main/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L290) + or [`MutableFingerprint.mutableMethod`](https://github.com/revanced/revanced-patcher/blob/main/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L298) + also creates a mutable clone of the class through a `ClassProxy`. + If you now were to proxy the same class, the fingerprint just proxied, the same mutable clone instance would be used. + This also applies for fingerprints, which resolve to the same method. + + > **Warning**: Rely on the immutable types as much as possible to avoid creating mutable clones. + + An example on how this api is used can be found + in [`GeneralAdsPatch`](https://github.com/revanced/revanced-patches/blob/f870178a77d4cb52e1940baa67aaa9526169d10d/src/main/kotlin/app/revanced/patches/reddit/ad/general/patch/GeneralAdsPatch.kt#L33). + +- #### 🔍 Find mutable classes with `BytecodeContext.findClass(predicate)` + + This api allows to find classes by a predicate or the class name. + It will return a `ClassProxy` instance by proxying the found class + and thus either access the immutable or when necessary, the mutable clone of the class. + + An example on how this api is used can be found + in [`HideCastButtonPatch`](https://github.com/revanced/revanced-patches/blob/0533e6c63e8da02f0b2b4df9652450c178255215/src/main/kotlin/app/revanced/patches/youtube/layout/castbutton/patch/HideCastButtonPatch.kt#L39). + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..22b83ba702 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,14 @@ +# 🧩 Development of [ReVanced Patches](https://github.com/revanced/revanced-patches) + +This guide teaches the fundamentals of patches and everything necessary to create your own patch from scratch. + +> Existing patches are adviced to be taken as reference throughout the entire guide. + +## 📖 Table of content + +1. [👶 Preparing a development environment](0_preparation.md) +2. [💉 Introduction to ReVanced Patcher](1_introduction.md) +3. [🧩 Skeleton of a Patch](2_skeleton.md) + - [🔎 Fingerprinting](3_fingerprinting.md) +4. [📜 Patch file structure and conventions](4_structure_and_conventions.md) +5. [💪 Advanced APIs](5_apis.md) From 914bb7fbdbe648b74a335cd5be62371483374500 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 19 Aug 2023 15:15:35 +0200 Subject: [PATCH 2/8] docs: update API usage --- docs/1_introduction.md | 65 ++++++++++++++++++++++++---------------- docs/2_skeleton.md | 28 +++++++---------- docs/3_fingerprinting.md | 20 ++++++------- docs/5_apis.md | 2 -- 4 files changed, 60 insertions(+), 55 deletions(-) diff --git a/docs/1_introduction.md b/docs/1_introduction.md index d5dfc99c57..f771c6a861 100644 --- a/docs/1_introduction.md +++ b/docs/1_introduction.md @@ -5,35 +5,50 @@ Familiarize yourself with [ReVanced Patcher](https://github.com/revanced/revance ## 📙 How it works ```kt -// Prepare patches to apply and files to merge - -val patches = PatchBundle.Jar("revanced-patches.jar").loadPatches() -val mergeList = listOf("integrations.apk") - -// Create the options for the patcher - -val options = PatcherOptions( - inputFile = File("some.apk"), - resourceCacheDirectory = File("cache"), -) - -// Create the patcher and add the prepared patches and files - +/** + * Instantiate ReVanced Patcher with options. + * This will decode the app manifest of the input file to read package metadata + * such as package name and version code. + */ +val options = PatcherOptions(inputFile = File("some.apk")) val patcher = Patcher(options) - .also { it.addPatches(patches) } - .also { it.addFiles(mergeList) } - -// Execute and save the patched files -patcher.executePatches().forEach { (patch, result) -> - val log = if (!result.isSuccess) - "failed" - else - "succeeded" - println("$patch $log") +/** + * Load ReVanced Patches and ReVanced Integrations. + * + * PatchBundleLoader can be used to load a list of patches from the supplied file in a new ClassLoader instance. + * You can now set their options and supply them to ReVanced Patcher. + * + * Executing patches multiple times from the same ClassLoader instance may fail because they may not reset their state. + * Therefore, a new PatchBundleLoader should be used for every execution of ReVanced Patcher. + */ +val patches = PatchBundleLoader.Jar(File("revanced-patches.jar")) +val integrations = listOf(File("integrations.apk")) + +// Add them to ReVanced Patcher. +patcher.acceptPatches(patches) +patcher.acceptIntegrations(integrations) + +// Execute patches (blocking asynchronous flow of PatchResult). +// Since this is an asynchronous flow, it can be cancelled if necessary. +runBlocking { + patcher.apply(false).collect { patchResult -> + if (patchResult.exception != null) + println("${patchResult.patchName} failed:\n${patchResult.exception}") + else + println("${patchResult.patchName} succeeded") + } } -val result = patcher.save() +// Compile patched DEX files and resources. +val result = patcher.get() + +val dexFiles = result.dexFiles // Patched DEX files. +val resourceFile = result.resourceFile // File containing patched resources. +val doNotCompress = result.doNotCompress // Files that should not be compressed. + +// Close patcher to free resources. +patcher.close() ``` ## ⏭️ Whats next diff --git a/docs/2_skeleton.md b/docs/2_skeleton.md index 983de64ff6..3c3f2ced96 100644 --- a/docs/2_skeleton.md +++ b/docs/2_skeleton.md @@ -19,9 +19,9 @@ package app.revanced.patches.ads.patch class DisableAdsPatch : BytecodePatch( listOf(LoadAdsFingerprint) ) { - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { val result = LoadAdsFingerprint.result - ?: return PatchResultError("LoadAdsFingerprint not found") + ?: throw PatchException("LoadAdsFingerprint not found") result.mutableMethod.replaceInstructions( 0, @@ -30,15 +30,13 @@ class DisableAdsPatch : BytecodePatch( return v0 """ ) - - return PatchResultSuccess() } } ``` ## 🔎 Dissecting the example patch -Lets start with understanding, how a patch is structured. A patch is mainly built out of three components: +Let's start with understanding, how a patch is structured. A patch is mainly built out of three components: 1. 📝 Patch annotations @@ -62,7 +60,7 @@ Lets start with understanding, how a patch is structured. A patch is mainly buil - Patches can be annotated with `@DependsOn`. If the current patch depends on other patches, it can declare them as dependencies. - Example: _The patch to remove ads needs to patch the bytecode. Additionally it makes use of a second patch, to get rid of resource files in the app which show ads in the app._ + Example: _The patch to remove ads needs to patch the bytecode. Additionally, it makes use of a second patch, to get rid of resource files in the app which show ads in the app._ - **All patches** should be annotated with `@Compatibility`. This annotation is the most complex, but **most important** one and serves the purpose of constraining a patch to a package. Every patch is compatible with usually one or more packages. Additionally, the constraint can optionally be extended to versions of the package to discourage the use of the patch with versions outside of the constraint. @@ -99,7 +97,7 @@ Lets start with understanding, how a patch is structured. A patch is mainly buil 3. 🏁 The `execute` method ```kt - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { // ... } ``` @@ -107,26 +105,26 @@ Lets start with understanding, how a patch is structured. A patch is mainly buil The `execute` method is declared in the `Patch` interface and therefore part of any patch: ```kt - fun execute(context: /* Omitted */ T): PatchResult + fun execute(context: /* Omitted */ T) ``` It is the **first** method executed when running the patch. The current example extends off `BytecodePatch`. Since patches that extend on it can interact with the bytecode, the signature for the execute method when implemented requires a [BytecodeContext](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/data/Context.kt#L23) as a parameter: ```kt - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { // ... } ``` The `BytecodeContext` contains everything necessary related to bytecode for patches, including every class of the app on which the patch will be applied. Likewise, a `ResourcePatch` will require a [ResourceContext](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/data/Context.kt#L89) parameter and provide the patch with everything necessary to patch resources. - The `execute` method has to be returned with [PatchResult](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt#L3). Patches may return early with `PatchResultError` if something went wrong. If this patch is used as a dependency for other patches, those patches will not execute subsequently. If a patch succeeds, `PatchResultSuccess` must be returned. + Patches may throw `PatchException` if something went wrong. If this patch is used as a dependency for other patches, those patches will not execute subsequently. In the current example the `execute` method runs the following code to replace instructions at the index `0` of the methods instruction list: ```kt val result = LoadAdsFingerprint.result - ?: return PatchResultError("LoadAdsFingerprint not found") + ?: throw PatchException("LoadAdsFingerprint not found") result.mutableMethod.replaceInstructions( 0, @@ -135,7 +133,6 @@ Lets start with understanding, how a patch is structured. A patch is mainly buil return v0 """ ) - return PatchResultSuccess() ``` > **Note**: Details of this implementation and what exactly `Fingerprints` are will be introduced properly on another page. @@ -152,11 +149,8 @@ package app.revanced.patches.examples.minimal.patch @Description("Demonstrates a minimal implementation of a patch.") @Compatibility([Package("com.some.app")]) class MinimalExamplePatch : BytecodePatch() { - override fun execute(context: BytecodeContext) { - println("${MinimalExamplePatch::class.patchName} is being executed." ) - - return PatchResultSuccess() - } + override fun execute(context: BytecodeContext) = + println("${MinimalExamplePatch::class.patchName} is being executed." ) } ``` diff --git a/docs/3_fingerprinting.md b/docs/3_fingerprinting.md index b5769da3d5..fa7ee6c281 100644 --- a/docs/3_fingerprinting.md +++ b/docs/3_fingerprinting.md @@ -87,9 +87,9 @@ The fingerprint can now be used in the patch by accessing [`MethodFingerprint.re class DisableAdsPatch : BytecodePatch( listOf(LoadAdsFingerprint) ) { - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { val result = LoadAdsFingerprint.result - ?: return PatchResultError("LoadAdsFingerprint not found") + ?: throw PatchException("LoadAdsFingerprint not found") // ... } @@ -130,9 +130,9 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl class DisableAdsPatch : BytecodePatch( /* listOf(LoadAdsFingerprint) */ ) { - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { val result = LoadAdsFingerprint.also { it.resolve(context, context.classes) }.result - ?: return PatchResultError("LoadAdsFingerprint not found") + ?: throw PatchException("LoadAdsFingerprint not found") // ... } @@ -147,11 +147,11 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl class DisableAdsPatch : BytecodePatch( listOf(LoadAdsFingerprint) ) { - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } val result = LoadAdsFingerprint.also { it.resolve(context, adsLoaderClass) }.result - ?: return PatchResultError("LoadAdsFingerprint not found") + ?: throw PatchException("LoadAdsFingerprint not found") // ... } @@ -166,10 +166,10 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl class DisableAdsPatch : BytecodePatch( /* listOf(LoadAdsFingerprint) */ ) { - override fun execute(context: BytecodeContext): PatchResult { + override fun execute(context: BytecodeContext) { // Make sure this fingerprint succeeds as the result is required val adsFingerprintResult = LoadAdsFingerprint.result - ?: return PatchResultError("LoadAdsFingerprint not found") + ?: throw PatchException("LoadAdsFingerprint not found") // Additional fingerprint to get the indices of two strings val proStringsFingerprint = object : MethodFingerprint( @@ -185,9 +185,7 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl println("The index of the string '${match.string}' is ${match.index}") } - } ?: return PatchResultError("pro strings fingerprint not found") - - return PatchResultSuccess + } ?: throw PatchException("pro strings fingerprint not found") } } ``` diff --git a/docs/5_apis.md b/docs/5_apis.md index 84fee68d07..347c6d2ced 100644 --- a/docs/5_apis.md +++ b/docs/5_apis.md @@ -34,8 +34,6 @@ // Code classProxy.mutableClass.fields.add(someField) - - return PatchResultSuccess() } ``` > **Note**: The mutable clone will now be used for every future modification on the class, even in other patches, From 524f783d6453f7645f554b898dc54b60cdec792e Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 20 Sep 2023 06:21:27 +0200 Subject: [PATCH 3/8] docs: update API usage --- docs/1_introduction.md | 63 +++++++------ docs/2_skeleton.md | 135 ++++++++++++++++++---------- docs/3_fingerprinting.md | 80 +++++++++++------ docs/4_structure_and_conventions.md | 49 ++++++---- 4 files changed, 203 insertions(+), 124 deletions(-) diff --git a/docs/1_introduction.md b/docs/1_introduction.md index f771c6a861..0904d8854c 100644 --- a/docs/1_introduction.md +++ b/docs/1_introduction.md @@ -5,50 +5,47 @@ Familiarize yourself with [ReVanced Patcher](https://github.com/revanced/revance ## 📙 How it works ```kt -/** - * Instantiate ReVanced Patcher with options. - * This will decode the app manifest of the input file to read package metadata - * such as package name and version code. - */ -val options = PatcherOptions(inputFile = File("some.apk")) -val patcher = Patcher(options) - /** * Load ReVanced Patches and ReVanced Integrations. * * PatchBundleLoader can be used to load a list of patches from the supplied file in a new ClassLoader instance. - * You can now set their options and supply them to ReVanced Patcher. - * + * You can set their options and supply them to ReVanced Patcher. + * * Executing patches multiple times from the same ClassLoader instance may fail because they may not reset their state. * Therefore, a new PatchBundleLoader should be used for every execution of ReVanced Patcher. */ val patches = PatchBundleLoader.Jar(File("revanced-patches.jar")) val integrations = listOf(File("integrations.apk")) -// Add them to ReVanced Patcher. -patcher.acceptPatches(patches) -patcher.acceptIntegrations(integrations) - -// Execute patches (blocking asynchronous flow of PatchResult). -// Since this is an asynchronous flow, it can be cancelled if necessary. -runBlocking { - patcher.apply(false).collect { patchResult -> - if (patchResult.exception != null) - println("${patchResult.patchName} failed:\n${patchResult.exception}") - else - println("${patchResult.patchName} succeeded") - } +/** + * Instantiate ReVanced Patcher with options. + * This will decode the app manifest of the input file to read package metadata + * such as package name and version code. + */ +val options = PatcherOptions(inputFile = File("some.apk")) +Patcher(options).use { patcher -> + val patcherResult = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(patches) + + // Execute patches. + runBlocking { + patcher.apply(false).collect { patchResult -> + if (patchResult.exception != null) + println("${patchResult.patchName} failed:\n${patchResult.exception}") + else + println("${patchResult.patchName} succeeded") + } + } + }.get() + + // Compile patched DEX files and resources. + val result = patcher.get() + + val dexFiles = result.dexFiles // Patched DEX files. + val resourceFile = result.resourceFile // File containing patched resources. + val doNotCompress = result.doNotCompress // Files that should not be compressed. } - -// Compile patched DEX files and resources. -val result = patcher.get() - -val dexFiles = result.dexFiles // Patched DEX files. -val resourceFile = result.resourceFile // File containing patched resources. -val doNotCompress = result.doNotCompress // Files that should not be compressed. - -// Close patcher to free resources. -patcher.close() ``` ## ⏭️ Whats next diff --git a/docs/2_skeleton.md b/docs/2_skeleton.md index 3c3f2ced96..394d1bd1db 100644 --- a/docs/2_skeleton.md +++ b/docs/2_skeleton.md @@ -11,24 +11,25 @@ package app.revanced.patches.ads.patch // Imports -@Patch -@Name("Disable ads") -@Description("Disables ads.") -@DependsOn([DisableAdResourcePatch:class]) -@Compatibility([Package("com.some.app", arrayOf("1.3.0"))]) -class DisableAdsPatch : BytecodePatch( - listOf(LoadAdsFingerprint) +@Patch( + name = "Disable Ads", + description = "Disable ads.", + dependsOn = [DisableAdResourcePatch::class], + compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] +) +object DisableAdsPatch : BytecodePatch( + setOf(LoadAdsFingerprint) ) { override fun execute(context: BytecodeContext) { val result = LoadAdsFingerprint.result ?: throw PatchException("LoadAdsFingerprint not found") result.mutableMethod.replaceInstructions( - 0, - """ - const/4 v0, 0x1 - return v0 - """ + 0, + """ + const/4 v0, 0x1 + return v0 + """ ) } } @@ -40,59 +41,87 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui 1. 📝 Patch annotations + > Note: + > Patch annotations can be imported from and processed by the Maven package `app.revanced.revanced-patch-annotation-processor` + > By default, the parameters of the super constructor of patches can be used. + ```kt - @Patch - @Name("Disable Ads") - @Description("Disables ads.") - @DependsOn([DisableAdResourcePatch:class]) - @Compatibility([Package("com.some.app", arrayOf("1.3.0"))]) + @Patch( + name = "Disable Ads", + description = "Disable ads.", + dependsOn = [DisableAdResourcePatch::class], + compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] + ) ``` To give context about the patch, annotations are used. They serve different but important purposes: - - Every visible patch **should** be annotated with `@Patch` to be picked up by `PatchBundle` from the [introduction](1_introduction.md). Patches which are not annotated with `@Patch` can be referenced by other patches. We refer to those as _patch dependencies_. Patch dependencies are useful to structure multiple patches. - - Example: _To add settings switches to an app, first, a patch is required that can provide a basic framework for other patches to add their toggles to that app. Those patches refer to the dependency patch and use its framework to add their toggles to an app. [ReVanced Patcher](https://github.com/revanced/revanced-patcher) will execute the dependency and then the patch itself. The dependency can prepare a preference screen when executed and then initialize itself for further use by other patches._ + - Every visible that sets `@Patch.name` will be loadable by `PatchBundleLoader` from the [introduction](1_introduction.md). + Patches that do not set `@Patch.name` can be referenced by other patches. + We refer to those as _patch dependencies_. Patch dependencies are useful to structure multiple patches. - - Visible patches **should** be annotated with `@Name`. This annotation does not serve any functional purpose. Instead, it allows referring to the patch with a name. [ReVanced Patches](https://github.com/revanced/revanced-patches) use _Sentence casing_ by convention, but any name can be used for patches. Patches with no `@Patch` annotation do not require the `@Name` annotation, because they are only useable as dependencies for other patches, and therefore are not visible through `PatchBundle`. + Example: _To add settings switches to an app, first, a patch is required that can provide a basic framework + for other patches to add their toggles to that app. Those patches refer to the dependency patch + and use its framework to add their toggles to an app. [ReVanced Patcher](https://github.com/revanced/revanced-patcher) will execute the dependency + and then the patch itself. The dependency can prepare a preference screen when executed and then initialize itself + for further use by other patches._ - - Visible patches should be annotated with `@Description`. This annotation serves the same purpose as the annotation `@Name`. It is used to give the patch a short description. + - Patches may set `@Path.description`. + This annotation is used to briefly describe the patch. - - Patches can be annotated with `@DependsOn`. If the current patch depends on other patches, it can declare them as dependencies. + - Patches may set `@Patch.dependencies`. + If the current patch depends on other patches, it can declare them as dependencies. - Example: _The patch to remove ads needs to patch the bytecode. Additionally, it makes use of a second patch, to get rid of resource files in the app which show ads in the app._ + Example: _The patch to remove ads needs to patch the bytecode. + Additionally, it makes use of a second patch, to get rid of resource files in the app which show ads in the app._ - - **All patches** should be annotated with `@Compatibility`. This annotation is the most complex, but **most important** one and serves the purpose of constraining a patch to a package. Every patch is compatible with usually one or more packages. Additionally, the constraint can optionally be extended to versions of the package to discourage the use of the patch with versions outside of the constraint. + - Patches may set `@Patch.compatiblePackages`. + This annotation serves the purpose of constraining a patch to a package. + Every patch is compatible with usually one or more packages. + Additionally, the constraint may specify versions of the package it is guaranteed to be compatible with. - Example: _The patch disables ads for an app. The app regularly updates and the code of the app mutates heavily. In that case the patch might not be compatible for future, untested versions of the app. To discourage the use of the app with other versions than the versions, this patch was confirmed to work on, it is constrained to those versions only._ + Example: _The patch disables ads for an app. + The app regularly updates and the code of the app mutates heavily. In that case the patch might not be compatible + for future, untested versions of the app. To discourage the use of the app with other versions than the versions, + this patch was confirmed to work on, it is constrained to those versions only._ - - Annotate a patch with `@RequiresIntegrations` if it depends on additional integrations to be merged by [ReVanced Patcher](https://github.com/revanced/revanced-patcher). + - A patch may set `@Patch.requiresIntegrations` to true, + if it depends on additional integrations to be merged by [ReVanced Patcher](https://github.com/revanced/revanced-patcher). - > Integrations are precompiled classes which are useful to off-load and useful for developing complex patches. Details of integrations and what exactly integrations are will be introduced properly on another page. + > Integrations are precompiled classes which are useful to off-load and useful for developing complex patches. + Details of integrations and what exactly integrations are will be introduced properly on another page. 2. 🏗️ Patch class ```kt - class DisableAdsPatch : BytecodePatch( /* Parameters */ ) { + object DisableAdsPatch : BytecodePatch( /* Parameters */ ) { // ... } ``` - Usually, patches consist out of a single class. The class can be used to create methods and fields for the patch, or provide a framework for other patches, in case it is meant to be used as a dependency patch. + Usually, patches consist out of a single object class. + The class can be used to create methods and fields for the patch, or provide a framework for other patches, + in case it is meant to be used as a dependency patch. [ReVanced Patches](https://github.com/revanced/revanced-patches) follow a convention to name the class of patches: - Example: _The class for a patch which disables ads should be called `DisableAdsPatch`, for a patch which adds a new download feature it should be called `DownloadsPatch`._ + Example: _The class for a patch which disables ads should be called `DisableAdsPatch`, + for a patch which adds a new download feature it should be called `DownloadsPatch`._ - Each patch implicitly implements the [Patch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L15) interface when extending off [ResourcePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L35) or [BytecodePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L42). The current example extends off `BytecodePatch`: + Each patch implicitly extends the [Patch](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt#L27) class +3. when extending off [ResourcePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L35) or [BytecodePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L42). The current example extends off `BytecodePatch`: ```kt - class DisableAdsPatch : BytecodePatch( /* Parameters */ ) { + object DisableAdsPatch : BytecodePatch( /* Parameters */ ) { // ... } ``` - If the patch extends off `ResourcePatch`, it is able to **patch resources** such as `XML`, `PNG` or similar files. On the other hand, if the patche extends off `BytecodePatch`, it is able to **patch the bytecode** of an app. If a patch needs access to the resources and the bytecode at the same time. Either can use the other as a dependency. **Circular dependencies are unhandled.** + If the patch extends off `ResourcePatch`, it is able to **patch resources** such as `XML`, `PNG` or similar files. + On the other hand, if the patch extends off `BytecodePatch`, it is able to **patch the bytecode** of an app. + If a patch needs access to the resources and the bytecode at the same time. + Either can use the other as a dependency. + **Patches involving circular dependencies can not be added to a `Patcher` instance.** 3. 🏁 The `execute` method @@ -102,13 +131,15 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui } ``` - The `execute` method is declared in the `Patch` interface and therefore part of any patch: + The `execute` method is declared in the `Patch` class and therefore part of any patch: ```kt fun execute(context: /* Omitted */ T) ``` - It is the **first** method executed when running the patch. The current example extends off `BytecodePatch`. Since patches that extend on it can interact with the bytecode, the signature for the execute method when implemented requires a [BytecodeContext](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/data/Context.kt#L23) as a parameter: + It is the **first** method executed when running the patch. + The current example extends off `BytecodePatch`. Since patches that extend on it can interact with the bytecode, + the signature for the execute method when implemented requires a [BytecodeContext](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt) as a parameter: ```kt override fun execute(context: BytecodeContext) { @@ -116,22 +147,27 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui } ``` - The `BytecodeContext` contains everything necessary related to bytecode for patches, including every class of the app on which the patch will be applied. Likewise, a `ResourcePatch` will require a [ResourceContext](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/data/Context.kt#L89) parameter and provide the patch with everything necessary to patch resources. + The `BytecodeContext` contains everything necessary related to bytecode for patches, + including every class of the app on which the patch will be applied. + Likewise, a `ResourcePatch` will require a [ResourceContext](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt) + parameter and provide the patch with everything necessary to patch resources. - Patches may throw `PatchException` if something went wrong. If this patch is used as a dependency for other patches, those patches will not execute subsequently. + Patches may throw `PatchException` if something went wrong. + If this patch is used as a dependency for other patches, those patches will not execute subsequently. - In the current example the `execute` method runs the following code to replace instructions at the index `0` of the methods instruction list: + In the current example the `execute` method runs the following code to replace instructions at the index `0` + of the methods instruction list: ```kt val result = LoadAdsFingerprint.result ?: throw PatchException("LoadAdsFingerprint not found") result.mutableMethod.replaceInstructions( - 0, - """ - const/4 v0, 0x1 - return v0 - """ + 0, + """ + const/4 v0, 0x1 + return v0 + """ ) ``` @@ -144,11 +180,12 @@ package app.revanced.patches.examples.minimal.patch // Imports -@Patch -@Name("Minimal Demonstration") -@Description("Demonstrates a minimal implementation of a patch.") -@Compatibility([Package("com.some.app")]) -class MinimalExamplePatch : BytecodePatch() { +@Patch( + name = "Minimal Demonstration", + description = "Demonstrates a minimal implementation of a patch.", + compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] +) +object MinimalExamplePatch : BytecodePatch() { override fun execute(context: BytecodeContext) = println("${MinimalExamplePatch::class.patchName} is being executed." ) } diff --git a/docs/3_fingerprinting.md b/docs/3_fingerprinting.md index fa7ee6c281..e163693082 100644 --- a/docs/3_fingerprinting.md +++ b/docs/3_fingerprinting.md @@ -46,7 +46,12 @@ The example fingerprint called `LoadAdsFingerprint` which extends on [`MethodFin ## 🚀 How it works -Each attribute of the fingerprint is responsible to describe a specific but distinct part of the method. The combination out of those should be and ideally remain unique to all methods in all classes. In the case of the example fingerprint, the `customFingerprint` attribute is responsible to find the class the method is defined in. This greatly increases the uniqueness of the fingerprint, because now the possible methods reduce down to that class. Adding the signature of the method and a string the method implementation refers to in combination now creates a unique fingerprint in the current example: +Each attribute of the fingerprint is responsible to describe a specific but distinct part of the method. +The combination out of those should be and ideally remain unique to all methods in all classes. +In the case of the example fingerprint, the `customFingerprint` attribute is responsible to find the class +the method is defined in. This greatly increases the uniqueness of the fingerprint, because now the possible methods +reduce down to that class. Adding the signature of the method and a string the method implementation refers to in +combination now creates a unique fingerprint in the current example: - Package & class (Line 4) @@ -74,18 +79,19 @@ Each attribute of the fingerprint is responsible to describe a specific but dist After creating a fingerprint, add it to the constructor of the `BytecodePatch`: ```kt -class DisableAdsPatch : BytecodePatch( - listOf(LoadAdsFingerprint) +object DisableAdsPatch : BytecodePatch( + setOf(LoadAdsFingerprint) ) { /* .. */ } ``` -ReVanced Patcher will try to [resolve the fingerprint](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L63) **before** it calls the `execute` method of the patch. +ReVanced Patcher will try to [resolve the fingerprint](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L182) +**before** it calls the `execute` method of the patch. -The fingerprint can now be used in the patch by accessing [`MethodFingerprint.result`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L227): +The fingerprint can now be used in the patch by accessing [`MethodFingerprint.result`](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L44): ```kt -class DisableAdsPatch : BytecodePatch( - listOf(LoadAdsFingerprint) +object DisableAdsPatch : BytecodePatch( + setOf(LoadAdsFingerprint) ) { override fun execute(context: BytecodeContext) { val result = LoadAdsFingerprint.result @@ -96,7 +102,10 @@ class DisableAdsPatch : BytecodePatch( } ``` -> **Note**: `MethodFingerprint.result` **can be null** if the fingerprint does not match any method. In such case, the fingerprint needs to be fixed and made more resilient if the error is caused by a later version of an app which the fingerprint was not tested on. A fingerprint is good, if it is _light_, but still resilient - like Carbon fiber-reinforced polymers. +> **Note**: `MethodFingerprint.result` **can be null** if the fingerprint does not match any method. +> In such case, the fingerprint needs to be fixed and made more resilient if the error is caused by a later version +> of an app which the fingerprint was not tested on. A fingerprint is good, if it is _light_, +> but still resilient - like Carbon fiber-reinforced polymers. If the fingerprint resolved to a method, the following properties are now available: @@ -118,17 +127,23 @@ data class MethodFingerprintResult( ## 🏹 Different ways to resolve a fingerprint -Usually, fingerprints are mostly resolved by the patcher, but it is also possible to manually resolve a fingerprint in a patch. This can be quite useful in lots of situations. To resolve a fingerprint you need a `BytecodeContext` to resolve it on. This context contains classes and thus methods to which the fingerprint can be resolved against. Example: _You have a fingerprint which you manually want to resolve **without** the help of the patcher._ +Usually, fingerprints are mostly resolved by ReVanced Patcher, but it is also possible to manually resolve a +fingerprint in a patch. This can be quite useful in lots of situations. To resolve a fingerprint you need +a `BytecodeContext` to resolve it on. This context contains classes and thus methods to which the fingerprint +can be resolved against. Example: _You have a fingerprint which you manually want to resolve +**without** the help of ReVanced Patcher._ -> **Note**: A fingerprint should not be added to the constructor of `BytecodePatch` if manual resolution is intended, because the patcher would try resolve it before manual resolution. +> **Note**: A fingerprint should not be added to the constructor of `BytecodePatch` if manual resolution is intended, +> because ReVanced Patcher would try resolve it before manual resolution. -- On a **list of classes** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L49) +- On a **list of classes** using [`MethodFingerprint.resolve`](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L263) - This can be useful, if a fingerprint should be resolved to a smaller subset of classes, otherwise the fingerprint can be resolved by the patcher automatically. + This can be useful, if a fingerprint should be resolved to a smaller subset of classes, + otherwise the fingerprint can be resolved by ReVanced Patcher automatically. ```kt - class DisableAdsPatch : BytecodePatch( - /* listOf(LoadAdsFingerprint) */ + object DisableAdsPatch : BytecodePatch( + /* setOf(LoadAdsFingerprint) */ ) { override fun execute(context: BytecodeContext) { val result = LoadAdsFingerprint.also { it.resolve(context, context.classes) }.result @@ -141,11 +156,11 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl - On a **single class** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L63) - Sometimes you know a class but you need certain methods. In such case, you can resolve fingerprints on a class. + Sometimes you know a class, but you need certain methods. In such case, you can resolve fingerprints on a class. ```kt - class DisableAdsPatch : BytecodePatch( - listOf(LoadAdsFingerprint) + object DisableAdsPatch : BytecodePatch( + setOf(LoadAdsFingerprint) ) { override fun execute(context: BytecodeContext) { val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } @@ -160,7 +175,12 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl - On a **method** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L78) - Resolving a fingerprint on a method is mostly only useful if the fingerprint is used to resolve certain information about a method such as `MethodFingerprintResult.scanResult`. Example: _A fingerprint should be used to resolve the method which loads ads. For that the fingerprint is added to the constructor of `BytecodePatch`. An additional fingerprint is responsible for finding the indices of the instructions with certain string references in the implementation of the method the first fingerprint resolved to._ + Resolving a fingerprint on a method is mostly only useful + if the fingerprint is used to resolve certain information about a method such as `MethodFingerprintResult.scanResult`. + Example: _A fingerprint should be used to resolve the method which loads ads. + For that the fingerprint is added to the constructor of `BytecodePatch`. + An additional fingerprint is responsible for finding the indices of the instructions with certain string references + in the implementation of the method the first fingerprint resolved to._ ```kt class DisableAdsPatch : BytecodePatch( @@ -192,7 +212,8 @@ Usually, fingerprints are mostly resolved by the patcher, but it is also possibl ## 🎯 The result of a fingerprint -After a `MethodFingerprint` resolves successfully, its result can be used. The result contains mutable and immutable references to the method and the class it is defined in. +After a `MethodFingerprint` resolves successfully, its result can be used. +The result contains mutable and immutable references to the method and the class it is defined in. > **Warning**: By default the immutable references **should be used** to prevent a mutable copy of the immutable references. For a patch to properly use a fingerprint though, usually write access is required. For that the mutable references can be used. @@ -219,23 +240,30 @@ data class MethodFingerprintScanResult( The following properties are utilized by bytecode patches: -- The `MethodFingerprint.strings` allows patches to know the indices of the instructions which hold references to the strings. +- The `MethodFingerprint.strings` allows patches to know the indices of the instructions + which hold references to the strings. -- If a fingerprint defines `MethodFingerprint.opcodes`, the start and end index of the first instructions matching that pattern will be available. These are useful to patch the implementation of methods relative to the pattern. Ideally the pattern contains the instructions opcodes pattern which is to be patched, in order to guarantee a successfull patch. +- If a fingerprint defines `MethodFingerprint.opcodes`, the start and end index of the first instructions + matching that pattern will be available. These are useful to patch the implementation of methods + relative to the pattern. Ideally the pattern contains the instructions opcodes pattern + which is to be patched, in order to guarantee a successfull patch. - > **Note**: Sometimes long patterns might be necessary, but the bigger the pattern list, the higher the chance it mutates if the app updates. For that reason the annotation `FuzzyPatternScanMethod` can be used on a fingerprint. The `FuzzyPatternScanMethod.threshold` will define, how many opcodes can remain unmatched. `PatternScanResult.warnings` can then be used, if is necessary to know where pattern missmatches occured. + > **Note**: Sometimes long patterns might be necessary, but the bigger the pattern list, the higher the chance + it mutates if the app updates. For that reason the annotation `FuzzyPatternScanMethod` can be used + on a fingerprint. The `FuzzyPatternScanMethod.threshold` will define, how many opcodes can remain unmatched. + `PatternScanResult.warnings` can then be used, if is necessary to know where pattern missmatches occured. ## ⭐ Closely related code examples ### 🧩 Patches -- [CommentsPatch](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/comments/bytecode/patch/CommentsPatch.kt) -- [MusicVideoAdsPatch](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/music/ad/video/patch/MusicVideoAdsPatch.kt) +- [`VideoAdsPatch`](/https://github.com/ReVanced/revanced-patches/blob/7c431c867d62f024855bb07f0723dbbf0af034ae/src/main/kotlin/app/revanced/patches/twitch/ad/video/VideoAdsPatch.kt) +- [`RememberVideoQualityPatch`](https://github.com/ReVanced/revanced-patches/blob/7c431c867d62f024855bb07f0723dbbf0af034ae/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt) ### 🔍 Fingerprints -- [LoadVideoAdsFingerprint](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/ad/video/fingerprints/LoadVideoAdsFingerprint.kt) -- [SeekbarTappingParentFingerprint](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/fingerprints/SeekbarTappingParentFingerprint.kt) +- [`LoadVideoAdsFingerprint`](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/ad/video/fingerprints/LoadVideoAdsFingerprint.kt) +- [`SeekbarTappingParentFingerprint`](https://github.com/revanced/revanced-patches/blob/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/fingerprints/SeekbarTappingParentFingerprint.kt) ## ⏭️ Whats next diff --git a/docs/4_structure_and_conventions.md b/docs/4_structure_and_conventions.md index 145a65d6e2..325dcf4004 100644 --- a/docs/4_structure_and_conventions.md +++ b/docs/4_structure_and_conventions.md @@ -8,41 +8,58 @@ Each patch is structured the following way: ```text 📦your.patches.app.category.patch - ├ 📂annotations - ├ └ ⚙️SomePatchCompatibility.kt ├ 📂fingerprints ├ ├ 🔍SomeFingerprintA.kt ├ └ 🔍SomeFingerprintB.kt - ├ 📂patch - └ └ 🧩SomePatch.kt + └ 🧩SomePatch.kt ``` ### 🆗 Example -As an example the structure of [`VideoAdsPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/ad/video) can be used as a reference: +As an example the structure of [`RememberVideoQualityPatch`](https://github.com/ReVanced/revanced-patches/blob/7c431c867d62f024855bb07f0723dbbf0af034ae/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt) can be used as a reference: ```text 📦app.revanced.patches.youtube.ad.video - ├ 📂annotations - ├ └ ⚙️VideoAdsCompatibility.kt ├ 📂fingerprints - ├ └ 🔍LoadVideoAdsFingerprint.kt - ├ 📂patch - └ └ 🧩VideoAdsPatch.kt + ├ ├ 🔍NewVideoQualityChangedFingerprint.kt + ├ ├ 🔍SetQualityByIndexMethodClassFieldReferenceFingerprint.kt + ├ ├ 🔍VideoQualityItemOnClickParentFingerprint.kt + ├ └ 🔍VideoQualitySetterFingerprint.kt + └ 🧩RememberVideoQualityPatch.kt ``` ## 📙 Conventions > **Note**: More ⭐ equals more importance -- ⭐⭐ **`@Patch` should be named by what they accomplish**. Example: _To patch ads on videos, the patch should be called `HideVideoAdsPatch`._ +- ⭐⭐ **Patches should be named by what they accomplish**. + Example: _To patch ads on videos, the patch should be called `HideVideoAdsPatch`._ -- ⭐⭐ **`@Description` should be written in third person and end with punctuation**. Example: _Removes ads in the video player._ +- ⭐⭐ **`@Description` should be written in third person and end with punctuation**. + Example: _Removes ads in the video player._ -- ⭐ **Resource and bytecode patches should be properly separated**. That means, bytecode patches handle patching bytecode, while resource patches handle resources. As an example, [`SponsorBlockPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock) can be used. +- ⭐ **Resource and bytecode patches should be properly separated**. + That means, bytecode patches handle patching bytecode, while resource patches handle resources. + As an example, [`SponsorBlockPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock) can be used. -- ⭐⭐⭐ **Allocate as little code as possible in patches**. This reduces the risk of failing patches. In the example of [`SponsorBlockPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock), most of the code logic is written in [revanced-integrations](https://github.com/revanced/revanced-integrations). The patches now only insert references to public methods from the integrations which are merged into the app which is far better than writing huge bytecode patches. +- ⭐⭐⭐ **Allocate as little code as possible in patches**. + This reduces the risk of failing patches. In the example of [`SponsorBlockPatch`](https://github.com/revanced/revanced-patches/tree/2d10caffad3619791a0c3a670002a47051d4731e/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock), + most of the code logic is written in [revanced-integrations](https://github.com/revanced/revanced-integrations). + The patches now only insert references to public methods from the integrations + which are merged into the app which is far better than writing huge bytecode patches. -- ⭐⭐⭐ **Create small but strong fingerprints**. This is essential for patches to last long, because fingerprints create the foundation for patches to find the places where patches need to be done. A small fingerprint guarantees that it remains in tact in case the app updates and code mutates, but can also can cause problems if it is not unique enough and for example resolve to a wrong method or give the wrong indices of instructions if a pattern is used. A fingerprint consisting out of couple distinct strings is a small but strong fingerprint, on the other hand, a fingerprint which contains a huge list of opcodes can be strong, but is likely fail to resolve in the future because the instructions could mutate with an update of the app. +- ⭐⭐⭐ **Create small but strong fingerprints**. + This is essential for patches to last long, because fingerprints + create the foundation for patches to find the places where patches need to be done. + A small fingerprint guarantees that it remains intact in case the app updates and code mutates, + but can also can cause problems if it is not unique enough and for example resolve + to a wrong method or give the wrong indices of instructions if a pattern is used. + A fingerprint consisting out of couple distinct strings is a small but strong fingerprint, + on the other hand, a fingerprint which contains a huge list of opcodes can be strong, + but is likely fail to resolve in the future because the instructions could mutate with + an update of the app. -- ⭐⭐⭐ **Document patches**. This is essential as a future reference when reading the code. Explaining what certain patches do and accomplish guarantees, that the code can be understood in the future in the case it needs to be updated. Example code comment: _Patch the return value to true in order to spoof the pro status of the user. This turns off ads._ +- ⭐⭐⭐ **Document patches**. This is essential as a future reference when reading the code. + Explaining what certain patches do and accomplish guarantees, + that the code can be understood in the future in the case it needs to be updated. + Example code comment: _Patch the return value to true in order to spoof the pro status of the user.This turns off ads._ From 861362712faf364aa7ed7f5b1b8a0d19d59d088f Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 26 Oct 2023 22:47:26 +0200 Subject: [PATCH 4/8] docs: Reference patch name property --- docs/2_skeleton.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/2_skeleton.md b/docs/2_skeleton.md index 394d1bd1db..5c83e0b708 100644 --- a/docs/2_skeleton.md +++ b/docs/2_skeleton.md @@ -187,7 +187,7 @@ package app.revanced.patches.examples.minimal.patch ) object MinimalExamplePatch : BytecodePatch() { override fun execute(context: BytecodeContext) = - println("${MinimalExamplePatch::class.patchName} is being executed." ) + println("${MinimalExamplePatch.name} is being executed." ) } ``` From 7245d5a33e94243d3af4f21e7f75178830be3a36 Mon Sep 17 00:00:00 2001 From: Hans5958 Date: Thu, 7 Dec 2023 22:55:43 +0700 Subject: [PATCH 5/8] docs: Fix broken link to `VideoAdsPatch` (#3385) --- docs/3_fingerprinting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/3_fingerprinting.md b/docs/3_fingerprinting.md index e163693082..05c53a446e 100644 --- a/docs/3_fingerprinting.md +++ b/docs/3_fingerprinting.md @@ -257,7 +257,7 @@ The following properties are utilized by bytecode patches: ### 🧩 Patches -- [`VideoAdsPatch`](/https://github.com/ReVanced/revanced-patches/blob/7c431c867d62f024855bb07f0723dbbf0af034ae/src/main/kotlin/app/revanced/patches/twitch/ad/video/VideoAdsPatch.kt) +- [`VideoAdsPatch`](https://github.com/ReVanced/revanced-patches/blob/7c431c867d62f024855bb07f0723dbbf0af034ae/src/main/kotlin/app/revanced/patches/twitch/ad/video/VideoAdsPatch.kt) - [`RememberVideoQualityPatch`](https://github.com/ReVanced/revanced-patches/blob/7c431c867d62f024855bb07f0723dbbf0af034ae/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt) ### 🔍 Fingerprints From 90f33da2e8570eb897d586b29e5d174593a93a2f Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 9 Dec 2023 21:55:35 +0100 Subject: [PATCH 6/8] docs: Improve wording and update note --- docs/2_skeleton.md | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/2_skeleton.md b/docs/2_skeleton.md index 5c83e0b708..54418d4f4a 100644 --- a/docs/2_skeleton.md +++ b/docs/2_skeleton.md @@ -1,6 +1,6 @@ # 🧩 Skeleton of a Patch -Patches are what make ReVanced, ReVanced. On the following page the basic structure of a patch will be explained. +Patches are what make ReVanced, ReVanced. On the following page, the basic structure of a patch will be explained. ## ⛳️ Example patch @@ -37,13 +37,12 @@ object DisableAdsPatch : BytecodePatch( ## 🔎 Dissecting the example patch -Let's start with understanding, how a patch is structured. A patch is mainly built out of three components: +Let's start with understanding how a patch is structured. A patch is mainly built out of three components: 1. 📝 Patch annotations > Note: - > Patch annotations can be imported from and processed by the Maven package `app.revanced.revanced-patch-annotation-processor` - > By default, the parameters of the super constructor of patches can be used. + > A patch can either be instantiated using the `@Patch` annotation or the parameters of the super constructor. ```kt @Patch( @@ -54,26 +53,26 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui ) ``` - To give context about the patch, annotations are used. They serve different but important purposes: + Annotations are used to give context to the patch. They serve different but important purposes: - Every visible that sets `@Patch.name` will be loadable by `PatchBundleLoader` from the [introduction](1_introduction.md). Patches that do not set `@Patch.name` can be referenced by other patches. - We refer to those as _patch dependencies_. Patch dependencies are useful to structure multiple patches. + We refer to those as _patch dependencies_. Patch dependencies are useful for structuring multiple patches. Example: _To add settings switches to an app, first, a patch is required that can provide a basic framework for other patches to add their toggles to that app. Those patches refer to the dependency patch and use its framework to add their toggles to an app. [ReVanced Patcher](https://github.com/revanced/revanced-patcher) will execute the dependency - and then the patch itself. The dependency can prepare a preference screen when executed and then initialize itself + and the patch itself. When executed, the dependency can prepare a preference screen and then initialize itself for further use by other patches._ - Patches may set `@Path.description`. - This annotation is used to briefly describe the patch. + This annotation is used to describe the patch briefly. - Patches may set `@Patch.dependencies`. If the current patch depends on other patches, it can declare them as dependencies. Example: _The patch to remove ads needs to patch the bytecode. - Additionally, it makes use of a second patch, to get rid of resource files in the app which show ads in the app._ + Additionally, it makes use of a second patch to get rid of resource files in the app that show ads in the app._ - Patches may set `@Patch.compatiblePackages`. This annotation serves the purpose of constraining a patch to a package. @@ -81,14 +80,14 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui Additionally, the constraint may specify versions of the package it is guaranteed to be compatible with. Example: _The patch disables ads for an app. - The app regularly updates and the code of the app mutates heavily. In that case the patch might not be compatible - for future, untested versions of the app. To discourage the use of the app with other versions than the versions, - this patch was confirmed to work on, it is constrained to those versions only._ + The app regularly updates, and the code of the app mutates heavily. In that case, the patch might not be compatible +with future untested app versions. To discourage the use of the app with versions other than the ones +this patch was confirmed to work on, it is constrained to those versions only._ - A patch may set `@Patch.requiresIntegrations` to true, if it depends on additional integrations to be merged by [ReVanced Patcher](https://github.com/revanced/revanced-patcher). - > Integrations are precompiled classes which are useful to off-load and useful for developing complex patches. + > Integrations are precompiled classes that are useful for offloading and developing complex patches. Details of integrations and what exactly integrations are will be introduced properly on another page. 2. 🏗️ Patch class @@ -99,14 +98,14 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui } ``` - Usually, patches consist out of a single object class. - The class can be used to create methods and fields for the patch, or provide a framework for other patches, + Usually, patches consist of a single object class. + The class can be used to create methods and fields for the patch or provide a framework for other patches, in case it is meant to be used as a dependency patch. [ReVanced Patches](https://github.com/revanced/revanced-patches) follow a convention to name the class of patches: - Example: _The class for a patch which disables ads should be called `DisableAdsPatch`, - for a patch which adds a new download feature it should be called `DownloadsPatch`._ + Example: _The class for a patch that disables ads should be called `DisableAdsPatch`, + for a patch that adds a new download feature, it should be called `DownloadsPatch`._ Each patch implicitly extends the [Patch](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt#L27) class 3. when extending off [ResourcePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L35) or [BytecodePatch](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/patch/Patch.kt#L42). The current example extends off `BytecodePatch`: @@ -117,9 +116,9 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui } ``` - If the patch extends off `ResourcePatch`, it is able to **patch resources** such as `XML`, `PNG` or similar files. - On the other hand, if the patch extends off `BytecodePatch`, it is able to **patch the bytecode** of an app. - If a patch needs access to the resources and the bytecode at the same time. + If the patch extends off `ResourcePatch`, it can **patch resources** such as `XML`, `PNG` or similar files. + On the other hand, if the patch extends off `BytecodePatch`, it can **patch the bytecode** of an app. + If a patch needs access to the resources and the bytecode simultaneously. Either can use the other as a dependency. **Patches involving circular dependencies can not be added to a `Patcher` instance.** @@ -131,7 +130,7 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui } ``` - The `execute` method is declared in the `Patch` class and therefore part of any patch: + The `execute` method is declared in the `Patch` class and, therefore, part of any patch: ```kt fun execute(context: /* Omitted */ T) @@ -152,10 +151,10 @@ Let's start with understanding, how a patch is structured. A patch is mainly bui Likewise, a `ResourcePatch` will require a [ResourceContext](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt) parameter and provide the patch with everything necessary to patch resources. - Patches may throw `PatchException` if something went wrong. + Patches may throw `PatchException` if something goes wrong. If this patch is used as a dependency for other patches, those patches will not execute subsequently. - In the current example the `execute` method runs the following code to replace instructions at the index `0` + In the current example, the `execute` method runs the following code to replace instructions at the index `0` of the methods instruction list: ```kt From b6bbf7fdce76e87561b0f3758e9d58021e81fbee Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sun, 17 Dec 2023 17:01:33 +0100 Subject: [PATCH 7/8] docs: Fix reconstructed code syntax --- docs/3_fingerprinting.md | 67 ++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/docs/3_fingerprinting.md b/docs/3_fingerprinting.md index 05c53a446e..b26996cee9 100644 --- a/docs/3_fingerprinting.md +++ b/docs/3_fingerprinting.md @@ -1,6 +1,6 @@ # 🔎 Fingerprinting -Fingerprinting is the process of creating uniquely identifyable data about something arbitrarily large. In the context of ReVanced, fingerprinting is essential to be able to find classes, methods and fields without knowing their original names or certain other attributes, which would be used to identify them under normal circumstances. +Fingerprinting is the process of creating uniquely identifiable data about something arbitrarily large. In the context of ReVanced, fingerprinting is essential to be able to find classes, methods and fields without knowing their original names or certain other attributes, which would be used to identify them under normal circumstances. ## ⛳️ Example fingerprint @@ -24,33 +24,33 @@ object LoadAdsFingerprint : MethodFingerprint( ## 🆗 Understanding the example fingerprint -The example fingerprint called `LoadAdsFingerprint` which extends on [`MethodFingerprint`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L28) is made to uniquely identify a certain method by capturing various attributes of the method such as the return type, access flags, an opcode pattern and more. The following code can be inferred just from the fingerprint: +The example fingerprint called `LoadAdsFingerprint`, which extends on [`MethodFingerprint`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L28) is made to uniquely identify a certain method by capturing various attributes of the method such as the return type, access flags, an opcode pattern and more. The following Java code can be reconstructed from the fingerprint: -```kt - package com.some.app.ads +```java + import com.some.app.ads; // Imports 4 class Loader { - 5 public final Boolean (: Boolean) { + 5 public final boolean (boolean ) { // ... - 8 val userStatus = "pro"; + 8 var userStatus = "pro"; // ... -12 return +12 return ; } } ``` ## 🚀 How it works -Each attribute of the fingerprint is responsible to describe a specific but distinct part of the method. +Each fingerprint attribute describes a specific but distinct part of the method. The combination out of those should be and ideally remain unique to all methods in all classes. -In the case of the example fingerprint, the `customFingerprint` attribute is responsible to find the class -the method is defined in. This greatly increases the uniqueness of the fingerprint, because now the possible methods -reduce down to that class. Adding the signature of the method and a string the method implementation refers to in +In the case of the example fingerprint, the `customFingerprint` attribute is responsible for finding the class +the method is defined in. This greatly increases the uniqueness of the fingerprint because the possible methods +are now reduced to that class. Adding the signature of the method and a string the method implementation refers to in combination now creates a unique fingerprint in the current example: - Package & class (Line 4) @@ -103,9 +103,9 @@ object DisableAdsPatch : BytecodePatch( ``` > **Note**: `MethodFingerprint.result` **can be null** if the fingerprint does not match any method. -> In such case, the fingerprint needs to be fixed and made more resilient if the error is caused by a later version -> of an app which the fingerprint was not tested on. A fingerprint is good, if it is _light_, -> but still resilient - like Carbon fiber-reinforced polymers. +> In such cases, the fingerprint needs to be fixed and made more resilient if a later version causes the error +> of an app in which the fingerprint was not tested. A fingerprint is good if it is _light_, +> but still resilient - like Carbon fibre-reinforced polymers. If the fingerprint resolved to a method, the following properties are now available: @@ -127,19 +127,19 @@ data class MethodFingerprintResult( ## 🏹 Different ways to resolve a fingerprint -Usually, fingerprints are mostly resolved by ReVanced Patcher, but it is also possible to manually resolve a -fingerprint in a patch. This can be quite useful in lots of situations. To resolve a fingerprint you need -a `BytecodeContext` to resolve it on. This context contains classes and thus methods to which the fingerprint -can be resolved against. Example: _You have a fingerprint which you manually want to resolve +Usually, fingerprints are resolved by ReVanced Patcher, but it is also possible to manually resolve a +fingerprint in a patch. This can be quite useful in lots of situations. To resolve a fingerprint, you need +a `BytecodeContext` to resolve it. This context contains classes and, thus, methods against which the fingerprint +can be resolved. Example: _You have a fingerprint which you manually want to resolve **without** the help of ReVanced Patcher._ > **Note**: A fingerprint should not be added to the constructor of `BytecodePatch` if manual resolution is intended, -> because ReVanced Patcher would try resolve it before manual resolution. +> because ReVanced Patcher would try to resolve it before manual resolution. - On a **list of classes** using [`MethodFingerprint.resolve`](https://github.com/ReVanced/revanced-patcher/blob/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L263) - This can be useful, if a fingerprint should be resolved to a smaller subset of classes, - otherwise the fingerprint can be resolved by ReVanced Patcher automatically. + This can be useful if a fingerprint should be resolved to a smaller subset of classes, + Otherwise, the fingerprint can be resolved automatically by ReVanced Patcher. ```kt object DisableAdsPatch : BytecodePatch( @@ -156,7 +156,7 @@ can be resolved against. Example: _You have a fingerprint which you manually wan - On a **single class** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L63) - Sometimes you know a class, but you need certain methods. In such case, you can resolve fingerprints on a class. + Sometimes, you know a class, but you need certain methods. In such cases, you can resolve fingerprints on a class. ```kt object DisableAdsPatch : BytecodePatch( @@ -176,11 +176,11 @@ can be resolved against. Example: _You have a fingerprint which you manually wan - On a **method** using [`MethodFingerprint.resolve`](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L78) Resolving a fingerprint on a method is mostly only useful - if the fingerprint is used to resolve certain information about a method such as `MethodFingerprintResult.scanResult`. + if the fingerprint is used to resolve certain information about a method, such as `MethodFingerprintResult.scanResult`. Example: _A fingerprint should be used to resolve the method which loads ads. - For that the fingerprint is added to the constructor of `BytecodePatch`. + For that, the fingerprint is added to the constructor of `BytecodePatch`. An additional fingerprint is responsible for finding the indices of the instructions with certain string references - in the implementation of the method the first fingerprint resolved to._ + in implementing the method the first fingerprint resolved to._ ```kt class DisableAdsPatch : BytecodePatch( @@ -213,9 +213,9 @@ can be resolved against. Example: _You have a fingerprint which you manually wan ## 🎯 The result of a fingerprint After a `MethodFingerprint` resolves successfully, its result can be used. -The result contains mutable and immutable references to the method and the class it is defined in. +The result contains mutable and immutable references to the method and its defined class. -> **Warning**: By default the immutable references **should be used** to prevent a mutable copy of the immutable references. For a patch to properly use a fingerprint though, usually write access is required. For that the mutable references can be used. +> **Warning**: By default, the immutable references **should be used** to prevent a mutable copy of the immutable references. For a patch to properly use a fingerprint, though, usually write access is required. For that, mutable references can be used. Among them, the result also contains [MethodFingerprintResult.scanResult](https://github.com/revanced/revanced-patcher/blob/d2f91a8545567429d64a1bcad6ca1dab62ec95bf/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt#L239) which contains additional useful properties: @@ -238,20 +238,19 @@ data class MethodFingerprintScanResult( } ``` -The following properties are utilized by bytecode patches: +Bytecode patches utilize the following properties: - The `MethodFingerprint.strings` allows patches to know the indices of the instructions which hold references to the strings. - If a fingerprint defines `MethodFingerprint.opcodes`, the start and end index of the first instructions matching that pattern will be available. These are useful to patch the implementation of methods - relative to the pattern. Ideally the pattern contains the instructions opcodes pattern - which is to be patched, in order to guarantee a successfull patch. + relative to the pattern. Ideally, the pattern contains instructions for patching the opcodes pattern to guarantee a successful patch. > **Note**: Sometimes long patterns might be necessary, but the bigger the pattern list, the higher the chance - it mutates if the app updates. For that reason the annotation `FuzzyPatternScanMethod` can be used - on a fingerprint. The `FuzzyPatternScanMethod.threshold` will define, how many opcodes can remain unmatched. - `PatternScanResult.warnings` can then be used, if is necessary to know where pattern missmatches occured. + it mutates if the app updates. Therefore, the annotation `FuzzyPatternScanMethod` can be used + on a fingerprint. The `FuzzyPatternScanMethod.threshold` will define how many opcodes can remain unmatched. + If necessary, `PatternScanResult.warnings` can then be used to know where pattern mismatches occurred. ## ⭐ Closely related code examples @@ -267,6 +266,6 @@ The following properties are utilized by bytecode patches: ## ⏭️ Whats next -The next section will give a suggestion on coding conventions and on the file structure of a patch. +The next section will give a suggestion on coding conventions and the file structure of a patch. Continue: [📜 Patch file structure and conventions](4_structure_and_conventions.md) From eddd8b1ad6ce5fca5721ed67efb9151b76c1f521 Mon Sep 17 00:00:00 2001 From: KobeW50 <84587632+KobeW50@users.noreply.github.com> Date: Tue, 19 Dec 2023 19:11:32 -0500 Subject: [PATCH 8/8] docs: Fix typo (#2522) --- docs/2_skeleton.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/2_skeleton.md b/docs/2_skeleton.md index 54418d4f4a..f2b613b75e 100644 --- a/docs/2_skeleton.md +++ b/docs/2_skeleton.md @@ -65,7 +65,7 @@ Let's start with understanding how a patch is structured. A patch is mainly buil and the patch itself. When executed, the dependency can prepare a preference screen and then initialize itself for further use by other patches._ - - Patches may set `@Path.description`. + - Patches may set `@Patch.description`. This annotation is used to describe the patch briefly. - Patches may set `@Patch.dependencies`.