chore: Add tests, fix running patches, add DSL for patch loader

This commit is contained in:
oSumAtrIX 2024-04-26 02:24:49 +02:00
parent 8a64ccf250
commit c5f02e8c28
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
7 changed files with 340 additions and 168 deletions

View File

@ -231,6 +231,9 @@ public final class app/revanced/patcher/patch/PatchKt {
public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun bytecodePatch$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;)Ljava/util/Set;
public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;ILjava/lang/Object;)Ljava/util/Set;
public static final fun loadPatchesFromJar (Ljava/util/Set;)Ljava/util/Set;
public static final fun rawResourcePatch (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun rawResourcePatch$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;ZZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
@ -242,7 +245,7 @@ public final class app/revanced/patcher/patch/PatchKt {
}
public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public synthetic fun <init> (Ljava/lang/ClassLoader;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun add (Lapp/revanced/patcher/patch/Patch;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
@ -261,15 +264,6 @@ public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, ko
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader {
public fun <init> ([Ljava/io/File;Ljava/io/File;)V
public synthetic fun <init> ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader {
public fun <init> ([Ljava/io/File;)V
}
public class app/revanced/patcher/patch/PatchOption {
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Lkotlin/jvm/functions/Function2;)V
public final fun getDefault ()Ljava/lang/Object;

View File

@ -49,8 +49,8 @@ dependencies {
// Exclude, otherwise the org.w3c.dom API breaks.
exclude(group = "xerces", module = "xmlParserAPIs")
}
testImplementation(libs.kotlin.test)
testImplementation(libs.mockk)
}
kotlin {

View File

@ -3,6 +3,7 @@ android = "4.1.1.4"
apktool-lib = "2.9.3"
kotlin = "1.9.22"
kotlinx-coroutines-core = "1.7.3"
mockk = "1.13.10"
multidexlib2 = "3.0.3.r3"
smali = "3.0.5"
binary-compatibility-validator = "0.14.0"
@ -14,6 +15,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }

View File

@ -105,106 +105,103 @@ class Patcher(
* @param returnOnError If true, [Patcher] will return immediately if a [Patch] fails.
* @return A pair of the name of the [Patch] and its [PatchResult].
*/
override fun apply(returnOnError: Boolean) =
flow {
fun Patch<*>.execute(
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
// If the patch was executed before or failed, return it's the result.
executedPatches[this]?.let { patchResult ->
patchResult.exception ?: return patchResult
override fun apply(returnOnError: Boolean) = flow {
fun Patch<*>.execute(
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
// If the patch was executed before or failed, return it's the result.
executedPatches[this]?.let { patchResult ->
patchResult.exception ?: return patchResult
return PatchResult(this, PatchException("'$this' failed previously"))
}
// Recursively execute all dependency patches.
dependencies.forEach { dependency ->
execute(executedPatches).exception?.let {
return PatchResult(
this,
PatchException(
"'$this' depends on '$dependency' that raised an exception:\n${it.stackTraceToString()}",
),
)
}
}
// Execute the patch.
return try {
execute(context)
PatchResult(this)
} catch (exception: PatchException) {
PatchResult(this, exception)
} catch (exception: Exception) {
PatchResult(this, PatchException(exception))
}.also { executedPatches[this] = it }
return PatchResult(this, PatchException("'$this' failed previously"))
}
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
LookupMap.initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed.
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(config.resourceMode)
}
logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
context.executablePatches.sortedBy { it.name }.forEach { patch ->
val patchResult = patch.execute(executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow
} ?: run {
if (patch is Closeable) return@run
emit(patchResult)
}
}
executedPatches.values.filter { it.exception == null }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch
val result =
try {
(patch as Closeable).close()
executedPatch
} catch (exception: PatchException) {
PatchResult(patch, exception)
} catch (exception: Exception) {
PatchResult(patch, PatchException(exception))
}
result.exception?.let {
emit(
PatchResult(
patch,
PatchException(
"'$patch' raised an exception while being closed: ${it.stackTraceToString()}",
result.exception,
),
// Recursively execute all dependency patches.
dependencies.forEach { dependency ->
dependency.execute(executedPatches).exception?.let {
return PatchResult(
this,
PatchException(
"'$this' depends on '$dependency' that raised an exception:\n${it.stackTraceToString()}",
),
)
if (returnOnError) return@flow
} ?: run {
patch.name ?: return@run
emit(result)
}
}
// Execute the patch.
return try {
execute(context)
PatchResult(this)
} catch (exception: PatchException) {
PatchResult(this, exception)
} catch (exception: Exception) {
PatchResult(this, PatchException(exception))
}.also { executedPatches[this] = it }
}
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
LookupMap.initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed.
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(config.resourceMode)
}
logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
context.executablePatches.sortedBy { it.name }.forEach { patch ->
val patchResult = patch.execute(executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow
} ?: run {
emit(patchResult)
}
}
executedPatches.values.filter { it.exception == null }.asReversed().forEach { executionResult ->
val patch = executionResult.patch
val result =
try {
patch.finalize(context)
executionResult
} catch (exception: PatchException) {
PatchResult(patch, exception)
} catch (exception: Exception) {
PatchResult(patch, PatchException(exception))
}
result.exception?.let {
emit(
PatchResult(
patch,
PatchException(
"'$patch' raised an exception while being closed: ${it.stackTraceToString()}",
result.exception,
),
),
)
if (returnOnError) return@flow
} ?: run {
patch.name ?: return@run
emit(result)
}
}
}
override fun close() = LookupMap.clearLookupMaps()
/**

View File

@ -29,11 +29,11 @@ annotation class FuzzyPatternScanMethod(
/**
* A fingerprint to resolve methods.
*
* @param returnType The method's return type compared using [String.startsWith].
* @param accessFlags The method's exact access flags using values of [AccessFlags].
* @param returnType The return type compared using [String.startsWith].
* @param accessFlags The exact access flags using values of [AccessFlags].
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
* @param strings A list of the method's strings compared each using [String.contains].
* @param opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
* @param strings A list of the strings compared each using [String.contains].
* @param custom A custom condition for this fingerprint.
*/
@Suppress("MemberVisibilityCanBePrivate")
@ -312,12 +312,8 @@ class MethodFingerprint(
* - Faster: Specify [MethodFingerprint.accessFlags], [MethodFingerprint.returnType] and [MethodFingerprint.parameters].
* - Fastest: Specify [MethodFingerprint.strings], with at least one string being an exact (non-partial) match.
*/
internal fun Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodePatchContext) {
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
forEach { fingerprint ->
fingerprint.resolveUsingLookupMap(context)
}
internal fun Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodePatchContext) = forEach { fingerprint ->
fingerprint.resolveUsingLookupMap(context)
}
/**
@ -410,11 +406,11 @@ class MethodFingerprintResult(
/**
* A builder for [MethodFingerprint].
*
* @property returnType The method's return type compared using [String.startsWith].
* @property accessFlags The method's exact access flags using values of [AccessFlags].
* @property returnType The return type compared using [String.startsWith].
* @property accessFlags The exact access flags using values of [AccessFlags].
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @property opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
* @property strings A list of the method's strings compared each using [String.contains].
* @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.
*
* @constructor Creates a new [MethodFingerprintBuilder].
@ -428,34 +424,34 @@ class MethodFingerprintBuilder internal constructor() {
private var customBlock: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null
/**
* Set the method's return type.
* Set the return type.
*
* @param returnType The method's return type compared using [String.startsWith].
* @param returnType The return type compared using [String.startsWith].
*/
fun returns(returnType: String) {
infix fun returns(returnType: String) {
this.returnType = returnType
}
/**
* Set the method's access flags.
* Set the access flags.
*
* @param accessFlags The method's exact access flags using values of [AccessFlags].
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
/**
* Set the method's access flags.
* Set the access flags.
*
* @param accessFlags The method's exact access flags using values of [AccessFlags].
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: AccessFlags) {
this.accessFlags = accessFlags.value
}
/**
* Set the method's parameters.
* Set the parameters.
*
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
*/
@ -464,9 +460,9 @@ class MethodFingerprintBuilder internal constructor() {
}
/**
* Set the method's opcodes.
* Set the opcodes.
*
* @param opcodes An opcode pattern of the method's instructions.
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
@ -474,9 +470,9 @@ class MethodFingerprintBuilder internal constructor() {
}
/**
* Set the method's opcodes.
* Set the opcodes.
*
* @param instructions A list of the method's instructions or opcode names in SMALI format.
* @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.
@ -497,9 +493,9 @@ class MethodFingerprintBuilder internal constructor() {
}
/**
* Set the method's strings.
* Set the strings.
*
* @param strings A list of the method's strings compared each using [String.contains].
* @param strings A list of strings compared each using [String.contains].
*/
fun strings(vararg strings: String) {
this.strings = strings.toList()
@ -539,4 +535,4 @@ fun methodFingerprint(block: MethodFingerprintBuilder.() -> Unit) =
* @return The created [MethodFingerprint].
*/
fun BytecodePatchBuilder.methodFingerprint(block: MethodFingerprintBuilder.() -> Unit) =
MethodFingerprintBuilder().apply(block).build()() // Invoke to add to its set of fingerprints.
app.revanced.patcher.fingerprint.methodFingerprint(block)() // Invoke to add to its set of fingerprints.

View File

@ -257,17 +257,17 @@ sealed class PatchBuilder<C : PatchContext<*>>(
protected var finalizeBlock: ((C) -> Unit) = { }
/**
* Adds a compatible packages to the patch.
* Add a compatible package to the patch.
*
* @param versions The versions of the package.
*/
operator fun String.invoke(vararg versions: String) {
if (compatiblePackages == null) compatiblePackages = mutableSetOf()
compatiblePackages!!.add(this to versions.toSet())
compatiblePackages!! += this to versions.toSet()
}
/**
* Adds the patch as a dependency.
* Add the patch as a dependency.
*
* @return The added patch.
*/
@ -276,7 +276,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
}
/**
* Adds options to the patch.
* Add options to the patch.
*
* @param option The options to add.
*/
@ -285,7 +285,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
}
/**
* Sets the execution block of the patch.
* Set the execution block of the patch.
*
* @param block The execution block of the patch.
*/
@ -294,7 +294,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
}
/**
* Sets the finalizing block of the patch.
* Set the finalizing block of the patch.
*
* @param block The closing block of the patch.
*/
@ -339,7 +339,7 @@ class BytecodePatchBuilder internal constructor(
}
operator fun MethodFingerprint.getValue(nothing: Nothing?, property: KProperty<*>) = result
?: throw PatchException("Failed to resolve ${this.javaClass.simpleName}")
?: throw PatchException("Can't delegate unresolved fingerprint result to ${property.name}.")
override fun build() = BytecodePatch(
name,
@ -446,7 +446,7 @@ fun bytecodePatch(
) = BytecodePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block) as BytecodePatch
/**
* Creates a new [BytecodePatch] and adds it to the patch.
* Create a new [BytecodePatch] and add it to the patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
@ -466,7 +466,7 @@ fun PatchBuilder<*>.bytecodePatch(
) = BytecodePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block)() as BytecodePatch
/**
* Creates a new [RawResourcePatch].
* Create a new [RawResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
@ -485,7 +485,7 @@ fun rawResourcePatch(
) = RawResourcePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block) as RawResourcePatch
/**
* Creates a new [RawResourcePatch] and adds it to the patch.
* Create a new [RawResourcePatch] and add it to the patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
@ -505,7 +505,7 @@ fun PatchBuilder<*>.rawResourcePatch(
) = RawResourcePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block)() as RawResourcePatch
/**
* Creates a new [ResourcePatch].
* Create a new [ResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
@ -525,7 +525,7 @@ fun resourcePatch(
) = ResourcePatchBuilder(name, description, use, requiresIntegrations).buildPatch(block) as ResourcePatch
/**
* Creates a new [ResourcePatch] and adds it to the patch.
* Create a new [ResourcePatch] and add it to the patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
@ -568,20 +568,19 @@ class PatchResult internal constructor(val patch: Patch<*>, val exception: Patch
* Loads patches from JAR or DEX files declared as public fields or properties.
* Patches with no name are not loaded.
*
* @param classLoader The [ClassLoader] to use for loading the classes.
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
* @param patchesFiles A set of JAR or DEX files to load the patches from.
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
* @param classLoader The [ClassLoader] to use for loading the classes.
*/
sealed class PatchLoader private constructor(
classLoader: ClassLoader,
patchesFiles: Set<File>,
getBinaryClassNames: (patchesFile: File) -> List<String>,
// This constructor parameter is unfortunately necessary,
// so that a reference to the mutable set is present in the constructor to be able to add patches to it.
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
private val patchSet: MutableSet<Patch<*>> = mutableSetOf(),
) : PatchSet by patchSet {
private val getBinaryClassNames: (patchesFile: File) -> List<String>,
private val classLoader: ClassLoader,
) : PatchSet by mutableSetOf() {
init {
@Suppress("UNCHECKED_CAST", "LeakingThis")
val thisSet = this as MutableSet<Patch<*>>
patchesFiles.asSequence().flatMap(getBinaryClassNames).map {
classLoader.loadClass(it)
}.flatMap {
@ -598,9 +597,7 @@ sealed class PatchLoader private constructor(
patchFields + patchMethods
}.filter {
it.name != null
}.toList().let { patches ->
patchSet.addAll(patches)
}
}.toList().let(thisSet::addAll)
}
/**
@ -608,15 +605,15 @@ sealed class PatchLoader private constructor(
*
* @param patchesFiles The JAR files to load the patches from.
*
* @constructor Creates a new [PatchLoader] for JAR files.
* @constructor Create a new [PatchLoader] for JAR files.
*/
class Jar(vararg patchesFiles: File) : PatchLoader(
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
patchesFiles.toSet(),
internal class Jar(patchesFiles: Set<File>) : PatchLoader(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
)
/**
@ -626,21 +623,41 @@ sealed class PatchLoader private constructor(
* @param optimizedDexDirectory The directory to store optimized DEX files in.
* This parameter is deprecated and has no effect since API level 26.
*
* @constructor Creates a new [PatchLoader] for [Dex] files.
* @constructor Create a new [PatchLoader] for [Dex] files.
*/
class Dex(vararg patchesFiles: File, optimizedDexDirectory: File? = null) : PatchLoader(
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
PatchLoader::class.java.classLoader,
),
patchesFiles.toSet(),
internal class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) : PatchLoader(
patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
PatchLoader::class.java.classLoader,
),
)
}
/**
* Loads patches from JAR files.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromJar(patchesFiles: Set<File>): PatchSet =
PatchLoader.Jar(patchesFiles)
/**
* Loads patches from DEX files.
*
* @param patchesFiles The DEX files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null): PatchSet =
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)

View File

@ -0,0 +1,166 @@
package app.revanced.patcher
import app.revanced.patcher.fingerprint.methodFingerprint
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchSet
import app.revanced.patcher.patch.ResourcePatchContext
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertDoesNotThrow
import java.util.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
object PatcherTest {
private lateinit var patcher: Patcher
@BeforeEach
fun setUp() {
patcher = mockk<Patcher> {
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
setPrivateField(
"config",
mockk<PatcherConfig> {
every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
},
)
setPrivateField(
"logger",
Logger.getAnonymousLogger(),
)
every { context.bytecodeContext.classes } returns mockk(relaxed = true)
every { context.bytecodeContext.integrations } returns mockk(relaxed = true)
every { apply(false) } answers { callOriginal() }
}
}
@Test
fun `executes patches in correct order`() {
val executed = mutableListOf<String>()
val patches = setOf(
bytecodePatch { execute { executed += "1" } },
bytecodePatch {
// Directly depend on this patch.
bytecodePatch {
execute { executed += "2" }
finalize { executed += "-2" }
}
// Manually depend on this patch.
app.revanced.patcher.patch.bytecodePatch { execute { executed += "3" } }()
execute { executed += "4" }
finalize { executed += "-1" }
},
)
assert(executed.isEmpty())
patches()
assertEquals(
listOf("1", "2", "3", "4", "-1", "-2"),
executed,
"Expected patches to be executed in correct order.",
)
}
@Test
fun `throws if unresolved fingerprint result is delegated`() {
val patches = setOf(
bytecodePatch {
// Fingerprint can never be resolved.
val result by methodFingerprint {}
// Manually add the fingerprint.
app.revanced.patcher.fingerprint.methodFingerprint { }()
execute {
// Throws, because the fingerprint can't be resolved.
result.scanResult
}
},
)
assertEquals(2, patches.first().fingerprints.size)
val results = patches()
assertTrue(
results.any { it.exception != null },
"Expected an exception because the fingerprint can't be resolved.",
)
}
@Test
fun `resolves fingerprint`() {
mockClassWithMethod()
val patches = setOf(bytecodePatch { methodFingerprint { this returns "V" }() })
assertNull(
patches.first().fingerprints.first().result,
"Expected fingerprint to be unresolved before execution.",
)
patches()
assertDoesNotThrow("Expected fingerprint to be resolved.") {
assertEquals(
"V",
patches.first().fingerprints.first().result!!.method.returnType,
"Expected fingerprint to be resolved.",
)
}
}
private operator fun PatchSet.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
return runBlocking { patcher.apply(false).toList() }
}
private fun Any.setPrivateField(field: String, value: Any) {
this::class.java.getDeclaredField(field).apply {
this.isAccessible = true
set(this@setPrivateField, value)
}
}
private fun mockClassWithMethod() {
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
mutableSetOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(
ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
null,
),
),
),
),
)
}
}