diff --git a/CHANGELOG.md b/CHANGELOG.md index de154e60..e1157b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,68 @@ +# [15.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0-dev.3...v15.0.0-dev.4) (2023-09-13) + + +### Bug Fixes + +* Account for source patch dependency for tests ([6918418](https://github.com/ReVanced/revanced-patcher/commit/69184187d90f126478d2f49415c1e3381217557f)) +* Always make the generated patch depend on the source patch ([8de3063](https://github.com/ReVanced/revanced-patcher/commit/8de30633ae6eb7acf7f0a351e26d4a6c2fdbdfec)) +* Catch correct exception ([637d487](https://github.com/ReVanced/revanced-patcher/commit/637d48746ff8694e01c5aead1c75a9a1efeb5ac8)) +* Delegate `PatchBundleLoader` by mutable set of patches ([9a109c1](https://github.com/ReVanced/revanced-patcher/commit/9a109c129b135a634be1aad4130a06d9e8e96ecd)) +* Do not resolve the proxied patch to the proxy in the dependency list ([e112837](https://github.com/ReVanced/revanced-patcher/commit/e11283744a21fe2d09435e99d6924462b6aac3b8)) +* Do not set `CompatiblePackage.versions` if `@CompatiblePackage.versions` is empty ([6b1e0a1](https://github.com/ReVanced/revanced-patcher/commit/6b1e0a16568124e9f82fb5740353360fa8ec614a)) +* Filter for patches correctly ([4bc4b0d](https://github.com/ReVanced/revanced-patcher/commit/4bc4b0dc0104073b62528d02a88383cecd7a50e7)) +* Find dependency in `context.allPatches` ([670f015](https://github.com/ReVanced/revanced-patcher/commit/670f0153de10c6f0db25b08df1c01a2905037f84)) +* Log the correct patch names ([9fdb8f0](https://github.com/ReVanced/revanced-patcher/commit/9fdb8f087f62babf6081879db65c80db639aa0a7)) +* Print patch name instead of class name ([4e7811e](https://github.com/ReVanced/revanced-patcher/commit/4e7811ea07762667a1f22526dc176022038f60eb)) +* Print stack trace of exception ([aa71146](https://github.com/ReVanced/revanced-patcher/commit/aa71146b1bf4ffebcc81a1663e15abae89e97ff0)) +* Run code-block if `executablePatches` does not yet contain `patch` ([1d7aeca](https://github.com/ReVanced/revanced-patcher/commit/1d7aeca696be873dfaf88eaa6d312949a3b8572b)) +* Suppress logger when loading patches in `PatchBundleLoader` ([72c9eb2](https://github.com/ReVanced/revanced-patcher/commit/72c9eb212985f99f3390cf1faa10ab547d2dbe7e)) + + +### Code Refactoring + +* Internalize processor constructor ([a802d0d](https://github.com/ReVanced/revanced-patcher/commit/a802d0df463695976e85d8391762942eb977920b)) + + +### BREAKING CHANGES + +* This gets rid of the public constructor. + +# [15.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0-dev.2...v15.0.0-dev.3) (2023-09-06) + + +### Bug Fixes + +* Make `CompatiblePackage.versions` a property ([67b7dff](https://github.com/ReVanced/revanced-patcher/commit/67b7dff67a212b4fc30eb4f0cbe58f0ba09fb09a)) +* Use correct module name ([080fbe9](https://github.com/ReVanced/revanced-patcher/commit/080fbe9feb9d4ea9ec4e599ecef296eacd803b05)) + + +* feat Use `Set` as super type for `PatchBundleLoader` ([4b76d19](https://github.com/ReVanced/revanced-patcher/commit/4b76d1959691babf8c99d3d5235df4a4388956f0)) + + +### BREAKING CHANGES + +* `PatchBundleLoader` is not a map anymore +* This renames packages and the Maven package. + +# [15.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0-dev.1...v15.0.0-dev.2) (2023-09-06) + +# [15.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.2.2...v15.0.0-dev.1) (2023-09-04) + + +* feat!: Add patch annotation processor ([3fc6a13](https://github.com/ReVanced/revanced-patcher/commit/3fc6a139eef67237c116fb4e3e29bf9542d3a981)) +* feat!: Remove patch annotations ([3b4db3d](https://github.com/ReVanced/revanced-patcher/commit/3b4db3ddb72cdcee8af2f787eadf58eeb37543de)) + + +### Features + +* Add patch annotation processor ([#231](https://github.com/ReVanced/revanced-patcher/issues/231)) ([a29931f](https://github.com/ReVanced/revanced-patcher/commit/a29931f2ec0a666dba209b54855425d9dc2f4462)) + + +### BREAKING CHANGES + +* The manifest for patches has been removed, and the properties have been added to patches. Patches are now `OptionsContainer`. The `@Patch` annotation has been removed in favour of the `@Patch` annotation from the annotation processor. +* Patch annotations have been removed. PatcherException is now thrown in various places. PatchBundleLoader is now a map of patches associated by their name. Patches are now instances. + ## [14.2.2](https://github.com/ReVanced/revanced-patcher/compare/v14.2.1...v14.2.2) (2023-08-30) diff --git a/build.gradle.kts b/build.gradle.kts index 402fc71b..491907e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,58 +1,10 @@ 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" +allprojects { + apply(plugin = "maven-publish") -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"]) - } - } -} + group = "app.revanced" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2f8c42d3..f4f06991 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official -version = 14.2.2 +version = 15.0.0-dev.4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b214cbc..512c2964 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-annotation-processor/api/revanced-patch-annotation-processor.api b/revanced-patch-annotation-processor/api/revanced-patch-annotation-processor.api new file mode 100644 index 00000000..c1536a91 --- /dev/null +++ b/revanced-patch-annotation-processor/api/revanced-patch-annotation-processor.api @@ -0,0 +1,24 @@ +public abstract interface annotation class app/revanced/patcher/patch/annotation/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/annotation/Patch : java/lang/annotation/Annotation { + public abstract fun compatiblePackages ()[Lapp/revanced/patcher/patch/annotation/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/annotation/processor/PatchProcessor : com/google/devtools/ksp/processing/SymbolProcessor { + public fun process (Lcom/google/devtools/ksp/processing/Resolver;)Ljava/util/List; +} + +public final class app/revanced/patcher/patch/annotation/processor/PatchProcessorProvider : com/google/devtools/ksp/processing/SymbolProcessorProvider { + public fun ()V + public fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lapp/revanced/patcher/patch/annotation/processor/PatchProcessor; + public synthetic fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lcom/google/devtools/ksp/processing/SymbolProcessor; +} + diff --git a/revanced-patch-annotation-processor/build.gradle.kts b/revanced-patch-annotation-processor/build.gradle.kts new file mode 100644 index 00000000..2c2f860a --- /dev/null +++ b/revanced-patch-annotation-processor/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + kotlin("jvm") version "1.9.0" + alias(libs.plugins.ksp) +} + +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-patcher") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + publications { + create("gpr") { + from(components["java"]) + + version = project.version.toString() + + pom { + name = "ReVanced patch annotation processor" + description = "Annotation processor for patches." + url = "https://revanced.app" + + licenses { + license { + name = "GNU General Public License v3.0" + url = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + developers { + developer { + id = "ReVanced" + name = "ReVanced" + email = "contact@revanced.app" + } + } + scm { + connection = "scm:git:git://github.com/revanced/revanced-patcher.git" + developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git" + url = "https://github.com/revanced/revanced-patcher" + } + } + } + } +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/settings.gradle.kts b/revanced-patch-annotation-processor/settings.gradle.kts new file mode 100644 index 00000000..f1d81608 --- /dev/null +++ b/revanced-patch-annotation-processor/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "revanced-patch-annotation-processor" + diff --git a/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/PatchAnnotations.kt b/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/PatchAnnotations.kt new file mode 100644 index 00000000..c6919bc9 --- /dev/null +++ b/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/PatchAnnotations.kt @@ -0,0 +1,38 @@ +package app.revanced.patcher.patch.annotation + +import java.lang.annotation.Inherited +import kotlin.reflect.KClass + +/** + * Annotation for [app.revanced.patcher.patch.Patch] classes. + * + * @param name The name of the patch. If empty, the patch will be unnamed. + * @param description The description of the patch. If empty, no description will be used. + * @param dependencies The patches this patch depends on. + * @param compatiblePackages The packages this patch is compatible with. + * @param use Whether this patch should be used. + * @param requiresIntegrations Whether this patch requires integrations. + */ +@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-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/processor/PatchProcessor.kt b/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/processor/PatchProcessor.kt new file mode 100644 index 00000000..325d0054 --- /dev/null +++ b/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/processor/PatchProcessor.kt @@ -0,0 +1,207 @@ +package app.revanced.patcher.patch.annotation.processor + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.ResourcePatch +import app.revanced.patcher.patch.annotation.Patch +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +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 internal constructor( + private val codeGenerator: CodeGenerator, +) : 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 patches = 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).ifEmpty { null } + ?.joinToString(", ") { version -> "\"$version\"" } + + CodeBlock.of( + "%T(%S, %L)", + app.revanced.patcher.patch.Patch.CompatiblePackage::class, + packageName, + packageVersions?.let { "setOf($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 with, + // unlike the annotated patch. + val dependencyResolutionMap = buildMap { + patches.values.filter { it.dependencies != null }.flatMap { + it.dependencies!! + }.distinct().forEach { dependency -> + patches.keys.find { it.qualifiedName?.asString() == dependency.toString() } + ?.let { patch -> + this[dependency] = ClassName( + patch.packageName.asString(), + patch.simpleName.asString() + "Generated" + ) + } + } + } + + patches.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(", ") + ) + } + + // The generated patch always depends on the source patch. + addSuperclassConstructorParameter( + "dependencies = setOf(%L)", + buildList { + patchAnnotation.dependencies?.forEach { dependency -> + add("${(dependencyResolutionMap[dependency] ?: dependency)}::class") + } + + add("${patchDeclaration.toClassName()}::class") + }.joinToString(", "), + ) + + addSuperclassConstructorParameter( + "use = %L", patchAnnotation.use + ) + + addSuperclassConstructorParameter( + "requiresIntegrations = %L", + patchAnnotation.requiresIntegrations + ) + } + .addFunction( + FunSpec.builder("execute") + .addModifiers(KModifier.OVERRIDE) + .addParameter("context", contextClass) + .build() + ) + .addInitializerBlock( + CodeBlock.builder() + .add( + "%T.options.forEach { (_, option) ->", + patchDeclaration.toClassName() + ) + .addStatement( + "options.register(option)" + ) + .add( + "}" + ) + .build() + ) + .build() + ).build().writeTo( + codeGenerator, + Dependencies(false, patchDeclaration.containingFile!!) + ) + } + + return emptyList() + } +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/processor/PatchProcessorProvider.kt b/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/processor/PatchProcessorProvider.kt new file mode 100644 index 00000000..9e3cc63e --- /dev/null +++ b/revanced-patch-annotation-processor/src/main/kotlin/app/revanced/patcher/patch/annotation/processor/PatchProcessorProvider.kt @@ -0,0 +1,9 @@ +package app.revanced.patcher.patch.annotation.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) +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/revanced-patch-annotation-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider new file mode 100644 index 00000000..52884f49 --- /dev/null +++ b/revanced-patch-annotation-processor/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -0,0 +1 @@ +app.revanced.patcher.patch.annotation.processor.PatchProcessorProvider \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/test/kotlin/app/revanced/patcher/patch/annotation/processor/TestPatchAnnotationProcessor.kt b/revanced-patch-annotation-processor/src/test/kotlin/app/revanced/patcher/patch/annotation/processor/TestPatchAnnotationProcessor.kt new file mode 100644 index 00000000..f3e2e433 --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/kotlin/app/revanced/patcher/patch/annotation/processor/TestPatchAnnotationProcessor.kt @@ -0,0 +1,147 @@ +package app.revanced.patcher.patch.annotation.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.assertNull + +class TestPatchAnnotationProcessor { + // region Processing + + @Test + fun testProcessing() = assertEquals( + "Processable patch", compile( + getSourceFile( + "processing", "ProcessablePatch" + ) + ).loadPatch("$SAMPLE_PACKAGE.processing.ProcessablePatchGenerated").name + ) + + @Test + fun generateNullProperties() = compile( + getSourceFile( + "null", "NullPropertiesPatch" + ) + ).loadPatch("$SAMPLE_PACKAGE.null.NullPropertiesPatchGenerated").let { + assertNull(it.description) // Because no description was provided. + assertNull(it.compatiblePackages!!.first().versions) // Because no versions were provided. + } + + // 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") + + assert(patch.options.isNotEmpty()) + assertEquals(patch.options["print"].title, "Print message") + } + + // endregion + + // region Limitations + + @Test + fun failingManualDependency() = assertEquals( + 1, // Generated patch is always dependent on source class. + compile( + getSourceFile( + "limitations/manualdependency", "DependentPatch" + ), getSourceFile( + "limitations/manualdependency", "DependencyPatch" + ) + ).loadPatch("$SAMPLE_PACKAGE.limitations.manualdependency.DependentPatchGenerated").dependencies!!.size + ) + + // endregion + + private companion object Utils { + const val SAMPLE_PACKAGE = "app.revanced.patcher.patch.annotation.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 { + val resourceName = "app/revanced/patcher/patch/annotation/processor/samples/$sample/$className.kt" + return SourceFile.kotlin( + "$className.kt", + TestPatchAnnotationProcessor::class.java.classLoader.getResourceAsStream(resourceName) + ?.readAllBytes() + ?.toString(Charsets.UTF_8) + ?: error("Could not find resource $resourceName") + ) + } + + /** + * 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-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/dependencies/DependencyPatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/dependencies/DependencyPatch.kt new file mode 100644 index 00000000..22302748 --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/dependencies/DependencyPatch.kt @@ -0,0 +1,10 @@ +package app.revanced.patcher.patch.annotation.processor.samples.dependencies + +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.ResourcePatch +import app.revanced.patcher.patch.annotation.Patch + +@Patch(name = "Dependency patch") +object DependencyPatch : ResourcePatch() { + override fun execute(context: ResourceContext) {} +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/dependencies/DependentPatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/dependencies/DependentPatch.kt new file mode 100644 index 00000000..d20dc519 --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/dependencies/DependentPatch.kt @@ -0,0 +1,12 @@ +package app.revanced.patcher.patch.annotation.processor.samples.dependencies +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.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-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/limitations/manualdependency/DependencyPatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/limitations/manualdependency/DependencyPatch.kt new file mode 100644 index 00000000..c373c346 --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/limitations/manualdependency/DependencyPatch.kt @@ -0,0 +1,10 @@ +package app.revanced.patcher.patch.annotation.processor.samples.limitations.manualdependency + +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.ResourcePatch +import app.revanced.patcher.patch.annotation.Patch + +@Patch(name = "Dependency patch") +object DependencyPatch : ResourcePatch() { + override fun execute(context: ResourceContext) { } +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/limitations/manualdependency/DependentPatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/limitations/manualdependency/DependentPatch.kt new file mode 100644 index 00000000..5bc396cb --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/limitations/manualdependency/DependentPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patcher.patch.annotation.processor.samples.limitations.manualdependency +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.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-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/null/NullPropertiesPatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/null/NullPropertiesPatch.kt new file mode 100644 index 00000000..a013dbac --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/null/NullPropertiesPatch.kt @@ -0,0 +1,14 @@ +package app.revanced.patcher.patch.annotation.processor.samples.`null` + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch + +@Patch( + "Patch with null properties", + compatiblePackages = [CompatiblePackage("com.google.android.youtube")], +) +object NullPropertiesPatch : BytecodePatch() { + override fun execute(context: BytecodeContext) {} +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/options/OptionsPatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/options/OptionsPatch.kt new file mode 100644 index 00000000..6bc10271 --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/options/OptionsPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patcher.patch.annotation.processor.samples.options + +import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.ResourcePatch +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.options.types.StringPatchOption.Companion.stringPatchOption + +@Patch(name = "Options patch") +object OptionsPatch : ResourcePatch() { + override fun execute(context: ResourceContext) {} + + @Suppress("unused") + private val printOption by stringPatchOption( + "print", + null, + "Print message", + "The message to print." + ) +} \ No newline at end of file diff --git a/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/processing/ProcessablePatch.kt b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/processing/ProcessablePatch.kt new file mode 100644 index 00000000..86767dbd --- /dev/null +++ b/revanced-patch-annotation-processor/src/test/resources/app/revanced/patcher/patch/annotation/processor/samples/processing/ProcessablePatch.kt @@ -0,0 +1,10 @@ +package app.revanced.patcher.patch.annotation.processor.samples.processing + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.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 75% rename from api/revanced-patcher.api rename to revanced-patcher/api/revanced-patcher.api index 7a761a9c..d1c75d89 100644 --- a/api/revanced-patcher.api +++ b/revanced-patcher/api/revanced-patcher.api @@ -7,40 +7,22 @@ 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 abstract class app/revanced/patcher/PatchBundleLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker { + public synthetic fun (Ljava/lang/ClassLoader;[Ljava/io/File;Lkotlin/jvm/functions/Function1;Ljava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun add (Lapp/revanced/patcher/patch/Patch;)Z public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (ILjava/util/Collection;)Z public fun addAll (Ljava/util/Collection;)Z public fun clear ()V - public fun contains (Ljava/lang/Class;)Z + public fun contains (Lapp/revanced/patcher/patch/Patch;)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 fun getSize ()I - public fun indexOf (Ljava/lang/Class;)I - public final fun indexOf (Ljava/lang/Object;)I 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 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 size ()I - public fun subList (II)Ljava/util/List; public fun toArray ()[Ljava/lang/Object; public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; } @@ -49,12 +31,10 @@ public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/pat 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 +56,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 +103,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 +170,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,109 +306,210 @@ 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 { - public fun (Ljava/lang/Object;)V - public final fun getValue ()Ljava/lang/Object; +public abstract class app/revanced/patcher/patch/Patch { + 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 getOptions ()Lapp/revanced/patcher/patch/options/PatchOptions; + 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/InvalidTypeException : java/lang/Exception { - public fun (Ljava/lang/String;Ljava/lang/String;)V - public final fun getExpected ()Ljava/lang/String; - public final fun getGot ()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 fun getVersions ()Ljava/util/Set; } -public final class app/revanced/patcher/patch/NoSuchOptionException : java/lang/Exception { +public final class app/revanced/patcher/patch/PatchException : java/lang/Exception { public fun (Ljava/lang/String;)V - public final fun getOption ()Ljava/lang/String; -} - -public abstract class app/revanced/patcher/patch/OptionsContainer { - public fun ()V - public final fun getOptions ()Lapp/revanced/patcher/patch/PatchOptions; - protected final fun option (Lapp/revanced/patcher/patch/PatchOption;)Lapp/revanced/patcher/patch/PatchOption; + public fun (Ljava/lang/String;Ljava/lang/Throwable;)V + public fun (Ljava/lang/Throwable;)V } -public abstract interface class app/revanced/patcher/patch/Patch { - public abstract fun execute (Lapp/revanced/patcher/data/Context;)V +public final class app/revanced/patcher/patch/PatchResult { + public final fun getException ()Lapp/revanced/patcher/patch/PatchException; + public final fun getPatch ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patcher/patch/PatchException : java/lang/Exception { - public fun (Ljava/lang/String;)V - public fun (Ljava/lang/String;Ljava/lang/Throwable;)V - public fun (Ljava/lang/Throwable;)V +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 abstract class app/revanced/patcher/patch/PatchOption { - public synthetic fun (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +public abstract class app/revanced/patcher/patch/options/PatchOption { + public fun (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V public final fun getDescription ()Ljava/lang/String; public final fun getKey ()Ljava/lang/String; public final fun getRequired ()Z public final fun getTitle ()Ljava/lang/String; - public final fun getValidator ()Lkotlin/jvm/functions/Function1; + public final fun getValidate ()Lkotlin/jvm/functions/Function1; public final fun getValue ()Ljava/lang/Object; public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object; public final fun setValue (Ljava/lang/Object;)V public final fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V } -public final class app/revanced/patcher/patch/PatchOption$BooleanOption : app/revanced/patcher/patch/PatchOption { - public fun (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public abstract class app/revanced/patcher/patch/options/PatchOptionException : java/lang/Exception { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class app/revanced/patcher/patch/PatchOption$IntListOption : app/revanced/patcher/patch/PatchOption$ListOption { - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class app/revanced/patcher/patch/options/PatchOptionException$InvalidValueTypeException : app/revanced/patcher/patch/options/PatchOptionException { + public fun (Ljava/lang/String;Ljava/lang/String;)V } -public abstract class app/revanced/patcher/patch/PatchOption$ListOption : app/revanced/patcher/patch/PatchOption { - public synthetic fun (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Iterable;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Iterable;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getOptions ()Ljava/lang/Iterable; +public final class app/revanced/patcher/patch/options/PatchOptionException$PatchOptionNotFoundException : java/lang/Exception { + public fun (Ljava/lang/String;)V } -public final class app/revanced/patcher/patch/PatchOption$StringListOption : app/revanced/patcher/patch/PatchOption$ListOption { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Iterable;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Iterable;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class app/revanced/patcher/patch/options/PatchOptionException$ValueRequiredException : java/lang/Exception { + public fun (Lapp/revanced/patcher/patch/options/PatchOption;)V } -public final class app/revanced/patcher/patch/PatchOption$StringOption : app/revanced/patcher/patch/PatchOption { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class app/revanced/patcher/patch/options/PatchOptionException$ValueValidationException : java/lang/Exception { + public fun (Ljava/lang/Object;Lapp/revanced/patcher/patch/options/PatchOption;)V } -public final class app/revanced/patcher/patch/PatchOptions : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker { - public fun ([Lapp/revanced/patcher/patch/PatchOption;)V - public final fun getUntyped (Ljava/lang/String;)Lapp/revanced/patcher/patch/PatchOption; - public fun iterator ()Ljava/util/Iterator; - public final fun nullify (Ljava/lang/String;)V +public final class app/revanced/patcher/patch/options/PatchOptions : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { + public fun ()V + public fun clear ()V + public final fun containsKey (Ljava/lang/Object;)Z + public fun containsKey (Ljava/lang/String;)Z + public fun containsValue (Lapp/revanced/patcher/patch/options/PatchOption;)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/options/PatchOption; + public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; + public fun get (Ljava/lang/String;)Lapp/revanced/patcher/patch/options/PatchOption; + public fun getEntries ()Ljava/util/Set; + public fun getKeys ()Ljava/util/Set; + public fun getSize ()I + public fun getValues ()Ljava/util/Collection; + public fun isEmpty ()Z + public final fun keySet ()Ljava/util/Set; + public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/options/PatchOption;)Lapp/revanced/patcher/patch/options/PatchOption; + public fun putAll (Ljava/util/Map;)V + public final fun register (Lapp/revanced/patcher/patch/options/PatchOption;)V + public final fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption; + public final synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object; + public fun remove (Ljava/lang/String;)Lapp/revanced/patcher/patch/options/PatchOption; + public final fun set (Ljava/lang/String;Ljava/lang/Object;)V + public final fun size ()I + public final fun values ()Ljava/util/Collection; } -public final class app/revanced/patcher/patch/PatchResult { - public final fun getException ()Lapp/revanced/patcher/patch/PatchException; - public final fun getPatchName ()Ljava/lang/String; +public final class app/revanced/patcher/patch/options/types/BooleanPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/BooleanPatchOption$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/BooleanPatchOption$Companion { + public final fun booleanPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/BooleanPatchOption; + public static synthetic fun booleanPatchOption$default (Lapp/revanced/patcher/patch/options/types/BooleanPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/BooleanPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/FloatPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/FloatPatchOption$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/Float;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/FloatPatchOption$Companion { + public final fun floatPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Float;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/FloatPatchOption; + public static synthetic fun floatPatchOption$default (Lapp/revanced/patcher/patch/options/types/FloatPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Float;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/FloatPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/IntPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/IntPatchOption$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/IntPatchOption$Companion { + public final fun intPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/IntPatchOption; + public static synthetic fun intPatchOption$default (Lapp/revanced/patcher/patch/options/types/IntPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/IntPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/LongPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/LongPatchOption$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/LongPatchOption$Companion { + public final fun longPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/LongPatchOption; + public static synthetic fun longPatchOption$default (Lapp/revanced/patcher/patch/options/types/LongPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/LongPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/StringPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/StringPatchOption$Companion; + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/StringPatchOption$Companion { + public final fun stringPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/StringPatchOption; + public static synthetic fun stringPatchOption$default (Lapp/revanced/patcher/patch/options/types/StringPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/StringPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption$Companion; + public synthetic fun (Ljava/lang/String;[Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption$Companion { + public final fun booleanArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption; + public static synthetic fun booleanArrayPatchOption$default (Lapp/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Boolean;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/array/FloatArrayPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/array/FloatArrayPatchOption$Companion; + public synthetic fun (Ljava/lang/String;[Ljava/lang/Float;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/patch/options/types/array/FloatArrayPatchOption$Companion { + public final fun floatArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Float;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/array/FloatArrayPatchOption; + public static synthetic fun floatArrayPatchOption$default (Lapp/revanced/patcher/patch/options/types/array/FloatArrayPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Float;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/array/FloatArrayPatchOption; +} + +public final class app/revanced/patcher/patch/options/types/array/IntArrayPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/array/IntArrayPatchOption$Companion; + public synthetic fun (Ljava/lang/String;[Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class app/revanced/patcher/patch/RequirementNotMetException : java/lang/Exception { - public static final field INSTANCE Lapp/revanced/patcher/patch/RequirementNotMetException; +public final class app/revanced/patcher/patch/options/types/array/IntArrayPatchOption$Companion { + public final fun intArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/array/IntArrayPatchOption; + public static synthetic fun intArrayPatchOption$default (Lapp/revanced/patcher/patch/options/types/array/IntArrayPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Integer;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/array/IntArrayPatchOption; } -public abstract interface class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { +public final class app/revanced/patcher/patch/options/types/array/LongArrayPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/array/LongArrayPatchOption$Companion; + public synthetic fun (Ljava/lang/String;[Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public abstract interface annotation class app/revanced/patcher/patch/annotations/DependsOn : java/lang/annotation/Annotation { - public abstract fun dependencies ()[Ljava/lang/Class; +public final class app/revanced/patcher/patch/options/types/array/LongArrayPatchOption$Companion { + public final fun longArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/array/LongArrayPatchOption; + public static synthetic fun longArrayPatchOption$default (Lapp/revanced/patcher/patch/options/types/array/LongArrayPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/array/LongArrayPatchOption; } -public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation { - public abstract fun include ()Z +public final class app/revanced/patcher/patch/options/types/array/StringArrayPatchOption : app/revanced/patcher/patch/options/PatchOption { + public static final field Companion Lapp/revanced/patcher/patch/options/types/array/StringArrayPatchOption$Companion; + public synthetic fun (Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public abstract interface annotation class app/revanced/patcher/patch/annotations/RequiresIntegrations : java/lang/annotation/Annotation { +public final class app/revanced/patcher/patch/options/types/array/StringArrayPatchOption$Companion { + public final fun stringArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/options/types/array/StringArrayPatchOption; + public static synthetic fun stringArrayPatchOption$default (Lapp/revanced/patcher/patch/options/types/array/StringArrayPatchOption$Companion;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/types/array/StringArrayPatchOption; } 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 00000000..1a0713fa --- /dev/null +++ b/revanced-patcher/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + kotlin("jvm") version "1.9.0" +} + +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-annotation-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"]) + + version = project.version.toString() + + pom { + name = "ReVanced Patcher" + description = "Patcher used by ReVanced." + url = "https://revanced.app" + + licenses { + license { + name = "GNU General Public License v3.0" + url = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + developers { + developer { + id = "ReVanced" + name = "ReVanced" + email = "contact@revanced.app" + } + } + scm { + connection = "scm:git:git://github.com/revanced/revanced-patcher.git" + developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git" + url = "https://github.com/revanced/revanced-patcher" + } + } + } + } +} diff --git a/revanced-patcher/settings.gradle.kts b/revanced-patcher/settings.gradle.kts new file mode 100644 index 00000000..2a8c8539 --- /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 00000000..825bafcb --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt @@ -0,0 +1,127 @@ +@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 + +/** + * A set of [Patch]es. + */ +typealias PatchSet = Set> + +/** + * 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. + * @param patchBundles A set of patches to initialize this instance with. + */ +sealed class PatchBundleLoader private constructor( + classLoader: ClassLoader, + patchBundles: Array, + getBinaryClassNames: (patchBundle: File) -> List, + // This constructor parameter is unfortunately necessary, + // so that a reference to the mutable set is present in the constructor to be able to add patches to it. + // because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter. + private val patchSet: MutableSet> = mutableSetOf() +) : PatchSet by patchSet { + private val logger = Logger.getLogger(PatchBundleLoader::class.java.name) + + init { + patchBundles.flatMap(getBinaryClassNames).asSequence().map { + classLoader.loadClass(it) + }.filter { + Patch::class.java.isAssignableFrom(it) + }.mapNotNull { patchClass -> + patchClass.getInstance(logger, silent = true) + }.filter { + it.name != null + }.let { patches -> + patchSet.addAll(patches) + } + } + + internal companion object Utils { + /** + * Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used. + * + * @param logger The [Logger] to use for logging. + * @param silent Whether to suppress logging. + * @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated. + */ + internal fun Class<*>.getInstance(logger: Logger, silent: Boolean = false): Patch<*>? { + return try { + getField("INSTANCE").get(null) + } catch (exception: NoSuchFieldException) { + if (!silent) logger.fine( + "Patch class '${name}' has no INSTANCE field, therefor not a singleton. " + + "Will try to instantiate it." + ) + + try { + getDeclaredConstructor().newInstance() + } catch (exception: Exception) { + if (!silent) logger.severe( + "Patch class '${name}' is not singleton and has no suitable constructor, " + + "therefor cannot be instantiated and will be ignored." + ) + + return null + } + } as Patch<*> + } + } + + /** + * 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 00000000..3128b208 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -0,0 +1,279 @@ +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) ?: run { + 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 ?: patch.toString() + + 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 { dependencyClass -> + val dependency = context.allPatches[dependencyClass]!! + val result = executePatch(dependency, executedPatches) + + result.exception?.let { + return PatchResult( + patch, + PatchException( + "'$patchName' depends on '${dependency.name ?: dependency}' " + + "that raised an exception:\n${it.stackTraceToString()}" + ) + ) + } + } + + return try { + // TODO: Implement this in a more polymorphic way. + when (patch) { + is BytecodePatch -> { + patch.fingerprints.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.values.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.stackTraceToString()}", + 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 4aa83969..c21cfa6d 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 00000000..2313a998 --- /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 00000000..866b7e5c --- /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 3374930f..439b4307 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 19010445..e722f7d9 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 48587210..bb223bfd 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,9 +159,12 @@ 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 Set.resolveUsingLookupMap(context: BytecodeContext) { if (methods.isEmpty()) throw PatchException("lookup map not initialized") + forEach { fingerprint -> + fingerprint.resolveUsingLookupMap(context) + } for (fingerprint in this) { fingerprint.resolveUsingLookupMap(context) } 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/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt new file mode 100644 index 00000000..7c154818 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patcher.patch + +import app.revanced.patcher.PatchClass +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint + +/** + * A ReVanced [Patch] that works on [BytecodeContext]. + * + * @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed. + * @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/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 00000000..c1777041 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -0,0 +1,71 @@ +@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.Context +import app.revanced.patcher.patch.options.PatchOptions +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, +) { + /** + * The options of the patch associated by the options key. + */ + val options = PatchOptions() + + /** + * 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, + val versions: Set? = null, + ) +} + 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/PatchResult.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt similarity index 55% rename from src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt rename to revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt index 01fab9c9..71d40b72 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt @@ -3,8 +3,8 @@ package app.revanced.patcher.patch /** * A result of executing a [Patch]. * - * @param patchName The name of the [Patch]. + * @param patch The [Patch] that was executed. * @param exception The [PatchException] thrown, if any. */ @Suppress("MemberVisibilityCanBePrivate") -class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null) \ No newline at end of file +class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null) \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/ResourcePatch.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/ResourcePatch.kt new file mode 100644 index 00000000..b5d42299 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/ResourcePatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patcher.patch + +import app.revanced.patcher.PatchClass +import app.revanced.patcher.data.ResourceContext + +/** + * 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) \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOption.kt new file mode 100644 index 00000000..df130bf4 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOption.kt @@ -0,0 +1,40 @@ +package app.revanced.patcher.patch.options + +import app.revanced.patcher.patch.Patch +import kotlin.reflect.KProperty + +/** + * A [Patch] option. + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @param validate The function to validate values of the option. + * @param T The value type of the option. + */ +abstract class PatchOption( + val key: String, + default: T?, + val title: String?, + val description: String?, + val required: Boolean, + val validate: (T?) -> Boolean +) { + /** + * The value of the [PatchOption]. + */ + var value: T? = default + set(value) { + if (required && value == null) throw PatchOptionException.ValueRequiredException(this) + if (!validate(value)) throw PatchOptionException.ValueValidationException(value, this) + + field = value + } + + operator fun getValue(thisRef: Any?, property: KProperty<*>) = value + + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + this.value = value + } +} \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptionException.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptionException.kt new file mode 100644 index 00000000..1f84803d --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptionException.kt @@ -0,0 +1,41 @@ +package app.revanced.patcher.patch.options + +/** + * An exception thrown when using [PatchOption]s. + * + * @param errorMessage The exception message. + */ +sealed class PatchOptionException(errorMessage: String) : Exception(errorMessage, null) { + /** + * An exception thrown when a [PatchOption] is set to an invalid value. + * + * @param invalidType The type of the value that was passed. + * @param expectedType The type of the value that was expected. + */ + class InvalidValueTypeException(invalidType: String, expectedType: String) : + PatchOptionException("Type $expectedType was expected but received type $invalidType") + + /** + * An exception thrown when a value did not satisfy the value conditions specified by the [PatchOption]. + * + * @param value The value that failed validation. + */ + class ValueValidationException(value: Any?, option: PatchOption<*>) : + Exception("The option value \"$value\" failed validation for ${option.key}") + + /** + * An exception thrown when a value is required but null was passed. + * + * @param option The [PatchOption] that requires a value. + */ + class ValueRequiredException(option: PatchOption<*>) : + Exception("The option ${option.key} requires a value, but null was passed") + + /** + * An exception thrown when a [PatchOption] is not found. + * + * @param key The key of the [PatchOption]. + */ + class PatchOptionNotFoundException(key: String) + : Exception("No option with key $key") +} \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptions.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptions.kt new file mode 100644 index 00000000..481da72f --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptions.kt @@ -0,0 +1,45 @@ +package app.revanced.patcher.patch.options + + +/** + * A map of [PatchOption]s associated by their keys. + * + * @param options The [PatchOption]s to initialize with. + */ +class PatchOptions internal constructor( + private val options: MutableMap> = mutableMapOf() +) : MutableMap> by options { + /** + * Register a [PatchOption]. Acts like [MutableMap.put]. + * @param value The [PatchOption] to register. + */ + fun register(value: PatchOption<*>) { + options[value.key] = value + } + + /** + * Set an option's value. + * @param key The identifier. + * @param value The value. + * @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist. + */ + operator fun set(key: String, value: T?) { + val option = this[key] + + try { + @Suppress("UNCHECKED_CAST") + (option as PatchOption).value = value + } catch (e: ClassCastException) { + throw PatchOptionException.InvalidValueTypeException( + value?.let { it::class.java.name } ?: "null", + option.value?.let { it::class.java.name } ?: "null", + ) + } + } + + /** + * Get an option. + */ + override operator fun get(key: String) = + options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key) +} \ No newline at end of file diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/BooleanPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/BooleanPatchOption.kt new file mode 100644 index 00000000..471478c4 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/BooleanPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [Boolean]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class BooleanPatchOption private constructor( + key: String, + default: Boolean?, + title: String?, + description: String?, + required: Boolean, + validator: (Boolean?) -> Boolean +) : PatchOption(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [BooleanPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [BooleanPatchOption]. + * + * @see BooleanPatchOption + * @see PatchOption + */ + fun > T.booleanPatchOption( + key: String, + default: Boolean? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Boolean?) -> Boolean = { true } + ) = BooleanPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/FloatPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/FloatPatchOption.kt new file mode 100644 index 00000000..beec13ab --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/FloatPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [Float]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class FloatPatchOption private constructor( + key: String, + default: Float?, + title: String?, + description: String?, + required: Boolean, + validator: (Float?) -> Boolean +) : PatchOption(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [FloatPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [FloatPatchOption]. + * + * @see FloatPatchOption + * @see PatchOption + */ + fun > T.floatPatchOption( + key: String, + default: Float? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Float?) -> Boolean = { true } + ) = FloatPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/IntPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/IntPatchOption.kt new file mode 100644 index 00000000..c815b59c --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/IntPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing an [Integer]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class IntPatchOption private constructor( + key: String, + default: Int?, + title: String?, + description: String?, + required: Boolean, + validator: (Int?) -> Boolean +) : PatchOption(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [IntPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [IntPatchOption]. + * + * @see IntPatchOption + * @see PatchOption + */ + fun > T.intPatchOption( + key: String, + default: Int? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Int?) -> Boolean = { true } + ) = IntPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/LongPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/LongPatchOption.kt new file mode 100644 index 00000000..563fa217 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/LongPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [Long]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class LongPatchOption private constructor( + key: String, + default: Long?, + title: String?, + description: String?, + required: Boolean, + validator: (Long?) -> Boolean +) : PatchOption(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [LongPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [LongPatchOption]. + * + * @see LongPatchOption + * @see PatchOption + */ + fun > T.longPatchOption( + key: String, + default: Long? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Long?) -> Boolean = { true } + ) = LongPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/StringPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/StringPatchOption.kt new file mode 100644 index 00000000..5e2677f2 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/StringPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [String]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class StringPatchOption private constructor( + key: String, + default: String?, + title: String?, + description: String?, + required: Boolean, + validator: (String?) -> Boolean +) : PatchOption(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [StringPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [StringPatchOption]. + * + * @see StringPatchOption + * @see PatchOption + */ + fun > T.stringPatchOption( + key: String, + default: String? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (String?) -> Boolean = { true } + ) = StringPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption.kt new file mode 100644 index 00000000..84bd809f --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/BooleanArrayPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types.array + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [Boolean] array. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class BooleanArrayPatchOption private constructor( + key: String, + default: Array?, + title: String?, + description: String?, + required: Boolean, + validator: (Array?) -> Boolean +) : PatchOption>(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [BooleanArrayPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [BooleanArrayPatchOption]. + * + * @see BooleanArrayPatchOption + * @see PatchOption + */ + fun > T.booleanArrayPatchOption( + key: String, + default: Array? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Array?) -> Boolean = { true } + ) = BooleanArrayPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/FloatArrayPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/FloatArrayPatchOption.kt new file mode 100644 index 00000000..c9e829da --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/FloatArrayPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types.array + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [Float] array. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class FloatArrayPatchOption private constructor( + key: String, + default: Array?, + title: String?, + description: String?, + required: Boolean, + validator: (Array?) -> Boolean +) : PatchOption>(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [FloatArrayPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [FloatArrayPatchOption]. + * + * @see FloatArrayPatchOption + * @see PatchOption + */ + fun > T.floatArrayPatchOption( + key: String, + default: Array? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Array?) -> Boolean = { true } + ) = FloatArrayPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/IntArrayPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/IntArrayPatchOption.kt new file mode 100644 index 00000000..28211cf3 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/IntArrayPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types.array + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing an [Integer] array. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class IntArrayPatchOption private constructor( + key: String, + default: Array?, + title: String?, + description: String?, + required: Boolean, + validator: (Array?) -> Boolean +) : PatchOption>(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [IntArrayPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [IntArrayPatchOption]. + * + * @see IntArrayPatchOption + * @see PatchOption + */ + fun > T.intArrayPatchOption( + key: String, + default: Array? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Array?) -> Boolean = { true } + ) = IntArrayPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/LongArrayPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/LongArrayPatchOption.kt new file mode 100644 index 00000000..babb6907 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/LongArrayPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types.array + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [Long] array. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class LongArrayPatchOption private constructor( + key: String, + default: Array?, + title: String?, + description: String?, + required: Boolean, + validator: (Array?) -> Boolean +) : PatchOption>(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [LongArrayPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [LongArrayPatchOption]. + * + * @see LongArrayPatchOption + * @see PatchOption + */ + fun > T.longArrayPatchOption( + key: String, + default: Array? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Array?) -> Boolean = { true } + ) = LongArrayPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} diff --git a/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/StringArrayPatchOption.kt b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/StringArrayPatchOption.kt new file mode 100644 index 00000000..a73053c8 --- /dev/null +++ b/revanced-patcher/src/main/kotlin/app/revanced/patcher/patch/options/types/array/StringArrayPatchOption.kt @@ -0,0 +1,48 @@ +package app.revanced.patcher.patch.options.types.array + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.options.PatchOption + +/** + * A [PatchOption] representing a [String] array. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * + * @see PatchOption + */ +class StringArrayPatchOption private constructor( + key: String, + default: Array?, + title: String?, + description: String?, + required: Boolean, + validator: (Array?) -> Boolean +) : PatchOption>(key, default, title, description, required, validator) { + companion object { + /** + * Create a new [StringArrayPatchOption] and add it to the current [Patch]. + * + * @param key The identifier. + * @param default The default value. + * @param title The title. + * @param description A description. + * @param required Whether the option is required. + * @return The created [StringArrayPatchOption]. + * + * @see StringArrayPatchOption + * @see PatchOption + */ + fun > T.stringArrayPatchOption( + key: String, + default: Array? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: (Array?) -> Boolean = { true } + ) = StringArrayPatchOption(key, default, title, description, required, validator).also { options.register(it) } + } +} 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 feed3aa7..a7746340 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/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/options/PatchOptionsTest.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/options/PatchOptionsTest.kt new file mode 100644 index 00000000..95538d1f --- /dev/null +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/options/PatchOptionsTest.kt @@ -0,0 +1,59 @@ +package app.revanced.patcher.patch.options + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.options.types.BooleanPatchOption.Companion.booleanPatchOption +import app.revanced.patcher.patch.options.types.StringPatchOption.Companion.stringPatchOption +import app.revanced.patcher.patch.options.types.array.StringArrayPatchOption.Companion.stringArrayPatchOption +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import kotlin.test.Test + +internal class PatchOptionsTest { + @Test + fun `should not fail because default value is unvalidated`() { + assertDoesNotThrow { + OptionsTestPatch.options["required"].value + } + } + + @Test + fun `should throw due to incorrect type`() { + assertThrows { + OptionsTestPatch.options["bool"] = 0 + } + } + + @Test + fun `should be nullable`() { + OptionsTestPatch.options["bool"] = null + } + + @Test + fun `option should not be found`() { + assertThrows { + OptionsTestPatch.options["this option does not exist"] = 1 + } + } + + @Test + fun `should be able to add options manually`() { + assertThrows { + OptionsTestPatch.options["array"] = OptionsTestPatch.stringArrayOption + } + assertDoesNotThrow { + OptionsTestPatch.options.register(OptionsTestPatch.stringArrayOption) + } + } + + private object OptionsTestPatch : BytecodePatch() { + private var stringOption by stringPatchOption("string", "default") + private var booleanOption by booleanPatchOption("bool", true) + private var requiredStringOption by stringPatchOption("required", "default", required = true) + private var nullDefaultRequiredOption by stringPatchOption("null", null, required = true) + + val stringArrayOption = stringArrayPatchOption("array", arrayOf("1", "2")) + + override fun execute(context: BytecodeContext) {} + } +} \ No newline at end of file diff --git a/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleBytecodePatch.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleBytecodePatch.kt new file mode 100644 index 00000000..cca07ed4 --- /dev/null +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleBytecodePatch.kt @@ -0,0 +1,146 @@ +package app.revanced.patcher.patch.usage + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.extensions.or +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Format +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c +import com.android.tools.smali.dexlib2.immutable.ImmutableField +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference +import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue +import com.android.tools.smali.dexlib2.util.Preconditions +import com.google.common.collect.ImmutableList + +@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)) { + // Entry point of a patch. Supplied fingerprints are resolved at this point. + override fun execute(context: BytecodeContext) { + ExampleFingerprint.result?.let { result -> + // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." + // Get the start index of our opcode pattern. + // This will be the index of the instruction with the opcode CONST_STRING. + val startIndex = result.scanResult.patternScanResult!!.startIndex + + result.mutableMethod.apply { + replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") + + // Store the fields initial value into the first virtual register. + replaceInstruction(0, "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;") + + // Now let's create a new call to our method and print the return value! + // You can also use the smali compiler to create instructions. + // For this sake of example I reuse the TestClass field dummyField inside the virtual register 0. + // + // Control flow instructions are not supported as of now. + addInstructionsWithLabels( + startIndex + 2, + """ + invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; + move-result-object v1 + invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + """ + ) + } + + // Find the class in which the method matching our fingerprint is defined in. + context.findClass(result.classDef.type)!!.mutableClass.apply { + // Add a new method that returns a string. + methods.add( + ImmutableMethod( + result.classDef.type, + "returnHello", + null, + "Ljava/lang/String;", + AccessFlags.PRIVATE or AccessFlags.STATIC, + null, + null, + ImmutableMethodImplementation( + 1, + ImmutableList.of( + BuilderInstruction21c( + Opcode.CONST_STRING, + 0, + ImmutableStringReference("Hello, ReVanced! Adding bytecode.") + ), + BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) + ), + null, + null + ) + ).toMutable() + ) + + // Add a field in the main class. + // We will use this field in our method below to call println on. + // The field holds the Ljava/io/PrintStream->out; field. + fields.add( + ImmutableField( + type, + "dummyField", + "Ljava/io/PrintStream;", + AccessFlags.PRIVATE or AccessFlags.STATIC, + ImmutableFieldEncodedValue( + ImmutableFieldReference( + "Ljava/lang/System;", + "out", + "Ljava/io/PrintStream;" + ) + ), + null, + null + ).toMutable() + ) + } + } ?: throw PatchException("Fingerprint failed to resolve.") + } + + /** + * Replace an existing instruction with a new one containing a reference to a new string. + * @param index The index of the instruction to replace. + * @param string The replacement string. + */ + private fun MutableMethod.replaceStringAt(index: Int, string: String) { + val instruction = getInstruction(index) + + // Utility method of dexlib2. + Preconditions.checkFormat(instruction.opcode, Format.Format21c) + + // Cast this to an instruction of the format 21c. + // The instruction format can be found in the docs at + // https://source.android.com/devices/tech/dalvik/dalvik-bytecode + val strInstruction = instruction as Instruction21c + + // In our case we want an instruction with the opcode CONST_STRING + // The format is 21c, so we create a new BuilderInstruction21c + // This instruction will hold the string reference constant in the virtual register of the original instruction + // For that a reference to the string is needed. It can be created with an ImmutableStringReference. + // At last, use the method replaceInstruction to replace it at the given index startIndex. + replaceInstruction( + index, + "const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}" + ) + } +} + diff --git a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleFingerprint.kt similarity index 82% rename from src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt rename to revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleFingerprint.kt index 3cb5b067..0958aef3 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleFingerprint.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleFingerprint.kt @@ -1,4 +1,4 @@ -package app.revanced.patcher.usage.bytecode +package app.revanced.patcher.patch.usage import app.revanced.patcher.extensions.or import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint @@ -12,8 +12,8 @@ object ExampleFingerprint : MethodFingerprint( listOf("[L"), listOf( Opcode.SGET_OBJECT, - null, // Testing unknown opcodes. - Opcode.INVOKE_STATIC, // This is intentionally wrong to test the Fuzzy resolver. + null, // Matching unknown opcodes. + Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching. Opcode.RETURN_VOID ), null diff --git a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/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/patch/usage/ExampleResourcePatch.kt index 6c45b0ab..f7a99819 100644 --- a/src/test/kotlin/app/revanced/patcher/usage/resource/patch/ExampleResourcePatch.kt +++ b/revanced-patcher/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleResourcePatch.kt @@ -1,18 +1,11 @@ -package app.revanced.patcher.usage.resource.patch +package app.revanced.patcher.patch.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 d398cc61..3761ccf9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,4 +19,4 @@ dependencyResolutionManagement { } } -rootProject.name = "revanced-patcher" +include("revanced-patch-annotation-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 b69b140c..00000000 --- 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 2959f6b7..00000000 --- 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 ed7780c8..00000000 --- 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 19e61e8c..00000000 --- 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 9f9cd3a6..00000000 --- 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 493056ea..00000000 --- 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/OptionsContainer.kt b/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt deleted file mode 100644 index f14ca483..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/OptionsContainer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patcher.patch - -/** - * A container for patch options. - */ -abstract class OptionsContainer { - /** - * A list of [PatchOption]s. - * @see PatchOptions - */ - @Suppress("MemberVisibilityCanBePrivate") - val options = PatchOptions() - - protected fun option(opt: PatchOption): PatchOption { - options.register(opt) - return opt - } -} \ 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 621145bc..00000000 --- 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/PatchOption.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt deleted file mode 100644 index ecf3a852..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchOption.kt +++ /dev/null @@ -1,232 +0,0 @@ -@file:Suppress("CanBeParameter", "MemberVisibilityCanBePrivate", "UNCHECKED_CAST") - -package app.revanced.patcher.patch - -import java.nio.file.Path -import kotlin.io.path.pathString -import kotlin.reflect.KProperty - -class NoSuchOptionException(val option: String) : Exception("No such option: $option") -class IllegalValueException(val value: Any?) : Exception("Illegal value: $value") -class InvalidTypeException(val got: String, val expected: String) : - Exception("Invalid option value type: $got, expected $expected") - -object RequirementNotMetException : Exception("null was passed into an option that requires a value") - -/** - * A registry for an array of [PatchOption]s. - * @param options An array of [PatchOption]s. - */ -class PatchOptions(vararg options: PatchOption<*>) : Iterable> { - private val register = mutableMapOf>() - - init { - options.forEach { register(it) } - } - - internal fun register(option: PatchOption<*>) { - if (register.containsKey(option.key)) { - throw IllegalStateException("Multiple options found with the same key") - } - register[option.key] = option - } - - /** - * Get a [PatchOption] by its key. - * @param key The key of the [PatchOption]. - */ - @JvmName("getUntyped") - operator fun get(key: String) = register[key] ?: throw NoSuchOptionException(key) - - /** - * Get a [PatchOption] by its key. - * @param key The key of the [PatchOption]. - */ - inline operator fun get(key: String): PatchOption { - val opt = get(key) - if (opt.value !is T) throw InvalidTypeException( - opt.value?.let { it::class.java.canonicalName } ?: "null", - T::class.java.canonicalName - ) - return opt as PatchOption - } - - /** - * Set the value of a [PatchOption]. - * @param key The key of the [PatchOption]. - * @param value The value you want it to be. - * Please note that using the wrong value type results in a runtime error. - */ - inline operator fun set(key: String, value: T) { - val opt = get(key) - if (opt.value !is T) throw InvalidTypeException( - T::class.java.canonicalName, - opt.value?.let { it::class.java.canonicalName } ?: "null" - ) - opt.value = value - } - - /** - * Sets the value of a [PatchOption] to `null`. - * @param key The key of the [PatchOption]. - */ - fun nullify(key: String) { - get(key).value = null - } - - override fun iterator() = register.values.iterator() -} - -/** - * A [Patch] option. - * @param key Unique identifier of the option. Example: _`settings`_ - * @param default The default value of the option. - * @param title A human-readable title of the option. Example: _Patch Settings_ - * @param description A human-readable description of the option. Example: _Settings for the patches._ - * @param required Whether the option is required. - */ -@Suppress("MemberVisibilityCanBePrivate") -sealed class PatchOption( - val key: String, - default: T?, - val title: String, - val description: String, - val required: Boolean, - val validator: (T?) -> Boolean -) { - var value: T? = default - get() { - if (field == null && required) { - throw RequirementNotMetException - } - return field - } - set(value) { - if (value == null && required) { - throw RequirementNotMetException - } - if (!validator(value)) { - throw IllegalValueException(value) - } - field = value - } - - /** - * Gets the value of the option. - * Please note that using the wrong value type results in a runtime error. - */ - @JvmName("getValueTyped") - inline operator fun getValue(thisRef: Nothing?, property: KProperty<*>): V? { - if (value !is V?) throw InvalidTypeException( - V::class.java.canonicalName, - value?.let { it::class.java.canonicalName } ?: "null" - ) - return value as? V? - } - - operator fun getValue(thisRef: Any?, property: KProperty<*>) = value - - /** - * Gets the value of the option. - * Please note that using the wrong value type results in a runtime error. - */ - @JvmName("setValueTyped") - inline operator fun setValue(thisRef: Nothing?, property: KProperty<*>, new: V) { - if (value !is V) throw InvalidTypeException( - V::class.java.canonicalName, - value?.let { it::class.java.canonicalName } ?: "null" - ) - value = new as T - } - - operator fun setValue(thisRef: Any?, property: KProperty<*>, new: T?) { - value = new - } - - /** - * A [PatchOption] representing a [String]. - * @see PatchOption - */ - class StringOption( - key: String, - default: String?, - title: String, - description: String, - required: Boolean = false, - validator: (String?) -> Boolean = { true } - ) : PatchOption( - key, default, title, description, required, validator - ) - - /** - * A [PatchOption] representing a [Boolean]. - * @see PatchOption - */ - class BooleanOption( - key: String, - default: Boolean?, - title: String, - description: String, - required: Boolean = false, - validator: (Boolean?) -> Boolean = { true } - ) : PatchOption( - key, default, title, description, required, validator - ) - - /** - * A [PatchOption] with a list of allowed options. - * @param options A list of allowed options for the [ListOption]. - * @see PatchOption - */ - sealed class ListOption( - key: String, - default: E?, - val options: Iterable, - title: String, - description: String, - required: Boolean = false, - validator: (E?) -> Boolean = { true } - ) : PatchOption( - key, default, title, description, required, { - (it?.let { it in options } ?: true) && validator(it) - } - ) { - init { - if (default != null && default !in options) { - throw IllegalStateException("Default option must be an allowed option") - } - } - } - - /** - * A [ListOption] of type [String]. - * @see ListOption - */ - class StringListOption( - key: String, - default: String?, - options: Iterable, - title: String, - description: String, - required: Boolean = false, - validator: (String?) -> Boolean = { true } - ) : ListOption( - key, default, options, title, description, required, validator - ) - - /** - * A [ListOption] of type [Int]. - * @see ListOption - */ - class IntListOption( - key: String, - default: Int?, - options: Iterable, - title: String, - description: String, - required: Boolean = false, - validator: (Int?) -> Boolean = { true } - ) : ListOption( - key, default, options, title, description, required, validator - ) -} 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 4e45c382..00000000 --- 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/issues/Issue98.kt b/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt deleted file mode 100644 index 9e095b5f..00000000 --- a/src/test/kotlin/app/revanced/patcher/issues/Issue98.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patcher.issues - -import app.revanced.patcher.patch.PatchOption -import org.junit.jupiter.api.Test -import kotlin.test.assertNull - -internal class Issue98 { - companion object { - var key1: String? by PatchOption.StringOption( - "key1", null, "title", "description" - ) - } - - @Test - fun `should infer nullable type correctly`() { - assertNull(key1) - } -} \ No newline at end of file diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt deleted file mode 100644 index 8d370111..00000000 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchOptionsTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -package app.revanced.patcher.patch - -import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull - -internal class PatchOptionsTest { - private val options = ExampleBytecodePatch.options - - @Test - fun `should not throw an exception`() { - for (option in options) { - when (option) { - is PatchOption.StringOption -> { - option.value = "Hello World" - } - - is PatchOption.BooleanOption -> { - option.value = false - } - - is PatchOption.StringListOption -> { - option.value = option.options.first() - for (choice in option.options) { - assertNotNull(choice) - } - } - - is PatchOption.IntListOption -> { - option.value = option.options.first() - for (choice in option.options) { - assertNotNull(choice) - } - } - } - } - val option = options.get("key1") - // or: val option: String? by options["key1"] - // then you won't need `.value` every time - assertEquals("Hello World", option.value) - options["key1"] = "Hello, world!" - assertEquals("Hello, world!", option.value) - } - - @Test - fun `should return a different value when changed`() { - var value: String? by options["key1"] - val current = value + "" // force a copy - value = "Hello, world!" - assertNotEquals(current, value) - } - - @Test - fun `should be able to set value to null`() { - // Sadly, doing: - // > options["key2"] = null - // is not possible because Kotlin - // cannot reify the type "Nothing?". - // So we have to do this instead: - options["key2"] = null as Any? - // This is a cleaner replacement for the above: - options.nullify("key2") - } - - @Test - fun `should fail because the option does not exist`() { - assertThrows { - options["this option does not exist"] = 123 - } - } - - @Test - fun `should fail because of invalid value type when setting an option`() { - assertThrows { - options["key1"] = 123 - } - } - - @Test - fun `should fail because of invalid value type when getting an option`() { - assertThrows { - options.get("key1") - } - } - - @Test - fun `should fail because of an illegal value`() { - assertThrows { - options["key3"] = "this value is not an allowed option" - } - } - - @Test - fun `should fail because the requirement is not met`() { - assertThrows { - options.nullify("key1") - } - } - - @Test - fun `should fail because getting a non-initialized option is illegal`() { - assertThrows { - options["key5"].value - } - } -} \ 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 85f0a328..00000000 --- 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/bytecode/ExampleBytecodePatch.kt b/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt deleted file mode 100644 index f41a6824..00000000 --- a/src/test/kotlin/app/revanced/patcher/usage/bytecode/ExampleBytecodePatch.kt +++ /dev/null @@ -1,190 +0,0 @@ -package app.revanced.patcher.usage.bytecode - -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.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 -import com.android.tools.smali.dexlib2.Format -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation -import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x -import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c -import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c -import com.android.tools.smali.dexlib2.immutable.ImmutableField -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation -import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReference -import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference -import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue -import com.android.tools.smali.dexlib2.util.Preconditions -import com.google.common.collect.ImmutableList - -@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) { - // Get the resolved method by its fingerprint from the resolver cache - val result = ExampleFingerprint.result!! - - // Patch options - println(key1) - key2 = false - - // Get the implementation for the resolved method - val method = result.mutableMethod - val implementation = method.implementation!! - - // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." - // Get the start index of our opcode pattern. - // This will be the index of the instruction with the opcode CONST_STRING. - val startIndex = result.scanResult.patternScanResult!!.startIndex - - implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") - - // Get the class in which the method matching our fingerprint is defined in. - val mainClass = context.findClass { - it.type == result.classDef.type - }!!.mutableClass - - // Add a new method returning a string - mainClass.methods.add( - ImmutableMethod( - result.classDef.type, - "returnHello", - null, - "Ljava/lang/String;", - AccessFlags.PRIVATE or AccessFlags.STATIC, - null, - null, - ImmutableMethodImplementation( - 1, - ImmutableList.of( - BuilderInstruction21c( - Opcode.CONST_STRING, - 0, - ImmutableStringReference("Hello, ReVanced! Adding bytecode.") - ), - BuilderInstruction11x(Opcode.RETURN_OBJECT, 0) - ), - null, - null - ) - ).toMutable() - ) - - // Add a field in the main class - // We will use this field in our method below to call println on - // The field holds the Ljava/io/PrintStream->out; field - mainClass.fields.add( - ImmutableField( - mainClass.type, - "dummyField", - "Ljava/io/PrintStream;", - AccessFlags.PRIVATE or AccessFlags.STATIC, - ImmutableFieldEncodedValue( - ImmutableFieldReference( - "Ljava/lang/System;", - "out", - "Ljava/io/PrintStream;" - ) - ), - null, - null - ).toMutable() - ) - - // store the fields initial value into the first virtual register - method.replaceInstruction(0, "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;") - - // Now let's create a new call to our method and print the return value! - // You can also use the smali compiler to create instructions. - // For this sake of example I reuse the TestClass field dummyField inside the virtual register 0. - // - // Control flow instructions are not supported as of now. - method.addInstructionsWithLabels( - startIndex + 2, - """ - invoke-static { }, LTestClass;->returnHello()Ljava/lang/String; - move-result-object v1 - invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V - """ - ) - } - - /** - * Replace the string for an instruction at the given index with a new one. - * @param index The index of the instruction to replace the string for - * @param string The replacing string - */ - private fun MutableMethodImplementation.replaceStringAt(index: Int, string: String) { - val instruction = this.instructions[index] - - // Utility method of dexlib2 - Preconditions.checkFormat(instruction.opcode, Format.Format21c) - - // Cast this to an instruction of the format 21c - // The instruction format can be found in the docs at - // https://source.android.com/devices/tech/dalvik/dalvik-bytecode - val strInstruction = instruction as Instruction21c - - // In our case we want an instruction with the opcode CONST_STRING - // The format is 21c, so we create a new BuilderInstruction21c - // This instruction will hold the string reference constant in the virtual register of the original instruction - // For that a reference to the string is needed. It can be created with an ImmutableStringReference. - // At last, use the method replaceInstruction to replace it at the given index startIndex. - this.replaceInstruction( - index, - BuilderInstruction21c( - Opcode.CONST_STRING, - strInstruction.registerA, - ImmutableStringReference(string) - ) - ) - } - - @Suppress("unused") - companion object : OptionsContainer() { - 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 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 key5 by option( - PatchOption.StringOption( - "key5", null, "title", "description", true - ) - ) - } -} 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 059649e0..00000000 --- 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 -