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.
This commit is contained in:
oSumAtrIX 2023-09-01 03:37:52 +02:00
parent c4a7117ee8
commit 3b4db3ddb7
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
16 changed files with 383 additions and 349 deletions

View File

@ -7,54 +7,56 @@ public final class app/revanced/patcher/PackageMetadata {
public final fun getPackageVersion ()Ljava/lang/String; public final fun getPackageVersion ()Ljava/lang/String;
} }
public abstract class app/revanced/patcher/PatchBundleLoader : java/util/List, kotlin/jvm/internal/markers/KMutableList { public abstract class app/revanced/patcher/PatchBundleLoader : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker {
public synthetic fun <init> (Ljava/lang/Iterable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/ClassLoader;[Ljava/io/File;Lkotlin/jvm/functions/Function1;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 fun clear ()V public fun clear ()V
public fun contains (Ljava/lang/Class;)Z public synthetic fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public final fun contains (Ljava/lang/Object;)Z public fun compute (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch;
public fun containsAll (Ljava/util/Collection;)Z public synthetic fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;
public fun get (I)Ljava/lang/Class; public fun computeIfAbsent (Ljava/lang/String;Ljava/util/function/Function;)Lapp/revanced/patcher/patch/Patch;
public synthetic fun get (I)Ljava/lang/Object; 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 getSize ()I
public fun indexOf (Ljava/lang/Class;)I public fun getValues ()Ljava/util/Collection;
public final fun indexOf (Ljava/lang/Object;)I
public fun isEmpty ()Z public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator; public final fun keySet ()Ljava/util/Set;
public fun lastIndexOf (Ljava/lang/Class;)I public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public final fun lastIndexOf (Ljava/lang/Object;)I public fun merge (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Patch;
public fun listIterator ()Ljava/util/ListIterator; public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun listIterator (I)Ljava/util/ListIterator; public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch;
public final fun remove (I)Ljava/lang/Class; public fun putAll (Ljava/util/Map;)V
public synthetic fun remove (I)Ljava/lang/Object; public synthetic fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Class;)Z public fun putIfAbsent (Ljava/lang/String;Lapp/revanced/patcher/patch/Patch;)Lapp/revanced/patcher/patch/Patch;
public final fun remove (Ljava/lang/Object;)Z public fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Patch;
public fun removeAll (Ljava/util/Collection;)Z public synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object;
public fun removeAt (I)Ljava/lang/Class; public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z
public fun retainAll (Ljava/util/Collection;)Z public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun set (ILjava/lang/Class;)Ljava/lang/Class; public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z
public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; 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 final fun size ()I
public fun subList (II)Ljava/util/List; public final fun values ()Ljava/util/Collection;
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
} }
public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/patcher/PatchBundleLoader { public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/patcher/PatchBundleLoader {
public fun <init> ([Ljava/io/File;)V public fun <init> ([Ljava/io/File;)V
public fun <init> ([Ljava/io/File;Ljava/io/File;)V public fun <init> ([Ljava/io/File;Ljava/io/File;)V
public synthetic fun <init> ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> ([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 final class app/revanced/patcher/PatchBundleLoader$Jar : app/revanced/patcher/PatchBundleLoader {
public fun <init> ([Ljava/io/File;)V public fun <init> ([Ljava/io/File;)V
public synthetic fun remove (I)Ljava/lang/Object;
} }
public abstract interface class app/revanced/patcher/PatchExecutorFunction : java/util/function/Function { 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 final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata;
} }
public abstract class app/revanced/patcher/PatcherException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (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 final class app/revanced/patcher/PatcherOptions {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (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 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 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 (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun findClass (Lkotlin/jvm/functions/Function1;)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 final class app/revanced/patcher/extensions/MethodFingerprintExtensions {
public static final field INSTANCE Lapp/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 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 { 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 abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public fun <init> ()V public fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;[Lapp/revanced/patcher/fingerprint/method/impl/MethodFingerprint;)V
public fun <init> (Ljava/lang/Iterable;)V
public synthetic fun <init> (Ljava/lang/Iterable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
} }
public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception { public final class app/revanced/patcher/patch/IllegalValueException : java/lang/Exception {
@ -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; 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 <init> (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 abstract fun execute (Lapp/revanced/patcher/data/Context;)V
public final fun getManifest ()Lapp/revanced/patcher/patch/Patch$Manifest;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/Patch$Manifest {
public fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;ZLapp/revanced/patcher/patch/PatchOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getCompatiblePackages ()Ljava/util/Set;
public final fun 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 <init> (Ljava/lang/String;Ljava/util/Set;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getName ()Ljava/lang/String;
public final fun getVersions ()Ljava/util/Set;
} }
public final class app/revanced/patcher/patch/PatchException : java/lang/Exception { public final class app/revanced/patcher/patch/PatchException : java/lang/Exception {
@ -429,26 +435,21 @@ public final class app/revanced/patcher/patch/PatchOptions : java/lang/Iterable,
} }
public final class app/revanced/patcher/patch/PatchResult { 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 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 final class app/revanced/patcher/patch/RequirementNotMetException : java/lang/Exception {
public static final field INSTANCE Lapp/revanced/patcher/patch/RequirementNotMetException; 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 class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
} public fun <init> (Lapp/revanced/patcher/patch/Patch$Manifest;)V
public abstract interface annotation class app/revanced/patcher/patch/annotations/DependsOn : java/lang/annotation/Annotation {
public abstract fun dependencies ()[Ljava/lang/Class;
} }
public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation { public abstract 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 { public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable {

View File

@ -3,36 +3,85 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import dalvik.system.DexClassLoader import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO import lanchon.multidexlib2.MultiDexIO
import java.io.File import java.io.File
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.jar.JarFile import java.util.jar.JarFile
import java.util.logging.Logger
import kotlin.reflect.KClass
/** /**
* A patch bundle. * [Patch]es mapped by their name.
*/
typealias PatchMap = Map<String, Patch<*>>
/**
* A [Patch] class.
*/
typealias PatchClass = KClass<out Patch<*>>
/**
* A loader of [Patch]es from patch bundles.
* This will load all [Patch]es from the given patch bundles.
* *
* * @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
* @param fromClasses The classes to get [Patch]es from. * @param classLoader The [ClassLoader] to use for loading the classes.
*/ */
sealed class PatchBundleLoader private constructor( sealed class PatchBundleLoader private constructor(
fromClasses: Iterable<Class<*>> classLoader: ClassLoader,
) : MutableList<PatchClass> by mutableListOf() { patchBundles: Array<out File>,
getBinaryClassNames: (patchBundle: File) -> List<String>,
) : PatchMap by mutableMapOf() {
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
init { init {
fromClasses.filter { patchBundles.flatMap(getBinaryClassNames).map {
classLoader.loadClass(it)
}.filter {
if (it.isAnnotation) return@filter false if (it.isAnnotation) return@filter false
it.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) != null 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") @Suppress("UNCHECKED_CAST")
it as PatchClass (this as MutableMap<String, Patch<*>>).putAll(patches)
}.sortedBy { }
it.patchName }
}.let { addAll(it) }
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. * @param patchBundles The path to patch bundles of JAR format.
*/ */
class Jar(vararg patchBundles: File) : PatchBundleLoader( class Jar(vararg patchBundles: File) : PatchBundleLoader(
with( URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()),
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()) patchBundles,
) { { patchBundle ->
patchBundles.flatMap { patchBundle -> JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
// Get the names of all classes in the DEX file.
JarFile(patchBundle).entries().asSequence()
.filter { it.name.endsWith(".class") }
.map { it.name.replace('/', '.').replace(".class", "") } .map { it.name.replace('/', '.').replace(".class", "") }
.map { loadClass(it) }
} }
}) )
/** /**
* A [PatchBundleLoader] for [Dex] files. * 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. * This parameter is deprecated and has no effect since API level 26.
*/ */
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader( class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
with(
DexClassLoader( DexClassLoader(
patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath, patchBundles.joinToString(File.pathSeparator) { it.absolutePath }, optimizedDexDirectory?.absolutePath,
null, null,
PatchBundleLoader::class.java.classLoader PatchBundleLoader::class.java.classLoader
) ),
) { patchBundles,
patchBundles { patchBundle ->
.flatMap { MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
MultiDexIO.readDexFile(true, it, BasicDexFileNamer(), null, null).classes .map { classDef ->
classDef.type.substring(1, classDef.length - 1)
} }
.map { classDef -> classDef.type.substring(1, classDef.length - 1) } }
.map { loadClass(it) } ) {
}) {
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.") @Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null) constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)
} }

View File

@ -1,11 +1,8 @@
package app.revanced.patcher 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.data.ResourceContext
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively 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
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.*
@ -49,32 +46,78 @@ class Patcher(
context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY) context.resourceContext.decodeResources(ResourceContext.ResourceDecodingMode.MANIFEST_ONLY)
} }
override fun acceptPatches(patches: List<PatchClass>) {
/** /**
* Returns true if at least one patches or its dependencies matches the given predicate. * 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.
*/ */
fun PatchClass.anyRecursively(predicate: (PatchClass) -> Boolean): Boolean = @Suppress("NAME_SHADOWING")
predicate(this) || dependencies?.any { dependency -> override fun acceptPatches(patches: List<Patch<*>>) {
dependency.java.anyRecursively(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.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<PatchClass, MutableList<PatchClass>>()
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 } ?: false
// Determine if resource patching is required. context.allPatches.values.let { patches ->
for (patch in patches) { // Determine, if resource patching is required.
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) { for (patch in patches)
if (patch.anyRecursively { patch is ResourcePatch }) {
options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL options.resourceDecodingMode = ResourceContext.ResourceDecodingMode.FULL
break break
} }
}
// Determine if merging integrations is required. // Determine, if merging integrations is required.
for (patch in patches) { for (patch in patches)
if (patch.anyRecursively { it.requiresIntegrations }) { if (!patch.anyRecursively { it.manifest.requiresIntegrations }) {
context.bytecodeContext.integrations.merge = true context.bytecodeContext.integrations.merge = true
break break
} }
} }
context.patches.addAll(patches)
} }
/** /**
@ -93,50 +136,44 @@ class Patcher(
* @return A pair of the name of the [Patch] and its [PatchResult]. * @return A pair of the name of the [Patch] and its [PatchResult].
*/ */
override fun apply(returnOnError: Boolean) = flow { override fun apply(returnOnError: Boolean) = flow {
class ExecutedPatch(val patchInstance: Patch<Context<*>>, val patchResult: PatchResult)
/** /**
* Execute a [Patch] and its dependencies recursively. * 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. * @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
* @return The result of executing the [Patch]. * @return The result of executing the [Patch].
*/ */
fun executePatch( fun executePatch(
patchClass: PatchClass, patch: Patch<*>,
executedPatches: LinkedHashMap<String, ExecutedPatch> executedPatches: LinkedHashMap<Patch<*>, PatchResult>
): PatchResult { ): PatchResult {
val patchName = patchClass.patchName val patchName = patch.manifest.name
executedPatches[patchName]?.let { executedPatch -> executedPatches[patch]?.let { patchResult ->
executedPatch.patchResult.exception ?: return executedPatch.patchResult patchResult.exception ?: return patchResult
// Return a new result with an exception indicating that the patch was not executed previously, // 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. // 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. // Recursively execute all dependency patches.
patchClass.dependencies?.forEach { dependencyClass -> patch.manifest.dependencies?.forEach { dependencyName ->
val dependency = dependencyClass.java val dependency = context.executablePatches[dependencyName]!!
val result = executePatch(dependency, executedPatches) val result = executePatch(dependency, executedPatches)
result.exception?.let { result.exception?.let {
return PatchResult( return PatchResult(
patchName, patch,
PatchException( PatchException("'$patchName' depends on '${dependency}' that raised an exception: $it")
"'$patchName' depends on '${dependency.patchName}' that raised an exception: $it"
)
) )
} }
} }
// TODO: Implement this in a more polymorphic way. // TODO: Implement this in a more polymorphic way.
val patchInstance = patchClass.getDeclaredConstructor().newInstance() val patchContext = if (patch is BytecodePatch) {
patch.fingerprints.asList().resolveUsingLookupMap(context.bytecodeContext)
val patchContext = if (patchInstance is BytecodePatch) {
patchInstance.fingerprints?.resolveUsingLookupMap(context.bytecodeContext)
context.bytecodeContext context.bytecodeContext
} else { } else {
@ -144,14 +181,14 @@ class Patcher(
} }
return try { return try {
patchInstance.execute(patchContext) patch.execute(patchContext)
PatchResult(patchName) PatchResult(patch)
} catch (exception: PatchException) { } catch (exception: PatchException) {
PatchResult(patchName, exception) PatchResult(patch, exception)
} catch (exception: Exception) { } catch (exception: Exception) {
PatchResult(patchName, PatchException(exception)) PatchResult(patch, PatchException(exception))
}.also { executedPatches[patchName] = ExecutedPatch(patchInstance, it) } }.also { executedPatches[patch] = it }
} }
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
@ -164,51 +201,54 @@ class Patcher(
logger.info("Executing patches") logger.info("Executing patches")
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // Key is name. val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.patches.forEach { patch -> context.executablePatches.map { it.value }.sortedBy { it.manifest.name }.forEach { patch ->
val result = executePatch(patch, executedPatches) val patchResult = executePatch(patch, executedPatches)
// If the patch failed, or if the patch is not closeable, emit the result. // If the patch failed, emit the result, even if it is closeable.
// Results of patches that are closeable will be emitted later. // Results of successfully executed patches that are closeable will be emitted later.
result.exception?.let { patchResult.exception?.let {
emit(result) // Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow if (returnOnError) return@flow
} ?: run { } ?: run {
if (executedPatches[result.patchName]!!.patchInstance is Closeable) return@run if (patch is Closeable) return@run
emit(result) emit(patchResult)
} }
} }
executedPatches.values executedPatches.values
.filter { it.patchResult.exception == null } .filter { it.exception == null }
.filter { it.patchInstance is Closeable }.asReversed().forEach { executedPatch -> .filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patchName = executedPatch.patchResult.patchName val patch = executedPatch.patch
val result = try { val result = try {
(executedPatch.patchInstance as Closeable).close() (patch as Closeable).close()
executedPatch.patchResult executedPatch
} catch (exception: PatchException) { } catch (exception: PatchException) {
PatchResult(patchName, exception) PatchResult(patch, exception)
} catch (exception: Exception) { } catch (exception: Exception) {
PatchResult(patchName, PatchException(exception)) PatchResult(patch, PatchException(exception))
} }
result.exception?.let { result.exception?.let {
emit( emit(
PatchResult( PatchResult(
patchName, patch,
PatchException("'$patchName' raised an exception while being closed: $it") PatchException(
"'${patch.manifest.name}' raised an exception while being closed: $it",
result.exception
)
) )
) )
if (returnOnError) return@flow if (returnOnError) return@flow
} ?: run { } ?: run {
executedPatch patch::class
.patchInstance::class
.java .java
.findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class) .findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)
?: return@run ?: return@run
@ -218,9 +258,7 @@ class Patcher(
} }
} }
override fun close() { override fun close() = MethodFingerprint.clearFingerprintResolutionLookupMaps()
MethodFingerprint.clearFingerprintResolutionLookupMaps()
}
/** /**
* Compile and save the patched APK file. * Compile and save the patched APK file.

View File

@ -3,7 +3,6 @@ package app.revanced.patcher
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchClass
import brut.androlib.apk.ApkInfo import brut.androlib.apk.ApkInfo
import brut.directory.ExtFile import brut.directory.ExtFile
@ -19,9 +18,14 @@ class PatcherContext internal constructor(options: PatcherOptions) {
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile))) val packageMetadata = PackageMetadata(ApkInfo(ExtFile(options.inputFile)))
/** /**
* The list of [Patch]es to execute. * The map of [Patch]es associated by their [PatchClass].
*/ */
internal val patches = mutableListOf<PatchClass>() internal val executablePatches = mutableMapOf<PatchClass, Patch<*>>()
/**
* The map of all [Patch]es and their dependencies associated by their [PatchClass].
*/
internal val allPatches = mutableMapOf<PatchClass, Patch<*>>()
/** /**
* The [ResourceContext] of this [PatcherContext]. * The [ResourceContext] of this [PatcherContext].
@ -33,5 +37,4 @@ class PatcherContext internal constructor(options: PatcherOptions) {
* The [BytecodeContext] of this [PatcherContext]. * The [BytecodeContext] of this [PatcherContext].
* This holds the current state of the bytecode. * This holds the current state of the bytecode.
*/ */
internal val bytecodeContext = BytecodeContext(options) internal val bytecodeContext = BytecodeContext(options) }
}

View File

@ -0,0 +1,16 @@
package app.revanced.patcher
/**
* An exception thrown by ReVanced [Patcher].
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
class CircularDependencyException internal constructor(dependant: String) : PatcherException(
"Patch '$dependant' causes a circular dependency"
)
}

View File

@ -1,8 +1,8 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.patch.PatchClass import app.revanced.patcher.patch.Patch
@FunctionalInterface @FunctionalInterface
interface PatchesConsumer { interface PatchesConsumer {
fun acceptPatches(patches: List<PatchClass>) fun acceptPatches(patches: List<Patch<*>>)
} }

View File

@ -1,23 +0,0 @@
package app.revanced.patcher.annotation
import app.revanced.patcher.patch.Patch
/**
* Annotation to constrain a [Patch] to compatible packages.
* @param compatiblePackages A list of packages a [Patch] is compatible with.
*/
@Target(AnnotationTarget.CLASS)
annotation class Compatibility(
val compatiblePackages: Array<Package>,
)
/**
* Annotation to represent packages a patch can be compatible with.
* @param name The package identifier name.
* @param versions The versions of the package the [Patch] is compatible with.
*/
@Target()
annotation class Package(
val name: String,
val versions: Array<String> = [],
)

View File

@ -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,
)

View File

@ -4,7 +4,6 @@ import app.revanced.patcher.PatcherContext
import app.revanced.patcher.PatcherOptions import app.revanced.patcher.PatcherOptions
import app.revanced.patcher.PatcherResult import app.revanced.patcher.PatcherResult
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.annotations.RequiresIntegrations
import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.ProxyClassList import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.method.MethodWalker import app.revanced.patcher.util.method.MethodWalker

View File

@ -5,13 +5,7 @@ import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
object MethodFingerprintExtensions { object MethodFingerprintExtensions {
// TODO: Make this a property.
/**
* The name of a [MethodFingerprint].
*/
val MethodFingerprint.name: String
get() = this.javaClass.simpleName
/** /**
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint]. * The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
*/ */

View File

@ -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
}
}
}

View File

@ -159,7 +159,7 @@ abstract class MethodFingerprint(
* - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*/ */
internal fun Iterable<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) { internal fun List<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
if (methods.isEmpty()) throw PatchException("lookup map not initialized") if (methods.isEmpty()) throw PatchException("lookup map not initialized")
for (fingerprint in this) { for (fingerprint in this) {

View File

@ -1,39 +1,97 @@
package app.revanced.patcher.patch 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.BytecodeContext
import app.revanced.patcher.data.Context import app.revanced.patcher.data.Context
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import java.io.Closeable import java.io.Closeable
typealias PatchClass = Class<out Patch<Context<*>>>
/** /**
* A ReVanced patch. * A ReVanced patch.
* *
* If it implements [Closeable], it will be closed after all patches have been executed. * If an implementation of [Patch] also implements [Closeable]
* Closing will be done in reverse execution order. * 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<out T : Context<*>> { sealed class Patch<out T : Context<*>>(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. * @param context The [Context] the patch will work on.
* @return The result of executing the patch. * @return The result of executing the patch.
*/ */
fun execute(context: @UnsafeVariance T) abstract fun execute(context: @UnsafeVariance T)
override fun hashCode() = 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<PatchClass>? = null,
val compatiblePackages: Set<CompatiblePackage>? = null,
// TODO: Remove this property, once integrations are coupled with patches.
val requiresIntegrations: Boolean = false,
val options: PatchOptions? = null,
) {
override fun hashCode() = name.hashCode()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Manifest
return name == other.name
}
/**
* A package a [Patch] is compatible with.
*
* @param name The name of the package.
* @param versions The versions of the package.
*/
class CompatiblePackage(
val name: String,
val versions: Set<String>? = null,
)
}
} }
/** /**
* Resource patch for the Patcher. * A ReVanced [Patch] that works on [ResourceContext].
*
* @param metadata The manifest of the [ResourcePatch].
*/ */
interface ResourcePatch : Patch<ResourceContext> abstract class ResourcePatch(
metadata: Manifest,
) : Patch<ResourceContext>(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( abstract class BytecodePatch(
internal val fingerprints: Iterable<MethodFingerprint>? = null manifest: Manifest,
) : Patch<BytecodeContext> internal vararg val fingerprints: MethodFingerprint,
) : Patch<BytecodeContext>(manifest)

View File

@ -3,8 +3,18 @@ package app.revanced.patcher.patch
/** /**
* A result of executing a [Patch]. * A result of executing a [Patch].
* *
* @param patchName The name of the [Patch]. * @param patch The [Patch] that was executed.
* @param exception The [PatchException] thrown, if any. * @param exception The [PatchException] thrown, if any.
*/ */
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
class PatchResult internal constructor(val patchName: String, val exception: PatchException? = null) 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
}
}

View File

@ -0,0 +1,7 @@
package app.revanced.patcher.patch.annotations
/**
* Annotation to mark a class as a patch.
*/
@Target(AnnotationTarget.CLASS)
annotation class Patch

View File

@ -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<KClass<out Patch<Context<*>>>> = []
)
// 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