From 3b4db3ddb72cdcee8af2f787eadf58eeb37543de Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Fri, 1 Sep 2023 03:37:52 +0200 Subject: [PATCH] feat!: Remove patch annotations Annotations required reflection and working with them turned out to be rather cumbersome. The annotations have been replaced with properties for the most part. BREAKING CHANGE: 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. --- api/revanced-patcher.api | 153 +++++++-------- .../app/revanced/patcher/PatchBundleLoader.kt | 115 +++++++---- .../kotlin/app/revanced/patcher/Patcher.kt | 180 +++++++++++------- .../app/revanced/patcher/PatcherContext.kt | 13 +- .../app/revanced/patcher/PatcherException.kt | 16 ++ .../app/revanced/patcher/PatchesConsumer.kt | 4 +- .../annotation/CompatibilityAnnotation.kt | 23 --- .../patcher/annotation/MetadataAnnotation.kt | 21 -- .../revanced/patcher/data/BytecodeContext.kt | 1 - .../extensions/MethodFingerprintExtensions.kt | 8 +- .../patcher/extensions/PatchExtensions.kt | 64 ------- .../method/impl/MethodFingerprint.kt | 2 +- .../app/revanced/patcher/patch/Patch.kt | 84 ++++++-- .../app/revanced/patcher/patch/PatchResult.kt | 14 +- .../patcher/patch/annotations/Patch.kt | 7 + .../patch/annotations/PatchAnnotation.kt | 27 --- 16 files changed, 383 insertions(+), 349 deletions(-) create mode 100644 src/main/kotlin/app/revanced/patcher/PatcherException.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt create mode 100644 src/main/kotlin/app/revanced/patcher/patch/annotations/Patch.kt delete mode 100644 src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 7a761a9..160d13b 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -7,54 +7,56 @@ public final class app/revanced/patcher/PackageMetadata { public final fun getPackageVersion ()Ljava/lang/String; } -public abstract class app/revanced/patcher/PatchBundleLoader : java/util/List, kotlin/jvm/internal/markers/KMutableList { - public synthetic fun (Ljava/lang/Iterable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun add (ILjava/lang/Class;)V - public synthetic fun add (ILjava/lang/Object;)V - public fun add (Ljava/lang/Class;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (ILjava/util/Collection;)Z - public fun addAll (Ljava/util/Collection;)Z +public abstract class app/revanced/patcher/PatchBundleLoader : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { + public synthetic fun (Ljava/lang/ClassLoader;[Ljava/io/File;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun clear ()V - public fun contains (Ljava/lang/Class;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public fun get (I)Ljava/lang/Class; - public synthetic fun get (I)Ljava/lang/Object; + public synthetic fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun compute (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object; + public fun computeIfAbsent (Ljava/lang/String;Ljava/util/function/Function;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun computeIfPresent (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch; + public final fun containsKey (Ljava/lang/Object;)Z + public fun containsKey (Ljava/lang/String;)Z + public fun containsValue (Lapp/revanced/patcher/patch/Patch;)Z + public final fun containsValue (Ljava/lang/Object;)Z + public final fun entrySet ()Ljava/util/Set; + public final fun get (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Patch; + public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; + public fun get (Ljava/lang/String;)Lapp/revanced/patcher/patch/Patch; + public fun getEntries ()Ljava/util/Set; + public fun getKeys ()Ljava/util/Set; public fun getSize ()I - public fun indexOf (Ljava/lang/Class;)I - public final fun indexOf (Ljava/lang/Object;)I + public fun getValues ()Ljava/util/Collection; public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun lastIndexOf (Ljava/lang/Class;)I - public final fun lastIndexOf (Ljava/lang/Object;)I - public fun listIterator ()Ljava/util/ListIterator; - public fun listIterator (I)Ljava/util/ListIterator; - public final fun remove (I)Ljava/lang/Class; - public synthetic fun remove (I)Ljava/lang/Object; - public fun remove (Ljava/lang/Class;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun removeAt (I)Ljava/lang/Class; - public fun retainAll (Ljava/util/Collection;)Z - public fun set (ILjava/lang/Class;)Ljava/lang/Class; - public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; + public final fun keySet ()Ljava/util/Set; + public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object; + public fun merge (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch; + public fun putAll (Ljava/util/Map;)V + public synthetic fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public fun putIfAbsent (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch; + public fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Patch; + public synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object; + public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z + public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z + public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch; + public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;Lapp/revanced/patcher/patch/Patch;)Z + public fun replaceAll (Ljava/util/function/BiFunction;)V public final fun size ()I - public fun subList (II)Ljava/util/List; - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; + public final fun values ()Ljava/util/Collection; } public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/patcher/PatchBundleLoader { public fun ([Ljava/io/File;)V public fun ([Ljava/io/File;Ljava/io/File;)V public synthetic fun ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun remove (I)Ljava/lang/Object; } public final class app/revanced/patcher/PatchBundleLoader$Jar : app/revanced/patcher/PatchBundleLoader { public fun ([Ljava/io/File;)V - public synthetic fun remove (I)Ljava/lang/Object; } public abstract interface class app/revanced/patcher/PatchExecutorFunction : java/util/function/Function { @@ -76,6 +78,14 @@ public final class app/revanced/patcher/PatcherContext { public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata; } +public abstract class app/revanced/patcher/PatcherException : java/lang/Exception { + public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + +public final class app/revanced/patcher/PatcherException$CircularDependencyException : app/revanced/patcher/PatcherException { +} + public final class app/revanced/patcher/PatcherOptions { public fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V public synthetic fun (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -115,23 +125,6 @@ public abstract interface class app/revanced/patcher/PatchesConsumer { public abstract fun acceptPatches (Ljava/util/List;)V } -public abstract interface annotation class app/revanced/patcher/annotation/Compatibility : java/lang/annotation/Annotation { - public abstract fun compatiblePackages ()[Lapp/revanced/patcher/annotation/Package; -} - -public abstract interface annotation class app/revanced/patcher/annotation/Description : java/lang/annotation/Annotation { - public abstract fun description ()Ljava/lang/String; -} - -public abstract interface annotation class app/revanced/patcher/annotation/Name : java/lang/annotation/Annotation { - public abstract fun name ()Ljava/lang/String; -} - -public abstract interface annotation class app/revanced/patcher/annotation/Package : java/lang/annotation/Annotation { - public abstract fun name ()Ljava/lang/String; - public abstract fun versions ()[Ljava/lang/String; -} - public final class app/revanced/patcher/data/BytecodeContext : app/revanced/patcher/data/Context { public final fun findClass (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy; public final fun findClass (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy; @@ -199,17 +192,6 @@ public final class app/revanced/patcher/extensions/InstructionExtensions { public final class app/revanced/patcher/extensions/MethodFingerprintExtensions { public static final field INSTANCE Lapp/revanced/patcher/extensions/MethodFingerprintExtensions; public final fun getFuzzyPatternScanMethod (Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/method/annotation/FuzzyPatternScanMethod; - public final fun getName (Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)Ljava/lang/String; -} - -public final class app/revanced/patcher/extensions/PatchExtensions { - public static final field INSTANCE Lapp/revanced/patcher/extensions/PatchExtensions; - public final fun getCompatiblePackages (Ljava/lang/Class;)[Lapp/revanced/patcher/annotation/Package; - public final fun getDependencies (Ljava/lang/Class;)[Lkotlin/reflect/KClass; - public final fun getDescription (Ljava/lang/Class;)Ljava/lang/String; - public final fun getInclude (Ljava/lang/Class;)Z - public final fun getOptions (Ljava/lang/Class;)Lapp/revanced/patcher/patch/PatchOptions; - public final fun getPatchName (Ljava/lang/Class;)Ljava/lang/String; } public abstract interface class app/revanced/patcher/fingerprint/Fingerprint { @@ -345,9 +327,7 @@ 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 (Lapp/revanced/patcher/patch/Patch$Manifest;[Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)V } public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception { @@ -372,8 +352,34 @@ public abstract class app/revanced/patcher/patch/OptionsContainer { protected final fun option (Lapp/revanced/patcher/patch/PatchOption;)Lapp/revanced/patcher/patch/PatchOption; } -public abstract interface class app/revanced/patcher/patch/Patch { +public abstract class app/revanced/patcher/patch/Patch { + public synthetic fun (Lapp/revanced/patcher/patch/Patch$Manifest;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z public abstract fun execute (Lapp/revanced/patcher/data/Context;)V + public final fun getManifest ()Lapp/revanced/patcher/patch/Patch$Manifest; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/revanced/patcher/patch/Patch$Manifest { + public fun (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getCompatiblePackages ()Ljava/util/Set; + public final fun 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/PatchOptions; + public final fun getRequiresIntegrations ()Z + public final fun getUse ()Z + public fun hashCode ()I +} + +public final class app/revanced/patcher/patch/Patch$Manifest$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/PatchException : java/lang/Exception { @@ -429,26 +435,21 @@ public final class app/revanced/patcher/patch/PatchOptions : java/lang/Iterable, } public final class app/revanced/patcher/patch/PatchResult { + public fun equals (Ljava/lang/Object;)Z public final fun getException ()Lapp/revanced/patcher/patch/PatchException; - public final fun getPatchName ()Ljava/lang/String; + public final fun getPatch ()Lapp/revanced/patcher/patch/Patch; + public fun hashCode ()I } public final class app/revanced/patcher/patch/RequirementNotMetException : java/lang/Exception { public static final field INSTANCE Lapp/revanced/patcher/patch/RequirementNotMetException; } -public abstract interface class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { -} - -public abstract interface annotation class app/revanced/patcher/patch/annotations/DependsOn : java/lang/annotation/Annotation { - public abstract fun dependencies ()[Ljava/lang/Class; +public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { + public fun (Lapp/revanced/patcher/patch/Patch$Manifest;)V } public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation { - public abstract fun include ()Z -} - -public abstract interface annotation class app/revanced/patcher/patch/annotations/RequiresIntegrations : java/lang/annotation/Annotation { } public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable { diff --git a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt b/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt index b69b140..246b4c9 100644 --- a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt +++ b/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt @@ -3,36 +3,85 @@ 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 +import java.util.logging.Logger +import kotlin.reflect.KClass /** - * A patch bundle. + * [Patch]es mapped by their name. + */ +typealias PatchMap = Map> + +/** + * A [Patch] class. + */ +typealias PatchClass = KClass> + +/** + * A loader of [Patch]es from patch bundles. + * This will load all [Patch]es from the given patch bundles. * - * - * @param fromClasses The classes to get [Patch]es from. + * @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle. + * @param classLoader The [ClassLoader] to use for loading the classes. */ sealed class PatchBundleLoader private constructor( - fromClasses: Iterable> -) : MutableList by mutableListOf() { + classLoader: ClassLoader, + patchBundles: Array, + getBinaryClassNames: (patchBundle: File) -> List, +) : PatchMap by mutableMapOf() { + private val logger = Logger.getLogger(PatchBundleLoader::class.java.name) + init { - fromClasses.filter { + patchBundles.flatMap(getBinaryClassNames).map { + classLoader.loadClass(it) + }.filter { if (it.isAnnotation) return@filter false it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null - }.map { + }.mapNotNull { patchClass -> + patchClass.getInstance(logger) + }.associateBy { it.manifest.name } + let { patches -> @Suppress("UNCHECKED_CAST") - it as PatchClass - }.sortedBy { - it.patchName - }.let { addAll(it) } + (this as MutableMap>).putAll(patches) + } + } + + + internal companion object Utils { + /** + * Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used. + * + * @param logger The [Logger] to use for logging. + * @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated. + */ + internal fun Class<*>.getInstance(logger: Logger): Patch<*>? { + return try { + getField("INSTANCE").get(null) + } catch (exception: NoSuchFileException) { + logger.fine( + "Patch class '${name}' has no INSTANCE field, therefor not a singleton. " + + "Will try to instantiate it." + ) + + try { + getDeclaredConstructor().newInstance() + } catch (exception: Exception) { + logger.severe( + "Patch class '${name}' is not singleton and has no suitable constructor, " + + "therefor cannot be instantiated and will be ignored." + ) + + return null + } + } as Patch<*> + } } /** @@ -41,18 +90,13 @@ sealed class PatchBundleLoader private constructor( * @param patchBundles The path to patch bundles of JAR format. */ class Jar(vararg patchBundles: File) : PatchBundleLoader( - with( - URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()) - ) { - patchBundles.flatMap { patchBundle -> - // Get the names of all classes in the DEX file. - - JarFile(patchBundle).entries().asSequence() - .filter { it.name.endsWith(".class") } - .map { it.name.replace('/', '.').replace(".class", "") } - .map { loadClass(it) } - } - }) + 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. @@ -62,20 +106,19 @@ sealed class PatchBundleLoader private constructor( * This parameter is deprecated and has no effect since API level 26. */ class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader( - with( - DexClassLoader( + 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) } - }) { + ), + 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) } diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index 2959f6b..160859a 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,11 +1,8 @@ package app.revanced.patcher -import app.revanced.patcher.data.Context +import app.revanced.patcher.PatchBundleLoader.Utils.getInstance 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.* @@ -49,32 +46,78 @@ class Patcher( context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY) } - override fun acceptPatches(patches: List) { + /** + * 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. + */ + @Suppress("NAME_SHADOWING") + override fun acceptPatches(patches: List>) { /** - * Returns true if at least one patches or its dependencies matches the given predicate. + * 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.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean = - predicate(this) || dependencies?.any { dependency -> - dependency.java.anyRecursively(predicate) + fun PatchClass.putDependenciesRecursively() { + if (context.allPatches.contains(this)) return + + val dependency = this.java.getInstance(logger)!! + context.allPatches[this] = dependency + + dependency.manifest.dependencies?.forEach { it.putDependenciesRecursively() } + } + + // Add all patches and their dependencies to the context. + for (patch in patches) context.executablePatches.putIfAbsent(patch::class, patch) ?: { + context.allPatches[patch::class] = patch + + patch.manifest.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) || manifest.dependencies?.any { dependency -> + context.allPatches[dependency]!!.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 - } - } + 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 - } + // Determine, if merging integrations is required. + for (patch in patches) + if (!patch.anyRecursively { it.manifest.requiresIntegrations }) { + context.bytecodeContext.integrations.merge = true + break + } } - - context.patches.addAll(patches) } /** @@ -93,50 +136,44 @@ class Patcher( * @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 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( - patchClass: PatchClass, - executedPatches: LinkedHashMap + patch: Patch<*>, + executedPatches: LinkedHashMap, PatchResult> ): PatchResult { - val patchName = patchClass.patchName + val patchName = patch.manifest.name - executedPatches[patchName]?.let { executedPatch -> - executedPatch.patchResult.exception ?: return executedPatch.patchResult + 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(patchName, PatchException("'$patchName' did not succeed previously")) + return PatchResult(patch, PatchException("'$patchName' did not succeed previously")) } // Recursively execute all dependency patches. - patchClass.dependencies?.forEach { dependencyClass -> - val dependency = dependencyClass.java - + patch.manifest.dependencies?.forEach { dependencyName -> + val dependency = context.executablePatches[dependencyName]!! val result = executePatch(dependency, executedPatches) result.exception?.let { return PatchResult( - patchName, - PatchException( - "'$patchName' depends on '${dependency.patchName}' that raised an exception: $it" - ) + patch, + PatchException("'$patchName' depends on '${dependency}' 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) + val patchContext = if (patch is BytecodePatch) { + patch.fingerprints.asList().resolveUsingLookupMap(context.bytecodeContext) context.bytecodeContext } else { @@ -144,14 +181,14 @@ class Patcher( } return try { - patchInstance.execute(patchContext) + patch.execute(patchContext) - PatchResult(patchName) + PatchResult(patch) } catch (exception: PatchException) { - PatchResult(patchName, exception) + PatchResult(patch, exception) } catch (exception: Exception) { - PatchResult(patchName, PatchException(exception)) - }.also { executedPatches[patchName] = ExecutedPatch(patchInstance, it) } + PatchResult(patch, PatchException(exception)) + }.also { executedPatches[patch] = it } } if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() @@ -164,51 +201,54 @@ class Patcher( logger.info("Executing patches") - val executedPatches = LinkedHashMap() // Key is name. + val executedPatches = LinkedHashMap, PatchResult>() // Key is name. - context.patches.forEach { patch -> - val result = executePatch(patch, executedPatches) + context.executablePatches.map { it.value }.sortedBy { it.manifest.name }.forEach { patch -> + val patchResult = 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 the patch failed, emit the result, even if it is closeable. + // Results of successfully 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 (executedPatches[result.patchName]!!.patchInstance is Closeable) return@run + if (patch is Closeable) return@run - emit(result) + emit(patchResult) } } executedPatches.values - .filter { it.patchResult.exception == null } - .filter { it.patchInstance is Closeable }.asReversed().forEach { executedPatch -> - val patchName = executedPatch.patchResult.patchName + .filter { it.exception == null } + .filter { it.patch is Closeable }.asReversed().forEach { executedPatch -> + val patch = executedPatch.patch val result = try { - (executedPatch.patchInstance as Closeable).close() + (patch as Closeable).close() - executedPatch.patchResult + executedPatch } catch (exception: PatchException) { - PatchResult(patchName, exception) + PatchResult(patch, exception) } catch (exception: Exception) { - PatchResult(patchName, PatchException(exception)) + PatchResult(patch, PatchException(exception)) } result.exception?.let { emit( PatchResult( - patchName, - PatchException("'$patchName' raised an exception while being closed: $it") + patch, + PatchException( + "'${patch.manifest.name}' raised an exception while being closed: $it", + result.exception + ) ) ) if (returnOnError) return@flow } ?: run { - executedPatch - .patchInstance::class + patch::class .java .findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) ?: return@run @@ -218,9 +258,7 @@ class Patcher( } } - override fun close() { - MethodFingerprint.clearFingerprintResolutionLookupMaps() - } + override fun close() = MethodFingerprint.clearFingerprintResolutionLookupMaps() /** * Compile and save the patched APK file. diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt index 4aa8396..c21cfa6 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ b/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/src/main/kotlin/app/revanced/patcher/PatcherException.kt b/src/main/kotlin/app/revanced/patcher/PatcherException.kt new file mode 100644 index 0000000..2313a99 --- /dev/null +++ b/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/PatchesConsumer.kt b/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt index ed7780c..866b7e5 100644 --- a/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt +++ b/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt @@ -1,8 +1,8 @@ package app.revanced.patcher -import app.revanced.patcher.patch.PatchClass +import app.revanced.patcher.patch.Patch @FunctionalInterface interface PatchesConsumer { - fun acceptPatches(patches: List) + fun acceptPatches(patches: List>) } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt b/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt deleted file mode 100644 index 19e61e8..0000000 --- a/src/main/kotlin/app/revanced/patcher/annotation/CompatibilityAnnotation.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.patcher.annotation - -import app.revanced.patcher.patch.Patch - -/** - * Annotation to constrain a [Patch] to compatible packages. - * @param compatiblePackages A list of packages a [Patch] is compatible with. - */ -@Target(AnnotationTarget.CLASS) -annotation class Compatibility( - val compatiblePackages: Array, -) - -/** - * Annotation to represent packages a patch can be compatible with. - * @param name The package identifier name. - * @param versions The versions of the package the [Patch] is compatible with. - */ -@Target() -annotation class Package( - val name: String, - val versions: Array = [], -) diff --git a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt b/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt deleted file mode 100644 index 9f9cd3a..0000000 --- a/src/main/kotlin/app/revanced/patcher/annotation/MetadataAnnotation.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patcher.annotation - -import app.revanced.patcher.patch.Patch - -/** - * Annotation to name a [Patch]. - * @param name A suggestive name for the [Patch]. - */ -@Target(AnnotationTarget.CLASS) -annotation class Name( - val name: String, -) - -/** - * Annotation to describe a [Patch]. - * @param description A description for the [Patch]. - */ -@Target(AnnotationTarget.CLASS) -annotation class Description( - val description: String, -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt b/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt index 3374930..439b430 100644 --- a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt +++ b/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/extensions/MethodFingerprintExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt index 1901044..e722f7d 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt +++ b/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/extensions/PatchExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt deleted file mode 100644 index 493056e..0000000 --- a/src/main/kotlin/app/revanced/patcher/extensions/PatchExtensions.kt +++ /dev/null @@ -1,64 +0,0 @@ -package app.revanced.patcher.extensions - -import app.revanced.patcher.annotation.Compatibility -import app.revanced.patcher.annotation.Description -import app.revanced.patcher.annotation.Name -import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively -import app.revanced.patcher.patch.OptionsContainer -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.patch.PatchClass -import app.revanced.patcher.patch.PatchOptions -import app.revanced.patcher.patch.annotations.DependsOn -import app.revanced.patcher.patch.annotations.RequiresIntegrations -import kotlin.reflect.KVisibility -import kotlin.reflect.full.companionObject -import kotlin.reflect.full.companionObjectInstance - -object PatchExtensions { - /** - * The name of a [Patch]. - */ - val PatchClass.patchName: String - get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName - - /** - * Weather or not a [Patch] should be included. - */ - val PatchClass.include - get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include - - /** - * The description of a [Patch]. - */ - val PatchClass.description - get() = findAnnotationRecursively(Description::class)?.description - - /** - * The dependencies of a [Patch]. - */ - val PatchClass.dependencies - get() = findAnnotationRecursively(DependsOn::class)?.dependencies - - /** - * The packages a [Patch] is compatible with. - */ - val PatchClass.compatiblePackages - get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages - - /** - * Weather or not a [Patch] requires integrations. - */ - internal val PatchClass.requiresIntegrations - get() = findAnnotationRecursively(RequiresIntegrations::class) != null - - /** - * The options of a [Patch]. - */ - val PatchClass.options: PatchOptions? - get() = kotlin.companionObject?.let { cl -> - if (cl.visibility != KVisibility.PUBLIC) return null - kotlin.companionObjectInstance?.let { - (it as? OptionsContainer)?.options - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt index 4858721..3682a02 100644 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/fingerprint/method/impl/MethodFingerprint.kt @@ -159,7 +159,7 @@ abstract class MethodFingerprint( * - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. */ - internal fun Iterable.resolveUsingLookupMap(context: BytecodeContext) { + internal fun List.resolveUsingLookupMap(context: BytecodeContext) { if (methods.isEmpty()) throw PatchException("lookup map not initialized") for (fingerprint in this) { diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 621145b..66b8b20 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -1,39 +1,97 @@ package app.revanced.patcher.patch +import app.revanced.patcher.PatchClass +import app.revanced.patcher.Patcher import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.Context import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import java.io.Closeable -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. + * If an implementation of [Patch] also implements [Closeable] + * it will be closed in reverse execution order of patches executed by ReVanced [Patcher]. + * + * @param manifest The manifest of the [Patch]. + * @param T The [Context] type this patch will work on. */ -sealed interface Patch> { +sealed class Patch>(val manifest: Manifest) { /** - * The main function of the [Patch] which the patcher will call. + * The execution function of the patch. * * @param context The [Context] the patch will work on. * @return The result of executing the patch. */ - fun execute(context: @UnsafeVariance T) + abstract fun execute(context: @UnsafeVariance T) + + override fun hashCode() = manifest.hashCode() + + override fun equals(other: Any?) = other is Patch<*> && manifest == other.manifest + + override fun toString() = manifest.name + + /** + * The manifest of a [Patch]. + * + * @param name The name of the patch. + * @param description The description of the patch. + * @param use Weather or not the patch should be used. + * @param dependencies The names of patches this patch depends on. + * @param compatiblePackages The packages the patch is compatible with. + * @param requiresIntegrations Weather or not the patch requires integrations. + * @param options The options of the patch. + */ + class Manifest( + val name: String, + val description: String, + val use: Boolean = true, + val dependencies: Set? = null, + val compatiblePackages: Set? = null, + // TODO: Remove this property, once integrations are coupled with patches. + val requiresIntegrations: Boolean = false, + val options: PatchOptions? = null, + ) { + override fun hashCode() = name.hashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Manifest + + return name == other.name + } + + /** + * A package a [Patch] is compatible with. + * + * @param name The name of the package. + * @param versions The versions of the package. + */ + class CompatiblePackage( + val name: String, + val versions: Set? = null, + ) + } } /** - * Resource patch for the Patcher. + * A ReVanced [Patch] that works on [ResourceContext]. + * + * @param metadata The manifest of the [ResourcePatch]. */ -interface ResourcePatch : Patch +abstract class ResourcePatch( + metadata: Manifest, +) : Patch(metadata) /** - * Bytecode patch for the Patcher. + * A ReVanced [Patch] that works on [BytecodeContext]. * - * @param fingerprints A list of [MethodFingerprint] this patch relies on. + * @param manifest The manifest of the [BytecodePatch]. + * @param fingerprints A list of [MethodFingerprint]s which will be resolved before the patch is executed. */ abstract class BytecodePatch( - internal val fingerprints: Iterable? = null -) : Patch \ No newline at end of file + manifest: Manifest, + internal vararg val fingerprints: MethodFingerprint, +) : Patch(manifest) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt index 01fab9c..5be69f5 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt @@ -3,8 +3,18 @@ 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) { + override fun hashCode() = patch.hashCode() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PatchResult + + return patch == other.patch + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotations/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/annotations/Patch.kt new file mode 100644 index 0000000..4928d87 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/annotations/Patch.kt @@ -0,0 +1,7 @@ +package app.revanced.patcher.patch.annotations + +/** + * Annotation to mark a class as a patch. + */ +@Target(AnnotationTarget.CLASS) +annotation class Patch \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt b/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt deleted file mode 100644 index 4e45c38..0000000 --- a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.revanced.patcher.patch.annotations - -import app.revanced.patcher.data.Context -import app.revanced.patcher.patch.Patch -import kotlin.reflect.KClass - -/** - * Annotation to mark a class as a patch. - * @param include If false, the patch should be treated as optional by default. - */ -@Target(AnnotationTarget.CLASS) -annotation class Patch(val include: Boolean = true) - -/** - * Annotation for dependencies of [Patch]es. - */ -@Target(AnnotationTarget.CLASS) -annotation class DependsOn( - val dependencies: Array>>> = [] -) - -// TODO: Remove this annotation, once integrations are coupled with patches. -/** - * Annotation to mark [Patch]es which depend on integrations. - */ -@Target(AnnotationTarget.CLASS) -annotation class RequiresIntegrations \ No newline at end of file