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 abstract class app/revanced/patcher/PatchBundleLoader : java/util/List, kotlin/jvm/internal/markers/KMutableList {
public synthetic fun <init> (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 <init> (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 <init> ([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 remove (I)Ljava/lang/Object;
}
public final class app/revanced/patcher/PatchBundleLoader$Jar : app/revanced/patcher/PatchBundleLoader {
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 {
@ -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 <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 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
@ -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 <init> ()V
public fun <init> (Ljava/lang/Iterable;)V
public synthetic fun <init> (Ljava/lang/Iterable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (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 <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 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 {
@ -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 <init> (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 {

View file

@ -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<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 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<Class<*>>
) : MutableList<PatchClass> by mutableListOf() {
classLoader: ClassLoader,
patchBundles: Array<out File>,
getBinaryClassNames: (patchBundle: File) -> List<String>,
) : 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<String, Patch<*>>).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)
}

View file

@ -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<PatchClass>) {
/**
* 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<Patch<*>>) {
/**
* 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<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
// 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<Context<*>>, 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<String, ExecutedPatch>
patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, 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<String, ExecutedPatch>() // Key is name.
val executedPatches = LinkedHashMap<Patch<*>, 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.

View file

@ -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<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].
@ -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)
}
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
import app.revanced.patcher.patch.PatchClass
import app.revanced.patcher.patch.Patch
@FunctionalInterface
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.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

View file

@ -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].
*/

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].
* - 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")
for (fingerprint in this) {

View file

@ -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<out Patch<Context<*>>>
/**
* 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<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.
* @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(
internal val fingerprints: Iterable<MethodFingerprint>? = null
) : Patch<BytecodeContext>
manifest: Manifest,
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].
*
* @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)
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