feat: Builds now

This commit is contained in:
oSumAtrIX
2025-11-22 19:23:48 +01:00
parent 0b5e8b791d
commit e2c781f12c
14 changed files with 2927 additions and 741 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -57,7 +57,7 @@ kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs = listOf("-Xcontext-receivers")
freeCompilerArgs = listOf("-Xcontext-parameters")
}
}

View File

@@ -1,3 +1,3 @@
org.gradle.parallel = true
org.gradle.caching = true
version = 21.1.0-dev.5
version = 22.0.0

View File

@@ -0,0 +1,909 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
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.opcode
import app.revanced.patcher.extensions.string
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import com.android.tools.smali.dexlib2.AccessFlags
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
/**
* A fingerprint for a method. A fingerprint is a partial description of a method.
* It is used to uniquely match a method by its characteristics.
*
* An example fingerprint for a public method that takes a single string parameter and returns void:
* ```
* fingerprint {
* accessFlags(AccessFlags.PUBLIC)
* returns("V")
* parameters("Ljava/lang/String;")
* }
* ```
*
* @param accessFlags The exact access flags using values of [AccessFlags].
* @param returnType The return type. Compared using [String.startsWith].
* @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType].
* @param filters A list of filters to match, declared in the same order the instructions appear in the method.
* @param strings A list of the strings that appear anywhere in the method. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
*/
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val filters: List<InstructionFilter>?,
@Deprecated("Instead use instruction filters")
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
/**
* Clears the current match, forcing this fingerprint to resolve again.
* This method should only be used if this fingerprint is re-used after it's modified,
* and the prior match indexes are no longer correct.
*/
fun clearMatch() {
_matchOrNull = null
}
/**
* The match for this [Fingerprint], or `null` if no matches exist.
*/
context(context: BytecodePatchContext)
fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
val matchStrings = indexedMatcher<Instruction> {
strings?.forEach { add { string { contains(it) } } }
}
val matchIndices = indexedMatcher<Instruction>()
val match = context(_: MatchContext) fun Method.(): Boolean {
if (this@Fingerprint.accessFlags != null && this@Fingerprint.accessFlags != accessFlags)
return false
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]!!))
return false
if (strings != null && !matchStrings(instructionsOrNull ?: return false))
return false
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 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) {
val definingClass = definingClass()
if (!reference.definingClass.endsWith(definingClass))
// else, the method call is for 'this' class.
if (!(definingClass == "this" && reference.definingClass == method.definingClass)) return false
}
true
}
is LiteralFilter -> {
instruction.wideLiteral?.equals(literal()) ?: return false
opcodes != null && !opcodes.contains(instruction.opcode)
}
is MethodCallFilter -> {
val reference = instruction.methodReference ?: return false
if (name != null && reference.name != name()) return false
if (returnType != null && !reference.returnType.startsWith(returnType())) return false
if (parameters != null && !parametersStartsWith(
reference.parameterTypes, parameters()
)
) return false
if (definingClass != null) {
val definingClass = definingClass()
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)
}
is NewInstanceFilter -> {
if (opcodes != null && !opcodes.contains(instruction.opcode)) return false
instruction.reference<TypeReference> { endsWith(type) }
}
is OpcodeFilter -> instruction.opcode(opcode)
is StringFilter -> {
val string = instruction.stringReference?.string ?: return false
val filterString = string()
when (matchType) {
StringMatchType.EQUALS -> string == filterString
StringMatchType.CONTAINS -> string.contains(filterString)
StringMatchType.STARTS_WITH -> string.startsWith(filterString)
StringMatchType.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 MatchAfterAnywhere -> add { filter.evaluate(this) }
is MatchAfterWithin -> after(atLeast = 1, atMost = location.matchDistance) {
filter.evaluate(
this
)
}
is MatchFirst -> first { filter.evaluate(this) }
}
}
})
return false
return true
}
val allStrings = buildList {
if (filters != null) addAll(
(filters.filterIsInstance<StringFilter>() + filters.filterIsInstance<AnyInstruction>().flatMap {
it.filters.filterIsInstance<StringFilter>()
})
)
}.map { it.string() } + (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 instructionMatches = filters?.withIndex()?.map { (i, filter) ->
Match.InstructionMatch(filter, matchIndices.indices[i], method.getInstruction(i))
}
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(
context,
context.lookupMaps.classDefsByType[method.definingClass]!!,
method,
instructionMatches,
stringMatches,
)
}
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the
* fingerprint is already matched to a method, null otherwise.
*/
context(_: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): Match? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = matchOrNull(classDef, method)
if (match != null) {
_matchOrNull = match
return match
}
}
return null
}
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
* otherwise `null`.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
): Match? {
if (_matchOrNull != null) return _matchOrNull
return matchOrNull(context.lookupMaps.classDefsByType[method.definingClass]!!, method)
}
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
* otherwise `null`.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
method: Method,
): Match? {
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
return null
}
if (accessFlags != null && accessFlags != method.accessFlags) {
return null
}
// TODO: parseParameters()
if (parameters != null && !parametersStartsWith(method.parameterTypes, parameters)) {
return null
}
if (custom != null && !custom.invoke(method, classDef)) {
return null
}
// Legacy string declarations.
val stringMatches: List<Match.StringMatch>? = if (strings == null) {
null
} else {
buildList {
val instructions = method.instructionsOrNull ?: return null
var stringsList: MutableList<String>? = null
instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
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 null
}
}
val instructionMatches = if (filters == null) {
null
} else {
val instructions = method.instructionsOrNull?.toList() ?: return null
fun matchFilters(): List<Match.InstructionMatch>? {
val lastMethodIndex = instructions.lastIndex
var instructionMatches: MutableList<Match.InstructionMatch>? = 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<Match.InstructionMatch>(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
}
_matchOrNull = Match(
context,
classDef,
method,
instructionMatches,
stringMatches,
)
return _matchOrNull
}
fun patchException() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @return The [Match] of this fingerprint.
* @throws PatchException If the [Fingerprint] failed to match.
*/
context(_: BytecodePatchContext)
fun match() = matchOrNull() ?: throw patchException()
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] of this fingerprint.
* @throws PatchException If the fingerprint failed to match.
*/
context(_: BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw patchException()
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] of this fingerprint.
* @throws PatchException If the fingerprint failed to match.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw patchException()
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] of this fingerprint.
* @throws PatchException If the fingerprint failed to match.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(classDef, method) ?: throw patchException()
/**
* The class the matching method is a member of, or null if this fingerprint did not match.
*/
context(_: BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull()?.originalClassDef
/**
* The matching method, or null of this fingerprint did not match.
*/
context(_: BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull()?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val classDefOrNull
get() = matchOrNull()?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val methodOrNull
get() = matchOrNull()?.method
/**
* The match for the opcode pattern, or null if this fingerprint did not match.
*/
context(_: BytecodePatchContext)
@Deprecated("instead use instructionMatchesOrNull")
val patternMatchOrNull: PatternMatch?
get() {
val match = this.matchOrNull()
if (match == null || match.instructionMatchesOrNull == null) {
return null
}
return match.patternMatch
}
/**
* The match for the instruction filters, or null if this fingerprint did not match.
*/
context(_: BytecodePatchContext)
val instructionMatchesOrNull
get() = matchOrNull()?.instructionMatchesOrNull
/**
* The matches for the strings, or null if this fingerprint did not match.
*
* This does not give matches for strings declared using [string] instruction filters.
*/
context(_: BytecodePatchContext)
@Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
val stringMatchesOrNull
get() = matchOrNull()?.stringMatchesOrNull
/**
* The class the matching method is a member of.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val originalClassDef
get() = match().originalClassDef
/**
* The matching method.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val originalMethod
get() = match().originalMethod
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val classDef
get() = match().classDef
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val method
get() = match().method
/**
* The match for the opcode pattern.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
@Deprecated("Instead use instructionMatch")
val patternMatch
get() = match().patternMatch
/**
* Instruction filter matches.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
val instructionMatches
get() = match().instructionMatches
/**
* The matches for the strings declared using `strings()`.
* This does not give matches for strings declared using [string] instruction filters.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
@Deprecated("Instead use string instructions and `instructionMatches()`")
val stringMatches
get() = match().stringMatches
}
/**
* A match of a [Fingerprint].
*
* @param originalClassDef The class the matching method is a member of.
* @param originalMethod The matching method.
* @param _instructionMatches The match for the instruction filters.
* @param _stringMatches The matches for the strings declared using `strings()`.
*/
class Match internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
val originalMethod: Method,
private val _instructionMatches: List<InstructionMatch>?,
private val _stringMatches: List<StringMatch>?,
) {
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*/
val classDef by lazy {
with(context) {
originalClassDef.mutable()
}
}
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*/
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
@Deprecated("Instead use instructionMatches", ReplaceWith("instructionMatches"))
val patternMatch by lazy {
if (_instructionMatches == null) throw PatchException("Did not match $this")
@SuppressWarnings("deprecation")
PatternMatch(_instructionMatches.first().index, _instructionMatches.last().index)
}
val instructionMatches
get() = _instructionMatches ?: throw PatchException("Fingerprint declared no instruction filters")
val instructionMatchesOrNull = _instructionMatches
@Deprecated("Instead use string instructions and `instructionMatches()`")
val stringMatches
get() = _stringMatches ?: throw PatchException("Fingerprint declared no strings")
@Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
val stringMatchesOrNull = _stringMatches
/**
* A match for an opcode pattern.
* @param startIndex The index of the first opcode of the pattern in the method.
* @param endIndex The index of the last opcode of the pattern in the method.
*/
@Deprecated("Instead use InstructionMatch")
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
/**
* A match for a string.
*
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
@Deprecated("Instead use string instructions and `InstructionMatch`")
class StringMatch internal constructor(val string: String, val index: Int)
/**
* A match for a [InstructionFilter].
* @param filter The filter that matched
* @param index The instruction index it matched with.
* @param instruction The instruction that matched.
*/
class InstructionMatch internal constructor(
val filter: InstructionFilter,
val index: Int,
val instruction: Instruction
) {
@Suppress("UNCHECKED_CAST")
fun <T> getInstruction(): T = instruction as T
}
}
/**
* A builder for [Fingerprint].
*
* @property accessFlags The exact access flags using values of [AccessFlags].
* @property returnType The return type compared using [String.startsWith].
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @property instructionFilters Filters to match the method instructions.
* @property strings A list of the strings compared each using [String.contains].
* @property customBlock A custom condition for this fingerprint.
*
* @constructor Create a new [FingerprintBuilder].
*/
class FingerprintBuilder() {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var instructionFilters: List<InstructionFilter>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
/**
* Set the return type.
*
* If [accessFlags] includes [AccessFlags.CONSTRUCTOR], then there is no need to
* set a return type set since constructors are always void return type.
*
* @param returnType The return type compared using [String.startsWith].
*/
fun returns(returnType: String) {
this.returnType = returnType
}
/**
* Set the parameters.
*
* @param parameters The parameters of the method.
* Partial matches allowed and follow the same rules as [returnType].
*/
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
private fun verifyNoFiltersSet() {
if (this.instructionFilters != null) {
throw PatchException("Instruction filters already set")
}
}
/**
* A pattern of opcodes, where each opcode must appear immediately after the previous.
*
* To use opcodes with other [InstructionFilter] objects,
* instead use [instructions] with individual opcodes declared using [opcode].
*
* This method is identical to declaring individual opcode filters
* with [InstructionFilter.location] set to [InstructionLocation.MatchAfterImmediately]
* for all but the first opcode.
*
* Unless absolutely necessary, it is recommended to instead use [instructions]
* with more fine grained filters.
*
* ```
* opcodes(
* Opcode.INVOKE_VIRTUAL, // First opcode matches anywhere in the method.
* Opcode.MOVE_RESULT_OBJECT, // Must match exactly after INVOKE_VIRTUAL.
* Opcode.IPUT_OBJECT // Must match exactly after MOVE_RESULT_OBJECT.
* )
* ```
* is identical to:
* ```
* instructions(
* opcode(Opcode.INVOKE_VIRTUAL), // First opcode matches anywhere in the method.
* opcode(Opcode.MOVE_RESULT_OBJECT, maxAfter = 0), // Must match exactly after INVOKE_VIRTUAL.
* opcode(Opcode.IPUT_OBJECT, maxAfter = 0) // Must match exactly after MOVE_RESULT_OBJECT.
* )
* ```
*
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
verifyNoFiltersSet()
if (opcodes.isEmpty()) throw IllegalArgumentException("One or more opcodes is required")
this.instructionFilters = OpcodesFilter.listOfOpcodes(opcodes.toList())
}
/**
* A pattern of opcodes from SMALI formatted text,
* where each opcode must appear immediately after the previous opcode.
*
* Unless absolutely necessary, it is recommended to instead use [instructions].
*
* @param instructions A list of instructions or opcode names in SMALI format.
* - Wildcard or unknown opcodes can be specified by `null`.
* - Empty lines are ignored.
* - Each instruction must be on a new line.
* - The opcode name is enough, no need to specify the operands.
*
* @throws Exception If an unknown opcode is used.
*/
fun opcodes(instructions: String) {
verifyNoFiltersSet()
if (instructions.isBlank()) throw IllegalArgumentException("No instructions declared (empty string)")
this.instructionFilters = OpcodesFilter.listOfOpcodes(
instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.map {
// Remove any operands.
val name = it.split(" ", limit = 1).first().trim()
if (name == "null") return@map null
opcodesByName[name] ?: throw IllegalArgumentException("Unknown opcode: $name")
}
)
}
/**
* A list of instruction filters to match.
*/
fun instructions(vararg instructionFilters: InstructionFilter) {
verifyNoFiltersSet()
if (instructionFilters.isEmpty()) throw IllegalArgumentException("One or more instructions is required")
this.instructionFilters = instructionFilters.toList()
}
/**
* Set the strings.
*
* @param strings A list of strings compared each using [String.contains].
*/
@Deprecated("Instead use `instruction()` filters and `string()` instruction declarations")
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
/**
* Set a custom condition for this fingerprint.
*
* @param customBlock A custom condition for this fingerprint.
*/
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
fun build(): Fingerprint {
// If access flags include constructor then
// skip the return type check since it's always void.
if (returnType?.equals("V") == true && accessFlags != null
&& AccessFlags.CONSTRUCTOR.isSet(accessFlags!!)
) {
returnType = null
}
return Fingerprint(
accessFlags,
returnType,
parameters,
instructionFilters,
strings,
customBlock,
)
}
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
fun fingerprint(
block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder().apply(block).build()
/**
* Matches two lists of parameters, where the first parameter list
* starts with the values of the second list.
*/
internal fun parametersStartsWith(
targetMethodParameters: Iterable<CharSequence>,
fingerprintParameters: Iterable<CharSequence>,
): Boolean {
if (fingerprintParameters.count() != targetMethodParameters.count()) return false
val fingerprintIterator = fingerprintParameters.iterator()
targetMethodParameters.forEach {
if (!it.startsWith(fingerprintIterator.next())) return false
}
return true
}

View File

@@ -0,0 +1,983 @@
@file:Suppress("unused")
package app.revanced.patcher
import app.revanced.patcher.FieldAccessFilter.Companion.parseJvmFieldAccess
import app.revanced.patcher.MethodCallFilter.Companion.parseJvmMethodCall
import com.android.tools.smali.dexlib2.Opcode
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.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
import java.util.*
/**
* Simple interface to control how much space is allowed between a previous
* [InstructionFilter match and the current [InstructionFilter].
*/
fun interface InstructionLocation {
/**
* @param previouslyMatchedIndex The previously matched index, or -1 if this is the first filter.
* @param currentIndex The current method index that is about to be checked.
*/
fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean
/**
* Matching can occur anywhere after the previous instruction filter match index.
* Is the default behavior for all filters.
*/
class MatchAfterAnywhere : InstructionLocation {
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) = true
}
/**
* Matches the first instruction of a method.
*
* This can only be used for the first filter, and using with any other filter will throw an exception.
*/
class MatchFirst() : InstructionLocation {
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean {
if (previouslyMatchedIndex >= 0) {
throw IllegalArgumentException(
"MatchFirst can only be used for the first instruction filter"
)
}
return true
}
}
/**
* Instruction index immediately after the previous filter.
*
* Useful for opcodes that must always appear immediately after the last filter such as:
* - [Opcode.MOVE_RESULT]
* - [Opcode.MOVE_RESULT_WIDE]
* - [Opcode.MOVE_RESULT_OBJECT]
*
* This cannot be used for the first filter and will throw an exception.
*/
class MatchAfterImmediately() : InstructionLocation {
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean {
if (previouslyMatchedIndex < 0) {
throw IllegalArgumentException(
"MatchAfterImmediately cannot be used for the first instruction filter"
)
}
return currentIndex - 1 == previouslyMatchedIndex
}
}
/**
* Instruction index can occur within a range of the previous instruction filter match index.
* used to constrain instruction matching to a region after the previous instruction filter.
*
* This cannot be used for the first filter and will throw an exception.
*
* @param matchDistance The number of unmatched instructions that can exist between the
* current instruction filter and the previously matched instruction filter.
* A value of 0 means the current filter can only match immediately after
* the previously matched instruction (making this functionally identical to
* [MatchAfterImmediately]). A value of 10 means between 0 and 10 unmatched
* instructions can exist between the previously matched instruction and
* the current instruction filter.
*/
class MatchAfterWithin(var matchDistance: Int) : InstructionLocation {
init {
if (matchDistance < 0) {
throw IllegalArgumentException("matchDistance must be non-negative")
}
}
override fun indexIsValidForMatching(previouslyMatchedIndex: Int, currentIndex: Int) : Boolean {
if (previouslyMatchedIndex < 0) {
throw IllegalArgumentException(
"MatchAfterImmediately cannot be used for the first instruction filter"
)
}
return currentIndex - previouslyMatchedIndex - 1 <= matchDistance
}
}
}
/**
* Matches method [Instruction] objects, similar to how [Fingerprint] matches entire fingerprints.
*
* The most basic filters match only opcodes and nothing more,
* and more precise filters can match:
* - Field references (get/put opcodes) by name/type.
* - Method calls (invoke_* opcodes) by name/parameter/return type.
* - Object instantiation for specific class types.
* - Literal const values.
*/
fun interface InstructionFilter {
/**
* The [InstructionLocation] associated with this filter.
*/
val location: InstructionLocation
get() = InstructionLocation.MatchAfterAnywhere()
/**
* If this filter matches the method instruction.
*
* @param enclosingMethod The method of that contains [instruction].
* @param instruction The instruction to check for a match.
*/
fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean
}
class AnyInstruction internal constructor(
internal val filters: List<InstructionFilter>,
override val location : InstructionLocation
) : InstructionFilter {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
) : Boolean {
return filters.any { filter ->
filter.matches(enclosingMethod, instruction)
}
}
}
/**
* Logical OR operator where the first filter that matches satisfies this filter.
*/
fun anyInstruction(
vararg filters: InstructionFilter,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = AnyInstruction(filters.asList(), location)
open class OpcodeFilter(
val opcode: Opcode,
override val location : InstructionLocation
) : InstructionFilter {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
return instruction.opcode == opcode
}
}
/**
* Single opcode.
*/
fun opcode(
opcode: Opcode,
location: InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = OpcodeFilter(opcode, location)
/**
* Matches a single instruction from many kinds of opcodes.
* If matching only a single opcode instead use [OpcodeFilter].
*/
open class OpcodesFilter private constructor(
val opcodes: EnumSet<Opcode>?,
override val location : InstructionLocation
) : InstructionFilter {
protected constructor(
/**
* Value of `null` will match any opcode.
*/
opcodes: List<Opcode>?,
location : InstructionLocation
) : this(if (opcodes == null) null else EnumSet.copyOf(opcodes), location)
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (opcodes == null) {
return true // Match anything.
}
return opcodes.contains(instruction.opcode)
}
companion object {
/**
* First opcode can match anywhere in a method, but all
* subsequent opcodes must match after the previous opcode.
*
* A value of `null` indicates to match any opcode.
*/
internal fun listOfOpcodes(opcodes: Collection<Opcode?>): List<InstructionFilter> {
val list = ArrayList<InstructionFilter>(opcodes.size)
var location: InstructionLocation? = null
opcodes.forEach { opcode ->
// First opcode can match anywhere.
val opcodeLocation = location ?: InstructionLocation.MatchAfterAnywhere()
list += if (opcode == null) {
// Null opcode matches anything.
OpcodesFilter(
null as List<Opcode>?,
opcodeLocation
)
} else {
OpcodeFilter(opcode, opcodeLocation)
}
if (location == null) {
location = InstructionLocation.MatchAfterImmediately()
}
}
return list
}
}
}
class LiteralFilter internal constructor(
var literal: () -> Long,
opcodes: List<Opcode>? = null,
location : InstructionLocation
) : OpcodesFilter(opcodes, location) {
private var literalValue: Long? = null
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (!super.matches(enclosingMethod, instruction)) {
return false
}
if (instruction !is WideLiteralInstruction) return false
if (literalValue == null) {
literalValue = literal()
}
return instruction.wideLiteral == literalValue
}
}
/**
* Literal value, such as:
* `const v1, 0x7f080318`
*
* that can be matched using:
* `LiteralFilter(0x7f080318)`
* or
* `LiteralFilter(2131231512)`
*/
fun literal(
literal: () -> Long,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = LiteralFilter(literal, opcodes, location)
/**
* Literal value, such as:
* `const v1, 0x7f080318`
*
* that can be matched using:
* `LiteralFilter(0x7f080318)`
* or
* `LiteralFilter(2131231512L)`
*/
fun literal(
literal: Long,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = LiteralFilter({ literal }, opcodes, location)
/**
* Integer point literal.
*/
fun literal(
literal: Int,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = LiteralFilter({ literal.toLong() }, opcodes, location)
/**
* Double point literal.
*/
fun literal(
literal: Double,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = LiteralFilter({ literal.toRawBits() }, opcodes, location)
/**
* Floating point literal.
*/
fun literal(
literal: Float,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = LiteralFilter({ literal.toRawBits().toLong() }, opcodes, location)
enum class StringMatchType {
EQUALS,
CONTAINS,
STARTS_WITH,
ENDS_WITH
}
class StringFilter internal constructor(
var string: () -> String,
var matchType: StringMatchType,
location : InstructionLocation
) : OpcodesFilter(listOf(Opcode.CONST_STRING, Opcode.CONST_STRING_JUMBO), location) {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (!super.matches(enclosingMethod, instruction)) {
return false
}
val instructionString = ((instruction as ReferenceInstruction).reference as StringReference).string
val filterString = string()
return when (matchType) {
StringMatchType.EQUALS -> instructionString == filterString
StringMatchType.CONTAINS -> instructionString.contains(filterString)
StringMatchType.STARTS_WITH -> instructionString.startsWith(filterString)
StringMatchType.ENDS_WITH -> instructionString.endsWith(filterString)
}
}
}
/**
* Literal String instruction.
*/
fun string(
string: () -> String,
/**
* If [string] is a partial match, where the target string contains this string.
* For more precise matching, consider using [anyInstruction] with multiple exact string declarations.
*/
matchType: StringMatchType = StringMatchType.EQUALS,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = StringFilter(string, matchType, location)
/**
* Literal String instruction.
*/
fun string(
string: String,
/**
* How to compare [string] against the string constant opcode. For more precise matching
* of multiple strings, consider using [anyInstruction] with multiple exact string declarations.
*/
matchType: StringMatchType = StringMatchType.EQUALS,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = StringFilter({ string }, matchType, location)
class MethodCallFilter internal constructor(
val definingClass: (() -> String)? = null,
val name: (() -> String)? = null,
val parameters: (() -> List<String>)? = null,
val returnType: (() -> String)? = null,
opcodes: List<Opcode>? = null,
location : InstructionLocation
) : OpcodesFilter(opcodes, location) {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (!super.matches(enclosingMethod, instruction)) {
return false
}
val reference = (instruction as? ReferenceInstruction)?.reference as? MethodReference
if (reference == null) return false
if (definingClass != null) {
val referenceClass = reference.definingClass
val definingClass = definingClass()
if (!referenceClass.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" && referenceClass == enclosingMethod.definingClass)) {
return false
} // else, the method call is for 'this' class.
}
}
if (name != null && reference.name != name()) {
return false
}
if (returnType != null && !reference.returnType.startsWith(returnType())) {
return false
}
if (parameters != null && !parametersStartsWith(reference.parameterTypes, parameters())) {
return false
}
return true
}
companion object {
private val regex = Regex("""^(L[^;]+;)->([^(\s]+)\(([^)]*)\)(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""")
internal fun parseJvmMethodCall(
methodSignature: String,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
): MethodCallFilter {
val matchResult = regex.matchEntire(methodSignature)
?: throw IllegalArgumentException("Invalid method signature: $methodSignature")
val classDescriptor = matchResult.groupValues[1]
val methodName = matchResult.groupValues[2]
val paramDescriptorString = matchResult.groupValues[3]
val returnDescriptor = matchResult.groupValues[4]
val paramDescriptors = parseParameterDescriptors(paramDescriptorString)
return MethodCallFilter(
{ classDescriptor },
{ methodName },
{ paramDescriptors },
{ returnDescriptor },
opcodes,
location
)
}
/**
* Parses a single JVM type descriptor or an array descriptor at the current position.
* For example: Lcom/example/SomeClass; or I or [I or [Lcom/example/SomeClass;
*/
private fun parseSingleType(params: String, startIndex: Int): Pair<String, Int> {
var i = startIndex
// Skip past array declaration, including multi-dimensional arrays.
val paramsLength = params.length
while (i < paramsLength && params[i] == '[') {
i++
}
return if (i < paramsLength && params[i] == 'L') {
// It's an object type starting with 'L', read until ';'
val semicolonPos = params.indexOf(';', i)
if (semicolonPos < 0) {
throw IllegalArgumentException("Malformed object descriptor (missing semicolon): $params")
}
// Substring from startIndex up to and including the semicolon.
val typeDescriptor = params.substring(startIndex, semicolonPos + 1)
typeDescriptor to (semicolonPos + 1)
} else {
// It's either a primitive or we've already consumed the array part
// So just take one character (e.g. 'I', 'Z', 'B', etc.)
val typeDescriptor = params.substring(startIndex, i + 1)
typeDescriptor to (i + 1)
}
}
/**
* Parses the parameters into a list of JVM type descriptors.
*/
private fun parseParameterDescriptors(paramString: String): List<String> {
val result = mutableListOf<String>()
var currentIndex = 0
val stringLength = paramString.length
while (currentIndex < stringLength) {
val (type, nextIndex) = parseSingleType(paramString, currentIndex)
result.add(type)
currentIndex = nextIndex
}
return result
}
}
}
/**
* Identifies method calls.
*
* `Null` parameters matches anything.
*
* By default any type of method call matches.
* Specify opcodes if a specific type of method call is desired (such as only static calls).
*/
fun methodCall(
/**
* Defining class of the method call. Matches using endsWith().
*
* For calls to a method in the same class, use 'this' as the defining class.
* Note: 'this' does not work for methods declared only in a superclass.
*/
definingClass: (() -> String)? = null,
/**
* Method name. Must be exact match of the method name.
*/
name: (() -> String)? = null,
/**
* Parameters of the method call. Each parameter matches
* using startsWith() and semantics are the same as [Fingerprint].
*/
parameters: (() -> List<String>)? = null,
/**
* Return type. Matches using startsWith()
*/
returnType: (() -> String)? = null,
/**
* Opcode types to match. By default this matches any method call opcode:
* `Opcode.INVOKE_*`.
*
* If this filter must match specific types of method call, then specify the desired opcodes
* such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
*/
opcodes: List<Opcode>? = null,
/**
* The locations where this filter is allowed to match.
*/
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = MethodCallFilter(
definingClass,
name,
parameters,
returnType,
opcodes,
location
)
fun methodCall(
/**
* Defining class of the method call. Matches using endsWith().
*
* For calls to a method in the same class, use 'this' as the defining class.
* Note: 'this' does not work for methods declared only in a superclass.
*/
definingClass: String? = null,
/**
* Method name. Must be exact match of the method name.
*/
name: String? = null,
/**
* Parameters of the method call. Each parameter matches
* using startsWith() and semantics are the same as [Fingerprint].
*/
parameters: List<String>? = null,
/**
* Return type. Matches using startsWith()
*/
returnType: String? = null,
/**
* Opcode types to match. By default this matches any method call opcode:
* `Opcode.INVOKE_*`.
*
* If this filter must match specific types of method call, then specify the desired opcodes
* such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
*/
opcodes: List<Opcode>? = null,
/**
* The locations where this filter is allowed to match.
*/
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = MethodCallFilter(
if (definingClass != null) {
{ definingClass }
} else null,
if (name != null) {
{ name }
} else null,
if (parameters != null) {
{ parameters }
} else null,
if (returnType != null) {
{ returnType }
} else null,
opcodes,
location
)
fun methodCall(
/**
* Defining class of the method call. Matches using endsWith().
*
* For calls to a method in the same class, use 'this' as the defining class.
* Note: 'this' does not work for methods declared only in a superclass.
*/
definingClass: String? = null,
/**
* Method name. Must be exact match of the method name.
*/
name: String? = null,
/**
* Parameters of the method call. Each parameter matches
* using startsWith() and semantics are the same as [Fingerprint].
*/
parameters: List<String>? = null,
/**
* Return type. Matches using startsWith()
*/
returnType: String? = null,
/**
* Opcode types to match. By default this matches any method call opcode:
* `Opcode.INVOKE_*`.
*
* If this filter must match specific types of method call, then specify the desired opcodes
* such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
*/
opcode: Opcode,
/**
* The locations where this filter is allowed to match.
*/
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = MethodCallFilter(
if (definingClass != null) {
{ definingClass }
} else null,
if (name != null) {
{ name }
} else null,
if (parameters != null) {
{ parameters }
} else null,
if (returnType != null) {
{ returnType }
} else null,
listOf(opcode),
location
)
/**
* Method call for a copy pasted SMALI style method signature. e.g.:
* `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`
*
* Does not support obfuscated method names or parameter/return types.
*/
fun methodCall(
smali: String,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = parseJvmMethodCall(smali, opcodes, location)
/**
* Method call for a copy pasted SMALI style method signature. e.g.:
* `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`
*
* Does not support obfuscated method names or parameter/return types.
*/
fun methodCall(
smali: String,
opcode: Opcode,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = parseJvmMethodCall(smali, listOf(opcode), location)
class FieldAccessFilter internal constructor(
val definingClass: (() -> String)? = null,
val name: (() -> String)? = null,
val type: (() -> String)? = null,
opcodes: List<Opcode>? = null,
location : InstructionLocation
) : OpcodesFilter(opcodes, location) {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (!super.matches(enclosingMethod, instruction)) {
return false
}
val reference = (instruction as? ReferenceInstruction)?.reference as? FieldReference
if (reference == null) return false
if (definingClass != null) {
val referenceClass = reference.definingClass
val definingClass = definingClass()
if (!referenceClass.endsWith(definingClass)) {
if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) {
return false
} // else, the method call is for 'this' class.
}
}
if (name != null && reference.name != name()) {
return false
}
if (type != null && !reference.type.startsWith(type())) {
return false
}
return true
}
internal companion object {
private val regex = Regex("""^(L[^;]+;)->([^:]+):(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""")
internal fun parseJvmFieldAccess(
fieldSignature: String,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
): FieldAccessFilter {
val matchResult = regex.matchEntire(fieldSignature)
?: throw IllegalArgumentException("Invalid field access smali: $fieldSignature")
return fieldAccess(
definingClass = matchResult.groupValues[1],
name = matchResult.groupValues[2],
type = matchResult.groupValues[3],
opcodes = opcodes,
location = location
)
}
}
}
/**
* Matches a field call, such as:
* `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
*/
fun fieldAccess(
/**
* Defining class of the field call. Matches using endsWith().
*
* For calls to a method in the same class, use 'this' as the defining class.
* Note: 'this' does not work for fields found in superclasses.
*/
definingClass: (() -> String)? = null,
/**
* Name of the field. Must be a full match of the field name.
*/
name: (() -> String)? = null,
/**
* Class type of field. Partial matches using startsWith() is allowed.
*/
type: (() -> String)? = null,
/**
* Valid opcodes matches for this instruction.
* By default this matches any kind of field access
* (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc).
*/
opcodes: List<Opcode>? = null,
/**
* The locations where this filter is allowed to match.
*/
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = FieldAccessFilter(definingClass, name, type, opcodes, location)
/**
* Matches a field call, such as:
* `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
*/
fun fieldAccess(
/**
* Defining class of the field call. Matches using endsWith().
*
* For calls to a method in the same class, use 'this' as the defining class.
* Note: 'this' does not work for fields found in superclasses.
*/
definingClass: String? = null,
/**
* Name of the field. Must be a full match of the field name.
*/
name: String? = null,
/**
* Class type of field. Partial matches using startsWith() is allowed.
*/
type: String? = null,
/**
* Valid opcodes matches for this instruction.
* By default this matches any kind of field access
* (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc).
*/
opcodes: List<Opcode>? = null,
/**
* The locations where this filter is allowed to match.
*/
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = FieldAccessFilter(
if (definingClass != null) {
{ definingClass }
} else null,
if (name != null) {
{ name }
} else null,
if (type != null) {
{ type }
} else null,
opcodes,
location
)
/**
* Matches a field call, such as:
* `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
*/
fun fieldAccess(
/**
* Defining class of the field call. Matches using endsWith().
*
* For calls to a method in the same class, use 'this' as the defining class.
* Note: 'this' does not work for fields found in superclasses.
*/
definingClass: String? = null,
/**
* Name of the field. Must be a full match of the field name.
*/
name: String? = null,
/**
* Class type of field. Partial matches using startsWith() is allowed.
*/
type: String? = null,
opcode: Opcode,
/**
* The locations where this filter is allowed to match.
*/
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = fieldAccess(
definingClass,
name,
type,
listOf(opcode),
location
)
/**
* Field access for a copy pasted SMALI style field access call. e.g.:
* `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`
*
* Does not support obfuscated field names or obfuscated field types.
*/
fun fieldAccess(
smali: String,
opcodes: List<Opcode>? = null,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = parseJvmFieldAccess(smali, opcodes, location)
/**
* Field access for a copy pasted SMALI style field access call. e.g.:
* `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`
*
* Does not support obfuscated field names or obfuscated field types.
*/
fun fieldAccess(
smali: String,
opcode: Opcode,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = parseJvmFieldAccess(smali, listOf(opcode), location)
class NewInstanceFilter internal constructor (
var type: () -> String,
location : InstructionLocation
) : OpcodesFilter(listOf(Opcode.NEW_INSTANCE, Opcode.NEW_ARRAY), location) {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (!super.matches(enclosingMethod, instruction)) {
return false
}
val reference = (instruction as? ReferenceInstruction)?.reference as? TypeReference
if (reference == null) return false
return reference.type.endsWith(type())
}
}
/**
* Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type.
*
* @param type Class type that matches the target instruction using [String.endsWith].
*/
fun newInstancetype(
type: () -> String,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = NewInstanceFilter(type, location)
/**
* Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type.
*
* @param type Class type that matches the target instruction using [String.endsWith].
*/
fun newInstance(
type: String,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) : NewInstanceFilter {
if (!type.endsWith(";")) {
throw IllegalArgumentException("Class type does not end with a semicolon: $type")
}
return NewInstanceFilter({ type }, location)
}
class CheckCastFilter internal constructor (
var type: () -> String,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) : OpcodeFilter(Opcode.CHECK_CAST, location) {
override fun matches(
enclosingMethod: Method,
instruction: Instruction
): Boolean {
if (!super.matches(enclosingMethod, instruction)) {
return false
}
val reference = (instruction as? ReferenceInstruction)?.reference as? TypeReference
if (reference == null) return false
return reference.type.endsWith(type())
}
}
/**
* Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type.
*
* @param type Class type that matches the target instruction using [String.endsWith].
*/
fun checkCast(
type: () -> String,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) = CheckCastFilter(type, location)
/**
* Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type.
*
* @param type Class type that matches the target instruction using [String.endsWith].
*/
fun checkCast(
type: String,
location : InstructionLocation = InstructionLocation.MatchAfterAnywhere()
) : CheckCastFilter {
if (!type.endsWith(";")) {
throw IllegalArgumentException("Class type does not end with a semicolon: $type")
}
return CheckCastFilter({ type }, location)
}

View File

@@ -4,15 +4,14 @@ package app.revanced.patcher
import app.revanced.patcher.Matcher.MatchContext
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.extensions.instructions
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
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
@@ -52,110 +51,104 @@ fun MethodImplementation.anyTryBlock(predicate: TryBlock<out ExceptionHandler>.(
fun MethodImplementation.anyDebugItem(predicate: Any.() -> Boolean) = debugItems.any(predicate)
fun Iterable<Instruction>.anyInstruction(predicate: Instruction.() -> Boolean) = any(predicate)
fun BytecodePatchContext.firstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
with(MatchContext()) { classDefs.firstOrNull { it.predicate() } }
fun BytecodePatchContext.firstClassDefOrNull(predicate: MatchPredicate<ClassDef>) =
with(predicate) { with(MatchContext()) { classDefs.firstOrNull { it.match() } } }
fun BytecodePatchContext.firstClassDef(predicate: MatchPredicate<ClassDef>) =
fun BytecodePatchContext.firstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
requireNotNull(firstClassDefOrNull(predicate))
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: MatchPredicate<ClassDef>) =
fun BytecodePatchContext.firstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
firstClassDefOrNull(predicate)?.mutable()
fun BytecodePatchContext.firstClassDefMutable(predicate: MatchPredicate<ClassDef>) =
fun BytecodePatchContext.firstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
requireNotNull(firstClassDefMutableOrNull(predicate))
fun BytecodePatchContext.firstClassDefOrNull(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = lookupMaps.classDefsByType[type]?.takeIf {
predicate == null || with(predicate) { with(MatchContext()) { it.match() } }
predicate == null || with(MatchContext()) { it.predicate() }
}
fun BytecodePatchContext.firstClassDef(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(firstClassDefOrNull(type, predicate))
fun BytecodePatchContext.firstClassDefMutableOrNull(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = firstClassDefOrNull(type, predicate)?.mutable()
fun BytecodePatchContext.firstClassDefMutable(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(firstClassDefMutableOrNull(type, predicate))
fun Iterable<ClassDef>.firstMethodOrNull(predicate: MatchPredicate<Method>) = with(predicate) {
fun Iterable<ClassDef>.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
with(MatchContext()) {
this@firstMethodOrNull.asSequence().flatMap { it.methods.asSequence() }.firstOrNull { it.match() }
this@firstMethodOrNull.asSequence().flatMap { it.methods.asSequence() }.firstOrNull { it.predicate() }
}
}
fun Iterable<ClassDef>.firstMethod(predicate: MatchPredicate<Method>) = requireNotNull(firstMethodOrNull(predicate))
fun Iterable<ClassDef>.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodOrNull(predicate))
context(BytecodePatchContext)
fun Iterable<ClassDef>.firstMethodMutableOrNull(predicate: MatchPredicate<Method>): MutableMethod? {
with(predicate) {
context(context: BytecodePatchContext)
fun Iterable<ClassDef>.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean): MutableMethod? =
with(context) {
with(MatchContext()) {
this@firstMethodMutableOrNull.forEach { classDef ->
classDef.methods.firstOrNull { it.match() }?.let { method ->
classDef.methods.firstOrNull { it.predicate() }?.let { method ->
return classDef.mutable().methods.first { MethodUtil.methodSignaturesMatch(it, method) }
}
}
null
}
}
return null
}
context(BytecodePatchContext)
fun Iterable<ClassDef>.firstMethodMutable(predicate: MatchPredicate<Method>) =
context(_: BytecodePatchContext)
fun Iterable<ClassDef>.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodMutableOrNull(predicate))
fun BytecodePatchContext.firstMethodOrNull(predicate: MatchPredicate<Method>) =
fun BytecodePatchContext.firstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
classDefs.firstMethodOrNull(predicate)
fun BytecodePatchContext.firstMethod(predicate: MatchPredicate<Method>) =
fun BytecodePatchContext.firstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodOrNull(predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: MatchPredicate<Method>) =
fun BytecodePatchContext.firstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
classDefs.firstMethodMutableOrNull(predicate)
fun BytecodePatchContext.firstMethodMutable(predicate: MatchPredicate<Method>) =
fun BytecodePatchContext.firstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(firstMethodMutableOrNull(predicate))
fun BytecodePatchContext.firstMethodOrNull(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
) = with(predicate) {
with(MatchContext()) {
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.match() }
}
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = with(MatchContext()) {
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.firstOrNull { it.predicate() }
}
fun BytecodePatchContext.firstMethod(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = requireNotNull(firstMethodOrNull(*strings, predicate = predicate))
fun BytecodePatchContext.firstMethodMutableOrNull(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
) = with(predicate) {
with(MatchContext()) {
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
methods.firstOrNull { it.match() }?.let { method ->
firstClassDefMutable(method.definingClass).methods.first {
MethodUtil.methodSignaturesMatch(
method, it
)
}
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = with(MatchContext()) {
strings.mapNotNull { lookupMaps.methodsByStrings[it] }.minByOrNull { it.size }?.let { methods ->
methods.firstOrNull { it.predicate() }?.let { method ->
firstClassDefMutable(method.definingClass).methods.first {
MethodUtil.methodSignaturesMatch(
method, it
)
}
}
}
}
fun BytecodePatchContext.firstMethodMutable(
vararg strings: String, predicate: MatchPredicate<Method> = MatchPredicate { true }
vararg strings: String, predicate: context(MatchContext) Method.() -> Boolean = { true }
) = requireNotNull(firstMethodMutableOrNull(*strings, predicate = predicate))
inline fun <reified C, T> ReadOnlyProperty(crossinline block: C.(KProperty<*>) -> T) =
@@ -165,67 +158,82 @@ inline fun <reified C, T> ReadOnlyProperty(crossinline block: C.(KProperty<*>) -
thisRef.block(property)
}
fun gettingFirstClassDefOrNull(predicate: MatchPredicate<ClassDef>) =
fun gettingFirstClassDefOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(predicate) }
fun gettingFirstClassDef(predicate: MatchPredicate<ClassDef>) = requireNotNull(gettingFirstClassDefOrNull(predicate))
fun gettingFirstClassDef(predicate: context(MatchContext) ClassDef.() -> Boolean) =
requireNotNull(gettingFirstClassDefOrNull(predicate))
fun gettingFirstClassDefMutableOrNull(predicate: MatchPredicate<ClassDef>) =
fun gettingFirstClassDefMutableOrNull(predicate: context(MatchContext) ClassDef.() -> Boolean) =
ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(predicate) }
fun gettingFirstClassDefMutable(predicate: MatchPredicate<ClassDef>) =
fun gettingFirstClassDefMutable(predicate: context(MatchContext) ClassDef.() -> Boolean) =
requireNotNull(gettingFirstClassDefMutableOrNull(predicate))
fun gettingFirstClassDefOrNull(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefOrNull(type, predicate) }
fun gettingFirstClassDef(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(gettingFirstClassDefOrNull(type, predicate))
fun gettingFirstClassDefMutableOrNull(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = ReadOnlyProperty<BytecodePatchContext, ClassDef?> { firstClassDefMutableOrNull(type, predicate) }
fun gettingFirstClassDefMutable(
type: String, predicate: (MatchPredicate<ClassDef>)? = null
type: String, predicate: (context(MatchContext) ClassDef.() -> Boolean)? = null
) = requireNotNull(gettingFirstClassDefMutableOrNull(type, predicate))
fun gettingFirstMethodOrNull(predicate: MatchPredicate<Method>) =
fun gettingFirstMethodOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(predicate) }
fun gettingFirstMethod(predicate: MatchPredicate<Method>) = requireNotNull(gettingFirstMethodOrNull(predicate))
fun gettingFirstMethod(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(gettingFirstMethodOrNull(predicate))
fun gettingFirstMethodMutableOrNull(predicate: MatchPredicate<Method>) =
fun gettingFirstMethodMutableOrNull(predicate: context(MatchContext) Method.() -> Boolean) =
ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(predicate) }
fun gettingFirstMethodMutable(predicate: MatchPredicate<Method>) =
fun gettingFirstMethodMutable(predicate: context(MatchContext) Method.() -> Boolean) =
requireNotNull(gettingFirstMethodMutableOrNull(predicate))
fun gettingFirstMethodOrNull(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodOrNull(*strings, predicate = predicate) }
fun gettingFirstMethod(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = requireNotNull(gettingFirstMethodOrNull(*strings, predicate = predicate))
fun gettingFirstMethodMutableOrNull(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = ReadOnlyProperty<BytecodePatchContext, Method?> { firstMethodMutableOrNull(*strings, predicate = predicate) }
fun gettingFirstMethodMutable(
vararg strings: String,
predicate: MatchPredicate<Method> = MatchPredicate { true },
predicate: context(MatchContext) Method.() -> Boolean = { true },
) = requireNotNull(gettingFirstMethodMutableOrNull(*strings, predicate = predicate))
fun interface MatchPredicate<T> {
context(MatchContext) fun T.match(): Boolean
}
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)
fun <T> Iterable<T>.matchIndexed(build: IndexedMatcher<T>.() -> Unit) =
indexedMatcher(build)(this)
context(_: MatchContext)
operator fun <T> IndexedMatcher<T>.invoke(iterable: Iterable<T>, key: String, builder: IndexedMatcher<T>.() -> Unit) =
remember(key) { IndexedMatcher<T>().apply(builder) }(iterable)
context(_: MatchContext)
fun <T> Iterable<T>.matchIndexed(key: String, build: IndexedMatcher<T>.() -> Unit) =
indexedMatcher<T>()(this, key, build)
abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
var matchIndex = -1
@@ -234,101 +242,12 @@ abstract class Matcher<T, U> : MutableList<U> by mutableListOf() {
abstract operator fun invoke(haystack: Iterable<T>): Boolean
class MatchContext internal constructor() : MutableMap<String, Any> by mutableMapOf() {
inline fun <reified V : Any> remember(key: String, defaultValue: () -> V) =
get(key) as? V ?: defaultValue().also { put(key, it) }
}
}
fun <T> slidingWindowMatcher(build: SlidingWindowMatcher<T>.() -> Unit) =
SlidingWindowMatcher<T>().apply(build)
context(MatchContext)
fun <T> Iterable<T>.matchSlidingWindow(key: String, build: SlidingWindowMatcher<T>.() -> Unit) =
remember(key) { slidingWindowMatcher(build) }(this)
fun <T> Iterable<T>.matchSlidingWindow(build: SlidingWindowMatcher<T>.() -> Unit) =
slidingWindowMatcher(build)(this)
class SlidingWindowMatcher<T>() : Matcher<T, T.() -> Boolean>() {
override operator fun invoke(haystack: Iterable<T>): Boolean {
val haystackCount = haystack.count()
val needleSize = size
if (needleSize == 0) return false
for (i in 0..(haystackCount - needleSize)) {
var matched = true
for (j in 0 until needleSize) {
if (!this[j].invoke(haystack.elementAt(i + j))) {
matched = false
break
}
}
if (matched) {
matchIndex = i
return true
}
}
matchIndex = -1
return false
}
}
fun findStringsMatcher(build: MutableList<String>.() -> Unit) =
FindStringsMatcher().apply(build)
class FindStringsMatcher() : Matcher<Instruction, String>() {
private val _matchedStrings = mutableListOf<Pair<String, Int>>()
val matchedStrings: List<Pair<String, Int>> = _matchedStrings
override fun invoke(haystack: Iterable<Instruction>): Boolean {
_matchedStrings.clear()
val remaining = indices.toMutableList()
haystack.forEachIndexed { hayIndex, instruction ->
val string = ((instruction as? ReferenceInstruction)?.reference as? StringReference)?.string
?: return@forEachIndexed
val index = remaining.firstOrNull { this[it] in string } ?: return@forEachIndexed
_matchedStrings += this[index] to hayIndex
remaining -= index
}
return remaining.isEmpty()
}
}
fun BytecodePatchContext.a() {
val match = indexedMatcher<Instruction> {
first { opcode == Opcode.OR_INT_2ADDR }
after { opcode == Opcode.RETURN_VOID }
after(atLeast = 2, atMost = 5) { opcode == Opcode.MOVE_RESULT_OBJECT }
opcode(Opcode.RETURN_VOID)
}
val myMethod = firstMethod {
implementation { match(instructions) }
}
match._indices // Mapped in same order as defined
}
fun IndexedMatcher<Instruction>.opcode(opcode: Opcode) {
after { this.opcode == opcode }
}
context(MatchContext)
fun Method.instructions(key: String, build: IndexedMatcher<Instruction>.() -> Unit) =
instructions.matchIndexed("instructions", build)
fun <T> indexedMatcher(build: IndexedMatcher<T>.() -> Unit) =
IndexedMatcher<T>().apply(build)
context(MatchContext)
fun <T> Iterable<T>.matchIndexed(key: String, build: IndexedMatcher<T>.() -> Unit) =
remember<IndexedMatcher<T>>(key) { indexedMatcher(build) }(this)
context(context: MatchContext)
inline fun <reified V : Any> remember(key: String, defaultValue: () -> V) =
context[key] as? V ?: defaultValue().also { context[key] = it }
class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
private val _indices: MutableList<Int> = mutableListOf()
@@ -395,3 +314,62 @@ class IndexedMatcher<T>() : Matcher<T, T.() -> Boolean>() {
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.
}
}
firstMethodMutable {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) && instructions.matchIndexed("anotherKey") {
first { opcode(Opcode.RETURN_VOID) }
}
}
}

View File

@@ -1,7 +1,5 @@
package app.revanced.patcher.dex.mutable
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue
import app.revanced.patcher.dex.mutable.encodedValue.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement

View File

@@ -1,9 +1,12 @@
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.Opcode
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.reference.Reference
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) =
@@ -12,3 +15,52 @@ fun <T : Reference> Instruction.reference(predicate: T.() -> Boolean) =
@Suppress("UNCHECKED_CAST")
fun <T : Reference> Instruction.reference2(predicate: T.() -> Boolean) =
((this as? DualReferenceInstruction)?.reference2 as? T)?.predicate() ?: false
fun Instruction.methodReference(predicate: MethodReference.() -> Boolean) =
reference(predicate)
fun Instruction.methodReference(methodReference: MethodReference) =
methodReference { MethodUtil.methodSignaturesMatch(methodReference, this) }
fun Instruction.fieldReference(predicate: FieldReference.() -> Boolean) =
reference(predicate)
fun Instruction.fieldReference(fieldName: String) =
fieldReference { name == fieldName }
fun Instruction.type(predicate: String.() -> Boolean) =
reference<TypeReference> { type.predicate() }
fun Instruction.type(type: String) =
type { this == type }
fun Instruction.string(predicate: String.() -> Boolean) =
reference<StringReference> { string.predicate() }
fun Instruction.string(string: String) =
string { this == string }
fun Instruction.opcode(opcode: Opcode) = this.opcode == opcode
fun Instruction.wideLiteral(wideLiteral: Long) = (this as? WideLiteralInstruction)?.wideLiteral == wideLiteral
private inline fun <reified T : Reference> Instruction.reference(): T? =
(this as? ReferenceInstruction)?.reference as? T
val Instruction.reference: Reference?
get() = reference()
val Instruction.methodReference get() =
reference<MethodReference>()
val Instruction.fieldReference get() =
reference<FieldReference>()
val Instruction.typeReference get() =
reference<TypeReference>()
val Instruction.stringReference get() =
reference<StringReference>()
val Instruction.wideLiteral get() =
(this as? WideLiteralInstruction)?.wideLiteral

View File

@@ -1,23 +1,19 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patcher.util.smali.toInstructions
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
import com.android.tools.smali.dexlib2.builder.Label
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10t
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction20t
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22t
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction30t
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction31t
import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.MethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import kotlin.collections.forEach
import kotlin.collections.indexOf
fun Method.accessFlags(vararg flags: AccessFlags) =
accessFlags.and(flags.map { it.ordinal }.reduce { acc, i -> acc or i }) != 0
/**
* Add instructions to a method at the given index.
@@ -382,3 +378,10 @@ val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instruct
* @return The label.
*/
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
/**
* A class that represents a label for an instruction.
* @param name The label name.
* @param instruction The instruction that this label is for.
*/
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)

View File

@@ -2,9 +2,9 @@
package app.revanced.patcher.util
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.dex.mutable.MutableMethod
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
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
@@ -23,12 +23,12 @@ import kotlin.reflect.KProperty
* @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference.
*/
context(BytecodePatchContext)
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
private var lastNavigatedMethodReference = startMethod
context(_: BytecodePatchContext)
private val lastNavigatedMethodInstructions
get() = with(original()) {
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
@@ -41,6 +41,7 @@ class MethodNavigator internal constructor(
*
* @return This [MethodNavigator].
*/
context(_: BytecodePatchContext)
fun to(vararg index: Int): MethodNavigator {
index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
@@ -55,6 +56,7 @@ class MethodNavigator internal constructor(
* @param index The index of the method to navigate to.
* @param predicate The predicate to match.
*/
context(_: BytecodePatchContext)
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index)
@@ -79,22 +81,27 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method mutably.
*/
fun stop() = classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
as MutableMethod
context(context: BytecodePatchContext)
fun stop() = with(context) {
classDefs.find(matchesCurrentMethodReferenceDefiningClass)!!.mutable().firstMethodBySignature
as MutableMethod
}
/**
* Get the last navigated method mutably.
*
* @return The last navigated method mutably.
*/
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = stop()
operator fun getValue(context: BytecodePatchContext?, property: KProperty<*>) =
context(requireNotNull(context)) { stop() }
/**
* Get the last navigated method immutably.
*
* @return The last navigated method immutably.
*/
fun original(): Method = classDefs.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
context(context: BytecodePatchContext)
fun original(): Method = context.classDefs.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/**
* Predicate to match the class defining the current method reference.

View File

@@ -0,0 +1,66 @@
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 dexBuilder = DexBuilder(Opcodes.getDefault())
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)
val classDef = walker.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}

View File

@@ -1,10 +0,0 @@
package app.revanced.patcher.util.smali
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
/**
* A class that represents a label for an instruction.
* @param name The label name.
* @param instruction The instruction that this label is for.
*/
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)

View File

@@ -1,95 +0,0 @@
package app.revanced.patcher.util.smali
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.LexerErrorInterface
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.InputStreamReader
import java.util.*
private const val METHOD_TEMPLATE = """
.class LInlineCompiler;
.super Ljava/lang/Object;
.method %s dummyMethod(%s)V
.registers %d
%s
.end method
"""
class InlineSmaliCompiler {
companion object {
/**
* Compiles a string of Smali code to a list of instructions.
* Special registers (such as p0, p1) will only work correctly
* if the parameters and registers of the method are passed.
*/
fun compile(
instructions: String,
parameters: String,
registers: Int,
forStaticMethod: Boolean,
): List<BuilderInstruction> {
val input =
METHOD_TEMPLATE.format(
Locale.ENGLISH,
if (forStaticMethod) {
"static"
} else {
""
},
parameters,
registers,
instructions,
)
val reader = InputStreamReader(input.byteInputStream(), Charsets.UTF_8)
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)
val result = parser.smali_file()
if (parser.numberOfSyntaxErrors > 0 || lexer.numberOfSyntaxErrors > 0) {
throw IllegalStateException(
"Encountered ${parser.numberOfSyntaxErrors} parser syntax errors and ${lexer.numberOfSyntaxErrors} lexer syntax errors!",
)
}
val treeStream = CommonTreeNodeStream(result.tree)
treeStream.tokenStream = tokens
val dexGen = smaliTreeWalker(treeStream)
dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = dexGen.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}
}
}
/**
* 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 method The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(method: MutableMethod? = null): List<BuilderInstruction> {
return InlineSmaliCompiler.compile(
this,
method?.parameters?.joinToString("") { it } ?: "",
method?.implementation?.registerCount ?: 1,
method?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true,
)
}
/**
* Compile a line of Smali code to an instruction.
* @param templateMethod The method to compile the instructions against.
* @return The instruction.
*/
fun String.toInstruction(templateMethod: MutableMethod? = null) = this.toInstructions(templateMethod).first()

View File

@@ -1,10 +1,8 @@
package app.revanced.patcher.util.smali
import app.revanced.patcher.dex.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.newLabel
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
@@ -22,7 +20,7 @@ internal object InlineSmaliCompilerTest {
@Test
fun `outputs valid instruction`() {
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
val have = "const-string v0, \"Test\"".toInstruction()
val have = "const-string v0, \"Test\"".toInstructions().first()
assertInstructionsEqual(want, have)
}