feat: Composition API

This commit is contained in:
oSumAtrIX
2025-11-28 16:07:31 +01:00
parent e6eaf6cb73
commit 4106ce4070
8 changed files with 336 additions and 92 deletions

View File

@@ -48,7 +48,6 @@ class Fingerprint internal constructor(
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
@@ -117,7 +116,7 @@ class Fingerprint internal constructor(
} else null
currentMethod = this
return filters == null || matchIndices(instructionsOrNull ?: return false, "match") {
return filters == null || matchIndices(instructionsOrNull ?: return false) {
filters.forEach { filter ->
val filterMatches: Instruction.() -> Boolean = { filter.matches(currentMethod, this) }
@@ -264,7 +263,7 @@ class Fingerprint internal constructor(
} else null
return filters == null || matchIndices.apply {
return filters == null || matchIndices(instructionsOrNull ?: return false) {
filters.forEach { filter ->
val filterMatches: Instruction.() -> Boolean = { filter.matches(method, this) }
@@ -277,7 +276,7 @@ class Fingerprint internal constructor(
is MatchFirst -> head { filterMatches() }
}
}
}(instructionsOrNull ?: return false)
}
}
if (!context(MatchContext()) { method.match() }) return null

View File

@@ -4,8 +4,12 @@ package app.revanced.patcher
import app.revanced.patcher.Matcher.MatchContext
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.extensions.accessFlags
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.gettingBytecodePatch
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
@@ -217,7 +221,6 @@ fun gettingFirstMethodMutable(
fun <T> indexedMatcher() = IndexedMatcher<T>()
// Add lambda to emit instructions if matched (or matched arg)
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
IndexedMatcher<T>().apply(build)
@@ -234,7 +237,7 @@ operator fun <T> IndexedMatcher<T>.invoke(key: Any, iterable: Iterable<T>, build
context(_: MatchContext)
operator fun <T> IndexedMatcher<T>.invoke(iterable: Iterable<T>, builder: IndexedMatcher<T>.() -> Unit) =
invoke(hashCode(), iterable, builder)
invoke(this@invoke.hashCode(), iterable, builder)
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
var matchIndex = -1
@@ -357,3 +360,245 @@ class IndexedMatcher<T>() : Matcher<T, T.(lastMatchedIndex: Int, currentIndex: I
fun add(predicate: T.() -> Boolean) = add { _, _ -> predicate() }
}
class DeclarativePredicateBuilder<T> {
private val children = mutableListOf<T.() -> Boolean>()
fun anyOf(block: DeclarativePredicateBuilder<T>.() -> Unit) {
val child = DeclarativePredicateBuilder<T>().apply(block)
children += { child.children.any { it() } }
}
fun predicate(block: T.() -> Boolean) {
children += block
}
fun all(target: T): Boolean = children.all { target.it() }
fun any(target: T): Boolean = children.all { target.it() }
}
fun <T> T.declarativePredicate(build: DeclarativePredicateBuilder<T>.() -> Unit) =
DeclarativePredicateBuilder<T>().apply(build).all(this)
context(_: MatchContext)
fun <T> T.rememberDeclarativePredicate(key: Any, block: DeclarativePredicateBuilder<T>.() -> Unit): Boolean =
remember(key) { DeclarativePredicateBuilder<T>().apply(block) }.all(this)
context(_: MatchContext)
private fun <T> T.rememberDeclarativePredicate(predicate: context(MatchContext, T) DeclarativePredicateBuilder<T>.() -> Unit) =
rememberDeclarativePredicate("declarative predicate build") { predicate() }
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefOrNull { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefMutableOrNull { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(predicate))
fun BytecodePatchContext.firstClassDefByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefOrNull(type) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefByDeclarativePredicateOrNull(type, predicate))
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = firstClassDefMutableOrNull(type) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstClassDefMutableByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(firstClassDefMutableByDeclarativePredicateOrNull(type, predicate))
fun BytecodePatchContext.firstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = firstMethodMutableOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun BytecodePatchContext.firstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(firstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun gettingFirstClassDefByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefOrNull(type) { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(gettingFirstClassDefByDeclarativePredicateOrNull(type, predicate))
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefMutableOrNull(type) { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicate(
type: String,
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(gettingFirstClassDefMutableByDeclarativePredicateOrNull(type, predicate))
fun gettingFirstClassDefByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(gettingFirstClassDefByDeclarativePredicateOrNull(predicate))
fun gettingFirstClassDefMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = gettingFirstClassDefMutableOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstClassDefMutableByDeclarativePredicate(
predicate: context(MatchContext, ClassDef) DeclarativePredicateBuilder<ClassDef>.() -> Unit
) = requireNotNull(gettingFirstClassDefMutableByDeclarativePredicateOrNull(predicate))
fun gettingFirstMethodByDeclarativePredicateOrNull(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(gettingFirstMethodByDeclarativePredicateOrNull(predicate))
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodMutableOrNull { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(gettingFirstMethodMutableByDeclarativePredicateOrNull(predicate))
fun gettingFirstMethodByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(gettingFirstMethodByDeclarativePredicateOrNull(*strings, predicate = predicate))
fun gettingFirstMethodMutableByDeclarativePredicateOrNull(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = gettingFirstMethodMutableOrNull(*strings) { rememberDeclarativePredicate(predicate) }
fun gettingFirstMethodMutableByDeclarativePredicate(
vararg strings: String,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) = requireNotNull(gettingFirstMethodMutableByDeclarativePredicateOrNull(*strings, predicate = predicate))
class CompositionBuilder() {
val indexedMatcher = IndexedMatcher<Instruction>()
var accessFlagsPredicate: (DeclarativePredicateBuilder<Method>.() -> Unit)? = null
var returnsPredicate: (DeclarativePredicateBuilder<Method>.() -> Unit)? = null
var parameterTypesPredicate: (DeclarativePredicateBuilder<Method>.() -> Unit)? = null
var instructionsPredicate: (context(MatchContext) DeclarativePredicateBuilder<Method>.() -> Unit)? = null
var customPredicate: (context(MatchContext) DeclarativePredicateBuilder<Method>.() -> Unit)? = null
fun accessFlags(vararg flags: AccessFlags) {
accessFlagsPredicate = { predicate { accessFlags(*flags) } }
}
fun returns(returnType: String) {
returnsPredicate = { predicate { this.returnType.startsWith(returnType) } }
}
fun parameterTypes(vararg parameterTypes: String) {
parameterTypesPredicate = {
predicate {
this.parameterTypes.size == parameterTypes.size && this.parameterTypes.zip(parameterTypes)
.all { (a, b) -> a.startsWith(b) }
}
}
}
fun instructions(build: context(MatchContext, Method) IndexedMatcher<Instruction>.() -> Unit) {
instructionsPredicate = {
predicate {
implementation { indexedMatcher(this@CompositionBuilder.hashCode(), instructions) { build() } }
}
}
}
fun custom(block: context(MatchContext) Method.() -> Boolean) {
customPredicate = { predicate { block() } }
}
fun build() = Composition(indexedMatcher) {
accessFlagsPredicate?.invoke(this)
returnsPredicate?.invoke(this)
parameterTypesPredicate?.invoke(this)
with(contextOf<MatchContext>()) {
instructionsPredicate?.invoke(this@with, this@Composition)
customPredicate?.invoke(this@with, this@Composition)
}
}
}
class Composition internal constructor(
private val indexedMatcher: IndexedMatcher<Instruction>,
predicate: context(MatchContext, Method) DeclarativePredicateBuilder<Method>.() -> Unit
) {
val methodOrNull by gettingFirstMethodMutableByDeclarativePredicateOrNull(predicate)
val method = requireNotNull(methodOrNull)
val indices get() = indexedMatcher.indices
}
fun composeFirstMethod(predicate: CompositionBuilder.() -> Unit) = CompositionBuilder().apply(predicate).build()
val methodComposition = composeFirstMethod {
accessFlags(AccessFlags.PUBLIC)
instructions {
head { opcode == Opcode.RETURN }
fun opcode(opcode: Opcode) = add { this.opcode == opcode }
opcode(Opcode.NOP)
}
}
val patch by gettingBytecodePatch {
execute {
val mutableMethod = methodComposition.method
methodComposition.indices.first() // Opcode == return
firstClassDef(mutableMethod.definingClass).methods
firstClassDefMutable { type == mutableMethod.definingClass }
firstClassDefByDeclarativePredicateOrNull {
anyOf {
predicate { type == mutableMethod.definingClass }
predicate { type == "Ltest;" }
}
}
}
}

View File

@@ -1,12 +1,24 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.dex.mutable.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.iface.instruction.DualReferenceInstruction
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.*
import com.android.tools.smali.dexlib2.util.MethodUtil
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.StringReader
inline fun <reified T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
((this as? ReferenceInstruction)?.reference as? T)?.predicate() ?: false
@@ -48,17 +60,72 @@ private inline fun <reified T : Reference> Instruction.reference(): T? =
val Instruction.reference: Reference?
get() = reference()
val Instruction.methodReference get() =
reference<MethodReference>()
val Instruction.methodReference
get() =
reference<MethodReference>()
val Instruction.fieldReference get() =
reference<FieldReference>()
val Instruction.fieldReference
get() =
reference<FieldReference>()
val Instruction.typeReference get() =
reference<TypeReference>()
val Instruction.typeReference
get() =
reference<TypeReference>()
val Instruction.stringReference get() =
reference<StringReference>()
val Instruction.stringReference
get() =
reference<StringReference>()
val Instruction.wideLiteral get() =
(this as? WideLiteralInstruction)?.wideLiteral
val Instruction.wideLiteral
get() =
(this as? WideLiteralInstruction)?.wideLiteral
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 sb by lazy { StringBuilder(512) }
/**
* Compile lines of Smali code to a list of instructions.
*
* Note: Adding compiled instructions to an existing method with
* offset instructions WITHOUT specifying a parent method will not work.
* @param templateMethod The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(templateMethod: MutableMethod? = null): List<BuilderInstruction> {
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
sb.setLength(0) // reset
if (isStatic) sb.append(STATIC_HEADER) else sb.append(HEADER)
sb.append(parameters).append(")V\n")
sb.append(" .registers ").append(registers).append("\n")
sb.append(trimIndent()).append("\n")
sb.append(".end method")
val reader = StringReader(sb.toString())
val lexer = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)
val fileTree = parser.smali_file()
if (lexer.numberOfSyntaxErrors > 0 || parser.numberOfSyntaxErrors > 0) {
throw IllegalStateException(
"Lexer errors: ${lexer.numberOfSyntaxErrors}, Parser errors: ${parser.numberOfSyntaxErrors}"
)
}
val treeStream = CommonTreeNodeStream(fileTree.tree).apply {
tokenStream = tokens
}
val walker = smaliTreeWalker(treeStream)
walker.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = walker.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}

View File

@@ -1,7 +1,6 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.util.toInstructions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction

View File

@@ -21,6 +21,13 @@ typealias PackageName = String
typealias VersionName = String
typealias Package = Pair<PackageName, Set<VersionName>?>
/**
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
*/
sealed interface PatchContext<T> : Supplier<T>
/**
* A patch.
*

View File

@@ -1,9 +0,0 @@
package app.revanced.patcher.patch
import java.util.function.Supplier
/**
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
*/
sealed interface PatchContext<T> : Supplier<T>

View File

@@ -1,64 +0,0 @@
package app.revanced.patcher.util
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.extensions.instructions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
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 sb by lazy { StringBuilder(512) }
/**
* Compile lines of Smali code to a list of instructions.
*
* Note: Adding compiled instructions to an existing method with
* offset instructions WITHOUT specifying a parent method will not work.
* @param templateMethod The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(templateMethod: MutableMethod? = null): List<BuilderInstruction> {
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
sb.setLength(0) // reset
if (isStatic) sb.append(STATIC_HEADER) else sb.append(HEADER)
sb.append(parameters).append(")V\n")
sb.append(" .registers ").append(registers).append("\n")
sb.append(trimIndent()).append("\n")
sb.append(".end method")
val reader = StringReader(sb.toString())
val lexer = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)
val fileTree = parser.smali_file()
if (lexer.numberOfSyntaxErrors > 0 || parser.numberOfSyntaxErrors > 0) {
throw IllegalStateException(
"Lexer errors: ${lexer.numberOfSyntaxErrors}, Parser errors: ${parser.numberOfSyntaxErrors}"
)
}
val treeStream = CommonTreeNodeStream(fileTree.tree).apply {
tokenStream = tokens
}
val walker = smaliTreeWalker(treeStream)
walker.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = walker.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}

View File

@@ -1,7 +1,7 @@
package app.revanced.patcher
import app.revanced.patcher.extensions.toInstructions
import app.revanced.patcher.patch.*
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
@@ -298,7 +298,7 @@ internal object PatcherTest {
val matchIndices = indexedMatcher<Instruction>()
val method by gettingFirstMethod {
implementation {
matchIndices(instructions, "match") {
matchIndices(instructions) {
head { opcode == Opcode.CONST_STRING }
add { opcode == Opcode.IPUT_OBJECT }
}