You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-06 16:38:50 +02:00
Compare commits
154 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
77bbf6be1f | ||
![]() |
bd053b7e99 | ||
![]() |
fd742eba63 | ||
![]() |
ba9d998681 | ||
![]() |
75df245ec3 | ||
![]() |
4164cb0dea | ||
![]() |
18fe35ae73 | ||
![]() |
f9bc95f220 | ||
![]() |
d2f91a8545 | ||
![]() |
4016bdc37f | ||
![]() |
538b2a8599 | ||
![]() |
4aa14bbb85 | ||
![]() |
d37452997b | ||
![]() |
db21d5e953 | ||
![]() |
4d581811db | ||
![]() |
8c502448be | ||
![]() |
fec16d9442 | ||
![]() |
5583904994 | ||
![]() |
797286b758 | ||
![]() |
4ae9ad09d6 | ||
![]() |
447e1ad30e | ||
![]() |
843e62ad29 | ||
![]() |
9c07ffcc7a | ||
![]() |
438321330e | ||
![]() |
3ba4be240b | ||
![]() |
98ce0abfa9 | ||
![]() |
db4348c4fa | ||
![]() |
4839f87519 | ||
![]() |
809862c997 | ||
![]() |
fd5c878cee | ||
![]() |
124332f0e9 | ||
![]() |
d4cf0cea52 | ||
![]() |
76676fb567 | ||
![]() |
d802ef844e | ||
![]() |
90fc547673 | ||
![]() |
3813e28ac2 | ||
![]() |
a2bb4004c7 | ||
![]() |
a0cb449c60 | ||
![]() |
e0271790b8 | ||
![]() |
4bfd7ebff8 | ||
![]() |
2f7e62ef65 | ||
![]() |
4485af8036 | ||
![]() |
085a3a479d | ||
![]() |
f75c9a78b8 | ||
![]() |
172655bde0 | ||
![]() |
456db7289a | ||
![]() |
e722e3f4f9 | ||
![]() |
c348c1f0a0 | ||
![]() |
ed1851013e | ||
![]() |
e31ac1f132 | ||
![]() |
8f78f85e4a | ||
![]() |
0be2677519 | ||
![]() |
b873228ef0 | ||
![]() |
639ff1c0ba | ||
![]() |
f30671ddd1 | ||
![]() |
76c45dd7c1 | ||
![]() |
1bafb77355 | ||
![]() |
25f74dc5e9 | ||
![]() |
6e73631d4d | ||
![]() |
7761d5b85e | ||
![]() |
62aa295e73 | ||
![]() |
596ede1b12 | ||
![]() |
7debe62738 | ||
![]() |
002f84da1a | ||
![]() |
aff4968e6f | ||
![]() |
1d989abd55 | ||
![]() |
f1775f83d0 | ||
![]() |
4055939c08 | ||
![]() |
85120374d6 | ||
![]() |
8b4819faa1 | ||
![]() |
d219276298 | ||
![]() |
79f91e0e5a | ||
![]() |
fadf62f594 | ||
![]() |
ad3d332e27 | ||
![]() |
8f66df7666 | ||
![]() |
80c2e80925 | ||
![]() |
c3db23d3c7 | ||
![]() |
c28584736e | ||
![]() |
6b909c1ee6 | ||
![]() |
0e8446516e | ||
![]() |
aa46b953db | ||
![]() |
a562e476c0 | ||
![]() |
75d2be8803 | ||
![]() |
d6308e126c | ||
![]() |
bb97af4d86 | ||
![]() |
392164862c | ||
![]() |
53e807dec1 | ||
![]() |
288d50a8b4 | ||
![]() |
131dedd4b0 | ||
![]() |
5a92d5c29d | ||
![]() |
4b81318710 | ||
![]() |
44f6a3ebc5 | ||
![]() |
7882a8d928 | ||
![]() |
cc3d32748b | ||
![]() |
f9da2ad531 | ||
![]() |
b19e1131e8 | ||
![]() |
123ad54c15 | ||
![]() |
09f6ab4155 | ||
![]() |
01cf3fb50f | ||
![]() |
6c5f9d4198 | ||
![]() |
7b2d058144 | ||
![]() |
db2804270e | ||
![]() |
2572cd04b5 | ||
![]() |
5eb8b428b9 | ||
![]() |
3a118d9b9d | ||
![]() |
14a73bfcaf | ||
![]() |
567bf52e16 | ||
![]() |
35c6489dba | ||
![]() |
371f0c4d0b | ||
![]() |
1b42f65d95 | ||
![]() |
2aee0cbd0f | ||
![]() |
19256b5437 | ||
![]() |
67a5237541 | ||
![]() |
4e2e772389 | ||
![]() |
799bc9e163 | ||
![]() |
2431785d0e | ||
![]() |
fb3c0e87d4 | ||
![]() |
5f7ef2dbff | ||
![]() |
ec1d8a8fba | ||
![]() |
814ce0b9ae | ||
![]() |
8d95b14f35 | ||
![]() |
711b8a25a7 | ||
![]() |
fbb09f38dc | ||
![]() |
9a4d30e152 | ||
![]() |
368c61c1bf | ||
![]() |
bbd40bf2f6 | ||
![]() |
3de999a2d3 | ||
![]() |
83cbb2f110 | ||
![]() |
fcc7fa75d0 | ||
![]() |
495ebface8 | ||
![]() |
8c12f8d488 | ||
![]() |
523f67b238 | ||
![]() |
4813a8b48e | ||
![]() |
d5c66022c9 | ||
![]() |
1d4034b36c | ||
![]() |
dd2d696d00 | ||
![]() |
6326321b65 | ||
![]() |
f291a4ae3e | ||
![]() |
817b8db019 | ||
![]() |
a321b8971b | ||
![]() |
783b2de9db | ||
![]() |
77604d4078 | ||
![]() |
21b5404180 | ||
![]() |
9ac6d5c7da | ||
![]() |
1b39278b24 | ||
![]() |
0ebab8bf59 | ||
![]() |
112bc998f4 | ||
![]() |
12c96bf818 | ||
![]() |
91298a8790 | ||
![]() |
f2a7cff41c | ||
![]() |
dd941233ca | ||
![]() |
fc06dd1c29 | ||
![]() |
482af78f2b | ||
![]() |
6bfe5716c3 |
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
name: 🐞 Bug report
|
||||||
|
description: Report a very clearly broken issue.
|
||||||
|
title: 'bug: <title>'
|
||||||
|
labels: [bug]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# ReVanced bug report
|
||||||
|
|
||||||
|
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-patcher/labels/bug).
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Type
|
||||||
|
options:
|
||||||
|
- Crash
|
||||||
|
- Cosmetic
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Bug description
|
||||||
|
description: How did you find the bug? Any additional details that might help?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
description: Add the steps to reproduce this bug including your environment.
|
||||||
|
placeholder: Step 1. Download some files. Step 2. ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Relevant log output
|
||||||
|
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots or videos
|
||||||
|
description: Add screenshots or videos that show the bug here.
|
||||||
|
placeholder: Drag and drop the screenshots/videos into this box.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Solution
|
||||||
|
description: If applicable, add a possible solution.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add additional context here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: I filled out all of the requested information in this issue properly.
|
||||||
|
required: true
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: 📃 Documentation
|
||||||
|
url: https://github.com/revanced/revanced-documentation/
|
||||||
|
about: Don't know how or where to start? Check out our documentation!
|
||||||
|
- name: 🗨 Discussions
|
||||||
|
url: https://github.com/revanced/revanced-suggestions/discussions
|
||||||
|
about: Got something you think should change or be added? Search for or start a new discussion!
|
58
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
58
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
name: ⭐ Feature request
|
||||||
|
description: Create a detailed feature request.
|
||||||
|
title: 'feat: <title>'
|
||||||
|
labels: [feature-request]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
# ReVanced feature request
|
||||||
|
|
||||||
|
Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead.
|
||||||
|
Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-patcher/labels/feature-request).
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Type
|
||||||
|
options:
|
||||||
|
- Functionality
|
||||||
|
- Cosmetic
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Issue
|
||||||
|
description: What is the current problem. Why does it require a feature request?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Feature
|
||||||
|
description: Describe your feature in detail. How does it solve the issue?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Motivation
|
||||||
|
description: Why should your feature should be considered?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: Add additional context here.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: checkboxes
|
||||||
|
id: acknowledgements
|
||||||
|
attributes:
|
||||||
|
label: Acknowledgements
|
||||||
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
|
options:
|
||||||
|
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
||||||
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: I filled out all of the requested information in this issue properly.
|
||||||
|
required: true
|
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -15,17 +15,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup JDK
|
- name: Setup JDK
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'temurin'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
|
1
.idea/.gitignore
generated
vendored
1
.idea/.gitignore
generated
vendored
@@ -6,3 +6,4 @@
|
|||||||
# Datasource local storage ignored files
|
# Datasource local storage ignored files
|
||||||
/dataSources/
|
/dataSources/
|
||||||
/dataSources.local.xml
|
/dataSources.local.xml
|
||||||
|
/kotlinc.xml
|
||||||
|
399
CHANGELOG.md
399
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1 +1,2 @@
|
|||||||
# Patcher
|
# Patcher
|
||||||
|
Patcher framework used in the ReVanced project.
|
@@ -24,8 +24,9 @@ dependencies {
|
|||||||
implementation("xpp3:xpp3:1.1.4c")
|
implementation("xpp3:xpp3:1.1.4c")
|
||||||
implementation("org.smali:smali:2.5.2")
|
implementation("org.smali:smali:2.5.2")
|
||||||
implementation("app.revanced:multidexlib2:2.5.2.r2")
|
implementation("app.revanced:multidexlib2:2.5.2.r2")
|
||||||
implementation("org.apktool:apktool-lib:2.6.9-SNAPSHOT")
|
implementation("org.apktool:apktool-lib:2.8.1-SNAPSHOT")
|
||||||
|
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ tasks {
|
|||||||
events("PASSED", "SKIPPED", "FAILED")
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 2.3.0
|
version = 6.1.1
|
||||||
|
File diff suppressed because it is too large
Load Diff
19
src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Normal file
19
src/main/kotlin/app/revanced/patcher/PatcherContext.kt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.patcher
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.data.Context
|
||||||
|
import app.revanced.patcher.data.PackageMetadata
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
data class PatcherContext(
|
||||||
|
val classes: MutableList<ClassDef>,
|
||||||
|
val resourceCacheDirectory: File,
|
||||||
|
) {
|
||||||
|
val packageMetadata = PackageMetadata()
|
||||||
|
internal val patches = mutableListOf<Class<out Patch<Context>>>()
|
||||||
|
internal val bytecodeContext = BytecodeContext(classes)
|
||||||
|
internal val resourceContext = ResourceContext(resourceCacheDirectory)
|
||||||
|
}
|
@@ -1,19 +0,0 @@
|
|||||||
package app.revanced.patcher
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.Data
|
|
||||||
import app.revanced.patcher.data.PackageMetadata
|
|
||||||
import app.revanced.patcher.data.impl.BytecodeData
|
|
||||||
import app.revanced.patcher.data.impl.ResourceData
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
data class PatcherData(
|
|
||||||
internal val internalClasses: MutableList<ClassDef>,
|
|
||||||
internal val resourceCacheDirectory: String,
|
|
||||||
val packageMetadata: PackageMetadata
|
|
||||||
) {
|
|
||||||
internal val patches = mutableListOf<Class<out Patch<Data>>>()
|
|
||||||
internal val bytecodeData = BytecodeData(internalClasses)
|
|
||||||
internal val resourceData = ResourceData(File(resourceCacheDirectory))
|
|
||||||
}
|
|
@@ -8,7 +8,6 @@ import java.io.File
|
|||||||
* Options for the [Patcher].
|
* Options for the [Patcher].
|
||||||
* @param inputFile The input file (usually an apk file).
|
* @param inputFile The input file (usually an apk file).
|
||||||
* @param resourceCacheDirectory Directory to cache resources.
|
* @param resourceCacheDirectory Directory to cache resources.
|
||||||
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
|
|
||||||
* @param aaptPath Optional path to a custom aapt binary.
|
* @param aaptPath Optional path to a custom aapt binary.
|
||||||
* @param frameworkFolderLocation Optional path to a custom framework folder.
|
* @param frameworkFolderLocation Optional path to a custom framework folder.
|
||||||
* @param logger Custom logger implementation for the [Patcher].
|
* @param logger Custom logger implementation for the [Patcher].
|
||||||
@@ -16,7 +15,6 @@ import java.io.File
|
|||||||
data class PatcherOptions(
|
data class PatcherOptions(
|
||||||
internal val inputFile: File,
|
internal val inputFile: File,
|
||||||
internal val resourceCacheDirectory: String,
|
internal val resourceCacheDirectory: String,
|
||||||
internal val patchResources: Boolean = false,
|
|
||||||
internal val aaptPath: String = "",
|
internal val aaptPath: String = "",
|
||||||
internal val frameworkFolderLocation: String? = null,
|
internal val frameworkFolderLocation: String? = null,
|
||||||
internal val logger: Logger = NopLogger
|
internal val logger: Logger = NopLogger
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
package app.revanced.patcher.annotation
|
package app.revanced.patcher.annotation
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotation to constrain a [Patch] or [MethodFingerprint] to compatible packages.
|
* Annotation to constrain a [Patch] to compatible packages.
|
||||||
* @param compatiblePackages A list of packages a [Patch] or [MethodFingerprint] is compatible with.
|
* @param compatiblePackages A list of packages a [Patch] is compatible with.
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@@ -17,12 +16,12 @@ annotation class Compatibility(
|
|||||||
/**
|
/**
|
||||||
* Annotation to represent packages a patch can be compatible with.
|
* Annotation to represent packages a patch can be compatible with.
|
||||||
* @param name The package identifier name.
|
* @param name The package identifier name.
|
||||||
* @param versions The versions of the package the [Patch] or [MethodFingerprint]is compatible with.
|
* @param versions The versions of the package the [Patch] is compatible with.
|
||||||
*/
|
*/
|
||||||
@Target()
|
@Target()
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@MustBeDocumented
|
@MustBeDocumented
|
||||||
annotation class Package(
|
annotation class Package(
|
||||||
val name: String,
|
val name: String,
|
||||||
val versions: Array<String>
|
val versions: Array<String> = [],
|
||||||
)
|
)
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
package app.revanced.patcher.annotation
|
package app.revanced.patcher.annotation
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotation to name a [Patch] or [MethodFingerprint].
|
* Annotation to name a [Patch].
|
||||||
* @param name A suggestive name for the [Patch] or [MethodFingerprint].
|
* @param name A suggestive name for the [Patch].
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@@ -15,8 +14,8 @@ annotation class Name(
|
|||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotation to describe a [Patch] or [MethodFingerprint].
|
* Annotation to describe a [Patch].
|
||||||
* @param description A description for the [Patch] or [MethodFingerprint].
|
* @param description A description for the [Patch].
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@@ -27,8 +26,8 @@ annotation class Description(
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotation to version a [Patch] or [MethodFingerprint].
|
* Annotation to version a [Patch].
|
||||||
* @param version The version of a [Patch] or [MethodFingerprint].
|
* @param version The version of a [Patch].
|
||||||
*/
|
*/
|
||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
181
src/main/kotlin/app/revanced/patcher/data/Context.kt
Normal file
181
src/main/kotlin/app/revanced/patcher/data/Context.kt
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package app.revanced.patcher.data
|
||||||
|
|
||||||
|
import app.revanced.patcher.util.ProxyBackedClassList
|
||||||
|
import app.revanced.patcher.util.method.MethodWalker
|
||||||
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
import org.jf.dexlib2.iface.Method
|
||||||
|
import org.w3c.dom.Document
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.io.OutputStream
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
|
import javax.xml.transform.TransformerFactory
|
||||||
|
import javax.xml.transform.dom.DOMSource
|
||||||
|
import javax.xml.transform.stream.StreamResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
|
||||||
|
*/
|
||||||
|
|
||||||
|
sealed interface Context
|
||||||
|
|
||||||
|
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
|
||||||
|
/**
|
||||||
|
* The list of classes.
|
||||||
|
*/
|
||||||
|
val classes = ProxyBackedClassList(classes)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a class by a given class name.
|
||||||
|
*
|
||||||
|
* @param className The name of the class.
|
||||||
|
* @return A proxy for the first class that matches the class name.
|
||||||
|
*/
|
||||||
|
fun findClass(className: String) = findClass { it.type.contains(className) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a class by a given predicate.
|
||||||
|
*
|
||||||
|
* @param predicate A predicate to match the class.
|
||||||
|
* @return A proxy for the first class that matches the predicate.
|
||||||
|
*/
|
||||||
|
fun findClass(predicate: (ClassDef) -> Boolean) =
|
||||||
|
// if we already proxied the class matching the predicate...
|
||||||
|
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
|
||||||
|
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
||||||
|
classes.find(predicate)?.let { proxy(it) }
|
||||||
|
|
||||||
|
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
|
||||||
|
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
|
||||||
|
if (proxy == null) {
|
||||||
|
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
|
||||||
|
this.classes.add(proxy)
|
||||||
|
}
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
|
||||||
|
for (element in this) {
|
||||||
|
if (predicate(element)) {
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [MethodWalker] instance for the current [BytecodeContext].
|
||||||
|
*
|
||||||
|
* @param startMethod The method to start at.
|
||||||
|
* @return A [MethodWalker] instance.
|
||||||
|
*/
|
||||||
|
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
|
||||||
|
return MethodWalker(this, startMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
|
||||||
|
for ((index, element) in this.withIndex()) {
|
||||||
|
if (predicate(element)) {
|
||||||
|
return element to index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
|
||||||
|
val xmlEditor = XmlFileHolder()
|
||||||
|
|
||||||
|
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
|
||||||
|
|
||||||
|
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
|
||||||
|
|
||||||
|
inner class XmlFileHolder {
|
||||||
|
operator fun get(inputStream: InputStream) =
|
||||||
|
DomFileEditor(inputStream)
|
||||||
|
|
||||||
|
operator fun get(path: String): DomFileEditor {
|
||||||
|
return DomFileEditor(this@ResourceContext[path])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for a file that can be edited as a dom document.
|
||||||
|
*
|
||||||
|
* This constructor does not check for locks to the file when writing.
|
||||||
|
* Use the secondary constructor.
|
||||||
|
*
|
||||||
|
* @param inputStream the input stream to read the xml file from.
|
||||||
|
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class DomFileEditor internal constructor(
|
||||||
|
private val inputStream: InputStream,
|
||||||
|
private val outputStream: Lazy<OutputStream>? = null,
|
||||||
|
) : Closeable {
|
||||||
|
// path to the xml file to unlock the resource when closing the editor
|
||||||
|
private var filePath: String? = null
|
||||||
|
private var closed: Boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The document of the xml file
|
||||||
|
*/
|
||||||
|
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
|
||||||
|
.also(Document::normalize)
|
||||||
|
|
||||||
|
|
||||||
|
// lazily open an output stream
|
||||||
|
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
|
||||||
|
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
|
||||||
|
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
|
||||||
|
// increase the lock
|
||||||
|
locks.merge(file.path, 1, Integer::sum)
|
||||||
|
filePath = file.path
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the editor. Write backs and decreases the lock count.
|
||||||
|
*
|
||||||
|
* Will not write back to the file if the file is still locked.
|
||||||
|
*/
|
||||||
|
override fun close() {
|
||||||
|
if (closed) return
|
||||||
|
|
||||||
|
inputStream.close()
|
||||||
|
|
||||||
|
// if the output stream is not null, do not close it
|
||||||
|
outputStream?.let {
|
||||||
|
// prevent writing to same file, if it is being locked
|
||||||
|
// isLocked will be false if the editor was created through a stream
|
||||||
|
val isLocked = filePath?.let { path ->
|
||||||
|
val isLocked = locks[path]!! > 1
|
||||||
|
// decrease the lock count if the editor was opened for a file
|
||||||
|
locks.merge(path, -1, Integer::sum)
|
||||||
|
isLocked
|
||||||
|
} ?: false
|
||||||
|
|
||||||
|
// if unlocked, write back to the file
|
||||||
|
if (!isLocked) {
|
||||||
|
it.value.use { stream ->
|
||||||
|
val result = StreamResult(stream)
|
||||||
|
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.value.close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
// map of concurrent open files
|
||||||
|
val locks = mutableMapOf<String, Int>()
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +0,0 @@
|
|||||||
package app.revanced.patcher.data
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.impl.BytecodeData
|
|
||||||
import app.revanced.patcher.data.impl.ResourceData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constraint interface for [BytecodeData] and [ResourceData]
|
|
||||||
*/
|
|
||||||
interface Data
|
|
@@ -1,69 +0,0 @@
|
|||||||
package app.revanced.patcher.data.impl
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.Data
|
|
||||||
import app.revanced.patcher.util.ProxyBackedClassList
|
|
||||||
import app.revanced.patcher.util.method.MethodWalker
|
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
|
||||||
import org.jf.dexlib2.iface.Method
|
|
||||||
|
|
||||||
class BytecodeData(
|
|
||||||
internalClasses: MutableList<ClassDef>
|
|
||||||
) : Data {
|
|
||||||
val classes = ProxyBackedClassList(internalClasses)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a class by a given class name.
|
|
||||||
* @param className The name of the class.
|
|
||||||
* @return A proxy for the first class that matches the class name.
|
|
||||||
*/
|
|
||||||
fun findClass(className: String) = findClass { it.type.contains(className) }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a class by a given predicate.
|
|
||||||
* @param predicate A predicate to match the class.
|
|
||||||
* @return A proxy for the first class that matches the predicate.
|
|
||||||
*/
|
|
||||||
fun findClass(predicate: (ClassDef) -> Boolean) =
|
|
||||||
// if we already proxied the class matching the predicate...
|
|
||||||
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
|
|
||||||
// else resolve the class to a proxy and return it, if the predicate is matching a class
|
|
||||||
classes.find(predicate)?.let { proxy(it) }
|
|
||||||
|
|
||||||
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
|
|
||||||
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
|
|
||||||
if (proxy == null) {
|
|
||||||
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
|
|
||||||
this.classes.add(proxy)
|
|
||||||
}
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class MethodNotFoundException(s: String) : Exception(s)
|
|
||||||
|
|
||||||
internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
|
|
||||||
for (element in this) {
|
|
||||||
if (predicate(element)) {
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a [MethodWalker] instance for the current [BytecodeData].
|
|
||||||
* @param startMethod The method to start at.
|
|
||||||
* @return A [MethodWalker] instance.
|
|
||||||
*/
|
|
||||||
fun BytecodeData.toMethodWalker(startMethod: Method): MethodWalker {
|
|
||||||
return MethodWalker(this, startMethod)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
|
|
||||||
for ((index, element) in this.withIndex()) {
|
|
||||||
if (predicate(element)) {
|
|
||||||
return element to index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
@@ -1,42 +0,0 @@
|
|||||||
package app.revanced.patcher.data.impl
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.Data
|
|
||||||
import org.w3c.dom.Document
|
|
||||||
import java.io.Closeable
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import javax.xml.parsers.DocumentBuilderFactory
|
|
||||||
import javax.xml.transform.TransformerFactory
|
|
||||||
import javax.xml.transform.dom.DOMSource
|
|
||||||
import javax.xml.transform.stream.StreamResult
|
|
||||||
|
|
||||||
class ResourceData(private val resourceCacheDirectory: File) : Data, Iterable<File> {
|
|
||||||
val xmlEditor = XmlFileHolder()
|
|
||||||
|
|
||||||
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
|
|
||||||
|
|
||||||
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
|
|
||||||
|
|
||||||
inner class XmlFileHolder {
|
|
||||||
operator fun get(inputStream: InputStream, outputStream: OutputStream) =
|
|
||||||
DomFileEditor(inputStream, lazyOf(outputStream))
|
|
||||||
|
|
||||||
operator fun get(path: String) = DomFileEditor(this@ResourceData[path])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DomFileEditor internal constructor(inputStream: InputStream, private val outputStream: Lazy<OutputStream>) : Closeable {
|
|
||||||
|
|
||||||
// lazily open an output stream
|
|
||||||
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
|
|
||||||
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
|
|
||||||
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() })
|
|
||||||
|
|
||||||
val file: Document =
|
|
||||||
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream).also(Document::normalize)
|
|
||||||
|
|
||||||
override fun close() =
|
|
||||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), StreamResult(outputStream.value))
|
|
||||||
|
|
||||||
}
|
|
@@ -1,20 +1,24 @@
|
|||||||
package app.revanced.patcher.extensions
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
import app.revanced.patcher.annotation.Compatibility
|
import app.revanced.patcher.annotation.*
|
||||||
import app.revanced.patcher.annotation.Description
|
import app.revanced.patcher.data.Context
|
||||||
import app.revanced.patcher.annotation.Name
|
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||||
import app.revanced.patcher.annotation.Version
|
|
||||||
import app.revanced.patcher.data.Data
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
import app.revanced.patcher.patch.OptionsContainer
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchOptions
|
||||||
|
import app.revanced.patcher.patch.annotations.DependsOn
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KVisibility
|
||||||
|
import kotlin.reflect.full.companionObject
|
||||||
|
import kotlin.reflect.full.companionObjectInstance
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively find a given annotation on a class.
|
* Recursively find a given annotation on a class.
|
||||||
* @param targetAnnotation The annotation to find.
|
* @param targetAnnotation The annotation to find.
|
||||||
* @return The annotation.
|
* @return The annotation.
|
||||||
*/
|
*/
|
||||||
private fun <T : Annotation> Class<*>.recursiveAnnotation(targetAnnotation: KClass<T>) =
|
private fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>) =
|
||||||
this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
|
this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
|
||||||
|
|
||||||
|
|
||||||
@@ -36,22 +40,40 @@ private fun <T : Annotation> Class<*>.findAnnotationRecursively(
|
|||||||
}
|
}
|
||||||
|
|
||||||
object PatchExtensions {
|
object PatchExtensions {
|
||||||
val Class<out Patch<Data>>.patchName: String
|
val Class<out Patch<Context>>.patchName: String
|
||||||
get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
|
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
||||||
val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version
|
|
||||||
val Class<out Patch<Data>>.include get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
val Class<out Patch<Context>>.version
|
||||||
val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description
|
get() = findAnnotationRecursively(Version::class)?.version
|
||||||
val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Dependencies::class)?.dependencies
|
|
||||||
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages
|
val Class<out Patch<Context>>.include
|
||||||
|
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
||||||
|
|
||||||
|
val Class<out Patch<Context>>.description
|
||||||
|
get() = findAnnotationRecursively(Description::class)?.description
|
||||||
|
|
||||||
|
val Class<out Patch<Context>>.dependencies
|
||||||
|
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
||||||
|
|
||||||
|
val Class<out Patch<Context>>.compatiblePackages
|
||||||
|
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
||||||
|
|
||||||
|
val Class<out Patch<Context>>.options: PatchOptions?
|
||||||
|
get() = kotlin.companionObject?.let { cl ->
|
||||||
|
if (cl.visibility != KVisibility.PUBLIC) return null
|
||||||
|
kotlin.companionObjectInstance?.let {
|
||||||
|
(it as? OptionsContainer)?.options
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object MethodFingerprintExtensions {
|
object MethodFingerprintExtensions {
|
||||||
val MethodFingerprint.name: String
|
val MethodFingerprint.name: String
|
||||||
get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName
|
get() = this.javaClass.simpleName
|
||||||
val MethodFingerprint.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1"
|
|
||||||
val MethodFingerprint.description get() = javaClass.recursiveAnnotation(Description::class)?.description
|
val MethodFingerprint.fuzzyPatternScanMethod
|
||||||
val MethodFingerprint.compatiblePackages get() = javaClass.recursiveAnnotation(Compatibility::class)?.compatiblePackages
|
get() = javaClass.findAnnotationRecursively(FuzzyPatternScanMethod::class)
|
||||||
val MethodFingerprint.matchingMethod get() = javaClass.recursiveAnnotation(app.revanced.patcher.fingerprint.method.annotation.MatchingMethod::class)
|
|
||||||
val MethodFingerprint.fuzzyPatternScanMethod get() = javaClass.recursiveAnnotation(app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod::class)
|
val MethodFingerprint.fuzzyScanThreshold
|
||||||
val MethodFingerprint.fuzzyScanThreshold get() = fuzzyPatternScanMethod?.threshold ?: 0
|
get() = fuzzyPatternScanMethod?.threshold ?: 0
|
||||||
}
|
}
|
@@ -2,16 +2,19 @@ package app.revanced.patcher.extensions
|
|||||||
|
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
|
import app.revanced.patcher.util.smali.ExternalLabel
|
||||||
import app.revanced.patcher.util.smali.toInstruction
|
import app.revanced.patcher.util.smali.toInstruction
|
||||||
import app.revanced.patcher.util.smali.toInstructions
|
import app.revanced.patcher.util.smali.toInstructions
|
||||||
import org.jf.dexlib2.AccessFlags
|
import org.jf.dexlib2.AccessFlags
|
||||||
import org.jf.dexlib2.builder.BuilderInstruction
|
import org.jf.dexlib2.builder.BuilderInstruction
|
||||||
|
import org.jf.dexlib2.builder.BuilderOffsetInstruction
|
||||||
|
import org.jf.dexlib2.builder.Label
|
||||||
import org.jf.dexlib2.builder.MutableMethodImplementation
|
import org.jf.dexlib2.builder.MutableMethodImplementation
|
||||||
|
import org.jf.dexlib2.builder.instruction.*
|
||||||
import org.jf.dexlib2.iface.Method
|
import org.jf.dexlib2.iface.Method
|
||||||
import org.jf.dexlib2.iface.reference.MethodReference
|
import org.jf.dexlib2.iface.instruction.Instruction
|
||||||
import org.jf.dexlib2.immutable.ImmutableMethod
|
import org.jf.dexlib2.immutable.ImmutableMethod
|
||||||
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
import org.jf.dexlib2.immutable.ImmutableMethodImplementation
|
||||||
import org.jf.dexlib2.util.MethodUtil
|
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value
|
infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value
|
||||||
@@ -23,6 +26,12 @@ fun MutableMethodImplementation.addInstructions(index: Int, instructions: List<B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) {
|
||||||
|
for (instruction in instructions) {
|
||||||
|
this.addInstruction(instruction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun MutableMethodImplementation.replaceInstructions(index: Int, instructions: List<BuilderInstruction>) {
|
fun MutableMethodImplementation.replaceInstructions(index: Int, instructions: List<BuilderInstruction>) {
|
||||||
for (i in instructions.lastIndex downTo 0) {
|
for (i in instructions.lastIndex downTo 0) {
|
||||||
this.replaceInstruction(index + i, instructions[i])
|
this.replaceInstruction(index + i, instructions[i])
|
||||||
@@ -35,28 +44,13 @@ fun MutableMethodImplementation.removeInstructions(index: Int, count: Int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare a method to another, considering constructors and parameters.
|
|
||||||
* @param otherMethod The method to compare against.
|
|
||||||
* @return True if the methods match given the conditions.
|
|
||||||
*/
|
|
||||||
fun Method.softCompareTo(
|
|
||||||
otherMethod: MethodReference
|
|
||||||
): Boolean {
|
|
||||||
if (MethodUtil.isConstructor(this) && !parametersEqual(this.parameterTypes, otherMethod.parameterTypes))
|
|
||||||
return false
|
|
||||||
return this.name == otherMethod.name
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the method.
|
* Clones the method.
|
||||||
* @param registerCount This parameter allows you to change the register count of the method.
|
* @param registerCount This parameter allows you to change the register count of the method.
|
||||||
* This may be a positive or negative number.
|
* This may be a positive or negative number.
|
||||||
* @return The **immutable** cloned method. Call [toMutable] or [cloneMutable] to get a **mutable** copy.
|
* @return The **immutable** cloned method. Call [toMutable] or [cloneMutable] to get a **mutable** copy.
|
||||||
*/
|
*/
|
||||||
internal fun Method.clone(
|
internal fun Method.clone(registerCount: Int = 0): ImmutableMethod {
|
||||||
registerCount: Int = 0,
|
|
||||||
): ImmutableMethod {
|
|
||||||
val clonedImplementation = implementation?.let {
|
val clonedImplementation = implementation?.let {
|
||||||
ImmutableMethodImplementation(
|
ImmutableMethodImplementation(
|
||||||
it.registerCount + registerCount,
|
it.registerCount + registerCount,
|
||||||
@@ -66,14 +60,7 @@ internal fun Method.clone(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return ImmutableMethod(
|
return ImmutableMethod(
|
||||||
returnType,
|
returnType, name, parameters, returnType, accessFlags, annotations, hiddenApiRestrictions, clonedImplementation
|
||||||
name,
|
|
||||||
parameters,
|
|
||||||
returnType,
|
|
||||||
accessFlags,
|
|
||||||
annotations,
|
|
||||||
hiddenApiRestrictions,
|
|
||||||
clonedImplementation
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,16 +91,89 @@ fun MutableMethod.replaceInstruction(index: Int, instruction: String) =
|
|||||||
* Remove a smali instruction within the method.
|
* Remove a smali instruction within the method.
|
||||||
* @param index The index to delete the instruction at.
|
* @param index The index to delete the instruction at.
|
||||||
*/
|
*/
|
||||||
fun MutableMethod.removeInstruction(index: Int) =
|
fun MutableMethod.removeInstruction(index: Int) = this.implementation!!.removeInstruction(index)
|
||||||
this.implementation!!.removeInstruction(index)
|
|
||||||
|
/**
|
||||||
|
* Create a label for the instruction at given index in the method's implementation.
|
||||||
|
* @param index The index to create the label for the instruction at.
|
||||||
|
* @return The label.
|
||||||
|
*/
|
||||||
|
fun MutableMethod.label(index: Int) = this.implementation!!.newLabelForIndex(index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the instruction at given index in the method's implementation.
|
||||||
|
* @param index The index to get the instruction at.
|
||||||
|
* @return The instruction.
|
||||||
|
*/
|
||||||
|
fun MutableMethod.instruction(index: Int): BuilderInstruction = this.implementation!!.instructions[index]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add smali instructions to the method.
|
* Add smali instructions to the method.
|
||||||
* @param index The index to insert the instructions at.
|
* @param index The index to insert the instructions at.
|
||||||
|
* @param smali The smali instructions to add.
|
||||||
|
* @param externalLabels A list of [ExternalLabel] representing a list of labels for instructions which are not in the method to compile.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fun MutableMethod.addInstructions(index: Int, smali: String, externalLabels: List<ExternalLabel> = emptyList()) {
|
||||||
|
// Create reference dummy instructions for the instructions.
|
||||||
|
val nopedSmali = StringBuilder(smali).also { builder ->
|
||||||
|
externalLabels.forEach { (name, _) ->
|
||||||
|
builder.append("\n:$name\nnop")
|
||||||
|
}
|
||||||
|
}.toString()
|
||||||
|
|
||||||
|
// Compile the instructions with the dummy labels
|
||||||
|
val compiledInstructions = nopedSmali.toInstructions(this)
|
||||||
|
|
||||||
|
// Add the compiled list of instructions to the method.
|
||||||
|
val methodImplementation = this.implementation!!
|
||||||
|
methodImplementation.addInstructions(index, compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size))
|
||||||
|
|
||||||
|
val methodInstructions = methodImplementation.instructions
|
||||||
|
methodInstructions.subList(index, index + compiledInstructions.size - externalLabels.size)
|
||||||
|
.forEachIndexed { compiledInstructionIndex, compiledInstruction ->
|
||||||
|
// If the compiled instruction is not an offset instruction, skip it.
|
||||||
|
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new label for the instruction and replaces it with the label of the [compiledInstruction] at [compiledInstructionIndex].
|
||||||
|
*/
|
||||||
|
fun Instruction.makeNewLabel() {
|
||||||
|
// Create the final label.
|
||||||
|
val label = methodImplementation.newLabelForIndex(methodInstructions.indexOf(this))
|
||||||
|
// Create the final instruction with the new label.
|
||||||
|
val newInstruction = replaceOffset(
|
||||||
|
compiledInstruction, label
|
||||||
|
)
|
||||||
|
// Replace the instruction pointing to the dummy label with the new instruction pointing to the real instruction.
|
||||||
|
methodImplementation.replaceInstruction(index + compiledInstructionIndex, newInstruction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the compiled instruction targets its own instruction,
|
||||||
|
// which means it points to some of its own, simply an offset has to be applied.
|
||||||
|
val labelIndex = compiledInstruction.target.location.index
|
||||||
|
if (labelIndex < compiledInstructions.size - externalLabels.size) {
|
||||||
|
// Get the targets index (insertion index + the index of the dummy instruction).
|
||||||
|
methodInstructions[index + labelIndex].makeNewLabel()
|
||||||
|
return@forEachIndexed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the compiled instruction points to a dummy instruction,
|
||||||
|
// we can find the real instruction which it was created for by calculation.
|
||||||
|
|
||||||
|
// Get the index of the instruction in the externalLabels list which the dummy instruction was created for.
|
||||||
|
// this line works because we created the dummy instructions in the same order as the externalLabels list.
|
||||||
|
val (_, instruction) = externalLabels[(compiledInstructions.size - 1) - labelIndex]
|
||||||
|
instruction.makeNewLabel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add smali instructions to the end of the method.
|
||||||
* @param instructions The smali instructions to add.
|
* @param instructions The smali instructions to add.
|
||||||
*/
|
*/
|
||||||
fun MutableMethod.addInstructions(index: Int, instructions: String) =
|
fun MutableMethod.addInstructions(instructions: String, labels: List<ExternalLabel> = emptyList()) =
|
||||||
this.implementation!!.addInstructions(index, instructions.toInstructions(this))
|
this.addInstructions(this.implementation!!.instructions.size, instructions, labels)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace smali instructions within the method.
|
* Replace smali instructions within the method.
|
||||||
@@ -128,8 +188,21 @@ fun MutableMethod.replaceInstructions(index: Int, instructions: String) =
|
|||||||
* @param index The index to remove the instructions at.
|
* @param index The index to remove the instructions at.
|
||||||
* @param count The amount of instructions to remove.
|
* @param count The amount of instructions to remove.
|
||||||
*/
|
*/
|
||||||
fun MutableMethod.removeInstructions(index: Int, count: Int) =
|
fun MutableMethod.removeInstructions(index: Int, count: Int) = this.implementation!!.removeInstructions(index, count)
|
||||||
this.implementation!!.removeInstructions(index, count)
|
|
||||||
|
private fun replaceOffset(
|
||||||
|
i: BuilderOffsetInstruction, label: Label
|
||||||
|
): BuilderOffsetInstruction {
|
||||||
|
return when (i) {
|
||||||
|
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
|
||||||
|
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
|
||||||
|
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
|
||||||
|
is BuilderInstruction22t -> BuilderInstruction22t(i.opcode, i.registerA, i.registerB, label)
|
||||||
|
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
|
||||||
|
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
|
||||||
|
else -> throw IllegalStateException("A non-offset instruction was given, this should never happen!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clones the method.
|
* Clones the method.
|
||||||
@@ -137,14 +210,11 @@ fun MutableMethod.removeInstructions(index: Int, count: Int) =
|
|||||||
* This may be a positive or negative number.
|
* This may be a positive or negative number.
|
||||||
* @return The **mutable** cloned method. Call [clone] to get an **immutable** copy.
|
* @return The **mutable** cloned method. Call [clone] to get an **immutable** copy.
|
||||||
*/
|
*/
|
||||||
internal fun Method.cloneMutable(
|
internal fun Method.cloneMutable(registerCount: Int = 0) = clone(registerCount).toMutable()
|
||||||
registerCount: Int = 0,
|
|
||||||
) = clone(registerCount).toMutable()
|
|
||||||
|
|
||||||
// FIXME: also check the order of parameters as different order equals different method overload
|
// FIXME: also check the order of parameters as different order equals different method overload
|
||||||
internal fun parametersEqual(
|
internal fun parametersEqual(
|
||||||
parameters1: Iterable<CharSequence>,
|
parameters1: Iterable<CharSequence>, parameters2: Iterable<CharSequence>
|
||||||
parameters2: Iterable<CharSequence>
|
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return parameters1.count() == parameters2.count() && parameters1.all { parameter ->
|
return parameters1.count() == parameters2.count() && parameters1.all { parameter ->
|
||||||
parameters2.any {
|
parameters2.any {
|
||||||
@@ -155,10 +225,9 @@ internal fun parametersEqual(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val nullOutputStream: OutputStream =
|
internal val nullOutputStream = object : OutputStream() {
|
||||||
object : OutputStream() {
|
override fun write(b: Int) {}
|
||||||
override fun write(b: Int) {}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Should be used to parse a list of parameters represented by their first letter,
|
* Should be used to parse a list of parameters represented by their first letter,
|
||||||
|
@@ -2,18 +2,6 @@ package app.revanced.patcher.fingerprint.method.annotation
|
|||||||
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotations for a method which matches to a [MethodFingerprint].
|
|
||||||
* @param definingClass The defining class name of the method.
|
|
||||||
* @param name A suggestive name for the method which the [MethodFingerprint] was created for.
|
|
||||||
*/
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
annotation class MatchingMethod(
|
|
||||||
val definingClass: String = "L<unspecified-class>;",
|
|
||||||
val name: String = "<unspecified-method>"
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
||||||
* @param threshold if [threshold] or more of the opcodes do not match, skip.
|
* @param threshold if [threshold] or more of the opcodes do not match, skip.
|
||||||
@@ -22,11 +10,4 @@ annotation class MatchingMethod(
|
|||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
annotation class FuzzyPatternScanMethod(
|
annotation class FuzzyPatternScanMethod(
|
||||||
val threshold: Int = 1
|
val threshold: Int = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotations to scan a pattern [MethodFingerprint] directly.
|
|
||||||
*/
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
annotation class DirectPatternScanMethod
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,158 +0,0 @@
|
|||||||
package app.revanced.patcher.fingerprint.method.utils
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.impl.BytecodeData
|
|
||||||
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
|
|
||||||
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyScanThreshold
|
|
||||||
import app.revanced.patcher.extensions.parametersEqual
|
|
||||||
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.PatternScanResult
|
|
||||||
import org.jf.dexlib2.Opcode
|
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
|
||||||
import org.jf.dexlib2.iface.Method
|
|
||||||
import org.jf.dexlib2.iface.instruction.Instruction
|
|
||||||
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
|
|
||||||
import org.jf.dexlib2.iface.reference.StringReference
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for [MethodFingerprint]
|
|
||||||
*/
|
|
||||||
object MethodFingerprintUtils {
|
|
||||||
/**
|
|
||||||
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
|
|
||||||
* @param context The classes on which to resolve the [MethodFingerprint].
|
|
||||||
* @param forData The [BytecodeData] to host proxies.
|
|
||||||
* @return True if the resolution was successful, false otherwise.
|
|
||||||
*/
|
|
||||||
fun Iterable<MethodFingerprint>.resolve(forData: BytecodeData, context: Iterable<ClassDef>) {
|
|
||||||
for (fingerprint in this) // For each fingerprint
|
|
||||||
classes@ for (classDef in context) // search through all classes for the fingerprint
|
|
||||||
if (fingerprint.resolve(forData, classDef))
|
|
||||||
break@classes // if the resolution succeeded, continue with the next fingerprint
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a [MethodFingerprint] against a [ClassDef].
|
|
||||||
* @param context The class on which to resolve the [MethodFingerprint].
|
|
||||||
* @param forData The [BytecodeData] to host proxies.
|
|
||||||
* @return True if the resolution was successful, false otherwise.
|
|
||||||
*/
|
|
||||||
fun MethodFingerprint.resolve(forData: BytecodeData, context: ClassDef): Boolean {
|
|
||||||
for (method in context.methods)
|
|
||||||
if (this.resolve(forData, method, context))
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a [MethodFingerprint] against a [Method].
|
|
||||||
* @param context The context on which to resolve the [MethodFingerprint].
|
|
||||||
* @param classDef The class of the matching [Method].
|
|
||||||
* @param forData The [BytecodeData] to host proxies.
|
|
||||||
* @return True if the resolution was successful, false otherwise.
|
|
||||||
*/
|
|
||||||
fun MethodFingerprint.resolve(forData: BytecodeData, context: Method, classDef: ClassDef): Boolean {
|
|
||||||
val methodFingerprint = this
|
|
||||||
|
|
||||||
if (methodFingerprint.returnType != null && !context.returnType.startsWith(methodFingerprint.returnType))
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (methodFingerprint.access != null && methodFingerprint.access != context.accessFlags)
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
if (methodFingerprint.parameters != null && !parametersEqual(
|
|
||||||
methodFingerprint.parameters, // TODO: parseParameters()
|
|
||||||
context.parameterTypes
|
|
||||||
)
|
|
||||||
) return false
|
|
||||||
|
|
||||||
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(context))
|
|
||||||
return false
|
|
||||||
|
|
||||||
if (methodFingerprint.strings != null) {
|
|
||||||
val implementation = context.implementation ?: return false
|
|
||||||
|
|
||||||
val stringsList = methodFingerprint.strings.toMutableList()
|
|
||||||
|
|
||||||
implementation.instructions.forEach { instruction ->
|
|
||||||
if (instruction.opcode.ordinal != Opcode.CONST_STRING.ordinal) return@forEach
|
|
||||||
|
|
||||||
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
|
||||||
val index = stringsList.indexOfFirst { it == string }
|
|
||||||
if (index != -1) stringsList.removeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringsList.isNotEmpty()) return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val patternScanResult = if (methodFingerprint.opcodes != null) {
|
|
||||||
context.implementation?.instructions ?: return false
|
|
||||||
|
|
||||||
context.patternScan(methodFingerprint) ?: return false
|
|
||||||
} else null
|
|
||||||
|
|
||||||
methodFingerprint.result = MethodFingerprintResult(context, classDef, patternScanResult, forData)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Method.patternScan(
|
|
||||||
fingerprint: MethodFingerprint
|
|
||||||
): PatternScanResult? {
|
|
||||||
val instructions = this.implementation!!.instructions
|
|
||||||
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyScanThreshold
|
|
||||||
|
|
||||||
val pattern = fingerprint.opcodes!!
|
|
||||||
val instructionLength = instructions.count()
|
|
||||||
val patternLength = pattern.count()
|
|
||||||
|
|
||||||
for (index in 0 until instructionLength) {
|
|
||||||
var patternIndex = 0
|
|
||||||
var threshold = fingerprintFuzzyPatternScanThreshold
|
|
||||||
|
|
||||||
while (index + patternIndex < instructionLength) {
|
|
||||||
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
|
|
||||||
val patternOpcode = pattern.elementAt(patternIndex)
|
|
||||||
|
|
||||||
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
|
|
||||||
// reaching maximum threshold (0) means,
|
|
||||||
// the pattern does not match to the current instructions
|
|
||||||
if (threshold-- == 0) break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (patternIndex < patternLength - 1) {
|
|
||||||
// if the entire pattern has not been scanned yet
|
|
||||||
// continue the scan
|
|
||||||
patternIndex++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
|
|
||||||
val result = PatternScanResult(index, index + patternIndex)
|
|
||||||
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
|
|
||||||
result.warnings = result.createWarnings(pattern, instructions)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PatternScanResult.createWarnings(
|
|
||||||
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
|
|
||||||
) = buildList {
|
|
||||||
for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) {
|
|
||||||
val originalOpcode = instructions.elementAt(instructionIndex).opcode
|
|
||||||
val patternOpcode = pattern.elementAt(patternIndex)
|
|
||||||
|
|
||||||
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
|
|
||||||
|
|
||||||
this.add(PatternScanResult.Warning(originalOpcode, patternOpcode, instructionIndex, patternIndex))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private operator fun ClassDef.component1() = this
|
|
||||||
private operator fun ClassDef.component2() = this.methods
|
|
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container for patch options.
|
||||||
|
*/
|
||||||
|
abstract class OptionsContainer {
|
||||||
|
/**
|
||||||
|
* A list of [PatchOption]s.
|
||||||
|
* @see PatchOptions
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val options = PatchOptions()
|
||||||
|
|
||||||
|
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
|
||||||
|
options.register(opt)
|
||||||
|
return opt
|
||||||
|
}
|
||||||
|
}
|
@@ -1,18 +1,44 @@
|
|||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.data.Data
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
import app.revanced.patcher.patch.impl.BytecodePatch
|
import app.revanced.patcher.data.Context
|
||||||
import app.revanced.patcher.patch.impl.ResourcePatch
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
|
import java.io.Closeable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ReVanced patch.
|
* A ReVanced patch.
|
||||||
* Can either be a [ResourcePatch] or a [BytecodePatch].
|
*
|
||||||
|
* If it implements [Closeable], it will be closed after all patches have been executed.
|
||||||
|
* Closing will be done in reverse execution order.
|
||||||
*/
|
*/
|
||||||
abstract class Patch<out T : Data> {
|
sealed interface Patch<out T : Context> : Closeable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main function of the [Patch] which the patcher will call.
|
* The main function of the [Patch] which the patcher will call.
|
||||||
|
*
|
||||||
|
* @param context The [Context] the patch will work on.
|
||||||
|
* @return The result of executing the patch.
|
||||||
*/
|
*/
|
||||||
abstract fun execute(data: @UnsafeVariance T): PatchResult
|
fun execute(context: @UnsafeVariance T): PatchResult
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* The closing function for this patch.
|
||||||
|
*
|
||||||
|
* This can be treated like popping the patch from the current patch stack.
|
||||||
|
*/
|
||||||
|
override fun close() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource patch for the Patcher.
|
||||||
|
*/
|
||||||
|
interface ResourcePatch : Patch<ResourceContext>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytecode patch for the Patcher.
|
||||||
|
*
|
||||||
|
* @param fingerprints A list of [MethodFingerprint] this patch relies on.
|
||||||
|
*/
|
||||||
|
abstract class BytecodePatch(
|
||||||
|
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
||||||
|
) : Patch<BytecodeContext>
|
248
src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt
Normal file
248
src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST")
|
||||||
|
|
||||||
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
|
import java.nio.file.Path
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
|
||||||
|
class IllegalValueException(val value: Any?) : Exception("Illegal value: $value")
|
||||||
|
class InvalidTypeException(val got: String, val expected: String) :
|
||||||
|
Exception("Invalid option value type: $got, expected $expected")
|
||||||
|
|
||||||
|
object RequirementNotMetException : Exception("null was passed into an option that requires a value")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A registry for an array of [PatchOption]s.
|
||||||
|
* @param options An array of [PatchOption]s.
|
||||||
|
*/
|
||||||
|
class PatchOptions(vararg options: PatchOption<*>) : Iterable<PatchOption<*>> {
|
||||||
|
private val register = mutableMapOf<String, PatchOption<*>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
options.forEach { register(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun register(option: PatchOption<*>) {
|
||||||
|
if (register.containsKey(option.key)) {
|
||||||
|
throw IllegalStateException("Multiple options found with the same key")
|
||||||
|
}
|
||||||
|
register[option.key] = option
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [PatchOption] by its key.
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
*/
|
||||||
|
@JvmName("getUntyped")
|
||||||
|
operator fun get(key: String) = register[key] ?: throw NoSuchOptionException(key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a [PatchOption] by its key.
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
*/
|
||||||
|
inline operator fun <reified T> get(key: String): PatchOption<T> {
|
||||||
|
val opt = get(key)
|
||||||
|
if (opt.value !is T) throw InvalidTypeException(
|
||||||
|
opt.value?.let { it::class.java.canonicalName } ?: "null",
|
||||||
|
T::class.java.canonicalName
|
||||||
|
)
|
||||||
|
return opt as PatchOption<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of a [PatchOption].
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
* @param value The value you want it to be.
|
||||||
|
* Please note that using the wrong value type results in a runtime error.
|
||||||
|
*/
|
||||||
|
inline operator fun <reified T> set(key: String, value: T) {
|
||||||
|
val opt = get<T>(key)
|
||||||
|
if (opt.value !is T) throw InvalidTypeException(
|
||||||
|
T::class.java.canonicalName,
|
||||||
|
opt.value?.let { it::class.java.canonicalName } ?: "null"
|
||||||
|
)
|
||||||
|
opt.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of a [PatchOption] to `null`.
|
||||||
|
* @param key The key of the [PatchOption].
|
||||||
|
*/
|
||||||
|
fun nullify(key: String) {
|
||||||
|
get(key).value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun iterator() = register.values.iterator()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Patch] option.
|
||||||
|
* @param key Unique identifier of the option. Example: _`settings.microg.enabled`_
|
||||||
|
* @param default The default value of the option.
|
||||||
|
* @param title A human-readable title of the option. Example: _MicroG Settings_
|
||||||
|
* @param description A human-readable description of the option. Example: _Settings integration for MicroG._
|
||||||
|
* @param required Whether the option is required.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
sealed class PatchOption<T>(
|
||||||
|
val key: String,
|
||||||
|
default: T?,
|
||||||
|
val title: String,
|
||||||
|
val description: String,
|
||||||
|
val required: Boolean,
|
||||||
|
val validator: (T?) -> Boolean
|
||||||
|
) {
|
||||||
|
var value: T? = default
|
||||||
|
get() {
|
||||||
|
if (field == null && required) {
|
||||||
|
throw RequirementNotMetException
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
set(value) {
|
||||||
|
if (value == null && required) {
|
||||||
|
throw RequirementNotMetException
|
||||||
|
}
|
||||||
|
if (!validator(value)) {
|
||||||
|
throw IllegalValueException(value)
|
||||||
|
}
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of the option.
|
||||||
|
* Please note that using the wrong value type results in a runtime error.
|
||||||
|
*/
|
||||||
|
@JvmName("getValueTyped")
|
||||||
|
inline operator fun <reified V> getValue(thisRef: Nothing?, property: KProperty<*>): V? {
|
||||||
|
if (value !is V?) throw InvalidTypeException(
|
||||||
|
V::class.java.canonicalName,
|
||||||
|
value?.let { it::class.java.canonicalName } ?: "null"
|
||||||
|
)
|
||||||
|
return value as? V?
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of the option.
|
||||||
|
* Please note that using the wrong value type results in a runtime error.
|
||||||
|
*/
|
||||||
|
@JvmName("setValueTyped")
|
||||||
|
inline operator fun <reified V> setValue(thisRef: Nothing?, property: KProperty<*>, new: V) {
|
||||||
|
if (value !is V) throw InvalidTypeException(
|
||||||
|
V::class.java.canonicalName,
|
||||||
|
value?.let { it::class.java.canonicalName } ?: "null"
|
||||||
|
)
|
||||||
|
value = new as T
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun setValue(thisRef: Any?, property: KProperty<*>, new: T?) {
|
||||||
|
value = new
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] representing a [String].
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
class StringOption(
|
||||||
|
key: String,
|
||||||
|
default: String?,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (String?) -> Boolean = { true }
|
||||||
|
) : PatchOption<String>(
|
||||||
|
key, default, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] representing a [Boolean].
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
class BooleanOption(
|
||||||
|
key: String,
|
||||||
|
default: Boolean?,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (Boolean?) -> Boolean = { true }
|
||||||
|
) : PatchOption<Boolean>(
|
||||||
|
key, default, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] with a list of allowed options.
|
||||||
|
* @param options A list of allowed options for the [ListOption].
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
sealed class ListOption<E>(
|
||||||
|
key: String,
|
||||||
|
default: E?,
|
||||||
|
val options: Iterable<E>,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (E?) -> Boolean = { true }
|
||||||
|
) : PatchOption<E>(
|
||||||
|
key, default, title, description, required, {
|
||||||
|
(it?.let { it in options } ?: true) && validator(it)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
if (default != null && default !in options) {
|
||||||
|
throw IllegalStateException("Default option must be an allowed option")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ListOption] of type [String].
|
||||||
|
* @see ListOption
|
||||||
|
*/
|
||||||
|
class StringListOption(
|
||||||
|
key: String,
|
||||||
|
default: String?,
|
||||||
|
options: Iterable<String>,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (String?) -> Boolean = { true }
|
||||||
|
) : ListOption<String>(
|
||||||
|
key, default, options, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ListOption] of type [Int].
|
||||||
|
* @see ListOption
|
||||||
|
*/
|
||||||
|
class IntListOption(
|
||||||
|
key: String,
|
||||||
|
default: Int?,
|
||||||
|
options: Iterable<Int>,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (Int?) -> Boolean = { true }
|
||||||
|
) : ListOption<Int>(
|
||||||
|
key, default, options, title, description, required, validator
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [PatchOption] representing a [Path], backed by a [String].
|
||||||
|
* The validator passes a [String], if you need a [Path] you will have to convert it yourself.
|
||||||
|
* @see PatchOption
|
||||||
|
*/
|
||||||
|
class PathOption(
|
||||||
|
key: String,
|
||||||
|
default: Path?,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
required: Boolean = false,
|
||||||
|
validator: (String?) -> Boolean = { true }
|
||||||
|
) : PatchOption<String>(
|
||||||
|
key, default?.pathString, title, description, required, validator
|
||||||
|
)
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
package app.revanced.patcher.patch.annotations
|
package app.revanced.patcher.patch.annotations
|
||||||
|
|
||||||
import app.revanced.patcher.data.Data
|
import app.revanced.patcher.data.Context
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
@@ -19,6 +19,6 @@ annotation class Patch(val include: Boolean = true)
|
|||||||
@Target(AnnotationTarget.CLASS)
|
@Target(AnnotationTarget.CLASS)
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
@MustBeDocumented
|
@MustBeDocumented
|
||||||
annotation class Dependencies(
|
annotation class DependsOn(
|
||||||
val dependencies: Array<KClass<out Patch<Data>>> = []
|
val dependencies: Array<KClass<out Patch<Context>>> = []
|
||||||
)
|
)
|
@@ -1,13 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.impl
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.impl.BytecodeData
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bytecode patch for the Patcher.
|
|
||||||
* @param fingerprints A list of [MethodFingerprint] this patch relies on.
|
|
||||||
*/
|
|
||||||
abstract class BytecodePatch(
|
|
||||||
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
|
||||||
) : Patch<BytecodeData>()
|
|
@@ -1,9 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.impl
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.impl.ResourceData
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resource patch for the Patcher.
|
|
||||||
*/
|
|
||||||
abstract class ResourcePatch : Patch<ResourceData>()
|
|
@@ -3,40 +3,44 @@ package app.revanced.patcher.util
|
|||||||
import app.revanced.patcher.util.proxy.ClassProxy
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
import org.jf.dexlib2.iface.ClassDef
|
import org.jf.dexlib2.iface.ClassDef
|
||||||
|
|
||||||
class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> {
|
/**
|
||||||
private val internalProxies = mutableListOf<ClassProxy>()
|
* A class that represents a set of classes and proxies.
|
||||||
internal val proxies: List<ClassProxy> = internalProxies
|
*
|
||||||
|
* @param classes The classes to be backed by proxies.
|
||||||
fun add(classDef: ClassDef) = internalClasses.add(classDef)
|
*/
|
||||||
fun add(classProxy: ClassProxy) = internalProxies.add(classProxy)
|
class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
|
||||||
|
internal val proxies = mutableListOf<ClassProxy>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply all resolved classes into [internalClasses] and clean the [proxies] list.
|
* Add a [ClassDef].
|
||||||
*/
|
*/
|
||||||
internal fun applyProxies() {
|
fun add(classDef: ClassDef) = classes.add(classDef)
|
||||||
// FIXME: check if this could cause issues when multiple patches use the same proxy
|
|
||||||
internalProxies.removeIf { proxy ->
|
|
||||||
// if the proxy is unused, keep it in the list
|
|
||||||
if (!proxy.proxyUsed) return@removeIf false
|
|
||||||
|
|
||||||
// if it has been used, replace the internal class which it proxied
|
/**
|
||||||
val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type }
|
* Add a [ClassProxy].
|
||||||
internalClasses[index] = proxy.mutatedClass
|
*/
|
||||||
|
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace all classes with their mutated versions.
|
||||||
|
*/
|
||||||
|
internal fun replaceClasses() =
|
||||||
|
proxies.removeIf { proxy ->
|
||||||
|
// if the proxy is unused, keep it in the list
|
||||||
|
if (!proxy.resolved) return@removeIf false
|
||||||
|
|
||||||
|
// if it has been used, replace the original class with the new class
|
||||||
|
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
|
||||||
|
classes[index] = proxy.mutableClass
|
||||||
|
|
||||||
// return true to remove it from the proxies list
|
// return true to remove it from the proxies list
|
||||||
return@removeIf true
|
return@removeIf true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override val size get() = internalClasses.size
|
|
||||||
override fun contains(element: ClassDef) = internalClasses.contains(element)
|
override val size get() = classes.size
|
||||||
override fun containsAll(elements: Collection<ClassDef>) = internalClasses.containsAll(elements)
|
override fun contains(element: ClassDef) = classes.contains(element)
|
||||||
override fun get(index: Int) = internalClasses[index]
|
override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
|
||||||
override fun indexOf(element: ClassDef) = internalClasses.indexOf(element)
|
override fun isEmpty() = classes.isEmpty()
|
||||||
override fun isEmpty() = internalClasses.isEmpty()
|
override fun iterator() = classes.iterator()
|
||||||
override fun iterator() = internalClasses.iterator()
|
|
||||||
override fun lastIndexOf(element: ClassDef) = internalClasses.lastIndexOf(element)
|
|
||||||
override fun listIterator() = internalClasses.listIterator()
|
|
||||||
override fun listIterator(index: Int) = internalClasses.listIterator(index)
|
|
||||||
override fun subList(fromIndex: Int, toIndex: Int) = internalClasses.subList(fromIndex, toIndex)
|
|
||||||
}
|
}
|
18
src/main/kotlin/app/revanced/patcher/util/VersionReader.kt
Normal file
18
src/main/kotlin/app/revanced/patcher/util/VersionReader.kt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package app.revanced.patcher.util
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal object VersionReader {
|
||||||
|
@JvmStatic
|
||||||
|
private val props = Properties().apply {
|
||||||
|
load(
|
||||||
|
VersionReader::class.java.getResourceAsStream("/revanced-patcher/version.properties")
|
||||||
|
?: throw IllegalStateException("Could not load version.properties")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun read(): String {
|
||||||
|
return props.getProperty("version") ?: throw IllegalStateException("Version not found")
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user