diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 0b32374..c1fcea7 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - MESSAGE: merge branch \`${{ github.head_ref || github.ref_name }}\` to \`main\` + MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main` jobs: pull-request: diff --git a/.releaserc b/.releaserc index 88136e0..08e7db8 100644 --- a/.releaserc +++ b/.releaserc @@ -27,6 +27,11 @@ clearWorkspace: true } ], - "@semantic-release/github" + [ + "@semantic-release/github", + { + successComment: false + } + ] ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index c799d56..3185e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# [6.4.0-dev.2](https://github.com/revanced/revanced-patcher/compare/v6.4.0-dev.1...v6.4.0-dev.2) (2023-01-02) + + +### Features + +* add missing setter to `MutableMethod` ([8f3ecc3](https://github.com/revanced/revanced-patcher/commit/8f3ecc318c39f0270aff53efdee7a1c8d82af421)) +* do not fix methods or methods in class merger ([4102f43](https://github.com/revanced/revanced-patcher/commit/4102f43b8a9473fd0ee96c5d4fb8f6e9b4e30e70)) +* fix method and field access when merging classes ([5c09ef7](https://github.com/revanced/revanced-patcher/commit/5c09ef7837f9b731e137b66c19da77f63c007595)) + +# [6.4.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.2...v6.4.0-dev.1) (2022-12-20) + + +### Features + +* make `aaptPath` nullable ([#146](https://github.com/revanced/revanced-patcher/issues/146)) ([9f0a09a](https://github.com/revanced/revanced-patcher/commit/9f0a09a7569fd5dd78afa27cb66a73d1662edc69)) + ## [6.3.2](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2) (2022-12-18) diff --git a/build.gradle.kts b/build.gradle.kts index cd0567b..792b28d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation("xpp3:xpp3:1.1.4c") implementation("org.smali:smali:2.5.2") implementation("app.revanced:multidexlib2:2.5.2.r2") - implementation("org.apktool:apktool-lib:2.8.1-SNAPSHOT") + implementation("org.apktool:apktool-lib:2.9.0-SNAPSHOT") implementation(kotlin("reflect")) testImplementation(kotlin("test")) diff --git a/gradle.properties b/gradle.properties index 46c6409..6a7357b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ kotlin.code.style = official -version = 6.3.2 +version = 6.4.0-dev.2 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/PatcherOptions.kt b/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt index 13085f0..4e39733 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt @@ -15,7 +15,7 @@ import java.io.File data class PatcherOptions( internal val inputFile: File, internal val resourceCacheDirectory: String, - internal val aaptPath: String = "", + internal val aaptPath: String? = null, internal val frameworkFolderLocation: String? = null, internal val logger: Logger = NopLogger ) 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 }