fix: Couple more matching fixes

This commit is contained in:
oSumAtrIX
2025-11-26 10:25:19 +01:00
parent fcdaf324fe
commit 14f2eb69e4
10 changed files with 187 additions and 179 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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<TypeReference> { 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) }

View File

@@ -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

View File

@@ -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 <reified V : Any> remember(key: Any, defaultValue: () -> V) =
context[key] as? V ?: defaultValue().also { context[key] = it }
class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
class IndexedMatcher<T>() : Matcher<T, T.(lastMatchedIndex: Int, currentIndex: Int) -> Boolean>() {
private val _indices: MutableList<Int> = mutableListOf()
val indices: List<Int> = _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<T>): Boolean {
// Defensive, in case haystack is not a list.
// Normalize to list
val hayList = haystack as? List<T> ?: 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<Int>()
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<Instruction, Instruction.() -> Boolean>() {
override fun invoke(haystack: Iterable<Instruction>) = true
}.apply { add { true } }
// Probably gonna make this ctor internal.
IndexedMatcher<Instruction>().apply { add { true } }
// Since there is a function for it.
val m = indexedMatcher<Instruction>()
m.apply { add { true } }
// You can directly use the extension function to match.
listOf<Instruction>().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<Instruction>()
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<Instruction>()
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() }
}

View File

@@ -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 <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
inline fun <reified T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
((this as? ReferenceInstruction)?.reference as? T)?.predicate() ?: false
@Suppress("UNCHECKED_CAST")
fun <T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
inline fun <reified T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
((this as? DualReferenceInstruction)?.reference2 as? T)?.predicate() ?: false
fun Instruction.methodReference(predicate: MethodReference.() -> Boolean) =

View File

@@ -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<BuilderIn
}
val walker = smaliTreeWalker(treeStream)
walker.setDexBuilder(dexBuilder)
walker.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = walker.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }

View File

@@ -1,7 +1,8 @@
package app.revanced.patcher.extensions
package app.revanced.patcher
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction

View File

@@ -1,19 +1,22 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.toInstructions
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.*
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll
import java.util.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.*
internal object PatcherTest {
private lateinit var patcher: Patcher
@@ -162,6 +165,91 @@ internal object PatcherTest {
)
}
@Test
fun `matcher finds indices correctly`() {
val iterable = (1..10).toList()
val matcher = indexedMatcher<Int>()
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<Instruction>()
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<Patch<*>>.invoke(): List<PatchResult> {
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<BytecodePatch>()) } } just runs
return runBlocking { patcher().toList() }

View File

@@ -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)
}
}
}