From 5674c1f2a245e44f40daee035513314f2bd2a2ce Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 8 Jan 2026 00:13:10 +0100 Subject: [PATCH] more completeness --- .../app/revanced/patcher/InstructionFilter.kt | 1082 ----------------- .../kotlin/app/revanced/patcher/Matching.kt | 321 ++--- .../app/revanced/patcher/MatchingTest.kt | 35 +- 3 files changed, 196 insertions(+), 1242 deletions(-) delete mode 100644 patcher/src/commonMain/kotlin/app/revanced/patcher/InstructionFilter.kt diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/InstructionFilter.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/InstructionFilter.kt deleted file mode 100644 index dd2f68b..0000000 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/InstructionFilter.kt +++ /dev/null @@ -1,1082 +0,0 @@ -// Temporarily adding this file for development purposes of patches - -@file:Suppress("unused") - -package app.revanced.patcher - -import app.revanced.patcher.FieldAccessFilter.Companion.parseJvmFieldAccess -import app.revanced.patcher.MethodCallFilter.Companion.parseJvmMethodCall -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.instruction.Instruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.iface.reference.StringReference -import com.android.tools.smali.dexlib2.iface.reference.TypeReference -import java.util.EnumSet - -/** - * Simple interface to control how much space is allowed between a previous - * [InstructionFilter] match and the current [InstructionFilter]. - */ -fun interface InstructionLocation { - /** - * @param previouslyMatchedIndex The previously matched index, or -1 if this is the first filter. - * @param currentIndex The current method index that is about to be checked. - */ - fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int): Boolean - - /** - * Matching can occur anywhere after the previous instruction filter match index. - * Is the default behavior for all filters. - */ - class MatchAfterAnywhere : InstructionLocation { - override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) = true - } - - /** - * Matches the first instruction of a method. - * - * This can only be used for the first filter, and using with any other filter will throw an exception. - */ - class MatchFirst() : InstructionLocation { - override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int): Boolean { - require(previouslyMatchedIndex < 0) { - "MatchFirst can only be used for the first instruction filter" - } - return true - } - } - - /** - * Instruction index immediately after the previous filter. - * - * Useful for opcodes that must always appear immediately after the last filter such as: - * - [Opcode.MOVE_RESULT] - * - [Opcode.MOVE_RESULT_WIDE] - * - [Opcode.MOVE_RESULT_OBJECT] - * - * This cannot be used for the first filter and will throw an exception. - */ - class MatchAfterImmediately() : InstructionLocation { - override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int): Boolean { - require(previouslyMatchedIndex >= 0) { - "MatchAfterImmediately cannot be used for the first instruction filter" - } - return currentIndex - 1 == previouslyMatchedIndex - } - } - - /** - * Instruction index can occur within a range of the previous instruction filter match index. - * used to constrain instruction matching to a region after the previous instruction filter. - * - * This cannot be used for the first filter and will throw an exception. - * - * @param matchDistance The number of unmatched instructions that can exist between the - * current instruction filter and the previously matched instruction filter. - * A value of 0 means the current filter can only match immediately after - * the previously matched instruction (making this functionally identical to - * [MatchAfterImmediately]). A value of 10 means between 0 and 10 unmatched - * instructions can exist between the previously matched instruction and - * the current instruction filter. - */ - class MatchAfterWithin(val matchDistance: Int) : InstructionLocation { - init { - require(matchDistance >= 0) { - "matchDistance must be non-negative" - } - } - - override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int): Boolean { - require(previouslyMatchedIndex >= 0) { - "MatchAfterImmediately cannot be used for the first instruction filter" - } - return currentIndex - previouslyMatchedIndex - 1 <= matchDistance - } - } - - /** - * Instruction index can occur only after a minimum number of unmatched instructions from the - * previous instruction match. Or if this is used with the first filter of a fingerprint then - * this can only match starting from a given instruction index. - * - * @param minimumDistanceFromLastInstruction The minimum number of unmatched instructions that - * must exist between this instruction and the last matched instruction. A value of 0 is - * functionally identical to [MatchAfterImmediately]. - */ - class MatchAfterAtLeast(var minimumDistanceFromLastInstruction: Int) : InstructionLocation { - init { - require(minimumDistanceFromLastInstruction >= 0) { - "minimumDistanceFromLastInstruction must >= 0" - } - } - - override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int): Boolean { - return currentIndex - previouslyMatchedIndex - 1 >= minimumDistanceFromLastInstruction - } - } - - /** - * Functionally combines both [MatchAfterAtLeast] and [MatchAfterWithin] to give a bounded range - * where the next instruction must match relative to the previous matched instruction. - * - * Unlike [MatchAfterImmediately] or [MatchAfterWithin], this can be used for the first filter - * to constrain matching to a specific range starting from index 0. - * - * @param minimumDistanceFromLastInstruction The minimum number of unmatched instructions that - * must exist between this instruction and the last - * matched instruction. - * @param maximumDistanceFromLastInstruction The maximum number of unmatched instructions - * that can exist between this instruction and the - * last matched instruction. - */ - class MatchAfterRange( - val minimumDistanceFromLastInstruction: Int, - val maximumDistanceFromLastInstruction: Int - ) : InstructionLocation { - - private val minMatcher = MatchAfterAtLeast(minimumDistanceFromLastInstruction) - private val maxMatcher = MatchAfterWithin(maximumDistanceFromLastInstruction) - - init { - require(minimumDistanceFromLastInstruction <= maximumDistanceFromLastInstruction) { - "minimumDistanceFromLastInstruction must be <= maximumDistanceFromLastInstruction" - } - } - - override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int): Boolean { - // For the first filter, previouslyMatchedIndex will be -1, and both delegates - // will correctly enforce their own semantics starting from index 0. - return minMatcher.indexIsValidForMatching(previouslyMatchedIndex, currentIndex) && - maxMatcher.indexIsValidForMatching(previouslyMatchedIndex, currentIndex) - } - } -} - - -/** - * String comparison type. - */ -enum class StringComparisonType { - EQUALS, - CONTAINS, - STARTS_WITH, - ENDS_WITH; - - /** - * @param targetString The target string to search - * @param searchString To search for in the target string (or to compare entirely for equality). - */ - fun compare(targetString: String, searchString: String): Boolean { - return when (this) { - EQUALS -> targetString == searchString - CONTAINS -> targetString.contains(searchString) - STARTS_WITH -> targetString.startsWith(searchString) - ENDS_WITH -> targetString.endsWith(searchString) - } - } - - /** - * Throws [IllegalArgumentException] if the class type search string is invalid and can never match. - */ - internal fun validateSearchStringForClassType(classTypeSearchString: String) { - when (this) { - EQUALS -> { - STARTS_WITH.validateSearchStringForClassType(classTypeSearchString) - ENDS_WITH.validateSearchStringForClassType(classTypeSearchString) - } - - CONTAINS -> Unit // Nothing to validate, anything goes. - STARTS_WITH -> require(classTypeSearchString.startsWith('L')) { - "Class type does not start with L: $classTypeSearchString" - } - - ENDS_WITH -> require(classTypeSearchString.endsWith(';')) { - "Class type does not end with a semicolon: $classTypeSearchString" - } - } - } -} - - -/** - * Matches method [Instruction] objects, similar to how [Fingerprint] matches entire methods. - * - * The most basic filters match only opcodes and nothing more, - * and more precise filters can match: - * - Field references (get/put opcodes) by name/type. - * - Method calls (invoke_* opcodes) by name/parameter/return type. - * - Object instantiation for specific class types. - * - Literal const values. - * - * If creating a custom filter for unusual or app specific purposes, consider extending - * [OpcodeFilter] or [OpcodesFilter] to reduce boilerplate opcode checking logic. - */ -fun interface InstructionFilter { - - /** - * The [InstructionLocation] associated with this filter. - */ - val location: InstructionLocation - get() = InstructionLocation.MatchAfterAnywhere() - - /** - * If this filter matches the method instruction. - * - * @param enclosingMethod The method of that contains [instruction]. - * @param instruction The instruction to check for a match. - */ - fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean -} - - -class AnyInstruction internal constructor( - internal val filters: List, - override val location: InstructionLocation -) : InstructionFilter { - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - return filters.any { filter -> - filter.matches(enclosingMethod, instruction) - } - } -} - -/** - * Logical OR operator where the first filter that matches satisfies this filter. - * - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun anyInstruction( - vararg filters: InstructionFilter, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = AnyInstruction(filters.asList(), location) - - -/** - * Single opcode match. - * - * Patches can extend this as desired to do unusual or app specific instruction filtering. - * Or Alternatively can implement [InstructionFilter] directly. - * - * @param opcode Opcode to match. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -open class OpcodeFilter( - val opcode: Opcode, - override val location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) : InstructionFilter { - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - return instruction.opcode == opcode - } -} - -/** - * Single opcode match. - * - * @param opcode Opcode to match. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun opcode( - opcode: Opcode, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = OpcodeFilter(opcode, location) - - -/** - * Matches a single instruction from many kinds of opcodes. - * - * Patches can extend this as desired to do unusual or app specific instruction filtering. - * Or Alternatively can implement [InstructionFilter] directly. - * - * @param opcodes Set of opcodes to match to. Value of `null` will match any opcode. - * If matching only a single opcode then instead use [OpcodeFilter]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -open class OpcodesFilter protected constructor( - val opcodes: EnumSet?, - override val location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) : InstructionFilter { - - protected constructor( - opcodes: List?, - location: InstructionLocation - ) : this(if (opcodes == null) null else EnumSet.copyOf(opcodes), location) - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (opcodes == null) { - return true // Match anything. - } - return opcodes.contains(instruction.opcode) - } - - internal companion object { - /** - * First opcode can match anywhere in a method, but all - * subsequent opcodes must match after the previous opcode. - * - * A value of `null` indicates to match any opcode. - */ - internal fun listOfOpcodes(opcodes: Collection): List { - val list = ArrayList(opcodes.size) - var location: InstructionLocation? = null - - opcodes.forEach { opcode -> - // First opcode can match anywhere. - val opcodeLocation = location ?: InstructionLocation.MatchAfterAnywhere() - - list += if (opcode == null) { - // Null opcode matches anything. - OpcodesFilter( - null as List?, - opcodeLocation - ) - } else { - OpcodeFilter(opcode, opcodeLocation) - } - - if (location == null) { - location = InstructionLocation.MatchAfterImmediately() - } - } - - return list - } - } -} - - -class LiteralFilter internal constructor( - val literal: () -> Long, - opcodes: List? = null, - location: InstructionLocation -) : OpcodesFilter(opcodes, location) { - - /** - * Store the lambda value instead of calling it more than once. - */ - private val literalValue: Long by lazy(literal) - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (!super.matches(enclosingMethod, instruction)) { - return false - } - - if (instruction !is WideLiteralInstruction) return false - - return instruction.wideLiteral == literalValue - } -} - -/** - * Long literal. Automatically converts literal to opcode hex. - * - * @param literal Literal number. - * @param opcodes Opcodes to match. By default this matches any literal number opcode such as: - * [Opcode.CONST_4], [Opcode.CONST_16], [Opcode.CONST], [Opcode.CONST_WIDE]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun literal( - literal: Long, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = LiteralFilter({ literal }, opcodes, location) - -/** - * Integer literal. Automatically converts literal to opcode hex. - * - * @param literal Literal number. - * @param opcodes Opcodes to match. By default this matches any literal number opcode such as: - * [Opcode.CONST_4], [Opcode.CONST_16], [Opcode.CONST], [Opcode.CONST_WIDE]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun literal( - literal: Int, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = LiteralFilter({ literal.toLong() }, opcodes, location) - -/** - * Double point literal. Automatically converts literal to opcode hex. - * - * @param literal Literal number. - * @param opcodes Opcodes to match. By default this matches any literal number opcode such as: - * [Opcode.CONST_4], [Opcode.CONST_16], [Opcode.CONST], [Opcode.CONST_WIDE]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun literal( - literal: Double, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = LiteralFilter({ literal.toRawBits() }, opcodes, location) - -/** - * Floating point literal. Automatically converts literal to opcode hex. - * - * @param literal Floating point literal. - * @param opcodes Opcodes to match. By default this matches any literal number opcode such as: - * [Opcode.CONST_4], [Opcode.CONST_16], [Opcode.CONST], [Opcode.CONST_WIDE]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun literal( - literal: Float, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = LiteralFilter({ literal.toRawBits().toLong() }, opcodes, location) - -/** - * Literal number value. Automatically converts the provided number to opcode hex. - * - * @param literal Literal number. - * @param opcodes Opcodes to match. By default this matches any literal number opcode such as: - * [Opcode.CONST_4], [Opcode.CONST_16], [Opcode.CONST], [Opcode.CONST_WIDE]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun literal( - literal: () -> Long, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = LiteralFilter(literal, opcodes, location) - - -class MethodCallFilter internal constructor( - val definingClass: String? = null, - val name: String? = null, - val parameters: List? = null, - val returnType: String? = null, - opcodes: List? = null, - location: InstructionLocation -) : OpcodesFilter(opcodes, location) { - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (!super.matches(enclosingMethod, instruction)) { - return false - } - - val reference = (instruction as? ReferenceInstruction)?.reference as? MethodReference - ?: return false - - if (definingClass != null) { - val referenceClass = reference.definingClass - - if (!StringComparisonType.ENDS_WITH.compare(referenceClass, definingClass)) { - // Check if 'this' defining class is used. - // Would be nice if this also checked all super classes, - // but doing so requires iteratively checking all superclasses - // up to the root class since class defs are mere Strings. - if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) { - return false - } // else, the method call is for 'this' class. - } - } - - if (name != null && reference.name != name) { - return false - } - - if (returnType != null && - !StringComparisonType.STARTS_WITH.compare(reference.returnType, returnType) - ) { - return false - } - fun parametersStartsWith( - parameters1: Iterable, - parameters2: Iterable, - ): Boolean { - if (parameters1.count() != parameters2.count()) return false - val iterator1 = parameters1.iterator() - parameters2.forEach { - if (!it.startsWith(iterator1.next())) return false - } - return true - } - if (parameters != null && - !parametersStartsWith(reference.parameterTypes, parameters) - ) { - return false - } - - return true - } - - internal companion object { - private val regex = Regex("""^(L[^;]+;)->([^(\s]+)\(([^)]*)\)(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""") - - internal fun parseJvmMethodCall( - methodSignature: String, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() - ): MethodCallFilter { - val matchResult = regex.matchEntire(methodSignature) - ?: throw IllegalArgumentException("Invalid method signature: $methodSignature") - - val classDescriptor = matchResult.groupValues[1] - val methodName = matchResult.groupValues[2] - val paramDescriptorString = matchResult.groupValues[3] - val returnDescriptor = matchResult.groupValues[4] - - val paramDescriptors = parseParameterDescriptors(paramDescriptorString) - - return MethodCallFilter( - classDescriptor, - methodName, - paramDescriptors, - returnDescriptor, - opcodes, - location - ) - } - - /** - * Parses a single JVM type descriptor or an array descriptor at the current position. - * For example: Lcom/example/SomeClass; or I or [I or [Lcom/example/SomeClass; - */ - private fun parseSingleType(params: String, startIndex: Int): Pair { - var i = startIndex - - // Skip past array declaration, including multi-dimensional arrays. - val paramsLength = params.length - while (i < paramsLength && params[i] == '[') { - i++ - } - - return if (i < paramsLength && params[i] == 'L') { - // It's an object type starting with 'L', read until ';' - val semicolonPos = params.indexOf(';', i) - if (semicolonPos < 0) { - throw IllegalArgumentException("Malformed object descriptor (missing semicolon): $params") - } - // Substring from startIndex up to and including the semicolon. - val typeDescriptor = params.substring(startIndex, semicolonPos + 1) - typeDescriptor to (semicolonPos + 1) - } else { - // It's either a primitive or we've already consumed the array part - // So just take one character (e.g. 'I', 'Z', 'B', etc.) - val typeDescriptor = params.substring(startIndex, i + 1) - typeDescriptor to (i + 1) - } - } - - /** - * Parses the parameters into a list of JVM type descriptors. - */ - private fun parseParameterDescriptors(paramString: String): List { - val result = mutableListOf() - var currentIndex = 0 - val stringLength = paramString.length - - while (currentIndex < stringLength) { - val (type, nextIndex) = parseSingleType(paramString, currentIndex) - result.add(type) - currentIndex = nextIndex - } - - return result - } - } -} - -/** - * Matches a method call, such as: - * `invoke-virtual {v3, v4}, La;->b(I)V` - * - * @param definingClass Defining class of the field call. Compares using [StringComparisonType.ENDS_WITH]. - * For calls to a method in the same class, use 'this' as the defining class. - * Note: 'this' does not work for fields found in superclasses. - * @param name Full name of the method. Compares using [StringComparisonType.EQUALS]. - * @param parameters Parameters of the method call. Each parameter matches using[StringComparisonType.STARTS_WITH] - * and semantics are the same as [Fingerprint] parameters. - * @param returnType Return type. Matches using [StringComparisonType.STARTS_WITH]. - * @param opcodes Opcode types to match. By default this matches any method call opcode: `Opcode.INVOKE_*`. - * If this filter must match specific types of method call, then specify the desired opcodes -such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun methodCall( - definingClass: String? = null, - name: String? = null, - parameters: List? = null, - returnType: String? = null, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = MethodCallFilter( - definingClass, - name, - parameters, - returnType, - opcodes, - location -) - -/** - * Matches a method call, such as: - * `invoke-virtual {v3, v4}, La;->b(I)V` - * - * @param definingClass Defining class of the field call. Compares using [StringComparisonType.ENDS_WITH]. - * For calls to a method in the same class, use 'this' as the defining class. - * Note: 'this' does not work for fields found in superclasses. - * @param name Full name of the method. Compares using [StringComparisonType.EQUALS]. - * @param parameters Parameters of the method call. Each parameter matches using[StringComparisonType.STARTS_WITH] - * and semantics are the same as [Fingerprint] parameters. - * @param returnType Return type. Matches using [StringComparisonType.STARTS_WITH]. - * @param opcode Single opcode type to match. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun methodCall( - definingClass: String? = null, - name: String? = null, - parameters: List? = null, - returnType: String? = null, - opcode: Opcode, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = MethodCallFilter( - definingClass, - name, - parameters, - returnType, - listOf(opcode), - location -) - -/** - * Method call for a copy pasted SMALI style method signature. e.g.: - * `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;` - * - * Should never be used with obfuscated method names or parameter/return types. - * - * @param smali Smali method call reference, such as - * `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`. - * @param opcodes List of all possible opcodes to match. Defaults to matching all method calls types: `Opcode.INVOKE_*`. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun methodCall( - smali: String, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = parseJvmMethodCall(smali, opcodes, location) - -/** - * Method call for a copy pasted SMALI style method signature. e.g.: - * `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;` - * - * Should never be used with obfuscated method names or parameter/return types. - * - * @param smali Smali method call reference, such as - * `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`. - * @param opcode Single opcode type to match. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun methodCall( - smali: String, - opcode: Opcode, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = parseJvmMethodCall(smali, listOf(opcode), location) - - -class FieldAccessFilter internal constructor( - val definingClass: String? = null, - val name: String? = null, - val type: String? = null, - opcodes: List? = null, - location: InstructionLocation -) : OpcodesFilter(opcodes, location) { - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (!super.matches(enclosingMethod, instruction)) { - return false - } - - val reference = (instruction as? ReferenceInstruction)?.reference as? FieldReference - ?: return false - - if (definingClass != null) { - val referenceClass = reference.definingClass - - if (!referenceClass.endsWith(definingClass)) { - if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) { - return false - } // else, the method call is for 'this' class. - } - } - - if (name != null && reference.name != name) { - return false - } - - if (type != null && !reference.type.startsWith(type)) { - return false - } - - return true - } - - internal companion object { - private val regex = Regex("""^(L[^;]+;)->([^:]+):(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""") - - internal fun parseJvmFieldAccess( - fieldSignature: String, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() - ): FieldAccessFilter { - val matchResult = regex.matchEntire(fieldSignature) - ?: throw IllegalArgumentException("Invalid field access smali: $fieldSignature") - - return fieldAccess( - definingClass = matchResult.groupValues[1], - name = matchResult.groupValues[2], - type = matchResult.groupValues[3], - opcodes = opcodes, - location = location - ) - } - } -} - - -/** - * Matches a field call, such as: - * `iget-object v0, p0, Lahhh;->g:Landroid/view/View;` - * - * @param definingClass Defining class of the field call. Compares using [StringComparisonType.ENDS_WITH]. - * For calls to a method in the same class, use 'this' as the defining class. - * Note: 'this' does not work for fields found in superclasses. - * @param name Full name of the field. Compares using [StringComparisonType.EQUALS]. - * @param type Class type of field. Compares using [StringComparisonType.STARTS_WITH]. - * @param opcode Single opcode type to match. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun fieldAccess( - definingClass: String? = null, - name: String? = null, - type: String? = null, - opcode: Opcode, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = fieldAccess( - definingClass, - name, - type, - listOf(opcode), - location -) - -/** - * Matches a field call, such as: - * `iget-object v0, p0, Lahhh;->g:Landroid/view/View;` - * - * @param definingClass Defining class of the field call. Compares using [StringComparisonType.ENDS_WITH]. - * For calls to a method in the same class, use 'this' as the defining class. - * Note: 'this' does not work for fields found in superclasses. - * @param name Full name of the field. Compares using [StringComparisonType.EQUALS]. - * @param type Class type of field. Compares using [StringComparisonType.STARTS_WITH]. - * @param opcodes List of all possible opcodes to match. Defaults to matching all get/put opcodes. - * (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc). - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun fieldAccess( - definingClass: String? = null, - name: String? = null, - type: String? = null, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = FieldAccessFilter( - definingClass, - name, - type, - opcodes, - location -) - -/** - * Field access for a copy pasted SMALI style field access call. e.g.: - * `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;` - * - * Should never be used with obfuscated field names or obfuscated field types. - * @param smali Smali field access statement, such as `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`. - * @param opcodes List of all possible opcodes to match. Defaults to matching all get/put opcodes. - * (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc). - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun fieldAccess( - smali: String, - opcodes: List? = null, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = parseJvmFieldAccess(smali, opcodes, location) - -/** - * Field access for a copy pasted SMALI style field access call. e.g.: - * `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;` - * - * Should never be used with obfuscated field names or obfuscated field types. - * - * @param smali Smali field access statement, such as `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`. - * @param opcode Single opcode type to match. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun fieldAccess( - smali: String, - opcode: Opcode, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = parseJvmFieldAccess(smali, listOf(opcode), location) - - -class StringFilter internal constructor( - val string: () -> String, - val comparison: StringComparisonType, - location: InstructionLocation -) : OpcodesFilter(listOf(Opcode.CONST_STRING, Opcode.CONST_STRING_JUMBO), location) { - - /** - * Store the lambda value instead of calling it more than once. - */ - private val stringValue: String by lazy(string) - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (!super.matches(enclosingMethod, instruction)) { - return false - } - - val stringReference = (instruction as ReferenceInstruction).reference as StringReference - return comparison.compare(stringReference.string, stringValue) - } -} - -/** - * Literal String instruction. - * - * @param string string literal, using exact matching of [StringComparisonType.EQUALS]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun string( - string: String, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = StringFilter({ string }, StringComparisonType.EQUALS, location) - -/** - * Literal String instruction. - * - * @param string string literal, using exact matching of [StringComparisonType.EQUALS]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun string( - string: () -> String, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = StringFilter(string, StringComparisonType.EQUALS, location) - -/** - * Literal String instruction. - * - * @param string string literal. - * @param comparison How to compare the string literal. For more precise matching of strings, - * consider using [anyInstruction] with multiple exact string declarations. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun string( - string: String, - /** - * How to match a given string opcode literal. Default is exact string equality. For more - * precise matching of multiple strings, consider using [anyInstruction] with multiple - * exact string declarations. - */ - comparison: StringComparisonType = StringComparisonType.EQUALS, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = StringFilter({ string }, comparison, location) - -/** - * Literal String instruction. - * - * @param string string literal. - * @param comparison How to compare the string literal. For more precise matching of strings, - * consider using [anyInstruction] with multiple exact string declarations. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun string( - string: () -> String, - comparison: StringComparisonType, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = StringFilter(string, comparison, location) - - -class NewInstanceFilter internal constructor( - val type: () -> String, - val comparison: StringComparisonType, - location: InstructionLocation -) : OpcodesFilter(listOf(Opcode.NEW_INSTANCE, Opcode.NEW_ARRAY), location) { - - /** - * Store the lambda value instead of calling it more than once. - */ - private val typeValue: String by lazy { - val typeValue = type() - comparison.validateSearchStringForClassType(typeValue) - typeValue - } - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (!super.matches(enclosingMethod, instruction)) { - return false - } - - val reference = (instruction as ReferenceInstruction).reference as TypeReference - return comparison.compare(reference.type, typeValue) - } -} - -/** - * Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type. - * - * @param type Class type, compared using [StringComparisonType.ENDS_WITH]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun newInstance( - type: String, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = NewInstanceFilter({ type }, StringComparisonType.ENDS_WITH, location) - -/** - * Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type. - * - * @param type Class type, compared using [StringComparisonType.ENDS_WITH]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun newInstance( - type: () -> String, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere(), -) = NewInstanceFilter(type, StringComparisonType.ENDS_WITH, location) - -/** - * Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type. - * - * @param type Class type. - * @param comparison How to compare the opcode class type. For more precise matching of types, - * consider using [anyInstruction] with multiple exact type declarations. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun newInstance( - type: String, - comparison: StringComparisonType, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = NewInstanceFilter({ type }, comparison, location) - -/** - * Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type. - * - * @param type Class type. - * @param comparison How to compare the opcode class type. For more precise matching of types, - * consider using [anyInstruction] with multiple exact type declarations. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun newInstance( - type: () -> String, - comparison: StringComparisonType, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = NewInstanceFilter(type, comparison, location) - - -class CheckCastFilter internal constructor( - val type: () -> String, - val comparison: StringComparisonType, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) : OpcodeFilter(Opcode.CHECK_CAST, location) { - - /** - * Store the lambda value instead of calling it more than once. - */ - private val typeValue: String by lazy { - val typeValue = type() - comparison.validateSearchStringForClassType(typeValue) - typeValue - } - - override fun matches( - enclosingMethod: Method, - instruction: Instruction - ): Boolean { - if (!super.matches(enclosingMethod, instruction)) { - return false - } - - val reference = (instruction as ReferenceInstruction).reference as TypeReference - return comparison.compare(reference.type, typeValue) - } -} - -/** - * Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type. - * - * @param type Class type, compared using [StringComparisonType.ENDS_WITH]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun checkCast( - type: String, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = CheckCastFilter({ type }, StringComparisonType.ENDS_WITH, location) - -/** - * Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type. - * - * @param type Class type, compared using [StringComparisonType.ENDS_WITH]. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun checkCast( - type: () -> String, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = CheckCastFilter(type, StringComparisonType.ENDS_WITH, location) - -/** - * Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type using the provided string comparison type. - * - * @param type Class type. - * @param comparison How to compare the opcode class type. For more precise matching of types, - * consider using [anyInstruction] with multiple exact type declarations. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun checkCast( - type: String, - comparison: StringComparisonType, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = CheckCastFilter({ type }, comparison, location) - -/** - * Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type using the provided string comparison type. - * - * @param type Class type. - * @param comparison How to compare the opcode class type. For more precise matching of types, - * consider using [anyInstruction] with multiple exact type declarations. - * @param location Where this filter is allowed to match. Default is anywhere after the previous instruction. - */ -fun checkCast( - type: () -> String, - comparison: StringComparisonType, - location: InstructionLocation = InstructionLocation.MatchAfterAnywhere() -) = CheckCastFilter(type, comparison, location) diff --git a/patcher/src/commonMain/kotlin/app/revanced/patcher/Matching.kt b/patcher/src/commonMain/kotlin/app/revanced/patcher/Matching.kt index e660817..d813fef 100644 --- a/patcher/src/commonMain/kotlin/app/revanced/patcher/Matching.kt +++ b/patcher/src/commonMain/kotlin/app/revanced/patcher/Matching.kt @@ -21,80 +21,79 @@ import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.* import com.android.tools.smali.dexlib2.iface.Annotation import com.android.tools.smali.dexlib2.iface.instruction.* +import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.mutable.MutableMethod import com.android.tools.smali.dexlib2.util.MethodUtil import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty -fun Iterable.anyClassDef(predicate: ClassDef.() -> Boolean) = any(predicate) +typealias Predicate = T.() -> Boolean +typealias Function = T.() -> Unit -fun ClassDef.anyMethod(predicate: Method.() -> Boolean) = methods.any(predicate) +fun Iterable.anyClassDef(predicate: Predicate) = any(predicate) -fun ClassDef.anyDirectMethod(predicate: Method.() -> Boolean) = directMethods.any(predicate) +fun ClassDef.anyMethod(predicate: Predicate) = methods.any(predicate) -fun ClassDef.anyVirtualMethod(predicate: Method.() -> Boolean) = virtualMethods.any(predicate) +fun ClassDef.anyDirectMethod(predicate: Predicate) = directMethods.any(predicate) -fun ClassDef.anyField(predicate: Field.() -> Boolean) = fields.any(predicate) +fun ClassDef.anyVirtualMethod(predicate: Predicate) = virtualMethods.any(predicate) -fun ClassDef.anyInstanceField(predicate: Field.() -> Boolean) = instanceFields.any(predicate) +fun ClassDef.anyField(predicate: Predicate) = fields.any(predicate) -fun ClassDef.anyStaticField(predicate: Field.() -> Boolean) = staticFields.any(predicate) +fun ClassDef.anyInstanceField(predicate: Predicate) = instanceFields.any(predicate) -fun ClassDef.anyInterface(predicate: String.() -> Boolean) = interfaces.any(predicate) +fun ClassDef.anyStaticField(predicate: Predicate) = staticFields.any(predicate) -fun ClassDef.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate) +fun ClassDef.anyInterface(predicate: Predicate) = interfaces.any(predicate) -fun Method.implementation(predicate: MethodImplementation.() -> Boolean) = implementation?.predicate() ?: false +fun ClassDef.anyAnnotation(predicate: Predicate) = annotations.any(predicate) -fun Method.anyParameter(predicate: MethodParameter.() -> Boolean) = parameters.any(predicate) +fun Method.implementation(predicate: Predicate) = implementation?.predicate() ?: false -fun Method.anyParameterType(predicate: CharSequence.() -> Boolean) = parameterTypes.any(predicate) +fun Method.anyParameter(predicate: Predicate) = parameters.any(predicate) -fun Method.anyAnnotation(predicate: Annotation.() -> Boolean) = annotations.any(predicate) +fun Method.anyParameterType(predicate: Predicate) = parameterTypes.any(predicate) -fun Method.anyHiddenApiRestriction(predicate: HiddenApiRestriction.() -> Boolean) = hiddenApiRestrictions.any(predicate) +fun Method.anyAnnotation(predicate: Predicate) = annotations.any(predicate) -fun MethodImplementation.anyInstruction(predicate: Instruction.() -> Boolean) = instructions.any(predicate) +fun Method.anyHiddenApiRestriction(predicate: Predicate) = hiddenApiRestrictions.any(predicate) -fun MethodImplementation.anyTryBlock(predicate: TryBlock.() -> Boolean) = tryBlocks.any(predicate) +fun MethodImplementation.anyInstruction(predicate: Predicate) = instructions.any(predicate) -fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate) +fun MethodImplementation.anyTryBlock(predicate: Predicate>) = tryBlocks.any(predicate) -fun Iterable.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate) +fun MethodImplementation.anyDebugItem(predicate: Predicate) = debugItems.any(predicate) + +fun Iterable.anyInstruction(predicate: Predicate) = any(predicate) private typealias ClassDefPredicate = context(PredicateContext) ClassDef.() -> Boolean - private typealias MethodPredicate = context(PredicateContext) Method.() -> Boolean - inline fun PredicateContext.remember(key: Any, defaultValue: () -> V) = if (key in this) get(key) as V else defaultValue().also { put(key, it) } private fun cachedReadOnlyProperty(block: BytecodePatchContext.(KProperty<*>) -> T) = - JVMConflict.cachedReadOnlyProperty(block) + object : ReadOnlyProperty { + private val cache = HashMap(1) -private object JVMConflict { - fun cachedReadOnlyProperty(block: R.(KProperty<*>) -> T) = object : ReadOnlyProperty { - private val cache = HashMap(1) - - override fun getValue(thisRef: R, property: KProperty<*>) = - (if (thisRef in cache) cache[thisRef] else cache.getOrPut(thisRef) { thisRef.block(property) }) + override fun getValue(thisRef: BytecodePatchContext, property: KProperty<*>) = + if (thisRef in cache) cache.getValue(thisRef) + else cache.getOrPut(thisRef) { thisRef.block(property) } } -} -class MutablePredicateList internal constructor() : MutableList Boolean> by mutableListOf() +class MutablePredicateList internal constructor() : MutableList> by mutableListOf() private typealias DeclarativePredicate = context(PredicateContext) MutablePredicateList.() -> Unit - -fun T.declarativePredicate(build: MutablePredicateList.() -> Unit) = +fun T.declarativePredicate(build: Function>) = context(MutablePredicateList().apply(build)) { all(this) } context(context: PredicateContext) -fun T.rememberDeclarativePredicate(key: Any, block: MutablePredicateList.() -> Unit) = +fun T.rememberDeclarativePredicate(key: Any, block: Function>) = context(context.remember(key) { MutablePredicateList().apply(block) }) { all(this) } @@ -567,58 +566,102 @@ private inline fun withPredicateContext(block: PredicateContext.() -> T) = P fun indexedMatcher() = IndexedMatcher() -fun indexedMatcher(build: IndexedMatcher.() -> Unit) = +fun indexedMatcher(build: Function>) = IndexedMatcher().apply(build) -fun Iterable.matchIndexed(build: IndexedMatcher.() -> Unit) = +fun Iterable.matchIndexed(build: Function>) = indexedMatcher(build)(this) context(_: PredicateContext) -fun Iterable.rememberMatchIndexed(key: Any, build: IndexedMatcher.() -> Unit) = +fun Iterable.rememberMatchIndexed(key: Any, build: Function>) = indexedMatcher()(key, this, build) -fun head( - predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean -): T.(Int, Int) -> Boolean = { lastMatchedIndex, currentIndex -> - currentIndex == 0 && predicate(lastMatchedIndex, currentIndex) +context(_: PredicateContext) +fun Iterable.matchIndexed( + key: Any, + vararg items: IndexedMatcherPredicate +) = indexedMatcher()(key, this) { items.forEach { +it } } + +fun at( + index: Int = 0, + predicate: IndexedMatcherPredicate +): IndexedMatcherPredicate = { lastMatchedIndex, currentIndex, setNextIndex -> + currentIndex == index && predicate(lastMatchedIndex, currentIndex, setNextIndex) } -fun head(predicate: T.() -> Boolean): T.(Int, Int) -> Boolean = - head { _, _ -> predicate() } +fun at(index: Int = 0, predicate: Predicate) = + at(index) { _, _, _ -> predicate() } + +fun at(predicate: IndexedMatcherPredicate): IndexedMatcherPredicate = + at(0) { lastMatchedIndex, currentIndex, setNextIndex -> predicate(lastMatchedIndex, currentIndex, setNextIndex) } + +fun at(predicate: Predicate) = + at { _, _, _ -> predicate() } -context(matcher: IndexedMatcher) fun after( range: IntRange = 1..1, - predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean -): T.(Int, Int) -> Boolean = predicate@{ lastMatchedIndex, currentIndex -> + predicate: IndexedMatcherPredicate +): IndexedMatcherPredicate = predicate@{ lastMatchedIndex, currentIndex, setNextIndex -> val distance = currentIndex - lastMatchedIndex - matcher.nextIndex = when { - distance < range.first -> lastMatchedIndex + range.first - distance > range.last -> -1 - else -> return@predicate predicate(lastMatchedIndex, currentIndex) - } + setNextIndex( + when { + distance < range.first -> lastMatchedIndex + range.first + distance > range.last -> -1 + else -> return@predicate predicate(lastMatchedIndex, currentIndex, setNextIndex) + } + ) false } -context(_: IndexedMatcher) -fun after(range: IntRange = 1..1, predicate: T.() -> Boolean) = - after(range) { _, _ -> predicate() } +fun after(range: IntRange = 1..1, predicate: Predicate) = + after(range) { _, _, _ -> predicate() } + +fun after(predicate: IndexedMatcherPredicate) = + after(1..1) { lastMatchedIndex, currentIndex, setNextIndex -> + predicate(lastMatchedIndex, currentIndex, setNextIndex) + } + +fun after(predicate: Predicate) = + after { _, _, _ -> predicate() } + + +fun anyOf( + vararg predicates: IndexedMatcherPredicate +): IndexedMatcherPredicate = { currentIndex, lastMatchedIndex, setNextIndex -> + predicates.any { predicate -> predicate(currentIndex, lastMatchedIndex, setNextIndex) } +} + +fun allOf( + vararg predicates: IndexedMatcherPredicate +): IndexedMatcherPredicate = { currentIndex, lastMatchedIndex, setNextIndex -> + predicates.all { predicate -> predicate(currentIndex, lastMatchedIndex, setNextIndex) } +} + +fun noneOf( + vararg predicates: IndexedMatcherPredicate +): IndexedMatcherPredicate = { currentIndex, lastMatchedIndex, setNextIndex -> + predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex, setNextIndex) } +} context(matcher: IndexedMatcher) -operator fun (T.(Int, Int) -> Boolean).unaryPlus() = matcher.add(this) +operator fun IndexedMatcherPredicate.unaryPlus() = matcher.add(this) -class IndexedMatcher : Matcher Boolean>() { +typealias IndexedMatcherPredicate = + T.(lastMatchedIndex: Int, currentIndex: Int, setNextIndex: (Int?) -> Unit) -> Boolean + +class IndexedMatcher : + Matcher>() { val indices: List field = mutableListOf() private var lastMatchedIndex = -1 private var currentIndex = -1 - var nextIndex: Int? = null + private var nextIndex: Int? = null override fun invoke(haystack: Iterable): Boolean { - // Normalize to list + // Ensure list for indexed access. val hay = haystack as? List ?: haystack.toList() indices.clear() @@ -658,7 +701,7 @@ class IndexedMatcher : Matcher> M.invoke( key: Any, iterable: Iterable, - builder: M.() -> Unit + builder: Function ) = context.remember(key) { apply(builder) }(iterable) -context(_: PredicateContext) -inline operator fun > M.invoke( - iterable: Iterable, - builder: M.() -> Unit -) = invoke(this@invoke.hashCode(), iterable, builder) - abstract class Matcher : MutableList by mutableListOf() { var matchIndex = -1 protected set @@ -716,19 +753,25 @@ abstract class Matcher : MutableList by mutableListOf() { // endregion Matcher context(list: MutablePredicateList) -fun allOf(block: MutablePredicateList.() -> Unit) { +fun allOf(block: Function>) { val child = MutablePredicateList().apply(block) list.add { child.all { it() } } } context(list: MutablePredicateList) -fun anyOf(block: MutablePredicateList.() -> Unit) { +fun anyOf(block: Function>) { val child = MutablePredicateList().apply(block) list.add { child.any { it() } } } context(list: MutablePredicateList) -fun predicate(block: T.() -> Boolean) { +fun noneOf(block: Function>) { + val child = MutablePredicateList().apply(block) + list.add { child.none { it() } } +} + +context(list: MutablePredicateList) +fun predicate(block: Predicate) { list.add(block) } @@ -741,16 +784,28 @@ fun any(target: T): Boolean = list.any { target.it() } fun MutablePredicateList.accessFlags(vararg flags: AccessFlags) = predicate { accessFlags(flags = flags) } +fun MutablePredicateList.returnType( + predicate: Predicate +) = predicate { this.returnType.predicate() } + fun MutablePredicateList.returnType( returnType: String, compare: String.(String) -> Boolean = String::startsWith ) = predicate { this.returnType.compare(returnType) } +fun MutablePredicateList.name( + predicate: Predicate +) = predicate { this.name.predicate() } + fun MutablePredicateList.name( name: String, compare: String.(String) -> Boolean = String::equals ) = predicate { this.name.compare(name) } +fun MutablePredicateList.definingClass( + predicate: Predicate +) = predicate { this.definingClass.predicate() } + fun MutablePredicateList.definingClass( definingClass: String, compare: String.(String) -> Boolean = String::equals @@ -769,7 +824,7 @@ fun MutablePredicateList.instructions( } fun MutablePredicateList.instructions( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean + vararg predicates: IndexedMatcherPredicate ) = instructions { predicates.forEach { +it } } @@ -784,42 +839,41 @@ fun MutablePredicateList.instructions( context(matcher: IndexedMatcher) fun MutablePredicateList.instructions( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -) = instructions { - predicates.forEach { +it } -} + vararg predicates: IndexedMatcherPredicate +) = instructions { predicates.forEach { +it } } -fun MutablePredicateList.custom(block: Method.() -> Boolean) { +fun MutablePredicateList.custom(block: Predicate) { predicate { block() } } -context(_: IndexedMatcher) inline fun `is`( - crossinline predicate: T.() -> Boolean = { true } -): Instruction.(Int, Int) -> Boolean = { _, _ -> (this as? T)?.predicate() == true } + crossinline predicate: Predicate = { true } +): IndexedMatcherPredicate = { _, _, _ -> (this as? T)?.predicate() == true } -fun instruction(predicate: Instruction.() -> Boolean): Instruction.(Int, Int) -> Boolean = { _, _ -> predicate() } +fun instruction(predicate: Predicate = { true }): IndexedMatcherPredicate = + { _, _, _ -> predicate() } -fun registers(predicate: IntArray.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> - when (this) { - is RegisterRangeInstruction -> - IntArray(registerCount) { startRegister + it }.predicate() +fun registers(predicate: Predicate = { true }): IndexedMatcherPredicate = + { _, _, _ -> + when (this) { + is RegisterRangeInstruction -> + IntArray(registerCount) { startRegister + it }.predicate() - is FiveRegisterInstruction -> - intArrayOf(registerC, registerD, registerE, registerF, registerG).predicate() + is FiveRegisterInstruction -> + intArrayOf(registerC, registerD, registerE, registerF, registerG).predicate() - is ThreeRegisterInstruction -> - intArrayOf(registerA, registerB, registerC).predicate() + is ThreeRegisterInstruction -> + intArrayOf(registerA, registerB, registerC).predicate() - is TwoRegisterInstruction -> - intArrayOf(registerA, registerB).predicate() + is TwoRegisterInstruction -> + intArrayOf(registerA, registerB).predicate() - is OneRegisterInstruction -> - intArrayOf(registerA).predicate() + is OneRegisterInstruction -> + intArrayOf(registerA).predicate() - else -> false + else -> false + } } -} fun registers( vararg registers: Int, @@ -828,47 +882,54 @@ fun registers( } ) = registers({ compare(registers) }) -fun literal(predicate: Long.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = - { _, _ -> wideLiteral?.predicate() == true } +fun literal(predicate: Predicate = { true }): IndexedMatcherPredicate = + { _, _, _ -> wideLiteral?.predicate() == true } fun literal(literal: Long, compare: Long.(Long) -> Boolean = Long::equals) = literal { compare(literal) } -fun reference(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = - predicate@{ _, _ -> this.reference?.toString()?.predicate() == true } -fun reference(reference: String, compare: String.(String) -> Boolean = String::equals) = - reference { compare(reference) } +inline fun reference( + crossinline predicate: Predicate = { true } +): IndexedMatcherPredicate = { _, _, _ -> + (reference as? T)?.predicate() == true +} -fun field(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> - fieldReference?.name?.predicate() == true +fun reference( + reference: String, + compare: String.(String) -> Boolean = String::equals +): IndexedMatcherPredicate = { _, _, _ -> this.reference?.toString()?.compare(reference) == true } + +fun field(predicate: Predicate = { true }): IndexedMatcherPredicate = { _, _, _ -> + fieldReference?.predicate() == true } fun field(name: String, compare: String.(String) -> Boolean = String::equals) = - field { compare(name) } + field { this.name.compare(name) } -fun type(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = - { _, _ -> type?.predicate() == true } +fun type(predicate: Predicate = { true }): IndexedMatcherPredicate = + { _, _, _ -> type?.predicate() == true } -fun type(type: String, compare: String.(String) -> Boolean = String::equals) = +fun type(type: String, compare: String.(type: String) -> Boolean = String::equals) = type { compare(type) } -fun method(predicate: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = { _, _ -> - methodReference?.name?.predicate() == true +fun method(predicate: Predicate = { true }): IndexedMatcherPredicate = { _, _, _ -> + methodReference?.predicate() == true } fun method(name: String, compare: String.(String) -> Boolean = String::equals) = - method { compare(name) } + method { this.name.compare(name) } -fun string(compare: String.() -> Boolean = { true }): Instruction.(Int, Int) -> Boolean = predicate@{ _, _ -> - this@predicate.string?.compare() == true -} +fun string(compare: Predicate = { true }): IndexedMatcherPredicate = + predicate@{ _, _, _ -> + this@predicate.string?.compare() == true + } context(stringsList: MutableList) fun string( string: String, compare: String.(String) -> Boolean = String::equals -): Instruction.(Int, Int) -> Boolean { +): IndexedMatcherPredicate { if (compare == String::equals) stringsList += string return string { compare(string) } @@ -877,32 +938,14 @@ fun string( fun string(string: String, compare: String.(String) -> Boolean = String::equals) = string { compare(string) } context(stringsList: MutableList) -operator fun String.invoke(compare: String.(String) -> Boolean = String::equals): Instruction.(Int, Int) -> Boolean { +operator fun String.invoke(compare: String.(String) -> Boolean = String::equals): IndexedMatcherPredicate { if (compare == String::equals) stringsList += this - return { _, _ -> string?.compare(this@invoke) == true } + return { _, _, _ -> string?.compare(this@invoke) == true } } -operator fun Opcode.invoke(): Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean = - { _, _ -> opcode == this@invoke } - -fun anyOf( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> - predicates.any { predicate -> predicate(currentIndex, lastMatchedIndex) } -} - -fun allOf( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> - predicates.all { predicate -> predicate(currentIndex, lastMatchedIndex) } -} - -fun noneOf( - vararg predicates: Instruction.(currentIndex: Int, lastMatchedIndex: Int) -> Boolean -): Instruction.(Int, Int) -> Boolean = { currentIndex, lastMatchedIndex -> - predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex) } -} +operator fun Opcode.invoke(): IndexedMatcherPredicate = + { _, _, _ -> opcode == this@invoke } private typealias BuildDeclarativePredicate = context( PredicateContext, @@ -915,18 +958,8 @@ fun firstMethodComposite( build: BuildDeclarativePredicate ) = MatchBuilder(strings = strings, build) -val a = firstMethodComposite { - name("exampleMethod") - definingClass("Lcom/example/MyClass;") - returnType("V") - instructions( - head(Opcode.RETURN_VOID()), - after(1..5, Opcode.INVOKE_VIRTUAL()) - ) -} - class MatchBuilder private constructor( - val strings: MutableList, + private val strings: MutableList, indexedMatcher: IndexedMatcher, build: BuildDeclarativePredicate, ) { @@ -936,7 +969,7 @@ class MatchBuilder private constructor( private val predicate: DeclarativePredicate = context(strings, indexedMatcher) { { build() } } - private val indices = indexedMatcher.indices + val indices = indexedMatcher.indices private val BytecodePatchContext.cachedImmutableMethodOrNull by gettingFirstMethodDeclarativelyOrNull(strings = strings.toTypedArray(), predicate) diff --git a/patcher/src/jvmTest/kotlin/app/revanced/patcher/MatchingTest.kt b/patcher/src/jvmTest/kotlin/app/revanced/patcher/MatchingTest.kt index 81282ec..55bac4c 100644 --- a/patcher/src/jvmTest/kotlin/app/revanced/patcher/MatchingTest.kt +++ b/patcher/src/jvmTest/kotlin/app/revanced/patcher/MatchingTest.kt @@ -5,7 +5,8 @@ import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethodDeclar import app.revanced.patcher.patch.bytecodePatch import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t +import com.android.tools.smali.dexlib2.iface.reference.StringReference import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.TestInstance @@ -29,12 +30,14 @@ class MatchingTest : PatcherTestBase() { if (fail) returnType("doesnt exist") instructions( - head(Opcode.CONST_STRING()), + reference(), + at(Opcode.CONST_STRING()), `is`(), noneOf(registers()), string("test", String::contains), after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))), allOf(), + `is`(), type("PrintStream;", String::endsWith) ) } @@ -82,7 +85,7 @@ class MatchingTest : PatcherTestBase() { val matcher = indexedMatcher() matcher.apply { - +head { this > 5 } + +at { this > 5 } } assertFalse( matcher(iterable), @@ -90,7 +93,7 @@ class MatchingTest : PatcherTestBase() { ) matcher.clear() - matcher.apply { +head { this == 1 } }(iterable) + matcher.apply { +at { this == 1 } }(iterable) assertEquals( listOf(0), matcher.indices, @@ -98,11 +101,11 @@ class MatchingTest : PatcherTestBase() { ) matcher.clear() - matcher.apply { add { _, _ -> this > 0 } }(iterable) + matcher.apply { add { _, _, _ -> this > 0 } }(iterable) assertEquals(1, matcher.indices.size, "Should only match once.") matcher.clear() - matcher.apply { add { _, _ -> this == 2 } }(iterable) + matcher.apply { add { _, _, _ -> this == 2 } }(iterable) assertEquals( listOf(1), matcher.indices, @@ -111,9 +114,9 @@ class MatchingTest : PatcherTestBase() { matcher.clear() matcher.apply { - +head { this == 1 } - add { _, _ -> this == 2 } - add { _, _ -> this == 4 } + +at { this == 1 } + add { _, _, _ -> this == 2 } + add { _, _, _ -> this == 4 } }(iterable) assertEquals( listOf(0, 1, 3), @@ -123,7 +126,7 @@ class MatchingTest : PatcherTestBase() { matcher.clear() matcher.apply { - +after { this == 1 } + +after { this == 1 } }(iterable) assertEquals( listOf(0), @@ -133,7 +136,7 @@ class MatchingTest : PatcherTestBase() { matcher.clear() matcher.apply { - +after(2..Int.MAX_VALUE) { this == 1 } + +after(2..Int.MAX_VALUE) { this == 1 } } assertFalse( matcher(iterable), @@ -142,7 +145,7 @@ class MatchingTest : PatcherTestBase() { matcher.clear() matcher.apply { - +after(1..1) { this == 2 } + +after(1..1) { this == 2 } } assertFalse( matcher(iterable), @@ -151,10 +154,10 @@ class MatchingTest : PatcherTestBase() { matcher.clear() matcher.apply { - +head { this == 1 } - +after(2..5) { this == 4 } - add { _, _ -> this == 8 } - add { _, _ -> this == 9 } + +at { this == 1 } + +after(2..5) { this == 4 } + add { _, _, _ -> this == 8 } + add { _, _, _ -> this == 9 } }(iterable) assertEquals( listOf(0, 3, 7, 8),