diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt index aae1a28..984431f 100644 --- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -5,9 +5,11 @@ package app.revanced.patcher import app.revanced.patcher.InstructionLocation.* import app.revanced.patcher.Match.PatternMatch import app.revanced.patcher.Matcher.MatchContext -import app.revanced.patcher.extensions.* +import app.revanced.patcher.extensions.getInstruction +import app.revanced.patcher.extensions.instructionsOrNull import app.revanced.patcher.extensions.opcode import app.revanced.patcher.extensions.string +import app.revanced.patcher.extensions.stringReference import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.PatchException import com.android.tools.smali.dexlib2.AccessFlags @@ -15,9 +17,6 @@ import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef 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.reference.StringReference -import com.android.tools.smali.dexlib2.iface.reference.TypeReference import com.android.tools.smali.dexlib2.util.MethodUtil /** @@ -69,110 +68,69 @@ class Fingerprint internal constructor( fun matchOrNull(): Match? { if (_matchOrNull != null) return _matchOrNull - val matchStrings = indexedMatcher { - strings?.forEach { add { string { contains(it) } } } - } + var stringMatches: List? = null val matchIndices = indexedMatcher() - val match = context(_: MatchContext) fun Method.(): Boolean { + // This line is needed, because the method must be passed by reference to "matchIndices". + // Referencing the method directly would "hardcode" it in the cached pattern by value. + // By using this variable, the reference can be updated for each method. + lateinit var currentMethod: Method + + context(_: MatchContext) + fun Method.match(): Boolean { if (this@Fingerprint.accessFlags != null && this@Fingerprint.accessFlags != accessFlags) return false - if (this@Fingerprint.returnType != null && this@Fingerprint.returnType != returnType) + if (this@Fingerprint.returnType != null && !returnType.startsWith(this@Fingerprint.returnType)) return false - if (this@Fingerprint.parameters != null && !parametersStartsWith(parameterTypes, this@Fingerprint.parameters)) + if (this@Fingerprint.parameters != null && !parametersStartsWith( + parameterTypes, + this@Fingerprint.parameters + ) + ) return false if (custom != null && !custom(this, context.lookupMaps.classDefsByType[definingClass]!!)) return false - if (strings != null && !matchStrings(instructionsOrNull ?: return false)) - return false + stringMatches = if (strings != null) { + val instructions = instructionsOrNull ?: return false + var stringsList: MutableList? = null - fun InstructionFilter.evaluate(instruction: Instruction): Boolean { - return when (this) { - is AnyInstruction -> filters.any { evaluate(instruction) } - is CheckCastFilter -> instruction.opcode(Opcode.CHECK_CAST) && instruction.typeReference?.endsWith( - typeValue - ) ?: false + buildList { + instructions.forEachIndexed { instructionIndex, instruction -> + if (stringsList == null) stringsList = strings.toMutableList() - is FieldAccessFilter -> { - val reference = instruction.fieldReference ?: return false + val string = instruction.stringReference?.string ?: return@forEachIndexed + val index = stringsList.indexOfFirst(string::contains) + if (index < 0) return@forEachIndexed - if (name != null && reference.name != name) return false - if (type != null && !reference.type.startsWith(type)) return false - - definingClass == null || reference.definingClass.endsWith(definingClass) || - (definingClass == "this" && reference.definingClass != method.definingClass) + add(Match.StringMatch(string, instructionIndex)) + stringsList.removeAt(index) } - is LiteralFilter -> instruction.wideLiteral == literalValue - && opcodes?.contains(instruction.opcode) ?: true + if (stringsList == null || stringsList.isNotEmpty()) return false + } - is MethodCallFilter -> { - val reference = instruction.methodReference ?: return false + } else null - if (name != null && reference.name != name) return false + currentMethod = this + return filters == null || matchIndices(instructionsOrNull ?: return false, "match") { + filters.forEach { filter -> + val filterMatches: Instruction.() -> Boolean = { filter.matches(currentMethod, this) } - if (returnType != null && !reference.returnType.startsWith(returnType)) return false - - if (parameters != null && !parametersStartsWith(reference.parameterTypes, parameters)) - return false - - if ((definingClass != null && !reference.definingClass.endsWith(definingClass)) || - (definingClass == "this" && reference.definingClass != method.definingClass) - ) return false - - opcodes?.contains(instruction.opcode) ?: true + when (val location = filter.location) { + is MatchAfterImmediately -> after { filterMatches() } + is MatchAfterWithin -> after(1..location.matchDistance) { filterMatches() } + is MatchAfterAnywhere -> add { filterMatches() } + is MatchAfterAtLeast -> after(location.minimumDistanceFromLastInstruction..Int.MAX_VALUE) { filterMatches() } + is MatchAfterRange -> after(location.minimumDistanceFromLastInstruction..location.maximumDistanceFromLastInstruction) { filterMatches() } + is MatchFirst -> head { filterMatches() } } - - is NewInstanceFilter -> { - if (opcodes != null && !opcodes.contains(instruction.opcode)) return false - instruction.reference { endsWith(type) } - } - - is OpcodeFilter -> instruction.opcode(opcode) - is StringFilter -> { - val string = instruction.stringReference?.string ?: return false - - val filterString = stringValue - when (comparison) { - StringComparisonType.EQUALS -> string == filterString - StringComparisonType.CONTAINS -> string.contains(filterString) - StringComparisonType.STARTS_WITH -> string.startsWith(filterString) - StringComparisonType.ENDS_WITH -> string.endsWith(filterString) - } - } - - is OpcodesFilter -> opcodes?.contains(instruction.opcode) == true - else -> throw IllegalStateException("Unknown InstructionFilter type: ${this::class.java}") } } - - if (filters != null && !matchIndices(instructionsOrNull ?: return false, "match") { - filters.forEach { filter -> - when (val location = filter.location) { - is MatchAfterImmediately -> after { filter.evaluate(this) } - is MatchAfterWithin -> after(1..location.matchDistance) { filter.evaluate(this) } - is MatchAfterAnywhere -> add { filter.evaluate(this) } - is MatchAfterAtLeast -> after( - location.minimumDistanceFromLastInstruction..Int.MAX_VALUE - ) { filter.evaluate(this) } - - is MatchAfterRange -> after( - location.minimumDistanceFromLastInstruction.. - location.maximumDistanceFromLastInstruction - ) { filter.evaluate(this) } - - is MatchFirst -> first { filter.evaluate(this) } - } - } - }) - return false - - return true } val allStrings = buildList { @@ -183,30 +141,28 @@ class Fingerprint internal constructor( ) }.map { it.stringValue } + (strings ?: emptyList()) - val method = if (allStrings.isNotEmpty()) - context.firstMethodOrNull(strings = allStrings.toTypedArray()) { match() } ?: - // Maybe a better way exists - context(MatchContext()) { - context.lookupMaps.methodsWithString.first { it.match() } - } - else context.firstMethodOrNull { match() } ?: return null + val method = if (allStrings.isNotEmpty()) { + context.firstMethodOrNull(strings = allStrings.toTypedArray()) { match() } + ?: context(MatchContext()) { context.lookupMaps.methodsWithString.firstOrNull { it.match() } } + } else { + context.firstMethodOrNull { match() } + } ?: return null val instructionMatches = filters?.withIndex()?.map { (i, filter) -> - Match.InstructionMatch(filter, matchIndices.indices[i], method.getInstruction(i)) + val matchIndex = matchIndices.indices[i] + + Match.InstructionMatch(filter, matchIndex, method.getInstruction(matchIndex)) } - val stringMatches = if (strings != null) matchStrings.indices.map { i -> - // TODO: Should we use the methods string or the fingerprints string - Match.StringMatch(method.getInstruction(i).stringReference!!.string, i) - } else null - - return Match( + _matchOrNull = Match( context, context.lookupMaps.classDefsByType[method.definingClass]!!, method, instructionMatches, stringMatches, ) + + return _matchOrNull } /** @@ -265,124 +221,74 @@ class Fingerprint internal constructor( ): Match? { if (_matchOrNull != null) return _matchOrNull - if (returnType != null && !method.returnType.startsWith(returnType)) { - return null - } + var stringMatches: List? = null - if (accessFlags != null && accessFlags != method.accessFlags) { - return null - } + val matchIndices = indexedMatcher() - // TODO: parseParameters() - if (parameters != null && !parametersStartsWith(method.parameterTypes, parameters)) { - return null - } + context(_: MatchContext) + fun Method.match(): Boolean { + if (this@Fingerprint.accessFlags != null && this@Fingerprint.accessFlags != accessFlags) + return false - if (custom != null && !custom.invoke(method, classDef)) { - return null - } + if (this@Fingerprint.returnType != null && !returnType.startsWith(this@Fingerprint.returnType)) + return false - // Legacy string declarations. - val stringMatches: List? = if (strings == null) { - null - } else { - buildList { - val instructions = method.instructionsOrNull ?: return null + if (this@Fingerprint.parameters != null && !parametersStartsWith( + parameterTypes, + this@Fingerprint.parameters + ) + ) + return false + if (custom != null && !custom(this, classDef)) + return false + + stringMatches = if (strings != null) { + val instructions = instructionsOrNull ?: return false var stringsList: MutableList? = null - instructions.forEachIndexed { instructionIndex, instruction -> - if ( - instruction.opcode != Opcode.CONST_STRING && - instruction.opcode != Opcode.CONST_STRING_JUMBO - ) { - return@forEachIndexed + buildList { + instructions.forEachIndexed { instructionIndex, instruction -> + if (stringsList == null) stringsList = strings.toMutableList() + + val string = instruction.stringReference?.string ?: return@forEachIndexed + val index = stringsList.indexOfFirst(string::contains) + if (index < 0) return@forEachIndexed + + add(Match.StringMatch(string, instructionIndex)) + stringsList.removeAt(index) } - val string = ((instruction as ReferenceInstruction).reference as StringReference).string - if (stringsList == null) { - stringsList = strings.toMutableList() - } - val index = stringsList.indexOfFirst(string::contains) - if (index < 0) return@forEachIndexed - - add(Match.StringMatch(string, instructionIndex)) - stringsList.removeAt(index) + if (stringsList == null || stringsList.isNotEmpty()) return false } - if (stringsList == null || stringsList.isNotEmpty()) return null - } + } else null + + return filters == null || matchIndices.apply { + filters.forEach { filter -> + val filterMatches: Instruction.() -> Boolean = { filter.matches(method, this) } + + when (val location = filter.location) { + is MatchAfterImmediately -> after { filterMatches() } + is MatchAfterWithin -> after(1..location.matchDistance) { filterMatches() } + is MatchAfterAnywhere -> add { filterMatches() } + is MatchAfterAtLeast -> after(location.minimumDistanceFromLastInstruction..Int.MAX_VALUE) { filterMatches() } + is MatchAfterRange -> after(location.minimumDistanceFromLastInstruction..location.maximumDistanceFromLastInstruction) { filterMatches() } + is MatchFirst -> head { filterMatches() } + } + } + }(instructionsOrNull ?: return false) } - val instructionMatches = if (filters == null) { - null - } else { - val instructions = method.instructionsOrNull?.toList() ?: return null - - fun matchFilters(): List? { - val lastMethodIndex = instructions.lastIndex - var instructionMatches: MutableList? = null - - var firstInstructionIndex = 0 - var lastMatchIndex = -1 - - firstFilterLoop@ while (true) { - // Matched index of the first filter. - var firstFilterIndex = -1 - var subIndex = firstInstructionIndex - - for (filterIndex in filters.indices) { - val filter = filters[filterIndex] - val location = filter.location - var instructionsMatched = false - - while (subIndex <= lastMethodIndex && - location.indexIsValidForMatching( - lastMatchIndex, subIndex - ) - ) { - val instruction = instructions[subIndex] - if (filter.matches(method, instruction)) { - lastMatchIndex = subIndex - - if (filterIndex == 0) { - firstFilterIndex = subIndex - } - if (instructionMatches == null) { - instructionMatches = ArrayList(filters.size) - } - instructionMatches += Match.InstructionMatch(filter, subIndex, instruction) - instructionsMatched = true - subIndex++ - break - } - subIndex++ - } - - if (!instructionsMatched) { - if (filterIndex == 0) { - return null // First filter has no more matches to start from. - } - - // Try again with the first filter, starting from - // the next possible first filter index. - firstInstructionIndex = firstFilterIndex + 1 - instructionMatches?.clear() - continue@firstFilterLoop - } - } - - // All instruction filters matches. - return instructionMatches - } - } - - matchFilters() ?: return null + if (!context(MatchContext()) { method.match() }) return null + val instructionMatches = filters?.withIndex()?.map { (i, filter) -> + val matchIndex = matchIndices.indices[i] + Match.InstructionMatch(filter, matchIndex, method.getInstruction(matchIndex)) } _matchOrNull = Match( context, - classDef, + context.lookupMaps.classDefsByType[method.definingClass]!!, method, instructionMatches, stringMatches, @@ -600,11 +506,7 @@ class Match internal constructor( * Accessing this property allocates a new mutable instance. * Use [originalClassDef] if mutable access is not required. */ - val classDef by lazy { - with(context) { - originalClassDef.mutable() - } - } + val classDef by lazy { with(context) { originalClassDef.mutable() } } /** * The mutable version of [originalMethod]. diff --git a/src/main/kotlin/app/revanced/patcher/Matching.kt b/src/main/kotlin/app/revanced/patcher/Matching.kt index 107dba3..6b862ae 100644 --- a/src/main/kotlin/app/revanced/patcher/Matching.kt +++ b/src/main/kotlin/app/revanced/patcher/Matching.kt @@ -251,52 +251,101 @@ class IndexedMatcher() : Matcher): Boolean { // Normalize to list - val hayList = haystack as? List ?: haystack.toList() + val hay = haystack as? List ?: haystack.toList() _indices.clear() - lastMatchedIndex = -1 + this@IndexedMatcher.lastMatchedIndex = -1 currentIndex = -1 - for (predicate in this) { - var matched = false + data class Frame( + val patternIndex: Int, + val lastMatchedIndex: Int, + val previousFrame: Frame?, + var nextHayIndex: Int, + val matchedIndex: Int + ) - // Continue scanning from the position after the last successful match - for (i in (lastMatchedIndex + 1) until hayList.size) { - currentIndex = i - val element = hayList[i] + val stack = ArrayDeque() + stack.add( + Frame( + patternIndex = 0, + lastMatchedIndex = -1, + previousFrame = null, + nextHayIndex = 0, + matchedIndex = -1 + ) + ) - if (element.predicate(lastMatchedIndex, currentIndex)) { - _indices += i - lastMatchedIndex = i - matched = true - break - } + while (stack.isNotEmpty()) { + val frame = stack.last() + + if (frame.nextHayIndex >= hay.size || nextIndex == -1) { + stack.removeLast() + nextIndex = null + continue } - if (!matched) { - return false + val i = frame.nextHayIndex + currentIndex = i + lastMatchedIndex = frame.lastMatchedIndex + nextIndex = null + + if (this[frame.patternIndex](hay[i], lastMatchedIndex, currentIndex)) { + Frame( + patternIndex = frame.patternIndex + 1, + lastMatchedIndex = i, + previousFrame = frame, + nextHayIndex = i + 1, + matchedIndex = i + ).also { + if (it.patternIndex == size) { + _indices += buildList(size) { + var f: Frame? = it + while (f != null && f.matchedIndex != -1) { + add(f.matchedIndex) + f = f.previousFrame + } + }.asReversed() + + return true + } + }.let(stack::add) + } + + frame.nextHayIndex = when (val nextIndex = nextIndex) { + null -> frame.nextHayIndex + 1 + -1 -> 0 // Frame will be removed next loop. + else -> nextIndex } } - return true + return false } - fun first(predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) = + fun head(predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) = add { lastMatchedIndex, currentIndex -> currentIndex == 0 && predicate(lastMatchedIndex, currentIndex) } - fun first(predicate: T.() -> Boolean) = - first { _, _ -> predicate() } + fun head(predicate: T.() -> Boolean) = + head { _, _ -> predicate() } fun after(range: IntRange = 1..1, predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) = add { lastMatchedIndex, currentIndex -> - currentIndex - lastMatchedIndex in range && predicate(lastMatchedIndex, currentIndex) + val distance = currentIndex - lastMatchedIndex + + nextIndex = when { + distance < range.first -> lastMatchedIndex + range.first + distance > range.last -> -1 + else -> return@add predicate(lastMatchedIndex, currentIndex) + } + + false } fun after(range: IntRange = 1..1, predicate: T.() -> Boolean) = diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt index 4021b9c..b1727de 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -156,6 +156,8 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi PatcherResult.PatchedDexFile(it.name, it.inputStream()) }.toSet() + // Free up more memory, although it is unclear if this is actually helpful. + classDefs.clear() System.gc() return patchedDexFileResults @@ -163,7 +165,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi override fun close() { try { - classDefs.clear() _lookupMaps = null } catch (e: IOException) { logger.warning("Failed to clear BytecodePatchContext: ${e.message}") diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt index 17ddba9..64f57e9 100644 --- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -170,14 +170,14 @@ internal object PatcherTest { val iterable = (1..10).toList() val matcher = indexedMatcher() - matcher.apply { first { this > 5 } } + matcher.apply { head { this > 5 } } assertFalse( matcher(iterable), "Should not match at any other index than first" ) matcher.clear() - matcher.apply { first { this == 1 } }(iterable) + matcher.apply { head { this == 1 } }(iterable) assertEquals( listOf(0), matcher.indices, @@ -198,7 +198,7 @@ internal object PatcherTest { matcher.clear() matcher.apply { - first { this == 1 } + head { this == 1 } add { this == 2 } add { this == 4 } }(iterable) @@ -238,8 +238,8 @@ internal object PatcherTest { matcher.clear() matcher.apply { - first { this == 1 } - after(2..5) { this == 4} + head { this == 1 } + after(2..5) { this == 4 } add { this == 8 } add { this == 9 } }(iterable) @@ -299,7 +299,7 @@ internal object PatcherTest { val method by gettingFirstMethod { implementation { matchIndices(instructions, "match") { - first { opcode == Opcode.CONST_STRING } + head { opcode == Opcode.CONST_STRING } add { opcode == Opcode.IPUT_OBJECT } } }