You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-10 05:30:49 +02:00
Compare commits
104 Commits
v14.2.2
...
v18.0.0-de
Author | SHA1 | Date | |
---|---|---|---|
![]() |
167bd83f4e | ||
![]() |
aed1eac315 | ||
![]() |
54a2f8f16f | ||
![]() |
2ca543ffb9 | ||
![]() |
58e7f815a5 | ||
![]() |
15b38fc841 | ||
![]() |
e2ca50729d | ||
![]() |
6192089b71 | ||
![]() |
a4212f6bf9 | ||
![]() |
124a2e9d3e | ||
![]() |
f77624b3b9 | ||
![]() |
a76ac04214 | ||
![]() |
0447fa9c28 | ||
![]() |
54ac1394a9 | ||
![]() |
0b04c73ac5 | ||
![]() |
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 |
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:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pull-request:
|
pull-request:
|
||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
uses: repo-sync/pull-request@v2
|
uses: repo-sync/pull-request@v2
|
||||||
with:
|
with:
|
||||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Make sure the release step uses its own credentials:
|
# Make sure the release step uses its own credentials:
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||||
|
317
CHANGELOG.md
317
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,33 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.8.20"
|
kotlin("jvm") version "1.9.10"
|
||||||
`maven-publish`
|
|
||||||
alias(libs.plugins.binary.compatibility.validator)
|
alias(libs.plugins.binary.compatibility.validator)
|
||||||
|
`maven-publish`
|
||||||
|
signing
|
||||||
|
java
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "app.revanced"
|
group = "app.revanced"
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation(libs.xpp3)
|
implementation(libs.xpp3)
|
||||||
@@ -19,40 +41,46 @@ dependencies {
|
|||||||
testImplementation(libs.kotlin.test)
|
testImplementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
testLogging {
|
|
||||||
events("PASSED", "SKIPPED", "FAILED")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processResources {
|
|
||||||
expand("projectVersion" to project.version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin { jvmToolchain(11) }
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
withJavadocJar()
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
publications {
|
||||||
mavenLocal()
|
create<MavenPublication>("revanced-patcher-publication") {
|
||||||
maven {
|
from(components["java"])
|
||||||
name = "GitHubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
version = project.version.toString()
|
||||||
credentials {
|
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
pom {
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
publications {
|
}
|
||||||
create<MavenPublication>("gpr") {
|
|
||||||
from(components["java"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +1,4 @@
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
org.gradle.caching = true
|
org.gradle.caching = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 14.2.2
|
version = 18.0.0-dev.6
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
[versions]
|
[versions]
|
||||||
android = "4.1.1.4"
|
android = "4.1.1.4"
|
||||||
kotlin-reflect = "1.9.0"
|
kotlin-reflect = "1.9.0"
|
||||||
apktool-lib = "2.8.2-6"
|
apktool-lib = "2.9.1"
|
||||||
kotlin-test = "1.8.20-RC"
|
kotlin-test = "1.8.20-RC"
|
||||||
kotlinx-coroutines-core = "1.7.1"
|
kotlinx-coroutines-core = "1.7.3"
|
||||||
multidexlib2 = "3.0.3.r2"
|
multidexlib2 = "3.0.3.r3"
|
||||||
smali = "3.0.3"
|
smali = "3.0.3"
|
||||||
xpp3 = "1.1.4c"
|
xpp3 = "1.1.4c"
|
||||||
binary-compatibility-validator = "0.13.2"
|
binary-compatibility-validator = "0.13.2"
|
||||||
@@ -12,7 +12,7 @@ binary-compatibility-validator = "0.13.2"
|
|||||||
[libraries]
|
[libraries]
|
||||||
android = { module = "com.google.android:android", version.ref = "android" }
|
android = { module = "com.google.android:android", version.ref = "android" }
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||||
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
|
apktool-lib = { module = "app.revanced:apktool", version.ref = "apktool-lib" }
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
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" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||||
|
@@ -1,22 +1 @@
|
|||||||
val githubUsername: String = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
rootProject.name = "revanced-patcher"
|
||||||
val githubPassword: String = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
google()
|
|
||||||
mavenLocal()
|
|
||||||
listOf("multidexlib2", "apktool").forEach { repo ->
|
|
||||||
maven {
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/$repo")
|
|
||||||
credentials {
|
|
||||||
username = githubUsername
|
|
||||||
password = githubPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "revanced-patcher"
|
|
@@ -2,37 +2,88 @@
|
|||||||
|
|
||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.PatchClass
|
|
||||||
import dalvik.system.DexClassLoader
|
import dalvik.system.DexClassLoader
|
||||||
import lanchon.multidexlib2.BasicDexFileNamer
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
import lanchon.multidexlib2.MultiDexIO
|
import lanchon.multidexlib2.MultiDexIO
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URLClassLoader
|
import java.net.URLClassLoader
|
||||||
import java.util.jar.JarFile
|
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 getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
|
||||||
* @param fromClasses The classes to get [Patch]es from.
|
* @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(
|
sealed class PatchBundleLoader private constructor(
|
||||||
fromClasses: Iterable<Class<*>>
|
classLoader: ClassLoader,
|
||||||
) : MutableList<PatchClass> by mutableListOf() {
|
patchBundles: Array<out File>,
|
||||||
init {
|
getBinaryClassNames: (patchBundle: File) -> List<String>,
|
||||||
fromClasses.filter {
|
// This constructor parameter is unfortunately necessary,
|
||||||
if (it.isAnnotation) return@filter false
|
// 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
|
init {
|
||||||
}.map {
|
patchBundles.flatMap(getBinaryClassNames).asSequence().map {
|
||||||
@Suppress("UNCHECKED_CAST")
|
classLoader.loadClass(it)
|
||||||
it as PatchClass
|
}.filter {
|
||||||
}.sortedBy {
|
Patch::class.java.isAssignableFrom(it)
|
||||||
it.patchName
|
}.mapNotNull { patchClass ->
|
||||||
}.let { addAll(it) }
|
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<*>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,18 +92,13 @@ sealed class PatchBundleLoader private constructor(
|
|||||||
* @param patchBundles The path to patch bundles of JAR format.
|
* @param patchBundles The path to patch bundles of JAR format.
|
||||||
*/
|
*/
|
||||||
class Jar(vararg patchBundles: File) : PatchBundleLoader(
|
class Jar(vararg patchBundles: File) : PatchBundleLoader(
|
||||||
with(
|
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()),
|
||||||
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray())
|
patchBundles,
|
||||||
) {
|
{ patchBundle ->
|
||||||
patchBundles.flatMap { patchBundle ->
|
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
|
||||||
// Get the names of all classes in the DEX file.
|
.map { it.name.replace('/', '.').replace(".class", "") }
|
||||||
|
}
|
||||||
JarFile(patchBundle).entries().asSequence()
|
)
|
||||||
.filter { it.name.endsWith(".class") }
|
|
||||||
.map { it.name.replace('/', '.').replace(".class", "") }
|
|
||||||
.map { loadClass(it) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [PatchBundleLoader] for [Dex] files.
|
* A [PatchBundleLoader] for [Dex] files.
|
||||||
@@ -62,20 +108,19 @@ sealed class PatchBundleLoader private constructor(
|
|||||||
* This parameter is deprecated and has no effect since API level 26.
|
* This parameter is deprecated and has no effect since API level 26.
|
||||||
*/
|
*/
|
||||||
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
|
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
|
||||||
with(
|
DexClassLoader(
|
||||||
DexClassLoader(
|
|
||||||
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath,
|
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath,
|
||||||
null,
|
null,
|
||||||
PatchBundleLoader::class.java.classLoader
|
PatchBundleLoader::class.java.classLoader
|
||||||
)
|
),
|
||||||
) {
|
patchBundles,
|
||||||
patchBundles
|
{ patchBundle ->
|
||||||
.flatMap {
|
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||||
MultiDexIO.readDexFile(true, it, BasicDexFileNamer(), null, null).classes
|
.map { classDef ->
|
||||||
}
|
classDef.type.substring(1, classDef.length - 1)
|
||||||
.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.")
|
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
|
||||||
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)
|
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.BytecodeContext
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.data.ResourceContext
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.PatchClass
|
|
||||||
import brut.androlib.apk.ApkInfo
|
import brut.androlib.apk.ApkInfo
|
||||||
import brut.directory.ExtFile
|
import brut.directory.ExtFile
|
||||||
|
|
||||||
@@ -19,9 +18,14 @@ class PatcherContext internal constructor(options: PatcherOptions) {
|
|||||||
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
|
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].
|
* 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,7 +1,6 @@
|
|||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.data.ResourceContext
|
||||||
import app.revanced.patcher.logging.impl.NopLogger
|
|
||||||
import brut.androlib.Config
|
import brut.androlib.Config
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
@@ -12,16 +11,15 @@ import java.util.logging.Logger
|
|||||||
* @param resourceCachePath The path to the directory to use for caching resources.
|
* @param resourceCachePath The path to the directory to use for caching resources.
|
||||||
* @param aaptBinaryPath The path to a custom aapt binary.
|
* @param aaptBinaryPath The path to a custom aapt binary.
|
||||||
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
||||||
* @param unusedLogger The logger to use for logging.
|
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
|
||||||
|
* This can impact memory usage.
|
||||||
*/
|
*/
|
||||||
data class PatcherOptions
|
data class PatcherOptions(
|
||||||
@Deprecated("Use the constructor without the logger parameter instead")
|
|
||||||
constructor(
|
|
||||||
internal val inputFile: File,
|
internal val inputFile: File,
|
||||||
internal val resourceCachePath: File = File("revanced-resource-cache"),
|
internal val resourceCachePath: File = File("revanced-resource-cache"),
|
||||||
internal val aaptBinaryPath: String? = null,
|
internal val aaptBinaryPath: String? = null,
|
||||||
internal val frameworkFileDirectory: String? = null,
|
internal val frameworkFileDirectory: String? = null,
|
||||||
internal val unusedLogger: app.revanced.patcher.logging.Logger = NopLogger
|
internal val multithreadingDexFileWriter: Boolean = false,
|
||||||
) {
|
) {
|
||||||
private val logger = Logger.getLogger(PatcherOptions::class.java.name)
|
private val logger = Logger.getLogger(PatcherOptions::class.java.name)
|
||||||
|
|
||||||
@@ -40,26 +38,6 @@ constructor(
|
|||||||
frameworkDirectory = frameworkFileDirectory
|
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.
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
inputFile: File,
|
|
||||||
resourceCachePath: File = File("revanced-resource-cache"),
|
|
||||||
aaptBinaryPath: String? = null,
|
|
||||||
frameworkFileDirectory: String? = null,
|
|
||||||
) : this(
|
|
||||||
inputFile,
|
|
||||||
resourceCachePath,
|
|
||||||
aaptBinaryPath,
|
|
||||||
frameworkFileDirectory,
|
|
||||||
NopLogger
|
|
||||||
)
|
|
||||||
|
|
||||||
fun recreateResourceCacheDirectory() = resourceCachePath.also {
|
fun recreateResourceCacheDirectory() = resourceCachePath.also {
|
||||||
if (it.exists()) {
|
if (it.exists()) {
|
||||||
logger.info("Deleting existing resource cache directory")
|
logger.info("Deleting existing resource cache directory")
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.patch.PatchClass
|
import app.revanced.patcher.patch.Patch
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
interface PatchesConsumer {
|
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,
|
|
||||||
)
|
|
@@ -4,7 +4,6 @@ import app.revanced.patcher.PatcherContext
|
|||||||
import app.revanced.patcher.PatcherOptions
|
import app.revanced.patcher.PatcherOptions
|
||||||
import app.revanced.patcher.PatcherResult
|
import app.revanced.patcher.PatcherResult
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.annotations.RequiresIntegrations
|
|
||||||
import app.revanced.patcher.util.ClassMerger.merge
|
import app.revanced.patcher.util.ClassMerger.merge
|
||||||
import app.revanced.patcher.util.ProxyClassList
|
import app.revanced.patcher.util.ProxyClassList
|
||||||
import app.revanced.patcher.util.method.MethodWalker
|
import app.revanced.patcher.util.method.MethodWalker
|
||||||
@@ -13,11 +12,11 @@ import com.android.tools.smali.dexlib2.Opcodes
|
|||||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
import com.android.tools.smali.dexlib2.iface.DexFile
|
||||||
import com.android.tools.smali.dexlib2.iface.Method
|
import com.android.tools.smali.dexlib2.iface.Method
|
||||||
import com.android.tools.smali.dexlib2.writer.io.MemoryDataStore
|
|
||||||
import lanchon.multidexlib2.BasicDexFileNamer
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
import lanchon.multidexlib2.DexIO
|
import lanchon.multidexlib2.DexIO
|
||||||
import lanchon.multidexlib2.MultiDexIO
|
import lanchon.multidexlib2.MultiDexIO
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileFilter
|
||||||
import java.io.Flushable
|
import java.io.Flushable
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
@@ -91,13 +90,43 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
|||||||
*/
|
*/
|
||||||
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile bytecode from the [BytecodeContext].
|
||||||
|
*
|
||||||
|
* @return The compiled bytecode.
|
||||||
|
*/
|
||||||
|
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].
|
* The integrations of a [PatcherContext].
|
||||||
*/
|
*/
|
||||||
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
|
||||||
/**
|
/**
|
||||||
* Whether to merge integrations.
|
* 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
|
var merge = false
|
||||||
|
|
||||||
@@ -109,7 +138,8 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
|||||||
|
|
||||||
logger.info("Merging integrations")
|
logger.info("Merging integrations")
|
||||||
|
|
||||||
// TODO: Multi-thread this.
|
val classMap = classes.associateBy { it.type }
|
||||||
|
|
||||||
this@Integrations.forEach { integrations ->
|
this@Integrations.forEach { integrations ->
|
||||||
MultiDexIO.readDexFile(
|
MultiDexIO.readDexFile(
|
||||||
true,
|
true,
|
||||||
@@ -117,8 +147,8 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
).classes.forEach classDef@{ classDef ->
|
).classes.forEach classDef@{ classDef ->
|
||||||
val existingClass = classes.find { it.type == classDef.type } ?: run {
|
val existingClass = classMap[classDef.type] ?: run {
|
||||||
logger.fine("Merging $classDef")
|
logger.fine("Adding $classDef")
|
||||||
classes.add(classDef)
|
classes.add(classDef)
|
||||||
return@classDef
|
return@classDef
|
||||||
}
|
}
|
||||||
@@ -132,27 +162,7 @@ class BytecodeContext internal constructor(private val options: PatcherOptions)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clear()
|
clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile bytecode from the [BytecodeContext].
|
|
||||||
*
|
|
||||||
* @return The compiled bytecode.
|
|
||||||
*/
|
|
||||||
override fun get(): List<PatcherResult.PatchedDexFile> {
|
|
||||||
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)) }
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -1,17 +1,11 @@
|
|||||||
package app.revanced.patcher.extensions
|
package app.revanced.patcher.extensions
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
||||||
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
|
||||||
object MethodFingerprintExtensions {
|
object MethodFingerprintExtensions {
|
||||||
|
// TODO: Make this a property.
|
||||||
/**
|
|
||||||
* The name of a [MethodFingerprint].
|
|
||||||
*/
|
|
||||||
val MethodFingerprint.name: String
|
|
||||||
get() = this.javaClass.simpleName
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
|
* 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
package app.revanced.patcher.fingerprint
|
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A ReVanced fingerprint.
|
|
||||||
* Can be a [MethodFingerprint].
|
|
||||||
*/
|
|
||||||
interface Fingerprint
|
|
125
src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt
Normal file
125
src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package app.revanced.patcher.fingerprint
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||||
|
import com.android.tools.smali.dexlib2.iface.Method
|
||||||
|
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
internal typealias MethodClassPair = Pair<Method, ClassDef>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup map for methods.
|
||||||
|
*/
|
||||||
|
internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by mutableMapOf() {
|
||||||
|
/**
|
||||||
|
* Adds a [MethodClassPair] to the list associated with the given key.
|
||||||
|
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
|
||||||
|
*/
|
||||||
|
fun add(
|
||||||
|
key: String,
|
||||||
|
methodClassPair: MethodClassPair
|
||||||
|
) {
|
||||||
|
getOrPut(key) { MethodClassList() }.add(methodClassPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of methods and the class they are a member of.
|
||||||
|
*/
|
||||||
|
internal class MethodClassList : LinkedList<MethodClassPair>()
|
||||||
|
|
||||||
|
companion object Maps {
|
||||||
|
/**
|
||||||
|
* A list of methods and the class they are a member of.
|
||||||
|
*/
|
||||||
|
internal val methods = MethodClassList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup map for methods keyed to the methods access flags, return type and parameter.
|
||||||
|
*/
|
||||||
|
internal val methodSignatureLookupMap = LookupMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup map for methods associated by strings referenced in the method.
|
||||||
|
*/
|
||||||
|
internal val methodStringsLookupMap = LookupMap()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes lookup maps for [MethodFingerprint] resolution
|
||||||
|
* using attributes of methods such as the method signature or strings.
|
||||||
|
*
|
||||||
|
* @param context The [BytecodeContext] containing the classes to initialize the lookup maps with.
|
||||||
|
*/
|
||||||
|
internal fun initializeLookupMaps(context: BytecodeContext) {
|
||||||
|
if (methods.isNotEmpty()) clearLookupMaps()
|
||||||
|
|
||||||
|
context.classes.forEach { classDef ->
|
||||||
|
classDef.methods.forEach { method ->
|
||||||
|
val methodClassPair = method to classDef
|
||||||
|
|
||||||
|
// For fingerprints with no access or return type specified.
|
||||||
|
methods += methodClassPair
|
||||||
|
|
||||||
|
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
|
||||||
|
|
||||||
|
// Add <access><returnType> as the key.
|
||||||
|
methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair)
|
||||||
|
|
||||||
|
// Add <access><returnType>[parameters] as the key.
|
||||||
|
methodSignatureLookupMap.add(
|
||||||
|
buildString {
|
||||||
|
append(accessFlagsReturnKey)
|
||||||
|
appendParameters(method.parameterTypes)
|
||||||
|
},
|
||||||
|
methodClassPair
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add strings contained in the method as the key.
|
||||||
|
method.implementation?.instructions?.forEach instructions@{ instruction ->
|
||||||
|
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO)
|
||||||
|
return@instructions
|
||||||
|
|
||||||
|
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||||
|
|
||||||
|
methodStringsLookupMap.add(string, methodClassPair)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the future, the class type could be added to the lookup map.
|
||||||
|
// This would require MethodFingerprint to be changed to include the class type.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the internal lookup maps created in [initializeLookupMaps]
|
||||||
|
*/
|
||||||
|
internal fun clearLookupMaps() {
|
||||||
|
methods.clear()
|
||||||
|
methodSignatureLookupMap.clear()
|
||||||
|
methodStringsLookupMap.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a string based on the parameter reference types of this method.
|
||||||
|
*/
|
||||||
|
internal fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
|
||||||
|
// Maximum parameters to use in the signature key.
|
||||||
|
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
|
||||||
|
// To keep the signature map from becoming needlessly bloated,
|
||||||
|
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
|
||||||
|
// The value of 5 was chosen based on local performance testing and is not set in stone.
|
||||||
|
val maxSignatureParameters = 5
|
||||||
|
// Must append a unique value before the parameters to distinguish this key includes the parameters.
|
||||||
|
// If this is not appended, then methods with no parameters
|
||||||
|
// will collide with different keys that specify access/return but omit the parameters.
|
||||||
|
append("p:")
|
||||||
|
parameters.forEachIndexed { index, parameter ->
|
||||||
|
if (index >= maxSignatureParameters) return
|
||||||
|
append(parameter.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,94 @@
|
|||||||
|
package app.revanced.patcher.fingerprint
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.util.proxy.ClassProxy
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||||
|
import com.android.tools.smali.dexlib2.iface.Method
|
||||||
|
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the result of a [MethodFingerprintResult].
|
||||||
|
*
|
||||||
|
* @param method The matching method.
|
||||||
|
* @param classDef The [ClassDef] that contains the matching [method].
|
||||||
|
* @param scanResult The result of scanning for the [MethodFingerprint].
|
||||||
|
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
class MethodFingerprintResult(
|
||||||
|
val method: Method,
|
||||||
|
val classDef: ClassDef,
|
||||||
|
val scanResult: MethodFingerprintScanResult,
|
||||||
|
internal val context: BytecodeContext
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Returns a mutable clone of [classDef]
|
||||||
|
*
|
||||||
|
* Please note, this method allocates a [ClassProxy].
|
||||||
|
* Use [classDef] where possible.
|
||||||
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
val mutableClass by lazy { context.proxy(classDef).mutableClass }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a mutable clone of [method]
|
||||||
|
*
|
||||||
|
* Please note, this method allocates a [ClassProxy].
|
||||||
|
* Use [method] where possible.
|
||||||
|
*/
|
||||||
|
val mutableMethod by lazy {
|
||||||
|
mutableClass.methods.first {
|
||||||
|
MethodUtil.methodSignaturesMatch(it, this.method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of scanning on the [MethodFingerprint].
|
||||||
|
* @param patternScanResult The result of the pattern scan.
|
||||||
|
* @param stringsScanResult The result of the string scan.
|
||||||
|
*/
|
||||||
|
class MethodFingerprintScanResult(
|
||||||
|
val patternScanResult: PatternScanResult?,
|
||||||
|
val stringsScanResult: StringsScanResult?
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The result of scanning strings on the [MethodFingerprint].
|
||||||
|
* @param matches The list of strings that were matched.
|
||||||
|
*/
|
||||||
|
class StringsScanResult(val matches: List<StringMatch>) {
|
||||||
|
/**
|
||||||
|
* Represents a match for a string at an index.
|
||||||
|
* @param string The string that was matched.
|
||||||
|
* @param index The index of the string.
|
||||||
|
*/
|
||||||
|
class StringMatch(val string: String, val index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The result of a pattern scan.
|
||||||
|
* @param startIndex The start index of the instructions where to which this pattern matches.
|
||||||
|
* @param endIndex The end index of the instructions where to which this pattern matches.
|
||||||
|
* @param warnings A list of warnings considering this [PatternScanResult].
|
||||||
|
*/
|
||||||
|
class PatternScanResult(
|
||||||
|
val startIndex: Int,
|
||||||
|
val endIndex: Int,
|
||||||
|
var warnings: List<Warning>? = null
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Represents warnings of the pattern scan.
|
||||||
|
* @param correctOpcode The opcode the instruction list has.
|
||||||
|
* @param wrongOpcode The opcode the pattern list of the signature currently has.
|
||||||
|
* @param instructionIndex The index of the opcode relative to the instruction list.
|
||||||
|
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
|
||||||
|
*/
|
||||||
|
class Warning(
|
||||||
|
val correctOpcode: Opcode,
|
||||||
|
val wrongOpcode: Opcode,
|
||||||
|
val instructionIndex: Int,
|
||||||
|
val patternIndex: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
package app.revanced.patcher.fingerprint.method.annotation
|
package app.revanced.patcher.fingerprint.annotation
|
||||||
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.MethodFingerprint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
|
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
|||||||
package app.revanced.patcher.logging
|
|
||||||
|
|
||||||
@Deprecated("This will be removed in a future release")
|
|
||||||
interface Logger {
|
|
||||||
fun error(msg: String) {}
|
|
||||||
fun warn(msg: String) {}
|
|
||||||
fun info(msg: String) {}
|
|
||||||
fun trace(msg: String) {}
|
|
||||||
}
|
|
@@ -1,6 +0,0 @@
|
|||||||
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.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,107 @@
|
|||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
package app.revanced.patcher.patch
|
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.Context
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.patch.options.PatchOptions
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
typealias PatchClass = Class<out Patch<Context<*>>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ReVanced patch.
|
* 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.
|
* @param T The [Context] type this patch will work on.
|
||||||
* Closing will be done in reverse execution order.
|
|
||||||
*/
|
*/
|
||||||
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().ifEmpty { null }) }
|
||||||
|
.toSet().ifEmpty { null }
|
||||||
|
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.
|
* @param context The [Context] the patch will work on.
|
||||||
* @return The result of executing the patch.
|
* @return The result of executing the patch.
|
||||||
*/
|
*/
|
||||||
fun execute(context: @UnsafeVariance T)
|
abstract fun execute(context: @UnsafeVariance T)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
override fun hashCode() = name.hashCode()
|
||||||
* Resource patch for the Patcher.
|
|
||||||
*/
|
|
||||||
interface ResourcePatch : Patch<ResourceContext>
|
|
||||||
|
|
||||||
/**
|
override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch"
|
||||||
* Bytecode patch for the Patcher.
|
|
||||||
*
|
override fun equals(other: Any?): Boolean {
|
||||||
* @param fingerprints A list of [MethodFingerprint] this patch relies on.
|
if (this === other) return true
|
||||||
*/
|
if (javaClass != other?.javaClass) return false
|
||||||
abstract class BytecodePatch(
|
|
||||||
internal val fingerprints: Iterable<MethodFingerprint>? = null
|
other as Patch<*>
|
||||||
) : Patch<BytecodeContext>
|
|
||||||
|
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].
|
* 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.
|
* @param exception The [PatchException] thrown, if any.
|
||||||
*/
|
*/
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
|
||||||
class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null)
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user