Skip to content

Commit

Permalink
Make accessing delegated match safer
Browse files Browse the repository at this point in the history
  • Loading branch information
oSumAtrIX committed Jun 20, 2024
1 parent dfbb70f commit 8821b6b
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 50 deletions.
21 changes: 11 additions & 10 deletions api/revanced-patcher.api
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ 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/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/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 {
Expand Down Expand Up @@ -193,21 +193,22 @@ public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$Meth
}

public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public final fun getClassLoader ()Ljava/lang/ClassLoader;
public final fun getExtension ()Ljava/lang/String;
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 getClassLoader ()Ljava/lang/ClassLoader;
public final fun getExtension ()Ljava/lang/String;
public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Fingerprint;
public final fun setClassLoader (Ljava/lang/ClassLoader;)V
public final fun setExtension (Ljava/lang/String;)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 fun <init> (Lapp/revanced/patcher/Fingerprint;)V
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 {
Expand Down
16 changes: 10 additions & 6 deletions docs/2_2_1_fingerprinting.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,22 @@ Once the fingerprint is matched, the match can be used in the patch:
val patch = bytecodePatch {
// Add a fingerprint and delegate its match to a variable.
val match by showAdsFingerprint()
val match2 by fingerprint {
// ...
}
execute {
val method = match.method
val method2 = match2.method
}
}
```
> [!WARNING]
> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If the match is delegated
> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If such a match is delegated
> to a variable, accessing it will raise an exception.
The match of a fingerprint contains mutable and immutable references to the method and the class it is defined in.
The match of a fingerprint contains mutable and immutable references to the method and the class it matches to.

```kt
class Match(
Expand Down Expand Up @@ -224,7 +228,7 @@ you can match the fingerprint on the list of classes:
execute { context ->
val match = showAdsFingerprint.apply {
match(context, context.classes)
}.match ?: throw PatchException("showAdsFingerprint not found")
}.match ?: throw PatchException("No match found")
}
```

Expand All @@ -238,7 +242,7 @@ you can match the fingerprint on the list of classes:

val match = showAdsFingerprint.apply {
match(context, adsLoaderClass)
}.match ?: throw PatchException("showAdsFingerprint not found")
}.match ?: throw PatchException("No match found")
}
```

Expand All @@ -260,7 +264,7 @@ or the indices of the instructions with certain string references.
match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}")
}
} ?: throw PatchException("pro strings fingerprint not found")
} ?: throw PatchException("No match found")
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/3_structure_and_conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Patches are organized in a specific way. The file structure looks as follows:
you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app
before this patch is executed.
Patches can then reference methods and classes from extensions.
A real-world example of integrations can be found in the [ReVanced Integrations](https://github.com/ReVanced/revanced-integrations) repository
A real-world example of extensions can be found in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository
- 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change.
In the example of an obfuscated method, it's better to fingerprint the method by its return type
and parameters rather than its name because the name is likely to change. An intelligent selection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.Closeable
import java.io.FileFilter
import java.io.InputStream
import java.util.*
import java.util.logging.Logger

Expand Down Expand Up @@ -57,20 +58,24 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal val lookupMaps by lazy { LookupMaps(classes) }

/**
* Merge the extension to [classes].
* Merge an extension to [classes].
*
* @param extensionInputStream The input stream of the extension to merge.
*/
internal fun mergeExtension(extension: ByteArray) {
internal fun merge(extensionInputStream: InputStream) {
val extension = extensionInputStream.readAllBytes()

RawDexIO.readRawDexFile(extension, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
logger.fine("Adding $classDef")
logger.fine("Adding class \"$classDef\"")

lookupMaps.classesByType[classDef.type] = classDef
classes += classDef

return@forEach
}

logger.fine("$classDef exists. 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.
Expand Down
44 changes: 19 additions & 25 deletions src/main/kotlin/app/revanced/patcher/patch/Patch.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.InputStream
import java.net.URLClassLoader
import java.util.jar.JarFile
import kotlin.reflect.KProperty
Expand Down Expand Up @@ -92,12 +93,11 @@ sealed class Patch<C : PatchContext<*>>(
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @param fingerprints The fingerprints that are resolved before the patch is executed.
* @param extension The name of the extension resource this patch uses.
* @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,
* in reverse order of execution.
* @param classLoader The [ClassLoader] to use for reading the extension from the resources.
*
* @constructor Create a new bytecode patch.
*/
Expand All @@ -109,10 +109,9 @@ class BytecodePatch internal constructor(
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
val fingerprints: Set<Fingerprint>,
val extension: String?,
val extension: InputStream?,
executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
finalizeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
val classLoader: ClassLoader?,
) : Patch<BytecodePatchContext>(
name,
description,
Expand All @@ -124,13 +123,7 @@ class BytecodePatch internal constructor(
finalizeBlock,
) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
if (extension != null) {
mergeExtension(
classLoader!!.getResourceAsStream(extension)?.readAllBytes()
?: throw PatchException("Extension resource \"$extension\" not found"),
)
}

extension?.let(::merge)
fingerprints.forEach { it.match(this) }

execute(this)
Expand Down Expand Up @@ -336,9 +329,8 @@ sealed class PatchBuilder<C : PatchContext<*>>(
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property fingerprints The fingerprints that are resolved before the patch is executed.
* @property extension The name of the extension resource this patch uses.
* @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.
* @property classLoader The [ClassLoader] to use for reading the extension from the resources.
*
* @constructor Create a new [BytecodePatchBuilder] builder.
*/
Expand All @@ -351,20 +343,23 @@ class BytecodePatchBuilder internal constructor(

/**
* 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() = apply {
fingerprints.add(this)
operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) })

class InvokedFingerprint(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}.")
}

operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>) = match
?: throw PatchException("Cannot delegate unresolved fingerprint result to ${property.name}.")

// Must be internal for the inlined function "extendWith".
@PublishedApi
internal var extension: String? = null

@PublishedApi
internal var classLoader: ClassLoader? = 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.
Expand All @@ -374,8 +369,8 @@ class BytecodePatchBuilder internal constructor(
* @param extension The name of the extension resource.
*/
inline fun extendWith(extension: String) = apply {
this.extension = extension
classLoader = object {}.javaClass.classLoader
this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension resource \"$extension\" not found")
}

override fun build() = BytecodePatch(
Expand All @@ -389,7 +384,6 @@ class BytecodePatchBuilder internal constructor(
extension,
executionBlock,
finalizeBlock,
classLoader,
)
}

Expand Down
8 changes: 4 additions & 4 deletions src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ internal object PatchTest {
val externalFingerprint = fingerprint {}

val patch = bytecodePatch(name = "Test") {
val result by externalFingerprint()
val internalFingerprint = fingerprint {}
val externalFingerprintMatch by externalFingerprint()
val internalFingerprintMatch by fingerprint {}

execute {
result.method
internalFingerprint.match
externalFingerprintMatch.method
internalFingerprintMatch.method
}
}

Expand Down

0 comments on commit 8821b6b

Please sign in to comment.