1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-10 05:30:49 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
oSumAtrIX
1f0eab36d9 fix build 2024-10-27 04:32:40 +01:00
14 changed files with 367 additions and 362 deletions

View File

@@ -1,4 +1,7 @@
public final class app/revanced/patcher/Fingerprint {
public final fun getMatch ()Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z
}
public final class app/revanced/patcher/FingerprintBuilder {
@@ -15,17 +18,20 @@ public final class app/revanced/patcher/FingerprintBuilder {
public final class app/revanced/patcher/FingerprintKt {
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
public static final fun fingerprint (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
public static synthetic fun fingerprint$default (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
}
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
}
public final class app/revanced/patcher/Match {
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/Match$PatternMatch;Ljava/util/List;Lapp/revanced/patcher/patch/BytecodePatchContext;)V
public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List;
}
@@ -57,8 +63,8 @@ public final class app/revanced/patcher/Patcher : java/io/Closeable {
}
public final class app/revanced/patcher/PatcherConfig {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
@@ -129,27 +135,30 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
}
public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public final fun getExtension ()Ljava/io/InputStream;
public final fun getFingerprints ()Ljava/util/Set;
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V
public final fun getExtension ()Ljava/io/InputStream;
public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public final fun setExtension (Ljava/io/InputStream;)V
}
public final class app/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint {
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
}
public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public fun close ()V
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/util/Set;
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
public final fun getMatch (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Match;
public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
}
@@ -277,7 +286,7 @@ public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jv
}
public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun getCompatiblePackages ()Ljava/util/Set;
@@ -294,13 +303,13 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun compatibleWith ([Ljava/lang/String;)V
public final fun compatibleWith ([Lkotlin/Pair;)V
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
public final fun execute (Lkotlin/jvm/functions/Function1;)V
public final fun finalize (Lkotlin/jvm/functions/Function1;)V
public final fun execute (Lkotlin/jvm/functions/Function2;)V
public final fun finalize (Lkotlin/jvm/functions/Function2;)V
protected final fun getCompatiblePackages ()Ljava/util/Set;
protected final fun getDependencies ()Ljava/util/Set;
protected final fun getDescription ()Ljava/lang/String;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set;
protected final fun getUse ()Z
@@ -308,8 +317,8 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
protected final fun setCompatiblePackages (Ljava/util/Set;)V
protected final fun setDependencies (Ljava/util/Set;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V
}
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {

View File

@@ -89,9 +89,9 @@ val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { pat
runBlocking {
patcher().collect { patchResult ->
if (patchResult.exception != null)
logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" }
logger.info("\"${patchResult.patch}\" failed:\n${patchResult.exception}")
else
logger.info { "\"${patchResult.patch}\" succeeded" }
logger.info("\"${patchResult.patch}\" succeeded")
}
}

View File

@@ -72,10 +72,6 @@ To start developing patches with ReVanced Patcher, you must prepare a developmen
Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.
> [!NOTE]
> To start a fresh project,
> you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template).
1. Clone the repository
```bash

File diff suppressed because it is too large Load Diff

View File

@@ -76,23 +76,23 @@ val disableAdsPatch = bytecodePatch(
) {
compatibleWith("com.some.app"("1.0.0"))
// Patches can depend on other patches, executing them first.
// Resource patch disables ads by patching resource files.
dependsOn(disableAdsResourcePatch)
// Merge precompiled DEX files into the patched app, before the patch is executed.
// Precompiled DEX file to be merged into the patched app.
extendWith("disable-ads.rve")
// Fingerprint to find the method to patch.
val showAdsMatch by showAdsFingerprint {
// More about fingerprints on the next page of the documentation.
}
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch.
val showAdsMatch by showAdsFingerprint {
// More about fingerprints on the next page of the documentation.
}
// In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads.
showAdsMatch.method.addInstructions(
showAdsMatch.mutableMethod.addInstructions(
0,
"""
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
@@ -146,10 +146,10 @@ loadPatchesJar(patches).apply {
The type of an option can be obtained from the `type` property of the option:
```kt
option.type // The KType of the option. Captures the full type information of the option.
option.type // The KType of the option.
```
Options can be declared outside a patch and added to a patch manually:
Options can be declared outside of a patch and added to a patch manually:
```kt
val option = stringOption(key = "option")
@@ -183,9 +183,11 @@ and use it in a patch:
```kt
val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve")
val match by methodFingerprint()
execute {
fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
}
}
```

View File

@@ -96,21 +96,21 @@ Example of patches:
@Surpress("unused")
val bytecodePatch = bytecodePatch {
execute {
// More about this on the next page of the documentation.
// TODO
}
}
@Surpress("unused")
val rawResourcePatch = rawResourcePatch {
execute {
// More about this on the next page of the documentation.
execute {
// TODO
}
}
@Surpress("unused")
val resourcePatch = resourcePatch {
execute {
// More about this on the next page of the documentation.
execute {
// TODO
}
}
```

View File

@@ -4,11 +4,13 @@ A handful of APIs are available to make patch development easier and more effici
## 📙 Overview
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
3. 🏃‍ Navigate method calls recursively by index with `navigate(Method).at(index)`
4. 💾 Read and write resource files with `get(Path, Boolean)`
5. 📃 Read and write DOM files using `document`
1. 👹 Mutate classes with `context.proxy(ClassDef)`
2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)`
3. 🏃‍ Easily access referenced methods recursively by index with `MethodNavigator`
4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications
(Available in ReVanced Patches)
5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)`
6. 📃 Read and write DOM files using `ResourcePatchContext.document`
### 🧰 APIs

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,16 @@ import java.util.logging.Logger
* @param temporaryFilesPath A path to a folder to store temporary files in.
* @param aaptBinaryPath A path to a custom aapt binary.
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
* This has impact on memory usage and performance.
*/
class PatcherConfig(
internal val apkFile: File,
private val temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null,
@Deprecated("This is going to be removed in the future because it is not needed anymore.")
internal val multithreadingDexFileWriter: Boolean = false,
) {
private val logger = Logger.getLogger(PatcherConfig::class.java.name)

View File

@@ -1,6 +1,8 @@
package app.revanced.patcher.patch
import app.revanced.patcher.*
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
@@ -21,7 +23,6 @@ import java.io.Closeable
import java.io.FileFilter
import java.util.*
import java.util.logging.Logger
import kotlin.reflect.KProperty
/**
* A context for patches containing the current state of the bytecode.
@@ -52,52 +53,19 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
).also { opcodes = it.opcodes }.classes.toMutableList(),
)
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
val Fingerprint.match get() = match(this@BytecodePatchContext)
/**
* 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.
*/
fun Fingerprint.match(classDef: ClassDef) = match(this@BytecodePatchContext, classDef)
/**
* 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.
*/
fun Fingerprint.match(method: Method) = match(this@BytecodePatchContext, method)
/**
* Get the match for this [Fingerprint].
*
* @throws IllegalStateException If the [Fingerprint] has not been matched.
*/
operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = _match
?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".")
/**
* The lookup maps for methods and the class they are a member of from the [classes].
*/
internal val lookupMaps by lazy { LookupMaps(classes) }
/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
* If no extension is present, the function will return early.
*
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
* Merge the extension of this patch.
*/
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
internal fun BytecodePatch.mergeExtension() {
extension?.use { extensionStream ->
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
logger.fine("Adding class \"$classDef\"")
classes += classDef
lookupMaps.classesByType[classDef.type] = classDef
@@ -105,7 +73,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
@@ -117,9 +85,18 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
classes += mergedClass
}
}
} ?: logger.fine("No extension to merge")
} ?: return logger.fine("No extension to merge")
}
/**
* Find a class by its type using a contains check.
*
* @param type The type of the class.
* @return A proxy for the first class that matches the type.
*/
@Deprecated("Use classBy { type in it.type } instead.", ReplaceWith("classBy { type in it.type }"))
fun classByType(type: String) = classBy { type in it.type }
/**
* Find a class with a predicate.
*
@@ -168,7 +145,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
if (config.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
@@ -178,7 +155,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
override fun getOpcodes() = this@BytecodePatchContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()

View File

@@ -2,6 +2,7 @@
package app.revanced.patcher.patch
import app.revanced.patcher.Fingerprint
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext
import dalvik.system.DexClassLoader
@@ -13,8 +14,8 @@ import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.net.URLClassLoader
import java.util.function.Supplier
import java.util.jar.JarFile
import kotlin.reflect.KProperty
typealias PackageName = String
typealias VersionName = String
@@ -45,10 +46,10 @@ sealed class Patch<C : PatchContext<*>>(
val dependencies: Set<Patch<*>>,
val compatiblePackages: Set<Package>?,
options: Set<Option<*>>,
private val executeBlock: (C) -> Unit,
private val executeBlock: Patch<C>.(C) -> Unit,
// Must be internal and nullable, so that Patcher.invoke can check,
// if a patch has a finalizing block in order to not emit it twice.
internal var finalizeBlock: ((C) -> Unit)?,
internal var finalizeBlock: (Patch<C>.(C) -> Unit)?,
) {
/**
* The options of the patch.
@@ -56,35 +57,35 @@ sealed class Patch<C : PatchContext<*>>(
val options = Options(options)
/**
* Calls the execution block of the patch.
* This function is called by [Patcher.invoke].
* Runs the execution block of the patch.
* Called by [Patcher].
*
* @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
*/
internal abstract fun execute(context: PatcherContext)
/**
* Calls the execution block of the patch.
* Runs the execution block of the patch.
*
* @param context The [PatchContext] to execute the patch with.
*/
fun execute(context: C) = executeBlock(context)
/**
* Calls the finalizing block of the patch.
* This function is called by [Patcher.invoke].
* Runs the finalizing block of the patch.
* Called by [Patcher].
*
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
*/
internal abstract fun finalize(context: PatcherContext)
/**
* Calls the finalizing block of the patch.
* Runs the finalizing block of the patch.
*
* @param context The [PatchContext] to finalize the patch with.
*/
fun finalize(context: C) {
finalizeBlock?.invoke(context)
finalizeBlock?.invoke(this, context)
}
override fun toString() = name ?: "Patch"
@@ -126,7 +127,8 @@ internal fun Iterable<Patch<*>>.forEachRecursively(
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @property extensionInputStream Getter for the extension input stream of the patch.
* @param fingerprints The fingerprints that are resolved before the patch is executed.
* @property extension An input stream of the extension resource this patch uses.
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
@@ -141,9 +143,10 @@ class BytecodePatch internal constructor(
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
val extensionInputStream: Supplier<InputStream>?,
executeBlock: (BytecodePatchContext) -> Unit,
finalizeBlock: ((BytecodePatchContext) -> Unit)?,
val fingerprints: Set<Fingerprint>,
val extension: InputStream?,
executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
finalizeBlock: (Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit)?,
) : Patch<BytecodePatchContext>(
name,
description,
@@ -155,7 +158,14 @@ class BytecodePatch internal constructor(
finalizeBlock,
) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
mergeExtension(this@BytecodePatch)
with(context.bytecodeContext) {
mergeExtension()
}
fingerprints.forEach {
it.match(this)
}
execute(this)
}
@@ -188,8 +198,8 @@ class RawResourcePatch internal constructor(
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
executeBlock: (ResourcePatchContext) -> Unit,
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>(
name,
description,
@@ -231,8 +241,8 @@ class ResourcePatch internal constructor(
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
executeBlock: (ResourcePatchContext) -> Unit,
finalizeBlock: ((ResourcePatchContext) -> Unit)?,
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>(
name,
description,
@@ -277,8 +287,8 @@ sealed class PatchBuilder<C : PatchContext<*>>(
protected var dependencies = mutableSetOf<Patch<*>>()
protected val options = mutableSetOf<Option<*>>()
protected var executionBlock: ((C) -> Unit) = { }
protected var finalizeBlock: ((C) -> Unit)? = null
protected var executionBlock: (Patch<C>.(C) -> Unit) = { }
protected var finalizeBlock: (Patch<C>.(C) -> Unit)? = null
/**
* Add an option to the patch.
@@ -337,7 +347,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
*
* @param block The execution block of the patch.
*/
fun execute(block: C.() -> Unit) {
fun execute(block: Patch<C>.(C) -> Unit) {
executionBlock = block
}
@@ -346,7 +356,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
*
* @param block The finalizing block of the patch.
*/
fun finalize(block: C.() -> Unit) {
fun finalize(block: Patch<C>.(C) -> Unit) {
finalizeBlock = block
}
@@ -375,7 +385,8 @@ private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property extensionInputStream Getter for the extension input stream of the patch.
* @property fingerprints The fingerprints that are resolved before the patch is executed.
* @property extension An input stream of the extension resource this patch uses.
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
*
* @constructor Create a new [BytecodePatchBuilder] builder.
@@ -385,9 +396,27 @@ class BytecodePatchBuilder internal constructor(
description: String?,
use: Boolean,
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
private val fingerprints = mutableSetOf<Fingerprint>()
/**
* Add the fingerprint to the patch.
*
* @return A wrapper for the fingerprint with the ability to delegate the match to the fingerprint.
*/
operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) })
class InvokedFingerprint internal constructor(private val fingerprint: Fingerprint) {
// The reason getValue isn't extending the Fingerprint class is
// because delegating makes only sense if the fingerprint was previously added to the patch by invoking it.
// It may be likely to forget invoking it. By wrapping the fingerprint into this class,
// the compiler will throw an error if the fingerprint was not invoked if attempting to delegate the match.
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = fingerprint.match
?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".")
}
// Must be internal for the inlined function "extendWith".
@PublishedApi
internal var extensionInputStream: Supplier<InputStream>? = null
internal var extension: InputStream? = null
// Inlining is necessary to get the class loader that loaded the patch
// to load the extension from the resources.
@@ -398,11 +427,8 @@ class BytecodePatchBuilder internal constructor(
*/
@Suppress("NOTHING_TO_INLINE")
inline fun extendWith(extension: String) = apply {
val classLoader = object {}.javaClass.classLoader
extensionInputStream = Supplier {
classLoader.getResourceAsStream(extension) ?: throw PatchException("Extension \"$extension\" not found")
}
this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension \"$extension\" not found")
}
override fun build() = BytecodePatch(
@@ -412,7 +438,8 @@ class BytecodePatchBuilder internal constructor(
compatiblePackages,
dependencies,
options,
extensionInputStream,
fingerprints,
extension,
executionBlock,
finalizeBlock,
)

View File

@@ -60,7 +60,7 @@ internal object ClassMerger {
if (missingMethods.isEmpty()) return this
logger.fine { "Found ${missingMethods.size} missing methods" }
logger.fine("Found ${missingMethods.size} missing methods")
return asMutableClass().apply {
methods.addAll(missingMethods.map { it.toMutable() })
@@ -80,7 +80,7 @@ internal object ClassMerger {
if (missingFields.isEmpty()) return this
logger.fine { "Found ${missingFields.size} missing fields" }
logger.fine("Found ${missingFields.size} missing fields")
return asMutableClass().apply {
fields.addAll(missingFields.map { it.toMutable() })
@@ -100,7 +100,7 @@ internal object ClassMerger {
context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy
logger.fine { "Publicizing ${this.type}" }
logger.fine("Publicizing ${this.type}")
accessFlags = accessFlags.toPublic()
}
@@ -124,7 +124,7 @@ internal object ClassMerger {
if (brokenFields.isEmpty()) return this
logger.fine { "Found ${brokenFields.size} broken fields" }
logger.fine("Found ${brokenFields.size} broken fields")
/**
* Make a field public.
@@ -153,7 +153,7 @@ internal object ClassMerger {
if (brokenMethods.isEmpty()) return this
logger.fine { "Found ${brokenMethods.size} methods" }
logger.fine("Found ${brokenMethods.size} methods")
/**
* Make a method public.

View File

@@ -3,21 +3,21 @@ package app.revanced.patcher
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import jdk.internal.module.ModuleBootstrap.patcher
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import java.util.logging.Logger
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
internal object PatcherTest {
private lateinit var patcher: Patcher
@@ -151,15 +151,19 @@ internal object PatcherTest {
@Test
fun `throws if unmatched fingerprint match is delegated`() {
val patch = bytecodePatch {
execute {
// Fingerprint can never match.
val match by fingerprint { }
// Fingerprint can never match.
val match by fingerprint { }
// Manually add the fingerprint.
app.revanced.patcher.fingerprint { }()
execute {
// Throws, because the fingerprint can't be matched.
match.patternMatch
}
}
assertEquals(2, patch.fingerprints.size)
assertTrue(
patch().exception != null,
"Expected an exception because the fingerprint can't match.",
@@ -168,6 +172,44 @@ internal object PatcherTest {
@Test
fun `matches fingerprint`() {
mockClassWithMethod()
val patches = setOf(bytecodePatch { fingerprint { this returns "V" } })
assertNull(
patches.first().fingerprints.first().match,
"Expected fingerprint to be matched before execution.",
)
patches()
assertDoesNotThrow("Expected fingerprint to be matched.") {
assertEquals(
"V",
patches.first().fingerprints.first().match!!.method.returnType,
"Expected fingerprint to be matched.",
)
}
}
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
every { with(patcher.context.bytecodeContext) { any<BytecodePatch>().mergeExtension() } } just runs
return runBlocking { patcher().toList() }
}
private operator fun Patch<*>.invoke() = setOf(this)().first()
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(
mutableListOf(
ImmutableClassDef(
@@ -193,50 +235,6 @@ internal object PatcherTest {
),
),
)
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match } } answers { callOriginal() }
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<ClassDef>()) } } answers { callOriginal() }
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<Method>()) } } answers { callOriginal() }
every { patcher.context.bytecodeContext.classBy(any()) } answers { callOriginal() }
every { patcher.context.bytecodeContext.proxy(any()) } answers { callOriginal() }
val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") }
val fingerprint3 = fingerprint { returns("V") }
val patches = setOf(
bytecodePatch {
execute {
fingerprint.match(classes.first().methods.first())
fingerprint2.match(classes.first())
fingerprint3.match
}
},
)
patches()
assertAll(
"Expected fingerprints to match.",
{ assertNotNull(fingerprint._match) },
{ assertNotNull(fingerprint2._match) },
{ assertNotNull(fingerprint3._match) },
)
}
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
every { with(patcher.context.bytecodeContext) { mergeExtension(any<BytecodePatch>()) } } just runs
return runBlocking { patcher().toList() }
}
private operator fun Patch<*>.invoke() = setOf(this)().first()
private fun Any.setPrivateField(field: String, value: Any) {
this::class.java.getDeclaredField(field).apply {
this.isAccessible = true
set(this@setPrivateField, value)
}
}
}

View File

@@ -1,5 +1,6 @@
package app.revanced.patcher.patch
import app.revanced.patcher.fingerprint
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -23,6 +24,23 @@ internal object PatchTest {
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
}
@Test
fun `can create patch with fingerprints`() {
val externalFingerprint = fingerprint {}
val patch = bytecodePatch(name = "Test") {
val externalFingerprintMatch by externalFingerprint()
val internalFingerprintMatch by fingerprint {}
execute {
externalFingerprintMatch.method
internalFingerprintMatch.method
}
}
assertEquals(2, patch.fingerprints.size)
}
@Test
fun `can create patch with dependencies`() {
val patch = bytecodePatch(name = "Test") {