diff --git a/build.gradle.kts b/build.gradle.kts index 402fc71..dc5e76f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,58 +1,4 @@ plugins { - kotlin("jvm") version "1.8.20" - `maven-publish` + kotlin("jvm") version "1.9.0" apply false alias(libs.plugins.binary.compatibility.validator) } - -group = "app.revanced" - -dependencies { - implementation(libs.kotlinx.coroutines.core) - implementation(libs.xpp3) - implementation(libs.smali) - implementation(libs.multidexlib2) - implementation(libs.apktool.lib) - implementation(libs.kotlin.reflect) - - compileOnly(libs.android) - - testImplementation(libs.kotlin.test) -} - -tasks { - test { - useJUnitPlatform() - testLogging { - events("PASSED", "SKIPPED", "FAILED") - } - } - - processResources { - expand("projectVersion" to project.version) - } -} - -kotlin { jvmToolchain(11) } - -java { - withSourcesJar() -} - -publishing { - repositories { - mavenLocal() - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } - publications { - create("gpr") { - from(components["java"]) - } - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b214cb..512c296 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,12 @@ kotlin-test = "1.8.20-RC" kotlinx-coroutines-core = "1.7.1" multidexlib2 = "3.0.3.r2" smali = "3.0.3" +symbol-processing-api = "1.9.0-1.0.11" xpp3 = "1.1.4c" 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] 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" } multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" } 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" } +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] binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/revanced-patch-annotations-processor/api/revanced-patch-annotations-processor.api b/revanced-patch-annotations-processor/api/revanced-patch-annotations-processor.api new file mode 100644 index 0000000..87bc05c --- /dev/null +++ b/revanced-patch-annotations-processor/api/revanced-patch-annotations-processor.api @@ -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 (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 ()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; +} + diff --git a/revanced-patch-annotations-processor/build.gradle.kts b/revanced-patch-annotations-processor/build.gradle.kts new file mode 100644 index 0000000..7b0f0be --- /dev/null +++ b/revanced-patch-annotations-processor/build.gradle.kts @@ -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("gpr") { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/settings.gradle.kts b/revanced-patch-annotations-processor/settings.gradle.kts new file mode 100644 index 0000000..a376e09 --- /dev/null +++ b/revanced-patch-annotations-processor/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "revanced-patch-annotations-processor" + diff --git a/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotations.kt b/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotations.kt new file mode 100644 index 0000000..6c28e6e --- /dev/null +++ b/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotations.kt @@ -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>> = [], + val compatiblePackages: Array = [], + 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 = [], +) \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/processor/PatchProcessor.kt b/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/processor/PatchProcessor.kt new file mode 100644 index 0000000..f597e8d --- /dev/null +++ b/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/processor/PatchProcessor.kt @@ -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 { + 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).ifEmpty { null } + + val compatiblePackages = + (annotation.property("compatiblePackages") as List).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?, + val compatiblePackages: List?, + 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) + .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() + } +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/processor/PatchProcessorProvider.kt b/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/processor/PatchProcessorProvider.kt new file mode 100644 index 0000000..95abc37 --- /dev/null +++ b/revanced-patch-annotations-processor/src/main/kotlin/app/revanced/patcher/patch/annotations/processor/PatchProcessorProvider.kt @@ -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) +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/revanced-patch-annotations-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 0000000..0a48684 --- /dev/null +++ b/revanced-patch-annotations-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +app.revanced.patcher.patch.annotations.processor.PatchProcessorProvider \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/kotlin/app/revanced/patcher/patch/annotations/processor/TestPatchAnnotationProcessor.kt b/revanced-patch-annotations-processor/src/test/kotlin/app/revanced/patcher/patch/annotations/processor/TestPatchAnnotationProcessor.kt new file mode 100644 index 0000000..850f261 --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/kotlin/app/revanced/patcher/patch/annotations/processor/TestPatchAnnotationProcessor.kt @@ -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 + } +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/dependencies/DependencyPatch.kt b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/dependencies/DependencyPatch.kt new file mode 100644 index 0000000..c03169d --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/dependencies/DependencyPatch.kt @@ -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) {} +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/dependencies/DependentPatch.kt b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/dependencies/DependentPatch.kt new file mode 100644 index 0000000..44d6df0 --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/dependencies/DependentPatch.kt @@ -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) {} +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/limitations/manualdependency/DependencyPatch.kt b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/limitations/manualdependency/DependencyPatch.kt new file mode 100644 index 0000000..d43bd2c --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/limitations/manualdependency/DependencyPatch.kt @@ -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) { } +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/limitations/manualdependency/DependentPatch.kt b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/limitations/manualdependency/DependentPatch.kt new file mode 100644 index 0000000..60121ec --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/limitations/manualdependency/DependentPatch.kt @@ -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) {} +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/options/OptionsPatch.kt b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/options/OptionsPatch.kt new file mode 100644 index 0000000..4aaf93e --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/options/OptionsPatch.kt @@ -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." + ) + ) +} \ No newline at end of file diff --git a/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/processing/ProcessablePatch.kt b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/processing/ProcessablePatch.kt new file mode 100644 index 0000000..28d04a0 --- /dev/null +++ b/revanced-patch-annotations-processor/src/test/resources/app/revanced/patcher/patch/annotations/processor/samples/processing/ProcessablePatch.kt @@ -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) {} +} \ No newline at end of file diff --git a/api/revanced-patcher.api b/revanced-patcher/api/revanced-patcher.api similarity index 91% rename from api/revanced-patcher.api rename to revanced-patcher/api/revanced-patcher.api index 7a761a9..8987c4e 100644 --- a/api/revanced-patcher.api +++ b/revanced-patcher/api/revanced-patcher.api @@ -7,54 +7,56 @@ public final class app/revanced/patcher/PackageMetadata { public final fun getPackageVersion ()Ljava/lang/String; } -public abstract class app/revanced/patcher/PatchBundleLoader : java/util/List, kotlin/jvm/internal/markers/KMutableList { - public synthetic fun (Ljava/lang/Iterable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun add (ILjava/lang/Class;)V - public synthetic fun add (ILjava/lang/Object;)V - public fun add (Ljava/lang/Class;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (ILjava/util/Collection;)Z - public fun addAll (Ljava/util/Collection;)Z +public abstract class app/revanced/patcher/PatchBundleLoader : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { + public synthetic fun (Ljava/lang/ClassLoader;[Ljava/io/File;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun clear ()V - public fun contains (Ljava/lang/Class;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public fun get (I)Ljava/lang/Class; - public synthetic fun get (I)Ljava/lang/Object; + public synthetic fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun compute (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object; + public fun computeIfAbsent (Ljava/lang/String;Ljava/util/function/Function;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun computeIfPresent (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch; + public final fun containsKey (Ljava/lang/Object;)Z + public fun containsKey (Ljava/lang/String;)Z + public fun containsValue (Lapp/revanced/patcher/patch/Patch;)Z + public final fun containsValue (Ljava/lang/Object;)Z + public final fun entrySet ()Ljava/util/Set; + public final fun get (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Patch; + public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; + public fun get (Ljava/lang/String;)Lapp/revanced/patcher/patch/Patch; + public fun getEntries ()Ljava/util/Set; + public fun getKeys ()Ljava/util/Set; public fun getSize ()I - public fun indexOf (Ljava/lang/Class;)I - public final fun indexOf (Ljava/lang/Object;)I + public fun getValues ()Ljava/util/Collection; public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun lastIndexOf (Ljava/lang/Class;)I - public final fun lastIndexOf (Ljava/lang/Object;)I - public fun listIterator ()Ljava/util/ListIterator; - public fun listIterator (I)Ljava/util/ListIterator; - public final fun remove (I)Ljava/lang/Class; - public synthetic fun remove (I)Ljava/lang/Object; - public fun remove (Ljava/lang/Class;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun removeAt (I)Ljava/lang/Class; - public fun retainAll (Ljava/util/Collection;)Z - public fun set (ILjava/lang/Class;)Ljava/lang/Class; - public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; + public final fun keySet ()Ljava/util/Set; + public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun merge (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch; + public fun putAll (Ljava/util/Map;)V + public synthetic fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun putIfAbsent (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch; + public fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object; + public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z + public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z + public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch; + public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;Lapp/revanced/patcher/patch/Patch;)Z + public fun replaceAll (Ljava/util/function/BiFunction;)V public final fun size ()I - public fun subList (II)Ljava/util/List; - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public final fun values ()Ljava/util/Collection; } public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/patcher/PatchBundleLoader { public fun ([Ljava/io/File;)V public fun ([Ljava/io/File;Ljava/io/File;)V public synthetic fun ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun remove (I)Ljava/lang/Object; } public final class app/revanced/patcher/PatchBundleLoader$Jar : app/revanced/patcher/PatchBundleLoader { public fun ([Ljava/io/File;)V - public synthetic fun remove (I)Ljava/lang/Object; } public abstract interface class app/revanced/patcher/PatchExecutorFunction : java/util/function/Function { @@ -76,6 +78,14 @@ public final class app/revanced/patcher/PatcherContext { public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata; } +public abstract class app/revanced/patcher/PatcherException : java/lang/Exception { + public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/PatcherException$CircularDependencyException : app/revanced/patcher/PatcherException { +} + public final class app/revanced/patcher/PatcherOptions { public fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V public synthetic fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -115,23 +125,6 @@ public abstract interface class app/revanced/patcher/PatchesConsumer { public abstract fun acceptPatches (Ljava/util/List;)V } -public abstract interface annotation class app/revanced/patcher/annotation/Compatibility : java/lang/annotation/Annotation { - public abstract fun compatiblePackages ()[Lapp/revanced/patcher/annotation/Package; -} - -public abstract interface annotation class app/revanced/patcher/annotation/Description : java/lang/annotation/Annotation { - public abstract fun description ()Ljava/lang/String; -} - -public abstract interface annotation class app/revanced/patcher/annotation/Name : java/lang/annotation/Annotation { - public abstract fun name ()Ljava/lang/String; -} - -public abstract interface annotation class app/revanced/patcher/annotation/Package : java/lang/annotation/Annotation { - public abstract fun name ()Ljava/lang/String; - public abstract fun versions ()[Ljava/lang/String; -} - public final class app/revanced/patcher/data/BytecodeContext : app/revanced/patcher/data/Context { public final fun findClass (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy; public final fun findClass (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy; @@ -199,17 +192,6 @@ public final class app/revanced/patcher/extensions/InstructionExtensions { public final class app/revanced/patcher/extensions/MethodFingerprintExtensions { public static final field INSTANCE Lapp/revanced/patcher/extensions/MethodFingerprintExtensions; public final fun getFuzzyPatternScanMethod (Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/method/annotation/FuzzyPatternScanMethod; - public final fun getName (Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)Ljava/lang/String; -} - -public final class app/revanced/patcher/extensions/PatchExtensions { - public static final field INSTANCE Lapp/revanced/patcher/extensions/PatchExtensions; - public final fun getCompatiblePackages (Ljava/lang/Class;)[Lapp/revanced/patcher/annotation/Package; - public final fun getDependencies (Ljava/lang/Class;)[Lkotlin/reflect/KClass; - public final fun getDescription (Ljava/lang/Class;)Ljava/lang/String; - public final fun getInclude (Ljava/lang/Class;)Z - public final fun getOptions (Ljava/lang/Class;)Lapp/revanced/patcher/patch/PatchOptions; - public final fun getPatchName (Ljava/lang/Class;)Ljava/lang/String; } public abstract interface class app/revanced/patcher/fingerprint/Fingerprint { @@ -346,8 +328,8 @@ 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 fun ()V - public fun (Ljava/lang/Iterable;)V - public synthetic fun (Ljava/lang/Iterable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/util/Set;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V + public synthetic fun (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 { @@ -368,12 +350,29 @@ public final class app/revanced/patcher/patch/NoSuchOptionException : java/lang/ public abstract class app/revanced/patcher/patch/OptionsContainer { public fun ()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; } -public abstract interface class app/revanced/patcher/patch/Patch { +public abstract class app/revanced/patcher/patch/Patch : app/revanced/patcher/patch/OptionsContainer { + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (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 abstract fun execute (Lapp/revanced/patcher/data/Context;)V + public final fun getCompatiblePackages ()Ljava/util/Set; + public final fun getDependencies ()Ljava/util/Set; + public final fun getDescription ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getRequiresIntegrations ()Z + public final fun getUse ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/revanced/patcher/patch/Patch$CompatiblePackage { + public fun (Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getName ()Ljava/lang/String; } public final class app/revanced/patcher/patch/PatchException : java/lang/Exception { @@ -429,26 +428,20 @@ public final class app/revanced/patcher/patch/PatchOptions : java/lang/Iterable, } public final class app/revanced/patcher/patch/PatchResult { + public fun equals (Ljava/lang/Object;)Z public final fun getException ()Lapp/revanced/patcher/patch/PatchException; - public final fun getPatchName ()Ljava/lang/String; + public final fun getPatch ()Lapp/revanced/patcher/patch/Patch; + public fun hashCode ()I } public final class app/revanced/patcher/patch/RequirementNotMetException : java/lang/Exception { public static final field INSTANCE Lapp/revanced/patcher/patch/RequirementNotMetException; } -public abstract interface class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { -} - -public abstract interface annotation class app/revanced/patcher/patch/annotations/DependsOn : java/lang/annotation/Annotation { - public abstract fun dependencies ()[Ljava/lang/Class; -} - -public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation { - public abstract fun include ()Z -} - -public abstract interface annotation class app/revanced/patcher/patch/annotations/RequiresIntegrations : java/lang/annotation/Annotation { +public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V } public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable { diff --git a/revanced-patcher/build.gradle.kts b/revanced-patcher/build.gradle.kts new file mode 100644 index 0000000..f8a97f4 --- /dev/null +++ b/revanced-patcher/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + kotlin("jvm") version "1.9.0" + `maven-publish` +} + +group = "app.revanced" + +dependencies { + implementation(libs.kotlinx.coroutines.core) + implementation(libs.xpp3) + implementation(libs.smali) + implementation(libs.multidexlib2) + implementation(libs.apktool.lib) + implementation(libs.kotlin.reflect) + + compileOnly(libs.android) + + testImplementation(project(":revanced-patch-annotations-processor")) + testImplementation(libs.kotlin.test) +} + +tasks { + test { + useJUnitPlatform() + testLogging { + events("PASSED", "SKIPPED", "FAILED") + } + } + + processResources { + expand("projectVersion" to project.version) + } +} + +kotlin { jvmToolchain(11) } + +java { + withSourcesJar() +} + +publishing { + repositories { + mavenLocal() + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + publications { + create("gpr") { + from(components["java"]) + } + } +} diff --git a/revanced-patcher/settings.gradle.kts b/revanced-patcher/settings.gradle.kts new file mode 100644 index 0000000..2a8c853 --- /dev/null +++ b/revanced-patcher/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "revanced-patcher" \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt diff --git a/src/main/kotlin/app/revanced/patcher/PackageMetadata.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PackageMetadata.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/PackageMetadata.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/PackageMetadata.kt diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt new file mode 100644 index 0000000..d58cf41 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt @@ -0,0 +1,124 @@ +@file:Suppress("unused") + +package app.revanced.patcher + +import app.revanced.patcher.patch.Patch +import dalvik.system.DexClassLoader +import lanchon.multidexlib2.BasicDexFileNamer +import lanchon.multidexlib2.MultiDexIO +import java.io.File +import java.net.URLClassLoader +import java.util.jar.JarFile +import java.util.logging.Logger +import kotlin.reflect.KClass + +/** + * [Patch]es mapped by their name. + */ +typealias PatchMap = Map> + +/** + * A [Patch] class. + */ +typealias PatchClass = KClass> + +/** + * 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 classLoader The [ClassLoader] to use for loading the classes. + */ +sealed class PatchBundleLoader private constructor( + classLoader: ClassLoader, + patchBundles: Array, + getBinaryClassNames: (patchBundle: File) -> List, +) : PatchMap by mutableMapOf() { + private val logger = Logger.getLogger(PatchBundleLoader::class.java.name) + + init { + patchBundles.flatMap(getBinaryClassNames).asSequence().map { + classLoader.loadClass(it) + }.filter { + it.isInstance(Patch::class.java) + }.mapNotNull { patchClass -> + patchClass.getInstance(logger) + }.filter { + it.name != null + }.associateBy { + it.name!! + }.let { patches -> + @Suppress("UNCHECKED_CAST") + (this as MutableMap>).putAll(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. + * @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated. + */ + internal fun Class<*>.getInstance(logger: Logger): Patch<*>? { + return try { + getField("INSTANCE").get(null) + } catch (exception: NoSuchFileException) { + logger.fine( + "Patch class '${name}' has no INSTANCE field, therefor not a singleton. " + + "Will try to instantiate it." + ) + + try { + getDeclaredConstructor().newInstance() + } catch (exception: Exception) { + 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<*> + } + } + + /** + * A [PatchBundleLoader] for JAR files. + * + * @param patchBundles The path to patch bundles of JAR format. + */ + class Jar(vararg patchBundles: File) : PatchBundleLoader( + URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()), + patchBundles, + { patchBundle -> + JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") } + .map { it.name.replace('/', '.').replace(".class", "") } + } + ) + + /** + * A [PatchBundleLoader] for [Dex] files. + * + * @param patchBundles The path to patch bundles of DEX format. + * @param optimizedDexDirectory The directory to store optimized DEX files in. + * This parameter is deprecated and has no effect since API level 26. + */ + class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader( + DexClassLoader( + patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath, + null, + PatchBundleLoader::class.java.classLoader + ), + patchBundles, + { patchBundle -> + MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes + .map { classDef -> + classDef.type.substring(1, classDef.length - 1) + } + } + ) { + @Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.") + constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt new file mode 100644 index 0000000..2874040 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -0,0 +1,276 @@ +package app.revanced.patcher + +import app.revanced.patcher.PatchBundleLoader.Utils.getInstance +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap +import app.revanced.patcher.patch.* +import kotlinx.coroutines.flow.flow +import java.io.Closeable +import java.io.File +import java.util.function.Supplier +import java.util.logging.Level +import java.util.logging.LogManager +import java.util.logging.Logger + +/** + * ReVanced Patcher. + * + * @param options The options for the patcher. + */ +class Patcher( + private val options: PatcherOptions +) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier, Closeable { + private val logger = Logger.getLogger(Patcher::class.java.name) + + /** + * The context of ReVanced [Patcher]. + * This holds the current state of the patcher. + */ + val context = PatcherContext(options) + + init { + LogManager.getLogManager().let { manager -> + // Disable root logger. + manager.getLogger("").level = Level.OFF + + // Enable ReVanced logging only. + manager.loggerNames + .toList() + .filter { it.startsWith("app.revanced") } + .map { manager.getLogger(it) } + .forEach { it.level = Level.INFO } + } + + 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]. + * + * @param patches The [Patch]es to add. + */ + @Suppress("NAME_SHADOWING") + override fun acceptPatches(patches: List>) { + /** + * Add dependencies of a [Patch] recursively to [PatcherContext.allPatches]. + * If a [Patch] is already in [PatcherContext.allPatches], it will not be added again. + */ + fun PatchClass.putDependenciesRecursively() { + if (context.allPatches.contains(this)) return + + val dependency = this.java.getInstance(logger)!! + context.allPatches[this] = dependency + + dependency.dependencies?.forEach { it.putDependenciesRecursively() } + } + + // Add all patches and their dependencies to the context. + for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: { + context.allPatches[patch::class] = patch + + patch.dependencies?.forEach { it.putDependenciesRecursively() } + } + + /* TODO: Fix circular dependency detection. + val graph = mutableMapOf>() + fun PatchClass.visit() { + if (this in graph) return + + val group = graph.getOrPut(this) { mutableListOf(this) } + + val dependencies = context.allPatches[this]!!.manifest.dependencies ?: return + dependencies.forEach { dependency -> + if (group == graph[dependency]) + throw PatcherException.CircularDependencyException(context.allPatches[this]!!.manifest.name) + + graph[dependency] = group.apply { add(dependency) } + dependency.visit() + } + } + */ + + /** + * Returns true if at least one patch or its dependencies matches the given predicate. + * + * @param predicate The predicate to match. + */ + fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean = + predicate(this) || dependencies?.any { dependency -> + context.allPatches[dependency]!!.anyRecursively(predicate) + } ?: false + + context.allPatches.values.let { patches -> + // Determine, if resource patching is required. + for (patch in patches) + if (patch.anyRecursively { patch is ResourcePatch }) { + options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL + break + } + + // Determine, if merging integrations is required. + for (patch in patches) + if (!patch.anyRecursively { it.requiresIntegrations }) { + context.bytecodeContext.integrations.merge = true + break + } + } + } + + /** + * Add integrations to the [Patcher]. + * + * @param integrations The integrations to add. Must be a DEX file or container of DEX files. + */ + override fun acceptIntegrations(integrations: List) { + context.bytecodeContext.integrations.addAll(integrations) + } + + /** + * Execute [Patch]es that were added to ReVanced [Patcher]. + * + * @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails. + * @return A pair of the name of the [Patch] and its [PatchResult]. + */ + override fun apply(returnOnError: Boolean) = flow { + + /** + * Execute a [Patch] and its dependencies recursively. + * + * @param patch The [Patch] to execute. + * @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies. + * @return The result of executing the [Patch]. + */ + fun executePatch( + patch: Patch<*>, + executedPatches: LinkedHashMap, PatchResult> + ): PatchResult { + val patchName = patch.name + + executedPatches[patch]?.let { patchResult -> + patchResult.exception ?: return patchResult + + // Return a new result with an exception indicating that the patch was not executed previously, + // because it is a dependency of another patch that failed. + return PatchResult(patch, PatchException("'$patchName' did not succeed previously")) + } + + // Recursively execute all dependency patches. + patch.dependencies?.forEach { dependencyName -> + val dependency = context.executablePatches[dependencyName]!! + val result = executePatch(dependency, executedPatches) + + result.exception?.let { + return PatchResult( + patch, + PatchException("'$patchName' depends on '${dependency}' that raised an exception: $it") + ) + } + } + + return try { + // 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) + } catch (exception: PatchException) { + PatchResult(patch, exception) + } catch (exception: Exception) { + PatchResult(patch, PatchException(exception)) + }.also { executedPatches[patch] = it } + } + + if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() + + MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext) + + // Prevent from decoding the app manifest twice if it is not needed. + if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) + context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL) + + logger.info("Executing patches") + + val executedPatches = LinkedHashMap, PatchResult>() // Key is name. + + context.executablePatches.map { it.value }.sortedBy { it.name }.forEach { patch -> + val patchResult = executePatch(patch, executedPatches) + + // If the patch failed, emit the result, even if it is closeable. + // Results of executed patches that are closeable will be emitted later. + patchResult.exception?.let { + // Propagate exception to caller instead of wrapping it in a new exception. + emit(patchResult) + + if (returnOnError) return@flow + } ?: run { + if (patch is Closeable) return@run + + emit(patchResult) + } + } + + executedPatches.values + .filter { it.exception == null } + .filter { it.patch is Closeable }.asReversed().forEach { executedPatch -> + val patch = executedPatch.patch + + val result = try { + (patch as Closeable).close() + + executedPatch + } catch (exception: PatchException) { + PatchResult(patch, exception) + } catch (exception: Exception) { + PatchResult(patch, PatchException(exception)) + } + + result.exception?.let { + emit( + PatchResult( + patch, + PatchException( + "'${patch.name}' raised an exception while being closed: $it", + result.exception + ) + ) + ) + + if (returnOnError) return@flow + } ?: run { + patch.name ?: return@run + + emit(result) + } + } + } + + override fun close() = MethodFingerprint.clearFingerprintResolutionLookupMaps() + + /** + * Compile and save the patched APK file. + * + * @return The [PatcherResult] containing the patched input files. + */ + override fun get() = PatcherResult( + context.bytecodeContext.get(), + context.resourceContext.get(), + context.packageMetadata.apkInfo.doNotCompress?.toList() + ) +} + diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherContext.kt similarity index 71% rename from src/main/kotlin/app/revanced/patcher/PatcherContext.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherContext.kt index 4aa8396..c21cfa6 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherContext.kt @@ -3,7 +3,6 @@ package app.revanced.patcher import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchClass import brut.androlib.apk.ApkInfo import brut.directory.ExtFile @@ -19,9 +18,14 @@ class PatcherContext internal constructor(options: PatcherOptions) { val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile))) /** - * The list of [Patch]es to execute. + * The map of [Patch]es associated by their [PatchClass]. */ - internal val patches = mutableListOf() + internal val executablePatches = mutableMapOf>() + + /** + * The map of all [Patch]es and their dependencies associated by their [PatchClass]. + */ + internal val allPatches = mutableMapOf>() /** * The [ResourceContext] of this [PatcherContext]. @@ -33,5 +37,4 @@ class PatcherContext internal constructor(options: PatcherOptions) { * The [BytecodeContext] of this [PatcherContext]. * This holds the current state of the bytecode. */ - internal val bytecodeContext = BytecodeContext(options) -} \ No newline at end of file + internal val bytecodeContext = BytecodeContext(options) } \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherException.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherException.kt new file mode 100644 index 0000000..2313a99 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherException.kt @@ -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" + ) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/PatcherOptions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt diff --git a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherResult.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/PatcherResult.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/PatcherResult.kt diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt new file mode 100644 index 0000000..866b7e5 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt @@ -0,0 +1,8 @@ +package app.revanced.patcher + +import app.revanced.patcher.patch.Patch + +@FunctionalInterface +interface PatchesConsumer { + fun acceptPatches(patches: List>) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt similarity index 98% rename from src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt index 3374930..439b430 100644 --- a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt @@ -4,7 +4,6 @@ import app.revanced.patcher.PatcherContext import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherResult 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.ProxyClassList import app.revanced.patcher.util.method.MethodWalker diff --git a/src/main/kotlin/app/revanced/patcher/data/Context.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/Context.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/data/Context.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/data/Context.kt diff --git a/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt diff --git a/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt similarity index 79% rename from src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt index 1901044..e722f7d 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt @@ -5,13 +5,7 @@ import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint object MethodFingerprintExtensions { - - /** - * The name of a [MethodFingerprint]. - */ - val MethodFingerprint.name: String - get() = this.javaClass.simpleName - + // TODO: Make this a property. /** * The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint]. */ diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/Fingerprint.kt diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/annotation/MethodFingerprintMetadata.kt diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt similarity index 99% rename from src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt index 4858721..3682a02 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt @@ -159,7 +159,7 @@ abstract class MethodFingerprint( * - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. */ - internal fun Iterable.resolveUsingLookupMap(context: BytecodeContext) { + internal fun List.resolveUsingLookupMap(context: BytecodeContext) { if (methods.isEmpty()) throw PatchException("lookup map not initialized") for (fingerprint in this) { diff --git a/src/main/kotlin/app/revanced/patcher/logging/Logger.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/Logger.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/logging/Logger.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/Logger.kt diff --git a/src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/logging/impl/NopLogger.kt diff --git a/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt similarity index 66% rename from src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt index f14ca48..52320a3 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt @@ -9,8 +9,13 @@ abstract class OptionsContainer { * @see PatchOptions */ @Suppress("MemberVisibilityCanBePrivate") - val options = PatchOptions() + open val options = PatchOptions() + /** + * Registers a [PatchOption]. + * @param opt The [PatchOption] to register. + * @return The registered [PatchOption]. + */ protected fun option(opt: PatchOption): PatchOption { options.register(opt) return opt diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt new file mode 100644 index 0000000..549180e --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -0,0 +1,109 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "UNUSED_PARAMETER") + +package app.revanced.patcher.patch + +import app.revanced.patcher.PatchClass +import app.revanced.patcher.Patcher +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.data.Context +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import java.io.Closeable + +/** + * 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]. + * + * @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. + */ +sealed class Patch>( + val name: String? = null, + val description: String? = null, + val compatiblePackages: Set? = null, + val dependencies: Set? = 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. + * + * @param context The [Context] the patch will work on. + * @return The result of executing the patch. + */ + abstract fun execute(context: @UnsafeVariance T) + + override fun hashCode() = name.hashCode() + + override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Patch<*> + + return name == other.name + } + + /** + * A package a [Patch] is compatible with. + * + * @param name The name of the package. + * @param versions The versions of the package. + */ + class CompatiblePackage( + val name: String, + versions: Set? = null, + ) +} + +/** + * A ReVanced [Patch] that works on [ResourceContext]. + * + * @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( + name: String? = null, + description: String? = null, + compatiblePackages: Set? = null, + dependencies: Set? = null, + use: Boolean = true, + // TODO: Remove this property, once integrations are coupled with patches. + requiresIntegrations: Boolean = false, +) : Patch(name, description, compatiblePackages, dependencies, use, requiresIntegrations) + +/** + * A ReVanced [Patch] that works on [BytecodeContext]. + * + * @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( + internal val fingerprints: Set = emptySet(), + name: String? = null, + description: String? = null, + compatiblePackages: Set? = null, + dependencies: Set? = null, + use: Boolean = true, + // TODO: Remove this property, once integrations are coupled with patches. + requiresIntegrations: Boolean = false, +) : Patch(name, description, compatiblePackages, dependencies, use, requiresIntegrations) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/patch/PatchException.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt similarity index 99% rename from src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt index ecf3a85..328debe 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt @@ -2,8 +2,6 @@ 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") diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt new file mode 100644 index 0000000..5be69f5 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt @@ -0,0 +1,20 @@ +package app.revanced.patcher.patch + +/** + * A result of executing a [Patch]. + * + * @param patch The [Patch] that was executed. + * @param exception The [PatchException] thrown, if any. + */ +@Suppress("MemberVisibilityCanBePrivate") +class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null) { + override fun hashCode() = patch.hashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PatchResult + + return patch == other.patch + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotationElement.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableClass.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableField.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethodParameter.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableAnnotationEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableArrayEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableBooleanEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableByteEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableCharEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableDoubleEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableEnumEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFieldEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableFloatEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableIntEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableLongEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodHandleEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableMethodTypeEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableNullEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableShortEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableStringEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/encodedValue/MutableTypeEncodedValue.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/ExternalLabel.kt diff --git a/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt similarity index 100% rename from src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt diff --git a/src/main/resources/app/revanced/patcher/version.properties b/revanced-patcher/src/main/resources/app/revanced/patcher/version.properties similarity index 100% rename from src/main/resources/app/revanced/patcher/version.properties rename to revanced-patcher/src/main/resources/app/revanced/patcher/version.properties diff --git a/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt similarity index 99% rename from src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt index feed3aa..a774634 100644 --- a/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/extensions/InstructionExtensionsTest.kt @@ -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.immutable.ImmutableMethod import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test +import kotlin.test.Test import kotlin.test.assertEquals private object InstructionExtensionsTest { diff --git a/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/issues/Issue98.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt similarity index 97% rename from src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt index 8d37011..778a3f4 100644 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt @@ -1,8 +1,8 @@ package app.revanced.patcher.patch -import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch -import org.junit.jupiter.api.Test +import app.revanced.patcher.usage.ExampleBytecodePatch import org.junit.jupiter.api.assertThrows +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals import kotlin.test.assertNotNull diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleBytecodePatch.kt similarity index 81% rename from src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleBytecodePatch.kt index f41a682..e0f66c5 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleBytecodePatch.kt @@ -1,18 +1,13 @@ -package app.revanced.patcher.usage.bytecode +package app.revanced.patcher.usage -import app.revanced.patcher.annotation.Description -import app.revanced.patcher.annotation.Name import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.or import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.PatchOption -import app.revanced.patcher.patch.annotations.DependsOn +import app.revanced.patcher.patch.annotations.CompatiblePackage import app.revanced.patcher.patch.annotations.Patch -import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility -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.MutableMethod.Companion.toMutable import com.android.tools.smali.dexlib2.AccessFlags @@ -30,13 +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.util.Preconditions import com.google.common.collect.ImmutableList +import kotlin.test.assertNotNull + +@Suppress("unused") +@Patch( + name = "Example bytecode patch", + description = "Example demonstration of a bytecode patch.", + dependencies = [ExampleResourcePatch::class], + compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))] +) +object ExampleBytecodePatch : BytecodePatch( + setOf(ExampleFingerprint) +) { -@Patch -@Name("example-bytecode-patch") -@Description("Example demonstration of a bytecode patch.") -@ExampleResourceCompatibility -@DependsOn([ExampleResourcePatch::class]) -class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { // This function will be executed by the patcher. // You can treat it as a constructor override fun execute(context: BytecodeContext) { @@ -44,7 +45,7 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { val result = ExampleFingerprint.result!! // Patch options - println(key1) + assertNotNull(key1) key2 = false // Get the implementation for the resolved method @@ -159,32 +160,34 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { ) } - @Suppress("unused") - companion object : OptionsContainer() { - private var key1 by option( - PatchOption.StringOption( - "key1", "default", "title", "description", true - ) + private var key1 by option( + 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 ) - } + ) } + diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleFingerprint.kt similarity index 93% rename from src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleFingerprint.kt index 3cb5b06..4ec21e0 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleFingerprint.kt @@ -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.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint diff --git a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleResourcePatch.kt similarity index 55% rename from src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleResourcePatch.kt index 6c45b0a..3314e04 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/usage/ExampleResourcePatch.kt @@ -1,18 +1,11 @@ -package app.revanced.patcher.usage.resource.patch +package app.revanced.patcher.usage -import app.revanced.patcher.annotation.Description -import app.revanced.patcher.annotation.Name import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotations.Patch -import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import org.w3c.dom.Element -@Patch -@Name("example-resource-patch") -@Description("Example demonstration of a resource patch.") -@ExampleResourceCompatibility -class ExampleResourcePatch : ResourcePatch { + +class ExampleResourcePatch : ResourcePatch() { override fun execute(context: ResourceContext) { context.xmlEditor["AndroidManifest.xml"].use { editor -> val element = editor // regular DomFileEditor diff --git a/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt similarity index 100% rename from src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index d398cc6..8fdd378 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,4 +19,4 @@ dependencyResolutionManagement { } } -rootProject.name = "revanced-patcher" +include("revanced-patch-annotations-processor", "revanced-patcher") \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt b/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt deleted file mode 100644 index b69b140..0000000 --- a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt +++ /dev/null @@ -1,82 +0,0 @@ -@file:Suppress("unused") - -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.PatchClass -import dalvik.system.DexClassLoader -import lanchon.multidexlib2.BasicDexFileNamer -import lanchon.multidexlib2.MultiDexIO -import java.io.File -import java.net.URLClassLoader -import java.util.jar.JarFile - -/** - * A patch bundle. - * - * - * @param fromClasses The classes to get [Patch]es from. - */ -sealed class PatchBundleLoader private constructor( - fromClasses: Iterable> -) : MutableList by mutableListOf() { - init { - fromClasses.filter { - if (it.isAnnotation) return@filter false - - it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null - }.map { - @Suppress("UNCHECKED_CAST") - it as PatchClass - }.sortedBy { - it.patchName - }.let { addAll(it) } - } - - /** - * A [PatchBundleLoader] for JAR files. - * - * @param patchBundles The path to patch bundles of JAR format. - */ - class Jar(vararg patchBundles: File) : PatchBundleLoader( - with( - URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()) - ) { - patchBundles.flatMap { patchBundle -> - // Get the names of all classes in the DEX file. - - JarFile(patchBundle).entries().asSequence() - .filter { it.name.endsWith(".class") } - .map { it.name.replace('/', '.').replace(".class", "") } - .map { loadClass(it) } - } - }) - - /** - * A [PatchBundleLoader] for [Dex] files. - * - * @param patchBundles The path to patch bundles of DEX format. - * @param optimizedDexDirectory The directory to store optimized DEX files in. - * This parameter is deprecated and has no effect since API level 26. - */ - class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader( - with( - DexClassLoader( - patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath, - null, - PatchBundleLoader::class.java.classLoader - ) - ) { - patchBundles - .flatMap { - MultiDexIO.readDexFile(true, it, BasicDexFileNamer(), null, null).classes - } - .map { classDef -> classDef.type.substring(1, classDef.length - 1) } - .map { loadClass(it) } - }) { - @Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.") - constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt deleted file mode 100644 index 2959f6b..0000000 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ /dev/null @@ -1,236 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.data.Context -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively -import app.revanced.patcher.extensions.PatchExtensions.dependencies -import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap -import app.revanced.patcher.patch.* -import kotlinx.coroutines.flow.flow -import java.io.Closeable -import java.io.File -import java.util.function.Supplier -import java.util.logging.Level -import java.util.logging.LogManager -import java.util.logging.Logger - -/** - * ReVanced Patcher. - * - * @param options The options for the patcher. - */ -class Patcher( - private val options: PatcherOptions -) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier, Closeable { - private val logger = Logger.getLogger(Patcher::class.java.name) - - /** - * The context of ReVanced [Patcher]. - * This holds the current state of the patcher. - */ - val context = PatcherContext(options) - - init { - LogManager.getLogManager().let { manager -> - // Disable root logger. - manager.getLogger("").level = Level.OFF - - // Enable ReVanced logging only. - manager.loggerNames - .toList() - .filter { it.startsWith("app.revanced") } - .map { manager.getLogger(it) } - .forEach { it.level = Level.INFO } - } - - context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY) - } - - override fun acceptPatches(patches: List) { - /** - * Returns true if at least one patches or its dependencies matches the given predicate. - */ - fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean = - predicate(this) || dependencies?.any { dependency -> - dependency.java.anyRecursively(predicate) - } ?: false - - // Determine if resource patching is required. - for (patch in patches) { - if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) { - options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL - break - } - } - - // Determine if merging integrations is required. - for (patch in patches) { - if (patch.anyRecursively { it.requiresIntegrations }) { - context.bytecodeContext.integrations.merge = true - break - } - } - - context.patches.addAll(patches) - } - - /** - * Add integrations to the [Patcher]. - * - * @param integrations The integrations to add. Must be a DEX file or container of DEX files. - */ - override fun acceptIntegrations(integrations: List) { - context.bytecodeContext.integrations.addAll(integrations) - } - - /** - * Execute [Patch]es that were added to ReVanced [Patcher]. - * - * @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails. - * @return A pair of the name of the [Patch] and its [PatchResult]. - */ - override fun apply(returnOnError: Boolean) = flow { - class ExecutedPatch(val patchInstance: Patch>, val patchResult: PatchResult) - - /** - * Execute a [Patch] and its dependencies recursively. - * - * @param patchClass The [Patch] to execute. - * @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies. - * @return The result of executing the [Patch]. - */ - fun executePatch( - patchClass: PatchClass, - executedPatches: LinkedHashMap - ): PatchResult { - val patchName = patchClass.patchName - - executedPatches[patchName]?.let { executedPatch -> - executedPatch.patchResult.exception ?: return executedPatch.patchResult - - // Return a new result with an exception indicating that the patch was not executed previously, - // because it is a dependency of another patch that failed. - return PatchResult(patchName, PatchException("'$patchName' did not succeed previously")) - } - - // Recursively execute all dependency patches. - patchClass.dependencies?.forEach { dependencyClass -> - val dependency = dependencyClass.java - - val result = executePatch(dependency, executedPatches) - - result.exception?.let { - return PatchResult( - patchName, - PatchException( - "'$patchName' depends on '${dependency.patchName}' that raised an exception: $it" - ) - ) - } - } - - // TODO: Implement this in a more polymorphic way. - val patchInstance = patchClass.getDeclaredConstructor().newInstance() - - val patchContext = if (patchInstance is BytecodePatch) { - patchInstance.fingerprints?.resolveUsingLookupMap(context.bytecodeContext) - - context.bytecodeContext - } else { - context.resourceContext - } - - return try { - patchInstance.execute(patchContext) - - PatchResult(patchName) - } catch (exception: PatchException) { - PatchResult(patchName, exception) - } catch (exception: Exception) { - PatchResult(patchName, PatchException(exception)) - }.also { executedPatches[patchName] = ExecutedPatch(patchInstance, it) } - } - - if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() - - MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext) - - // Prevent from decoding the app manifest twice if it is not needed. - if (options.resourceDecodingMode == ResourceContext.ResourceDecodingMode.FULL) - context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.FULL) - - logger.info("Executing patches") - - val executedPatches = LinkedHashMap() // Key is name. - - context.patches.forEach { patch -> - val result = executePatch(patch, executedPatches) - - // If the patch failed, or if the patch is not closeable, emit the result. - // Results of patches that are closeable will be emitted later. - result.exception?.let { - emit(result) - - if (returnOnError) return@flow - } ?: run { - if (executedPatches[result.patchName]!!.patchInstance is Closeable) return@run - - emit(result) - } - } - - executedPatches.values - .filter { it.patchResult.exception == null } - .filter { it.patchInstance is Closeable }.asReversed().forEach { executedPatch -> - val patchName = executedPatch.patchResult.patchName - - val result = try { - (executedPatch.patchInstance as Closeable).close() - - executedPatch.patchResult - } catch (exception: PatchException) { - PatchResult(patchName, exception) - } catch (exception: Exception) { - PatchResult(patchName, PatchException(exception)) - } - - result.exception?.let { - emit( - PatchResult( - patchName, - PatchException("'$patchName' raised an exception while being closed: $it") - ) - ) - - if (returnOnError) return@flow - } ?: run { - executedPatch - .patchInstance::class - .java - .findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) - ?: return@run - - emit(result) - } - } - } - - override fun close() { - MethodFingerprint.clearFingerprintResolutionLookupMaps() - } - - /** - * Compile and save the patched APK file. - * - * @return The [PatcherResult] containing the patched input files. - */ - override fun get() = PatcherResult( - context.bytecodeContext.get(), - context.resourceContext.get(), - context.packageMetadata.apkInfo.doNotCompress?.toList() - ) -} - diff --git a/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt b/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt deleted file mode 100644 index ed7780c..0000000 --- a/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.patcher - -import app.revanced.patcher.patch.PatchClass - -@FunctionalInterface -interface PatchesConsumer { - fun acceptPatches(patches: List) -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt b/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt deleted file mode 100644 index 19e61e8..0000000 --- a/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt +++ /dev/null @@ -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, -) - -/** - * 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 = [], -) diff --git a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt b/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt deleted file mode 100644 index 9f9cd3a..0000000 --- a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt +++ /dev/null @@ -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, -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt deleted file mode 100644 index 493056e..0000000 --- a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt +++ /dev/null @@ -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 - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt deleted file mode 100644 index 621145b..0000000 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ /dev/null @@ -1,39 +0,0 @@ -package app.revanced.patcher.patch - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.data.Context -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import java.io.Closeable - -typealias PatchClass = Class>> - -/** - * A ReVanced patch. - * - * If it implements [Closeable], it will be closed after all patches have been executed. - * Closing will be done in reverse execution order. - */ -sealed interface Patch> { - /** - * The main function of the [Patch] which the patcher will call. - * - * @param context The [Context] the patch will work on. - * @return The result of executing the patch. - */ - fun execute(context: @UnsafeVariance T) -} - -/** - * Resource patch for the Patcher. - */ -interface ResourcePatch : Patch - -/** - * Bytecode patch for the Patcher. - * - * @param fingerprints A list of [MethodFingerprint] this patch relies on. - */ -abstract class BytecodePatch( - internal val fingerprints: Iterable? = null -) : Patch \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt deleted file mode 100644 index 01fab9c..0000000 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package app.revanced.patcher.patch - -/** - * A result of executing a [Patch]. - * - * @param patchName The name of the [Patch]. - * @param exception The [PatchException] thrown, if any. - */ -@Suppress("MemberVisibilityCanBePrivate") -class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt b/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt deleted file mode 100644 index 4e45c38..0000000 --- a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.revanced.patcher.patch.annotations - -import app.revanced.patcher.data.Context -import app.revanced.patcher.patch.Patch -import kotlin.reflect.KClass - -/** - * Annotation to mark a class as a patch. - * @param include If false, the patch should be treated as optional by default. - */ -@Target(AnnotationTarget.CLASS) -annotation class Patch(val include: Boolean = true) - -/** - * Annotation for dependencies of [Patch]es. - */ -@Target(AnnotationTarget.CLASS) -annotation class DependsOn( - val dependencies: Array>>> = [] -) - -// TODO: Remove this annotation, once integrations are coupled with patches. -/** - * Annotation to mark [Patch]es which depend on integrations. - */ -@Target(AnnotationTarget.CLASS) -annotation class RequiresIntegrations \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt b/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt deleted file mode 100644 index 85f0a32..0000000 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodeCompatibility.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.patcher.usage.bytecode - -import app.revanced.patcher.annotation.Compatibility -import app.revanced.patcher.annotation.Package - -@Compatibility( - [Package( - "com.example.examplePackage", arrayOf("0.0.1", "0.0.2") - )] -) -@Target(AnnotationTarget.CLASS) -internal annotation class ExampleBytecodeCompatibility - diff --git a/src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt b/src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt deleted file mode 100644 index 059649e..0000000 --- a/src/test/kotlin/app/revanced/patcher/usage/resource/annotation/ExampleResourceCompatibility.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.patcher.usage.resource.annotation - -import app.revanced.patcher.annotation.Compatibility -import app.revanced.patcher.annotation.Package - -@Compatibility( - [Package( - "com.example.examplePackage", arrayOf("0.0.1", "0.0.2") - )] -) -@Target(AnnotationTarget.CLASS) -internal annotation class ExampleResourceCompatibility -