merge matching and tests module with patcher, make builder api context aware and refactor

This commit is contained in:
oSumAtrIX
2025-12-30 00:02:16 +01:00
parent 005c91bc08
commit 18570656cc
13 changed files with 71 additions and 195 deletions

View File

@@ -1,85 +0,0 @@
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.vanniktech.mavenPublish)
}
group = "app.revanced"
kotlin {
@OptIn(ExperimentalAbiValidation::class)
abiValidation {
enabled = true
}
jvm()
sourceSets {
commonMain.dependencies {
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(project(":patcher"))
}
jvmTest.dependencies {
implementation(libs.mockk)
implementation(libs.kotlin.test)
implementation(project(":tests"))
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexplicit-backing-fields",
"-Xcontext-parameters"
)
}
}
tasks {
named<Test>("jvmTest") {
useJUnitPlatform()
}
}
mavenPublishing {
publishing {
repositories {
maven {
name = "githubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials(PasswordCredentials::class)
}
}
}
signAllPublications()
extensions.getByType<SigningExtension>().useGpgCmd()
coordinates(group.toString(), project.name, version.toString())
pom {
name = "ReVanced Patcher Matching API"
description = "Matching API used by ReVanced."
inceptionYear = "2022"
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
url = "https://github.com/revanced/revanced-patcher"
}
}
}

View File

@@ -50,12 +50,14 @@ kotlin {
jvmTest.dependencies {
implementation(libs.mockk)
implementation(libs.kotlin.test)
implementation(project(":tests"))
}
}
compilerOptions {
freeCompilerArgs = listOf("-Xcontext-parameters")
freeCompilerArgs.addAll(
"-Xexplicit-backing-fields",
"-Xcontext-parameters"
)
}
}

View File

@@ -25,14 +25,14 @@ class Fingerprint internal constructor(
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
private var _matchOrNull: FingerprintMatch? = null
context(_: BytecodePatchContext)
private val matchOrNull: Match?
private val matchOrNull: FingerprintMatch?
get() = matchOrNull()
context(context: BytecodePatchContext)
internal fun matchOrNull(): Match? {
internal fun matchOrNull(): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
var match = strings?.mapNotNull {
@@ -57,7 +57,7 @@ class Fingerprint internal constructor(
context(_: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): Match? {
): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
@@ -77,7 +77,7 @@ class Fingerprint internal constructor(
fun matchOrNull(
method: Method,
classDef: ClassDef,
): Match? {
): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
@@ -109,7 +109,7 @@ class Fingerprint internal constructor(
return null
}
val stringMatches: List<Match.StringMatch>? =
val stringMatches: List<FingerprintMatch.StringMatch>? =
if (strings != null) {
buildList {
val instructions = method.instructionsOrNull ?: return null
@@ -128,7 +128,7 @@ class Fingerprint internal constructor(
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(Match.StringMatch(string, instructionIndex))
add(FingerprintMatch.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
@@ -141,7 +141,7 @@ class Fingerprint internal constructor(
val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return null
fun patternScan(): Match.PatternMatch? {
fun patternScan(): FingerprintMatch.PatternMatch? {
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
val instructionLength = instructions.count()
@@ -168,7 +168,7 @@ class Fingerprint internal constructor(
}
// The entire pattern has been scanned.
return Match.PatternMatch(
return FingerprintMatch.PatternMatch(
index,
index + patternIndex,
)
@@ -183,7 +183,7 @@ class Fingerprint internal constructor(
null
}
_matchOrNull = Match(
_matchOrNull = FingerprintMatch(
context,
classDef,
method,
@@ -266,7 +266,7 @@ class Fingerprint internal constructor(
}
@Deprecated("Use the matcher API instead.")
class Match internal constructor(
class FingerprintMatch internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
val originalMethod: Method,

View File

@@ -97,12 +97,12 @@ fun BytecodePatchContext.firstMethodOrNull(
fun BytecodePatchContext.firstMethod(
vararg strings: String,
predicate: MethodPredicate = { true },
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
) = requireNotNull(firstMethodOrNull(strings = strings, predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(
vararg strings: String,
predicate: MethodPredicate = { true },
) = firstMethodOrNull(*strings, predicate = predicate)?.let { method ->
) = firstMethodOrNull(strings = strings, predicate)?.let { method ->
firstClassDefMutable(method.definingClass).methods.first {
MethodUtil.methodSignaturesMatch(method, it)
}
@@ -110,7 +110,7 @@ fun BytecodePatchContext.firstMethodMutableOrNull(
fun BytecodePatchContext.firstMethodMutable(
vararg strings: String, predicate: MethodPredicate = { true }
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
) = requireNotNull(firstMethodMutableOrNull(strings = strings, predicate))
fun gettingFirstClassDefOrNull(
type: String? = null, predicate: ClassDefPredicate = { true }
@@ -131,22 +131,22 @@ fun gettingFirstClassDefMutable(
fun gettingFirstMethodOrNull(
vararg strings: String,
predicate: MethodPredicate = { true },
) = cachedReadOnlyProperty { firstMethodOrNull(*strings, predicate = predicate) }
) = cachedReadOnlyProperty { firstMethodOrNull(strings = strings, predicate) }
fun gettingFirstMethod(
vararg strings: String,
predicate: MethodPredicate = { true },
) = cachedReadOnlyProperty { firstMethod(*strings, predicate = predicate) }
) = cachedReadOnlyProperty { firstMethod(strings = strings, predicate) }
fun gettingFirstMethodMutableOrNull(
vararg strings: String,
predicate: MethodPredicate = { true },
) = cachedReadOnlyProperty { firstMethodMutableOrNull(*strings, predicate = predicate) }
) = cachedReadOnlyProperty { firstMethodMutableOrNull(strings = strings, predicate) }
fun gettingFirstMethodMutable(
vararg strings: String,
predicate: MethodPredicate = { true },
) = cachedReadOnlyProperty { firstMethodMutable(*strings, predicate = predicate) }
) = cachedReadOnlyProperty { firstMethodMutable(strings = strings, predicate) }
class PredicateContext internal constructor() : MutableMap<Any, Any> by mutableMapOf()
@@ -373,22 +373,22 @@ fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = firstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
) = firstMethodOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(strings = strings, predicate))
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = firstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
) = firstMethodMutableOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(strings = strings, predicate))
fun gettingFirstClassDefByDeclarativePredicateOrNull(
type: String? = null,
@@ -413,22 +413,22 @@ fun gettingFirstClassDefMutableByDeclarativePredicate(
fun gettingFirstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = gettingFirstMethodOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
) = gettingFirstMethodOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = cachedReadOnlyProperty { firstMethodByDeclarativePredicate(*strings, predicate = predicate) }
) = cachedReadOnlyProperty { firstMethodByDeclarativePredicate(strings = strings, predicate) }
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = gettingFirstMethodMutableOrNull(*strings) { rememberedDeclarativePredicate(predicate) }
) = gettingFirstMethodMutableOrNull(strings = strings) { rememberedDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: DeclarativeMethodPredicate = { }
) = cachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(*strings, predicate = predicate) }
) = cachedReadOnlyProperty { firstMethodMutableByDeclarativePredicate(strings = strings, predicate) }
context(list: MutableList<T.() -> Boolean>)
fun <T> allOf(block: MutableList<T.() -> Boolean>.() -> Unit) {
@@ -453,21 +453,9 @@ fun <T> all(target: T): Boolean = list.all { target.it() }
context(list: MutableList<T.() -> Boolean>)
fun <T> any(target: T): Boolean = list.any { target.it() }
fun firstMethodBuilder(
vararg strings: String,
builder:
context(PredicateContext, MutableList<Method.() -> Boolean>, IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
) = with(mutableListOf<String>()) stringsList@{
addAll(strings)
with(indexedMatcher<Instruction>()) {
Match(indices = indices, strings = this@stringsList) { builder() }
}
}
context(_: MutableList<Method.() -> Boolean>)
fun accessFlags(vararg flags: AccessFlags) =
predicate { accessFlags(*flags) }
predicate { accessFlags(flags = flags) }
context(_: MutableList<Method.() -> Boolean>)
fun returnType(
@@ -624,22 +612,39 @@ fun noneOf(
predicates.none { predicate -> predicate(currentIndex, lastMatchedIndex) }
}
class Match internal constructor(
val indices: List<Int>,
val strings: List<String>,
private val predicate: DeclarativeMethodPredicate
fun firstMethodBuilder(
vararg strings: String,
builder:
context(PredicateContext, MutableList<Method.() -> Boolean>, IndexedMatcher<Instruction>, MutableList<String>)() -> Unit
) = Match(strings = strings, builder)
class Match private constructor(
private val strings: MutableList<String>,
indexedMatcher: IndexedMatcher<Instruction> = indexedMatcher(),
build: context(
PredicateContext, MutableList<Method.() -> Boolean>,
IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
) {
private var _methodOrNull: MutableMethod? = null
internal constructor(
vararg strings: String,
builder: context(
PredicateContext, MutableList<Method.() -> Boolean>,
IndexedMatcher<Instruction>, MutableList<String>) () -> Unit
) : this(strings = mutableListOf(elements = strings), build = builder)
private val methodOrNullMap = HashMap<BytecodePatchContext, MutableMethod?>(1)
private val predicate: DeclarativeMethodPredicate = context(strings, indexedMatcher) { { build() } }
context(context: BytecodePatchContext)
val methodOrNull: MutableMethod?
get() = _methodOrNull ?: run {
_methodOrNull = if (strings.isEmpty())
context.firstMethodMutableByDeclarativePredicateOrNull(predicate = predicate)
else
context.firstMethodMutableByDeclarativePredicateOrNull(strings = strings.toTypedArray(), predicate)
_methodOrNull
val methodOrNull: MutableMethod?
get() = if (context in methodOrNullMap) methodOrNullMap[context]
else methodOrNullMap.getOrPut(context) {
context.firstMethodMutableByDeclarativePredicateOrNull(
strings = strings.toTypedArray(),
predicate
)
}
context(_: BytecodePatchContext)
@@ -650,4 +655,6 @@ class Match internal constructor(
context(_: BytecodePatchContext)
val classDef get() = requireNotNull(classDefOrNull)
val indices = indexedMatcher.indices
}

View File

@@ -236,7 +236,7 @@ by patchesByFile.values.flatten().toSet()
@Suppress("MISSING_DEPENDENCY_IN_INFERRED_TYPE_ANNOTATION_WARNING")
internal fun getPatches(classNames: List<String>, classLoader: ClassLoader): Set<Patch> {
fun Member.isUsable() =
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount != 0)
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount == 0)
fun Class<*>.getPatchFields() = fields
.filter { it.type.isPatch && it.isUsable() }

View File

@@ -19,7 +19,7 @@ internal class FingerprintTest : PatcherTestBase() {
"Fingerprints should match correctly."
)
assertNull(
fingerprint { returns("doesnt exist") }.originalMethodOrNull,
fingerprint { returns("does not exist") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
}

View File

@@ -18,7 +18,7 @@ object MatchingTest : PatcherTestBase() {
fun setup() = setupMock()
@Test
fun `matches via builder api`() {
fun `finds via builder api`() {
fun firstMethodBuilder(fail: Boolean = false) = firstMethodBuilder {
name("method")
definingClass("class")
@@ -45,7 +45,7 @@ object MatchingTest : PatcherTestBase() {
}
@Test
fun `matches via declarative api`() {
fun `finds via declarative api`() {
bytecodePatch {
apply {
val method = firstMethodByDeclarativePredicateOrNull {
@@ -64,7 +64,7 @@ object MatchingTest : PatcherTestBase() {
}
@Test
fun `predicate matcher works correctly`() {
fun `predicate api works correctly`() {
bytecodePatch {
apply {
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }

View File

@@ -77,15 +77,4 @@ internal class PatcherTest : PatcherTestBase() {
"afterDependents of a patch should be called " +
"regardless of dependant patches failing."
}
@Test
fun `throws if unmatched fingerprint match is used`() {
with(bytecodePatchContext) {
val fingerprint = fingerprint { strings("doesnt exist") }
assertThrows<PatchException>("Expected an exception because the fingerprint can't match.") {
fingerprint.patternMatch
}
}
}
}

View File

@@ -78,7 +78,9 @@ abstract class PatcherTestBase {
every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk<File>()
every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {
invokePrivateMethod($$"initializeCache$patcher")
javaClass.getDeclaredMethod($$"initializeCache$patcher").apply {
isAccessible = true
}.invoke(this)
}
every { get() } returns emptySet()
@@ -100,16 +102,4 @@ abstract class PatcherTestBase {
}
protected operator fun Patch.invoke() = setOf(this)()
private fun Any.setPrivateField(field: String, value: Any) {
this::class.java.getDeclaredField(field).apply {
this.isAccessible = true
set(this@setPrivateField, value)
}
}
private fun Any.invokePrivateMethod(method: String) =
javaClass.getDeclaredMethod(method).apply {
isAccessible = true
}.invoke(this)
}

View File

@@ -1,6 +1,5 @@
package app.revanced.patcher.patch
import app.revanced.patcher.patch.PatchBuilder.invoke
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.reflect.typeOf

View File

@@ -41,7 +41,7 @@ internal class SmaliTest {
""",
)
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(0).target.location.index
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(1).target.location.index
assertEquals(0, targetLocationIndex, "Label should point to index 0")
}

View File

@@ -22,5 +22,3 @@ dependencyResolutionManagement {
}
include(":patcher")
include(":matching")
include(":tests")

View File

@@ -1,24 +0,0 @@
import com.android.build.api.dsl.androidLibrary
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
group = "app.revanced"
kotlin {
jvm()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(project(":patcher"))
implementation(libs.mockk)
implementation(libs.kotlin.test)
}
}
}