mirror of
https://github.com/ReVanced/revanced-website.git
synced 2026-01-19 01:03:56 +00:00
feat: init docs
This commit is contained in:
7
docusaurus/docs/development/_category_.json
Normal file
7
docusaurus/docs/development/_category_.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "Development",
|
||||
"position": 2,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# 💼 Prerequisites
|
||||
|
||||
To develop with ReVanced, you will need to fulfill certain requirements.
|
||||
|
||||
## 🤝 Requirements
|
||||
|
||||
- A Java IDE such as [IntelliJ IDEA](https://www.jetbrains.com/idea/)
|
||||
- An Android IDE such as [Android Studio](https://developer.android.com/studio)
|
||||
- Understanding on how to use the ReVanced CLI
|
||||
- Java SDK 17 (Azul JDK or OpenJDK)
|
||||
@@ -0,0 +1,108 @@
|
||||
# 👨💻 Setup a development environment for ReVanced
|
||||
|
||||
A certain development environment is suggested to allow for streamlined development on ReVanced.
|
||||
|
||||
## 1. ⬇️ Clone necessary repositories
|
||||
|
||||
```bash
|
||||
repositories=(
|
||||
"revanced-cli"
|
||||
"revanced-patches"
|
||||
"revanced-patcher" # optional
|
||||
"revanced-integrations"
|
||||
)
|
||||
|
||||
for repository in "${repositories[@]}" ; do
|
||||
git clone -b dev --single-branch --depth 1 https://github.com/revanced/$repository
|
||||
done
|
||||
```
|
||||
|
||||
## 2. 🛠️ Build from source
|
||||
|
||||
### Before building you need to be authenticated to GitHub Packages. This will assume you have a GitHub account
|
||||
|
||||
Create a PAT with the scope `read:packages` [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced) and add your token to `~/.gradle/gradle.properties`.
|
||||
|
||||
Example `gradle.properties` file:
|
||||
|
||||
```properties
|
||||
gpr.user = ReVanced
|
||||
gpr.key = ghp_key
|
||||
```
|
||||
|
||||
### To build all projects, run the following command from the directory which contains the repositories
|
||||
|
||||
```bash
|
||||
repositories=(
|
||||
"revanced-cli"
|
||||
"revanced-patches"
|
||||
"revanced-patcher" # optional
|
||||
"revanced-integrations"
|
||||
)
|
||||
|
||||
for repository in "${repositories[@]}" ; do
|
||||
cd $repository
|
||||
./gradlew build
|
||||
cd ..
|
||||
done
|
||||
```
|
||||
|
||||
## 3. ⚙️ Setup IntelliJ IDEA
|
||||
|
||||
1. Open the `revanced-cli` project in IntelliJ IDEA and ensure you are using the right JDK from [💼 Prerequisites](0_prerequisites.md)
|
||||
2. Import the `revanced-patches` and optionally the `revanced-patcher` project as modules into the `revanced-cli` project
|
||||
3. Add a new Run/Debug configuration for the `revanced-cli` project; Make sure to add `Before launch` tasks to build `revanced-patches` and optionally publish `revanced-patcher`
|
||||
|
||||
Example configuration:
|
||||
|
||||
```xml
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run ReVanced CLI" type="JetRunConfigurationType">
|
||||
<option name="MAIN_CLASS_NAME" value="app.revanced.cli.main.MainKt" />
|
||||
<module name="revanced-cli.main" />
|
||||
<option name="PROGRAM_PARAMETERS" value="
|
||||
--options ../options.toml
|
||||
-o ../revanced.apk
|
||||
-a ../binaries/unpatched-input.apk
|
||||
-t ../revanced-cache
|
||||
-b ../revanced-patches/build/libs/revanced-patches-<version>.jar
|
||||
-m ../revanced-integrations/app/build/outputs/apk/release/revanced-integrations-<version>.apk
|
||||
-d device-name"
|
||||
/>
|
||||
<method v="2">
|
||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="revanced-patcher [publish]" run_configuration_type="GradleRunConfiguration" />
|
||||
<option name="RunConfigurationTask" enabled="true" run_configuration_name="revanced-patches [build]" run_configuration_type="GradleRunConfiguration" />
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
```
|
||||
|
||||
> **Note**: The build file names of `revanced-patches` and `revanced-integrations` change. **Do not forget to update them in the run configuration program arguments.**
|
||||
|
||||
## 4. ⚙️ Setup Android Studio
|
||||
|
||||
1. Open the `revanced-integrations` project in Android Studio and ensure you are using the latest Android SDK.
|
||||
2. Add a new default build configuration and confirm if it succeeds.
|
||||
|
||||
## 5. ⚠️ Troubleshoot your development environment
|
||||
|
||||
To confirm your development environment works as intended, set a breakpoint in any patch from the `revanced-patches` project in IntelliJ IDEA. Run the build configuration for `revanced-cli` and confirm that your IDE reaches and breaks at the breakpoint. Continue and let ReVanced CLI exit.
|
||||
|
||||
- If ReVanced CLI output is unexpected, confirm if you supplied the correct program arguments by following [💻 ReVanced CLI](/docs/category/revanced-cli).
|
||||
|
||||
- If the breakpoint was not hit, confirm that you correctly added the necessary projects as modules to the `revanced-cli` project
|
||||
|
||||
## ❗ Afterword
|
||||
|
||||
A couple of things should be considered with the development environment for ReVanced:
|
||||
|
||||
- Follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
|
||||
- Pull new commits from remote to keep your branch up to date
|
||||
|
||||
- Keep your Run/Debug configuration up to date. Ensure you use the correct paths in your program argument after pulling new commits. If you forget to do this, you might end up debugging for hours until realising, you supply the wrong paths to ReVanced CLI
|
||||
|
||||
- Use development branches and always branch off and PR to `dev` branches
|
||||
|
||||
- To use the local `revanced-patcher` project in the `revanced-cli` and `revanced-patches` projects, make sure you publish it to the local Maven repository with `./gradlew publish` and use the correct version in the `build.gradle.kts` file in the `revanced-cli` and `revanced-patches` projects, otherwise, it will use the package from GitHub Packages
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "ReVanced Development",
|
||||
"position": 1,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# 👶 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
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
# 💉 Introduction to 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()
|
||||
```
|
||||
167
docusaurus/docs/development/revanced-patches/2_skeleton.md
Normal file
167
docusaurus/docs/development/revanced-patches/2_skeleton.md
Normal file
@@ -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("0.1.0"))])
|
||||
@Version("0.0.1")
|
||||
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("0.1.0"))])
|
||||
@Version("0.0.1")
|
||||
```
|
||||
|
||||
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._
|
||||
|
||||
- Patches can be annotated with `@Version`.
|
||||
|
||||
> Currently, this annotation does not serve any purpose, but is added to patches by convention, in case a use case has been found.
|
||||
|
||||
- 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()
|
||||
}
|
||||
}
|
||||
```
|
||||
240
docusaurus/docs/development/revanced-patches/3_fingerprinting.md
Normal file
240
docusaurus/docs/development/revanced-patches/3_fingerprinting.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 🔎 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 <attributes> class Loader {
|
||||
5 public final Boolean <methodName>(<field>: Boolean) {
|
||||
// ...
|
||||
|
||||
8 val userStatus = "pro";
|
||||
|
||||
// ...
|
||||
|
||||
12 return <returnValue>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 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<Warning>? = null
|
||||
)
|
||||
|
||||
data class StringsScanResult(val matches: List<StringMatch>){
|
||||
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)
|
||||
@@ -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._
|
||||
65
docusaurus/docs/development/revanced-patches/5_apis.md
Normal file
65
docusaurus/docs/development/revanced-patches/5_apis.md
Normal file
@@ -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).
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"label": "ReVanced Patches",
|
||||
"position": 4,
|
||||
"link": {
|
||||
"type": "generated-index"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user