diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index eeaa556..cd33ee7 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -7,12 +7,8 @@ import app.revanced.patcher.extensions.PatchExtensions.patchName 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.TypeUtil.traverseClassHierarchy +import app.revanced.patcher.util.ClassMerger.merge import app.revanced.patcher.util.VersionReader -import app.revanced.patcher.util.proxy.mutableTypes.MutableClass -import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable -import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import brut.androlib.Androlib import brut.androlib.meta.UsesFramework import brut.androlib.options.BuildOptions @@ -26,11 +22,8 @@ import brut.directory.ExtFile import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.MultiDexIO -import org.jf.dexlib2.AccessFlags import org.jf.dexlib2.Opcodes -import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.DexFile -import org.jf.dexlib2.util.MethodUtil import org.jf.dexlib2.writer.io.MemoryDataStore import java.io.File import java.nio.file.Files @@ -89,100 +82,16 @@ class Patcher(private val options: PatcherOptions) { if (result == null) { logger.trace("Merging type $type") classes.add(classDef) - } else { - val (existingClass, existingClassIndex) = result + continue + } - logger.trace("Type $type exists. Adding missing methods and fields.") + val (existingClass, existingClassIndex) = result - /** - * Add missing fields and methods from [from]. - * - * @param from The class to add methods and fields from. - */ - fun ClassDef.addMissingFrom(from: ClassDef) { - var changed = false - fun ClassDef.transformClass(transform: (MutableClass) -> T): T { - fun toMutableClass() = - if (this@transformClass is MutableClass) this else this.toMutable() - return transform(toMutableClass()) - } + logger.trace("Type $type exists. Adding missing methods and fields.") - /** - * Check if the [AccessFlags.PUBLIC] flag is set. - * - * @return True, if the flag is set. - */ - fun Int.isPublic() = AccessFlags.PUBLIC.isSet(this) - - /** - * Make a class and its super class public recursively. - */ - fun MutableClass.publicize() { - context.bytecodeContext.traverseClassHierarchy(this) { - if (accessFlags.isPublic()) return@traverseClassHierarchy - - accessFlags = accessFlags.or(AccessFlags.PUBLIC.value) - } - } - - /** - * Add missing methods to the class, considering to publicise the [ClassDef] if necessary. - */ - fun ClassDef.addMissingMethods(): ClassDef { - fun getMissingMethods() = from.methods.filterNot { - this@addMissingMethods.methods.any { original -> - MethodUtil.methodSignaturesMatch(original, it) - } - } - - return getMissingMethods() - .apply { - if (isEmpty()) return@addMissingMethods this@addMissingMethods else changed = - true - } - .map { it.toMutable() } - .let { missingMethods -> - this@addMissingMethods.transformClass { classDef -> - classDef.apply { - // make sure the class is public, if the class contains public methods - if (missingMethods.any { it.accessFlags.isPublic() }) - classDef.publicize() - - methods.addAll(missingMethods) - } - } - } - } - - /** - * Add missing fields to the class, considering to publicise the [ClassDef] if necessary. - */ - fun ClassDef.addMissingFields(): ClassDef { - fun getMissingFields() = from.fields.filterNot { - this@addMissingFields.fields.any { original -> original.name == it.name } - } - - return getMissingFields() - .apply { - if (isEmpty()) return@addMissingFields this@addMissingFields else changed = true - } - .map { it.toMutable() } - .let { missingFields -> - this@addMissingFields.transformClass { classDef -> - // make sure the class is public, if the class contains public fields - if (missingFields.any { it.accessFlags.isPublic() }) - classDef.publicize() - - classDef.apply { fields.addAll(missingFields) } - } - } - } - - classes[existingClassIndex] = addMissingMethods().addMissingFields() - .apply { if (!changed) return } - } - - existingClass.addMissingFrom(classDef) + existingClass.merge(classDef, context, logger).let { mergedClass -> + if (mergedClass !== existingClass) // referential equality check + classes[existingClassIndex] = mergedClass } } } diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt new file mode 100644 index 0000000..abac334 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt @@ -0,0 +1,214 @@ +package app.revanced.patcher.util + +import app.revanced.patcher.PatcherContext +import app.revanced.patcher.extensions.or +import app.revanced.patcher.logging.Logger +import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass +import app.revanced.patcher.util.ClassMerger.Utils.filterAny +import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny +import app.revanced.patcher.util.ClassMerger.Utils.isPublic +import app.revanced.patcher.util.ClassMerger.Utils.toPublic +import app.revanced.patcher.util.TypeUtil.traverseClassHierarchy +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable +import app.revanced.patcher.util.proxy.mutableTypes.MutableField +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import org.jf.dexlib2.AccessFlags +import org.jf.dexlib2.iface.ClassDef +import org.jf.dexlib2.util.MethodUtil +import kotlin.reflect.KFunction2 + +/** + * Experimental class to merge a [ClassDef] with another. + * Note: This will not consider method implementations or if the class is missing a superclass or interfaces. + */ +internal object ClassMerger { + /** + * Merge a class with [otherClass]. + * + * @param otherClass The class to merge with + * @param context The context to traverse the class hierarchy in. + * @param logger A logger. + */ + fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this + //.fixFieldAccess(otherClass, logger) + //.fixMethodAccess(otherClass, logger) + .addMissingFields(otherClass, logger) + .addMissingMethods(otherClass, logger) + .publicize(otherClass, context, logger) + + /** + * Add methods which are missing but existing in [fromClass]. + * + * @param fromClass The class to add missing methods from. + * @param logger A logger. + */ + private fun ClassDef.addMissingMethods(fromClass: ClassDef, logger: Logger? = null): ClassDef { + val missingMethods = fromClass.methods.let { fromMethods -> + methods.filterNot { method -> + fromMethods.any { fromMethod -> + MethodUtil.methodSignaturesMatch(fromMethod, method) + } + } + } + + if (missingMethods.isEmpty()) return this + + logger?.trace("Found ${missingMethods.size} missing methods") + + return asMutableClass().apply { + methods.addAll(missingMethods.map { it.toMutable() }) + } + } + + /** + * Add fields which are missing but existing in [fromClass]. + * + * @param fromClass The class to add missing fields from. + * @param logger A logger. + */ + private fun ClassDef.addMissingFields(fromClass: ClassDef, logger: Logger? = null): ClassDef { + val missingFields = fields.filterNotAny(fromClass.fields) { field, fromField -> + fromField.name == field.name + } + + if (missingFields.isEmpty()) return this + + logger?.trace("Found ${missingFields.size} missing fields") + + return asMutableClass().apply { + fields.addAll(missingFields.map { it.toMutable() }) + } + } + + /** + * Make a class and its super class public recursively. + * @param reference The class to check the [AccessFlags] of. + * @param context The context to traverse the class hierarchy in. + * @param logger A logger. + */ + private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) = + if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) + this.asMutableClass().apply { + context.bytecodeContext.traverseClassHierarchy(this) { + if (accessFlags.isPublic()) return@traverseClassHierarchy + + logger?.trace("Publicizing ${this.type}") + + accessFlags = accessFlags.toPublic() + } + } + else this + + /** + * Publicize fields if they are public in [reference]. + * + * @param reference The class to check the [AccessFlags] of the fields in. + * @param logger A logger. + */ + private fun ClassDef.fixFieldAccess(reference: ClassDef, logger: Logger? = null): ClassDef { + val brokenFields = fields.filterAny(reference.fields) { field, referenceField -> + if (field.name != referenceField.name) return@filterAny false + + referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic() + } + + if (brokenFields.isEmpty()) return this + + logger?.trace("Found ${brokenFields.size} broken fields") + + /** + * Make a field public. + */ + fun MutableField.publicize() { + accessFlags = accessFlags.toPublic() + } + + return asMutableClass().apply { + fields.filter { brokenFields.contains(it) }.forEach(MutableField::publicize) + } + } + + /** + * Publicize methods if they are public in [reference]. + * + * @param reference The class to check the [AccessFlags] of the methods in. + * @param logger A logger. + */ + private fun ClassDef.fixMethodAccess(reference: ClassDef, logger: Logger? = null): ClassDef { + val brokenMethods = methods.filterAny(reference.methods) { method, referenceMethod -> + if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false + + referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic() + } + + if (brokenMethods.isEmpty()) return this + + logger?.trace("Found ${brokenMethods.size} methods") + + /** + * Make a method public. + */ + fun MutableMethod.publicize() { + accessFlags = accessFlags.toPublic() + } + + return asMutableClass().apply { + methods.filter { brokenMethods.contains(it) }.forEach(MutableMethod::publicize) + } + } + + private object Utils { + fun ClassDef.asMutableClass() = if (this is MutableClass) this else this.toMutable() + + /** + * Check if the [AccessFlags.PUBLIC] flag is set. + * + * @return True, if the flag is set. + */ + fun Int.isPublic() = AccessFlags.PUBLIC.isSet(this) + + /** + * Make [AccessFlags] public. + * + * @return The new [AccessFlags]. + */ + fun Int.toPublic() = this.or(AccessFlags.PUBLIC).and(AccessFlags.PRIVATE.value.inv()) + + /** + * Filter [this] on [needles] matching the given [predicate]. + * + * @param this The hay to filter for [needles]. + * @param needles The needles to filter [this] with. + * @param predicate The filter. + * @return The [this] filtered on [needles] matching the given [predicate]. + */ + fun Iterable.filterAny( + needles: Iterable, predicate: (HayType, NeedleType) -> Boolean + ) = Iterable::filter.any(this, needles, predicate) + + /** + * Filter [this] on [needles] not matching the given [predicate]. + * + * @param this The hay to filter for [needles]. + * @param needles The needles to filter [this] with. + * @param predicate The filter. + * @return The [this] filtered on [needles] not matching the given [predicate]. + */ + fun Iterable.filterNotAny( + needles: Iterable, predicate: (HayType, NeedleType) -> Boolean + ) = Iterable::filterNot.any(this, needles, predicate) + + fun KFunction2, (HayType) -> Boolean, List>.any( + haystack: Iterable, + needles: Iterable, + predicate: (HayType, NeedleType) -> Boolean + ) = this(haystack) { hay -> + needles.any { needle -> + predicate(hay, needle) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt b/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt index 9e17e15..9792b23 100644 --- a/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt +++ b/src/main/kotlin/app/revanced/patcher/util/proxy/mutableTypes/MutableMethod.kt @@ -20,6 +20,22 @@ class MutableMethod(method: Method) : Method, BaseMethodReference() { private val _parameterTypes by lazy { method.parameterTypes.toMutableList() } private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions } + fun setDefiningClass(definingClass: String) { + this.definingClass = definingClass + } + + fun setName(name: String) { + this.name = name + } + + fun setAccessFlags(accessFlags: Int) { + this.accessFlags = accessFlags + } + + fun setReturnType(returnType: String) { + this.returnType = returnType + } + override fun getDefiningClass(): String { return definingClass }