diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 03a9c2a..93e58e4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ apktool-lib = "2.10.1.1" binary-compatibility-validator = "0.18.1" kotlin = "2.2.21" kotlinx-coroutines-core = "1.10.2" -mockk = "1.14.5" +mockk = "1.14.6" multidexlib2 = "3.0.3.r3" # Tracking https://github.com/google/smali/issues/64. #noinspection GradleDependency diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 68e8816..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt index f87183b..aae1a28 100644 --- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -82,15 +82,11 @@ class Fingerprint internal constructor( if (this@Fingerprint.returnType != null && this@Fingerprint.returnType != returnType) return false - if (this@Fingerprint.parameters != null && parametersStartsWith( - this@Fingerprint.parameters, - parameters - ) - ) return false - - if (custom != null && !custom.invoke(this, context.lookupMaps.classDefsByType[definingClass]!!)) + 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 @@ -98,34 +94,22 @@ class Fingerprint internal constructor( fun InstructionFilter.evaluate(instruction: Instruction): Boolean { return when (this) { is AnyInstruction -> filters.any { evaluate(instruction) } - is CheckCastFilter -> { - val type = type() - - instruction.opcode(Opcode.CHECK_CAST) && - instruction.reference { endsWith(type) } - } + is CheckCastFilter -> instruction.opcode(Opcode.CHECK_CAST) && instruction.typeReference?.endsWith( + typeValue + ) ?: false is FieldAccessFilter -> { val reference = instruction.fieldReference ?: return false if (name != null && reference.name != name) return false - if (type != null && !reference.type.startsWith(type)) return false - if (definingClass != null) { - if (!reference.definingClass.endsWith(definingClass)) - // else, the method call is for 'this' class. - if (!(definingClass == "this" && reference.definingClass == method.definingClass)) return false - } - - true + definingClass == null || reference.definingClass.endsWith(definingClass) || + (definingClass == "this" && reference.definingClass != method.definingClass) } - is LiteralFilter -> { - instruction.wideLiteral?.equals(literal()) ?: return false - - opcodes != null && !opcodes.contains(instruction.opcode) - } + is LiteralFilter -> instruction.wideLiteral == literalValue + && opcodes?.contains(instruction.opcode) ?: true is MethodCallFilter -> { val reference = instruction.methodReference ?: return false @@ -134,25 +118,14 @@ class Fingerprint internal constructor( if (returnType != null && !reference.returnType.startsWith(returnType)) return false - if (parameters != null && !parametersStartsWith( - reference.parameterTypes, - parameters - ) + if (parameters != null && !parametersStartsWith(reference.parameterTypes, parameters)) + return false + + if ((definingClass != null && !reference.definingClass.endsWith(definingClass)) || + (definingClass == "this" && reference.definingClass != method.definingClass) ) return false - if (definingClass != null) { - if (!reference.definingClass.endsWith(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" && reference.definingClass == method.definingClass)) { - return false - } // else, the method call is for 'this' class. - } - } - - opcodes != null && !opcodes.contains(instruction.opcode) + opcodes?.contains(instruction.opcode) ?: true } is NewInstanceFilter -> { @@ -182,19 +155,15 @@ class Fingerprint internal constructor( 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 MatchAfterWithin -> after(atLeast = 1, atMost = location.matchDistance) { - filter.evaluate(this) - } - is MatchAfterAtLeast -> after( - atLeast = location.minimumDistanceFromLastInstruction, - atMost = Int.MAX_VALUE + location.minimumDistanceFromLastInstruction..Int.MAX_VALUE ) { filter.evaluate(this) } is MatchAfterRange -> after( - atLeast = location.minimumDistanceFromLastInstruction, - atMost = location.maximumDistanceFromLastInstruction + location.minimumDistanceFromLastInstruction.. + location.maximumDistanceFromLastInstruction ) { filter.evaluate(this) } is MatchFirst -> first { filter.evaluate(this) } diff --git a/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt b/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt index 99ce4a8..e54bd12 100644 --- a/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt +++ b/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt @@ -371,7 +371,7 @@ class LiteralFilter internal constructor( /** * Store the lambda value instead of calling it more than once. */ - private val literalValue: Long by lazy(literal) + internal val literalValue: Long by lazy(literal) override fun matches( enclosingMethod: Method, @@ -1005,7 +1005,7 @@ class CheckCastFilter internal constructor( /** * Store the lambda value instead of calling it more than once. */ - private val typeValue: String by lazy { + internal val typeValue: String by lazy { val typeValue = type() comparison.validateSearchStringForClassType(typeValue) typeValue diff --git a/src/main/kotlin/app/revanced/patcher/Matching.kt b/src/main/kotlin/app/revanced/patcher/Matching.kt index 68b5e60..107dba3 100644 --- a/src/main/kotlin/app/revanced/patcher/Matching.kt +++ b/src/main/kotlin/app/revanced/patcher/Matching.kt @@ -4,11 +4,8 @@ package app.revanced.patcher import app.revanced.patcher.Matcher.MatchContext import app.revanced.patcher.dex.mutable.MutableMethod -import app.revanced.patcher.extensions.* import app.revanced.patcher.patch.BytecodePatchContext -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.HiddenApiRestriction -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.Instruction @@ -248,129 +245,62 @@ context(context: MatchContext) inline fun remember(key: Any, defaultValue: () -> V) = context[key] as? V ?: defaultValue().also { context[key] = it } -class IndexedMatcher() : Matcher Boolean>() { +class IndexedMatcher() : Matcher Boolean>() { private val _indices: MutableList = mutableListOf() val indices: List = _indices private var lastMatchedIndex = -1 private var currentIndex = -1 + // TODO: Hint to stop searching for performance: private var stop = false + // Also make the APIs advance indices (e.g. atLeast, atMost) for performance. override fun invoke(haystack: Iterable): Boolean { - // Defensive, in case haystack is not a list. + // Normalize to list val hayList = haystack as? List ?: haystack.toList() _indices.clear() + lastMatchedIndex = -1 + currentIndex = -1 - var firstNeedleIndex = 0 + for (predicate in this) { + var matched = false - while (firstNeedleIndex <= hayList.lastIndex) { - lastMatchedIndex = -1 + // 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 tempIndices = mutableListOf() - - var matchedAll = true - var subIndex = firstNeedleIndex - - for (predicateIndex in _indices.indices) { - var predicateMatched = false - - while (subIndex <= hayList.lastIndex) { - currentIndex = subIndex - val element = hayList[subIndex] - if (this[predicateIndex](element)) { - tempIndices += subIndex - lastMatchedIndex = subIndex - predicateMatched = true - subIndex++ - break - } - subIndex++ - } - - if (!predicateMatched) { - // Restart from next possible first match - firstNeedleIndex = if (tempIndices.isNotEmpty()) tempIndices[0] + 1 else firstNeedleIndex + 1 - matchedAll = false + if (element.predicate(lastMatchedIndex, currentIndex)) { + _indices += i + lastMatchedIndex = i + matched = true break } } - if (matchedAll) { - _indices += tempIndices - return true + if (!matched) { + return false } } - return false + return true } - - fun first(predicate: T.() -> Boolean) = add { - if (lastMatchedIndex != -1) false - else predicate() - } - - fun after(atLeast: Int = 1, atMost: Int = 1, predicate: T.() -> Boolean) = add { - val distance = currentIndex - lastMatchedIndex - if (distance in atLeast..atMost) predicate() else false - } -} - -fun BytecodePatchContext.matchers() { - val customMatcher = object : Matcher Boolean>() { - override fun invoke(haystack: Iterable) = true - }.apply { add { true } } - - // Probably gonna make this ctor internal. - IndexedMatcher().apply { add { true } } - // Since there is a function for it. - val m = indexedMatcher() - m.apply { add { true } } - - // You can directly use the extension function to match. - listOf().matchIndexed { add { true } } - - // Inside a match context extension functions are cacheable: - firstMethod { instructions.matchIndexed("key") { add { true } } } - - // Or create a matcher outside a context and use it in a MatchContext as follows: - val match = indexedMatcher() - firstMethod { match(instructions, "anotherKey") { first { opcode(Opcode.RETURN_VOID) } } } - match.indices // so that you can access the matched indices later. -} - -fun BytecodePatchContext.a() { - val matcher = indexedMatcher() - - firstMethodMutableOrNull("string to find in lookupmap") { - returnType == "L" && matcher(instructions, "key") { - first { - // The first instruction is a field reference to a field in the class of the method being matched. - // We cache the field name using remember to avoid redundant lookups. - fieldReference!!.name == remember("fieldName") { - firstClassDef(definingClass).fields.first().name - } - } - after { fieldReference("fieldName") } - after { - opcode(Opcode.NEW_INSTANCE) && methodReference { toString() == "Lcom/example/MyClass;" } - } - // Followed by 2 to 4 string instructions starting with "test". - after(atLeast = 1, atMost = 2) { string { startsWith("test") } } - } && parameterTypes.matchIndexed("params") { - // Fully dynamic environment to customize depending on your needs. - operator fun String.plus(other: String) { - after { this == this@plus } - after { this == other } - } - - "L" + "I" + "Z" // Matches parameter types "L", "I", "Z" in order. + fun first(predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) = + add { lastMatchedIndex, currentIndex -> + currentIndex == 0 && predicate(lastMatchedIndex, currentIndex) } - } - firstMethodMutable { - accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) && instructions.matchIndexed("anotherKey") { - first { opcode(Opcode.RETURN_VOID) } + fun first(predicate: T.() -> Boolean) = + first { _, _ -> predicate() } + + fun after(range: IntRange = 1..1, predicate: T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean) = + add { lastMatchedIndex, currentIndex -> + currentIndex - lastMatchedIndex in range && predicate(lastMatchedIndex, currentIndex) } - } + + fun after(range: IntRange = 1..1, predicate: T.() -> Boolean) = + after(range) { _, _ -> predicate() } + + fun add(predicate: T.() -> Boolean) = add { _, _ -> predicate() } } diff --git a/src/main/kotlin/app/revanced/patcher/extensions/Instruction.kt b/src/main/kotlin/app/revanced/patcher/extensions/Instruction.kt index 46dcc30..4c6034f 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/Instruction.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/Instruction.kt @@ -8,12 +8,10 @@ import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction import com.android.tools.smali.dexlib2.iface.reference.* import com.android.tools.smali.dexlib2.util.MethodUtil -@Suppress("UNCHECKED_CAST") -fun Instruction.reference(predicate: T.() -> Boolean) = +inline fun Instruction.reference(predicate: T.() -> Boolean) = ((this as? ReferenceInstruction)?.reference as? T)?.predicate() ?: false -@Suppress("UNCHECKED_CAST") -fun Instruction.reference2(predicate: T.() -> Boolean) = +inline fun Instruction.reference2(predicate: T.() -> Boolean) = ((this as? DualReferenceInstruction)?.reference2 as? T)?.predicate() ?: false fun Instruction.methodReference(predicate: MethodReference.() -> Boolean) = diff --git a/src/main/kotlin/app/revanced/patcher/util/Smali.kt b/src/main/kotlin/app/revanced/patcher/util/Smali.kt index 2d1d2fa..2dd4c05 100644 --- a/src/main/kotlin/app/revanced/patcher/util/Smali.kt +++ b/src/main/kotlin/app/revanced/patcher/util/Smali.kt @@ -17,8 +17,6 @@ import java.io.StringReader private const val CLASS_HEADER = ".class LInlineCompiler;\n.super Ljava/lang/Object;\n" private const val STATIC_HEADER = "$CLASS_HEADER.method public static dummyMethod(" private const val HEADER = "$CLASS_HEADER.method public dummyMethod(" - -private val dexBuilder = DexBuilder(Opcodes.getDefault()) private val sb by lazy { StringBuilder(512) } /** @@ -59,7 +57,7 @@ fun String.toInstructions(templateMethod: MutableMethod? = null): List() + + matcher.apply { first { this > 5 } } + assertFalse( + matcher(iterable), + "Should not match at any other index than first" + ) + matcher.clear() + + matcher.apply { first { this == 1 } }(iterable) + assertEquals( + listOf(0), + matcher.indices, + "Should match at first index." + ) + matcher.clear() + + matcher.apply { add { this > 0 } }(iterable) + assertEquals(1, matcher.indices.size, "Should only match once.") + matcher.clear() + + matcher.apply { add { this == 2 } }(iterable) + assertEquals( + listOf(1), + matcher.indices, + "Should find the index correctly." + ) + matcher.clear() + + matcher.apply { + first { this == 1 } + add { this == 2 } + add { this == 4 } + }(iterable) + assertEquals( + listOf(0, 1, 3), + matcher.indices, + "Should match 1, 2 and 4 at indices 0, 1 and 3." + ) + matcher.clear() + + matcher.apply { + after { this == 1 } + }(iterable) + assertEquals( + listOf(0), + matcher.indices, + "Should match index 0 after nothing" + ) + matcher.clear() + + matcher.apply { + after(2..Int.MAX_VALUE) { this == 1 } + } + assertFalse( + matcher(iterable), + "Should not match, because 1 is out of range" + ) + matcher.clear() + + matcher.apply { + after(1..1) { this == 2 } + } + assertFalse( + matcher(iterable), + "Should not match, because 2 is at index 1" + ) + matcher.clear() + + matcher.apply { + first { this == 1 } + after(2..5) { this == 4} + add { this == 8 } + add { this == 9 } + }(iterable) + assertEquals( + listOf(0, 3, 7, 8), + matcher.indices, + "Should match indices correctly." + ) + } + @Test fun `matches fingerprint`() { every { patcher.context.bytecodeContext.classDefs } returns mutableSetOf( @@ -182,7 +270,22 @@ internal object PatcherTest { 0, null, null, - null, + ImmutableMethodImplementation( + 2, + """ + const-string v0, "Hello, World!" + iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream; + iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream; + return-void + const-string v0, "This is a test." + return-object v0 + invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V + invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J + check-cast p0, Ljava/io/PrintStream; + """.toInstructions(), + null, + null + ), ), ), ), @@ -192,12 +295,23 @@ internal object PatcherTest { val fingerprint2 = fingerprint { returns("V") } val fingerprint3 = fingerprint { returns("V") } + val matchIndices = indexedMatcher() + val method by gettingFirstMethod { + implementation { + matchIndices(instructions, "match") { + first { opcode == Opcode.CONST_STRING } + add { opcode == Opcode.IPUT_OBJECT } + } + } + } + val patches = setOf( bytecodePatch { execute { fingerprint.match(classDefs.first().methods.first()) fingerprint2.match(classDefs.first()) fingerprint3.originalClassDef + println(method) } }, ) @@ -216,7 +330,7 @@ internal object PatcherTest { private operator fun Set>.invoke(): List { every { patcher.context.executablePatches } returns toMutableSet() - every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classDefs) + every { patcher.context.bytecodeContext.lookupMaps } returns with(patcher.context.bytecodeContext) { LookupMaps() } every { with(patcher.context.bytecodeContext) { mergeExtension(any()) } } just runs return runBlocking { patcher().toList() } diff --git a/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt b/src/test/kotlin/app/revanced/patcher/util/SmaliTest.kt similarity index 96% rename from src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt rename to src/test/kotlin/app/revanced/patcher/util/SmaliTest.kt index c4d3f3e..acd88f0 100644 --- a/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt +++ b/src/test/kotlin/app/revanced/patcher/util/SmaliTest.kt @@ -1,8 +1,7 @@ -package app.revanced.patcher.util.smali +package app.revanced.patcher.util import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable import app.revanced.patcher.extensions.* -import app.revanced.patcher.util.toInstructions import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.BuilderInstruction @@ -16,7 +15,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -internal object InlineSmaliCompilerTest { +internal object SmaliTest { @Test fun `outputs valid instruction`() { val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction @@ -102,4 +101,4 @@ internal object InlineSmaliCompilerTest { assertEquals(want.format, have.format) assertEquals(want.codeUnits, have.codeUnits) } -} +} \ No newline at end of file