mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-10 21:36:16 +00:00
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:
@@ -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" } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) { }
|
||||
|
||||
Reference in New Issue
Block a user