diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api
index 2f173eb8..a44fdf5c 100644
--- a/api/revanced-patcher.api
+++ b/api/revanced-patcher.api
@@ -1,6 +1,26 @@
+public final class app/revanced/patcher/AnyInstruction : app/revanced/patcher/InstructionFilter {
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+}
+
+public final class app/revanced/patcher/CheckCastFilter : app/revanced/patcher/OpcodeFilter {
+ public final fun getType ()Lkotlin/jvm/functions/Function0;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+ public final fun setType (Lkotlin/jvm/functions/Function0;)V
+}
+
+public final class app/revanced/patcher/FieldAccessFilter : app/revanced/patcher/OpcodesFilter {
+ public final fun getDefiningClass ()Lkotlin/jvm/functions/Function0;
+ public final fun getName ()Lkotlin/jvm/functions/Function0;
+ public final fun getType ()Lkotlin/jvm/functions/Function0;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+}
+
public final class app/revanced/patcher/Fingerprint {
+ public final fun clearMatch ()V
public final fun getClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun getInstructionMatches (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
+ public final fun getInstructionMatchesOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun getMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
@@ -11,19 +31,26 @@ public final class app/revanced/patcher/Fingerprint {
public final fun getPatternMatchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun getStringMatchesOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
+ public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
+ public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
+ public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
- public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
+ public final fun patchException ()Lapp/revanced/patcher/patch/PatchException;
+ public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/FingerprintBuilder {
- public fun ()V
+ public fun (Ljava/lang/String;)V
public final fun accessFlags (I)V
public final fun accessFlags ([Lcom/android/tools/smali/dexlib2/AccessFlags;)V
+ public final fun build ()Lapp/revanced/patcher/Fingerprint;
public final fun custom (Lkotlin/jvm/functions/Function2;)V
+ public final fun getName ()Ljava/lang/String;
+ public final fun instructions ([Lapp/revanced/patcher/InstructionFilter;)V
public final fun opcodes (Ljava/lang/String;)V
public final fun opcodes ([Lcom/android/tools/smali/dexlib2/Opcode;)V
public final fun parameters ([Ljava/lang/String;)V
@@ -31,21 +58,103 @@ public final class app/revanced/patcher/FingerprintBuilder {
public final fun strings ([Ljava/lang/String;)V
}
+public final class app/revanced/patcher/FingerprintDelegate {
+ public fun (Lkotlin/jvm/functions/Function1;)V
+ public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Fingerprint;
+}
+
public final class app/revanced/patcher/FingerprintKt {
- public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
- public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
+ public static final fun fingerprint (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/FingerprintDelegate;
+}
+
+public abstract class app/revanced/patcher/InstructionFilter {
+ public static final field Companion Lapp/revanced/patcher/InstructionFilter$Companion;
+ public static final field METHOD_MAX_INSTRUCTIONS I
+ public fun ()V
+ public fun (I)V
+ public synthetic fun (IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun getMaxAfter ()I
+ public abstract fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+}
+
+public final class app/revanced/patcher/InstructionFilter$Companion {
+}
+
+public final class app/revanced/patcher/InstructionFilterKt {
+ public static final fun anyInstruction ([Lapp/revanced/patcher/InstructionFilter;I)Lapp/revanced/patcher/AnyInstruction;
+ public static synthetic fun anyInstruction$default ([Lapp/revanced/patcher/InstructionFilter;IILjava/lang/Object;)Lapp/revanced/patcher/AnyInstruction;
+ public static final fun checkCast (Ljava/lang/String;I)Lapp/revanced/patcher/CheckCastFilter;
+ public static final fun checkCast (Lkotlin/jvm/functions/Function0;I)Lapp/revanced/patcher/CheckCastFilter;
+ public static synthetic fun checkCast$default (Ljava/lang/String;IILjava/lang/Object;)Lapp/revanced/patcher/CheckCastFilter;
+ public static synthetic fun checkCast$default (Lkotlin/jvm/functions/Function0;IILjava/lang/Object;)Lapp/revanced/patcher/CheckCastFilter;
+ public static final fun fieldAccess (Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;I)Lapp/revanced/patcher/FieldAccessFilter;
+ public static final fun fieldAccess (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;I)Lapp/revanced/patcher/FieldAccessFilter;
+ public static final fun fieldAccess (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;I)Lapp/revanced/patcher/FieldAccessFilter;
+ public static final fun fieldAccess (Ljava/lang/String;Ljava/util/List;I)Lapp/revanced/patcher/FieldAccessFilter;
+ public static final fun fieldAccess (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/util/List;I)Lapp/revanced/patcher/FieldAccessFilter;
+ public static synthetic fun fieldAccess$default (Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;IILjava/lang/Object;)Lapp/revanced/patcher/FieldAccessFilter;
+ public static synthetic fun fieldAccess$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;IILjava/lang/Object;)Lapp/revanced/patcher/FieldAccessFilter;
+ public static synthetic fun fieldAccess$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/FieldAccessFilter;
+ public static synthetic fun fieldAccess$default (Ljava/lang/String;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/FieldAccessFilter;
+ public static synthetic fun fieldAccess$default (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/FieldAccessFilter;
+ public static final fun literal (DLjava/util/List;I)Lapp/revanced/patcher/LiteralFilter;
+ public static final fun literal (FLjava/util/List;I)Lapp/revanced/patcher/LiteralFilter;
+ public static final fun literal (ILjava/util/List;I)Lapp/revanced/patcher/LiteralFilter;
+ public static final fun literal (JLjava/util/List;I)Lapp/revanced/patcher/LiteralFilter;
+ public static final fun literal (Lkotlin/jvm/functions/Function0;Ljava/util/List;I)Lapp/revanced/patcher/LiteralFilter;
+ public static synthetic fun literal$default (DLjava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/LiteralFilter;
+ public static synthetic fun literal$default (FLjava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/LiteralFilter;
+ public static synthetic fun literal$default (ILjava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/LiteralFilter;
+ public static synthetic fun literal$default (JLjava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/LiteralFilter;
+ public static synthetic fun literal$default (Lkotlin/jvm/functions/Function0;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/LiteralFilter;
+ public static final fun methodCall (Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;I)Lapp/revanced/patcher/MethodCallFilter;
+ public static final fun methodCall (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;I)Lapp/revanced/patcher/MethodCallFilter;
+ public static final fun methodCall (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/util/List;I)Lapp/revanced/patcher/MethodCallFilter;
+ public static final fun methodCall (Ljava/lang/String;Ljava/util/List;I)Lapp/revanced/patcher/MethodCallFilter;
+ public static final fun methodCall (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/util/List;I)Lapp/revanced/patcher/MethodCallFilter;
+ public static synthetic fun methodCall$default (Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;IILjava/lang/Object;)Lapp/revanced/patcher/MethodCallFilter;
+ public static synthetic fun methodCall$default (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lcom/android/tools/smali/dexlib2/Opcode;IILjava/lang/Object;)Lapp/revanced/patcher/MethodCallFilter;
+ public static synthetic fun methodCall$default (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/MethodCallFilter;
+ public static synthetic fun methodCall$default (Ljava/lang/String;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/MethodCallFilter;
+ public static synthetic fun methodCall$default (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Ljava/util/List;IILjava/lang/Object;)Lapp/revanced/patcher/MethodCallFilter;
+ public static final fun newInstance (Ljava/lang/String;I)Lapp/revanced/patcher/NewInstanceFilter;
+ public static synthetic fun newInstance$default (Ljava/lang/String;IILjava/lang/Object;)Lapp/revanced/patcher/NewInstanceFilter;
+ public static final fun newInstancetype (Lkotlin/jvm/functions/Function0;I)Lapp/revanced/patcher/NewInstanceFilter;
+ public static synthetic fun newInstancetype$default (Lkotlin/jvm/functions/Function0;IILjava/lang/Object;)Lapp/revanced/patcher/NewInstanceFilter;
+ public static final fun opcode (Lcom/android/tools/smali/dexlib2/Opcode;I)Lapp/revanced/patcher/OpcodeFilter;
+ public static synthetic fun opcode$default (Lcom/android/tools/smali/dexlib2/Opcode;IILjava/lang/Object;)Lapp/revanced/patcher/OpcodeFilter;
+ public static final fun string (Ljava/lang/String;ZI)Lapp/revanced/patcher/StringFilter;
+ public static final fun string (Lkotlin/jvm/functions/Function0;ZI)Lapp/revanced/patcher/StringFilter;
+ public static synthetic fun string$default (Ljava/lang/String;ZIILjava/lang/Object;)Lapp/revanced/patcher/StringFilter;
+ public static synthetic fun string$default (Lkotlin/jvm/functions/Function0;ZIILjava/lang/Object;)Lapp/revanced/patcher/StringFilter;
}
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
}
+public final class app/revanced/patcher/LiteralFilter : app/revanced/patcher/OpcodesFilter {
+ public final fun getLiteral ()Lkotlin/jvm/functions/Function0;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+ public final fun setLiteral (Lkotlin/jvm/functions/Function0;)V
+}
+
public final class app/revanced/patcher/Match {
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun getInstructionMatches ()Ljava/util/List;
+ public final fun getInstructionMatchesOrNull ()Ljava/util/List;
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 final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List;
+ public final fun getStringMatchesOrNull ()Ljava/util/List;
+}
+
+public final class app/revanced/patcher/Match$InstructionMatch {
+ public final fun getFilter ()Lapp/revanced/patcher/InstructionFilter;
+ public final fun getIndex ()I
+ public final fun getInstruction ()Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
+ public final fun getInstruction ()Ljava/lang/Object;
}
public final class app/revanced/patcher/Match$PatternMatch {
@@ -58,6 +167,41 @@ public final class app/revanced/patcher/Match$StringMatch {
public final fun getString ()Ljava/lang/String;
}
+public final class app/revanced/patcher/MethodCallFilter : app/revanced/patcher/OpcodesFilter {
+ public static final field Companion Lapp/revanced/patcher/MethodCallFilter$Companion;
+ public final fun getDefiningClass ()Lkotlin/jvm/functions/Function0;
+ public final fun getName ()Lkotlin/jvm/functions/Function0;
+ public final fun getParameters ()Lkotlin/jvm/functions/Function0;
+ public final fun getReturnType ()Lkotlin/jvm/functions/Function0;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+}
+
+public final class app/revanced/patcher/MethodCallFilter$Companion {
+}
+
+public final class app/revanced/patcher/NewInstanceFilter : app/revanced/patcher/OpcodesFilter {
+ public final fun getType ()Lkotlin/jvm/functions/Function0;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+ public final fun setType (Lkotlin/jvm/functions/Function0;)V
+}
+
+public class app/revanced/patcher/OpcodeFilter : app/revanced/patcher/InstructionFilter {
+ public fun (Lcom/android/tools/smali/dexlib2/Opcode;I)V
+ public final fun getOpcode ()Lcom/android/tools/smali/dexlib2/Opcode;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+}
+
+public class app/revanced/patcher/OpcodesFilter : app/revanced/patcher/InstructionFilter {
+ public static final field Companion Lapp/revanced/patcher/OpcodesFilter$Companion;
+ protected fun (Ljava/util/List;I)V
+ public synthetic fun (Ljava/util/List;IILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun getOpcodes ()Ljava/util/EnumSet;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+}
+
+public final class app/revanced/patcher/OpcodesFilter$Companion {
+}
+
public final class app/revanced/patcher/PackageMetadata {
public final fun getPackageName ()Ljava/lang/String;
public final fun getPackageVersion ()Ljava/lang/String;
@@ -101,6 +245,14 @@ public final class app/revanced/patcher/PatcherResult$PatchedResources {
public final fun getResourcesApk ()Ljava/io/File;
}
+public final class app/revanced/patcher/StringFilter : app/revanced/patcher/OpcodesFilter {
+ public final fun getPartialMatch ()Z
+ public final fun getString ()Lkotlin/jvm/functions/Function0;
+ public fun matches (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;)Z
+ public final fun setPartialMatch (Z)V
+ public final fun setString (Lkotlin/jvm/functions/Function0;)V
+}
+
public final class app/revanced/patcher/extensions/ExtensionsKt {
public static final fun newLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lcom/android/tools/smali/dexlib2/builder/Label;
}
@@ -159,13 +311,21 @@ public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revance
}
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 classBy (Ljava/lang/String;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun classBy (Lkotlin/jvm/functions/Function1;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun classByOrNull (Ljava/lang/String;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun classByOrNull (Lkotlin/jvm/functions/Function1;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
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 getClasses ()Lapp/revanced/patcher/util/PatchClasses;
+ public final fun mutableClassBy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassBy (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassByOrNull (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassByOrNull (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator;
- public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
+ public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
}
public final class app/revanced/patcher/patch/Option {
@@ -493,46 +653,17 @@ public final class app/revanced/patcher/util/MethodNavigator {
public static synthetic fun to$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
}
-public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList {
- public fun add (ILcom/android/tools/smali/dexlib2/iface/ClassDef;)V
- public synthetic fun add (ILjava/lang/Object;)V
- public fun add (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
- public synthetic fun add (Ljava/lang/Object;)Z
- public fun addAll (ILjava/util/Collection;)Z
- public fun addAll (Ljava/util/Collection;)Z
- public fun clear ()V
- public fun contains (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
- public final fun contains (Ljava/lang/Object;)Z
- public fun containsAll (Ljava/util/Collection;)Z
- public fun get (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
- public synthetic fun get (I)Ljava/lang/Object;
- public fun getSize ()I
- public fun indexOf (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)I
- public final fun indexOf (Ljava/lang/Object;)I
- public fun isEmpty ()Z
- public fun iterator ()Ljava/util/Iterator;
- public fun lastIndexOf (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)I
- public final fun lastIndexOf (Ljava/lang/Object;)I
- public fun listIterator ()Ljava/util/ListIterator;
- public fun listIterator (I)Ljava/util/ListIterator;
- public final fun remove (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
- public synthetic fun remove (I)Ljava/lang/Object;
- public fun remove (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
- public final fun remove (Ljava/lang/Object;)Z
- public fun removeAll (Ljava/util/Collection;)Z
- public fun removeAt (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
- public fun retainAll (Ljava/util/Collection;)Z
- public fun set (ILcom/android/tools/smali/dexlib2/iface/ClassDef;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
- public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object;
- public final fun size ()I
- public fun subList (II)Ljava/util/List;
- public fun toArray ()[Ljava/lang/Object;
- public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
-}
-
-public final class app/revanced/patcher/util/proxy/ClassProxy {
- public final fun getImmutableClass ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
- public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+public final class app/revanced/patcher/util/PatchClasses {
+ public final fun classBy (Ljava/lang/String;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun classBy (Lkotlin/jvm/functions/Function1;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun classByOrNull (Ljava/lang/String;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun classByOrNull (Lkotlin/jvm/functions/Function1;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
+ public final fun forEach (Lkotlin/jvm/functions/Function1;)V
+ public final fun mutableClassBy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassBy (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassByOrNull (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
+ public final fun mutableClassByOrNull (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
}
public final class app/revanced/patcher/util/proxy/mutableTypes/MutableAnnotation : com/android/tools/smali/dexlib2/base/BaseAnnotation {
diff --git a/docs/1_patcher_intro.md b/docs/1_patcher_intro.md
index e3f6872b..f9d90333 100644
--- a/docs/1_patcher_intro.md
+++ b/docs/1_patcher_intro.md
@@ -108,4 +108,4 @@ val resources = patcherResult.resources
The next page teaches the fundamentals of ReVanced Patches.
-Continue: [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
+Continue: [🧩 Introduction to ReVanced Patches](2_0_0_patches_intro.md)
diff --git a/docs/2_patches_intro.md b/docs/2_0_0_patches_intro.md
similarity index 98%
rename from docs/2_patches_intro.md
rename to docs/2_0_0_patches_intro.md
index fe0f0d38..3b6bb0fb 100644
--- a/docs/2_patches_intro.md
+++ b/docs/2_0_0_patches_intro.md
@@ -123,4 +123,4 @@ val resourcePatch = resourcePatch {
The next page will guide you through creating a development environment for creating patches.
-Continue: [👶 Setting up a development environment](2_1_setup.md)
+Continue: [👨💻 Setting up a development environment](2_1_0_setup.md)
diff --git a/docs/2_1_setup.md b/docs/2_1_0_setup.md
similarity index 97%
rename from docs/2_1_setup.md
rename to docs/2_1_0_setup.md
index 4cfb9b57..b78794db 100644
--- a/docs/2_1_setup.md
+++ b/docs/2_1_0_setup.md
@@ -58,7 +58,7 @@
Continuing the legacy of Vanced
-# 👶 Setting up a development environment
+# 👨💻 Setting up a development environment
To start developing patches with ReVanced Patcher, you must prepare a development environment.
@@ -109,4 +109,4 @@ Throughout the documentation, [ReVanced Patches](https://github.com/revanced/rev
The next page will go into details about a ReVanced patch.
-Continue: [🧩 Anatomy of a patch](2_2_patch_anatomy.md)
+Continue: [🧩 Anatomy of a patch](2_2_0_patch_anatomy.md)
diff --git a/docs/2_2_patch_anatomy.md b/docs/2_2_0_patch_anatomy.md
similarity index 99%
rename from docs/2_2_patch_anatomy.md
rename to docs/2_2_0_patch_anatomy.md
index 33f737c2..2f9265a2 100644
--- a/docs/2_2_patch_anatomy.md
+++ b/docs/2_2_0_patch_anatomy.md
@@ -85,7 +85,7 @@ val disableAdsPatch = bytecodePatch(
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch.
- val showAdsFingerprint = fingerprint {
+ val showAdsFingerprint by fingerprint {
// More about fingerprints on the next page of the documentation.
}
diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md
index 9260948c..944df8da 100644
--- a/docs/2_2_1_fingerprinting.md
+++ b/docs/2_2_1_fingerprinting.md
@@ -65,116 +65,244 @@ It is used to uniquely match a method by its characteristics.
Fingerprinting is used to match methods with a limited amount of known information.
Methods with obfuscated names that change with each update are primary candidates for fingerprinting.
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
-access flags, an opcode pattern, strings, and more.
+access flags, instructions, strings, and more.
## ⛳️ Example fingerprint
-An example fingerprint is shown below:
-
```kt
-
-package app.revanced.patches.ads.fingerprints
-
-fingerprint {
+val showAdsFingerprint by fingerprint {
+ // Method signature.
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Z")
- parameters("Z")
- opcodes(Opcode.RETURN)
- strings("pro")
- custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
+ // Declared parameters are matched using String.startsWith()
+ // Non obfuscated classes should be declared using the full class name.
+ // While obfuscated class names must be declared only using the object type
+ // Since obfuscated names change between releases.
+ // Last parameter is simply `L` since it's an obfuscated class object.
+ parameters("Ljava/lang/String;", "I", "L")
+
+ // Method implementation:
+ instructions(
+ // Filter 1.
+ fieldAccess(
+ definingClass = "this",
+ type = "Ljava/util/Map;"
+ ),
+
+ // Filter 2.
+ string("showBannerAds"),
+
+ // Filter 3.
+ methodCall(
+ definingClass = "Ljava/lang/String;",
+ name = "equals",
+ ),
+
+ // Filter 4.
+ // maxAfter = 0 means this must match immediately after the last filter.
+ opcode(Opcode.MOVE_RESULT, maxAfter = 0),
+
+ // Filter 5.
+ literal(1337),
+
+ // Filter 6.
+ opcode(Opcode.IF_EQ),
+ )
+ custom { method, classDef ->
+ classDef.type == "Lcom/some/app/ads/AdsLoader;"
+ }
}
```
-## 🔎 Reconstructing the original code from the example fingerprint from above
+## 🔎 Example target app in Java and Smali
-The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
+```java
+package com.some.app.ads;
-The fingerprint contains the following information:
+class AdsLoader {
+ private final static Map m = new HashMap<>();
-- Method signature:
+ // Method to fingerprint.
+ public final boolean obfuscatedMethod(String parameter1, int parameter2, ObfuscatedClass parameter3) {
+ // Filter 1 target instruction.
+ String string = m.get(parameter1);
- ```kt
- accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
- returns("Z")
- parameters("Z")
- ```
+ unrelatedMethod(string);
-- Method implementation:
+ // Filter 2, 3, 4 target instructions, and the instructions to modify.
+ if ("showBannerAds".equals(string)) {
+ showBannerAds();
+ }
- ```kt
- opcodes(Opcode.RETURN)
- strings("pro")
- ```
+ // Filter 5 and 6 target instructions.
+ return parameter2 != 1337;
+ }
-- Package and class name:
+ private void showBannerAds() {
+ // ...
+ }
- ```kt
- custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
- ```
+ private void unrelatedMethod(String parameter) {
+ // ...
+ }
+}
+```
-With this information, the original code can be reconstructed:
+```asm
+# Method to fingerprint.
+.method public final obfuscatedMethod(Ljava/lang/String;ILObfuscatedClass;)Z
+ .registers 4
-```java
-package com.some.app.ads;
+ # Filter 1 target instruction.
+ sget-object v0, Lcom/some/app/ads/AdsLoader;->a:Ljava/util/Map;
-
+ invoke-interface {v0, p1}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object;
-class AdsLoader {
- public final boolean (boolean )
+ move-result-object p1
- {
- // ...
+ check-cast p1, Ljava/lang/String;
- var userStatus = "pro";
+ invoke-direct {p0, p1}, Lcom/some/app/ads/AdsLoader;->unrelatedMethod(Ljava/lang/String;)V
- // ...
+ # Filter 2 target instruction.
+ const-string v0, "showBannerAds"
- return ;
- }
-}
+ # Filter 3 target instruction.
+ invoke-virtual {v0, p1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
+
+ # Filter 4 target instruction.
+ move-result p1
+
+ if-eqz p1, :cond_16
+
+ invoke-direct {p0}, Lcom/some/app/ads/AdsLoader;->showBannerAds()V
+
+ # Filter 5 target instruction.
+ :cond_16
+ const/16 p1, 0x539
+
+ # Filter 6 target instruction.
+ if-eq p2, p1, :cond_1c
+
+ const/4 p1, 0x1
+
+ goto :goto_1d
+
+ :cond_1c
+ const/4 p1, 0x0
+
+ :goto_1d
+ return p1
+.end method
```
-Using that fingerprint, this method can be matched uniquely from all other methods.
+ Notice the fingerprint filters do not declare every instruction in the target method, and between
+ each filter, zero or more other instructions can exist. Instruction filters must be declared in
+ the same order as the instructions appear in the target method.
+
+ If the distance between each instruction declaration can be approximated, then the `maxAfter`
+ parameter can be used to restrict the instruction match to a maximum distance from the last
+ instruction. A value of 0 for the first instruction filter means the filter must be the first
+ instruction of the target method.
+
+ If a single instruction varies slightly between different app targets but otherwise the fingerprint
+ is still the same, the `anyInstruction()` wrapper can be used to specify the different expected
+ instruction. Such as:
+ ```
+ anyInstruction(
+ string("string in early app target, but not found in later target"),
+ string("updated string in latest app target, but not found in earlier target")
+ )
+ ```
+
+ To simplify some filter declarations, `methodCall` and `fieldAccess` can be declared using
+ copy-pasted un-obfuscated smali statements. Such as:
+ ```
+ methodCall(smali = "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;")
+ fieldAccess(smali = "Landroid/os/Build;->MODEL:Ljava/lang/String;")
+ ```
+
+ If a method cannot be uniquely identified using the built in filters, but a fixed pattern of
+ opcodes can identify the method, then the opcode pattern can be defined using the fingerprint
+ `opcodes()` declaration. Opcode patterns do not allow variable spacing between each opcode, and
+ all opcodes all must appear exactly as declared. Opcode patterns should be avoided whenever
+ possible due to their fragility and possibility of matching completely unrelated code.
> [!TIP]
-> A fingerprint should contain information about a method likely to remain the same across updates.
-> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated
-> app.
-> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
-> same.
+> A fingerprint should contain information about a method that is unlikely to change between updates.
+> Obfuscated class and method names should never be used.
## 🔨 How to use fingerprints
-After declaring a fingerprint, it can be used in a patch to find the method it matches to:
+After declaring a fingerprint it can be used in a patch to find the method it matches to:
```kt
-val fingerprint = fingerprint {
- // ...
+execute {
+ showAdsFingerprint.let {
+ // Changes the target code to:
+ // if (false) {
+ // showBannerAds();
+ // }
+ val filter4 = it.instructionMatches[3]
+ val moveResultIndex = filter3.index
+ val moveResultRegister = filter3.getInstruction().registerA
+
+ it.method.addInstructions(moveResultIndex + 1, "const/4 v$moveResultRegister, 0x0")
+ }
}
+```
-val patch = bytecodePatch {
- execute {
- fingerprint.method
- }
+Be careful if making more than 1 modification to the same method. Adding/removing instructions to
+a method can cause fingerprint match indexes to no longer be correct. The simplest solution is
+to modify the target method from the last match index to the first. Another solution is after modifying
+the target method to then call `clearMatch()` followed by `match()`, and then the instruction match indexes
+are up to date and correct.
+
+Modifying the example above to also change the code `return parameter2 != 1337;` into: `return false;`:
+
+```kt
+execute {
+ appFingerprint.let {
+ // Modify method from last indexes to first to preserve the correct fingerprint indexes.
+
+ // Remove conditional branch and always return false.
+ val filter6 = it.instructionMatches[5]
+ it.method.removeInstruction(filter6.index)
+
+ // Changes the target code to:
+ // if (false) {
+ // showBannerAds();
+ // }
+ val filter4 = it.instructionMatches[3]
+ val moveResultIndex = filter3.index
+ val moveResultRegister = filter3.getInstruction().registerA
+
+ it.method.addInstructions(moveResultIndex + 1, "const/4 v$moveResultRegister, 0x0")
+ }
}
```
-The fingerprint won't be matched again, if it has already been matched once, for performance reasons.
-This makes it useful, to share fingerprints between multiple patches,
-and let the first executing patch match the fingerprint:
+For performance reasons, a fingerprint will always match only once (unless `clearMatch()` is called).
+This makes it useful to share fingerprints between multiple patches, and the fingerprint matches on
+the first usage of it.
```kt
-// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
val mainActivityPatch1 = bytecodePatch {
- execute {
- mainActivityOnCreateFingerprint.method
- }
+ execute {
+ mainActivityOnCreateFingerprint.method.apply {
+ // Modifications made here.
+ }
+ }
}
val mainActivityPatch2 = bytecodePatch {
- execute {
- mainActivityOnCreateFingerprint.method
+ execute {
+ mainActivityOnCreateFingerprint.method.apply {
+ // More modifications made here.
+ // Fingerprint does not match again, and the match result indexes are still the same as
+ // found in mainActivityPatch1.
}
+ }
}
```
@@ -183,32 +311,16 @@ val mainActivityPatch2 = bytecodePatch {
> accessing certain properties of the fingerprint will raise an exception.
> Instead, the `orNull` properties can be used to return `null` if no match is found.
-> [!TIP]
-> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
-> function to fuzzy match the pattern.
-> `null` can be used as a wildcard to match any opcode:
->
-> ```kt
-> fingerprint(fuzzyPatternScanThreshhold = 2) {
-> opcodes(
-> Opcode.ICONST_0,
-> null,
-> Opcode.ICONST_1,
-> Opcode.IRETURN,
-> )
->}
-> ```
-
The following properties can be accessed in a fingerprint:
-- `originalClassDef`: The original class definition the fingerprint matches to.
-- `originalClassDefOrNull`: The original class definition the fingerprint matches to.
-- `originalMethod`: The original method the fingerprint matches to.
-- `originalMethodOrNull`: The original method the fingerprint matches to.
-- `classDef`: The class the fingerprint matches to.
-- `classDefOrNull`: The class the fingerprint matches to.
-- `method`: The method the fingerprint matches to. If no match is found, an exception is raised.
-- `methodOrNull`: The method the fingerprint matches to.
+- `originalClassDef`: The immutable class definition the fingerprint matches to. If no match is found, an exception is raised.
+- `originalClassDefOrNull`: The immutable class definition the fingerprint matches to, or null.
+- `originalMethod`: The immutable method the fingerprint matches to. If no match is found, an exception is raised.
+- `originalMethodOrNull`: The immutable method the fingerprint matches to, or null.
+- `classDef`: The mutable class the fingerprint matches to. If no match is found, an exception is raised.
+- `classDefOrNull`: The mutable class the fingerprint matches to, or null.
+- `method`: The mutable method the fingerprint matches to. If no match is found, an exception is raised.
+- `methodOrNull`: The mutable method the fingerprint matches to, or null.
The difference between the `original` and non-`original` properties is that the `original` properties return the
original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
@@ -216,75 +328,51 @@ The mutable copies can be modified. They are lazy properties, so they are only c
and only then will effectively replace the `original` method or class definition when accessed.
> [!TIP]
-> If only read-only access to the class or method is needed,
-> the `originalClassDef` and `originalMethod` properties should be used,
-> to avoid making a mutable copy of the class or method.
+> If only read-only access to the class or method is needed, the `originalClassDef` and
+> `originalMethod` properties should be used, to avoid making a mutable copy of the class or method.
## 🏹 Manually matching fingerprints
-By default, a fingerprint is matched automatically against all classes
-when one of the fingerprint's properties is accessed.
+By default, a fingerprint is matched automatically against all classes when one of the
+fingerprint's properties is accessed.
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function:
- In a **list of classes**, if the fingerprint can match in a known subset of classes
- If you have a known list of classes you know the fingerprint can match in,
- you can match the fingerprint on the list of classes:
+ If you have a known list of classes you know the fingerprint can match in, you can match the
+ fingerprint on the list of classes:
```kt
execute {
- val match = showAdsFingerprint(classes)
+ val match = showAdsFingerprint.match(classes)
}
```
- In a **single class**, if the fingerprint can match in a single known class
- If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
+ If you know the fingerprint can match a method in a specific class, you can match the fingerprint
+ in the class:
```kt
execute {
- val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }
+ val adsLoaderClass = classBy("Lcom/some/app/ads/Loader;")
val match = showAdsFingerprint.match(adsLoaderClass)
}
```
- Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.
+ Another common use case is to find the class of the target code by finger printing an easy
+ to identify method in that class (especially a method with string constants), then use the class
+ found to match a second fingerprint that finds the target method.
```kt
execute {
- // Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
- val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
+ // Match showAdsFingerprint to the class of the ads loader found by adsLoaderClassFingerprint.
+ val match = showAdsFingerprint.match(adsLoaderClassFingerprint.originalClassDef)
}
```
-- Match a **single method**, to extract certain information about it
-
- The match of a fingerprint contains useful information about the method,
- such as the start and end index of an opcode pattern or the indices of the instructions with certain string
- references.
- A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
-
- ```kt
- execute {
- val currentPlanFingerprint = fingerprint {
- strings("free", "trial")
- }
-
- currentPlanFingerprint.match(adsFingerprint.method).let { match ->
- match.stringMatches.forEach { match ->
- println("The index of the string '${match.string}' is ${match.index}")
- }
- }
- }
- ```
-
-> [!WARNING]
-> If the fingerprint can not be matched to any method, calling `match` will raise an
-> exception.
-> Instead, the `orNull` overloads can be used to return `null` if no match is found.
-
> [!TIP]
> To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
diff --git a/docs/3_structure_and_conventions.md b/docs/3_structure_and_conventions.md
index 3c8e27e3..e5e4a5f8 100644
--- a/docs/3_structure_and_conventions.md
+++ b/docs/3_structure_and_conventions.md
@@ -82,6 +82,7 @@ Patches are organized in a specific way. The file structure looks as follows:
- 🔥 Write the patch description in the third person, present tense, and end it with a period.
If a patch removes ads, the description can be omitted because of redundancy,
but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
+- 🔥 Name fingerprints with a best guess of what the target method does.
- 🔥 Write patches with modularity and reusability in mind. Patches can depend on each other,
so it is important to write patches in a way that can be used in different contexts.
- 🔥🔥 Keep patches as minimal as possible. This reduces the risk of failing patches.
@@ -93,13 +94,13 @@ Patches are organized in a specific way. The file structure looks as follows:
- 🔥🔥🔥 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
- of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates.
+ of an instructions filters in a method can result in a strong fingerprint dynamic to app updates.
- 🔥🔥🔥 Document your patches. Patches are abstract, so it is important to document parts of the code
- that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks
- of instructions that are modified or added to a method
+ that are not self-explanatory. For example, explain why and how a certain method is patched or
+ large blocks of instructions that are modified or added to a method.
## ⏭️ What's next
The next page discusses useful APIs for patch development.
-Continue: [💪 Advanced APIs](4_apis.md)
+Continue: [💪 Advanced APIs](4_0_0_apis)
diff --git a/docs/4_apis.md b/docs/4_0_0_apis.md
similarity index 57%
rename from docs/4_apis.md
rename to docs/4_0_0_apis.md
index d98a5cd3..546b79a0 100644
--- a/docs/4_apis.md
+++ b/docs/4_0_0_apis.md
@@ -4,59 +4,67 @@ 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)`
+1. 🔍 Find immutable classes with `classBy(String)`
+2. 👹 Create mutable replacements of classes with `mutableClassBy(ClassDef)`
3. 🏃 Navigate method calls recursively by index with `navigate(Method)`
4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)`
5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)`
### 🧰 APIs
-#### 👹 `proxy(ClassDef)`
+#### 🔍 `classBy(String)`
-By default, the classes are immutable, meaning they cannot be modified.
-To make a class mutable, use the `proxy(ClassDef)` function.
-This function creates a lazy mutable copy of the class definition.
-Accessing the property will replace the original class definition with the mutable copy,
-thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
+The `classBy(String)` function is an alternative to finding immutable classes
+from a constant string or from a String field of a fingerprint match.
```kt
execute {
- val mutableClass = proxy(classDef)
- mutableClass.methods.add(Method())
+ // Find the superclass of a fingerprint return type
+ val superClassOfReturnType = classBy(match().originalMethod.returnType).superclass
}
```
-#### 🔍 `classBy(Predicate)`
+#### 👹 `mutableClassBy(ClassDef)`
-The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate.
-It automatically proxies the class definition, making it mutable.
+By default, the classes are immutable and they cannot be modified.
+To make a class mutable use the `mutableClassBy(ClassDef)` function.
+Accessing the property will replace the original class definition with the mutable copy,
+thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
```kt
execute {
- // Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef
- val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef
+ // Find a class by the return type of a fingerprint
+ val superClassOfReturnType = classBy(match().originalMethod.returnType).superclass
+
+ val mutableClass = mutableClassBy(superClassOfReturnType)
+ mutableClass.methods.add(Method())
}
```
#### 🏃 `navigate(Method).at(index)`
-The `navigate(Method)` function allows you to navigate method calls recursively by index.
+The `navigate(Method)` function allows navigating method calls by index,
+and provides an easier way to parse the method call classes in code.
```kt
execute {
- // Sequentially navigate to the instructions at index 1 within 'someMethod'.
- val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
+ // Navigate to the method at index 5 within 'someMethod'.
+ // original() returns the original immutable method.
+ val original = navigate(someMethod).to(5).original()
- // Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
+ // Further navigate to the second occurrence of the opcode 'INVOKE_VIRTUAL'.
// stop() returns the mutable copy of the method.
- val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
-
- // Alternatively, to stop(), you can delegate the method to a variable.
- val method by navigate(someMethod).to(1)
+ val mutable = navigate(someMethod).to(2) {
+ instruction -> instruction.opcode == Opcode.INVOKE_VIRTUAL
+ }.stop()
- // You can chain multiple calls to at() to navigate deeper into the method.
- val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
+ // You can chain multiple to() calls together navigate multiple calls across different methods and classes.
+ //
+ // Navigate to:
+ // A. the method of the 5th instruction
+ // B. the method of the 10th instruction in method A
+ // C. the method of 2nd instruction of method B
+ val mutableDeep = navigate(someMethod).to(5, 10, 2).stop() // Mutable method Method C
}
```
diff --git a/docs/README.md b/docs/README.md
index 028389ff..1e0b9bb9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -65,9 +65,9 @@ This documentation contains the fundamentals of ReVanced Patcher and how to use
## 📖 Table of content
1. [💉 Introduction to ReVanced Patcher](1_patcher_intro.md)
-2. [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
- 1. [👶 Setting up a development environment](2_1_setup.md)
- 2. [🧩 Anatomy of a ReVanced patch](2_2_patch_anatomy.md)
+2. [🧩 Introduction to ReVanced Patches](2_0_0_patches_intro)
+ 1. [👨💻 Setting up a development environment](2_1_0_setup)
+ 2. [🧩 Anatomy of a ReVanced patch](2_2_0_patch_anatomy)
1. [🔎 Fingerprinting](2_2_1_fingerprinting.md)
3. [📜 Project structure and conventions](3_structure_and_conventions.md)
- 4. [💪 Advanced APIs](4_apis.md)
+ 4. [💪 Advanced APIs](4_0_0_apis)
diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt
index a44c00ba..5cd01b46 100644
--- a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt
+++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt
@@ -2,17 +2,18 @@
package app.revanced.patcher
+import app.revanced.patcher.Match.PatternMatch
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
-import app.revanced.patcher.patch.BytecodePatchContext
-import app.revanced.patcher.patch.PatchException
-import app.revanced.patcher.util.proxy.ClassProxy
+import app.revanced.patcher.patch.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
+import com.android.tools.smali.dexlib2.iface.instruction.Instruction
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
+import kotlin.reflect.KProperty
/**
* A fingerprint for a method. A fingerprint is a partial description of a method.
@@ -27,66 +28,74 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
* }
* ```
*
+ * @param name Human readable fingerprint name used for [toString] and error stack traces.
* @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 filters A list of filters to match, declared in the same order the instructions appear in the method.
+ * @param strings A list of the strings that appear anywhere in the method. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
- * @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
*/
class Fingerprint internal constructor(
+ internal val name: String,
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List?,
- internal val opcodes: List?,
+ internal val filters: List?,
+ @Deprecated("Instead use instruction filters")
internal val strings: List?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
- private val fuzzyPatternScanThreshold: Int,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/**
- * The match for this [Fingerprint]. Null if unmatched.
+ * Clears the current match, forcing this fingerprint to resolve again.
+ * This method should only be used if this fingerprint is re-used after it's modified,
+ * and the prior match indexes are no longer correct.
*/
- context(BytecodePatchContext)
- private val matchOrNull: Match?
- get() = matchOrNull()
+ fun clearMatch() {
+ _matchOrNull = null
+ }
/**
- * 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.
+ * The match for this [Fingerprint], or `null` if no matches exist.
*/
context(BytecodePatchContext)
- internal fun matchOrNull(): Match? {
+ fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
- var match = strings?.mapNotNull {
+ // Use string declarations to first check only the methods
+ // that contain one or more of fingerprint string.
+ var stringLiterals =
+ if (strings != null) {
+ // Old deprecated string declaration.
+ strings
+ } else {
+ filters?.filterIsInstance()
+ ?.map { it.string() }
+ }
+
+ stringLiterals?.mapNotNull {
lookupMaps.methodsByStrings[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
- methodClasses.forEach { (classDef, method) ->
+ methodClasses.forEach { (method, classDef) ->
val match = matchOrNull(classDef, method)
- if (match != null) return@let match
+ if (match != null) {
+ _matchOrNull = match
+ return match
+ }
}
-
- null
}
- if (match != null) return match
- classes.forEach { classDef ->
- match = matchOrNull(classDef)
- if (match != null) return match
+ // Check all classes.
+ classes.pool.values.forEach { classDef ->
+ val match = matchOrNull(classDef)
+ if (match != null) {
+ _matchOrNull = match
+ return match
+ }
}
return null
@@ -96,7 +105,8 @@ class Fingerprint internal constructor(
* 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.
+ * @return The [Match] if a match was found or if the
+ * fingerprint is already matched to a method, null otherwise.
*/
context(BytecodePatchContext)
fun matchOrNull(
@@ -105,8 +115,11 @@ class Fingerprint internal constructor(
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
- val match = matchOrNull(method, classDef)
- if (match != null) return match
+ val match = matchOrNull(classDef, method)
+ if (match != null) {
+ _matchOrNull = match
+ return match
+ }
}
return null
@@ -117,24 +130,30 @@ class Fingerprint internal constructor(
* 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.
+ * @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
+ * otherwise `null`.
*/
context(BytecodePatchContext)
fun matchOrNull(
method: Method,
- ) = matchOrNull(method, classBy { method.definingClass == it.type }!!.immutableClass)
+ ): Match? {
+ if (_matchOrNull != null) return _matchOrNull
+
+ return matchOrNull(classBy(method.definingClass), method)
+ }
/**
* 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.
+ * @return The [Match] if a match was found or if the fingerprint is previously matched to a method,
+ * otherwise `null`.
*/
context(BytecodePatchContext)
fun matchOrNull(
- method: Method,
classDef: ClassDef,
+ method: Method,
): Match? {
if (_matchOrNull != null) return _matchOrNull
@@ -146,20 +165,8 @@ class Fingerprint internal constructor(
return null
}
- fun parametersEqual(
- parameters1: Iterable,
- parameters2: Iterable,
- ): Boolean {
- if (parameters1.count() != parameters2.count()) return false
- val iterator1 = parameters1.iterator()
- parameters2.forEach {
- if (!it.startsWith(iterator1.next())) return false
- }
- return true
- }
-
// TODO: parseParameters()
- if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
+ if (parameters != null && !parametersStartsWith(method.parameterTypes, parameters)) {
return null
}
@@ -167,187 +174,223 @@ class Fingerprint internal constructor(
return null
}
- val stringMatches: List? =
- if (strings != null) {
- buildList {
- val instructions = method.instructionsOrNull ?: return null
-
- val stringsList = strings.toMutableList()
+ // Legacy string declarations.
+ val stringMatches: List? = if (strings == null) {
+ null
+ } else {
+ buildList {
+ val instructions = method.instructionsOrNull ?: return null
- instructions.forEachIndexed { instructionIndex, instruction ->
- if (
- instruction.opcode != Opcode.CONST_STRING &&
- instruction.opcode != Opcode.CONST_STRING_JUMBO
- ) {
- return@forEachIndexed
- }
+ var stringsList : MutableList? = null
- val string = ((instruction as ReferenceInstruction).reference as StringReference).string
- val index = stringsList.indexOfFirst(string::contains)
- if (index == -1) return@forEachIndexed
+ instructions.forEachIndexed { instructionIndex, instruction ->
+ if (
+ instruction.opcode != Opcode.CONST_STRING &&
+ instruction.opcode != Opcode.CONST_STRING_JUMBO
+ ) {
+ return@forEachIndexed
+ }
- add(Match.StringMatch(string, instructionIndex))
- stringsList.removeAt(index)
+ val string = ((instruction as ReferenceInstruction).reference as StringReference).string
+ if (stringsList == null) {
+ stringsList = strings.toMutableList()
}
+ val index = stringsList.indexOfFirst(string::contains)
+ if (index < 0) return@forEachIndexed
- if (stringsList.isNotEmpty()) return null
+ add(Match.StringMatch(string, instructionIndex))
+ stringsList.removeAt(index)
}
- } else {
- null
- }
- val patternMatch = if (opcodes != null) {
- val instructions = method.instructionsOrNull ?: return null
-
- fun patternScan(): Match.PatternMatch? {
- val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
-
- val instructionLength = instructions.count()
- val patternLength = opcodes.size
-
- for (index in 0 until instructionLength) {
- var patternIndex = 0
- var threshold = fingerprintFuzzyPatternScanThreshold
-
- while (index + patternIndex < instructionLength) {
- val originalOpcode = instructions.elementAt(index + patternIndex).opcode
- val patternOpcode = opcodes.elementAt(patternIndex)
+ if (stringsList == null || stringsList.isNotEmpty()) return null
+ }
+ }
- if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
- // Reaching maximum threshold (0) means,
- // the pattern does not match to the current instructions.
- if (threshold-- == 0) break
+ val instructionMatches = if (filters == null) {
+ null
+ } else {
+ val instructions = method.instructionsOrNull?.toList() ?: return null
+
+ fun matchFilters() : List? {
+ val lastMethodIndex = instructions.lastIndex
+ var instructionMatches : MutableList? = null
+
+ var firstInstructionIndex = 0
+ firstFilterLoop@ while (true) {
+ // Matched index of the first filter.
+ var firstFilterIndex = -1
+ var subIndex = firstInstructionIndex
+
+ for (filterIndex in filters.indices) {
+ val filter = filters[filterIndex]
+ val maxIndex = (subIndex + filter.maxAfter).coerceAtMost(lastMethodIndex)
+ var instructionsMatched = false
+
+ while (subIndex <= maxIndex) {
+ val instruction = instructions[subIndex]
+ if (filter.matches(method, instruction)) {
+ if (filterIndex == 0) {
+ firstFilterIndex = subIndex
+ }
+ if (instructionMatches == null) {
+ instructionMatches = ArrayList(filters.size)
+ }
+ instructionMatches += Match.InstructionMatch(filter, subIndex, instruction)
+ instructionsMatched = true
+ subIndex++
+ break
+ }
+ subIndex++
}
- if (patternIndex < patternLength - 1) {
- // If the entire pattern has not been scanned yet, continue the scan.
- patternIndex++
- continue
- }
+ if (!instructionsMatched) {
+ if (filterIndex == 0) {
+ return null // First filter has no more matches to start from.
+ }
- // The entire pattern has been scanned.
- return Match.PatternMatch(
- index,
- index + patternIndex,
- )
+ // Try again with the first filter, starting from
+ // the next possible first filter index.
+ firstInstructionIndex = firstFilterIndex + 1
+ instructionMatches?.clear()
+ continue@firstFilterLoop
+ }
}
- }
- return null
+ // All instruction filters matches.
+ return instructionMatches
+ }
}
- patternScan() ?: return null
- } else {
- null
+ matchFilters() ?: return null
}
_matchOrNull = Match(
+ classDef,
method,
- patternMatch,
+ instructionMatches,
stringMatches,
- classDef,
)
return _matchOrNull
}
- private val exception get() = PatchException("Failed to match the fingerprint: $this")
+ fun patchException() = PatchException("Failed to match the fingerprint: $this")
+
+ override fun toString() = name
+
/**
* The match for this [Fingerprint].
*
- * @throws PatchException If the [Fingerprint] has not been matched.
+ * @return The [Match] of this fingerprint.
+ * @throws PatchException If the [Fingerprint] failed to match.
*/
context(BytecodePatchContext)
- private val match
- get() = matchOrNull ?: throw exception
+ fun match() = matchOrNull() ?: throw patchException()
/**
* 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.
+ * @return The [Match] of this fingerprint.
+ * @throws PatchException If the fingerprint failed to match.
*/
context(BytecodePatchContext)
fun match(
classDef: ClassDef,
- ) = matchOrNull(classDef) ?: throw exception
+ ) = matchOrNull(classDef) ?: throw patchException()
/**
* 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.
+ * @return The [Match] of this fingerprint.
+ * @throws PatchException If the fingerprint failed to match.
*/
context(BytecodePatchContext)
fun match(
method: Method,
- ) = matchOrNull(method) ?: throw exception
+ ) = matchOrNull(method) ?: throw patchException()
/**
* 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.
+ * @return The [Match] of this fingerprint.
+ * @throws PatchException If the fingerprint failed to match.
*/
context(BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
- ) = matchOrNull(method, classDef) ?: throw exception
+ ) = matchOrNull(classDef, method) ?: throw patchException()
/**
- * The class the matching method is a member of.
+ * The class the matching method is a member of, or null if this fingerprint did not match.
*/
context(BytecodePatchContext)
val originalClassDefOrNull
- get() = matchOrNull?.originalClassDef
+ get() = matchOrNull()?.originalClassDef
/**
- * The matching method.
+ * The matching method, or null of this fingerprint did not match.
*/
context(BytecodePatchContext)
val originalMethodOrNull
- get() = matchOrNull?.originalMethod
+ get() = matchOrNull()?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
- * Accessing this property allocates a [ClassProxy].
+ * Accessing this property allocates a new mutable instance.
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val classDefOrNull
- get() = matchOrNull?.classDef
+ get() = matchOrNull()?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
- * Accessing this property allocates a [ClassProxy].
+ * Accessing this property allocates a new mutable instance.
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val methodOrNull
- get() = matchOrNull?.method
+ get() = matchOrNull()?.method
/**
- * The match for the opcode pattern.
+ * The match for the opcode pattern, or null if this fingerprint did not match.
+ */
+ context(BytecodePatchContext)
+ @Deprecated("instead use instructionMatchesOrNull")
+ val patternMatchOrNull : PatternMatch?
+ get() {
+ val match = this.matchOrNull()
+ if (match == null || match.instructionMatchesOrNull == null) {
+ return null
+ }
+ return match.patternMatch
+ }
+
+ /**
+ * The match for the instruction filters, or null if this fingerprint did not match.
*/
context(BytecodePatchContext)
- val patternMatchOrNull
- get() = matchOrNull?.patternMatch
+ val instructionMatchesOrNull
+ get() = matchOrNull()?.instructionMatchesOrNull
/**
- * The matches for the strings.
+ * The matches for the strings, or null if this fingerprint did not match.
+ *
+ * This does not give matches for strings declared using [string] instruction filters.
*/
context(BytecodePatchContext)
+ @Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
val stringMatchesOrNull
- get() = matchOrNull?.stringMatches
+ get() = matchOrNull()?.stringMatchesOrNull
/**
* The class the matching method is a member of.
@@ -356,7 +399,7 @@ class Fingerprint internal constructor(
*/
context(BytecodePatchContext)
val originalClassDef
- get() = match.originalClassDef
+ get() = match().originalClassDef
/**
* The matching method.
@@ -365,31 +408,31 @@ class Fingerprint internal constructor(
*/
context(BytecodePatchContext)
val originalMethod
- get() = match.originalMethod
+ get() = match().originalMethod
/**
* The mutable version of [originalClassDef].
*
- * Accessing this property allocates a [ClassProxy].
+ * Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val classDef
- get() = match.classDef
+ get() = match().classDef
/**
* The mutable version of [originalMethod].
*
- * Accessing this property allocates a [ClassProxy].
+ * Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val method
- get() = match.method
+ get() = match().method
/**
* The match for the opcode pattern.
@@ -397,17 +440,29 @@ class Fingerprint internal constructor(
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
+ @Deprecated("Instead use instructionMatch")
val patternMatch
- get() = match.patternMatch
+ get() = match().patternMatch
+
+ /**
+ * Instruction filter matches.
+ *
+ * @throws PatchException If the fingerprint has not been matched.
+ */
+ context(BytecodePatchContext)
+ val instructionMatches
+ get() = match().instructionMatches
/**
- * The matches for the strings.
+ * The matches for the strings declared using `strings()`.
+ * This does not give matches for strings declared using [string] instruction filters.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
+ @Deprecated("Instead use string instructions and `instructionMatches()`")
val stringMatches
- get() = match.stringMatches
+ get() = match().stringMatches
}
/**
@@ -415,37 +470,55 @@ class Fingerprint internal constructor(
*
* @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.
+ * @param _instructionMatches The match for the instruction filters.
+ * @param _stringMatches The matches for the strings declared using `strings()`.
*/
context(BytecodePatchContext)
class Match internal constructor(
- val originalMethod: Method,
- val patternMatch: PatternMatch?,
- val stringMatches: List?,
val originalClassDef: ClassDef,
+ val originalMethod: Method,
+ private val _instructionMatches: List?,
+ private val _stringMatches: List?,
) {
/**
* The mutable version of [originalClassDef].
*
- * Accessing this property allocates a [ClassProxy].
+ * Accessing this property allocates a new mutable instance.
* Use [originalClassDef] if mutable access is not required.
*/
- val classDef by lazy { proxy(originalClassDef).mutableClass }
+ val classDef by lazy { mutableClassBy(originalClassDef) }
/**
* The mutable version of [originalMethod].
*
- * Accessing this property allocates a [ClassProxy].
+ * Accessing this property allocates a new mutable instance.
* Use [originalMethod] if mutable access is not required.
*/
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
+ @Deprecated("Instead use instructionMatches", ReplaceWith("instructionMatches"))
+ val patternMatch by lazy {
+ if (_instructionMatches == null) throw PatchException("Did not match $this")
+ @SuppressWarnings("deprecation")
+ PatternMatch(_instructionMatches.first().index, _instructionMatches.last().index)
+ }
+
+ val instructionMatches
+ get() = _instructionMatches ?: throw PatchException("Fingerprint declared no instruction filters")
+ val instructionMatchesOrNull = _instructionMatches
+
+ @Deprecated("Instead use string instructions and `instructionMatches()`")
+ val stringMatches
+ get() = _stringMatches ?: throw PatchException("Fingerprint declared no strings")
+ @Deprecated("Instead use string instructions and `instructionMatchesOrNull()`")
+ val stringMatchesOrNull = _stringMatches
+
/**
* 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.
*/
+ @Deprecated("Instead use InstructionMatch")
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
@@ -457,29 +530,43 @@ class Match internal constructor(
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
+ @Deprecated("Instead use string instructions and `InstructionMatch`")
class StringMatch internal constructor(val string: String, val index: Int)
+
+ /**
+ * A match for a [InstructionFilter].
+ * @param filter The filter that matched
+ * @param index The instruction index it matched with.
+ * @param instruction The instruction that matched.
+ */
+ class InstructionMatch internal constructor(
+ val filter : InstructionFilter,
+ val index: Int,
+ val instruction: Instruction
+ ) {
+ @Suppress("UNCHECKED_CAST")
+ fun getInstruction(): T = instruction as T
+ }
}
/**
* A builder for [Fingerprint].
*
+ * @property name Name of the fingerprint, and usually identical to the variable name.
* @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 instructionFilters Filters to match the method instructions.
* @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].
*/
-class FingerprintBuilder internal constructor(
- private val fuzzyPatternScanThreshold: Int = 0,
-) {
+class FingerprintBuilder(val name: String) {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List? = null
- private var opcodes: List? = null
+ private var instructionFilters: List? = null
private var strings: List? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
@@ -504,6 +591,9 @@ class FingerprintBuilder internal constructor(
/**
* Set the return type.
*
+ * If [accessFlags] includes [AccessFlags.CONSTRUCTOR], then there is no need to
+ * set a return type set since constructors are always void return type.
+ *
* @param returnType The return type compared using [String.startsWith].
*/
fun returns(returnType: String) {
@@ -513,24 +603,62 @@ class FingerprintBuilder internal constructor(
/**
* Set the parameters.
*
- * @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
+ * @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()
}
+ private fun verifyNoFiltersSet() {
+ if (this.instructionFilters != null) {
+ throw PatchException("Instruction filters already set")
+ }
+ }
+
/**
- * Set the opcodes.
+ * A pattern of opcodes, where each opcode must appear immediately after the previous.
+ *
+ * To use opcodes with other [InstructionFilter] objects,
+ * instead use [instructions] with individual opcodes declared using [opcode].
+ *
+ * This method is identical to declaring individual opcode filters
+ * with [InstructionFilter.maxAfter] set to zero.
+ *
+ * Unless absolutely necessary, it is recommended to instead use [instructions]
+ * with more fine grained filters.
+ *
+ * ```
+ * opcodes(
+ * Opcode.INVOKE_VIRTUAL, // First opcode matches anywhere in the method.
+ * Opcode.MOVE_RESULT_OBJECT, // Must match exactly after INVOKE_VIRTUAL.
+ * Opcode.IPUT_OBJECT // Must match exactly after MOVE_RESULT_OBJECT.
+ * )
+ * ```
+ * is identical to:
+ * ```
+ * instructions(
+ * opcode(Opcode.INVOKE_VIRTUAL), // First opcode matches anywhere in the method.
+ * opcode(Opcode.MOVE_RESULT_OBJECT, maxAfter = 0), // Must match exactly after INVOKE_VIRTUAL.
+ * opcode(Opcode.IPUT_OBJECT, maxAfter = 0) // Must match exactly after MOVE_RESULT_OBJECT.
+ * )
+ * ```
*
* @param opcodes An opcode pattern of instructions.
- * Wildcard or unknown opcodes can be specified by `null`.
+ * Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
- this.opcodes = opcodes.toList()
+ verifyNoFiltersSet()
+ if (opcodes.isEmpty()) throw IllegalArgumentException("One or more opcodes is required")
+
+ this.instructionFilters = OpcodesFilter.listOfOpcodes(opcodes.toList())
}
/**
- * Set the opcodes.
+ * A pattern of opcodes from SMALI formatted text,
+ * where each opcode must appear immediately after the previous opcode.
+ *
+ * Unless absolutely necessary, it is recommended to instead use [instructions].
*
* @param instructions A list of instructions or opcode names in SMALI format.
* - Wildcard or unknown opcodes can be specified by `null`.
@@ -541,15 +669,30 @@ class FingerprintBuilder internal constructor(
* @throws Exception If an unknown opcode is used.
*/
fun opcodes(instructions: String) {
- this.opcodes = instructions.trimIndent().split("\n").filter {
- it.isNotBlank()
- }.map {
- // Remove any operands.
- val name = it.split(" ", limit = 1).first().trim()
- if (name == "null") return@map null
-
- opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
- }
+ verifyNoFiltersSet()
+ if (instructions.isBlank()) throw IllegalArgumentException("No instructions declared (empty string)")
+
+ this.instructionFilters = OpcodesFilter.listOfOpcodes(
+ instructions.trimIndent().split("\n").filter {
+ it.isNotBlank()
+ }.map {
+ // Remove any operands.
+ val name = it.split(" ", limit = 1).first().trim()
+ if (name == "null") return@map null
+
+ opcodesByName[name] ?: throw IllegalArgumentException("Unknown opcode: $name")
+ }
+ )
+ }
+
+ /**
+ * A list of instruction filters to match.
+ */
+ fun instructions(vararg instructionFilters: InstructionFilter) {
+ verifyNoFiltersSet()
+ if (instructionFilters.isEmpty()) throw IllegalArgumentException("One or more instructions is required")
+
+ this.instructionFilters = instructionFilters.toList()
}
/**
@@ -557,6 +700,7 @@ class FingerprintBuilder internal constructor(
*
* @param strings A list of strings compared each using [String.contains].
*/
+ @Deprecated("Instead use `instruction()` filters and `string()` instruction declarations")
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
@@ -570,30 +714,79 @@ class FingerprintBuilder internal constructor(
this.customBlock = customBlock
}
- internal fun build() = Fingerprint(
- accessFlags,
- returnType,
- parameters,
- opcodes,
- strings,
- customBlock,
- fuzzyPatternScanThreshold,
- )
+ fun build() : Fingerprint {
+ // If access flags include constructor then
+ // skip the return type check since it's always void.
+ if (returnType?.equals("V") == true && accessFlags != null
+ && AccessFlags.CONSTRUCTOR.isSet(accessFlags!!)
+ ) {
+ returnType = null
+ }
+
+ return Fingerprint(
+ name,
+ accessFlags,
+ returnType,
+ parameters,
+ instructionFilters,
+ strings,
+ customBlock,
+ )
+ }
+
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
+class FingerprintDelegate(
+ private val block: FingerprintBuilder.() -> Unit
+) {
+ // Must cache the fingerprint, otherwise on every usage
+ // a new fingerprint is built and resolved.
+ private var fingerprint: Fingerprint? = null
+
+ // Called when you read the property, e.g. `val x by fingerprint { ... }`
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): Fingerprint {
+ if (fingerprint == null) {
+ val builder = FingerprintBuilder(property.name)
+ builder.block()
+ fingerprint = builder.build()
+ }
+
+ return fingerprint!!
+ }
+}
+
/**
* 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].
*/
-fun fingerprint(
- fuzzyPatternScanThreshold: Int = 0,
- block: FingerprintBuilder.() -> Unit,
-) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()
+fun fingerprint(block: FingerprintBuilder.() -> Unit): FingerprintDelegate {
+ return FingerprintDelegate(block)
+}
+
+
+/**
+ * Matches two lists of parameters, where the first parameter list
+ * starts with the values of the second list.
+ */
+internal fun parametersStartsWith(
+ targetMethodParameters: Iterable,
+ fingerprintParameters: Iterable,
+): Boolean {
+ if (fingerprintParameters.count() != targetMethodParameters.count()) return false
+ val fingerprintIterator = fingerprintParameters.iterator()
+
+ targetMethodParameters.forEach {
+ if (!it.startsWith(fingerprintIterator.next())) return false
+ }
+
+ return true
+}
+
+
diff --git a/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt b/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt
new file mode 100644
index 00000000..cc626555
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patcher/InstructionFilter.kt
@@ -0,0 +1,872 @@
+@file:Suppress("unused")
+
+package app.revanced.patcher
+
+import app.revanced.patcher.FieldAccessFilter.Companion.parseJvmFieldAccess
+import app.revanced.patcher.InstructionFilter.Companion.METHOD_MAX_INSTRUCTIONS
+import app.revanced.patcher.MethodCallFilter.Companion.parseJvmMethodCall
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.Method
+import com.android.tools.smali.dexlib2.iface.instruction.Instruction
+import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.StringReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
+import java.util.EnumSet
+
+/**
+ * Matches method [Instruction] objects, similar to how [Fingerprint] matches entire fingerprints.
+ *
+ * The most basic filters match only opcodes and nothing more,
+ * and more precise filters can match:
+ * - Field references (get/put opcodes) by name/type.
+ * - Method calls (invoke_* opcodes) by name/parameter/return type.
+ * - Object instantiation for specific class types.
+ * - Literal const values.
+ *
+ * Variable space is allowed between each filter.
+ *
+ * All filters use a default [maxAfter] of [METHOD_MAX_INSTRUCTIONS]
+ * meaning they can match anywhere after the previous filter.
+ */
+abstract class InstructionFilter(
+ /**
+ * Maximum number of non matching method instructions that can appear before this filter.
+ * A value of zero means this filter must match immediately after the prior filter,
+ * or if this is the first filter then this may only match the first instruction of a method.
+ */
+ val maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+) {
+
+ init {
+ if (maxAfter < 0) {
+ throw IllegalArgumentException("maxAfter cannot be negative")
+ }
+ }
+
+ /**
+ * If this filter matches the method instruction.
+ *
+ * @param enclosingMethod The method of that contains [instruction].
+ * @param instruction The instruction to check for a match.
+ */
+ abstract fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean
+
+ companion object {
+ /**
+ * Maximum number of instructions allowed in a Java method.
+ * Indicates to allow a match anywhere after the previous filter.
+ */
+ const val METHOD_MAX_INSTRUCTIONS = 65535
+ }
+}
+
+
+
+class AnyInstruction internal constructor(
+ private val filters: List,
+ maxAfter: Int,
+) : InstructionFilter(maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ) : Boolean {
+ return filters.any { filter ->
+ filter.matches(enclosingMethod, instruction)
+ }
+ }
+}
+
+/**
+ * Logical OR operator where the first filter that matches satisfies this filter.
+ */
+fun anyInstruction(
+ vararg filters: InstructionFilter,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = AnyInstruction(filters.asList(), maxAfter)
+
+
+
+open class OpcodeFilter(
+ val opcode: Opcode,
+ maxAfter: Int,
+) : InstructionFilter(maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ return instruction.opcode == opcode
+ }
+}
+
+/**
+ * Single opcode.
+ */
+fun opcode(opcode: Opcode, maxAfter: Int = METHOD_MAX_INSTRUCTIONS) =
+ OpcodeFilter(opcode, maxAfter)
+
+
+
+/**
+ * Matches a single instruction from many kinds of opcodes.
+ * If matching only a single opcode instead use [OpcodeFilter].
+ */
+open class OpcodesFilter private constructor(
+ val opcodes: EnumSet?,
+ maxAfter: Int,
+) : InstructionFilter(maxAfter) {
+
+ protected constructor(
+ /**
+ * Value of `null` will match any opcode.
+ */
+ opcodes: List?,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+ ) : this(if (opcodes == null) null else EnumSet.copyOf(opcodes), maxAfter)
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (opcodes == null) {
+ return true // Match anything.
+ }
+ return opcodes.contains(instruction.opcode)
+ }
+
+ companion object {
+ /**
+ * First opcode can match anywhere in a method, but all
+ * subsequent opcodes must match after the previous opcode.
+ *
+ * A value of `null` indicates to match any opcode.
+ */
+ internal fun listOfOpcodes(opcodes: Collection): List {
+ var list = ArrayList(opcodes.size)
+
+ // First opcode can match anywhere.
+ var instructionsBefore = METHOD_MAX_INSTRUCTIONS
+ opcodes.forEach { opcode ->
+ list += if (opcode == null) {
+ // Null opcode matches anything.
+ OpcodesFilter(null as List?, instructionsBefore)
+ } else {
+ OpcodeFilter(opcode, instructionsBefore)
+ }
+ instructionsBefore = 0
+ }
+
+ return list
+ }
+ }
+}
+
+
+
+class LiteralFilter internal constructor(
+ var literal: () -> Long,
+ opcodes: List? = null,
+ maxAfter: Int,
+) : OpcodesFilter(opcodes, maxAfter) {
+
+ private var literalValue: Long? = null
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (!super.matches(enclosingMethod, instruction)) {
+ return false
+ }
+
+ if (instruction !is WideLiteralInstruction) return false
+
+ if (literalValue == null) {
+ literalValue = literal()
+ }
+
+ return instruction.wideLiteral == literalValue
+ }
+}
+
+/**
+ * Literal value, such as:
+ * `const v1, 0x7f080318`
+ *
+ * that can be matched using:
+ * `LiteralFilter(0x7f080318)`
+ * or
+ * `LiteralFilter(2131231512)`
+ */
+fun literal(
+ literal: () -> Long,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = LiteralFilter(literal, opcodes, maxAfter)
+
+/**
+ * Literal value, such as:
+ * `const v1, 0x7f080318`
+ *
+ * that can be matched using:
+ * `LiteralFilter(0x7f080318)`
+ * or
+ * `LiteralFilter(2131231512L)`
+ */
+fun literal(
+ literal: Long,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = LiteralFilter({ literal }, opcodes, maxAfter)
+
+/**
+ * Integer point literal.
+ */
+fun literal(
+ literal: Int,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = LiteralFilter({ literal.toLong() }, opcodes, maxAfter)
+
+/**
+ * Double point literal.
+ */
+fun literal(
+ literal: Double,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = LiteralFilter({ literal.toRawBits() }, opcodes, maxAfter)
+
+/**
+ * Floating point literal.
+ */
+fun literal(
+ literal: Float,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = LiteralFilter({ literal.toRawBits().toLong() }, opcodes, maxAfter)
+
+
+
+class StringFilter internal constructor(
+ var string: () -> String,
+ var partialMatch: Boolean,
+ maxAfter: Int,
+) : OpcodesFilter(listOf(Opcode.CONST_STRING, Opcode.CONST_STRING_JUMBO), maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (!super.matches(enclosingMethod, instruction)) {
+ return false
+ }
+
+ val instructionString = ((instruction as ReferenceInstruction).reference as StringReference).string
+ val filterString = string()
+
+ return if (partialMatch) {
+ instructionString.contains(filterString)
+ } else {
+ instructionString == filterString
+ }
+ }
+}
+
+/**
+ * Literal String instruction.
+ */
+fun string(
+ string: () -> String,
+ /**
+ * If [string] is a partial match, where the target string contains this string.
+ * For more precise matching, consider using [anyInstruction] with multiple exact string declarations.
+ */
+ partialMatch: Boolean = false,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = StringFilter(string, partialMatch, maxAfter)
+
+/**
+ * Literal String instruction.
+ */
+fun string(
+ string: String,
+ /**
+ * If [string] is a partial match, where the target string contains this string.
+ * For more precise matching, consider using [anyInstruction] with multiple exact string declarations.
+ */
+ partialMatch: Boolean = false,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = StringFilter({ string }, partialMatch, maxAfter)
+
+
+
+class MethodCallFilter internal constructor(
+ val definingClass: (() -> String)? = null,
+ val name: (() -> String)? = null,
+ val parameters: (() -> List)? = null,
+ val returnType: (() -> String)? = null,
+ opcodes: List? = null,
+ maxAfter: Int,
+) : OpcodesFilter(opcodes, maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (!super.matches(enclosingMethod, instruction)) {
+ return false
+ }
+
+ val reference = (instruction as? ReferenceInstruction)?.reference as? MethodReference
+ if (reference == null) return false
+
+ if (definingClass != null) {
+ val referenceClass = reference.definingClass
+ val definingClass = definingClass()
+
+ if (!referenceClass.endsWith(definingClass)) {
+ // Check if 'this' defining class is used.
+ // Would be nice if this also checked all super classes,
+ // but doing so requires iteratively checking all superclasses
+ // up to the root class since class defs are mere Strings.
+ if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) {
+ return false
+ } // else, the method call is for 'this' class.
+ }
+ }
+ if (name != null && reference.name != name()) {
+ return false
+ }
+ if (returnType != null && !reference.returnType.startsWith(returnType())) {
+ return false
+ }
+ if (parameters != null && !parametersStartsWith(reference.parameterTypes, parameters())) {
+ return false
+ }
+
+ return true
+ }
+
+ companion object {
+ private val regex = Regex("""^(L[^;]+;)->([^(\s]+)\(([^)]*)\)(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""")
+
+ internal fun parseJvmMethodCall(
+ methodSignature: String,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+ ): MethodCallFilter {
+ val matchResult = regex.matchEntire(methodSignature)
+ ?: throw IllegalArgumentException("Invalid method signature: $methodSignature")
+
+ val classDescriptor = matchResult.groupValues[1]
+ val methodName = matchResult.groupValues[2]
+ val paramDescriptorString = matchResult.groupValues[3]
+ val returnDescriptor = matchResult.groupValues[4]
+
+ val paramDescriptors = parseParameterDescriptors(paramDescriptorString)
+
+ return MethodCallFilter(
+ { classDescriptor },
+ { methodName },
+ { paramDescriptors },
+ { returnDescriptor },
+ opcodes,
+ maxAfter
+ )
+ }
+
+ /**
+ * Parses a single JVM type descriptor or an array descriptor at the current position.
+ * For example: Lcom/example/SomeClass; or I or [I or [Lcom/example/SomeClass;
+ */
+ private fun parseSingleType(params: String, startIndex: Int): Pair {
+ var i = startIndex
+
+ // Skip past array declaration, including multi-dimensional arrays.
+ val paramsLength = params.length
+ while (i < paramsLength && params[i] == '[') {
+ i++
+ }
+
+ return if (i < paramsLength && params[i] == 'L') {
+ // It's an object type starting with 'L', read until ';'
+ val semicolonPos = params.indexOf(';', i)
+ if (semicolonPos < 0) {
+ throw IllegalArgumentException("Malformed object descriptor (missing semicolon): $params")
+ }
+ // Substring from startIndex up to and including the semicolon.
+ val typeDescriptor = params.substring(startIndex, semicolonPos + 1)
+ typeDescriptor to (semicolonPos + 1)
+ } else {
+ // It's either a primitive or we've already consumed the array part
+ // So just take one character (e.g. 'I', 'Z', 'B', etc.)
+ val typeDescriptor = params.substring(startIndex, i + 1)
+ typeDescriptor to (i + 1)
+ }
+ }
+
+ /**
+ * Parses the parameters into a list of JVM type descriptors.
+ */
+ private fun parseParameterDescriptors(paramString: String): List {
+ val result = mutableListOf()
+ var currentIndex = 0
+ val stringLength = paramString.length
+
+ while (currentIndex < stringLength) {
+ val (type, nextIndex) = parseSingleType(paramString, currentIndex)
+ result.add(type)
+ currentIndex = nextIndex
+ }
+
+ return result
+ }
+ }
+}
+
+/**
+ * Identifies method calls.
+ *
+ * `Null` parameters matches anything.
+ *
+ * By default any type of method call matches.
+ * Specify opcodes if a specific type of method call is desired (such as only static calls).
+ */
+fun methodCall(
+ /**
+ * Defining class of the method call. Matches using endsWith().
+ *
+ * For calls to a method in the same class, use 'this' as the defining class.
+ * Note: 'this' does not work for methods declared only in a superclass.
+ */
+ definingClass: (() -> String)? = null,
+ /**
+ * Method name. Must be exact match of the method name.
+ */
+ name: (() -> String)? = null,
+ /**
+ * Parameters of the method call. Each parameter matches
+ * using startsWith() and semantics are the same as [Fingerprint].
+ */
+ parameters: (() -> List)? = null,
+ /**
+ * Return type. Matches using startsWith()
+ */
+ returnType: (() -> String)? = null,
+ /**
+ * Opcode types to match. By default this matches any method call opcode:
+ * `Opcode.INVOKE_*`.
+ *
+ * If this filter must match specific types of method call, then specify the desired opcodes
+ * such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
+ */
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = MethodCallFilter(
+ definingClass,
+ name,
+ parameters,
+ returnType,
+ opcodes,
+ maxAfter
+)
+
+fun methodCall(
+ /**
+ * Defining class of the method call. Matches using endsWith().
+ *
+ * For calls to a method in the same class, use 'this' as the defining class.
+ * Note: 'this' does not work for methods declared only in a superclass.
+ */
+ definingClass: String? = null,
+ /**
+ * Method name. Must be exact match of the method name.
+ */
+ name: String? = null,
+ /**
+ * Parameters of the method call. Each parameter matches
+ * using startsWith() and semantics are the same as [Fingerprint].
+ */
+ parameters: List? = null,
+ /**
+ * Return type. Matches using startsWith()
+ */
+ returnType: String? = null,
+ /**
+ * Opcode types to match. By default this matches any method call opcode:
+ * `Opcode.INVOKE_*`.
+ *
+ * If this filter must match specific types of method call, then specify the desired opcodes
+ * such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
+ */
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = MethodCallFilter(
+ if (definingClass != null) {
+ { definingClass }
+ } else null,
+ if (name != null) {
+ { name }
+ } else null,
+ if (parameters != null) {
+ { parameters }
+ } else null,
+ if (returnType != null) {
+ { returnType }
+ } else null,
+ opcodes,
+ maxAfter
+)
+
+fun methodCall(
+ /**
+ * Defining class of the method call. Matches using endsWith().
+ *
+ * For calls to a method in the same class, use 'this' as the defining class.
+ * Note: 'this' does not work for methods declared only in a superclass.
+ */
+ definingClass: String? = null,
+ /**
+ * Method name. Must be exact match of the method name.
+ */
+ name: String? = null,
+ /**
+ * Parameters of the method call. Each parameter matches
+ * using startsWith() and semantics are the same as [Fingerprint].
+ */
+ parameters: List? = null,
+ /**
+ * Return type. Matches using startsWith()
+ */
+ returnType: String? = null,
+ /**
+ * Opcode types to match. By default this matches any method call opcode:
+ * `Opcode.INVOKE_*`.
+ *
+ * If this filter must match specific types of method call, then specify the desired opcodes
+ * such as [Opcode.INVOKE_STATIC], [Opcode.INVOKE_STATIC_RANGE] to match only static calls.
+ */
+ opcode: Opcode,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = MethodCallFilter(
+ if (definingClass != null) {
+ { definingClass }
+ } else null,
+ if (name != null) {
+ { name }
+ } else null,
+ if (parameters != null) {
+ { parameters }
+ } else null,
+ if (returnType != null) {
+ { returnType }
+ } else null,
+ listOf(opcode),
+ maxAfter
+)
+
+/**
+ * Method call for a copy pasted SMALI style method signature. e.g.:
+ * `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`
+ *
+ * Does not support obfuscated method names or parameter/return types.
+ */
+fun methodCall(
+ smali: String,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+) = parseJvmMethodCall(smali, opcodes, maxAfter)
+
+/**
+ * Method call for a copy pasted SMALI style method signature. e.g.:
+ * `Landroid/view/View;->inflate(Landroid/content/Context;ILandroid/view/ViewGroup;)Landroid/view/View;`
+ *
+ * Does not support obfuscated method names or parameter/return types.
+ */
+fun methodCall(
+ smali: String,
+ opcode: Opcode,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+) = parseJvmMethodCall(smali, listOf(opcode), maxAfter)
+
+
+
+class FieldAccessFilter internal constructor(
+ val definingClass: (() -> String)? = null,
+ val name: (() -> String)? = null,
+ val type: (() -> String)? = null,
+ opcodes: List? = null,
+ maxAfter: Int,
+) : OpcodesFilter(opcodes, maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (!super.matches(enclosingMethod, instruction)) {
+ return false
+ }
+
+ val reference = (instruction as? ReferenceInstruction)?.reference as? FieldReference
+ if (reference == null) return false
+
+ if (definingClass != null) {
+ val referenceClass = reference.definingClass
+ val definingClass = definingClass()
+
+ if (!referenceClass.endsWith(definingClass)) {
+ if (!(definingClass == "this" && referenceClass == enclosingMethod.definingClass)) {
+ return false
+ } // else, the method call is for 'this' class.
+ }
+ }
+ if (name != null && reference.name != name()) {
+ return false
+ }
+ if (type != null && !reference.type.startsWith(type())) {
+ return false
+ }
+
+ return true
+ }
+
+ internal companion object {
+ private val regex = Regex("""^(L[^;]+;)->([^:]+):(\[?L[^;]+;|\[?[BCSIJFDZV])${'$'}""")
+
+ internal fun parseJvmFieldAccess(
+ fieldSignature: String,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+ ): FieldAccessFilter {
+ val matchResult = regex.matchEntire(fieldSignature)
+ ?: throw IllegalArgumentException("Invalid field access smali: $fieldSignature")
+
+ return fieldAccess(
+ definingClass = matchResult.groupValues[1],
+ name = matchResult.groupValues[2],
+ type = matchResult.groupValues[3],
+ opcodes = opcodes,
+ maxAfter = maxAfter
+ )
+ }
+ }
+}
+
+/**
+ * Matches a field call, such as:
+ * `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
+ */
+fun fieldAccess(
+ /**
+ * Defining class of the field call. Matches using endsWith().
+ *
+ * For calls to a method in the same class, use 'this' as the defining class.
+ * Note: 'this' does not work for fields found in superclasses.
+ */
+ definingClass: (() -> String)? = null,
+ /**
+ * Name of the field. Must be a full match of the field name.
+ */
+ name: (() -> String)? = null,
+ /**
+ * Class type of field. Partial matches using startsWith() is allowed.
+ */
+ type: (() -> String)? = null,
+ /**
+ * Valid opcodes matches for this instruction.
+ * By default this matches any kind of field access
+ * (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc).
+ */
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = FieldAccessFilter(definingClass, name, type, opcodes, maxAfter)
+
+/**
+ * Matches a field call, such as:
+ * `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
+ */
+fun fieldAccess(
+ /**
+ * Defining class of the field call. Matches using endsWith().
+ *
+ * For calls to a method in the same class, use 'this' as the defining class.
+ * Note: 'this' does not work for fields found in superclasses.
+ */
+ definingClass: String? = null,
+ /**
+ * Name of the field. Must be a full match of the field name.
+ */
+ name: String? = null,
+ /**
+ * Class type of field. Partial matches using startsWith() is allowed.
+ */
+ type: String? = null,
+ /**
+ * Valid opcodes matches for this instruction.
+ * By default this matches any kind of field access
+ * (`Opcode.IGET`, `Opcode.SGET`, `Opcode.IPUT`, `Opcode.SPUT`, etc).
+ */
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = FieldAccessFilter(
+ if (definingClass != null) {
+ { definingClass }
+ } else null,
+ if (name != null) {
+ { name }
+ } else null,
+ if (type != null) {
+ { type }
+ } else null,
+ opcodes,
+ maxAfter
+)
+
+/**
+ * Matches a field call, such as:
+ * `iget-object v0, p0, Lahhh;->g:Landroid/view/View;`
+ */
+fun fieldAccess(
+ /**
+ * Defining class of the field call. Matches using endsWith().
+ *
+ * For calls to a method in the same class, use 'this' as the defining class.
+ * Note: 'this' does not work for fields found in superclasses.
+ */
+ definingClass: String? = null,
+ /**
+ * Name of the field. Must be a full match of the field name.
+ */
+ name: String? = null,
+ /**
+ * Class type of field. Partial matches using startsWith() is allowed.
+ */
+ type: String? = null,
+ opcode: Opcode,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS,
+) = fieldAccess(
+ definingClass,
+ name,
+ type,
+ listOf(opcode),
+ maxAfter
+)
+
+/**
+ * Field access for a copy pasted SMALI style field access call. e.g.:
+ * `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`
+ *
+ * Does not support obfuscated field names or obfuscated field types.
+ */
+fun fieldAccess(
+ smali: String,
+ opcodes: List? = null,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+) = parseJvmFieldAccess(smali, opcodes, maxAfter)
+
+/**
+ * Field access for a copy pasted SMALI style field access call. e.g.:
+ * `Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;`
+ *
+ * Does not support obfuscated field names or obfuscated field types.
+ */
+fun fieldAccess(
+ smali: String,
+ opcode: Opcode,
+ maxAfter: Int = METHOD_MAX_INSTRUCTIONS
+) = parseJvmFieldAccess(smali, listOf(opcode), maxAfter)
+
+
+
+class NewInstanceFilter internal constructor (
+ var type: () -> String,
+ maxAfter : Int,
+) : OpcodesFilter(listOf(Opcode.NEW_INSTANCE, Opcode.NEW_ARRAY), maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (!super.matches(enclosingMethod, instruction)) {
+ return false
+ }
+
+ val reference = (instruction as? ReferenceInstruction)?.reference as? TypeReference
+ if (reference == null) return false
+
+ return reference.type.endsWith(type())
+ }
+}
+
+
+/**
+ * Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type.
+ *
+ * @param type Class type that matches the target instruction using [String.endsWith].
+ */
+fun newInstancetype(type: () -> String, maxAfter: Int = METHOD_MAX_INSTRUCTIONS) =
+ NewInstanceFilter(type, maxAfter)
+
+/**
+ * Opcode type [Opcode.NEW_INSTANCE] or [Opcode.NEW_ARRAY] with a non obfuscated class type.
+ *
+ * @param type Class type that matches the target instruction using [String.endsWith].
+ */
+fun newInstance(type: String, maxAfter: Int = METHOD_MAX_INSTRUCTIONS) : NewInstanceFilter {
+ if (!type.endsWith(";")) {
+ throw IllegalArgumentException("Class type does not end with a semicolon: $type")
+ }
+ return NewInstanceFilter({ type }, maxAfter)
+}
+
+
+
+class CheckCastFilter internal constructor (
+ var type: () -> String,
+ maxAfter : Int,
+) : OpcodeFilter(Opcode.CHECK_CAST, maxAfter) {
+
+ override fun matches(
+ enclosingMethod: Method,
+ instruction: Instruction
+ ): Boolean {
+ if (!super.matches(enclosingMethod, instruction)) {
+ return false
+ }
+
+ val reference = (instruction as? ReferenceInstruction)?.reference as? TypeReference
+ if (reference == null) return false
+
+ return reference.type.endsWith(type())
+ }
+}
+
+/**
+ * Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type.
+ *
+ * @param type Class type that matches the target instruction using [String.endsWith].
+ */
+fun checkCast(type: () -> String, maxAfter: Int = METHOD_MAX_INSTRUCTIONS) =
+ CheckCastFilter(type, maxAfter)
+
+/**
+ * Opcode type [Opcode.CHECK_CAST] with a non obfuscated class type.
+ *
+ * @param type Class type that matches the target instruction using [String.endsWith].
+ */
+fun checkCast(type: String, maxAfter: Int = METHOD_MAX_INSTRUCTIONS) : CheckCastFilter {
+ if (!type.endsWith(";")) {
+ throw IllegalArgumentException("Class type does not end with a semicolon: $type")
+ }
+
+ return CheckCastFilter({ type }, maxAfter)
+}
+
diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt
index 57374816..b945a96b 100644
--- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt
+++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt
@@ -6,8 +6,7 @@ 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
-import app.revanced.patcher.util.ProxyClassList
-import app.revanced.patcher.util.proxy.ClassProxy
+import app.revanced.patcher.util.PatchClasses
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
@@ -42,22 +41,22 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal val opcodes: Opcodes
/**
- * The list of classes.
+ * All classes for the target app and any extension classes.
*/
- val classes = ProxyClassList(
+ val classes = PatchClasses(
MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null,
- ).also { opcodes = it.opcodes }.classes.toMutableList(),
+ ).also { opcodes = it.opcodes }.classes
)
/**
* The lookup maps for methods and the class they are a member of from the [classes].
*/
- internal val lookupMaps by lazy { LookupMaps(classes) }
+ internal val lookupMaps by lazy { LookupMaps(classes.pool.values) }
/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
@@ -68,11 +67,10 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
- val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
+ val existingClass = classes.classByOrNull(classDef.type) ?: run {
logger.fine { "Adding class \"$classDef\"" }
- classes += classDef
- lookupMaps.classesByType[classDef.type] = classDef
+ classes.addClass(classDef)
return@forEach
}
@@ -85,32 +83,93 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
return@let
}
- classes -= existingClass
- classes += mergedClass
+ classes.addClass(mergedClass)
}
}
} ?: logger.fine("No extension to merge")
}
+ /**
+ * Find a class with a predicate.
+ *
+ * @param classType The full classname.
+ * @return An immutable instance of the class type.
+ * @see mutableClassBy
+ */
+ fun classBy(classType: String) = classes.classBy(classType)
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return An immutable instance of the class type.
+ * @see mutableClassBy
+ */
+ fun classBy(predicate: (ClassDef) -> Boolean) = classes.classBy(predicate)
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param classType The full classname.
+ * @return An immutable instance of the class type.
+ * @see mutableClassBy
+ */
+ fun classByOrNull(classType: String) = classes.classByOrNull(classType)
+
/**
* Find a class with a predicate.
*
* @param predicate A predicate to match the class.
- * @return A proxy for the first class that matches the predicate.
+ * @return An immutable instance of the class type.
*/
- fun classBy(predicate: (ClassDef) -> Boolean) =
- classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) }
+ fun classByOrNull(predicate: (ClassDef) -> Boolean) = classes.classByOrNull(predicate)
/**
- * Proxy the class to allow mutation.
+ * Find a class with a predicate.
*
- * @param classDef The class to proxy.
+ * @param classType The full classname.
+ * @return A mutable version of the class type.
+ */
+ fun mutableClassBy(classType: String) = classes.mutableClassBy(classType)
+
+ /**
+ * Find a class with a predicate.
*
- * @return A proxy for the class.
+ * @param classDef An immutable class.
+ * @return A mutable version of the class definition.
*/
- fun proxy(classDef: ClassDef) = classes.proxyPool.find {
- it.immutableClass.type == classDef.type
- } ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
+ fun mutableClassBy(classDef: ClassDef) = classes.mutableClassBy(classDef)
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return A mutable class that matches the predicate.
+ */
+ fun mutableClassBy(predicate: (ClassDef) -> Boolean) = classes.mutableClassBy(predicate)
+
+ /**
+ * Mutable class from a full class name.
+ * Returns `null` if class is not available, such as a built in Android or Java library.
+ *
+ * @param classType The full classname.
+ * @return A mutable version of the class type.
+ */
+ fun mutableClassByOrNull(classType: String) = classes.mutableClassByOrNull(classType)
+
+ /**
+ * Find a mutable class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return A mutable class that matches the predicate.
+ */
+ fun mutableClassByOrNull(predicate: (ClassDef) -> Boolean) = classes.mutableClassByOrNull(predicate)
+
+ /**
+ * @return The mutable instance of an immutable class.
+ */
+ @Deprecated("Instead use `mutableClassBy(String)`, `mutableClassBy(ClassDef)`, or `mutableClassBy(predicate)`")
+ fun proxy(classDef: ClassDef) = classes.mutableClassBy(classDef)
/**
* Navigate a method.
@@ -144,8 +203,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
this,
BasicDexFileNamer(),
object : DexFile {
- override fun getClasses() =
- this@BytecodePatchContext.classes.also(ProxyClassList::replaceClasses).toSet()
+ override fun getClasses() = this@BytecodePatchContext.classes.pool.values.toSet()
override fun getOpcodes() = this@BytecodePatchContext.opcodes
},
@@ -161,52 +219,45 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
}
/**
- * A lookup map for methods and the class they are a member of and classes.
+ * A lookup map for strings and the methods they are a member of.
*
* @param classes The list of classes to create the lookup maps from.
*/
- internal class LookupMaps internal constructor(classes: List) : Closeable {
+ internal class LookupMaps internal constructor(classes: Collection) : Closeable {
/**
- * Methods associated by strings referenced in it.
+ * Methods associated by strings referenced in them.
*/
internal val methodsByStrings = MethodClassPairsLookupMap()
- // Lookup map for fast checking if a class exists by its type.
- val classesByType = mutableMapOf().apply {
- classes.forEach { classDef -> put(classDef.type, classDef) }
- }
-
init {
classes.forEach { classDef ->
classDef.methods.forEach { method ->
- val methodClassPair: MethodClassPair = method to classDef
+ val methodClassPair: MethodClassPair by lazy {
+ method to classDef
+ }
// Add strings contained in the method as the key.
- method.instructionsOrNull?.forEach instructions@{ instruction ->
+ method.instructionsOrNull?.forEach { instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
- return@instructions
+ return@forEach
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
methodsByStrings[string] = methodClassPair
}
-
- // In the future, the class type could be added to the lookup map.
- // This would require MethodFingerprint to be changed to include the class type.
}
}
}
override fun close() {
methodsByStrings.clear()
- classesByType.clear()
}
}
override fun close() {
lookupMaps.close()
- classes.clear()
+ classes.close()
}
}
diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt
index d9a3a218..439286bb 100644
--- a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt
+++ b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt
@@ -181,7 +181,7 @@ internal object ClassMerger {
callback(targetClass)
targetClass.superclass ?: return
- this.classBy { targetClass.superclass == it.type }?.mutableClass?.let {
+ mutableClassByOrNull(targetClass.superclass!!)?.let {
traverseClassHierarchy(it, callback)
}
}
diff --git a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
index d894e9e7..35d4d515 100644
--- a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
+++ b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt
@@ -80,7 +80,7 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method mutably.
*/
- fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
+ fun stop() = mutableClassBy(lastNavigatedMethodReference.definingClass).firstMethodBySignature
as MutableMethod
/**
@@ -95,14 +95,7 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method immutably.
*/
- fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
-
- /**
- * Predicate to match the class defining the current method reference.
- */
- private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
- classDef.type == lastNavigatedMethodReference.definingClass
- }
+ fun original(): Method = classes.classBy(lastNavigatedMethodReference.definingClass).firstMethodBySignature
/**
* Find the first [lastNavigatedMethodReference] in the class.
diff --git a/src/main/kotlin/app/revanced/patcher/util/PatchClasses.kt b/src/main/kotlin/app/revanced/patcher/util/PatchClasses.kt
new file mode 100644
index 00000000..9e3043c8
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patcher/util/PatchClasses.kt
@@ -0,0 +1,127 @@
+package app.revanced.patcher.util
+
+import app.revanced.patcher.patch.PatchException
+import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
+import com.android.tools.smali.dexlib2.iface.ClassDef
+
+@Deprecated("Instead use PatchClasses")
+typealias ProxyClassList = PatchClasses
+
+/**
+ * All classes for the target app and any extension classes.
+ */
+class PatchClasses internal constructor(
+ /**
+ * Pool of both immutable and mutable classes.
+ */
+ internal val pool: MutableMap
+) {
+
+ internal constructor(set: Set) :
+ this(set.associateByTo(HashMap(set.size * 3 / 2)) { it.type })
+
+ internal fun addClass(classDef: ClassDef) {
+ pool[classDef.type] = classDef
+ }
+
+ internal fun close() {
+ pool.clear()
+ }
+
+ /**
+ * Iterate over all classes.
+ */
+ fun forEach(action: (ClassDef) -> Unit) {
+ pool.values.forEach(action)
+ }
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param classType The full classname.
+ * @return An immutable instance of the class type.
+ * @see mutableClassBy
+ */
+ fun classByOrNull(classType: String) = pool[classType]
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return An immutable instance of the class type, or null if not found.
+ */
+ fun classByOrNull(predicate: (ClassDef) -> Boolean) = pool.values.find(predicate)
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return An immutable instance of the class type.
+ */
+ fun classBy(predicate: (ClassDef) -> Boolean) = classByOrNull(predicate)
+ ?: throw PatchException("Could not find any class match")
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param classType The full classname.
+ * @return An immutable instance of the class type.
+ * @see mutableClassBy
+ */
+ fun classBy(classType: String) = classByOrNull(classType)
+ ?: throw PatchException("Could not find class: $classType")
+
+ /**
+ * Mutable class from a full class name.
+ * Returns `null` if class is not available, such as a built in Android or Java library.
+ *
+ * @param classDefType The full classname.
+ * @return A mutable version of the class type.
+ */
+ fun mutableClassByOrNull(classDefType: String) : MutableClass? {
+ var classDef = pool[classDefType]
+ if (classDef == null) return null
+ if (classDef is MutableClass) return classDef
+
+ classDef = MutableClass(classDef)
+ pool[classDefType] = classDef
+ return classDef
+ }
+
+ /**
+ * Find a class with a predicate.
+ *
+ * @param classDefType The full classname.
+ * @return A mutable version of the class type.
+ */
+ fun mutableClassBy(classDefType: String) = mutableClassByOrNull(classDefType)
+ ?: throw PatchException("Could not find class: $classDefType")
+
+ /**
+ * Find a mutable class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return A mutable class that matches the predicate.
+ */
+ fun mutableClassByOrNull(predicate: (ClassDef) -> Boolean) =
+ classByOrNull(predicate)?.let {
+ if (it is MutableClass) it else mutableClassBy(it.type)
+ }
+
+ /**
+ * @param classDef An immutable class.
+ * @return A mutable version of the class definition.
+ */
+ fun mutableClassBy(classDef: ClassDef) =
+ if (classDef is MutableClass) classDef else mutableClassBy(classDef.type)
+
+ /**
+ * Find a mutable class with a predicate.
+ *
+ * @param predicate A predicate to match the class.
+ * @return A mutable class that matches the predicate.
+ */
+ fun mutableClassBy(predicate: (ClassDef) -> Boolean) = mutableClassByOrNull(predicate)
+ ?: throw PatchException("Could not find any class match")
+
+}
diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt
deleted file mode 100644
index cdc334f8..00000000
--- a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package app.revanced.patcher.util
-
-import app.revanced.patcher.util.proxy.ClassProxy
-import com.android.tools.smali.dexlib2.iface.ClassDef
-
-/**
- * A list of classes and proxies.
- *
- * @param classes The classes to be backed by proxies.
- */
-class ProxyClassList internal constructor(classes: MutableList) : MutableList by classes {
- internal val proxyPool = mutableListOf()
-
- /**
- * Replace all classes with their mutated versions.
- */
- internal fun replaceClasses() =
- proxyPool.removeIf { proxy ->
- // If the proxy is unused, return false to keep it in the proxies list.
- if (!proxy.resolved) return@removeIf false
-
- // If it has been used, replace the original class with the mutable class.
- remove(proxy.immutableClass)
- add(proxy.mutableClass)
-
- // Return true to remove the proxy from the proxies list.
- return@removeIf true
- }
-}
diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt b/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt
deleted file mode 100644
index ccd8abd1..00000000
--- a/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package app.revanced.patcher.util.proxy
-
-import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
-import com.android.tools.smali.dexlib2.iface.ClassDef
-
-/**
- * A proxy class for a [ClassDef].
- *
- * A class proxy simply holds a reference to the original class
- * and allocates a mutable clone for the original class if needed.
- *
- * @param immutableClass The class to proxy.
- */
-class ClassProxy internal constructor(
- val immutableClass: ClassDef,
-) {
- /**
- * Weather the proxy was actually used.
- */
- internal var resolved = false
-
- /**
- * The mutable clone of the original class.
- *
- * Note: This is only allocated if the proxy is actually used.
- */
- val mutableClass by lazy {
- resolved = true
- if (immutableClass is MutableClass) {
- immutableClass
- } else {
- MutableClass(immutableClass)
- }
- }
-}
diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt
index bf4aab27..3398266a 100644
--- a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt
+++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt
@@ -2,7 +2,7 @@ package app.revanced.patcher
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
-import app.revanced.patcher.util.ProxyClassList
+import app.revanced.patcher.util.PatchClasses
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.*
@@ -10,6 +10,7 @@ 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.assertThrows
import java.util.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -39,6 +40,7 @@ internal object PatcherTest {
}
}
+
@Test
fun `executes patches in correct order`() {
val executed = mutableListOf()
@@ -70,6 +72,7 @@ internal object PatcherTest {
)
}
+
@Test
fun `handles execution of patches correctly when exceptions occur`() {
val executed = mutableListOf()
@@ -150,7 +153,7 @@ internal object PatcherTest {
val patch = bytecodePatch {
execute {
// Fingerprint can never match.
- val fingerprint = fingerprint { }
+ val fingerprint by fingerprint { }
// Throws, because the fingerprint can't be matched.
fingerprint.patternMatch
@@ -165,9 +168,9 @@ internal object PatcherTest {
@Test
fun `matches fingerprint`() {
- every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
- mutableListOf(
- ImmutableClassDef(
+ every { patcher.context.bytecodeContext.classes } returns PatchClasses(
+ mutableMapOf(
+ "class" to ImmutableClassDef(
"class",
0,
null,
@@ -191,15 +194,15 @@ internal object PatcherTest {
),
)
- val fingerprint = fingerprint { returns("V") }
- val fingerprint2 = fingerprint { returns("V") }
- val fingerprint3 = fingerprint { returns("V") }
+ val fingerprint by fingerprint { returns("V") }
+ val fingerprint2 by fingerprint { returns("V") }
+ val fingerprint3 by fingerprint { returns("V") }
val patches = setOf(
bytecodePatch {
execute {
- fingerprint.match(classes.first().methods.first())
- fingerprint2.match(classes.first())
+ fingerprint.match(classes.pool.values.first().methods.first())
+ fingerprint2.match(classes.pool.values.first())
fingerprint3.originalClassDef
}
},
@@ -217,9 +220,245 @@ internal object PatcherTest {
}
}
+ @Test
+ fun `MethodCallFilter smali parsing`() {
+ with(patcher.context.bytecodeContext) {
+ var definingClass = "Landroid/view/View;"
+ var name = "inflate"
+ var parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
+ var returnType = "Landroid/view/View;"
+ var methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ var filter = MethodCallFilter.parseJvmMethodCall(methodSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(parameters == filter.parameters!!()) },
+ { assertTrue(returnType == filter.returnType!!()) },
+ )
+
+
+ definingClass = "Landroid/view/View\$InnerClass;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "Landroid/view/ViewGroup\$ViewInnerClass;", "J")
+ returnType = "V"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ filter = MethodCallFilter.parseJvmMethodCall(methodSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(parameters == filter.parameters!!()) },
+ { assertTrue(returnType == filter.returnType!!()) },
+ )
+
+ definingClass = "Landroid/view/View\$InnerClass;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "Landroid/view/ViewGroup\$ViewInnerClass;", "J")
+ returnType = "I"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ filter = MethodCallFilter.parseJvmMethodCall(methodSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(parameters == filter.parameters!!()) },
+ { assertTrue(returnType == filter.returnType!!()) },
+ )
+
+ definingClass = "Landroid/view/View\$InnerClass;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "Landroid/view/ViewGroup\$ViewInnerClass;", "J")
+ returnType = "[I"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ filter = MethodCallFilter.parseJvmMethodCall(methodSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(parameters == filter.parameters!!()) },
+ { assertTrue(returnType == filter.returnType!!()) },
+ )
+ }
+ }
+
+ @Test
+ fun `MethodCallFilter smali bad input`() {
+ with(patcher.context.bytecodeContext) {
+ var definingClass = "Landroid/view/View;"
+ var name = "inflate"
+ var parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
+ var returnType = "Landroid/view/View;"
+ var methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ assertThrows("Bad max instructions before") {
+ MethodCallFilter.parseJvmMethodCall(methodSignature, null, -1)
+ }
+
+
+ definingClass = "Landroid/view/View"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
+ returnType = "Landroid/view/View;"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ assertThrows("Defining class missing semicolon") {
+ MethodCallFilter.parseJvmMethodCall(methodSignature)
+ }
+
+
+ definingClass = "Landroid/view/View;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup")
+ returnType = "Landroid/view/View"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ assertThrows("Return type missing semicolon") {
+ MethodCallFilter.parseJvmMethodCall(methodSignature)
+ }
+
+
+ definingClass = "Landroid/view/View;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
+ returnType = ""
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ assertThrows("Empty return type") {
+ MethodCallFilter.parseJvmMethodCall(methodSignature)
+ }
+
+
+ definingClass = "Landroid/view/View;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
+ returnType = "Landroid/view/View"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ assertThrows("Return type class missing semicolon") {
+ MethodCallFilter.parseJvmMethodCall(methodSignature)
+ }
+
+
+ definingClass = "Landroid/view/View;"
+ name = "inflate"
+ parameters = listOf("[Ljava/lang/String;", "I", "Z", "F", "J", "Landroid/view/ViewGroup;")
+ returnType = "Q"
+ methodSignature = "$definingClass->$name(${parameters.joinToString("")})$returnType"
+
+ assertThrows("Bad primitive type") {
+ MethodCallFilter.parseJvmMethodCall(methodSignature)
+ }
+ }
+ }
+
+ @Test
+ fun `FieldAccess smali parsing`() {
+ with(patcher.context.bytecodeContext) {
+ var definingClass = "Ljava/lang/Boolean;"
+ var name = "TRUE"
+ var type = "Ljava/lang/Boolean;"
+ var fieldSignature = "$definingClass->$name:$type"
+
+ var filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(type == filter.type!!()) },
+ )
+
+
+ definingClass = "Landroid/view/View\$InnerClass;"
+ name = "arrayField"
+ type = "[Ljava/lang/Boolean;"
+ fieldSignature = "$definingClass->$name:$type"
+
+ filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(type == filter.type!!()) },
+ )
+
+
+ definingClass = "Landroid/view/View\$InnerClass;"
+ name = "primitiveField"
+ type = "I"
+ fieldSignature = "$definingClass->$name:$type"
+
+ filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(type == filter.type!!()) },
+ )
+
+
+ definingClass = "Landroid/view/View\$InnerClass;"
+ name = "primitiveField"
+ type = "[I"
+ fieldSignature = "$definingClass->$name:$type"
+
+ filter = FieldAccessFilter.parseJvmFieldAccess(fieldSignature)
+
+ assertAll(
+ { assertTrue(definingClass == filter.definingClass!!()) },
+ { assertTrue(name == filter.name!!()) },
+ { assertTrue(type == filter.type!!()) },
+ )
+ }
+ }
+
+ @Test
+ fun `FieldAccess smali bad input`() {
+ with(patcher.context.bytecodeContext) {
+ assertThrows("Defining class missing semicolon") {
+ FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View->fieldName:Landroid/view/View;")
+ }
+
+ assertThrows("Type class missing semicolon") {
+ FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View;->fieldName:Landroid/view/View")
+ }
+
+ assertThrows("Empty field name") {
+ FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View;->:Landroid/view/View;")
+ }
+
+ assertThrows("Invalid primitive type") {
+ FieldAccessFilter.parseJvmFieldAccess("Landroid/view/View;->fieldName:Q")
+ }
+ }
+ }
+
+ @Test
+ fun `NewInstance bad input`() {
+ with(patcher.context.bytecodeContext) {
+ assertThrows("Defining class missing semicolon") {
+ newInstance("Lcom/whatever/BadClassType")
+ }
+ }
+ }
+
+
+ @Test
+ fun `CheckCast bad input`() {
+ with(patcher.context.bytecodeContext) {
+ assertThrows("Defining class missing semicolon") {
+ checkCast("Lcom/whatever/BadClassType")
+ }
+ }
+ }
+
private operator fun Set>.invoke(): List {
every { patcher.context.executablePatches } returns toMutableSet()
- every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
+ every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes.pool.values)
every { with(patcher.context.bytecodeContext) { mergeExtension(any()) } } just runs
return runBlocking { patcher().toList() }