Modernize patch api names, deprecate fingerprints, simplify patching code even more, add mutablemethod implementation setter, refactor tests and improve for better coverage

This commit is contained in:
oSumAtrIX
2025-12-29 07:41:07 +01:00
parent f17fbd8c40
commit 005c91bc08
13 changed files with 237 additions and 543 deletions

View File

@@ -15,7 +15,7 @@ import kotlin.test.assertNotNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
object MatchingTest : PatcherTestBase() {
@BeforeAll
fun setUp() = setUpMock()
fun setup() = setupMock()
@Test
fun `matches via builder api`() {
@@ -37,7 +37,7 @@ object MatchingTest : PatcherTestBase() {
}
bytecodePatch {
execute {
apply {
assertNotNull(firstMethodBuilder().methodOrNull) { "Expected to find a method" }
Assertions.assertNull(firstMethodBuilder(fail = true).methodOrNull) { "Expected to not find a method" }
}
@@ -47,7 +47,7 @@ object MatchingTest : PatcherTestBase() {
@Test
fun `matches via declarative api`() {
bytecodePatch {
execute {
apply {
val method = firstMethodByDeclarativePredicateOrNull {
anyOf {
predicate { name == "method" }
@@ -66,7 +66,7 @@ object MatchingTest : PatcherTestBase() {
@Test
fun `predicate matcher works correctly`() {
bytecodePatch {
execute {
apply {
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
}
}

View File

@@ -13,27 +13,7 @@ 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
/**
* 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 opcodes A pattern of instruction opcodes. `null` can be used as a wildcard.
* @param strings A list of the strings. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
* @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
*/
@Deprecated("Use the matcher API instead.")
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
@@ -47,26 +27,10 @@ class Fingerprint internal constructor(
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
context(_: BytecodePatchContext)
private val matchOrNull: Match?
get() = matchOrNull()
/**
* Match using [BytecodePatchContext.lookupMaps].
*
* Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match.
*
* Fingerprints can be optimized for performance:
* - Slowest: Specify [custom] or [opcodes] and nothing else.
* - Fast: Specify [accessFlags], [returnType].
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
context(context: BytecodePatchContext)
internal fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
@@ -90,12 +54,6 @@ class Fingerprint internal constructor(
return null
}
/**
* 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,
@@ -110,25 +68,11 @@ class Fingerprint internal constructor(
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 already matched to a method, null otherwise.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
/**
* 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 already matched to a method, null otherwise.
*/
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
@@ -252,171 +196,76 @@ class Fingerprint internal constructor(
private val exception get() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @throws PatchException If the [Fingerprint] has not been matched.
*/
context(_: BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
/**
* 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.
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
/**
* 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 already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
/**
* 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 already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
/**
* The class the matching method is a member of.
*/
context(_: BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
/**
* The matching method.
*/
context(_: BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a [ClassProxy].
* 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 [ClassProxy].
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(_: BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
/**
* The match for the opcode pattern.
*/
context(_: BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
/**
* The matches for the strings.
*/
context(_: BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
/**
* 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 [ClassProxy].
* 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 [ClassProxy].
* 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)
val patternMatch
get() = match.patternMatch
/**
* The matches for the strings.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(_: BytecodePatchContext)
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 patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings.
*/
@Deprecated("Use the matcher API instead.")
class Match internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
@@ -424,54 +273,24 @@ class Match internal constructor(
val patternMatch: PatternMatch?,
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 { context.classDefs[originalClassDef.type]!! }
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*/
val classDef by lazy {
val classDef = context.classDefs[originalClassDef.type]!!
context.classDefs.getOrReplaceMutable(classDef)
}
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
/**
* 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.
*/
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.
*/
class StringMatch internal constructor(val string: String, val index: Int)
}
/**
* 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 opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
* @property strings A list of the strings compared each using [String.contains].
* @property customBlock A custom condition for this fingerprint.
* @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning.
*
* @constructor Create a new [FingerprintBuilder].
*/
@Deprecated("Use the matcher API instead.")
class FingerprintBuilder internal constructor(
private val fuzzyPatternScanThreshold: Int = 0,
) {
@@ -482,63 +301,26 @@ class FingerprintBuilder internal constructor(
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.
*
* @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()
}
/**
* Set the opcodes.
*
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
this.opcodes = opcodes.toList()
}
/**
* Set the opcodes.
*
* @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) {
this.opcodes = instructions.trimIndent().split("\n").filter {
it.isNotBlank()
@@ -551,20 +333,10 @@ class FingerprintBuilder internal constructor(
}
}
/**
* Set the strings.
*
* @param strings A list of strings compared each using [String.contains].
*/
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
}
@@ -584,14 +356,7 @@ class FingerprintBuilder internal constructor(
}
}
/**
* Create a [Fingerprint].
*
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
* @param block The block to build the [Fingerprint].
*
* @return The created [Fingerprint].
*/
@Deprecated("Use the matcher API instead.")
fun fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,

View File

@@ -38,11 +38,7 @@ fun patcher(
val patches = getPatches(packageName, versionName)
return { emit: (PatchResult) -> Unit ->
if (patches.any { patch ->
fun Patch.check(): Boolean = type == PatchType.RESOURCE || dependencies.any { it.check() }
patch.check()
}
) resourcePatchContext.decodeResources()
if (patches.any { patch -> patch.patchesResources }) resourcePatchContext.decodeResources()
// After initializing the resource context, to keep memory usage time low.
val bytecodePatchContext = BytecodePatchContext(
@@ -54,74 +50,67 @@ fun patcher(
bytecodePatchContext.classDefs.initializeCache()
logger.info("Executing patches")
logger.info("Applying patches")
patches.execute(bytecodePatchContext, resourcePatchContext, emit)
patches.apply(bytecodePatchContext, resourcePatchContext, emit)
}
}
// Public for testing.
fun Set<Patch>.execute(
fun Set<Patch>.apply(
bytecodePatchContext: BytecodePatchContext,
resourcePatchContext: ResourcePatchContext,
emit: (PatchResult) -> Unit
): PatchesResult {
val executedPatches = LinkedHashMap<Patch, PatchResult>()
val appliedPatches = LinkedHashMap<Patch, PatchResult>()
sortedBy { it.name }.forEach { patch ->
fun Patch.execute(): PatchResult {
val result = executedPatches[this]
if (result != null) {
if (result.exception == null) return result
return patchResult(PatchException("The patch '$this' has failed previously"))
}
fun Patch.apply(): PatchResult {
val result = appliedPatches[this]
val failedDependency = dependencies.asSequence().map { it.execute() }.firstOrNull { it.exception != null }
if (failedDependency != null) {
return patchResult(
PatchException(
"The dependant patch \"$failedDependency\" of the patch \"$this\"" +
" raised an exception:\n${failedDependency.exception!!.stackTraceToString()}",
),
return if (result == null) {
val failedDependency = dependencies.asSequence().map { it.apply() }.firstOrNull { it.exception != null }
if (failedDependency != null) return patchResult(
"The dependant patch \"$failedDependency\" of the patch \"$this\" raised an exception:\n" +
failedDependency.exception!!.stackTraceToString(),
)
}
val exception = runCatching {
execute(bytecodePatchContext, resourcePatchContext)
}.exceptionOrNull() as? Exception
val exception = runCatching { apply(bytecodePatchContext, resourcePatchContext) }
.exceptionOrNull() as? Exception
return patchResult(exception).also { executedPatches[this] = it }
patchResult(exception).also { result -> appliedPatches[this] = result }
} else if (result.exception == null) result
else patchResult("The patch '$this' has failed previously")
}
val patchResult = patch.execute()
val patchResult = patch.apply()
// If an exception occurred or the patch has no finalize block, emit the result.
if (patchResult.exception != null || patch.finalize == null) {
if (patchResult.exception != null || patch.afterDependents == null) {
emit(patchResult)
}
}
val succeededPatchesWithFinalizeBlock = executedPatches.values.filter {
it.exception == null && it.patch.finalize != null
val succeededPatchesWithFinalizeBlock = appliedPatches.values.filter {
it.exception == null && it.patch.afterDependents != null
}
succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult ->
val patch = executionResult.patch
runCatching { patch.finalize!!.invoke(bytecodePatchContext, resourcePatchContext) }
.fold(
{ emit(executionResult) },
{
emit(
PatchResult(
patch,
PatchException(
"The patch \"$patch\" raised an exception:\n${it.stackTraceToString()}",
it,
),
)
succeededPatchesWithFinalizeBlock.asReversed().forEach { result ->
val patch = result.patch
runCatching { patch.afterDependents!!.invoke(bytecodePatchContext, resourcePatchContext) }.fold(
{ emit(result) },
{
emit(
PatchResult(
patch,
PatchException(
"The patch \"$patch\" raised an exception:\n" + it.stackTraceToString(),
it,
),
)
}
)
)
}
)
}
return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get())

View File

@@ -20,6 +20,8 @@ enum class PatchType(internal val prefix: String) {
RESOURCE("Resource")
}
internal val Patch.patchesResources: Boolean get() = type == PatchType.RESOURCE || dependencies.any { it.patchesResources }
open class Patch internal constructor(
val name: String?,
val description: String?,
@@ -27,10 +29,10 @@ open class Patch internal constructor(
val dependencies: Set<Patch>,
val compatiblePackages: Set<Package>?,
options: Set<Option<*>>,
internal val execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
internal val apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
// Must be nullable, so that Patcher.invoke can check,
// if a patch has "finalize" in order to not emit it twice.
internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
// if a patch has an "afterDependents" in order to not emit it twice.
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
internal val type: PatchType,
) {
val options = Options(options)
@@ -46,18 +48,18 @@ sealed class PatchBuilder<C : PatchContext<*>>(
private val dependencies = mutableSetOf<Patch>()
private val options = mutableSetOf<Option<*>>()
internal var execute: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
internal var finalize: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
internal var apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
context(_: BytecodePatchContext, _: ResourcePatchContext)
private val patchContext get() = getPatchContext()
fun execute(block: C.() -> Unit) {
execute = { block(patchContext) }
fun apply(block: C.() -> Unit) {
apply = { block(patchContext) }
}
fun finalize(block: C.() -> Unit) {
finalize = { block(patchContext) }
fun afterDependents(block: C.() -> Unit) {
afterDependents = { block(patchContext) }
}
operator fun <T> Option<T>.invoke() = apply {
@@ -90,8 +92,8 @@ sealed class PatchBuilder<C : PatchContext<*>>(
dependencies,
compatiblePackages,
options,
execute,
finalize,
apply,
afterDependents,
type,
)
}
@@ -101,7 +103,7 @@ class BytecodePatchBuilder private constructor(
) : PatchBuilder<BytecodePatchContext>(
PatchType.BYTECODE,
{
// Extend the context with the extension, before returning it to the patch for execution.
// Extend the context with the extension, before returning it to the patch before applying it.
contextOf<BytecodePatchContext>().apply {
if (extensionInputStream != null) extendWith(extensionInputStream)
}
@@ -109,7 +111,7 @@ class BytecodePatchBuilder private constructor(
) {
internal constructor() : this(null)
fun extendWith(extension: String) = apply {
fun extendWith(extension: String) {
// Should be the classloader which loaded the patch class.
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
@@ -198,9 +200,9 @@ class PatchException(errorMessage: String?, cause: Throwable?) : Exception(error
}
/**
* A result of executing a [Patch].
* A result of applying a [Patch].
*
* @param patch The [Patch] that was executed.
* @param patch The [Patch] that ran.
* @param exception The [PatchException] thrown, if any.
*/
class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null)
@@ -212,6 +214,14 @@ class PatchResult internal constructor(val patch: Patch, val exception: PatchExc
* @return The created [PatchResult].
*/
internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException())
/**
* Creates a [PatchResult] for this [Patch] with the given error message.
*
* @param errorMessage The error message.
* @return The created [PatchResult].
*/
internal fun Patch.patchResult(errorMessage: String) = PatchResult(this, PatchException(errorMessage))
private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this)
/**

View File

@@ -12,8 +12,8 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method {
private var accessFlags = method.accessFlags
private var returnType = method.returnType
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count)
private val _implementation by lazy { method.implementation?.let { MutableMethodImplementation(it) } }
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count).
private var implementation = method.implementation?.let(::MutableMethodImplementation)
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
@@ -35,6 +35,10 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method {
this.returnType = returnType
}
fun setImplementation(implementation: MutableMethodImplementation?) {
this.implementation = implementation
}
override fun getDefiningClass() = definingClass
override fun getName() = name
@@ -51,7 +55,7 @@ class MutableMethod(method: Method) : BaseMethodReference(), Method {
override fun getParameters() = _parameters
override fun getImplementation() = _implementation
override fun getImplementation() = implementation
companion object {
fun Method.toMutable() = MutableMethod(this)

View File

@@ -0,0 +1,27 @@
package app.revanced.patcher
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class FingerprintTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `matches fingerprints correctly`() {
with(bytecodePatchContext) {
assertNotNull(
fingerprint { returns("V") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
assertNull(
fingerprint { returns("doesnt exist") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
}
}
}

View File

@@ -5,154 +5,87 @@ import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class PatcherTest : PatcherTestBase() {
@BeforeAll
fun setUp() = setUpMock()
fun setup() = setupMock()
@Test
fun `executes patches in correct order`() {
val executed = mutableListOf<String>()
fun `applies patches in correct order`() {
val applied = mutableListOf<String>()
val patches = setOf(
bytecodePatch { execute { executed += "1" } },
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "2" }
finalize { executed += "-2" }
},
bytecodePatch { execute { executed += "3" } },
)
infix fun Patch.resultsIn(equals: List<String>) = this to equals
infix fun Pair<Patch, List<String>>.because(reason: String) {
runCatching { setOf(first)() }
execute { executed += "4" }
finalize { executed += "-1" }
},
)
assertEquals(second, applied, reason)
assert(executed.isEmpty())
patches()
assertEquals(
listOf("1", "2", "3", "4", "-1", "-2"),
executed,
"Expected patches to be executed in correct order.",
)
}
@Test
fun `handles execution of patches correctly when exceptions occur`() {
val executed = mutableListOf<String>()
infix fun Patch.resultsIn(equals: List<String>) {
val patches = setOf(this)
try {
patches()
} catch (_: PatchException) {
// Swallow expected exceptions for testing purposes.
}
assertEquals(equals, executed, "Expected patches to be executed in correct order.")
executed.clear()
applied.clear()
}
// No patches execute successfully,
// because the dependency patch throws an exception inside the execute block.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { throw PatchException("1") }
finalize { executed += "-2" }
apply { applied += "1" }
afterDependents { applied += "-2" }
},
bytecodePatch { apply { applied += "2" } },
)
apply { applied += "3" }
afterDependents { applied += "-1" }
} resultsIn listOf("1", "2", "3", "-1", "-2") because
"Patches should apply in post-order and afterDependents in pre-order."
execute { executed += "2" }
finalize { executed += "-1" }
} resultsIn emptyList()
// The dependency patch is executed successfully,
// because only the dependant patch throws an exception inside the execute block.
// Patches that depend on a failed patch should not be executed,
// but patches that are dependant by a failed patch should be finalized.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { executed += "-2" }
apply { throw PatchException("1") }
afterDependents { applied += "-2" }
},
)
apply { applied += "2" }
afterDependents { applied += "-1" }
} resultsIn emptyList() because
"Patches that depend on a patched that failed to apply should not be applied."
execute { throw PatchException("2") }
finalize { executed += "-1" }
} resultsIn listOf("1", "-2")
// Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch,
// the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { throw PatchException("-2") }
apply { applied += "1" }
afterDependents { applied += "-2" }
},
)
apply { throw PatchException("2") }
afterDependents { applied += "-1" }
} resultsIn listOf("1", "-2") because
"afterDependents of a patch should not be called if it failed to apply."
execute { executed += "2" }
finalize { executed += "-1" }
} resultsIn listOf("1", "2", "-1")
// The dependency patch is executed successfully,
// because the dependant patch raises an exception in the finalize block.
// Patches that depend on a failed patch should not be executed,
// but patches that are depended on by a failed patch should be executed.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { executed += "-2" }
apply { applied += "1" }
afterDependents { applied += "-2" }
},
)
execute { executed += "2" }
finalize { throw PatchException("-1") }
} resultsIn listOf("1", "2", "-2")
apply { applied += "2" }
afterDependents { throw PatchException("-1") }
} resultsIn listOf("1", "2", "-2") because
"afterDependents of a patch should be called " +
"regardless of dependant patches failing."
}
@Test
fun `throws if unmatched fingerprint match is used`() {
with(bytecodePatchContext) {
val fingerprint = fingerprint {
strings("doesnt exist")
}
val fingerprint = fingerprint { strings("doesnt exist") }
assertThrows<PatchException>("Expected an exception because the fingerprint can't match.") {
fingerprint.patternMatch
}
}
}
@Test
fun `matches fingerprint`() {
val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") }
val fingerprint3 = fingerprint { returns("V") }
with(bytecodePatchContext) {
assertAll(
"Expected fingerprints to match.",
{ assertNotNull(fingerprint.matchOrNull(this.classDefs.first().methods.first())) },
{ assertNotNull(fingerprint2.matchOrNull(this.classDefs.first())) },
{ assertNotNull(fingerprint3.originalClassDefOrNull) },
)
}
}
}

View File

@@ -1,8 +1,5 @@
package app.revanced.patcher
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
@@ -10,28 +7,30 @@ import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import kotlin.test.assertEquals
private object InstructionExtensionsTest {
private lateinit var testMethod: MutableMethod
private lateinit var testMethodImplementation: MutableMethodImplementation
internal class MethodExtensionsTest {
private val testInstructions = (0..9).map { i -> TestInstruction(i) }
private var method = ImmutableMethod(
"TestClass;",
"testMethod",
null,
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(16)
).toMutable()
@BeforeEach
fun createTestMethod() =
ImmutableMethod(
"TestClass;",
"testMethod",
null,
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(16).also { testMethodImplementation = it }.apply {
repeat(10) { i -> this.addInstruction(TestInstruction(i)) }
},
).let { testMethod = it.toMutable() }
fun setup() {
method.instructions.clear()
method.addInstructions(testInstructions)
}
@Test
fun addInstructionsToImplementationIndexed() =
@@ -219,11 +218,11 @@ private object InstructionExtensionsTest {
// region Helper methods
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
testMethodImplementation.apply(block)
method.implementation!!.apply(block)
}
private fun applyToMethod(block: MutableMethod.() -> Unit) {
testMethod.apply(block)
method.apply(block)
}
private fun MutableMethodImplementation.assertRegisterIs(

View File

@@ -1,10 +1,13 @@
package app.revanced.patcher.patch.options
package app.revanced.patcher.patch
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.PatchBuilder.invoke
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.reflect.typeOf
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
internal object OptionsTest {
private val externalOption = stringOption("external", "default")
@@ -141,4 +144,4 @@ internal object OptionsTest {
}
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
}
}

View File

@@ -1,44 +0,0 @@
@file:Suppress("unused")
package app.revanced.patcher.patch
import org.junit.jupiter.api.Test
import kotlin.reflect.jvm.javaField
import kotlin.test.assertEquals
internal object PatchLoaderTest {
@Test
fun `loads patches correctly`() {
val patchesClass = ::Public.javaField!!.declaringClass.name
val classLoader = ::Public.javaClass.classLoader
val patches = getPatches(listOf(patchesClass), classLoader)
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
}
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
val Public by creatingBytecodePatch {} // Loaded, because it's named.
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
publicNamedPatchFunction() // Not loaded, because it's parameterized.
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.

View File

@@ -1,5 +1,6 @@
package app.revanced.patcher.patch
import kotlin.reflect.jvm.javaField
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -38,7 +39,7 @@ internal object PatchTest {
val print by stringOption("print")
val custom = option<String>("custom")()
execute {
this.apply {
println(print)
println(custom.value)
}
@@ -46,4 +47,39 @@ internal object PatchTest {
assertEquals(2, patch.options.size)
}
@Test
fun `loads patches correctly`() {
val patchesClass = ::Public.javaField!!.declaringClass.name
val classLoader = ::Public.javaClass.classLoader
val patches = getPatches(listOf(patchesClass), classLoader)
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
}
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
val Public by creatingBytecodePatch {} // Loaded, because it's named.
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
publicNamedPatchFunction() // Not loaded, because it's parameterized.
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.

View File

@@ -1,43 +1,39 @@
package app.revanced.patcher.util
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import java.util.*
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal object SmaliTest {
@Test
fun `outputs valid instruction`() {
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
val have = "const-string v0, \"Test\"".toInstructions().first()
internal class SmaliTest {
val method = ImmutableMethod(
"Ldummy;",
"name",
emptyList(), // parameters
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(1),
).toMutable()
assertInstructionsEqual(want, have)
@BeforeEach
fun setup() {
method.instructions.clear()
}
@Test
fun `supports branching with own branches`() {
val method = createMethod()
val instructionCount = 8
val instructionIndex = instructionCount - 2
val targetIndex = instructionIndex - 1
method.addInstructions(
arrayOfNulls<String>(instructionCount).also {
Arrays.fill(it, "const/4 v0, 0x0")
}.joinToString("\n"),
)
fun `own branches work`() {
method.addInstructionsWithLabels(
targetIndex,
0,
"""
:test
const/4 v0, 0x1
@@ -45,14 +41,13 @@ internal object SmaliTest {
""",
)
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(0).target.location.index
assertEquals(targetIndex, instruction.target.location.index)
assertEquals(0, targetLocationIndex, "Label should point to index 0")
}
@Test
fun `supports branching to outside branches`() {
val method = createMethod()
fun `external branches work`() {
val instructionIndex = 3
val labelIndex = 1
@@ -63,10 +58,8 @@ internal object SmaliTest {
""",
)
assertEquals(labelIndex, method.newLabel(labelIndex).location.index)
method.addInstructionsWithLabels(
method.implementation!!.instructions.size,
method.instructions.size,
"""
const/4 v0, 0x1
if-eqz v0, :test
@@ -76,29 +69,8 @@ internal object SmaliTest {
)
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertTrue(instruction.target.isPlaced, "Label was not placed")
assertTrue(instruction.target.isPlaced, "Label should be placed")
assertEquals(labelIndex, instruction.target.location.index)
}
private fun createMethod(
name: String = "dummy",
returnType: String = "V",
accessFlags: Int = AccessFlags.STATIC.value,
registerCount: Int = 1,
) = ImmutableMethod(
"Ldummy;",
name,
emptyList(), // parameters
returnType,
accessFlags,
emptySet(),
emptySet(),
MutableMethodImplementation(registerCount),
).toMutable()
private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) {
assertEquals(want.opcode, have.opcode)
assertEquals(want.format, have.format)
assertEquals(want.codeUnits, have.codeUnits)
}
}

View File

@@ -21,7 +21,7 @@ abstract class PatcherTestBase {
protected lateinit var bytecodePatchContext: BytecodePatchContext
protected lateinit var resourcePatchContext: ResourcePatchContext
protected fun setUpMock(
protected fun setupMock(
method: ImmutableMethod = ImmutableMethod(
"class",
"method",
@@ -89,7 +89,7 @@ abstract class PatcherTestBase {
protected operator fun Set<Patch>.invoke() {
runCatching {
execute(
apply(
bytecodePatchContext,
resourcePatchContext
) { }