diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index cd33ee7..f6914e3 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,13 +1,12 @@ package app.revanced.patcher import app.revanced.patcher.data.Context -import app.revanced.patcher.data.findIndexed 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.extensions.nullOutputStream import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve import app.revanced.patcher.patch.* -import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.VersionReader import brut.androlib.Androlib import brut.androlib.meta.UsesFramework @@ -28,7 +27,7 @@ import org.jf.dexlib2.writer.io.MemoryDataStore import java.io.File import java.nio.file.Files -private val NAMER = BasicDexFileNamer() +internal val NAMER = BasicDexFileNamer() /** * The ReVanced Patcher. @@ -38,6 +37,7 @@ class Patcher(private val options: PatcherOptions) { private val logger = options.logger private val opcodes: Opcodes private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY + private var mergeIntegrations = false val context: PatcherContext companion object { @@ -64,37 +64,19 @@ class Patcher(private val options: PatcherOptions) { } /** - * Add additional dex file container to the patcher. - * @param files The dex file containers to add to the patcher. - * @param process The callback for [files] which are being added. + * Add integrations to be merged by the patcher. + * The integrations will only be merged, if necessary. + * + * @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files. + * @param callback The callback for [integrations] which are being added. */ - fun addFiles( - files: List, - process: (File) -> Unit + fun addIntegrations( + integrations: List, + callback: (File) -> Unit ) { - with(context.bytecodeContext.classes) { - for (file in files) { - process(file) - for (classDef in MultiDexIO.readDexFile(true, file, NAMER, null, null).classes) { - val type = classDef.type - - val result = classes.findIndexed { it.type == type } - if (result == null) { - logger.trace("Merging type $type") - classes.add(classDef) - continue - } - - val (existingClass, existingClassIndex) = result - - logger.trace("Type $type exists. Adding missing methods and fields.") - - existingClass.merge(classDef, context, logger).let { mergedClass -> - if (mergedClass !== existingClass) // referential equality check - classes[existingClassIndex] = mergedClass - } - } - } + context.integrations.apply integrations@{ + add(integrations) + this@integrations.callback = callback } } @@ -183,18 +165,29 @@ class Patcher(private val options: PatcherOptions) { */ fun addPatches(patches: Iterable>>) { /** - * Fill the cache with the instances of the [Patch]es for later use. - * Note: Dependencies of the [Patch] will be cached as well. + * Returns true if at least one patches or its dependencies matches the given predicate. */ - fun Class>.isResource() { - this.also { - if (!ResourcePatch::class.java.isAssignableFrom(it)) return@also - // set the mode to decode all resources before running the patches + fun Class>.anyRecursively(predicate: (Class>) -> Boolean): Boolean = + predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true + + + // Determine if resource patching is required. + for (patch in patches) { + if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) { resourceDecodingMode = ResourceDecodingMode.FULL - }.dependencies?.forEach { it.java.isResource() } + break + } } - context.patches.addAll(patches.onEach(Class>::isResource)) + // Determine if merging integrations is required. + for (patch in patches) { + if (patch.anyRecursively { it.requiresIntegrations }) { + mergeIntegrations = true + break + } + } + + context.patches.addAll(patches) } /** @@ -343,6 +336,8 @@ class Patcher(private val options: PatcherOptions) { } return sequence { + if (mergeIntegrations) context.integrations.merge(logger) + // prevent from decoding the manifest twice if it is not needed if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL) diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt index 9589517..941607d 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt @@ -1,10 +1,9 @@ package app.revanced.patcher -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.data.Context -import app.revanced.patcher.data.PackageMetadata -import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.data.* +import app.revanced.patcher.logging.Logger import app.revanced.patcher.patch.Patch +import app.revanced.patcher.util.ClassMerger.merge import org.jf.dexlib2.iface.ClassDef import java.io.File @@ -14,6 +13,52 @@ data class PatcherContext( ) { val packageMetadata = PackageMetadata() internal val patches = mutableListOf>>() + internal val integrations = Integrations(this) internal val bytecodeContext = BytecodeContext(classes) internal val resourceContext = ResourceContext(resourceCacheDirectory) + + internal class Integrations(val context: PatcherContext) { + var callback: ((File) -> Unit)? = null + private val integrations: MutableList = mutableListOf() + + fun add(integrations: List) = this@Integrations.integrations.addAll(integrations) + + /** + * Merge integrations. + * @param logger A logger. + */ + fun merge(logger: Logger) { + with(context.bytecodeContext.classes) { + for (integrations in integrations) { + callback?.let { it(integrations) } + + for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile( + true, + integrations, + NAMER, + null, + null + ).classes) { + val type = classDef.type + + val result = classes.findIndexed { it.type == type } + if (result == null) { + logger.trace("Merging type $type") + classes.add(classDef) + continue + } + + val (existingClass, existingClassIndex) = result + + logger.trace("Type $type exists. Adding missing methods and fields.") + + existingClass.merge(classDef, context, logger).let { mergedClass -> + if (mergedClass !== existingClass) // referential equality check + classes[existingClassIndex] = mergedClass + } + } + } + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt index cdc7358..21836e5 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt @@ -8,6 +8,7 @@ import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.PatchOptions import app.revanced.patcher.patch.annotations.DependsOn +import app.revanced.patcher.patch.annotations.RequiresIntegrations import kotlin.reflect.KClass import kotlin.reflect.KVisibility import kotlin.reflect.full.companionObject @@ -58,6 +59,9 @@ object PatchExtensions { val Class>.compatiblePackages get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages + internal val Class>.requiresIntegrations + get() = findAnnotationRecursively(RequiresIntegrations::class) != null + val Class>.options: PatchOptions? get() = kotlin.companionObject?.let { cl -> if (cl.visibility != KVisibility.PUBLIC) return null diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt b/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt index 317f78f..0ab81be 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/annotations/PatchAnnotation.kt @@ -5,7 +5,7 @@ import app.revanced.patcher.patch.Patch import kotlin.reflect.KClass /** - * Annotation to mark a Class as a patch. + * Annotation to mark a class as a patch. * @param include If false, the patch should be treated as optional by default. */ @Target(AnnotationTarget.CLASS) @@ -17,4 +17,11 @@ annotation class Patch(val include: Boolean = true) @Target(AnnotationTarget.CLASS) annotation class DependsOn( val dependencies: Array>> = [] -) \ No newline at end of file +) + + +/** + * Annotation to mark [Patch]es which depend on integrations. + */ +@Target(AnnotationTarget.CLASS) +annotation class RequiresIntegrations // required because integrations are decoupled from patches \ No newline at end of file