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
121 Commits
v14.0.0-de
...
v18.0.0-de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
079de45238 | ||
![]() |
56ce9ec2f9 | ||
![]() |
1b52e4b0f9 | ||
![]() |
098c2c1efa | ||
![]() |
64343e5a7c | ||
![]() |
0caf6caeb9 | ||
![]() |
55f6c2a9fc | ||
![]() |
c6095bc38a | ||
![]() |
09cd6aa568 | ||
![]() |
ebbaafb78e | ||
![]() |
e6de90d300 | ||
![]() |
c7922e90d0 | ||
![]() |
c1f4c0445a | ||
![]() |
caa634fac6 | ||
![]() |
f28bfe0dbd | ||
![]() |
155e787ff4 | ||
![]() |
c38f0ef42a | ||
![]() |
4456031459 | ||
![]() |
5fb59a227f | ||
![]() |
d9fb241d57 | ||
![]() |
642c4ea97e | ||
![]() |
8c8a251626 | ||
![]() |
5953d6cfb5 | ||
![]() |
a1962fe600 | ||
![]() |
77dbee3d6a | ||
![]() |
cb5e39d73e | ||
![]() |
38ef2f470a | ||
![]() |
129d84e108 | ||
![]() |
affeba76b8 | ||
![]() |
6059d3ca26 | ||
![]() |
444dee5a16 | ||
![]() |
d314466ce2 | ||
![]() |
fdaf9c21c8 | ||
![]() |
06c2b76f11 | ||
![]() |
3896b30738 | ||
![]() |
2c4b88e1a0 | ||
![]() |
dfc7e1596b | ||
![]() |
f590436399 | ||
![]() |
cbfb9ba02f | ||
![]() |
b4cfe80ad5 | ||
![]() |
b37906fa35 | ||
![]() |
356f1f1553 | ||
![]() |
e882af74ee | ||
![]() |
46875fb28e | ||
![]() |
417c3e4234 | ||
![]() |
6d2c28807b | ||
![]() |
4d6e08a650 | ||
![]() |
5cebc1fd30 | ||
![]() |
ac61731dc6 | ||
![]() |
9e4ffabd5c | ||
![]() |
3f410bd39f | ||
![]() |
d51bc32e37 | ||
![]() |
b7f6aa94cc | ||
![]() |
ff965e6953 | ||
![]() |
468d5d7421 | ||
![]() |
fc95b28c49 | ||
![]() |
69184187d9 | ||
![]() |
a802d0df46 | ||
![]() |
8de30633ae | ||
![]() |
a1fbb7990f | ||
![]() |
aa71146b1b | ||
![]() |
9fdb8f087f | ||
![]() |
670f0153de | ||
![]() |
1d7aeca696 | ||
![]() |
4e7811ea07 | ||
![]() |
e11283744a | ||
![]() |
91cdfd53ef | ||
![]() |
bc7d6b9941 | ||
![]() |
6b1e0a1656 | ||
![]() |
72c9eb2129 | ||
![]() |
4bc4b0dc01 | ||
![]() |
637d48746f | ||
![]() |
9a109c129b | ||
![]() |
d49e4ee5ea | ||
![]() |
30f0ea29a3 | ||
![]() |
49930f6565 | ||
![]() |
909d89fa8d | ||
![]() |
81d1d7f544 | ||
![]() |
67b7dff67a | ||
![]() |
4b76d19596 | ||
![]() |
080fbe9feb | ||
![]() |
d3721229bf | ||
![]() |
86c1c9c772 | ||
![]() |
c299817193 | ||
![]() |
fcc1de45ed | ||
![]() |
a29931f2ec | ||
![]() |
3fc6a139ee | ||
![]() |
4dd04975d9 | ||
![]() |
3b4db3ddb7 | ||
![]() |
c4a7117ee8 | ||
![]() |
b4e900fde8 | ||
![]() |
9818d730e4 | ||
![]() |
11a3378659 | ||
![]() |
1bb05f22d3 | ||
![]() |
26b70554c4 | ||
![]() |
93b29d2e83 | ||
![]() |
072986374a | ||
![]() |
2c590d212a | ||
![]() |
6cc863efb3 | ||
![]() |
b832812767 | ||
![]() |
c44558cacd | ||
![]() |
6d83a720cd | ||
![]() |
8d0dd9c448 | ||
![]() |
64020eec49 | ||
![]() |
4dedfb85cb | ||
![]() |
55d694579a | ||
![]() |
86db64edff | ||
![]() |
983563efb6 | ||
![]() |
37abb2db99 | ||
![]() |
5ba0b47e60 | ||
![]() |
e8f2087a6f | ||
![]() |
6ce99f5cdf | ||
![]() |
13c0c9cdd3 | ||
![]() |
58ffdb60d7 | ||
![]() |
ba56a6a2ee | ||
![]() |
ccccf5b1d2 | ||
![]() |
b507ac0a54 | ||
![]() |
e985676c2d | ||
![]() |
f7f4ba6c55 | ||
![]() |
4292f43814 | ||
![]() |
30bd4fd9fe |
6
.github/workflows/pull_request.yml
vendored
6
.github/workflows/pull_request.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: PR to main
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
|
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew clean --no-daemon
|
||||
run: ./gradlew build clean --no-daemon
|
||||
- name: Setup semantic-release
|
||||
run: npm install
|
||||
- name: Release
|
||||
|
387
CHANGELOG.md
387
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
842
api/revanced-patcher.api
Normal file
842
api/revanced-patcher.api
Normal file
File diff suppressed because it is too large
Load Diff
108
build.gradle.kts
108
build.gradle.kts
@@ -1,54 +1,48 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.8.20"
|
||||
kotlin("jvm") version "1.9.0"
|
||||
alias(libs.plugins.binary.compatibility.validator)
|
||||
`maven-publish`
|
||||
signing
|
||||
java
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
|
||||
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
mavenLocal()
|
||||
listOf("multidexlib2", "apktool").forEach { repo ->
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/$repo")
|
||||
credentials {
|
||||
username = githubUsername
|
||||
password = githubPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||
implementation("xpp3:xpp3:1.1.4c")
|
||||
implementation("com.android.tools.smali:smali:3.0.3")
|
||||
implementation("app.revanced:multidexlib2:3.0.3.r2")
|
||||
implementation("app.revanced:apktool-lib:2.8.2-5")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22")
|
||||
|
||||
compileOnly("com.google.android:android:4.1.1.4")
|
||||
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
||||
}
|
||||
|
||||
tasks {
|
||||
processResources {
|
||||
expand("projectVersion" to project.version)
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("PASSED", "SKIPPED", "FAILED")
|
||||
}
|
||||
}
|
||||
processResources {
|
||||
expand("projectVersion" to project.version)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.xpp3)
|
||||
implementation(libs.smali)
|
||||
implementation(libs.multidexlib2)
|
||||
implementation(libs.apktool.lib)
|
||||
implementation(libs.kotlin.reflect)
|
||||
|
||||
compileOnly(libs.android)
|
||||
|
||||
testImplementation(libs.kotlin.test)
|
||||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
@@ -57,22 +51,36 @@ kotlin {
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("GITHUB_ACTOR") != null)
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
publications {
|
||||
create<MavenPublication>("revanced-patcher-publication") {
|
||||
from(components["java"])
|
||||
|
||||
version = project.version.toString()
|
||||
|
||||
pom {
|
||||
name = "ReVanced Patcher"
|
||||
description = "Patcher used by ReVanced."
|
||||
url = "https://revanced.app"
|
||||
|
||||
licenses {
|
||||
license {
|
||||
name = "GNU General Public License v3.0"
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "ReVanced"
|
||||
name = "ReVanced"
|
||||
email = "contact@revanced.app"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
|
||||
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
|
||||
url = "https://github.com/revanced/revanced-patcher"
|
||||
}
|
||||
}
|
||||
else
|
||||
mavenLocal()
|
||||
}
|
||||
publications {
|
||||
register<MavenPublication>("gpr") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
kotlin.code.style = official
|
||||
version = 14.0.0-dev.4
|
||||
version = 18.0.0-dev.3
|
||||
|
23
gradle/libs.versions.toml
Normal file
23
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[versions]
|
||||
android = "4.1.1.4"
|
||||
kotlin-reflect = "1.9.0"
|
||||
apktool-lib = "2.9.1"
|
||||
kotlin-test = "1.8.20-RC"
|
||||
kotlinx-coroutines-core = "1.7.3"
|
||||
multidexlib2 = "3.0.3.r3"
|
||||
smali = "3.0.3"
|
||||
xpp3 = "1.1.4c"
|
||||
binary-compatibility-validator = "0.13.2"
|
||||
|
||||
[libraries]
|
||||
android = { module = "com.google.android:android", version.ref = "android" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||
apktool-lib = { module = "app.revanced:apktool", version.ref = "apktool-lib" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
||||
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
||||
|
||||
[plugins]
|
||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
@@ -1 +1 @@
|
||||
rootProject.name = "revanced-patcher"
|
||||
rootProject.name = "revanced-patcher"
|
@@ -2,33 +2,88 @@
|
||||
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchClass
|
||||
import dalvik.system.DexClassLoader
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* A patch bundle.
|
||||
* A set of [Patch]es.
|
||||
*/
|
||||
typealias PatchSet = Set<Patch<*>>
|
||||
|
||||
/**
|
||||
* A [Patch] class.
|
||||
*/
|
||||
typealias PatchClass = KClass<out Patch<*>>
|
||||
|
||||
/**
|
||||
* A loader of [Patch]es from patch bundles.
|
||||
* This will load all [Patch]es from the given patch bundles that have a name.
|
||||
*
|
||||
* @param fromClasses The classes to get [Patch]es from.
|
||||
* @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
|
||||
* @param classLoader The [ClassLoader] to use for loading the classes.
|
||||
* @param patchBundles A set of patches to initialize this instance with.
|
||||
*/
|
||||
sealed class PatchBundleLoader private constructor(
|
||||
fromClasses: Iterable<Class<*>>
|
||||
) : MutableList<PatchClass> by mutableListOf() {
|
||||
init {
|
||||
fromClasses.filter {
|
||||
if (it.isAnnotation) return@filter false
|
||||
classLoader: ClassLoader,
|
||||
patchBundles: Array<out File>,
|
||||
getBinaryClassNames: (patchBundle: File) -> List<String>,
|
||||
// This constructor parameter is unfortunately necessary,
|
||||
// so that a reference to the mutable set is present in the constructor to be able to add patches to it.
|
||||
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
|
||||
private val patchSet: MutableSet<Patch<*>> = mutableSetOf()
|
||||
) : PatchSet by patchSet {
|
||||
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
|
||||
|
||||
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null
|
||||
}.map {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
it as PatchClass
|
||||
}.let { addAll(it) }
|
||||
init {
|
||||
patchBundles.flatMap(getBinaryClassNames).asSequence().map {
|
||||
classLoader.loadClass(it)
|
||||
}.filter {
|
||||
Patch::class.java.isAssignableFrom(it)
|
||||
}.mapNotNull { patchClass ->
|
||||
patchClass.getInstance(logger, silent = true)
|
||||
}.filter {
|
||||
it.name != null
|
||||
}.let { patches ->
|
||||
patchSet.addAll(patches)
|
||||
}
|
||||
}
|
||||
|
||||
internal companion object Utils {
|
||||
/**
|
||||
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.
|
||||
*
|
||||
* @param logger The [Logger] to use for logging.
|
||||
* @param silent Whether to suppress logging.
|
||||
* @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated.
|
||||
*/
|
||||
internal fun Class<*>.getInstance(logger: Logger, silent: Boolean = false): Patch<*>? {
|
||||
return try {
|
||||
getField("INSTANCE").get(null)
|
||||
} catch (exception: NoSuchFieldException) {
|
||||
if (!silent) logger.fine(
|
||||
"Patch class '${name}' has no INSTANCE field, therefor not a singleton. " +
|
||||
"Will try to instantiate it."
|
||||
)
|
||||
|
||||
try {
|
||||
getDeclaredConstructor().newInstance()
|
||||
} catch (exception: Exception) {
|
||||
if (!silent) logger.severe(
|
||||
"Patch class '${name}' is not singleton and has no suitable constructor, " +
|
||||
"therefor cannot be instantiated and will be ignored."
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
} as Patch<*>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,36 +91,37 @@ sealed class PatchBundleLoader private constructor(
|
||||
*
|
||||
* @param patchBundles The path to patch bundles of JAR format.
|
||||
*/
|
||||
class Jar(vararg patchBundles: File) :
|
||||
PatchBundleLoader(with(URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray())) {
|
||||
patchBundles.flatMap { patchBundle ->
|
||||
// Get the names of all classes in the DEX file.
|
||||
|
||||
JarFile(patchBundle).entries().asSequence()
|
||||
.filter { it.name.endsWith(".class") }
|
||||
.map { it.name.replace('/', '.').replace(".class", "") }
|
||||
.map { loadClass(it) }
|
||||
}
|
||||
})
|
||||
class Jar(vararg patchBundles: File) : PatchBundleLoader(
|
||||
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
patchBundles,
|
||||
{ patchBundle ->
|
||||
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
|
||||
.map { it.name.replace('/', '.').replace(".class", "") }
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* A [PatchBundleLoader] for [Dex] files.
|
||||
*
|
||||
* @param patchBundles The path to patch bundles of DEX format.
|
||||
* @param optimizedDexDirectory The directory to store optimized DEX files in.
|
||||
* This parameter is deprecated and has no effect since API level 26.
|
||||
*/
|
||||
class Dex(vararg patchBundles: File) : PatchBundleLoader(with(
|
||||
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
|
||||
DexClassLoader(
|
||||
patchBundles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
null,
|
||||
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath,
|
||||
null,
|
||||
PatchBundleLoader::class.java.classLoader
|
||||
)
|
||||
),
|
||||
patchBundles,
|
||||
{ patchBundle ->
|
||||
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||
.map { classDef ->
|
||||
classDef.type.substring(1, classDef.length - 1)
|
||||
}
|
||||
}
|
||||
) {
|
||||
patchBundles
|
||||
.flatMap {
|
||||
MultiDexIO.readDexFile(true, it, BasicDexFileNamer(), null, null).classes
|
||||
}
|
||||
.map { classDef -> classDef.type.substring(1, classDef.length - 1) }
|
||||
.map { loadClass(it) }
|
||||
})
|
||||
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
|
||||
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@ package app.revanced.patcher
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchClass
|
||||
import brut.androlib.apk.ApkInfo
|
||||
import brut.directory.ExtFile
|
||||
|
||||
@@ -19,9 +18,14 @@ class PatcherContext internal constructor(options: PatcherOptions) {
|
||||
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
|
||||
|
||||
/**
|
||||
* The list of [Patch]es to execute.
|
||||
* The map of [Patch]es associated by their [PatchClass].
|
||||
*/
|
||||
internal val patches = mutableListOf<PatchClass>()
|
||||
internal val executablePatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
|
||||
/**
|
||||
* The map of all [Patch]es and their dependencies associated by their [PatchClass].
|
||||
*/
|
||||
internal val allPatches = mutableMapOf<PatchClass, Patch<*>>()
|
||||
|
||||
/**
|
||||
* The [ResourceContext] of this [PatcherContext].
|
||||
|
16
src/main/kotlin/app/revanced/patcher/PatcherException.kt
Normal file
16
src/main/kotlin/app/revanced/patcher/PatcherException.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
/**
|
||||
* An exception thrown by ReVanced [Patcher].
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
* @param cause The corresponding [Throwable].
|
||||
*/
|
||||
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||
constructor(errorMessage: String) : this(errorMessage, null)
|
||||
|
||||
|
||||
class CircularDependencyException internal constructor(dependant: String) : PatcherException(
|
||||
"Patch '$dependant' causes a circular dependency"
|
||||
)
|
||||
}
|
@@ -1,10 +1,9 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.logging.impl.NopLogger
|
||||
import brut.androlib.Config
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* Options for ReVanced [Patcher].
|
||||
@@ -12,15 +11,18 @@ import java.io.File
|
||||
* @param resourceCachePath The path to the directory to use for caching resources.
|
||||
* @param aaptBinaryPath The path to a custom aapt binary.
|
||||
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
||||
* @param logger A [Logger].
|
||||
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
||||
* This can impact memory usage.
|
||||
*/
|
||||
data class PatcherOptions(
|
||||
internal val inputFile: File,
|
||||
internal val resourceCachePath: File = File("revanced-resource-cache"),
|
||||
internal val aaptBinaryPath: String? = null,
|
||||
internal val frameworkFileDirectory: String? = null,
|
||||
internal val logger: Logger = NopLogger
|
||||
internal val multithreadingDexFileWriter: Boolean = false,
|
||||
) {
|
||||
private val logger = Logger.getLogger(PatcherOptions::class.java.name)
|
||||
|
||||
/**
|
||||
* The mode to use for resource decoding.
|
||||
* @see ResourceContext.ResourceDecodingMode
|
||||
@@ -36,12 +38,33 @@ data class PatcherOptions(
|
||||
frameworkDirectory = frameworkFileDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for ReVanced [Patcher].
|
||||
* @param inputFile The input file to patch.
|
||||
* @param resourceCachePath The path to the directory to use for caching resources.
|
||||
* @param aaptBinaryPath The path to a custom aapt binary.
|
||||
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
||||
*/
|
||||
@Deprecated("Use the constructor with the multithreadingDexFileWriter parameter instead")
|
||||
constructor(
|
||||
inputFile: File,
|
||||
resourceCachePath: File = File("revanced-resource-cache"),
|
||||
aaptBinaryPath: String? = null,
|
||||
frameworkFileDirectory: String? = null,
|
||||
) : this(
|
||||
inputFile,
|
||||
resourceCachePath,
|
||||
aaptBinaryPath,
|
||||
frameworkFileDirectory,
|
||||
false,
|
||||
)
|
||||
|
||||
fun recreateResourceCacheDirectory() = resourceCachePath.also {
|
||||
if (it.exists()) {
|
||||
logger.info("Deleting existing resource cache directory")
|
||||
|
||||
if (!it.deleteRecursively())
|
||||
logger.error("Failed to delete existing resource cache directory")
|
||||
logger.severe("Failed to delete existing resource cache directory")
|
||||
}
|
||||
|
||||
it.mkdirs()
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.PatchClass
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
@FunctionalInterface
|
||||
interface PatchesConsumer {
|
||||
fun acceptPatches(patches: List<PatchClass>)
|
||||
fun acceptPatches(patches: List<Patch<*>>)
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
/**
|
||||
* Annotation to constrain a [Patch] to compatible packages.
|
||||
* @param compatiblePackages A list of packages a [Patch] is compatible with.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Compatibility(
|
||||
val compatiblePackages: Array<Package>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to represent packages a patch can be compatible with.
|
||||
* @param name The package identifier name.
|
||||
* @param versions The versions of the package the [Patch] is compatible with.
|
||||
*/
|
||||
@Target()
|
||||
annotation class Package(
|
||||
val name: String,
|
||||
val versions: Array<String> = [],
|
||||
)
|
@@ -1,21 +0,0 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
/**
|
||||
* Annotation to name a [Patch].
|
||||
* @param name A suggestive name for the [Patch].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Name(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to describe a [Patch].
|
||||
* @param description A description for the [Patch].
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Description(
|
||||
val description: String,
|
||||
)
|
@@ -1,8 +1,9 @@
|
||||
package app.revanced.patcher.data
|
||||
|
||||
import app.revanced.patcher.PatcherContext
|
||||
import app.revanced.patcher.PatcherOptions
|
||||
import app.revanced.patcher.PatcherResult
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import app.revanced.patcher.util.ProxyClassList
|
||||
import app.revanced.patcher.util.method.MethodWalker
|
||||
@@ -11,12 +12,13 @@ import com.android.tools.smali.dexlib2.Opcodes
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.io.FileFilter
|
||||
import java.io.Flushable
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for bytecode.
|
||||
@@ -26,6 +28,8 @@ import java.io.Flushable
|
||||
*/
|
||||
class BytecodeContext internal constructor(private val options: PatcherOptions) :
|
||||
Context<List<PatcherResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(BytecodeContext::class.java.name)
|
||||
|
||||
/**
|
||||
* [Opcodes] of the supplied [PatcherOptions.inputFile].
|
||||
*/
|
||||
@@ -45,7 +49,7 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
||||
/**
|
||||
* The [Integrations] of this [PatcherContext].
|
||||
*/
|
||||
internal val integrations = Integrations(options.logger)
|
||||
internal val integrations = Integrations()
|
||||
|
||||
/**
|
||||
* Find a class by a given class name.
|
||||
@@ -87,14 +91,42 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
||||
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
||||
|
||||
/**
|
||||
* The integrations of a [PatcherContext].
|
||||
* Compile bytecode from the [BytecodeContext].
|
||||
*
|
||||
* @param logger The logger to use.
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
internal inner class Integrations(private val logger: Logger) : MutableList<File> by mutableListOf(), Flushable {
|
||||
override fun get(): List<PatcherResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
val patchedDexFileResults = options.resourceCachePath.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
if (options.multithreadingDexFileWriter) -1 else 1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
|
||||
override fun getOpcodes() = this@BytecodeContext.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE
|
||||
) { _, entryName, _ -> logger.info("Compiled $entryName") }
|
||||
}.listFiles(FileFilter { it.isFile })!!.map { PatcherResult.PatchedDexFile(it.name, it.inputStream()) }
|
||||
|
||||
System.gc()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
|
||||
/**
|
||||
* The integrations of a [PatcherContext].
|
||||
*/
|
||||
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
||||
/**
|
||||
* Whether to merge integrations.
|
||||
* True when any supplied [Patch] is annotated with [RequiresIntegrations].
|
||||
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
|
||||
*/
|
||||
var merge = false
|
||||
|
||||
@@ -104,6 +136,10 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
||||
override fun flush() {
|
||||
if (!merge) return
|
||||
|
||||
logger.info("Merging integrations")
|
||||
|
||||
val classMap = classes.associateBy { it.type }
|
||||
|
||||
this@Integrations.forEach { integrations ->
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
@@ -111,42 +147,22 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
||||
null,
|
||||
null
|
||||
).classes.forEach classDef@{ classDef ->
|
||||
val existingClass = classes.find { it == classDef } ?: run {
|
||||
logger.trace("Merging $classDef")
|
||||
val existingClass = classMap[classDef.type] ?: run {
|
||||
logger.fine("Adding $classDef")
|
||||
classes.add(classDef)
|
||||
return@classDef
|
||||
}
|
||||
|
||||
logger.trace("$classDef exists. Adding missing methods and fields.")
|
||||
logger.fine("$classDef exists. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, this@BytecodeContext, logger).let { mergedClass ->
|
||||
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) return@let
|
||||
classes.apply { remove(existingClass); add(mergedClass) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodeContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
override fun get(): List<PatcherResult.PatchedDexFile> {
|
||||
options.logger.info("Compiling modified dex files")
|
||||
|
||||
return mutableMapOf<String, MemoryDataStore>().apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true, -1, // Defaults to amount of available cores.
|
||||
this, BasicDexFileNamer(), object : DexFile {
|
||||
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
|
||||
override fun getOpcodes() = this@BytecodeContext.opcodes
|
||||
}, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
||||
)
|
||||
}.map { PatcherResult.PatchedDexFile(it.key, it.value.readAt(0)) }
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
* A context for resources.
|
||||
@@ -27,6 +28,8 @@ class ResourceContext internal constructor(
|
||||
private val context: PatcherContext,
|
||||
private val options: PatcherOptions
|
||||
) : Context<File?>, Iterable<File> {
|
||||
private val logger = Logger.getLogger(ResourceContext::class.java.name)
|
||||
|
||||
val xmlEditor = XmlFileHolder()
|
||||
|
||||
/**
|
||||
@@ -42,7 +45,7 @@ class ResourceContext internal constructor(
|
||||
ResourceDecodingMode.FULL -> {
|
||||
val outDir = options.recreateResourceCacheDirectory()
|
||||
|
||||
options.logger.info("Decoding resources")
|
||||
logger.info("Decoding resources")
|
||||
|
||||
resourcesDecoder.decodeResources(outDir)
|
||||
resourcesDecoder.decodeManifest(outDir)
|
||||
@@ -57,7 +60,7 @@ class ResourceContext internal constructor(
|
||||
}
|
||||
|
||||
ResourceDecodingMode.MANIFEST_ONLY -> {
|
||||
options.logger.info("Decoding app manifest")
|
||||
logger.info("Decoding app manifest")
|
||||
|
||||
// Decode manually instead of using resourceDecoder.decodeManifest
|
||||
// because it does not support decoding to an OutputStream.
|
||||
@@ -80,6 +83,16 @@ class ResourceContext internal constructor(
|
||||
versionInfo.let {
|
||||
metadata.packageVersion = it.versionName ?: it.versionCode
|
||||
}
|
||||
|
||||
/*
|
||||
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
|
||||
because ResourcesDecoder.decodeResources loads the main package
|
||||
and not XmlPullStreamDecoder.decodeManifest.
|
||||
See ARSCDecoder.readTableType for more info.
|
||||
|
||||
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
|
||||
*/
|
||||
metadata.apkInfo.sparseResources = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,7 +113,7 @@ class ResourceContext internal constructor(
|
||||
var resourceFile: File? = null
|
||||
|
||||
if (options.resourceDecodingMode == ResourceDecodingMode.FULL) {
|
||||
options.logger.info("Compiling modified resources")
|
||||
logger.info("Compiling modified resources")
|
||||
|
||||
val cacheDirectory = ExtFile(options.resourceCachePath)
|
||||
val aaptFile = cacheDirectory.resolve("aapt_temp_file").also {
|
||||
|
@@ -5,13 +5,7 @@ import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
|
||||
object MethodFingerprintExtensions {
|
||||
|
||||
/**
|
||||
* The name of a [MethodFingerprint].
|
||||
*/
|
||||
val MethodFingerprint.name: String
|
||||
get() = this.javaClass.simpleName
|
||||
|
||||
// TODO: Make this a property.
|
||||
/**
|
||||
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
|
||||
*/
|
||||
|
@@ -1,64 +0,0 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import app.revanced.patcher.annotation.Compatibility
|
||||
import app.revanced.patcher.annotation.Description
|
||||
import app.revanced.patcher.annotation.Name
|
||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||
import app.revanced.patcher.patch.OptionsContainer
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchClass
|
||||
import app.revanced.patcher.patch.PatchOptions
|
||||
import app.revanced.patcher.patch.annotations.DependsOn
|
||||
import app.revanced.patcher.patch.annotations.RequiresIntegrations
|
||||
import kotlin.reflect.KVisibility
|
||||
import kotlin.reflect.full.companionObject
|
||||
import kotlin.reflect.full.companionObjectInstance
|
||||
|
||||
object PatchExtensions {
|
||||
/**
|
||||
* The name of a [Patch].
|
||||
*/
|
||||
val PatchClass.patchName: String
|
||||
get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
|
||||
|
||||
/**
|
||||
* Weather or not a [Patch] should be included.
|
||||
*/
|
||||
val PatchClass.include
|
||||
get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
|
||||
|
||||
/**
|
||||
* The description of a [Patch].
|
||||
*/
|
||||
val PatchClass.description
|
||||
get() = findAnnotationRecursively(Description::class)?.description
|
||||
|
||||
/**
|
||||
* The dependencies of a [Patch].
|
||||
*/
|
||||
val PatchClass.dependencies
|
||||
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
|
||||
|
||||
/**
|
||||
* The packages a [Patch] is compatible with.
|
||||
*/
|
||||
val PatchClass.compatiblePackages
|
||||
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
|
||||
|
||||
/**
|
||||
* Weather or not a [Patch] requires integrations.
|
||||
*/
|
||||
internal val PatchClass.requiresIntegrations
|
||||
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
|
||||
|
||||
/**
|
||||
* The options of a [Patch].
|
||||
*/
|
||||
val PatchClass.options: PatchOptions?
|
||||
get() = kotlin.companionObject?.let { cl ->
|
||||
if (cl.visibility != KVisibility.PUBLIC) return null
|
||||
kotlin.companionObjectInstance?.let {
|
||||
(it as? OptionsContainer)?.options
|
||||
}
|
||||
}
|
||||
}
|
@@ -159,10 +159,10 @@ abstract class MethodFingerprint(
|
||||
* - Faster: Specify [accessFlags], [returnType] and [parameters].
|
||||
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
|
||||
*/
|
||||
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
|
||||
internal fun Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
|
||||
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
|
||||
|
||||
for (fingerprint in this) {
|
||||
forEach { fingerprint ->
|
||||
fingerprint.resolveUsingLookupMap(context)
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package app.revanced.patcher.logging
|
||||
|
||||
@Deprecated("This will be removed in a future release")
|
||||
interface Logger {
|
||||
fun error(msg: String) {}
|
||||
fun warn(msg: String) {}
|
||||
|
@@ -2,4 +2,5 @@ package app.revanced.patcher.logging.impl
|
||||
|
||||
import app.revanced.patcher.logging.Logger
|
||||
|
||||
@Deprecated("This will be removed in a future release")
|
||||
object NopLogger : Logger
|
13
src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt
Normal file
13
src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
|
||||
/**
|
||||
* A ReVanced [Patch] that works on [BytecodeContext].
|
||||
*
|
||||
* @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed.
|
||||
*/
|
||||
abstract class BytecodePatch(
|
||||
internal val fingerprints : Set<MethodFingerprint> = emptySet(),
|
||||
) : Patch<BytecodeContext>()
|
@@ -1,18 +0,0 @@
|
||||
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,39 +1,106 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.data.BytecodeContext
|
||||
import app.revanced.patcher.PatchClass
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||
import app.revanced.patcher.patch.options.PatchOptions
|
||||
import java.io.Closeable
|
||||
|
||||
typealias PatchClass = Class<out Patch<Context<*>>>
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
|
||||
/**
|
||||
* A ReVanced patch.
|
||||
* If an implementation of [Patch] also implements [Closeable]
|
||||
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
|
||||
*
|
||||
* If it implements [Closeable], it will be closed after all patches have been executed.
|
||||
* Closing will be done in reverse execution order.
|
||||
* @param T The [Context] type this patch will work on.
|
||||
*/
|
||||
sealed interface Patch<out T : Context<*>> {
|
||||
sealed class Patch<out T : Context<*>> {
|
||||
/**
|
||||
* The main function of the [Patch] which the patcher will call.
|
||||
* The name of the patch.
|
||||
*/
|
||||
var name: String? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The description of the patch.
|
||||
*/
|
||||
var description: String? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* The packages the patch is compatible with.
|
||||
*/
|
||||
var compatiblePackages: Set<CompatiblePackage>? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Other patches this patch depends on.
|
||||
*/
|
||||
var dependencies: Set<PatchClass>? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Weather or not the patch should be used.
|
||||
*/
|
||||
var use = true
|
||||
private set
|
||||
|
||||
|
||||
// TODO: Remove this property, once integrations are coupled with patches.
|
||||
/**
|
||||
* Weather or not the patch requires integrations.
|
||||
*/
|
||||
var requiresIntegrations = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* The options of the patch associated by the options key.
|
||||
*/
|
||||
val options = PatchOptions()
|
||||
|
||||
init {
|
||||
this::class.findAnnotation<app.revanced.patcher.patch.annotation.Patch>()?.let { annotation ->
|
||||
name = annotation.name.ifEmpty { null }
|
||||
description = annotation.description.ifEmpty { null }
|
||||
compatiblePackages = annotation.compatiblePackages
|
||||
.map { CompatiblePackage(it.name, it.versions.toSet()) }.toSet()
|
||||
dependencies = annotation.dependencies.toSet().ifEmpty { null }
|
||||
use = annotation.use
|
||||
requiresIntegrations = annotation.requiresIntegrations
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The execution function of the patch.
|
||||
*
|
||||
* @param context The [Context] the patch will work on.
|
||||
* @return The result of executing the patch.
|
||||
*/
|
||||
fun execute(context: @UnsafeVariance T)
|
||||
}
|
||||
abstract fun execute(context: @UnsafeVariance T)
|
||||
|
||||
/**
|
||||
* Resource patch for the Patcher.
|
||||
*/
|
||||
interface ResourcePatch : Patch<ResourceContext>
|
||||
override fun hashCode() = name.hashCode()
|
||||
|
||||
/**
|
||||
* 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>
|
||||
override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch"
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Patch<*>
|
||||
|
||||
return name == other.name
|
||||
}
|
||||
|
||||
/**
|
||||
* A package a [Patch] is compatible with.
|
||||
*
|
||||
* @param name The name of the package.
|
||||
* @param versions The versions of the package.
|
||||
*/
|
||||
class CompatiblePackage(
|
||||
val name: String,
|
||||
val versions: Set<String>? = null,
|
||||
)
|
||||
}
|
@@ -1,232 +0,0 @@
|
||||
@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`_
|
||||
* @param default The default value of the option.
|
||||
* @param title A human-readable title of the option. Example: _Patch Settings_
|
||||
* @param description A human-readable description of the option. Example: _Settings for the patches._
|
||||
* @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
|
||||
)
|
||||
}
|
@@ -3,8 +3,7 @@ package app.revanced.patcher.patch
|
||||
/**
|
||||
* A result of executing a [Patch].
|
||||
*
|
||||
* @param patchName The name of the [Patch].
|
||||
* @param patch The [Patch] that was executed.
|
||||
* @param exception The [PatchException] thrown, if any.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null)
|
||||
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
|
@@ -0,0 +1,8 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.data.ResourceContext
|
||||
|
||||
/**
|
||||
* A ReVanced [Patch] that works on [ResourceContext].
|
||||
*/
|
||||
abstract class ResourcePatch : Patch<ResourceContext>()
|
@@ -0,0 +1,37 @@
|
||||
package app.revanced.patcher.patch.annotation
|
||||
|
||||
import java.lang.annotation.Inherited
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Annotation for [app.revanced.patcher.patch.Patch] classes.
|
||||
*
|
||||
* @param name The name of the patch. If empty, the patch will be unnamed.
|
||||
* @param description The description of the patch. If empty, no description will be used.
|
||||
* @param dependencies The patches this patch depends on.
|
||||
* @param compatiblePackages The packages this patch is compatible with.
|
||||
* @param use Whether this patch should be used.
|
||||
* @param requiresIntegrations Whether this patch requires integrations.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Inherited
|
||||
annotation class Patch(
|
||||
val name: String = "",
|
||||
val description: String = "",
|
||||
val dependencies: Array<KClass<out app.revanced.patcher.patch.Patch<*>>> = [],
|
||||
val compatiblePackages: Array<CompatiblePackage> = [],
|
||||
val use: Boolean = true,
|
||||
// TODO: Remove this property, once integrations are coupled with patches.
|
||||
val requiresIntegrations: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* A package that a [app.revanced.patcher.patch.Patch] is compatible with.
|
||||
*
|
||||
* @param name The name of the package.
|
||||
* @param versions The versions of the package.
|
||||
*/
|
||||
annotation class CompatiblePackage(
|
||||
val name: String,
|
||||
val versions: Array<String> = [],
|
||||
)
|
@@ -1,27 +0,0 @@
|
||||
package app.revanced.patcher.patch.annotations
|
||||
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Annotation to mark a class as a patch.
|
||||
* @param include If false, the patch should be treated as optional by default.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Patch(val include: Boolean = true)
|
||||
|
||||
/**
|
||||
* Annotation for dependencies of [Patch]es.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class DependsOn(
|
||||
val dependencies: Array<KClass<out Patch<Context<*>>>> = []
|
||||
)
|
||||
|
||||
// TODO: Remove this annotation, once integrations are coupled with patches.
|
||||
/**
|
||||
* Annotation to mark [Patch]es which depend on integrations.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class RequiresIntegrations
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user