mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2024-11-10 01:02:22 +01:00
feat!: Add patch annotation processor
This commit introduces an annotation processor for patches. Patches can use the `@Patch` instead of super constructor parameters. BREAKING CHANGE: The manifest for patches has been removed, and the properties have been added to patches. Patches are now `OptionsContainer`. The `@Patch` annotation has been removed in favour of the `@Patch` annotation from the annotation processor.
This commit is contained in:
parent
4dd04975d9
commit
3fc6a139ee
29 changed files with 707 additions and 161 deletions
|
@ -6,8 +6,12 @@ kotlin-test = "1.8.20-RC"
|
||||||
kotlinx-coroutines-core = "1.7.1"
|
kotlinx-coroutines-core = "1.7.1"
|
||||||
multidexlib2 = "3.0.3.r2"
|
multidexlib2 = "3.0.3.r2"
|
||||||
smali = "3.0.3"
|
smali = "3.0.3"
|
||||||
|
symbol-processing-api = "1.9.0-1.0.11"
|
||||||
xpp3 = "1.1.4c"
|
xpp3 = "1.1.4c"
|
||||||
binary-compatibility-validator = "0.13.2"
|
binary-compatibility-validator = "0.13.2"
|
||||||
|
kotlin-compile-testing-ksp = "1.5.0"
|
||||||
|
kotlinpoet-ksp = "1.14.2"
|
||||||
|
ksp = "1.9.0-1.0.11"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
android = { module = "com.google.android:android", version.ref = "android" }
|
android = { module = "com.google.android:android", version.ref = "android" }
|
||||||
|
@ -17,7 +21,11 @@ kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotl
|
||||||
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" }
|
||||||
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
||||||
|
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbol-processing-api" }
|
||||||
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
||||||
|
kotlin-compile-testing = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "kotlin-compile-testing-ksp" }
|
||||||
|
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet-ksp" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
@ -0,0 +1,25 @@
|
||||||
|
public abstract interface annotation class app/revanced/patcher/patch/annotations/CompatiblePackage : java/lang/annotation/Annotation {
|
||||||
|
public abstract fun name ()Ljava/lang/String;
|
||||||
|
public abstract fun versions ()[Ljava/lang/String;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation {
|
||||||
|
public abstract fun compatiblePackages ()[Lapp/revanced/patcher/patch/annotations/CompatiblePackage;
|
||||||
|
public abstract fun dependencies ()[Ljava/lang/Class;
|
||||||
|
public abstract fun description ()Ljava/lang/String;
|
||||||
|
public abstract fun name ()Ljava/lang/String;
|
||||||
|
public abstract fun requiresIntegrations ()Z
|
||||||
|
public abstract fun use ()Z
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patcher/patch/annotations/processor/PatchProcessor : com/google/devtools/ksp/processing/SymbolProcessor {
|
||||||
|
public fun <init> (Lcom/google/devtools/ksp/processing/CodeGenerator;Lcom/google/devtools/ksp/processing/KSPLogger;)V
|
||||||
|
public fun process (Lcom/google/devtools/ksp/processing/Resolver;)Ljava/util/List;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patcher/patch/annotations/processor/PatchProcessorProvider : com/google/devtools/ksp/processing/SymbolProcessorProvider {
|
||||||
|
public fun <init> ()V
|
||||||
|
public fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lapp/revanced/patcher/patch/annotations/processor/PatchProcessor;
|
||||||
|
public synthetic fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lcom/google/devtools/ksp/processing/SymbolProcessor;
|
||||||
|
}
|
||||||
|
|
50
revanced-patch-annotations-processor/build.gradle.kts
Normal file
50
revanced-patch-annotations-processor/build.gradle.kts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.9.0"
|
||||||
|
`maven-publish`
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.symbol.processing.api)
|
||||||
|
implementation(libs.kotlinpoet.ksp)
|
||||||
|
implementation(project(":revanced-patcher"))
|
||||||
|
|
||||||
|
testImplementation(libs.kotlin.test)
|
||||||
|
testImplementation(libs.kotlin.compile.testing)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin { jvmToolchain(11) }
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
maven {
|
||||||
|
name = "GitHubPackages"
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/revanced-patch-annotations-processor")
|
||||||
|
credentials {
|
||||||
|
username = System.getenv("GITHUB_ACTOR")
|
||||||
|
password = System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
publications {
|
||||||
|
create<MavenPublication>("gpr") {
|
||||||
|
from(components["java"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
revanced-patch-annotations-processor/settings.gradle.kts
Normal file
2
revanced-patch-annotations-processor/settings.gradle.kts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
rootProject.name = "revanced-patch-annotations-processor"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
package app.revanced.patcher.patch.annotations
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
@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> = [],
|
||||||
|
)
|
|
@ -0,0 +1,198 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.PatchOptions
|
||||||
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
import com.google.devtools.ksp.processing.*
|
||||||
|
import com.google.devtools.ksp.symbol.KSAnnotated
|
||||||
|
import com.google.devtools.ksp.symbol.KSAnnotation
|
||||||
|
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
||||||
|
import com.google.devtools.ksp.symbol.KSType
|
||||||
|
import com.google.devtools.ksp.validate
|
||||||
|
import com.squareup.kotlinpoet.*
|
||||||
|
import com.squareup.kotlinpoet.ksp.toClassName
|
||||||
|
import com.squareup.kotlinpoet.ksp.writeTo
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
class PatchProcessor(
|
||||||
|
private val codeGenerator: CodeGenerator,
|
||||||
|
private val logger: KSPLogger
|
||||||
|
) : SymbolProcessor {
|
||||||
|
|
||||||
|
private fun KSAnnotated.isSubclassOf(cls: KClass<*>): Boolean {
|
||||||
|
if (this !is KSClassDeclaration) return false
|
||||||
|
|
||||||
|
if (qualifiedName?.asString() == cls.qualifiedName) return true
|
||||||
|
|
||||||
|
return superTypes.any { it.resolve().declaration.isSubclassOf(cls) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
|
val executablePatches = buildMap {
|
||||||
|
resolver.getSymbolsWithAnnotation(Patch::class.qualifiedName!!).filter {
|
||||||
|
// Do not check here if Patch is super of the class, because it is expensive.
|
||||||
|
// Check it later when processing.
|
||||||
|
it.validate() && it.isSubclassOf(app.revanced.patcher.patch.Patch::class)
|
||||||
|
}.map {
|
||||||
|
it as KSClassDeclaration
|
||||||
|
}.forEach { patchDeclaration ->
|
||||||
|
patchDeclaration.annotations.find {
|
||||||
|
it.annotationType.resolve().declaration.qualifiedName!!.asString() == Patch::class.qualifiedName!!
|
||||||
|
}?.let { annotation ->
|
||||||
|
fun KSAnnotation.property(name: String) =
|
||||||
|
arguments.find { it.name!!.asString() == name }?.value!!
|
||||||
|
|
||||||
|
val name =
|
||||||
|
annotation.property("name").toString().ifEmpty { null }
|
||||||
|
|
||||||
|
val description =
|
||||||
|
annotation.property("description").toString().ifEmpty { null }
|
||||||
|
|
||||||
|
val dependencies =
|
||||||
|
(annotation.property("dependencies") as List<KSType>).ifEmpty { null }
|
||||||
|
|
||||||
|
val compatiblePackages =
|
||||||
|
(annotation.property("compatiblePackages") as List<KSAnnotation>).ifEmpty { null }
|
||||||
|
|
||||||
|
val use =
|
||||||
|
annotation.property("use") as Boolean
|
||||||
|
|
||||||
|
val requiresIntegrations =
|
||||||
|
annotation.property("requiresIntegrations") as Boolean
|
||||||
|
|
||||||
|
// Data class for KotlinPoet
|
||||||
|
data class PatchData(
|
||||||
|
val name: String?,
|
||||||
|
val description: String?,
|
||||||
|
val dependencies: List<ClassName>?,
|
||||||
|
val compatiblePackages: List<CodeBlock>?,
|
||||||
|
val use: Boolean,
|
||||||
|
val requiresIntegrations: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
this[patchDeclaration] = PatchData(
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
dependencies?.map { dependency -> dependency.toClassName() },
|
||||||
|
compatiblePackages?.map {
|
||||||
|
val packageName = it.property("name")
|
||||||
|
val packageVersions = (it.property("versions") as List<String>)
|
||||||
|
.joinToString(", ") { version -> "\"$version\"" }
|
||||||
|
|
||||||
|
CodeBlock.of(
|
||||||
|
"%T(%S, setOf(%L))",
|
||||||
|
app.revanced.patcher.patch.Patch.CompatiblePackage::class,
|
||||||
|
packageName,
|
||||||
|
packageVersions
|
||||||
|
)
|
||||||
|
},
|
||||||
|
use,
|
||||||
|
requiresIntegrations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a patch depends on another, that is annotated, the dependency should be replaced with the generated patch,
|
||||||
|
// because the generated patch has all the necessary properties to invoke the super constructor,
|
||||||
|
// unlike the annotated patch.
|
||||||
|
val dependencyResolutionMap = buildMap {
|
||||||
|
executablePatches.values.filter { it.dependencies != null }.flatMap {
|
||||||
|
it.dependencies!!
|
||||||
|
}.distinct().forEach { dependency ->
|
||||||
|
executablePatches.keys.find { it.qualifiedName?.asString() == dependency.toString() }
|
||||||
|
?.let { patch ->
|
||||||
|
this[dependency] = ClassName(
|
||||||
|
patch.packageName.asString(),
|
||||||
|
patch.simpleName.asString() + "Generated"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// kotlin poet generate a class for each patch
|
||||||
|
executablePatches.forEach { (patchDeclaration, patchAnnotation) ->
|
||||||
|
val isBytecodePatch = patchDeclaration.isSubclassOf(BytecodePatch::class)
|
||||||
|
|
||||||
|
val superClass = if (isBytecodePatch) {
|
||||||
|
BytecodePatch::class
|
||||||
|
} else {
|
||||||
|
ResourcePatch::class
|
||||||
|
}
|
||||||
|
|
||||||
|
val contextClass = if (isBytecodePatch) {
|
||||||
|
BytecodeContext::class
|
||||||
|
} else {
|
||||||
|
ResourceContext::class
|
||||||
|
}
|
||||||
|
|
||||||
|
val generatedPatchClassName = ClassName(
|
||||||
|
patchDeclaration.packageName.asString(),
|
||||||
|
patchDeclaration.simpleName.asString() + "Generated"
|
||||||
|
)
|
||||||
|
|
||||||
|
FileSpec.builder(generatedPatchClassName)
|
||||||
|
.addType(
|
||||||
|
TypeSpec.objectBuilder(generatedPatchClassName)
|
||||||
|
.superclass(superClass).apply {
|
||||||
|
patchAnnotation.name?.let { name ->
|
||||||
|
addSuperclassConstructorParameter("name = %S", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
patchAnnotation.description?.let { description ->
|
||||||
|
addSuperclassConstructorParameter("description = %S", description)
|
||||||
|
}
|
||||||
|
|
||||||
|
patchAnnotation.compatiblePackages?.let { compatiblePackages ->
|
||||||
|
addSuperclassConstructorParameter(
|
||||||
|
"compatiblePackages = setOf(%L)",
|
||||||
|
compatiblePackages.joinToString(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
patchAnnotation.dependencies?.let { dependencies ->
|
||||||
|
addSuperclassConstructorParameter(
|
||||||
|
"dependencies = setOf(%L)",
|
||||||
|
buildList {
|
||||||
|
addAll(dependencies)
|
||||||
|
// Also add the source class of the generated class so that it is also executed
|
||||||
|
add(patchDeclaration.toClassName())
|
||||||
|
}.joinToString(", ") { dependency ->
|
||||||
|
"${(dependencyResolutionMap[dependency] ?: dependency)}::class"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
addSuperclassConstructorParameter(
|
||||||
|
"use = %L", patchAnnotation.use
|
||||||
|
)
|
||||||
|
|
||||||
|
addSuperclassConstructorParameter(
|
||||||
|
"requiresIntegrations = %L",
|
||||||
|
patchAnnotation.requiresIntegrations
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.addFunction(
|
||||||
|
FunSpec.builder("execute")
|
||||||
|
.addModifiers(KModifier.OVERRIDE)
|
||||||
|
.addParameter("context", contextClass)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.addProperty(
|
||||||
|
PropertySpec.builder("options", PatchOptions::class, KModifier.OVERRIDE)
|
||||||
|
.initializer("%T.options", patchDeclaration.toClassName())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
).build().writeTo(
|
||||||
|
codeGenerator,
|
||||||
|
Dependencies(false, patchDeclaration.containingFile!!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor
|
||||||
|
|
||||||
|
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
||||||
|
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
||||||
|
|
||||||
|
class PatchProcessorProvider : SymbolProcessorProvider {
|
||||||
|
override fun create(environment: SymbolProcessorEnvironment) =
|
||||||
|
PatchProcessor(environment.codeGenerator, environment.logger)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
app.revanced.patcher.patch.annotations.processor.PatchProcessorProvider
|
|
@ -0,0 +1,131 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import com.tschuchort.compiletesting.KotlinCompilation
|
||||||
|
import com.tschuchort.compiletesting.SourceFile
|
||||||
|
import com.tschuchort.compiletesting.kspWithCompilation
|
||||||
|
import com.tschuchort.compiletesting.symbolProcessorProviders
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
import kotlin.test.assertNull
|
||||||
|
|
||||||
|
class TestPatchAnnotationProcessor {
|
||||||
|
// region Processing
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProcessing() = assertEquals(
|
||||||
|
"Processable patch", compile(
|
||||||
|
getSourceFile(
|
||||||
|
"processing", "ProcessablePatch"
|
||||||
|
)
|
||||||
|
).loadPatch("$SAMPLE_PACKAGE.processing.ProcessablePatchGenerated").name
|
||||||
|
)
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Dependencies
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDependencies() {
|
||||||
|
compile(
|
||||||
|
getSourceFile(
|
||||||
|
"dependencies", "DependentPatch"
|
||||||
|
), getSourceFile(
|
||||||
|
"dependencies", "DependencyPatch"
|
||||||
|
)
|
||||||
|
).let { result ->
|
||||||
|
result.loadPatch("$SAMPLE_PACKAGE.dependencies.DependentPatchGenerated").let {
|
||||||
|
// Dependency as well as the source class of the generated class.
|
||||||
|
assertEquals(
|
||||||
|
2,
|
||||||
|
it.dependencies!!.size
|
||||||
|
)
|
||||||
|
|
||||||
|
// The last dependency is always the source class of the generated class to respect
|
||||||
|
// order of dependencies.
|
||||||
|
assertEquals(
|
||||||
|
result.loadPatch("$SAMPLE_PACKAGE.dependencies.DependentPatch")::class,
|
||||||
|
it.dependencies!!.last()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Options
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOptions() {
|
||||||
|
val patch = compile(
|
||||||
|
getSourceFile(
|
||||||
|
"options", "OptionsPatch"
|
||||||
|
)
|
||||||
|
).loadPatch("$SAMPLE_PACKAGE.options.OptionsPatchGenerated")
|
||||||
|
|
||||||
|
assertNotNull(patch.options)
|
||||||
|
assertEquals(patch.options["print"].title, "Print message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Limitations
|
||||||
|
@Test
|
||||||
|
fun failingManualDependency() = assertNull(
|
||||||
|
compile(
|
||||||
|
getSourceFile(
|
||||||
|
"limitations/manualdependency", "DependentPatch"
|
||||||
|
), getSourceFile(
|
||||||
|
"limitations/manualdependency", "DependencyPatch"
|
||||||
|
)
|
||||||
|
).loadPatch("$SAMPLE_PACKAGE.limitations.manualdependency.DependentPatchGenerated").dependencies
|
||||||
|
)
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
private companion object Utils {
|
||||||
|
const val SAMPLE_PACKAGE = "app.revanced.patcher.patch.annotations.processor.samples"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a source file from the given sample and class name.
|
||||||
|
*
|
||||||
|
* @param sample The sample to get the source file from.
|
||||||
|
* @param className The name of the class to get the source file from.
|
||||||
|
* @return The source file.
|
||||||
|
*/
|
||||||
|
fun getSourceFile(sample: String, className: String) = SourceFile.kotlin(
|
||||||
|
"$className.kt", TestPatchAnnotationProcessor::class.java.classLoader.getResourceAsStream(
|
||||||
|
"app/revanced/patcher/patch/annotations/processor/samples/$sample/$className.kt"
|
||||||
|
)?.readAllBytes()?.toString(Charsets.UTF_8) ?: error("Could not find resource $className")
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile the given source files and return the result.
|
||||||
|
*
|
||||||
|
* @param sourceFiles The source files to compile.
|
||||||
|
* @return The result of the compilation.
|
||||||
|
*/
|
||||||
|
fun compile(vararg sourceFiles: SourceFile) = KotlinCompilation().apply {
|
||||||
|
sources = sourceFiles.asList()
|
||||||
|
|
||||||
|
symbolProcessorProviders = listOf(PatchProcessorProvider())
|
||||||
|
|
||||||
|
// Required until https://github.com/tschuchortdev/kotlin-compile-testing/issues/312 closed.
|
||||||
|
kspWithCompilation = true
|
||||||
|
|
||||||
|
inheritClassPath = true
|
||||||
|
messageOutputStream = System.out
|
||||||
|
}.compile().also { result ->
|
||||||
|
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Class loading
|
||||||
|
|
||||||
|
fun KotlinCompilation.Result.loadPatch(name: String) = classLoader.loadClass(name).loadPatch()
|
||||||
|
|
||||||
|
fun Class<*>.loadPatch() = this.getField("INSTANCE").get(null) as Patch<*>
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor.samples.dependencies
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
|
||||||
|
@Patch(name = "Dependency patch")
|
||||||
|
object DependencyPatch : ResourcePatch() {
|
||||||
|
override fun execute(context: ResourceContext) {}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor.samples.dependencies
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
|
||||||
|
@Patch(
|
||||||
|
name = "Dependent patch",
|
||||||
|
dependencies = [DependencyPatch::class],
|
||||||
|
)
|
||||||
|
object DependentPatch : BytecodePatch() {
|
||||||
|
override fun execute(context: BytecodeContext) {}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor.samples.limitations.manualdependency
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
|
||||||
|
@Patch(name = "Dependency patch")
|
||||||
|
object DependencyPatch : ResourcePatch() {
|
||||||
|
override fun execute(context: ResourceContext) { }
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor.samples.limitations.manualdependency
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
|
||||||
|
@Patch(name = "Dependent patch")
|
||||||
|
object DependentPatch : BytecodePatch(
|
||||||
|
// Dependency will not be executed correctly if it is manually specified.
|
||||||
|
// The reason for this is that the dependency patch is annotated too,
|
||||||
|
// so the processor will generate a new patch class for it embedding the annotated information.
|
||||||
|
// Because the dependency is manually specified,
|
||||||
|
// the processor will not be able to change this dependency to the generated class,
|
||||||
|
// which means that the dependency will lose the annotated information.
|
||||||
|
dependencies = setOf(DependencyPatch::class)
|
||||||
|
) {
|
||||||
|
override fun execute(context: BytecodeContext) {}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor.samples.options
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.ResourceContext
|
||||||
|
import app.revanced.patcher.patch.PatchOption
|
||||||
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
|
||||||
|
@Patch(name = "Options patch")
|
||||||
|
object OptionsPatch : ResourcePatch() {
|
||||||
|
override fun execute(context: ResourceContext) {}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
private val printOption by option(
|
||||||
|
PatchOption.StringOption(
|
||||||
|
"print",
|
||||||
|
null,
|
||||||
|
"Print message",
|
||||||
|
"The message to print."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package app.revanced.patcher.patch.annotations.processor.samples.processing
|
||||||
|
|
||||||
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
|
|
||||||
|
@Patch("Processable patch")
|
||||||
|
object ProcessablePatch : BytecodePatch() {
|
||||||
|
override fun execute(context: BytecodeContext) {}
|
||||||
|
}
|
|
@ -327,7 +327,9 @@ public final class app/revanced/patcher/logging/impl/NopLogger : app/revanced/pa
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
|
public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
|
||||||
public fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;[Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)V
|
public fun <init> ()V
|
||||||
|
public fun <init> (Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
|
||||||
|
public synthetic fun <init> (Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception {
|
public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception {
|
||||||
|
@ -348,38 +350,29 @@ public final class app/revanced/patcher/patch/NoSuchOptionException : java/lang/
|
||||||
|
|
||||||
public abstract class app/revanced/patcher/patch/OptionsContainer {
|
public abstract class app/revanced/patcher/patch/OptionsContainer {
|
||||||
public fun <init> ()V
|
public fun <init> ()V
|
||||||
public final fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions;
|
public fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions;
|
||||||
protected final fun option (Lapp/revanced/patcher/patch/PatchOption;)Lapp/revanced/patcher/patch/PatchOption;
|
protected final fun option (Lapp/revanced/patcher/patch/PatchOption;)Lapp/revanced/patcher/patch/PatchOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/patcher/patch/Patch {
|
public abstract class app/revanced/patcher/patch/Patch : app/revanced/patcher/patch/OptionsContainer {
|
||||||
public synthetic fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
|
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
public fun equals (Ljava/lang/Object;)Z
|
public fun equals (Ljava/lang/Object;)Z
|
||||||
public abstract fun execute (Lapp/revanced/patcher/data/Context;)V
|
public abstract fun execute (Lapp/revanced/patcher/data/Context;)V
|
||||||
public final fun getManifest ()Lapp/revanced/patcher/patch/Patch$Manifest;
|
|
||||||
public fun hashCode ()I
|
|
||||||
public fun toString ()Ljava/lang/String;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/Patch$Manifest {
|
|
||||||
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;)V
|
|
||||||
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
|
||||||
public fun equals (Ljava/lang/Object;)Z
|
|
||||||
public final fun getCompatiblePackages ()Ljava/util/Set;
|
public final fun getCompatiblePackages ()Ljava/util/Set;
|
||||||
public final fun getDependencies ()Ljava/util/Set;
|
public final fun getDependencies ()Ljava/util/Set;
|
||||||
public final fun getDescription ()Ljava/lang/String;
|
public final fun getDescription ()Ljava/lang/String;
|
||||||
public final fun getName ()Ljava/lang/String;
|
public final fun getName ()Ljava/lang/String;
|
||||||
public final fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions;
|
|
||||||
public final fun getRequiresIntegrations ()Z
|
public final fun getRequiresIntegrations ()Z
|
||||||
public final fun getUse ()Z
|
public final fun getUse ()Z
|
||||||
public fun hashCode ()I
|
public fun hashCode ()I
|
||||||
|
public fun toString ()Ljava/lang/String;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/Patch$Manifest$CompatiblePackage {
|
public final class app/revanced/patcher/patch/Patch$CompatiblePackage {
|
||||||
public fun <init> (Ljava/lang/String;Ljava/util/Set;)V
|
public fun <init> (Ljava/lang/String;Ljava/util/Set;)V
|
||||||
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
public final fun getName ()Ljava/lang/String;
|
public final fun getName ()Ljava/lang/String;
|
||||||
public final fun getVersions ()Ljava/util/Set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/PatchException : java/lang/Exception {
|
public final class app/revanced/patcher/patch/PatchException : java/lang/Exception {
|
||||||
|
@ -446,10 +439,9 @@ public final class app/revanced/patcher/patch/RequirementNotMetException : java/
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
|
public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
|
||||||
public fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;)V
|
public fun <init> ()V
|
||||||
}
|
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
|
||||||
|
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||||
public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable {
|
public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable {
|
||||||
|
|
|
@ -15,6 +15,7 @@ dependencies {
|
||||||
|
|
||||||
compileOnly(libs.android)
|
compileOnly(libs.android)
|
||||||
|
|
||||||
|
testImplementation(project(":revanced-patch-annotations-processor"))
|
||||||
testImplementation(libs.kotlin.test)
|
testImplementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
package app.revanced.patcher
|
package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import dalvik.system.DexClassLoader
|
import dalvik.system.DexClassLoader
|
||||||
import lanchon.multidexlib2.BasicDexFileNamer
|
import lanchon.multidexlib2.BasicDexFileNamer
|
||||||
|
@ -25,7 +24,7 @@ typealias PatchClass = KClass<out Patch<*>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A loader of [Patch]es from patch bundles.
|
* A loader of [Patch]es from patch bundles.
|
||||||
* This will load all [Patch]es from the given 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 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 classLoader The [ClassLoader] to use for loading the classes.
|
||||||
|
@ -38,22 +37,22 @@ sealed class PatchBundleLoader private constructor(
|
||||||
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
|
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
patchBundles.flatMap(getBinaryClassNames).map {
|
patchBundles.flatMap(getBinaryClassNames).asSequence().map {
|
||||||
classLoader.loadClass(it)
|
classLoader.loadClass(it)
|
||||||
}.filter {
|
}.filter {
|
||||||
if (it.isAnnotation) return@filter false
|
it.isInstance(Patch::class.java)
|
||||||
|
|
||||||
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null
|
|
||||||
}.mapNotNull { patchClass ->
|
}.mapNotNull { patchClass ->
|
||||||
patchClass.getInstance(logger)
|
patchClass.getInstance(logger)
|
||||||
}.associateBy { it.manifest.name }
|
}.filter {
|
||||||
let { patches ->
|
it.name != null
|
||||||
|
}.associateBy {
|
||||||
|
it.name!!
|
||||||
|
}.let { patches ->
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
(this as MutableMap<String, Patch<*>>).putAll(patches)
|
(this as MutableMap<String, Patch<*>>).putAll(patches)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal companion object Utils {
|
internal companion object Utils {
|
||||||
/**
|
/**
|
||||||
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.
|
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.
|
||||||
|
|
|
@ -2,7 +2,6 @@ package app.revanced.patcher
|
||||||
|
|
||||||
import app.revanced.patcher.PatchBundleLoader.Utils.getInstance
|
import app.revanced.patcher.PatchBundleLoader.Utils.getInstance
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.data.ResourceContext
|
||||||
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
|
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
|
||||||
import app.revanced.patcher.patch.*
|
import app.revanced.patcher.patch.*
|
||||||
|
@ -46,13 +45,19 @@ class Patcher(
|
||||||
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY)
|
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Fix circular dependency detection.
|
||||||
|
// /**
|
||||||
|
// * Add [Patch]es to ReVanced [Patcher].
|
||||||
|
// * It is not guaranteed that all supplied [Patch]es will be accepted, if an exception is thrown.
|
||||||
|
// *
|
||||||
|
// * @param patches The [Patch]es to add.
|
||||||
|
// * @throws PatcherException.CircularDependencyException If a circular dependency is detected.
|
||||||
|
// */
|
||||||
/**
|
/**
|
||||||
* Add [Patch]es to ReVanced [Patcher].
|
* Add [Patch]es to ReVanced [Patcher].
|
||||||
* It is not guaranteed that all supplied [Patch]es will be accepted, if an exception is thrown.
|
|
||||||
*
|
*
|
||||||
* @param patches The [Patch]es to add.
|
* @param patches The [Patch]es to add.
|
||||||
* @throws PatcherException.CircularDependencyException If a circular dependency is detected.
|
*/
|
||||||
*/
|
|
||||||
@Suppress("NAME_SHADOWING")
|
@Suppress("NAME_SHADOWING")
|
||||||
override fun acceptPatches(patches: List<Patch<*>>) {
|
override fun acceptPatches(patches: List<Patch<*>>) {
|
||||||
/**
|
/**
|
||||||
|
@ -65,14 +70,14 @@ class Patcher(
|
||||||
val dependency = this.java.getInstance(logger)!!
|
val dependency = this.java.getInstance(logger)!!
|
||||||
context.allPatches[this] = dependency
|
context.allPatches[this] = dependency
|
||||||
|
|
||||||
dependency.manifest.dependencies?.forEach { it.putDependenciesRecursively() }
|
dependency.dependencies?.forEach { it.putDependenciesRecursively() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all patches and their dependencies to the context.
|
// Add all patches and their dependencies to the context.
|
||||||
for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: {
|
for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: {
|
||||||
context.allPatches[patch::class] = patch
|
context.allPatches[patch::class] = patch
|
||||||
|
|
||||||
patch.manifest.dependencies?.forEach { it.putDependenciesRecursively() }
|
patch.dependencies?.forEach { it.putDependenciesRecursively() }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO: Fix circular dependency detection.
|
/* TODO: Fix circular dependency detection.
|
||||||
|
@ -99,7 +104,7 @@ class Patcher(
|
||||||
* @param predicate The predicate to match.
|
* @param predicate The predicate to match.
|
||||||
*/
|
*/
|
||||||
fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean =
|
fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean =
|
||||||
predicate(this) || manifest.dependencies?.any { dependency ->
|
predicate(this) || dependencies?.any { dependency ->
|
||||||
context.allPatches[dependency]!!.anyRecursively(predicate)
|
context.allPatches[dependency]!!.anyRecursively(predicate)
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|
||||||
|
@ -113,7 +118,7 @@ class Patcher(
|
||||||
|
|
||||||
// Determine, if merging integrations is required.
|
// Determine, if merging integrations is required.
|
||||||
for (patch in patches)
|
for (patch in patches)
|
||||||
if (!patch.anyRecursively { it.manifest.requiresIntegrations }) {
|
if (!patch.anyRecursively { it.requiresIntegrations }) {
|
||||||
context.bytecodeContext.integrations.merge = true
|
context.bytecodeContext.integrations.merge = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -148,7 +153,7 @@ class Patcher(
|
||||||
patch: Patch<*>,
|
patch: Patch<*>,
|
||||||
executedPatches: LinkedHashMap<Patch<*>, PatchResult>
|
executedPatches: LinkedHashMap<Patch<*>, PatchResult>
|
||||||
): PatchResult {
|
): PatchResult {
|
||||||
val patchName = patch.manifest.name
|
val patchName = patch.name
|
||||||
|
|
||||||
executedPatches[patch]?.let { patchResult ->
|
executedPatches[patch]?.let { patchResult ->
|
||||||
patchResult.exception ?: return patchResult
|
patchResult.exception ?: return patchResult
|
||||||
|
@ -159,7 +164,7 @@ class Patcher(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursively execute all dependency patches.
|
// Recursively execute all dependency patches.
|
||||||
patch.manifest.dependencies?.forEach { dependencyName ->
|
patch.dependencies?.forEach { dependencyName ->
|
||||||
val dependency = context.executablePatches[dependencyName]!!
|
val dependency = context.executablePatches[dependencyName]!!
|
||||||
val result = executePatch(dependency, executedPatches)
|
val result = executePatch(dependency, executedPatches)
|
||||||
|
|
||||||
|
@ -171,17 +176,17 @@ class Patcher(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement this in a more polymorphic way.
|
|
||||||
val patchContext = if (patch is BytecodePatch) {
|
|
||||||
patch.fingerprints.asList().resolveUsingLookupMap(context.bytecodeContext)
|
|
||||||
|
|
||||||
context.bytecodeContext
|
|
||||||
} else {
|
|
||||||
context.resourceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
patch.execute(patchContext)
|
// TODO: Implement this in a more polymorphic way.
|
||||||
|
when (patch) {
|
||||||
|
is BytecodePatch -> {
|
||||||
|
patch.fingerprints.toList().resolveUsingLookupMap(context.bytecodeContext)
|
||||||
|
patch.execute(context.bytecodeContext)
|
||||||
|
}
|
||||||
|
is ResourcePatch -> {
|
||||||
|
patch.execute(context.resourceContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PatchResult(patch)
|
PatchResult(patch)
|
||||||
} catch (exception: PatchException) {
|
} catch (exception: PatchException) {
|
||||||
|
@ -203,11 +208,11 @@ class Patcher(
|
||||||
|
|
||||||
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
|
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
|
||||||
|
|
||||||
context.executablePatches.map { it.value }.sortedBy { it.manifest.name }.forEach { patch ->
|
context.executablePatches.map { it.value }.sortedBy { it.name }.forEach { patch ->
|
||||||
val patchResult = executePatch(patch, executedPatches)
|
val patchResult = executePatch(patch, executedPatches)
|
||||||
|
|
||||||
// If the patch failed, emit the result, even if it is closeable.
|
// If the patch failed, emit the result, even if it is closeable.
|
||||||
// Results of successfully executed patches that are closeable will be emitted later.
|
// Results of executed patches that are closeable will be emitted later.
|
||||||
patchResult.exception?.let {
|
patchResult.exception?.let {
|
||||||
// Propagate exception to caller instead of wrapping it in a new exception.
|
// Propagate exception to caller instead of wrapping it in a new exception.
|
||||||
emit(patchResult)
|
emit(patchResult)
|
||||||
|
@ -240,7 +245,7 @@ class Patcher(
|
||||||
PatchResult(
|
PatchResult(
|
||||||
patch,
|
patch,
|
||||||
PatchException(
|
PatchException(
|
||||||
"'${patch.manifest.name}' raised an exception while being closed: $it",
|
"'${patch.name}' raised an exception while being closed: $it",
|
||||||
result.exception
|
result.exception
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -248,10 +253,7 @@ class Patcher(
|
||||||
|
|
||||||
if (returnOnError) return@flow
|
if (returnOnError) return@flow
|
||||||
} ?: run {
|
} ?: run {
|
||||||
patch::class
|
patch.name ?: return@run
|
||||||
.java
|
|
||||||
.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
|
|
||||||
?: return@run
|
|
||||||
|
|
||||||
emit(result)
|
emit(result)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,13 @@ abstract class OptionsContainer {
|
||||||
* @see PatchOptions
|
* @see PatchOptions
|
||||||
*/
|
*/
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
val options = PatchOptions()
|
open val options = PatchOptions()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a [PatchOption].
|
||||||
|
* @param opt The [PatchOption] to register.
|
||||||
|
* @return The registered [PatchOption].
|
||||||
|
*/
|
||||||
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
|
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
|
||||||
options.register(opt)
|
options.register(opt)
|
||||||
return opt
|
return opt
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@file:Suppress("MemberVisibilityCanBePrivate", "UNUSED_PARAMETER")
|
||||||
|
|
||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.PatchClass
|
import app.revanced.patcher.PatchClass
|
||||||
|
@ -14,10 +16,23 @@ import java.io.Closeable
|
||||||
* If an implementation of [Patch] also implements [Closeable]
|
* If an implementation of [Patch] also implements [Closeable]
|
||||||
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
|
* it will be closed in reverse execution order of patches executed by ReVanced [Patcher].
|
||||||
*
|
*
|
||||||
* @param manifest The manifest of the [Patch].
|
* @param name The name of the patch.
|
||||||
|
* @param description The description of the patch.
|
||||||
|
* @param compatiblePackages The packages the patch is compatible with.
|
||||||
|
* @param dependencies The names of patches this patch depends on.
|
||||||
|
* @param use Weather or not the patch should be used.
|
||||||
|
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||||
* @param T The [Context] type this patch will work on.
|
* @param T The [Context] type this patch will work on.
|
||||||
*/
|
*/
|
||||||
sealed class Patch<out T : Context<*>>(val manifest: Manifest) {
|
sealed class Patch<out T : Context<*>>(
|
||||||
|
val name: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val compatiblePackages: Set<CompatiblePackage>? = null,
|
||||||
|
val dependencies: Set<PatchClass>? = null,
|
||||||
|
val use: Boolean = true,
|
||||||
|
// TODO: Remove this property, once integrations are coupled with patches.
|
||||||
|
val requiresIntegrations: Boolean = false,
|
||||||
|
) : OptionsContainer() {
|
||||||
/**
|
/**
|
||||||
* The execution function of the patch.
|
* The execution function of the patch.
|
||||||
*
|
*
|
||||||
|
@ -26,72 +41,69 @@ sealed class Patch<out T : Context<*>>(val manifest: Manifest) {
|
||||||
*/
|
*/
|
||||||
abstract fun execute(context: @UnsafeVariance T)
|
abstract fun execute(context: @UnsafeVariance T)
|
||||||
|
|
||||||
override fun hashCode() = manifest.hashCode()
|
override fun hashCode() = name.hashCode()
|
||||||
|
|
||||||
override fun equals(other: Any?) = other is Patch<*> && manifest == other.manifest
|
override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch"
|
||||||
|
|
||||||
override fun toString() = manifest.name
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as Patch<*>
|
||||||
|
|
||||||
|
return name == other.name
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The manifest of a [Patch].
|
* A package a [Patch] is compatible with.
|
||||||
*
|
*
|
||||||
* @param name The name of the patch.
|
* @param name The name of the package.
|
||||||
* @param description The description of the patch.
|
* @param versions The versions of the package.
|
||||||
* @param use Weather or not the patch should be used.
|
|
||||||
* @param dependencies The names of patches this patch depends on.
|
|
||||||
* @param compatiblePackages The packages the patch is compatible with.
|
|
||||||
* @param requiresIntegrations Weather or not the patch requires integrations.
|
|
||||||
* @param options The options of the patch.
|
|
||||||
*/
|
*/
|
||||||
class Manifest(
|
class CompatiblePackage(
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
versions: Set<String>? = null,
|
||||||
val use: Boolean = true,
|
)
|
||||||
val dependencies: Set<PatchClass>? = null,
|
|
||||||
val compatiblePackages: Set<CompatiblePackage>? = null,
|
|
||||||
// TODO: Remove this property, once integrations are coupled with patches.
|
|
||||||
val requiresIntegrations: Boolean = false,
|
|
||||||
val options: PatchOptions? = null,
|
|
||||||
) {
|
|
||||||
override fun hashCode() = name.hashCode()
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as Manifest
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ReVanced [Patch] that works on [ResourceContext].
|
* A ReVanced [Patch] that works on [ResourceContext].
|
||||||
*
|
*
|
||||||
* @param metadata The manifest of the [ResourcePatch].
|
* @param name The name of the patch.
|
||||||
|
* @param description The description of the patch.
|
||||||
|
* @param compatiblePackages The packages the patch is compatible with.
|
||||||
|
* @param dependencies The names of patches this patch depends on.
|
||||||
|
* @param use Weather or not the patch should be used.
|
||||||
|
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||||
*/
|
*/
|
||||||
abstract class ResourcePatch(
|
abstract class ResourcePatch(
|
||||||
metadata: Manifest,
|
name: String? = null,
|
||||||
) : Patch<ResourceContext>(metadata)
|
description: String? = null,
|
||||||
|
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||||
|
dependencies: Set<PatchClass>? = null,
|
||||||
|
use: Boolean = true,
|
||||||
|
// TODO: Remove this property, once integrations are coupled with patches.
|
||||||
|
requiresIntegrations: Boolean = false,
|
||||||
|
) : Patch<ResourceContext>(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ReVanced [Patch] that works on [BytecodeContext].
|
* A ReVanced [Patch] that works on [BytecodeContext].
|
||||||
*
|
*
|
||||||
* @param manifest The manifest of the [BytecodePatch].
|
|
||||||
* @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed.
|
* @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed.
|
||||||
|
* @param name The name of the patch.
|
||||||
|
* @param description The description of the patch.
|
||||||
|
* @param compatiblePackages The packages the patch is compatible with.
|
||||||
|
* @param dependencies The names of patches this patch depends on.
|
||||||
|
* @param use Weather or not the patch should be used.
|
||||||
|
* @param requiresIntegrations Weather or not the patch requires integrations.
|
||||||
*/
|
*/
|
||||||
abstract class BytecodePatch(
|
abstract class BytecodePatch(
|
||||||
manifest: Manifest,
|
internal val fingerprints: Set<MethodFingerprint> = emptySet(),
|
||||||
internal vararg val fingerprints: MethodFingerprint,
|
name: String? = null,
|
||||||
) : Patch<BytecodeContext>(manifest)
|
description: String? = null,
|
||||||
|
compatiblePackages: Set<CompatiblePackage>? = null,
|
||||||
|
dependencies: Set<PatchClass>? = null,
|
||||||
|
use: Boolean = true,
|
||||||
|
// TODO: Remove this property, once integrations are coupled with patches.
|
||||||
|
requiresIntegrations: Boolean = false,
|
||||||
|
) : Patch<BytecodeContext>(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
|
|
@ -2,8 +2,6 @@
|
||||||
|
|
||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import java.nio.file.Path
|
|
||||||
import kotlin.io.path.pathString
|
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
|
class NoSuchOptionException(val option: String) : Exception("No such option: $option")
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package app.revanced.patcher.patch.annotations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation to mark a class as a patch.
|
|
||||||
*/
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
annotation class Patch
|
|
|
@ -19,7 +19,7 @@ import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
private object InstructionExtensionsTest {
|
private object InstructionExtensionsTest {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package app.revanced.patcher.patch
|
package app.revanced.patcher.patch
|
||||||
|
|
||||||
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
|
import app.revanced.patcher.usage.ExampleBytecodePatch
|
||||||
import org.junit.jupiter.api.Test
|
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
import kotlin.test.Test
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertNotEquals
|
import kotlin.test.assertNotEquals
|
||||||
import kotlin.test.assertNotNull
|
import kotlin.test.assertNotNull
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
package app.revanced.patcher.usage.bytecode
|
package app.revanced.patcher.usage
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
import app.revanced.patcher.extensions.or
|
import app.revanced.patcher.extensions.or
|
||||||
import app.revanced.patcher.patch.BytecodePatch
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
import app.revanced.patcher.patch.OptionsContainer
|
|
||||||
import app.revanced.patcher.patch.PatchOption
|
import app.revanced.patcher.patch.PatchOption
|
||||||
|
import app.revanced.patcher.patch.annotations.CompatiblePackage
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
import app.revanced.patcher.patch.annotations.Patch
|
||||||
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
|
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
import com.android.tools.smali.dexlib2.AccessFlags
|
import com.android.tools.smali.dexlib2.AccessFlags
|
||||||
|
@ -26,19 +25,19 @@ import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringRefere
|
||||||
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
|
||||||
import com.android.tools.smali.dexlib2.util.Preconditions
|
import com.android.tools.smali.dexlib2.util.Preconditions
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
|
import kotlin.test.assertNotNull
|
||||||
|
|
||||||
@Patch
|
@Suppress("unused")
|
||||||
class ExampleBytecodePatch : BytecodePatch(
|
@Patch(
|
||||||
Manifest(
|
name = "Example bytecode patch",
|
||||||
"Example patch",
|
description = "Example demonstration of a bytecode patch.",
|
||||||
"Example demonstration of a bytecode patch.",
|
dependencies = [ExampleResourcePatch::class],
|
||||||
dependencies = setOf(ExampleResourcePatch::class),
|
compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))]
|
||||||
compatiblePackages = setOf(
|
)
|
||||||
Manifest.CompatiblePackage("com.example.examplePackage", setOf("0.0.1", "0.0.2"))
|
object ExampleBytecodePatch : BytecodePatch(
|
||||||
)
|
setOf(ExampleFingerprint)
|
||||||
),
|
|
||||||
ExampleFingerprint
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// This function will be executed by the patcher.
|
// This function will be executed by the patcher.
|
||||||
// You can treat it as a constructor
|
// You can treat it as a constructor
|
||||||
override fun execute(context: BytecodeContext) {
|
override fun execute(context: BytecodeContext) {
|
||||||
|
@ -46,7 +45,7 @@ class ExampleBytecodePatch : BytecodePatch(
|
||||||
val result = ExampleFingerprint.result!!
|
val result = ExampleFingerprint.result!!
|
||||||
|
|
||||||
// Patch options
|
// Patch options
|
||||||
println(key1)
|
assertNotNull(key1)
|
||||||
key2 = false
|
key2 = false
|
||||||
|
|
||||||
// Get the implementation for the resolved method
|
// Get the implementation for the resolved method
|
||||||
|
@ -161,32 +160,34 @@ class ExampleBytecodePatch : BytecodePatch(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
private var key1 by option(
|
||||||
companion object : OptionsContainer() {
|
PatchOption.StringOption(
|
||||||
private var key1 by option(
|
"key1", "default", "title", "description", true
|
||||||
PatchOption.StringOption(
|
|
||||||
"key1", "default", "title", "description", true
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
private var key2 by option(
|
)
|
||||||
PatchOption.BooleanOption(
|
|
||||||
"key2", true, "title", "description" // required defaults to false
|
private var key2 by option(
|
||||||
)
|
PatchOption.BooleanOption(
|
||||||
|
"key2", true, "title", "description" // required defaults to false
|
||||||
)
|
)
|
||||||
private var key3 by option(
|
)
|
||||||
PatchOption.StringListOption(
|
|
||||||
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
|
private var key3 by option(
|
||||||
)
|
PatchOption.StringListOption(
|
||||||
|
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
|
||||||
)
|
)
|
||||||
private var key4 by option(
|
)
|
||||||
PatchOption.IntListOption(
|
|
||||||
"key4", 1, listOf(1, 2, 3), "title", "description"
|
private var key4 by option(
|
||||||
)
|
PatchOption.IntListOption(
|
||||||
|
"key4", 1, listOf(1, 2, 3), "title", "description"
|
||||||
)
|
)
|
||||||
private var key5 by option(
|
)
|
||||||
PatchOption.StringOption(
|
|
||||||
"key5", null, "title", "description", true
|
private var key5 by option(
|
||||||
)
|
PatchOption.StringOption(
|
||||||
|
"key5", null, "title", "description", true
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package app.revanced.patcher.usage.bytecode
|
package app.revanced.patcher.usage
|
||||||
import app.revanced.patcher.extensions.or
|
import app.revanced.patcher.extensions.or
|
||||||
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
|
||||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
|
|
@ -1,10 +1,11 @@
|
||||||
package app.revanced.patcher.usage.resource.patch
|
package app.revanced.patcher.usage
|
||||||
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
import app.revanced.patcher.data.ResourceContext
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
import app.revanced.patcher.patch.ResourcePatch
|
||||||
import org.w3c.dom.Element
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
class ExampleResourcePatch : ResourcePatch(Manifest("Example name", "Example description")) {
|
|
||||||
|
class ExampleResourcePatch : ResourcePatch() {
|
||||||
override fun execute(context: ResourceContext) {
|
override fun execute(context: ResourceContext) {
|
||||||
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
context.xmlEditor["AndroidManifest.xml"].use { editor ->
|
||||||
val element = editor // regular DomFileEditor
|
val element = editor // regular DomFileEditor
|
|
@ -19,4 +19,4 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
include("revanced-patcher")
|
include("revanced-patch-annotations-processor", "revanced-patcher")
|
Loading…
Reference in a new issue