feat: Convert APIs to Kotlin DSL (#298)

This commit converts various APIs to Kotlin DSL.

BREAKING CHANGE: Various old APIs are removed, and DSL APIs are added instead.
This commit is contained in:
oSumAtrIX 2024-07-21 22:45:45 +02:00
parent 6e3ba7419b
commit 11a911dc67
64 changed files with 3461 additions and 3437 deletions

View file

@ -1,62 +1,65 @@
public abstract interface class app/revanced/patcher/IntegrationsConsumer { public final class app/revanced/patcher/Fingerprint {
public abstract fun acceptIntegrations (Ljava/util/List;)V public final fun getMatch ()Lapp/revanced/patcher/Match;
public abstract fun acceptIntegrations (Ljava/util/Set;)V public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z
}
public final class app/revanced/patcher/FingerprintBuilder {
public fun <init> ()V
public final fun accessFlags (I)V
public final fun accessFlags ([Lcom/android/tools/smali/dexlib2/AccessFlags;)V
public final fun custom (Lkotlin/jvm/functions/Function2;)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
public final fun returns (Ljava/lang/String;)V
public final fun strings ([Ljava/lang/String;)V
}
public final class app/revanced/patcher/FingerprintKt {
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
public static final fun fingerprint (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
public static synthetic fun fingerprint$default (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
} }
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation { public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
} }
public final class app/revanced/patcher/Match {
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/Match$PatternMatch;Ljava/util/List;Lapp/revanced/patcher/patch/BytecodePatchContext;)V
public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List;
}
public final class app/revanced/patcher/Match$PatternMatch {
public fun <init> (II)V
public final fun getEndIndex ()I
public final fun getStartIndex ()I
}
public final class app/revanced/patcher/Match$StringMatch {
public fun <init> (Ljava/lang/String;I)V
public final fun getIndex ()I
public final fun getString ()Ljava/lang/String;
}
public final class app/revanced/patcher/PackageMetadata { public final class app/revanced/patcher/PackageMetadata {
public final fun getPackageName ()Ljava/lang/String; public final fun getPackageName ()Ljava/lang/String;
public final fun getPackageVersion ()Ljava/lang/String; public final fun getPackageVersion ()Ljava/lang/String;
} }
public abstract class app/revanced/patcher/PatchBundleLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker { public final class app/revanced/patcher/Patcher : java/io/Closeable {
public synthetic fun <init> (Ljava/lang/ClassLoader;[Ljava/io/File;Lkotlin/jvm/functions/Function1;Ljava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun add (Lapp/revanced/patcher/patch/Patch;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public fun contains (Lapp/revanced/patcher/patch/Patch;)Z
public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public fun getSize ()I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun retainAll (Ljava/util/Collection;)Z
public final fun size ()I
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/patcher/PatchBundleLoader {
public fun <init> ([Ljava/io/File;)V
public fun <init> ([Ljava/io/File;Ljava/io/File;)V
public synthetic fun <init> ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/PatchBundleLoader$Jar : app/revanced/patcher/PatchBundleLoader {
public fun <init> ([Ljava/io/File;)V
}
public abstract interface class app/revanced/patcher/PatchExecutorFunction : java/util/function/Function {
}
public final class app/revanced/patcher/Patcher : app/revanced/patcher/IntegrationsConsumer, app/revanced/patcher/PatchExecutorFunction, app/revanced/patcher/PatchesConsumer, java/io/Closeable, java/util/function/Supplier {
public fun <init> (Lapp/revanced/patcher/PatcherConfig;)V public fun <init> (Lapp/revanced/patcher/PatcherConfig;)V
public fun <init> (Lapp/revanced/patcher/PatcherOptions;)V
public fun acceptIntegrations (Ljava/util/List;)V
public fun acceptIntegrations (Ljava/util/Set;)V
public fun acceptPatches (Ljava/util/List;)V
public fun acceptPatches (Ljava/util/Set;)V
public synthetic fun apply (Ljava/lang/Object;)Ljava/lang/Object;
public fun apply (Z)Lkotlinx/coroutines/flow/Flow;
public fun close ()V public fun close ()V
public fun get ()Lapp/revanced/patcher/PatcherResult; public final fun get ()Lapp/revanced/patcher/PatcherResult;
public synthetic fun get ()Ljava/lang/Object;
public final fun getContext ()Lapp/revanced/patcher/PatcherContext; public final fun getContext ()Lapp/revanced/patcher/PatcherContext;
public final fun invoke ()Lkotlinx/coroutines/flow/Flow;
public final fun plusAssign (Ljava/util/Set;)V
} }
public final class app/revanced/patcher/PatcherConfig { public final class app/revanced/patcher/PatcherConfig {
@ -68,45 +71,12 @@ public final class app/revanced/patcher/PatcherContext {
public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata; public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata;
} }
public abstract class app/revanced/patcher/PatcherException : java/lang/Exception {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/PatcherException$CircularDependencyException : app/revanced/patcher/PatcherException {
}
public final class app/revanced/patcher/PatcherOptions {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun copy (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)Lapp/revanced/patcher/PatcherOptions;
public static synthetic fun copy$default (Lapp/revanced/patcher/PatcherOptions;Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)Lapp/revanced/patcher/PatcherOptions;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public final fun recreateResourceCacheDirectory ()Ljava/io/File;
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/PatcherResult { public final class app/revanced/patcher/PatcherResult {
public fun <init> (Ljava/util/List;Ljava/io/File;Ljava/util/List;)V
public synthetic fun <init> (Ljava/util/List;Ljava/io/File;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/List;
public final fun component2 ()Ljava/io/File;
public final fun component3 ()Ljava/util/List;
public final fun copy (Ljava/util/List;Ljava/io/File;Ljava/util/List;)Lapp/revanced/patcher/PatcherResult;
public static synthetic fun copy$default (Lapp/revanced/patcher/PatcherResult;Ljava/util/List;Ljava/io/File;Ljava/util/List;ILjava/lang/Object;)Lapp/revanced/patcher/PatcherResult;
public fun equals (Ljava/lang/Object;)Z
public final fun getDexFiles ()Ljava/util/List;
public final fun getDexFiles ()Ljava/util/Set; public final fun getDexFiles ()Ljava/util/Set;
public final fun getDoNotCompress ()Ljava/util/List;
public final fun getResourceFile ()Ljava/io/File;
public final fun getResources ()Lapp/revanced/patcher/PatcherResult$PatchedResources; public final fun getResources ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/PatcherResult$PatchedDexFile { public final class app/revanced/patcher/PatcherResult$PatchedDexFile {
public fun <init> (Ljava/lang/String;Ljava/io/InputStream;)V
public final fun getName ()Ljava/lang/String; public final fun getName ()Ljava/lang/String;
public final fun getStream ()Ljava/io/InputStream; public final fun getStream ()Ljava/io/InputStream;
} }
@ -118,57 +88,8 @@ public final class app/revanced/patcher/PatcherResult$PatchedResources {
public final fun getResourcesApk ()Ljava/io/File; public final fun getResourcesApk ()Ljava/io/File;
} }
public abstract interface class app/revanced/patcher/PatchesConsumer {
public abstract fun acceptPatches (Ljava/util/List;)V
public abstract fun acceptPatches (Ljava/util/Set;)V
}
public final class app/revanced/patcher/PatchesConsumer$DefaultImpls {
public static fun acceptPatches (Lapp/revanced/patcher/PatchesConsumer;Ljava/util/List;)V
}
public final class app/revanced/patcher/data/BytecodeContext : app/revanced/patcher/data/Context {
public final fun findClass (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun findClass (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
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 proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun toMethodWalker (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/method/MethodWalker;
}
public abstract interface class app/revanced/patcher/data/Context : java/util/function/Supplier {
}
public final class app/revanced/patcher/data/ResourceContext : app/revanced/patcher/data/Context, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
public fun get ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
public synthetic fun get ()Ljava/lang/Object;
public final fun get (Ljava/lang/String;)Ljava/io/File;
public final fun get (Ljava/lang/String;Z)Ljava/io/File;
public static synthetic fun get$default (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File;
public final fun getDocument ()Lapp/revanced/patcher/data/ResourceContext$DocumentOperatable;
public final fun getXmlEditor ()Lapp/revanced/patcher/data/ResourceContext$XmlFileHolder;
public fun iterator ()Ljava/util/Iterator;
public final fun stageDelete (Lkotlin/jvm/functions/Function1;)Z
}
public final class app/revanced/patcher/data/ResourceContext$DocumentOperatable {
public fun <init> (Lapp/revanced/patcher/data/ResourceContext;)V
public final fun get (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document;
public final fun get (Ljava/lang/String;)Lapp/revanced/patcher/util/Document;
}
public final class app/revanced/patcher/data/ResourceContext$XmlFileHolder {
public fun <init> (Lapp/revanced/patcher/data/ResourceContext;)V
public final fun get (Ljava/io/InputStream;)Lapp/revanced/patcher/util/DomFileEditor;
public final fun get (Ljava/lang/String;)Lapp/revanced/patcher/util/DomFileEditor;
}
public final class app/revanced/patcher/extensions/ExtensionsKt { 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; public static final fun newLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lcom/android/tools/smali/dexlib2/builder/Label;
public static final fun or (ILcom/android/tools/smali/dexlib2/AccessFlags;)I
public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;I)I
public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;Lcom/android/tools/smali/dexlib2/AccessFlags;)I
} }
public final class app/revanced/patcher/extensions/InstructionExtensions { public final class app/revanced/patcher/extensions/InstructionExtensions {
@ -188,7 +109,18 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
public final fun getInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/Object; public final fun getInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/Object;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction; public final fun getInstruction (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)Ljava/lang/Object; public final fun getInstruction (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;I)Ljava/lang/Object;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;I)Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;I)Ljava/lang/Object;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/MethodImplementation;I)Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
public final fun getInstruction (Lcom/android/tools/smali/dexlib2/iface/MethodImplementation;I)Ljava/lang/Object;
public final fun getInstructionOrNull (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lcom/android/tools/smali/dexlib2/builder/BuilderInstruction;
public final fun getInstructionOrNull (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/Object;
public final fun getInstructionOrNull (Lcom/android/tools/smali/dexlib2/iface/Method;I)Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;
public final fun getInstructionOrNull (Lcom/android/tools/smali/dexlib2/iface/Method;I)Ljava/lang/Object;
public final fun getInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/util/List; public final fun getInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/util/List;
public final fun getInstructions (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Iterable;
public final fun getInstructionsOrNull (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/util/List;
public final fun getInstructionsOrNull (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Iterable;
public final fun removeInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V public final fun removeInstruction (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public final fun removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V public final fun removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
public final fun removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;II)V public final fun removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;II)V
@ -201,101 +133,176 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
public final fun replaceInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;ILjava/util/List;)V public final fun replaceInstructions (Lcom/android/tools/smali/dexlib2/builder/MutableMethodImplementation;ILjava/util/List;)V
} }
public final class app/revanced/patcher/extensions/MethodFingerprintExtensions { public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public static final field INSTANCE Lapp/revanced/patcher/extensions/MethodFingerprintExtensions; public final fun getExtension ()Ljava/io/InputStream;
public final fun getFuzzyPatternScanMethod (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod; public final fun getFingerprints ()Ljava/util/Set;
public fun toString ()Ljava/lang/String;
} }
public abstract class app/revanced/patcher/fingerprint/MethodFingerprint { public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public static final field Companion Lapp/revanced/patcher/fingerprint/MethodFingerprint$Companion; public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
public fun <init> ()V public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)V public final fun getExtension ()Ljava/io/InputStream;
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public final fun getFuzzyPatternScanMethod ()Lapp/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod; public final fun setExtension (Ljava/io/InputStream;)V
public final fun getResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;
public final fun resolve (Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun resolve (Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprint$Companion { public final class app/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint {
public final fun resolve (Ljava/lang/Iterable;Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/Iterable;)V public fun <init> (Lapp/revanced/patcher/Fingerprint;)V
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprintResult { public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext {
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult;Lapp/revanced/patcher/data/BytecodeContext;)V public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef; public final fun classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method; public synthetic fun get ()Ljava/lang/Object;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; public fun get ()Ljava/util/Set;
public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
public final fun getScanResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult; public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult { public final class app/revanced/patcher/patch/Option {
public fun <init> (Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult;Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult;)V public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public final fun getPatternScanResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult; public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getStringsScanResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult; public final fun getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()Ljava/lang/String;
public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Lkotlin/reflect/KType;
public final fun getValidator ()Lkotlin/jvm/functions/Function2;
public final fun getValue ()Ljava/lang/Object;
public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public final fun getValues ()Ljava/util/Map;
public final fun reset ()V
public final fun setValue (Ljava/lang/Object;)V
public final fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult { public abstract class app/revanced/patcher/patch/OptionException : java/lang/Exception {
public fun <init> (IILjava/util/List;)V public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (IILjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getEndIndex ()I
public final fun getStartIndex ()I
public final fun getWarnings ()Ljava/util/List;
public final fun setWarnings (Ljava/util/List;)V
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult$Warning { public final class app/revanced/patcher/patch/OptionException$InvalidValueTypeException : app/revanced/patcher/patch/OptionException {
public fun <init> (Lcom/android/tools/smali/dexlib2/Opcode;Lcom/android/tools/smali/dexlib2/Opcode;II)V public fun <init> (Ljava/lang/String;Ljava/lang/String;)V
public final fun getCorrectOpcode ()Lcom/android/tools/smali/dexlib2/Opcode;
public final fun getInstructionIndex ()I
public final fun getPatternIndex ()I
public final fun getWrongOpcode ()Lcom/android/tools/smali/dexlib2/Opcode;
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult { public final class app/revanced/patcher/patch/OptionException$OptionNotFoundException : app/revanced/patcher/patch/OptionException {
public fun <init> (Ljava/util/List;)V public fun <init> (Ljava/lang/String;)V
public final fun getMatches ()Ljava/util/List;
} }
public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult$StringMatch { public final class app/revanced/patcher/patch/OptionException$ValueRequiredException : app/revanced/patcher/patch/OptionException {
public fun <init> (Ljava/lang/String;I)V public fun <init> (Lapp/revanced/patcher/patch/Option;)V
public final fun getIndex ()I
public final fun getString ()Ljava/lang/String;
} }
public abstract interface annotation class app/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod : java/lang/annotation/Annotation { public final class app/revanced/patcher/patch/OptionException$ValueValidationException : app/revanced/patcher/patch/OptionException {
public abstract fun threshold ()I public fun <init> (Ljava/lang/Object;Lapp/revanced/patcher/patch/Option;)V
} }
public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { public final class app/revanced/patcher/patch/OptionKt {
public fun <init> ()V public static final fun booleanOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLjava/util/Set;)V public static synthetic fun booleanOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLjava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun booleansOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public fun <init> (Ljava/util/Set;)V public static synthetic fun booleansOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public synthetic fun <init> (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public static final fun floatOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun floatsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun intOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun intsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun longOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun longsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun stringOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun stringsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
}
public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker {
public fun clear ()V
public synthetic fun compute (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun compute (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Option;
public synthetic fun computeIfAbsent (Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;
public fun computeIfAbsent (Ljava/lang/String;Ljava/util/function/Function;)Lapp/revanced/patcher/patch/Option;
public synthetic fun computeIfPresent (Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun computeIfPresent (Ljava/lang/String;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Option;
public final fun containsKey (Ljava/lang/Object;)Z
public fun containsKey (Ljava/lang/String;)Z
public fun containsValue (Lapp/revanced/patcher/patch/Option;)Z
public final fun containsValue (Ljava/lang/Object;)Z
public final fun entrySet ()Ljava/util/Set;
public final fun get (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object;
public fun get (Ljava/lang/String;)Lapp/revanced/patcher/patch/Option;
public fun getEntries ()Ljava/util/Set;
public fun getKeys ()Ljava/util/Set;
public fun getSize ()I
public fun getValues ()Ljava/util/Collection;
public fun isEmpty ()Z
public final fun keySet ()Ljava/util/Set;
public synthetic fun merge (Ljava/lang/Object;Ljava/lang/Object;Ljava/util/function/BiFunction;)Ljava/lang/Object;
public fun merge (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Ljava/util/function/BiFunction;)Lapp/revanced/patcher/patch/Option;
public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public fun putAll (Ljava/util/Map;)V
public synthetic fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun putIfAbsent (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/Object;Ljava/lang/Object;)Z
public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public synthetic fun replace (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Z
public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public fun replace (Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Lapp/revanced/patcher/patch/Option;)Z
public fun replaceAll (Ljava/util/function/BiFunction;)V
public final fun set (Ljava/lang/String;Ljava/lang/Object;)V
public final fun size ()I
public final fun values ()Ljava/util/Collection;
} }
public abstract class app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
public abstract fun execute (Lapp/revanced/patcher/data/Context;)V public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun getCompatiblePackages ()Ljava/util/Set; public final fun getCompatiblePackages ()Ljava/util/Set;
public final fun getDependencies ()Ljava/util/Set; public final fun getDependencies ()Ljava/util/Set;
public final fun getDescription ()Ljava/lang/String; public final fun getDescription ()Ljava/lang/String;
public final fun getName ()Ljava/lang/String; public final fun getName ()Ljava/lang/String;
public final fun getOptions ()Lapp/revanced/patcher/patch/options/PatchOptions; public final fun getOptions ()Lapp/revanced/patcher/patch/Options;
public final fun getRequiresIntegrations ()Z
public final fun getUse ()Z public final fun getUse ()Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String; public fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/patch/Patch$CompatiblePackage { public abstract class app/revanced/patcher/patch/PatchBuilder {
public fun <init> (Ljava/lang/String;Ljava/util/Set;)V public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun compatibleWith ([Ljava/lang/String;)V
public final fun getName ()Ljava/lang/String; public final fun compatibleWith ([Lkotlin/Pair;)V
public final fun getVersions ()Ljava/util/Set; public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
public final fun execute (Lkotlin/jvm/functions/Function2;)V
public final fun finalize (Lkotlin/jvm/functions/Function2;)V
protected final fun getCompatiblePackages ()Ljava/util/Set;
protected final fun getDependencies ()Ljava/util/Set;
protected final fun getDescription ()Ljava/lang/String;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set;
protected final fun getUse ()Z
public final fun invoke (Lapp/revanced/patcher/patch/Option;)Lapp/revanced/patcher/patch/Option;
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
protected final fun setCompatiblePackages (Ljava/util/Set;)V
protected final fun setDependencies (Ljava/util/Set;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V
}
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
} }
public final class app/revanced/patcher/patch/PatchException : java/lang/Exception { public final class app/revanced/patcher/patch/PatchException : java/lang/Exception {
@ -304,128 +311,83 @@ public final class app/revanced/patcher/patch/PatchException : java/lang/Excepti
public fun <init> (Ljava/lang/Throwable;)V public fun <init> (Ljava/lang/Throwable;)V
} }
public final class app/revanced/patcher/patch/PatchKt {
public static final fun bytecodePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun bytecodePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
public static final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;)Lapp/revanced/patcher/patch/PatchLoader$Dex;
public static synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/patcher/patch/PatchLoader$Dex;
public static final fun loadPatchesFromJar (Ljava/util/Set;)Lapp/revanced/patcher/patch/PatchLoader$Jar;
public static final fun rawResourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static synthetic fun rawResourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/RawResourcePatch;
public static final fun resourcePatch (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/ResourcePatch;
public static synthetic fun resourcePatch$default (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
}
public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public synthetic fun <init> (Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun add (Lapp/revanced/patcher/patch/Patch;)Z
public synthetic fun add (Ljava/lang/Object;)Z
public fun addAll (Ljava/util/Collection;)Z
public fun clear ()V
public fun contains (Lapp/revanced/patcher/patch/Patch;)Z
public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)Z
public final fun getByPatchesFile ()Ljava/util/Map;
public fun getSize ()I
public fun isEmpty ()Z
public fun iterator ()Ljava/util/Iterator;
public fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)Z
public fun retainAll (Ljava/util/Collection;)Z
public final fun size ()I
public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
}
public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader {
public fun <init> (Ljava/util/Set;Ljava/io/File;)V
public synthetic fun <init> (Ljava/util/Set;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader {
public fun <init> (Ljava/util/Set;)V
}
public final class app/revanced/patcher/patch/PatchResult { public final class app/revanced/patcher/patch/PatchResult {
public final fun getException ()Lapp/revanced/patcher/patch/PatchException; public final fun getException ()Lapp/revanced/patcher/patch/PatchException;
public final fun getPatch ()Lapp/revanced/patcher/patch/Patch; public final fun getPatch ()Lapp/revanced/patcher/patch/Patch;
} }
public abstract class app/revanced/patcher/patch/RawResourcePatch : app/revanced/patcher/patch/Patch { public final class app/revanced/patcher/patch/RawResourcePatch : app/revanced/patcher/patch/Patch {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
public fun <init> ()V
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract interface annotation class app/revanced/patcher/patch/annotation/CompatiblePackage : java/lang/annotation/Annotation {
public abstract fun name ()Ljava/lang/String;
public abstract fun versions ()[Ljava/lang/String;
}
public abstract interface annotation class app/revanced/patcher/patch/annotation/Patch : java/lang/annotation/Annotation {
public abstract fun compatiblePackages ()[Lapp/revanced/patcher/patch/annotation/CompatiblePackage;
public abstract fun dependencies ()[Ljava/lang/Class;
public abstract fun description ()Ljava/lang/String;
public abstract fun name ()Ljava/lang/String;
public abstract fun requiresIntegrations ()Z
public abstract fun use ()Z
}
public class app/revanced/patcher/patch/options/PatchOption {
public static final field PatchExtensions Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Lkotlin/jvm/functions/Function2;)V
public final fun getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()Ljava/lang/String;
public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String;
public final fun getValidator ()Lkotlin/jvm/functions/Function2;
public final fun getValue ()Ljava/lang/Object;
public final fun getValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;)Ljava/lang/Object;
public final fun getValueType ()Ljava/lang/String;
public final fun getValues ()Ljava/util/Map;
public fun reset ()V
public final fun setValue (Ljava/lang/Object;)V
public final fun setValue (Ljava/lang/Object;Lkotlin/reflect/KProperty;Ljava/lang/Object;)V
public fun toString ()Ljava/lang/String; public fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/patch/options/PatchOption$PatchExtensions { public final class app/revanced/patcher/patch/RawResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public final fun booleanArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption; public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
public static synthetic fun booleanArrayPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun booleanPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun booleanPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun floatArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun floatArrayPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun floatPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun floatPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun intArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun intArrayPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun intPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun intPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun longArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun longArrayPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun longPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun longPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun registerNewPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Lkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun registerNewPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun stringArrayPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun stringArrayPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;[Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun stringPatchOption (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/options/PatchOption;
public static synthetic fun stringPatchOption$default (Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions;Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
} }
public abstract class app/revanced/patcher/patch/options/PatchOptionException : java/lang/Exception { public final class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/patch/options/PatchOptionException$InvalidValueTypeException : app/revanced/patcher/patch/options/PatchOptionException { public final class app/revanced/patcher/patch/ResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public fun <init> (Ljava/lang/String;Ljava/lang/String;)V public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
} }
public final class app/revanced/patcher/patch/options/PatchOptionException$PatchOptionNotFoundException : app/revanced/patcher/patch/options/PatchOptionException { public final class app/revanced/patcher/patch/ResourcePatchContext : app/revanced/patcher/patch/PatchContext {
public fun <init> (Ljava/lang/String;)V public fun get ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
public synthetic fun get ()Ljava/lang/Object;
public final fun get (Ljava/lang/String;Z)Ljava/io/File;
public static synthetic fun get$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;ZILjava/lang/Object;)Ljava/io/File;
public final fun getDocument ()Lapp/revanced/patcher/patch/ResourcePatchContext$DocumentOperatable;
public final fun stageDelete (Lkotlin/jvm/functions/Function1;)Z
} }
public final class app/revanced/patcher/patch/options/PatchOptionException$ValueRequiredException : app/revanced/patcher/patch/options/PatchOptionException { public final class app/revanced/patcher/patch/ResourcePatchContext$DocumentOperatable {
public fun <init> (Lapp/revanced/patcher/patch/options/PatchOption;)V public fun <init> (Lapp/revanced/patcher/patch/ResourcePatchContext;)V
} public final fun get (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document;
public final fun get (Ljava/lang/String;)Lapp/revanced/patcher/util/Document;
public final class app/revanced/patcher/patch/options/PatchOptionException$ValueValidationException : app/revanced/patcher/patch/options/PatchOptionException {
public fun <init> (Ljava/lang/Object;Lapp/revanced/patcher/patch/options/PatchOption;)V
}
public final class app/revanced/patcher/patch/options/PatchOptions : java/util/Map, kotlin/jvm/internal/markers/KMutableMap {
public fun <init> ()V
public fun clear ()V
public final fun containsKey (Ljava/lang/Object;)Z
public fun containsKey (Ljava/lang/String;)Z
public fun containsValue (Lapp/revanced/patcher/patch/options/PatchOption;)Z
public final fun containsValue (Ljava/lang/Object;)Z
public final fun entrySet ()Ljava/util/Set;
public final fun get (Ljava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object;
public fun get (Ljava/lang/String;)Lapp/revanced/patcher/patch/options/PatchOption;
public fun getEntries ()Ljava/util/Set;
public fun getKeys ()Ljava/util/Set;
public fun getSize ()I
public fun getValues ()Ljava/util/Collection;
public fun isEmpty ()Z
public final fun keySet ()Ljava/util/Set;
public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
public fun put (Ljava/lang/String;Lapp/revanced/patcher/patch/options/PatchOption;)Lapp/revanced/patcher/patch/options/PatchOption;
public fun putAll (Ljava/util/Map;)V
public final fun register (Lapp/revanced/patcher/patch/options/PatchOption;)V
public final fun remove (Ljava/lang/Object;)Lapp/revanced/patcher/patch/options/PatchOption;
public final synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object;
public fun remove (Ljava/lang/String;)Lapp/revanced/patcher/patch/options/PatchOption;
public final fun set (Ljava/lang/String;Ljava/lang/Object;)V
public final fun size ()I
public final fun values ()Ljava/util/Collection;
} }
public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document { public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document {
@ -500,39 +462,51 @@ public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w
public fun setXmlVersion (Ljava/lang/String;)V public fun setXmlVersion (Ljava/lang/String;)V
} }
public final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable { public final class app/revanced/patcher/util/MethodNavigator {
public fun <init> (Ljava/io/File;)V public final fun at (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator;
public fun close ()V public final fun at ([I)Lapp/revanced/patcher/util/MethodNavigator;
public final fun getFile ()Lorg/w3c/dom/Document; public static synthetic fun at$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun immutable ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun mutable ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
} }
public final class app/revanced/patcher/util/ProxyClassList : java/util/Set, kotlin/jvm/internal/markers/KMutableSet { public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList {
public final fun add (Lapp/revanced/patcher/util/proxy/ClassProxy;)Z 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 fun add (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public synthetic fun add (Ljava/lang/Object;)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 addAll (Ljava/util/Collection;)Z
public fun clear ()V public fun clear ()V
public fun contains (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z public fun contains (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun contains (Ljava/lang/Object;)Z public final fun contains (Ljava/lang/Object;)Z
public fun containsAll (Ljava/util/Collection;)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 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 isEmpty ()Z
public fun iterator ()Ljava/util/Iterator; 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 fun remove (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun remove (Ljava/lang/Object;)Z public final fun remove (Ljava/lang/Object;)Z
public fun removeAll (Ljava/util/Collection;)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 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 final fun size ()I
public fun subList (II)Ljava/util/List;
public fun toArray ()[Ljava/lang/Object; public fun toArray ()[Ljava/lang/Object;
public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object;
} }
public final class app/revanced/patcher/util/method/MethodWalker {
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun nextMethod (IZ)Lapp/revanced/patcher/util/method/MethodWalker;
public static synthetic fun nextMethod$default (Lapp/revanced/patcher/util/method/MethodWalker;IZILjava/lang/Object;)Lapp/revanced/patcher/util/method/MethodWalker;
}
public final class app/revanced/patcher/util/proxy/ClassProxy { public final class app/revanced/patcher/util/proxy/ClassProxy {
public final fun getImmutableClass ()Lcom/android/tools/smali/dexlib2/iface/ClassDef; public final fun getImmutableClass ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;

View file

@ -24,7 +24,6 @@ tasks {
repositories { repositories {
mavenCentral() mavenCentral()
mavenLocal()
google() google()
maven { maven {
// A repository must be specified for some reason. "registry" is a dummy. // A repository must be specified for some reason. "registry" is a dummy.
@ -49,8 +48,8 @@ dependencies {
// Exclude, otherwise the org.w3c.dom API breaks. // Exclude, otherwise the org.w3c.dom API breaks.
exclude(group = "xerces", module = "xmlParserAPIs") exclude(group = "xerces", module = "xmlParserAPIs")
} }
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
testImplementation(libs.mockk)
} }
kotlin { kotlin {

View file

@ -60,40 +60,43 @@
# 💉 Introduction to ReVanced Patcher # 💉 Introduction to ReVanced Patcher
In order to create patches for Android applications, you first need to understand the fundamentals of ReVanced Patcher. To create patches for Android apps, it is recommended to know the basic concept of ReVanced Patcher.
## 📙 How it works ## 📙 How it works
ReVanced Patcher is a library that allows you to modify Android applications by applying patches to their APKs. It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool) for resource decoding and encoding. ReVanced Patcher is a library that allows modifying Android apps by applying patches.
ReVanced Patcher accepts a list of patches and integrations, and applies them to a given APK file. It then returns the modified components of the APK file, such as modified dex files and resources, that can be repackaged into a new APK file. It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool)
for resource decoding and encoding.
ReVanced Patcher has a simple API that allows you to load patches and integrations from JAR files and apply them to an APK file. ReVanced Patcher receives a list of patches and applies them to a given APK file.
Later on, you will learn how to create patches. It then returns the modified components of the APK file, such as modified dex files and resources,
that can be repackaged into a new APK file.
ReVanced Patcher has a simple API that allows you to load patches from RVP (JAR or DEX container) files
and apply them to an APK file. Later on, you will learn how to create patches.
```kt ```kt
// Executed patches do not necessarily reset their state. val patches = loadPatchesFromJar(setOf(File("revanced-patches.rvp")))
// For that reason it is important to create a new instance of the PatchBundleLoader
// once the patches are executed instead of reusing the same instance of patches loaded by PatchBundleLoader.
val patches: PatchSet /* = Set<Patch<*>> */ = PatchBundleLoader.Jar(File("revanced-patches.jar"))
val integrations = setOf(File("integrations.apk"))
// Instantiating the patcher will decode the manifest of the APK file to read the package and version name. val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { patcher ->
val patcherConfig = PatcherConfig(apkFile = File("some.apk")) // Here you can access metadata about the APK file through patcher.context.packageMetadata
val patcherResult = Patcher(patcherConfig).use { patcher -> // such as package name, version code, version name, etc.
patcher.apply {
acceptIntegrations(integrations)
acceptPatches(patches)
// Execute patches. // Add patches.
runBlocking { patcher += patches
patcher.apply(returnOnError = false).collect { patchResult ->
if (patchResult.exception != null) // Execute the patches.
println("${patchResult.patchName} failed:\n${patchResult.exception}") runBlocking {
else patcher().collect { patchResult ->
println("${patchResult.patchName} succeeded") if (patchResult.exception != null)
} logger.info("\"${patchResult.patch}\" failed:\n${patchResult.exception}")
else
logger.info("\"${patchResult.patch}\" succeeded")
} }
}.get() }
// Compile and save the patched APK file components.
patcher.get()
} }
// The result of the patcher contains the modified components of the APK file that can be repackaged into a new APK file. // The result of the patcher contains the modified components of the APK file that can be repackaged into a new APK file.

View file

@ -98,7 +98,8 @@ Throughout the documentation, [ReVanced Patches](https://github.com/revanced/rev
3. Open the project in your IDE 3. Open the project in your IDE
> [!TIP] > [!TIP]
> It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation). > It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches
> by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
## ⏭️ What's next ## ⏭️ What's next

View file

@ -60,9 +60,10 @@
# 🔎 Fingerprinting # 🔎 Fingerprinting
In the context of ReVanced, fingerprinting is primarily used to resolve methods with a limited amount of known information. In the context of ReVanced, fingerprinting is primarily 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. 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. 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.
## ⛳️ Example fingerprint ## ⛳️ Example fingerprint
@ -72,14 +73,14 @@ Throughout the documentation, the following example will be used to demonstrate
package app.revanced.patches.ads.fingerprints package app.revanced.patches.ads.fingerprints
object ShowAdsFingerprint : MethodFingerprint( fingerprint {
returnType = "Z", accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, returns("Z")
parameters = listOf("Z"), parameters("Z")
opcodes = listOf(Opcode.RETURN), opcodes(Opcode.RETURN)
strings = listOf("pro"), strings("pro")
customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;" } custom { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;" }
) }
``` ```
## 🔎 Reconstructing the original code from a fingerprint ## 🔎 Reconstructing the original code from a fingerprint
@ -91,22 +92,22 @@ The fingerprint contains the following information:
- Method signature: - Method signature:
```kt ```kt
returnType = "Z", accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
access = AccessFlags.PUBLIC or AccessFlags.FINAL, returns("Z")
parameters = listOf("Z"), parameters("Z")
``` ```
- Method implementation: - Method implementation:
```kt ```kt
opcodes = listOf(Opcode.RETURN) opcodes(Opcode.RETURN)
strings = listOf("pro"), strings("pro")
``` ```
- Package and class name: - Package and class name:
```kt ```kt
customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;"} custom = { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;"}
``` ```
With this information, the original code can be reconstructed: With this information, the original code can be reconstructed:
@ -129,56 +130,78 @@ With this information, the original code can be reconstructed:
> [!TIP] > [!TIP]
> A fingerprint should contain information about a method likely to remain the same across updates. > 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 is likely to 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 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.
## 🔨 How to use fingerprints ## 🔨 How to use fingerprints
After creating a fingerprint, add it to the constructor of a `BytecodePatch`: Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually.
Fingerprints added to a patch are matched by ReVanced Patcher before the patch is executed.
```kt ```kt
object DisableAdsPatch : BytecodePatch( val fingerprint = fingerprint {
setOf(ShowAdsFingerprint)
) {
// ... // ...
} }
val patch = bytecodePatch {
// Directly create and add a fingerprint.
fingerprint {
// ...
}
// Add a fingerprint manually by invoking it.
fingerprint()
}
``` ```
> [!NOTE] > [!TIP]
> Fingerprints passed to the constructor of `BytecodePatch` are resolved by ReVanced Patcher before the patch is executed. > Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again.
> [!TIP] > [!TIP]
> Multiple patches can share fingerprints. If a fingerprint is resolved once, it will not be resolved again. > 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,
> )
>}
> ```
> [!TIP] Once the fingerprint is matched, the match can be used in the patch:
> If a fingerprint has an opcode pattern, you can use the `FuzzyPatternScanMethod` annotation to fuzzy match the pattern.
> Opcode pattern arrays can contain `null` values to indicate that the opcode at the index is unknown.
> Any opcode will match to a `null` value.
> [!WARNING]
> If the fingerprint can not be resolved because it does not match any method, the result of a fingerprint is `null`.
Once the fingerprint is resolved, the result can be used in the patch:
```kt ```kt
object DisableAdsPatch : BytecodePatch( val patch = bytecodePatch {
setOf(ShowAdsFingerprint) // Add a fingerprint and delegate its match to a variable.
) { val match by showAdsFingerprint()
override fun execute(context: BytecodeContext) { val match2 by fingerprint {
val result = ShowAdsFingerprint.result
?: throw PatchException("ShowAdsFingerprint not found")
// ... // ...
} }
execute {
val method = match.method
val method2 = match2.method
}
} }
``` ```
The result of a fingerprint that resolved successfully contains mutable and immutable references to the method and the class it is defined in. > [!WARNING]
> If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If such a match is delegated
> to a variable, accessing it will raise an exception.
The match of a fingerprint contains mutable and immutable references to the method and the class it matches to.
```kt ```kt
class MethodFingerprintResult( class Match(
val method: Method, val method: Method,
val classDef: ClassDef, val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult, val patternMatch: Match.PatternMatch?,
val stringMatches: List<Match.StringMatch>?,
// ... // ...
) { ) {
val mutableClass by lazy { /* ... */ } val mutableClass by lazy { /* ... */ }
@ -186,87 +209,68 @@ class MethodFingerprintResult(
// ... // ...
} }
class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult?,
) {
class StringsScanResult(val matches: List<StringMatch>) {
class StringMatch(val string: String, val index: Int)
}
class PatternScanResult(
val startIndex: Int,
val endIndex: Int,
// ...
) {
// ...
}
}
``` ```
## 🏹 Manual resolution of fingerprints ## 🏹 Manual matching of fingerprints
Unless a fingerprint is added to the constructor of `BytecodePatch`, the fingerprint will not be resolved automatically by ReVanced Patcher before the patch is executed. Unless a fingerprint is added to a patch, the fingerprint will not be matched automatically by ReVanced Patcher
Instead, the fingerprint can be resolved manually using various overloads of the `resolve` function of a fingerprint. before the patch is executed.
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function.
You can resolve a fingerprint in the following ways: You can match a fingerprint the following ways:
- On a **list of classes**, if the fingerprint can resolve on a known subset of classes - 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 resolve on, you can resolve 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 ```kt
override fun execute(context: BytecodeContext) { execute { context ->
val result = ShowAdsFingerprint.also { it.resolve(context, context.classes) }.result val match = showAdsFingerprint.apply {
?: throw PatchException("ShowAdsFingerprint not found") match(context, context.classes)
}.match ?: throw PatchException("No match found")
// ... }
}
``` ```
- On a **single class**, if the fingerprint can resolve on a single known class - In a **single class**, if the fingerprint can match in a single known class
If you know the fingerprint can resolve to a method in a specific class, you can resolve the fingerprint on the class: If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
```kt ```kt
override fun execute(context: BytecodeContext) { execute { context ->
val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" }
val result = ShowAdsFingerprint.also { it.resolve(context, adsLoaderClass) }.result val match = showAdsFingerprint.apply {
?: throw PatchException("ShowAdsFingerprint not found") match(context, adsLoaderClass)
}.match ?: throw PatchException("No match found")
// ...
} }
``` ```
- On a **single method**, to extract certain information about a method - Match a **single method**, to extract certain information about it
The result 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. 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: A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
```kt ```kt
override fun execute(context: BytecodeContext) { execute { context ->
val adsFingerprintResult = ShowAdsFingerprint.result val proStringsFingerprint = fingerprint {
?: throw PatchException("ShowAdsFingerprint not found") strings("free", "trial")
}
val proStringsFingerprint = object : MethodFingerprint( proStringsFingerprint.apply {
strings = listOf("free", "trial") match(context, adsFingerprintMatch.method)
) {} }.match?.let { match ->
match.stringMatches.forEach { match ->
proStringsFingerprint.also {
it.resolve(context, adsFingerprintResult.method)
}.result?.let { result ->
result.scanResult.stringsScanResult!!.matches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}") println("The index of the string '${match.string}' is ${match.index}")
} }
} ?: throw PatchException("No match found")
} ?: throw PatchException("pro strings fingerprint not found")
} }
``` ```
> [!TIP] > [!TIP]
> To see real-world examples of fingerprints, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository. > To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
## ⏭️ What's next ## ⏭️ What's next

View file

@ -64,145 +64,186 @@ Learn the API to create patches using ReVanced Patcher.
## ⛳️ Example patch ## ⛳️ Example patch
Throughout the documentation, the following example will be used to demonstrate the concepts of patches: The following example patch disables ads in an app.
In the following sections, each part of the patch will be explained in detail.
```kt ```kt
package app.revanced.patches.ads package app.revanced.patches.ads
@Patch( val disableAdsPatch = bytecodePatch(
name = "Disable ads", name = "Disable ads",
description = "Disable ads in the app.", description = "Disable ads in the app.",
dependencies = [DisableAdsResourcePatch::class], ) {
compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] compatibleWith("com.some.app"("1.0.0"))
)
object DisableAdsPatch : BytecodePatch( // Resource patch disables ads by patching resource files.
setOf(ShowAdsFingerprint) dependsOn(disableAdsResourcePatch)
) {
override fun execute(context: BytecodeContext) { // Precompiled DEX file to be merged into the patched app.
ShowAdsFingerprint.result?.let { result -> extendWith("disable-ads.rve")
result.mutableMethod.addInstructions(
0, // Fingerprint to find the method to patch.
""" val showAdsMatch by showAdsFingerprint {
# Return false. // More about fingerprints on the next page of the documentation.
const/4 v0, 0x0 }
return v0
""" // Business logic of the patch to disable ads in the app.
) execute {
} ?: throw PatchException("ShowAdsFingerprint not found") // In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads.
showAdsMatch.mutableMethod.addInstructions(
0,
"""
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
move-result v0
return v0
"""
)
} }
} }
``` ```
## 🔎 Breakdown > [!TIP]
> To see real-world examples of patches,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
The example patch consists of the following parts: ## 🧩 Patch API
### 📝 Patch annotation ### ⚙️ Patch options
Patches can have options to get and set before a patch is executed.
Options are useful for making patches configurable.
After loading the patches using `PatchLoader`, options can be set for a patch.
Multiple types are already inbuilt in ReVanced Patcher and are supported by any application that uses ReVanced Patcher.
To define an option, use available `option` functions:
```kt ```kt
@Patch( val patch = bytecodePatch(name = "Patch") {
name = "Disable ads", // Add an inbuilt option and delegate it to a property.
description = "Disable ads in the app.", val value by stringOption(key = "option")
dependencies = [DisableAdsResourcePatch::class],
compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])]
)
```
The `@Patch` annotation is used to provide metadata about the patch. // Add an option with a custom type and delegate it to a property.
val string by option<String>(key = "string")
Notable annotation parameters are:
execute {
- `name`: The name of the patch. This is used as an identifier for the patch. println(value)
If this parameter is not set, `PatchBundleLoader` will not load the patch. println(string)
Other patches can still use this patch as a dependency }
- `description`: A description of the patch. Can be unset if the name is descriptive enough
- `dependencies`: A set of patches which the patch depends on. The patches in this set will be executed before this patch. If a dependency patch raises an exception, this patch will not be executed; subsquently, other patches that depend on this patch will not be executed.
- `compatiblePackages`: A set of `CompatiblePackage` objects. Each `CompatiblePackage` object contains the package name and a set of compatible version names. This parameter can specify the packages and versions the patch is compatible with. Patches can still execute on incompatible packages, but it is recommended to use this parameter to list known compatible packages
- If unset, it is implied that the patch is compatible with all packages
- If the set of versions is unset, it is implied that the patch is compatible with all versions of the package
- If the set of versions is empty, it is implied that the patch is not compatible with any version of the package. This can be useful, for example, to prevent a patch from executing on specific packages that are known to be incompatible
> [!WARNING]
> Circular dependencies are not allowed. If a patch depends on another patch, the other patch cannot depend on the first patch.
> [!NOTE]
> The `@Patch` annotation is optional. If the patch does not require any metadata, it can be omitted.
> If the patch is only used as a dependency, the metadata, such as the `compatiblePackages` parameter, has no effect, as every dependency patch inherits the compatible packages of the patches that depend on it.
> [!TIP]
> An abstract patch class can be annotated with `@Patch`.
> Patches extending off the abstract patch class will inherit the metadata of the abstract patch class.
> [!TIP]
> Instead of the `@Patch` annotation, the superclass's constructor can be used. This is useful in the example scenario where you want to create an abstract patch class.
>
> Example:
>
> ```kt
> abstract class AbstractDisableAdsPatch(
> fingerprints: Set<Fingerprint>
> ) : BytecodePatch(
> name = "Disable ads",
> description = "Disable ads in the app.",
> fingerprints
> ) {
> // ...
> }
> ```
>
> Remember that this constructor has precedence over the `@Patch` annotation.
### 🏗️ Patch class
```kt
object DisableAdsPatch : BytecodePatch( /* Parameters */ ) {
// ...
} }
``` ```
Each patch class extends off a base class that implements the `Patch` interface. Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch:
The interface requires the `execute` method to be implemented.
Depending on which base class is extended, the patch can modify different parts of the APK as described in [🧩 Introduction to ReVanced Patches](2_introduction_to_patches.md).
> [!TIP]
> A patch is usually a singleton object, meaning only one patch instance exists in the JVM.
> Because dependencies are executed before the patch itself, a patch can rely on the state of the dependency patch.
> This is useful in the example scenario, where the `DisableAdsPatch` depends on the `DisableAdsResourcePatch`.
> The `DisableAdsResourcePatch` can, for example, be used to read the decoded resources of the app and provide the `DisableAdsPatch` with the necessary information to disable ads because the `DisableAdsResourcePatch` is executed before the `DisableAdsPatch` and is a singleton object.
### 🏁 The `execute` function
The `execute` function is declared in the `Patch` interface and needs to be implemented.
The `execute` function receives an instance of a context object that provides access to the APK. The patch can use this context to modify the APK as described in [🧩 Introduction to ReVanced Patches](2_introduction_to_patches.md).
In the current example, the patch adds instructions at the beginning of a method implementation in the Dalvik VM bytecode. The added instructions return `false` to disable ads in the current example:
```kt ```kt
val result = LoadAdsFingerprint.result loadPatchesJar(patches).apply {
?: throw PatchException("LoadAdsFingerprint not found") // Type is checked at runtime.
first { it.name == "Patch" }.options["option"] = "Value"
result.mutableMethod.addInstructions( }
0,
"""
# Return false.
const/4 v0, 0x0
return v0
"""
)
``` ```
The type of an option can be obtained from the `type` property of the option:
```kt
option.type // The KType of the option.
```
### 🧩 Extensions
An extension is a precompiled DEX file that is merged into the patched app before a patch is executed.
While patches are compile-time constructs, extensions are runtime constructs
that extend the patched app with additional classes.
Assume you want to add a complex feature to an app that would need multiple classes and methods:
```java
public class ComplexPatch {
public static void doSomething() {
// ...
}
}
```
After compiling the above code as a DEX file, you can add the DEX file as a resource in the patches file
and use it in a patch:
```kt
val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve")
val match by methodFingerprint()
execute {
match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
}
}
```
ReVanced Patcher merges the classes from the extension into `context.classes` before executing the patch.
When the patch is executed, it can reference the classes and methods from the extension.
> [!NOTE] > [!NOTE]
> This patch uses a fingerprint to find the method and replaces the method's instructions with new instructions. >
> The fingerprint is resolved on the classes present in `BytecodeContext`. > The [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template) repository
> Fingerprints will be explained in more detail on the next page. > is a template project to create patches and extensions.
> [!TIP] > [!TIP]
> The patch can also raise any `Exception` or `Throwable` at any time to indicate that the patch failed to execute. A `PatchException` is recommended to be raised if the patch fails to execute. > To see real-world examples of extensions,
> If any patch depends on this patch, the dependent patch will not be executed, whereas other patches that do not depend on this patch can still be executed. > check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
> ReVanced Patcher will handle any exception raised by a patch.
> [!TIP] ### ♻️ Finalization
> To see real-world examples of patches, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository.
Patches can have a finalization block called after all patches have been executed, in reverse order of patch execution.
The finalization block is called after all patches that depend on the patch have been executed.
This is useful for doing post-processing tasks.
A simple real-world example would be a patch that opens a resource file of the app for writing.
Other patches that depend on this patch can write to the file, and the finalization block can close the file.
```kt
val patch = bytecodePatch(name = "Patch") {
dependsOn(
bytecodePatch(name = "Dependency") {
execute {
print("1")
}
finalize {
print("4")
}
}
)
execute {
print("2")
}
finalize {
print("3")
}
}
```
Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`.
Finalization blocks are called in reverse order of patch execution, which means,
first, the finalization block of `Patch`, then the finalization block of `Dependency` is called.
The output after executing the patch above would be `1234`.
The same order is followed for multiple patches depending on the patch.
## 💡 Additional tips
- When using ´PatchLoader` to load patches, only patches with a name are loaded.
Refer to the inline documentation of `PatchLoader` for detailed information.
- Patches can depend on others. Dependencies are executed first.
The dependent patch will not be executed if a dependency raises an exception while executing.
- A patch can declare compatibility with specific packages and versions,
but patches can still be executed on any package or version.
It is recommended to declare compatibility to present known compatible packages and versions.
- If `compatibleWith` is not used, the patch is treated as compatible with any package
- If a package is specified with no versions, the patch is compatible with any version of the package
- If an empty array of versions is specified, the patch is not compatible with any version of the package.
This is useful for declaring incompatibility with a specific package.
- A patch can raise a `PatchException` at any time of execution to indicate that the patch failed to execute.
## ⏭️ What's next ## ⏭️ What's next

View file

@ -65,61 +65,62 @@ Learn the basic concepts of ReVanced Patcher and how to create patches.
## 📙 Fundamentals ## 📙 Fundamentals
A patch is a piece of code that modifies an Android application. A patch is a piece of code that modifies an Android application.
There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode, the APK resources, or arbitrary files in the APK: There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode,
the APK resources, or arbitrary files in the APK:
- A `BytecodePatch` modifies the Dalvik VM bytecode - A `BytecodePatch` modifies the Dalvik VM bytecode
- A `ResourcePatch` modifies (decoded) resources - A `ResourcePatch` modifies (decoded) resources
- A `RawResourcePatch` modifies arbitrary files - A `RawResourcePatch` modifies arbitrary files
Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way. Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies
before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way.
A patch class can be annotated with `@Patch` to provide metadata about and dependencies of the patch. The `execute` function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed.
Alternatively, a constructor of the superclass can be used. This is useful in the example scenario where you want to create an abstract patch class. The `execute` function receives an instance of a context object that provides access to the APK.
The patch can use this context to modify the APK.
The entry point of a patch is the `execute` function. This function is called by ReVanced Patcher when the patch is executed. The `execute` function receives an instance of the context object that provides access to the APK. The patch can use this context to modify the APK. Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs
to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources.
Each type of context provides different APIs to modify the APK. For example, the `BytecodeContext` provides APIs to modify the Dalvik VM bytecode, while the `ResourceContext` provides APIs to modify resources. The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources
if it is supplied a `ResourcePatch` for execution or if any patch depends on a `ResourcePatch`
and will not decode the resources before executing `RawResourcePatch`.
Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK,
whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case.
Decoding and building resources is a time- and resource-consuming,
so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`.
The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources if it is supplied a `ResourcePatch` for execution or if any kind of patch depends on a `ResourcePatch` and will not decode the resources before executing `RawResourcePatch`. Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK, whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case. Decoding and building resources is a time- and resource-consuming process, so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`. Example of patches:
Example of a `BytecodePatch`:
```kt ```kt
@Surpress("unused") @Surpress("unused")
object MyPatch : BytecodePatch() { val bytecodePatch = bytecodePatch {
override fun execute(context: BytecodeContext) { execute {
// Your patch code here // TODO
} }
} }
```
Example of a `ResourcePatch`:
```kt
@Surpress("unused") @Surpress("unused")
object MyPatch : ResourcePatch() { val rawResourcePatch = rawResourcePatch {
override fun execute(context: ResourceContext) { execute {
// Your patch code here // TODO
} }
} }
```
Example of a `RawResourcePatch`:
```kt
@Surpress("unused") @Surpress("unused")
object MyPatch : RawResourcePatch() { val resourcePatch = rawResourcePatch {
override fun execute(context: ResourceContext) { execute {
// Your patch code here // TODO
} }
} }
``` ```
> [!TIP] > [!TIP]
> To see real-world examples of patches, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository. > To see real-world examples of patches,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
## ⏭️ Whats next ## ⏭️ Whats next
The next page will guide you through setting up a development environment for creating patches. 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_setup.md)

View file

@ -64,31 +64,39 @@ Over time, a specific project structure and conventions have been established.
## 📁 File structure ## 📁 File structure
Patches are organized in a specific file structure. The file structure is as follows: Patches are organized in a specific way. The file structure looks as follows:
```text ```text
📦your.patches.app.category 📦your.patches.app.category
├ 📂fingerprints ├ 🔍Fingerprints.kt
├ ├ 🔍SomeFingerprintA.kt
├ └ 🔍SomeFingerprintB.kt
└ 🧩SomePatch.kt └ 🧩SomePatch.kt
``` ```
> [!NOTE]
> Moving fingerprints to a separate file isn't strictly necessary, but it helps the organization when a patch uses multiple fingerprints.
## 📙 Conventions ## 📙 Conventions
- 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `RemoveAdsPatch`. - 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `Remove ads`.
If a patch changes the color of a button, name it `ChangeButtonColorPatch` If a patch changes the color of a button, name it `Change button color`
- 🔥 Write the patch description in the third person, present tense, and end it with a period. - 🔥 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._ If a patch removes ads, the description can be omitted because of redundancy,
- 🔥 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. but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
- 🔥 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. - 🔥🔥 Keep patches as minimal as possible. This reduces the risk of failing patches.
Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch, Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch,
you can write code in integrations. Integrations are compiled classes that are merged into the app before patches are executed as described in [💉 Introduction to ReVanced Patcher](1_patcher_intro). you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app
Patches can then reference methods and classes from integrations. before this patch is executed.
A real-world example of integrations can be found in the [ReVanced Integrations](https://github.com/ReVanced/revanced-integrations) repository Patches can then reference methods and classes from extensions.
A real-world example of extensions can be found in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository
- 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change. - 🔥🔥🔥 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. In the example of an obfuscated method, it's better to fingerprint the method by its return type
- 🔥🔥🔥 Document your patches. Patches are abstract by nature, 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 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.
- 🔥🔥🔥 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
## ⏭️ What's next ## ⏭️ What's next

View file

@ -4,13 +4,13 @@ A handful of APIs are available to make patch development easier and more effici
## 📙 Overview ## 📙 Overview
1. 👹 Create new mutable classes with `context.proxy(ClassDef)` 1. 👹 Mutate classes with `context.proxy(ClassDef)`
2. 🔍 Find and proxy existing classes with `BytecodeContext.findClass(Predicate)` 2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)`
3. 🏃‍ Easily access referenced methods recursively by index with `BytecodeContext.toMethodWalker(Method)` 3. 🏃‍ Easily access referenced methods recursively by index with `MethodNavigator`
4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications (Available in ReVanced Patches) 4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications
5. 💾 Read and write (decoded) resources with `ResourceContext.get(Path, Boolean) ` (Available in ReVanced Patches)
6. 📃 Read and write DOM files using `ResourceContext.document` 5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)`
7. 🔧 Equip patches with configurable options using `Patch.options` 6. 📃 Read and write DOM files using `ResourcePatchContext.document`
### 🧰 APIs ### 🧰 APIs
@ -19,5 +19,9 @@ A handful of APIs are available to make patch development easier and more effici
## 🎉 Afterword ## 🎉 Afterword
ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches that outlive app updates. Patches make up ReVanced; without you, the community of patch developers, ReVanced would not be what it is today. We hope that this documentation has been helpful to you and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help, talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or feature request, ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches
that outlive app updates. Patches make up ReVanced; without you, the community of patch developers,
ReVanced would not be what it is today. We hope that this documentation has been helpful to you
and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help,
talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or feature request,
ReVanced ReVanced

View file

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

View file

@ -1,7 +1 @@
rootProject.name = "revanced-patcher" rootProject.name = "revanced-patcher"
buildCache {
local {
isEnabled = "CI" !in System.getenv()
}
}

View file

@ -0,0 +1,467 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package app.revanced.patcher
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps.Companion.appendParameters
import app.revanced.patcher.patch.MethodClassPairs
import app.revanced.patcher.util.proxy.ClassProxy
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.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* A fingerprint.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
* @param returnType The return type. Compared using [String.startsWith].
* @param parameters The parameters. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes A pattern of instruction opcodes. `null` can be used as a wildcard.
* @param strings A list of the strings. Compared using [String.contains].
* @param custom A custom condition for this fingerprint.
* @param fuzzyPatternScanThreshold The threshold for fuzzy scanning the [opcodes] pattern.
*/
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val opcodes: List<Opcode?>?,
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int,
) {
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
var match: Match? = null
private set
/**
* Match using [BytecodePatchContext.LookupMaps].
*
* Generally faster than the other [match] 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.
*
* @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/
internal fun match(context: BytecodePatchContext): Boolean {
val lookupMaps = context.lookupMaps
fun Fingerprint.match(methodClasses: MethodClassPairs): Boolean {
methodClasses.forEach { (classDef, method) ->
if (match(context, classDef, method)) return true
}
return false
}
// TODO: If only one string is necessary, why not use a single string for every fingerprint?
fun Fingerprint.lookupByStrings() = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }
if (lookupByStrings()?.let(::match) == true) {
return true
}
// No strings declared or none matched (partial matches are allowed).
// Use signature matching.
fun Fingerprint.lookupBySignature(): MethodClassPairs {
if (accessFlags == null) return lookupMaps.allMethods
var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type.
returnTypeValue = "V"
} else {
return lookupMaps.allMethods
}
}
val signature =
buildString {
append(accessFlags)
append(returnTypeValue.first())
appendParameters(parameters ?: return@buildString)
}
return lookupMaps.methodsBySignature[signature] ?: return MethodClassPairs()
}
return match(lookupBySignature())
}
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/
fun match(
context: BytecodePatchContext,
classDef: ClassDef,
): Boolean {
for (method in classDef.methods) {
if (match(context, method, classDef)) {
return true
}
}
return false
}
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/
fun match(
context: BytecodePatchContext,
method: Method,
) = match(context, method, context.classByType(method.definingClass)!!.immutableClass)
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/
internal fun match(
context: BytecodePatchContext,
method: Method,
classDef: ClassDef,
): Boolean {
if (match != null) return true
if (returnType != null && !method.returnType.startsWith(returnType)) {
return false
}
if (accessFlags != null && accessFlags != method.accessFlags) {
return false
}
fun parametersEqual(
parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): 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)) {
return false
}
if (custom != null && !custom.invoke(method, classDef)) {
return false
}
val stringMatches: List<Match.StringMatch>? =
if (strings != null) {
buildList {
val instructions = method.instructionsOrNull ?: return false
val stringsList = strings.toMutableList()
instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(Match.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return false
}
} else {
null
}
val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return false
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 (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// Reaching maximum threshold (0) means,
// the pattern does not match to the current instructions.
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// If the entire pattern has not been scanned yet, continue the scan.
patternIndex++
continue
}
// The entire pattern has been scanned.
return Match.PatternMatch(
index,
index + patternIndex,
)
}
}
return null
}
patternScan() ?: return false
} else {
null
}
match = Match(
method,
classDef,
patternMatch,
stringMatches,
context,
)
return true
}
}
/**
* A match for a [Fingerprint].
*
* @param method The matching method.
* @param classDef The class the matching method is a member of.
* @param patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings.
* @param context The context to create mutable proxies in.
*/
class Match(
val method: Method,
val classDef: ClassDef,
val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?,
internal val context: BytecodePatchContext,
) {
/**
* The mutable version of [classDef].
*
* Accessing this property allocates a [ClassProxy].
* Use [classDef] if mutable access is not required.
*/
val mutableClass by lazy { context.proxy(classDef).mutableClass }
/**
* The mutable version of [method].
*
* Accessing this property allocates a [ClassProxy].
* Use [method] if mutable access is not required.
*/
val mutableMethod by lazy { mutableClass.methods.first { MethodUtil.methodSignaturesMatch(it, method) } }
/**
* A match for an opcode pattern.
* @param startIndex The index of the first opcode of the pattern in the method.
* @param endIndex The index of the last opcode of the pattern in the method.
*/
class PatternMatch(
val startIndex: Int,
val endIndex: Int,
)
/**
* A match for a string.
*
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
class StringMatch(val string: String, val index: Int)
}
/**
* A builder for [Fingerprint].
*
* @property accessFlags The exact access flags using values of [AccessFlags].
* @property returnType The return type compared using [String.startsWith].
* @property parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @property opcodes An opcode pattern of the instructions. Wildcard or unknown opcodes can be specified by `null`.
* @property strings A list of the strings compared each using [String.contains].
* @property customBlock A custom condition for this fingerprint.
* @property fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning.
*
* @constructor Create a new [FingerprintBuilder].
*/
class FingerprintBuilder internal constructor(
private val fuzzyPatternScanThreshold: Int = 0,
) {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var opcodes: List<Opcode?>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
/**
* Set the access flags.
*
* @param accessFlags The exact access flags using values of [AccessFlags].
*/
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
/**
* Set the return type.
*
* @param returnType The return type compared using [String.startsWith].
*/
infix fun returns(returnType: String) {
this.returnType = returnType
}
/**
* Set the parameters.
*
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
*/
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
/**
* Set the opcodes.
*
* @param opcodes An opcode pattern of instructions.
* Wildcard or unknown opcodes can be specified by `null`.
*/
fun opcodes(vararg opcodes: Opcode?) {
this.opcodes = opcodes.toList()
}
/**
* Set the opcodes.
*
* @param instructions A list of instructions or opcode names in SMALI format.
* - Wildcard or unknown opcodes can be specified by `null`.
* - Empty lines are ignored.
* - Each instruction must be on a new line.
* - The opcode name is enough, no need to specify the operands.
*
* @throws Exception If an unknown opcode is used.
*/
fun opcodes(instructions: String) {
this.opcodes = instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.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")
}
}
/**
* Set the strings.
*
* @param strings A list of strings compared each using [String.contains].
*/
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
/**
* Set a custom condition for this fingerprint.
*
* @param customBlock A custom condition for this fingerprint.
*/
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
internal fun build() = Fingerprint(
accessFlags,
returnType,
parameters,
opcodes,
strings,
customBlock,
fuzzyPatternScanThreshold,
)
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
/**
* 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()
/**
* Create a [Fingerprint] and add it to the set of fingerprints.
*
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
* @param block The block to build the [Fingerprint].
*
* @return The created [Fingerprint].
*/
fun BytecodePatchBuilder.fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,
) = app.revanced.patcher.fingerprint(
fuzzyPatternScanThreshold,
block,
)() // Invoke to add it.

View file

@ -1,11 +0,0 @@
package app.revanced.patcher
import java.io.File
@FunctionalInterface
interface IntegrationsConsumer {
fun acceptIntegrations(integrations: Set<File>)
@Deprecated("Use acceptIntegrations(Set<File>) instead.")
fun acceptIntegrations(integrations: List<File>)
}

View file

@ -1,135 +0,0 @@
@file:Suppress("unused")
package app.revanced.patcher
import app.revanced.patcher.patch.Patch
import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
import java.util.logging.Logger
import kotlin.reflect.KClass
/**
* A set of [Patch]es.
*/
typealias PatchSet = Set<Patch<*>>
/**
* A [Patch] class.
*/
typealias PatchClass = KClass<out Patch<*>>
/**
* A loader of [Patch]es from patch bundles.
* This will load all [Patch]es from the given patch bundles that have a name.
*
* @param getBinaryClassNames A function that returns the binary names of all classes in a patch bundle.
* @param classLoader The [ClassLoader] to use for loading the classes.
* @param patchBundles A set of patches to initialize this instance with.
*/
sealed class PatchBundleLoader private constructor(
classLoader: ClassLoader,
patchBundles: Array<out File>,
getBinaryClassNames: (patchBundle: File) -> List<String>,
// This constructor parameter is unfortunately necessary,
// so that a reference to the mutable set is present in the constructor to be able to add patches to it.
// because the instance itself is a PatchSet, which is immutable, that is delegated by the parameter.
private val patchSet: MutableSet<Patch<*>> = mutableSetOf(),
) : PatchSet by patchSet {
private val logger = Logger.getLogger(PatchBundleLoader::class.java.name)
init {
patchBundles.flatMap(getBinaryClassNames).asSequence().map {
classLoader.loadClass(it)
}.filter {
Patch::class.java.isAssignableFrom(it)
}.mapNotNull { patchClass ->
patchClass.getInstance(logger, silent = true)
}.filter {
it.name != null
}.let { patches ->
patchSet.addAll(patches)
}
}
internal companion object Utils {
/**
* Instantiates a [Patch]. If the class is a singleton, the INSTANCE field will be used.
*
* @param logger The [Logger] to use for logging.
* @param silent Whether to suppress logging.
* @return The instantiated [Patch] or `null` if the [Patch] could not be instantiated.
*/
internal fun Class<*>.getInstance(
logger: Logger,
silent: Boolean = false,
): Patch<*>? {
return try {
getField("INSTANCE").get(null)
} catch (exception: NoSuchFieldException) {
if (!silent) {
logger.fine(
"Patch class '$name' has no INSTANCE field, therefor not a singleton. " +
"Attempting to instantiate it.",
)
}
try {
getDeclaredConstructor().newInstance()
} catch (exception: Exception) {
if (!silent) {
logger.severe(
"Patch class '$name' is not singleton and has no suitable constructor, " +
"therefor cannot be instantiated and is ignored.",
)
}
return null
}
} as Patch<*>
}
}
/**
* A [PatchBundleLoader] for JAR files.
*
* @param patchBundles The path to patch bundles of JAR format.
*/
class Jar(vararg patchBundles: File) : PatchBundleLoader(
URLClassLoader(patchBundles.map { it.toURI().toURL() }.toTypedArray()),
patchBundles,
{ patchBundle ->
JarFile(patchBundle).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
)
/**
* A [PatchBundleLoader] for [Dex] files.
*
* @param patchBundles The path to patch bundles of DEX format.
* @param optimizedDexDirectory The directory to store optimized DEX files in.
* This parameter is deprecated and has no effect since API level 26.
*/
class Dex(vararg patchBundles: File, optimizedDexDirectory: File? = null) : PatchBundleLoader(
DexClassLoader(
patchBundles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
PatchBundleLoader::class.java.classLoader,
),
patchBundles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
) {
@Deprecated("This constructor is deprecated. Use the constructor with the second parameter instead.")
constructor(vararg patchBundles: File) : this(*patchBundles, optimizedDexDirectory = null)
}
}

View file

@ -1,8 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.patch.PatchResult
import kotlinx.coroutines.flow.Flow
import java.util.function.Function
@FunctionalInterface
interface PatchExecutorFunction : Function<Boolean, Flow<PatchResult>>

View file

@ -1,13 +1,8 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.PatchBundleLoader.Utils.getInstance
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.fingerprint.LookupMap
import app.revanced.patcher.patch.* import app.revanced.patcher.patch.*
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import java.io.Closeable import java.io.Closeable
import java.io.File
import java.util.function.Supplier
import java.util.logging.Logger import java.util.logging.Logger
/** /**
@ -15,243 +10,149 @@ import java.util.logging.Logger
* *
* @param config The configuration to use for the patcher. * @param config The configuration to use for the patcher.
*/ */
class Patcher( class Patcher(private val config: PatcherConfig) : Closeable {
private val config: PatcherConfig, private val logger = Logger.getLogger(this::class.java.name)
) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier<PatcherResult>, Closeable {
private val logger = Logger.getLogger(Patcher::class.java.name)
/** /**
* A context for the patcher containing the current state of the patcher. * The context containing the current state of the patcher.
*/ */
val context = PatcherContext(config) val context = PatcherContext(config)
@Suppress("DEPRECATION")
@Deprecated("Use Patcher(PatcherConfig) instead.")
constructor(
patcherOptions: PatcherOptions,
) : this(
PatcherConfig(
patcherOptions.inputFile,
patcherOptions.resourceCachePath,
patcherOptions.aaptBinaryPath,
patcherOptions.frameworkFileDirectory,
patcherOptions.multithreadingDexFileWriter,
),
)
init { init {
context.resourceContext.decodeResources(ResourceContext.ResourceMode.NONE) context.resourceContext.decodeResources(ResourcePatchContext.ResourceMode.NONE)
} }
/** /**
* Add [Patch]es to ReVanced [Patcher]. * Add patches.
* *
* @param patches The [Patch]es to add. * @param patches The patches to add.
*/ */
@Suppress("NAME_SHADOWING") operator fun plusAssign(patches: Set<Patch<*>>) {
override fun acceptPatches(patches: PatchSet) { // Add all patches to the executablePatches set.
/** context.executablePatches += patches
* Add dependencies of a [Patch] recursively to [PatcherContext.allPatches].
* If a [Patch] is already in [PatcherContext.allPatches], it will not be added again.
*/
fun PatchClass.putDependenciesRecursively() {
if (context.allPatches.contains(this)) return
val dependency = this.java.getInstance(logger)!! // Add all patches and their dependencies to the allPatches set.
context.allPatches[this] = dependency
dependency.dependencies?.forEach { it.putDependenciesRecursively() }
}
// Add all patches and their dependencies to the context.
patches.forEach { patch -> patches.forEach { patch ->
context.executablePatches.putIfAbsent(patch::class, patch) ?: run { fun Patch<*>.addRecursively() =
context.allPatches[patch::class] = patch also(context.allPatches::add).dependencies.forEach(Patch<*>::addRecursively)
patch.dependencies?.forEach { it.putDependenciesRecursively() } patch.addRecursively()
}
} }
// TODO: Detect circular dependencies.
/**
* Returns true if at least one patch or its dependencies matches the given predicate.
*
* @param predicate The predicate to match.
*/
fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean = fun Patch<*>.anyRecursively(predicate: (Patch<*>) -> Boolean): Boolean =
predicate(this) || dependencies?.any { dependency -> predicate(this) || dependencies.any { dependency -> dependency.anyRecursively(predicate) }
context.allPatches[dependency]!!.anyRecursively(predicate)
} ?: false
context.allPatches.values.let { patches -> context.allPatches.let { allPatches ->
// Determine the resource mode. // Check, if what kind of resource mode is required.
config.resourceMode = if (allPatches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) {
config.resourceMode = if (patches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) { ResourcePatchContext.ResourceMode.FULL
ResourceContext.ResourceMode.FULL } else if (allPatches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) {
} else if (patches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) { ResourcePatchContext.ResourceMode.RAW_ONLY
ResourceContext.ResourceMode.RAW_ONLY
} else { } else {
ResourceContext.ResourceMode.NONE ResourcePatchContext.ResourceMode.NONE
} }
// Determine, if merging integrations is required.
for (patch in patches)
if (patch.anyRecursively { it.requiresIntegrations }) {
context.bytecodeContext.integrations.merge = true
break
}
} }
} }
/** /**
* Add integrations to the [Patcher]. * Execute added patches.
* *
* @param integrations The integrations to add. Must be a DEX file or container of DEX files. * @return A flow of [PatchResult]s.
*/ */
override fun acceptIntegrations(integrations: Set<File>) { operator fun invoke() = flow {
context.bytecodeContext.integrations.addAll(integrations) fun Patch<*>.execute(
} executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
// If the patch was executed before or failed, return it's the result.
executedPatches[this]?.let { patchResult ->
patchResult.exception ?: return patchResult
@Deprecated( return PatchResult(this, PatchException("The patch '$this' failed previously"))
"Use acceptIntegrations(Set<File>) instead.", }
ReplaceWith("acceptIntegrations(integrations.toSet())"),
)
override fun acceptIntegrations(integrations: List<File>) = acceptIntegrations(integrations.toSet())
/** // Recursively execute all dependency patches.
* Execute [Patch]es that were added to ReVanced [Patcher]. dependencies.forEach { dependency ->
* dependency.execute(executedPatches).exception?.let {
* @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails. return PatchResult(
* @return A pair of the name of the [Patch] and its [PatchResult]. this,
*/ PatchException(
override fun apply(returnOnError: Boolean) = "The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}",
flow { ),
/** )
* Execute a [Patch] and its dependencies recursively.
*
* @param patch The [Patch] to execute.
* @param executedPatches A map to prevent [Patch]es from being executed twice due to dependencies.
* @return The result of executing the [Patch].
*/
fun executePatch(
patch: Patch<*>,
executedPatches: LinkedHashMap<Patch<*>, PatchResult>,
): PatchResult {
val patchName = patch.toString()
executedPatches[patch]?.let { patchResult ->
patchResult.exception ?: return patchResult
// Return a new result with an exception indicating that the patch was not executed previously,
// because it is a dependency of another patch that failed.
return PatchResult(patch, PatchException("'$patchName' did not succeed previously"))
} }
}
// Recursively execute all dependency patches. // Execute the patch.
patch.dependencies?.forEach { dependencyClass -> return try {
val dependency = context.allPatches[dependencyClass]!! execute(context)
val result = executePatch(dependency, executedPatches)
result.exception?.let { PatchResult(this)
return PatchResult( } catch (exception: PatchException) {
patch, PatchResult(this, exception)
PatchException( } catch (exception: Exception) {
"'$patchName' depends on '${dependency.name ?: dependency}' " + PatchResult(this, PatchException(exception))
"that raised an exception:\n${it.stackTraceToString()}", }.also { executedPatches[this] = it }
), }
)
}
}
return try { // Prevent from decoding the app manifest twice if it is not needed.
patch.execute(context) if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(config.resourceMode)
}
PatchResult(patch) logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>()
context.executablePatches.sortedBy { it.name }.forEach { patch ->
val patchResult = patch.execute(executedPatches)
// If an exception occurred or the patch has no finalize block, emit the result.
if (patchResult.exception != null || patch.finalizeBlock == null) {
emit(patchResult)
}
}
val succeededPatchesWithFinalizeBlock = executedPatches.values.filter {
it.exception == null && it.patch.finalizeBlock != null
}
succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult ->
val patch = executionResult.patch
val result =
try {
patch.finalize(context)
executionResult
} catch (exception: PatchException) { } catch (exception: PatchException) {
PatchResult(patch, exception) PatchResult(patch, exception)
} catch (exception: Exception) { } catch (exception: Exception) {
PatchResult(patch, PatchException(exception)) PatchResult(patch, PatchException(exception))
}.also { executedPatches[patch] = it }
}
if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush()
LookupMap.initializeLookupMaps(context.bytecodeContext)
// Prevent from decoding the app manifest twice if it is not needed.
if (config.resourceMode != ResourceContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(config.resourceMode)
}
logger.info("Executing patches")
val executedPatches = LinkedHashMap<Patch<*>, PatchResult>() // Key is name.
context.executablePatches.values.sortedBy { it.name }.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
// If the patch failed, emit the result, even if it is closeable.
// Results of executed patches that are closeable will be emitted later.
patchResult.exception?.let {
// Propagate exception to caller instead of wrapping it in a new exception.
emit(patchResult)
if (returnOnError) return@flow
} ?: run {
if (patch is Closeable) return@run
emit(patchResult)
} }
if (result.exception != null) {
emit(
PatchResult(
patch,
PatchException(
"The patch \"$patch\" raised an exception: ${result.exception.stackTraceToString()}",
result.exception,
),
),
)
} else if (patch in context.executablePatches) {
emit(result)
} }
executedPatches.values
.filter { it.exception == null }
.filter { it.patch is Closeable }.asReversed().forEach { executedPatch ->
val patch = executedPatch.patch
val result =
try {
(patch as Closeable).close()
executedPatch
} catch (exception: PatchException) {
PatchResult(patch, exception)
} catch (exception: Exception) {
PatchResult(patch, PatchException(exception))
}
result.exception?.let {
emit(
PatchResult(
patch,
PatchException(
"'$patch' raised an exception while being closed: ${it.stackTraceToString()}",
result.exception,
),
),
)
if (returnOnError) return@flow
} ?: run {
patch.name ?: return@run
emit(result)
}
}
} }
}
override fun close() = LookupMap.clearLookupMaps() override fun close() = context.bytecodeContext.lookupMaps.close()
/** /**
* Compile and save the patched APK file. * Compile and save patched APK files.
* *
* @return The [PatcherResult] containing the patched input files. * @return The [PatcherResult] containing the patched APK files.
*/ */
@OptIn(InternalApi::class) @OptIn(InternalApi::class)
override fun get() = fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get())
PatcherResult(
context.bytecodeContext.get(),
context.resourceContext.get(),
)
} }

View file

@ -1,6 +1,6 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.ResourcePatchContext
import brut.androlib.Config import brut.androlib.Config
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
@ -27,9 +27,9 @@ class PatcherConfig(
/** /**
* The mode to use for resource decoding and compiling. * The mode to use for resource decoding and compiling.
* *
* @see ResourceContext.ResourceMode * @see ResourcePatchContext.ResourceMode
*/ */
internal var resourceMode = ResourceContext.ResourceMode.NONE internal var resourceMode = ResourcePatchContext.ResourceMode.NONE
/** /**
* The configuration for decoding and compiling resources. * The configuration for decoding and compiling resources.

View file

@ -1,8 +1,8 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatchContext
import brut.androlib.apk.ApkInfo import brut.androlib.apk.ApkInfo
import brut.directory.ExtFile import brut.directory.ExtFile
@ -19,22 +19,22 @@ class PatcherContext internal constructor(config: PatcherConfig) {
val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile))) val packageMetadata = PackageMetadata(ApkInfo(ExtFile(config.apkFile)))
/** /**
* The map of [Patch]es associated by their [PatchClass]. * The set of [Patch]es.
*/ */
internal val executablePatches = mutableMapOf<PatchClass, Patch<*>>() internal val executablePatches = mutableSetOf<Patch<*>>()
/** /**
* The map of all [Patch]es and their dependencies associated by their [PatchClass]. * The set of all [Patch]es and their dependencies.
*/ */
internal val allPatches = mutableMapOf<PatchClass, Patch<*>>() internal val allPatches = mutableSetOf<Patch<*>>()
/** /**
* A context for the patcher containing the current state of the resources. * The context for patches containing the current state of the resources.
*/ */
internal val resourceContext = ResourceContext(packageMetadata, config) internal val resourceContext = ResourcePatchContext(packageMetadata, config)
/** /**
* A context for the patcher containing the current state of the bytecode. * The context for patches containing the current state of the bytecode.
*/ */
internal val bytecodeContext = BytecodeContext(config) internal val bytecodeContext = BytecodePatchContext(config)
} }

View file

@ -1,15 +0,0 @@
package app.revanced.patcher
/**
* An exception thrown by ReVanced [Patcher].
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
sealed class PatcherException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
class CircularDependencyException internal constructor(dependant: String) : PatcherException(
"Patch '$dependant' causes a circular dependency",
)
}

View file

@ -1,25 +0,0 @@
package app.revanced.patcher
import java.io.File
@Deprecated("Use PatcherConfig instead.")
data class PatcherOptions(
internal val inputFile: File,
internal val resourceCachePath: File = File("revanced-resource-cache"),
internal val aaptBinaryPath: String? = null,
internal val frameworkFileDirectory: String? = null,
internal val multithreadingDexFileWriter: Boolean = false,
) {
@Deprecated("This method will be removed in the future.")
fun recreateResourceCacheDirectory(): File {
PatcherConfig(
inputFile,
resourceCachePath,
aaptBinaryPath,
frameworkFileDirectory,
multithreadingDexFileWriter,
).initializeTemporaryFilesDirectories()
return resourceCachePath
}
}

View file

@ -2,7 +2,6 @@ package app.revanced.patcher
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import kotlin.jvm.internal.Intrinsics
/** /**
* The result of a patcher. * The result of a patcher.
@ -15,87 +14,6 @@ class PatcherResult internal constructor(
val dexFiles: Set<PatchedDexFile>, val dexFiles: Set<PatchedDexFile>,
val resources: PatchedResources?, val resources: PatchedResources?,
) { ) {
@Deprecated("This method is not used anymore")
constructor(
dexFiles: List<PatchedDexFile>,
resourceFile: File?,
doNotCompress: List<String>? = null,
) : this(dexFiles.toSet(), PatchedResources(resourceFile, null, doNotCompress?.toSet() ?: emptySet(), emptySet()))
@Deprecated("This method is not used anymore")
fun component1(): List<PatchedDexFile> {
return dexFiles.toList()
}
@Deprecated("This method is not used anymore")
fun component2(): File? {
return resources?.resourcesApk
}
@Deprecated("This method is not used anymore")
fun component3(): List<String>? {
return resources?.doNotCompress?.toList()
}
@Deprecated("This method is not used anymore")
fun copy(
dexFiles: List<PatchedDexFile>,
resourceFile: File?,
doNotCompress: List<String>? = null,
): PatcherResult {
return PatcherResult(
dexFiles.toSet(),
PatchedResources(
resourceFile,
null,
doNotCompress?.toSet() ?: emptySet(),
emptySet(),
),
)
}
@Deprecated("This method is not used anymore")
override fun toString(): String {
return (("PatcherResult(dexFiles=" + this.dexFiles + ", resourceFile=" + this.resources?.resourcesApk) + ", doNotCompress=" + this.resources?.doNotCompress) + ")"
}
@Deprecated("This method is not used anymore")
override fun hashCode(): Int {
val result = dexFiles.hashCode()
return (
(
(result * 31) +
(if (this.resources?.resourcesApk == null) 0 else this.resources.resourcesApk.hashCode())
) * 31
) +
(if (this.resources?.doNotCompress == null) 0 else this.resources.doNotCompress.hashCode())
}
@Deprecated("This method is not used anymore")
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (other is PatcherResult) {
return Intrinsics.areEqual(this.dexFiles, other.dexFiles) && Intrinsics.areEqual(
this.resources?.resourcesApk,
other.resources?.resourcesApk,
) && Intrinsics.areEqual(this.resources?.doNotCompress, other.resources?.doNotCompress)
}
return false
}
@Suppress("DEPRECATION")
@Deprecated("This method is not used anymore")
fun getDexFiles() = component1()
@Suppress("DEPRECATION")
@Deprecated("This method is not used anymore")
fun getResourceFile() = component2()
@Suppress("DEPRECATION")
@Deprecated("This method is not used anymore")
fun getDoNotCompress() = component3()
/** /**
* A dex file. * A dex file.
@ -103,10 +21,7 @@ class PatcherResult internal constructor(
* @param name The original name of the dex file. * @param name The original name of the dex file.
* @param stream The dex file as [InputStream]. * @param stream The dex file as [InputStream].
*/ */
class PatchedDexFile class PatchedDexFile internal constructor(val name: String, val stream: InputStream)
// TODO: Add internal modifier.
@Deprecated("This constructor will be removed in the future.")
constructor(val name: String, val stream: InputStream)
/** /**
* The resources of a patched apk. * The resources of a patched apk.

View file

@ -1,10 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.patch.Patch
@FunctionalInterface
interface PatchesConsumer {
@Deprecated("Use acceptPatches(PatchSet) instead.", ReplaceWith("acceptPatches(patches.toSet())"))
fun acceptPatches(patches: List<Patch<*>>) = acceptPatches(patches.toSet())
fun acceptPatches(patches: PatchSet)
}

View file

@ -1,185 +0,0 @@
package app.revanced.patcher.data
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.method.MethodWalker
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.FileFilter
import java.io.Flushable
import java.util.logging.Logger
/**
* A context for the patcher containing the current state of the bytecode.
*
* @param config The [PatcherConfig] used to create this context.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodeContext internal constructor(private val config: PatcherConfig) :
Context<Set<PatcherResult.PatchedDexFile>> {
private val logger = Logger.getLogger(BytecodeContext::class.java.name)
/**
* [Opcodes] of the supplied [PatcherConfig.apkFile].
*/
internal lateinit var opcodes: Opcodes
/**
* The list of classes.
*/
val classes by lazy {
ProxyClassList(
MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null,
).also { opcodes = it.opcodes }.classes.toMutableSet(),
)
}
/**
* The [Integrations] of this [PatcherContext].
*/
internal val integrations = Integrations()
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) }
?: // else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
/**
* Proxy a class.
* This will allow the class to be modified.
*
* @param classDef The class to proxy.
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) =
this.classes.proxies.find { it.immutableClass.type == classDef.type } ?: let {
ClassProxy(classDef).also { this.classes.add(it) }
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun toMethodWalker(startMethod: Method) = MethodWalker(this, startMethod)
/**
* Compile bytecode from the [BytecodeContext].
*
* @return The compiled bytecode.
*/
@InternalApi
override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
val patchedDexFileResults =
config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
if (config.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = this@BytecodeContext.classes.also(ProxyClassList::replaceClasses)
override fun getOpcodes() = this@BytecodeContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map {
@Suppress("DEPRECATION")
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
System.gc()
return patchedDexFileResults
}
/**
* The integrations of a [PatcherContext].
*/
internal inner class Integrations : MutableList<File> by mutableListOf(), Flushable {
/**
* Whether to merge integrations.
* Set to true, if the field requiresIntegrations of any supplied [Patch] is true.
*/
var merge = false
/**
* Merge integrations into the [BytecodeContext] and flush all [Integrations].
*/
override fun flush() {
if (!merge) return
logger.info("Merging integrations")
val classMap = classes.associateBy { it.type }
this@Integrations.forEach { integrations ->
MultiDexIO.readDexFile(
true,
integrations,
BasicDexFileNamer(),
null,
null,
).classes.forEach classDef@{ classDef ->
val existingClass =
classMap[classDef.type] ?: run {
logger.fine("Adding $classDef")
classes.add(classDef)
return@classDef
}
logger.fine("$classDef exists. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodeContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) return@let
classes.apply {
remove(existingClass)
add(mergedClass)
}
}
}
}
clear()
}
}
}

View file

@ -1,9 +0,0 @@
package app.revanced.patcher.data
import java.util.function.Supplier
/**
* A common interface for contexts such as [ResourceContext] and [BytecodeContext].
*/
sealed interface Context<T> : Supplier<T>

View file

@ -1,61 +0,0 @@
@file:Suppress("UNCHECKED_CAST")
package app.revanced.patcher.extensions
import kotlin.reflect.KClass
internal object AnnotationExtensions {
/**
* Search for an annotation recursively.
*
* @param targetAnnotationClass The annotation class to search for.
* @param searchedClasses A set of annotations that have already been searched.
* @return The annotation if found, otherwise null.
*/
fun <T : Annotation> Class<*>.findAnnotationRecursively(
targetAnnotationClass: Class<T>,
searchedClasses: HashSet<Annotation> = hashSetOf(),
): T? {
annotations.forEach { annotation ->
// Terminate if the annotation is already searched.
if (annotation in searchedClasses) return@forEach
searchedClasses.add(annotation)
// Terminate if the annotation is found.
if (targetAnnotationClass == annotation.annotationClass.java) return annotation as T
return annotation.annotationClass.java.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
) ?: return@forEach
}
// Search the super class.
superclass?.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
)?.let { return it }
// Search the interfaces.
interfaces.forEach { superClass ->
return superClass.findAnnotationRecursively(
targetAnnotationClass,
searchedClasses,
) ?: return@forEach
}
return null
}
/**
* Search for an annotation recursively.
*
* First the annotations, then the annotated classes super class and then it's interfaces
* are searched for the annotation recursively.
*
* @param targetAnnotation The annotation to search for.
* @return The annotation if found, otherwise null.
*/
fun <T : Annotation> KClass<*>.findAnnotationRecursively(targetAnnotation: KClass<T>) =
java.findAnnotationRecursively(targetAnnotation.java)
}

View file

@ -1,7 +1,6 @@
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags
/** /**
* Create a label for the instruction at given index. * Create a label for the instruction at given index.
@ -10,24 +9,3 @@ import com.android.tools.smali.dexlib2.AccessFlags
* @return The label. * @return The label.
*/ */
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index) fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
/**
* Perform a bitwise OR operation between an [AccessFlags] and an [Int].
*
* @param other The [Int] to perform the operation with.
*/
infix fun Int.or(other: AccessFlags) = this or other.value
/**
* Perform a bitwise OR operation between two [AccessFlags].
*
* @param other The other [AccessFlags] to perform the operation with.
*/
infix fun AccessFlags.or(other: AccessFlags) = value or other.value
/**
* Perform a bitwise OR operation between an [Int] and an [AccessFlags].
*
* @param other The [AccessFlags] to perform the operation with.
*/
infix fun AccessFlags.or(other: Int) = value or other

View file

@ -9,6 +9,8 @@ import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.Label import com.android.tools.smali.dexlib2.builder.Label
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.* import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.MethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction
object InstructionExtensions { object InstructionExtensions {
@ -30,7 +32,7 @@ object InstructionExtensions {
* @param instructions The instructions to add. * @param instructions The instructions to add.
*/ */
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) = fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) =
instructions.forEach { this.addInstruction(it) } instructions.forEach { addInstruction(it) }
/** /**
* Remove instructions from a method at the given index. * Remove instructions from a method at the given index.
@ -178,8 +180,8 @@ object InstructionExtensions {
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
/** /**
* Creates a new label for the instruction * Create a new label for the instruction
* and replaces it with the label of the [compiledInstruction] at [compiledInstructionIndex]. * and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex].
*/ */
fun Instruction.makeNewLabel() { fun Instruction.makeNewLabel() {
fun replaceOffset( fun replaceOffset(
@ -310,6 +312,24 @@ object InstructionExtensions {
smaliInstructions: String, smaliInstructions: String,
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this)) ) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MethodImplementation.getInstruction(index: Int) = instructions.elementAt(index)
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/** /**
* Get an instruction at the given index. * Get an instruction at the given index.
* *
@ -328,12 +348,27 @@ object InstructionExtensions {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun Method.getInstructionOrNull(index: Int): Instruction? = implementation?.getInstruction(index)
/** /**
* Get an instruction at the given index. * Get an instruction at the given index.
* @param index The index to get the instruction at. * @param index The index to get the instruction at.
* @return The instruction. * @return The instruction.
*/ */
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = implementation!!.getInstruction(index) fun Method.getInstruction(index: Int): Instruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/** /**
* Get an instruction at the given index. * Get an instruction at the given index.
@ -341,11 +376,59 @@ object InstructionExtensions {
* @param T The type of instruction to return. * @param T The type of instruction to return.
* @return The instruction. * @return The instruction.
*/ */
fun <T> MutableMethod.getInstruction(index: Int): T = implementation!!.getInstruction<T>(index) fun <T> Method.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/** /**
* Get the instructions of a method. * Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun MutableMethod.getInstructionOrNull(index: Int): BuilderInstruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> MutableMethod.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val Method.instructionsOrNull: Iterable<Instruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions. * @return The instructions.
*/ */
fun MutableMethod.getInstructions(): MutableList<BuilderInstruction> = implementation!!.instructions val Method.instructions: Iterable<Instruction> get() = instructionsOrNull!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val MutableMethod.instructionsOrNull: MutableList<BuilderInstruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instructionsOrNull!!
} }

View file

@ -1,17 +0,0 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
object MethodFingerprintExtensions {
/**
* The [FuzzyPatternScanMethod] annotation of a [MethodFingerprint].
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
@Deprecated(
message = "Use the property instead.",
replaceWith = ReplaceWith("this.fuzzyPatternScanMethod"),
)
val MethodFingerprint.fuzzyPatternScanMethod
get() = this.fuzzyPatternScanMethod
}

View file

@ -1,125 +0,0 @@
package app.revanced.patcher.fingerprint
import app.revanced.patcher.data.BytecodeContext
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.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import java.util.*
internal typealias MethodClassPair = Pair<Method, ClassDef>
/**
* Lookup map for methods.
*/
internal class LookupMap : MutableMap<String, LookupMap.MethodClassList> by mutableMapOf() {
/**
* Adds a [MethodClassPair] to the list associated with the given key.
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
*/
fun add(
key: String,
methodClassPair: MethodClassPair,
) {
getOrPut(key) { MethodClassList() }.add(methodClassPair)
}
/**
* List of methods and the class they are a member of.
*/
internal class MethodClassList : LinkedList<MethodClassPair>()
companion object Maps {
/**
* A list of methods and the class they are a member of.
*/
internal val methods = MethodClassList()
/**
* Lookup map for methods keyed to the methods access flags, return type and parameter.
*/
internal val methodSignatureLookupMap = LookupMap()
/**
* Lookup map for methods associated by strings referenced in the method.
*/
internal val methodStringsLookupMap = LookupMap()
/**
* Initializes lookup maps for [MethodFingerprint] resolution
* using attributes of methods such as the method signature or strings.
*
* @param context The [BytecodeContext] containing the classes to initialize the lookup maps with.
*/
internal fun initializeLookupMaps(context: BytecodeContext) {
if (methods.isNotEmpty()) clearLookupMaps()
context.classes.forEach { classDef ->
classDef.methods.forEach { method ->
val methodClassPair = method to classDef
// For fingerprints with no access or return type specified.
methods += methodClassPair
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
// Add <access><returnType> as the key.
methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair)
// Add <access><returnType>[parameters] as the key.
methodSignatureLookupMap.add(
buildString {
append(accessFlagsReturnKey)
appendParameters(method.parameterTypes)
},
methodClassPair,
)
// Add strings contained in the method as the key.
method.implementation?.instructions?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@instructions
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
methodStringsLookupMap.add(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.
}
}
}
/**
* Clears the internal lookup maps created in [initializeLookupMaps].
*/
internal fun clearLookupMaps() {
methods.clear()
methodSignatureLookupMap.clear()
methodStringsLookupMap.clear()
}
/**
* Appends a string based on the parameter reference types of this method.
*/
internal fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
// Maximum parameters to use in the signature key.
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
// To keep the signature map from becoming needlessly bloated,
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
// The value of 5 was chosen based on local performance testing and is not set in stone.
val maxSignatureParameters = 5
// Must append a unique value before the parameters to distinguish this key includes the parameters.
// If this is not appended, then methods with no parameters
// will collide with different keys that specify access/return but omit the parameters.
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
}
}
}

View file

@ -1,357 +0,0 @@
package app.revanced.patcher.fingerprint
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import app.revanced.patcher.fingerprint.LookupMap.Maps.appendParameters
import app.revanced.patcher.fingerprint.LookupMap.Maps.initializeLookupMaps
import app.revanced.patcher.fingerprint.LookupMap.Maps.methodSignatureLookupMap
import app.revanced.patcher.fingerprint.LookupMap.Maps.methodStringsLookupMap
import app.revanced.patcher.fingerprint.LookupMap.Maps.methods
import app.revanced.patcher.fingerprint.MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.patch.PatchException
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
/**
* A fingerprint to resolve methods.
*
* @param returnType The method's return type compared using [String.startsWith].
* @param accessFlags The method's exact access flags using values of [AccessFlags].
* @param parameters The parameters of the method. Partial matches allowed and follow the same rules as [returnType].
* @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by `null`.
* @param strings A list of the method's strings compared each using [String.contains].
* @param customFingerprint A custom condition for this fingerprint.
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class MethodFingerprint(
internal val returnType: String? = null,
internal val accessFlags: Int? = null,
internal val parameters: Iterable<String>? = null,
internal val opcodes: Iterable<Opcode?>? = null,
internal val strings: Iterable<String>? = null,
internal val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null,
) {
/**
* The result of the [MethodFingerprint].
*/
var result: MethodFingerprintResult? = null
private set
/**
* The [FuzzyPatternScanMethod] annotation of the [MethodFingerprint].
*
* If the annotation is not present, this property is null.
*/
val fuzzyPatternScanMethod = this::class.findAnnotationRecursively(FuzzyPatternScanMethod::class)
/**
* Resolve a [MethodFingerprint] using the lookup map built by [initializeLookupMaps].
*
* [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable
* amount of time because they are resolved in sequence.
*
* For apps with many fingerprints, resolving performance can be improved by:
* - Slowest: Specify [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.
*/
internal fun resolveUsingLookupMap(context: BytecodeContext): Boolean {
/**
* Lookup [MethodClassPair]s that match the methods strings present in a [MethodFingerprint].
*
* @return A list of [MethodClassPair]s that match the methods strings present in a [MethodFingerprint].
*/
fun MethodFingerprint.methodStringsLookup(): LookupMap.MethodClassList? {
strings?.forEach {
val methods = methodStringsLookupMap[it]
if (methods != null) return methods
}
return null
}
/**
* Lookup [MethodClassPair]s that match the method signature present in a [MethodFingerprint].
*
* @return A list of [MethodClassPair]s that match the method signature present in a [MethodFingerprint].
*/
fun MethodFingerprint.methodSignatureLookup(): LookupMap.MethodClassList {
if (accessFlags == null) return methods
var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type
returnTypeValue = "V"
} else {
return methods
}
}
val key =
buildString {
append(accessFlags)
append(returnTypeValue.first())
if (parameters != null) appendParameters(parameters)
}
return methodSignatureLookupMap[key] ?: return LookupMap.MethodClassList()
}
/**
* Resolve a [MethodFingerprint] using a list of [MethodClassPair].
*
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolveUsingMethodClassPair(methodClasses: LookupMap.MethodClassList): Boolean {
methodClasses.forEach { classAndMethod ->
if (resolve(context, classAndMethod.first, classAndMethod.second)) return true
}
return false
}
val methodsWithSameStrings = methodStringsLookup()
if (methodsWithSameStrings != null) {
if (resolveUsingMethodClassPair(methodsWithSameStrings)) {
return true
}
}
// No strings declared or none matched (partial matches are allowed).
// Use signature matching.
return resolveUsingMethodClassPair(methodSignatureLookup())
}
/**
* Resolve a [MethodFingerprint] against a [ClassDef].
*
* @param forClass The class on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun resolve(
context: BytecodeContext,
forClass: ClassDef,
): Boolean {
for (method in forClass.methods)
if (resolve(context, method, forClass)) {
return true
}
return false
}
/**
* Resolve a [MethodFingerprint] against a [Method].
*
* @param method The class on which to resolve the [MethodFingerprint] in.
* @param forClass The class on which to resolve the [MethodFingerprint].
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/
fun resolve(
context: BytecodeContext,
method: Method,
forClass: ClassDef,
): Boolean {
val methodFingerprint = this
if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType)) {
return false
}
if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) {
return false
}
fun parametersEqual(
parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
if (methodFingerprint.parameters != null &&
!parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters()
method.parameterTypes,
)
) {
return false
}
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method, forClass)) {
return false
}
val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) {
StringsScanResult(
buildList {
val implementation = method.implementation ?: return false
val stringsList = methodFingerprint.strings.toMutableList()
implementation.instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(StringsScanResult.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return false
},
)
} else {
null
}
val patternScanResult =
if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.newWarnings(
pattern: Iterable<Opcode?>,
instructions: Iterable<Instruction>,
) = buildList {
for ((patternIndex, instructionIndex) in (this@newWarnings.startIndex until this@newWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
originalOpcode,
patternOpcode,
instructionIndex,
patternIndex,
),
)
}
}
fun Method.patternScan(
fingerprint: MethodFingerprint,
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyPatternScanMethod?.threshold ?: 0
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
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 = pattern.elementAt(patternIndex)
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
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex,
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.newWarnings(pattern, instructions)
return result
}
}
return null
}
method.patternScan(methodFingerprint) ?: return false
} else {
null
}
methodFingerprint.result =
MethodFingerprintResult(
method,
forClass,
MethodFingerprintResult.MethodFingerprintScanResult(
patternScanResult,
stringsScanResult,
),
context,
)
return true
}
companion object {
/**
* Resolve a list of [MethodFingerprint] using the lookup map built by [initializeLookupMaps].
*
* [MethodFingerprint] resolution is fast, but if many are present they can consume a noticeable
* amount of time because they are resolved in sequence.
*
* For apps with many fingerprints, resolving performance can be improved by:
* - Slowest: Specify [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.
*/
internal fun Set<MethodFingerprint>.resolveUsingLookupMap(context: BytecodeContext) {
if (methods.isEmpty()) throw PatchException("lookup map not initialized")
forEach { fingerprint ->
fingerprint.resolveUsingLookupMap(context)
}
}
/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
*
* @param classes The classes on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun Iterable<MethodFingerprint>.resolve(
context: BytecodeContext,
classes: Iterable<ClassDef>,
) = forEach { fingerprint ->
for (classDef in classes) {
if (fingerprint.resolve(context, classDef)) break
}
}
}
}

View file

@ -1,94 +0,0 @@
package app.revanced.patcher.fingerprint
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.util.proxy.ClassProxy
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.util.MethodUtil
/**
* Represents the result of a [MethodFingerprintResult].
*
* @param method The matching method.
* @param classDef The [ClassDef] that contains the matching [method].
* @param scanResult The result of scanning for the [MethodFingerprint].
* @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
*/
@Suppress("MemberVisibilityCanBePrivate")
class MethodFingerprintResult(
val method: Method,
val classDef: ClassDef,
val scanResult: MethodFingerprintScanResult,
internal val context: BytecodeContext,
) {
/**
* Returns a mutable clone of [classDef]
*
* Please note, this method allocates a [ClassProxy].
* Use [classDef] where possible.
*/
@Suppress("MemberVisibilityCanBePrivate")
val mutableClass by lazy { context.proxy(classDef).mutableClass }
/**
* Returns a mutable clone of [method]
*
* Please note, this method allocates a [ClassProxy].
* Use [method] where possible.
*/
val mutableMethod by lazy {
mutableClass.methods.first {
MethodUtil.methodSignaturesMatch(it, this.method)
}
}
/**
* The result of scanning on the [MethodFingerprint].
* @param patternScanResult The result of the pattern scan.
* @param stringsScanResult The result of the string scan.
*/
class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult?,
) {
/**
* The result of scanning strings on the [MethodFingerprint].
* @param matches The list of strings that were matched.
*/
class StringsScanResult(val matches: List<StringMatch>) {
/**
* Represents a match for a string at an index.
* @param string The string that was matched.
* @param index The index of the string.
*/
class StringMatch(val string: String, val index: Int)
}
/**
* The result of a pattern scan.
* @param startIndex The start index of the instructions where to which this pattern matches.
* @param endIndex The end index of the instructions where to which this pattern matches.
* @param warnings A list of warnings considering this [PatternScanResult].
*/
class PatternScanResult(
val startIndex: Int,
val endIndex: Int,
var warnings: List<Warning>? = null,
) {
/**
* Represents warnings of the pattern scan.
* @param correctOpcode The opcode the instruction list has.
* @param wrongOpcode The opcode the pattern list of the signature currently has.
* @param instructionIndex The index of the opcode relative to the instruction list.
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
*/
class Warning(
val correctOpcode: Opcode,
val wrongOpcode: Opcode,
val instructionIndex: Int,
val patternIndex: Int,
)
}
}
}

View file

@ -1,12 +0,0 @@
package app.revanced.patcher.fingerprint.annotation
import app.revanced.patcher.fingerprint.MethodFingerprint
/**
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
* @param threshold if [threshold] or more of the opcodes do not match, skip.
*/
@Target(AnnotationTarget.CLASS)
annotation class FuzzyPatternScanMethod(
val threshold: Int = 1,
)

View file

@ -1,68 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.MethodFingerprint.Companion.resolveUsingLookupMap
import java.io.Closeable
/**
* A [Patch] that accesses a [BytecodeContext].
*
* If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by [Patcher].
*/
@Suppress("unused")
abstract class BytecodePatch : Patch<BytecodeContext> {
/**
* The fingerprints to resolve before executing the patch.
*/
internal val fingerprints: Set<MethodFingerprint>
/**
* Create a new [BytecodePatch].
*
* @param fingerprints The fingerprints to resolve before executing the patch.
*/
constructor(fingerprints: Set<MethodFingerprint> = emptySet()) {
this.fingerprints = fingerprints
}
/**
* Create a new [BytecodePatch].
*
* @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies Other patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/
constructor(
name: String? = null,
description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
requiresIntegrations: Boolean = false,
fingerprints: Set<MethodFingerprint> = emptySet(),
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations) {
this.fingerprints = fingerprints
}
/**
* Create a new [BytecodePatch].
*/
@Deprecated(
"Use the constructor with fingerprints instead.",
ReplaceWith("BytecodePatch(emptySet())"),
)
constructor() : this(emptySet())
override fun execute(context: PatcherContext) {
fingerprints.resolveUsingLookupMap(context.bytecodeContext)
execute(context.bytecodeContext)
}
}

View file

@ -0,0 +1,280 @@
package app.revanced.patcher.patch
import app.revanced.patcher.InternalApi
import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import app.revanced.patcher.util.ProxyClassList
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.Closeable
import java.io.FileFilter
import java.io.InputStream
import java.util.*
import java.util.logging.Logger
/**
* A context for patches containing the current state of the bytecode.
*
* @param config The [PatcherConfig] used to create this context.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodePatchContext internal constructor(private val config: PatcherConfig) : PatchContext<Set<PatcherResult.PatchedDexFile>> {
private val logger = Logger.getLogger(BytecodePatchContext::class.java.name)
/**
* [Opcodes] of the supplied [PatcherConfig.apkFile].
*/
internal val opcodes: Opcodes
/**
* The list of classes.
*/
val classes = ProxyClassList(
MultiDexIO.readDexFile(
true,
config.apkFile,
BasicDexFileNamer(),
null,
null,
).also { opcodes = it.opcodes }.classes.toMutableList(),
)
/**
* The lookup maps for methods and the class they are a member of from the [classes].
*/
internal val lookupMaps by lazy { LookupMaps(classes) }
/**
* Merge an extension to [classes].
*
* @param extensionInputStream The input stream of the extension to merge.
*/
internal fun merge(extensionInputStream: InputStream) {
val extension = extensionInputStream.readAllBytes()
RawDexIO.readRawDexFile(extension, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
logger.fine("Adding class \"$classDef\"")
lookupMaps.classesByType[classDef.type] = classDef
classes += classDef
return@forEach
}
logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classes -= existingClass
classes += mergedClass
}
}
}
/**
* Find a class by its type using a contains check.
*
* @param type The type of the class.
* @return A proxy for the first class that matches the type.
*/
fun classByType(type: String) = classBy { type in it.type }
/**
* 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.
*/
fun classBy(predicate: (ClassDef) -> Boolean) =
classes.proxyPool.find { predicate(it.immutableClass) } ?: classes.find(predicate)?.let { proxy(it) }
/**
* Proxy the class to allow mutation.
*
* @param classDef The class to proxy.
*
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = this@BytecodePatchContext.classes.proxyPool.find {
it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) }
/**
* Navigate a method.
*
* @param method The method to navigate.
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: Method) = MethodNavigator(this@BytecodePatchContext, method)
/**
* Compile bytecode from the [BytecodePatchContext].
*
* @return The compiled bytecode.
*/
@InternalApi
override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
val patchedDexFileResults =
config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
if (config.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() =
this@BytecodePatchContext.classes.also(ProxyClassList::replaceClasses).toSet()
override fun getOpcodes() = this@BytecodePatchContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
System.gc()
return patchedDexFileResults
}
/**
* A lookup map for methods and the class they are a member of and classes.
*
* @param classes The list of classes to create the lookup maps from.
*/
internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable {
/**
* Classes associated by their type.
*/
internal val classesByType = classes.associateBy { it.type }.toMutableMap()
/**
* All methods and the class they are a member of.
*/
internal val allMethods = MethodClassPairs()
/**
* Methods associated by its access flags, return type and parameter.
*/
internal val methodsBySignature = MethodClassPairsLookupMap()
/**
* Methods associated by strings referenced in it.
*/
internal val methodsByStrings = MethodClassPairsLookupMap()
init {
classes.forEach { classDef ->
classDef.methods.forEach { method ->
val methodClassPair: MethodClassPair = method to classDef
// For fingerprints with no access or return type specified.
allMethods += methodClassPair
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
// Add <access><returnType> as the key.
methodsBySignature[accessFlagsReturnKey] = methodClassPair
// Add <access><returnType>[parameters] as the key.
methodsBySignature[
buildString {
append(accessFlagsReturnKey)
appendParameters(method.parameterTypes)
},
] = methodClassPair
// Add strings contained in the method as the key.
method.instructionsOrNull?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@instructions
}
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.
}
}
}
internal companion object {
/**
* Appends a string based on the parameter reference types of this method.
*/
internal fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
// Maximum parameters to use in the signature key.
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
// To keep the signature map from becoming needlessly bloated,
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
// The value of 5 was chosen based on local performance testing and is not set in stone.
val maxSignatureParameters = 5
// Must append a unique value before the parameters to distinguish this key includes the parameters.
// If this is not appended, then methods with no parameters
// will collide with different keys that specify access/return but omit the parameters.
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
}
}
override fun close() {
allMethods.clear()
methodsBySignature.clear()
methodsByStrings.clear()
}
}
}
/**
* A pair of a [Method] and the [ClassDef] it is a member of.
*/
internal typealias MethodClassPair = Pair<Method, ClassDef>
/**
* A list of [MethodClassPair]s.
*/
internal typealias MethodClassPairs = LinkedList<MethodClassPair>
/**
* A lookup map for [MethodClassPairs]s.
* The key is a string and the value is a list of [MethodClassPair]s.
*/
internal class MethodClassPairsLookupMap : MutableMap<String, MethodClassPairs> by mutableMapOf() {
/**
* Add a [MethodClassPair] associated by any key.
* If the key does not exist, a new list is created and the [MethodClassPair] is added to it.
*/
internal operator fun set(key: String, methodClassPair: MethodClassPair) =
apply { getOrPut(key) { MethodClassPairs() }.add(methodClassPair) }
}

View file

@ -0,0 +1,548 @@
package app.revanced.patcher.patch
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* An option.
*
* @param T The value type of the option.
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param type The type of the option value (to handle type erasure).
* @param validator The function to validate the option value.
*
* @constructor Create a new [Option].
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
class Option<T> @PublishedApi internal constructor(
val key: String,
val default: T? = null,
val values: Map<String, T?>? = null,
val title: String? = null,
val description: String? = null,
val required: Boolean = false,
val type: KType,
val validator: Option<T>.(T?) -> Boolean = { true },
) {
/**
* The value of the [Option].
*/
var value: T?
/**
* Set the value of the [Option].
*
* @param value The value to set.
*
* @throws OptionException.ValueRequiredException If the value is required but null.
* @throws OptionException.ValueValidationException If the value is invalid.
*/
set(value) {
assertRequiredButNotNull(value)
assertValid(value)
uncheckedValue = value
}
/**
* Get the value of the [Option].
*
* @return The value.
*
* @throws OptionException.ValueRequiredException If the value is required but null.
* @throws OptionException.ValueValidationException If the value is invalid.
*/
get() {
assertRequiredButNotNull(uncheckedValue)
assertValid(uncheckedValue)
return uncheckedValue
}
// The unchecked value is used to allow setting the value without validation.
private var uncheckedValue = default
/**
* Reset the [Option] to its default value.
* Override this method if you need to mutate the value instead of replacing it.
*/
fun reset() {
uncheckedValue = default
}
private fun assertRequiredButNotNull(value: T?) {
if (required && value == null) throw OptionException.ValueRequiredException(this)
}
private fun assertValid(value: T?) {
if (!validator(value)) throw OptionException.ValueValidationException(value, this)
}
override fun toString() = value.toString()
operator fun getValue(
thisRef: Any?,
property: KProperty<*>,
) = value
operator fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T?,
) {
this.value = value
}
}
/**
* A collection of [Option]s where options can be set and retrieved by key.
*
* @param options The options.
*
* @constructor Create a new [Options].
*/
class Options internal constructor(
private val options: Map<String, Option<*>>,
) : Map<String, Option<*>> by options {
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.key })
/**
* Set an option's value.
*
* @param key The key.
* @param value The value.
*
* @throws OptionException.OptionNotFoundException If the option does not exist.
*/
operator fun <T : Any> set(key: String, value: T?) {
val option = this[key]
try {
@Suppress("UNCHECKED_CAST")
(option as Option<T>).value = value
} catch (e: ClassCastException) {
throw OptionException.InvalidValueTypeException(
value?.let { it::class.java.name } ?: "null",
option.value?.let { it::class.java.name } ?: "null",
)
}
}
/**
* Get an option.
*
* @param key The key.
*
* @return The option.
*/
override fun get(key: String) = options[key] ?: throw OptionException.OptionNotFoundException(key)
}
/**
* Create a new [Option] with a string value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.stringOption(
key: String,
default: String? = null,
values: Map<String, String?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<String>.(String?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with an integer value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.intOption(
key: String,
default: Int? = null,
values: Map<String, Int?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Int?>.(Int?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a boolean value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.booleanOption(
key: String,
default: Boolean? = null,
values: Map<String, Boolean?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Boolean?>.(Boolean?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a float value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.floatOption(
key: String,
default: Float? = null,
values: Map<String, Float?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Float?>.(Float?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a long value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.longOption(
key: String,
default: Long? = null,
values: Map<String, Long?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Long?>.(Long?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a string list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.stringsOption(
key: String,
default: List<String>? = null,
values: Map<String, List<String>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with an integer list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.intsOption(
key: String,
default: List<Int>? = null,
values: Map<String, List<Int>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a boolean list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.booleansOption(
key: String,
default: List<Boolean>? = null,
values: Map<String, List<Boolean>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a float list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.floatsOption(
key: String,
default: List<Float>? = null,
values: Map<String, List<Float>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Float>>.(List<Float>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a long list value and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun PatchBuilder<*>.longsOption(
key: String,
default: List<Long>? = null,
values: Map<String, List<Long>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] and add it to the current [PatchBuilder].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
inline fun <reified T> PatchBuilder<*>.option(
key: String,
default: T? = null,
values: Map<String, T?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
noinline validator: Option<T>.(T?) -> Boolean = { true },
) = Option(
key,
default,
values,
title,
description,
required,
typeOf<T>(),
validator,
).also { it() }
/**
* An exception thrown when using [Option]s.
*
* @param errorMessage The exception message.
*/
sealed class OptionException(errorMessage: String) : Exception(errorMessage, null) {
/**
* An exception thrown when a [Option] is set to an invalid value.
*
* @param invalidType The type of the value that was passed.
* @param expectedType The type of the value that was expected.
*/
class InvalidValueTypeException(invalidType: String, expectedType: String) :
OptionException("Type $expectedType was expected but received type $invalidType")
/**
* An exception thrown when a value did not satisfy the value conditions specified by the [Option].
*
* @param value The value that failed validation.
*/
class ValueValidationException(value: Any?, option: Option<*>) :
OptionException("The option value \"$value\" failed validation for ${option.key}")
/**
* An exception thrown when a value is required but null was passed.
*
* @param option The [Option] that requires a value.
*/
class ValueRequiredException(option: Option<*>) :
OptionException("The option ${option.key} requires a value, but null was passed")
/**
* An exception thrown when a [Option] is not found.
*
* @param key The key of the [Option].
*/
class OptionNotFoundException(key: String) :
OptionException("No option with key $key")
}

View file

@ -1,133 +1,663 @@
@file:Suppress("MemberVisibilityCanBePrivate") @file:Suppress("MemberVisibilityCanBePrivate", "unused")
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass import app.revanced.patcher.Fingerprint
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext import app.revanced.patcher.PatcherContext
import app.revanced.patcher.data.Context import dalvik.system.DexClassLoader
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively import lanchon.multidexlib2.BasicDexFileNamer
import app.revanced.patcher.patch.options.PatchOptions import lanchon.multidexlib2.MultiDexIO
import java.io.Closeable import java.io.File
import java.io.InputStream
import java.net.URLClassLoader
import java.util.jar.JarFile
import kotlin.reflect.KProperty
typealias PackageName = String
typealias VersionName = String
typealias Package = Pair<PackageName, Set<VersionName>?>
/** /**
* A patch. * A patch.
* *
* If an implementation of [Patch] also implements [Closeable] * @param C The [PatchContext] to execute and finalize the patch with.
* it will be closed in reverse execution order of patches executed by [Patcher]. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param dependencies Other patches this patch depends on.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param options The options of the patch.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
* *
* @param T The [Context] type this patch will work on. * @constructor Create a new patch.
*/ */
sealed class Patch<out T : Context<*>> { sealed class Patch<C : PatchContext<*>>(
/** val name: String?,
* The name of the patch. val description: String?,
*/ val use: Boolean,
var name: String? = null val dependencies: Set<Patch<*>>,
private set val compatiblePackages: Set<Package>?,
options: Set<Option<*>>,
private val executeBlock: Patch<C>.(C) -> Unit,
// Must be internal and nullable, so that Patcher.invoke can check,
// if a patch has a finalizing block in order to not emit it twice.
internal var finalizeBlock: (Patch<C>.(C) -> Unit)?,
) {
val options = Options(options)
/** /**
* The description of the patch. * Runs the execution block of the patch.
*/ * Called by [Patcher].
var description: String? = null
private set
/**
* The packages the patch is compatible with.
*/
var compatiblePackages: Set<CompatiblePackage>? = null
private set
/**
* Other patches this patch depends on.
*/
var dependencies: Set<PatchClass>? = null
private set
/**
* Weather or not the patch should be used.
*/
var use = true
private set
// TODO: Remove this property, once integrations are coupled with patches.
/**
* Weather or not the patch requires integrations.
*/
var requiresIntegrations = false
private set
constructor(
name: String?,
description: String?,
compatiblePackages: Set<CompatiblePackage>?,
dependencies: Set<PatchClass>?,
use: Boolean,
requiresIntegrations: Boolean,
) {
this.name = name
this.description = description
this.compatiblePackages = compatiblePackages
this.dependencies = dependencies
this.use = use
this.requiresIntegrations = requiresIntegrations
}
constructor() {
this::class.findAnnotationRecursively(app.revanced.patcher.patch.annotation.Patch::class)?.let { annotation ->
this.name = annotation.name.ifEmpty { null }
this.description = annotation.description.ifEmpty { null }
this.compatiblePackages =
annotation.compatiblePackages
.map { CompatiblePackage(it.name, it.versions.toSet().ifEmpty { null }) }
.toSet().ifEmpty { null }
this.dependencies = annotation.dependencies.toSet().ifEmpty { null }
this.use = annotation.use
this.requiresIntegrations = annotation.requiresIntegrations
}
}
/**
* The options of the patch associated by the options key.
*/
val options = PatchOptions()
/**
* The execution function of the patch.
* This function is called by [Patcher].
* *
* @param context The [PatcherContext] the patch will work on. * @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
*/ */
internal abstract fun execute(context: PatcherContext) internal abstract fun execute(context: PatcherContext)
/** /**
* The execution function of the patch. * Runs the execution block of the patch.
* *
* @param context The [Context] the patch will work on. * @param context The [PatchContext] to execute the patch with.
* @return The result of executing the patch.
*/ */
abstract fun execute(context: @UnsafeVariance T) fun execute(context: C) = executeBlock(context)
override fun hashCode() = name.hashCode() /**
* Runs the finalizing block of the patch.
* Called by [Patcher].
*
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
*/
internal abstract fun finalize(context: PatcherContext)
override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch" /**
* Runs the finalizing block of the patch.
*
* @param context The [PatchContext] to finalize the patch with.
*/
fun finalize(context: C) {
finalizeBlock?.invoke(this, context)
}
override fun equals(other: Any?): Boolean { override fun toString() = name ?: "Patch"
if (this === other) return true }
if (javaClass != other?.javaClass) return false
other as Patch<*> /**
* A bytecode patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @param fingerprints The fingerprints that are resolved before the patch is executed.
* @property extension An input stream of the extension resource this patch uses.
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new bytecode patch.
*/
class BytecodePatch internal constructor(
name: String?,
description: String?,
use: Boolean,
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
val fingerprints: Set<Fingerprint>,
val extension: InputStream?,
executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
finalizeBlock: (Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit)?,
) : Patch<BytecodePatchContext>(
name,
description,
use,
dependencies,
compatiblePackages,
options,
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
extension?.let(::merge)
fingerprints.forEach { it.match(this) }
return name == other.name execute(this)
}
override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext)
override fun toString() = name ?: "BytecodePatch"
}
/**
* A raw resource patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new raw resource patch.
*/
class RawResourcePatch internal constructor(
name: String?,
description: String?,
use: Boolean,
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>(
name,
description,
use,
dependencies,
compatiblePackages,
options,
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = execute(context.resourceContext)
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "RawResourcePatch"
}
/**
* A resource patch.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on.
* @param options The options of the patch.
* @param executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new resource patch.
*/
class ResourcePatch internal constructor(
name: String?,
description: String?,
use: Boolean,
compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>,
options: Set<Option<*>>,
executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>(
name,
description,
use,
dependencies,
compatiblePackages,
options,
executeBlock,
finalizeBlock,
) {
override fun execute(context: PatcherContext) = execute(context.resourceContext)
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "ResourcePatch"
}
/**
* A [Patch] builder.
*
* @param C The [PatchContext] to execute and finalize the patch with.
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages.
* @property dependencies Other patches this patch depends on.
* @property options The options of the patch.
* @property executionBlock The execution block of the patch.
* @property finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
* in reverse order of execution.
*
* @constructor Create a new [Patch] builder.
*/
sealed class PatchBuilder<C : PatchContext<*>>(
protected val name: String?,
protected val description: String?,
protected val use: Boolean,
) {
protected var compatiblePackages: MutableSet<Package>? = null
protected var dependencies = mutableSetOf<Patch<*>>()
protected val options = mutableSetOf<Option<*>>()
protected var executionBlock: (Patch<C>.(C) -> Unit) = { }
protected var finalizeBlock: (Patch<C>.(C) -> Unit)? = null
/**
* Add an option to the patch.
*
* @return The added option.
*/
operator fun <T> Option<T>.invoke() = apply {
options += this
} }
/** /**
* A package a [Patch] is compatible with. * Create a package a patch is compatible with.
* *
* @param name The name of the package.
* @param versions The versions of the package. * @param versions The versions of the package.
*/ */
class CompatiblePackage( operator fun String.invoke(vararg versions: String) = this to versions.toSet()
val name: String,
val versions: Set<String>? = null, /**
* Add packages the patch is compatible with.
*
* @param packages The packages the patch is compatible with.
*/
fun compatibleWith(vararg packages: Package) {
if (compatiblePackages == null) {
compatiblePackages = mutableSetOf()
}
compatiblePackages!! += packages
}
/**
* Set the compatible packages of the patch.
*
* @param packages The packages the patch is compatible with.
*/
fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray())
/**
* Add dependencies to the patch.
*
* @param patches The patches the patch depends on.
*/
fun dependsOn(vararg patches: Patch<*>) {
dependencies += patches
}
/**
* Set the execution block of the patch.
*
* @param block The execution block of the patch.
*/
fun execute(block: Patch<C>.(C) -> Unit) {
executionBlock = block
}
/**
* Set the finalizing block of the patch.
*
* @param block The finalizing block of the patch.
*/
fun finalize(block: Patch<C>.(C) -> Unit) {
finalizeBlock = block
}
/**
* Build the patch.
*
* @return The built patch.
*/
internal abstract fun build(): Patch<C>
}
/**
* A [BytecodePatchBuilder] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @property fingerprints The fingerprints that are resolved before the patch is executed.
* @property extension An input stream of the extension resource this patch uses.
* An extension is a precompiled DEX file that is merged into the patched app before this patch is executed.
*
* @constructor Create a new [BytecodePatchBuilder] builder.
*/
class BytecodePatchBuilder internal constructor(
name: String?,
description: String?,
use: Boolean,
) : PatchBuilder<BytecodePatchContext>(name, description, use) {
private val fingerprints = mutableSetOf<Fingerprint>()
/**
* Add the fingerprint to the patch.
*
* @return A wrapper for the fingerprint with the ability to delegate the match to the fingerprint.
*/
operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) })
class InvokedFingerprint(private val fingerprint: Fingerprint) {
// The reason getValue isn't extending the Fingerprint class is
// because delegating makes only sense if the fingerprint was previously added to the patch by invoking it.
// It may be likely to forget invoking it. By wrapping the fingerprint into this class,
// the compiler will throw an error if the fingerprint was not invoked if attempting to delegate the match.
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = fingerprint.match
?: throw PatchException("No fingerprint match to delegate to ${property.name}.")
}
// Must be internal for the inlined function "extendWith".
@PublishedApi
internal var extension: InputStream? = null
// Inlining is necessary to get the class loader that loaded the patch
// to load the extension from the resources.
/**
* Set the extension of the patch.
*
* @param extension The name of the extension resource.
*/
inline fun extendWith(extension: String) = apply {
this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension resource \"$extension\" not found")
}
override fun build() = BytecodePatch(
name,
description,
use,
compatiblePackages,
dependencies,
options,
fingerprints,
extension,
executionBlock,
finalizeBlock,
) )
} }
/**
* A [RawResourcePatch] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
*
* @constructor Create a new [RawResourcePatch] builder.
*/
class RawResourcePatchBuilder internal constructor(
name: String?,
description: String?,
use: Boolean,
) : PatchBuilder<ResourcePatchContext>(name, description, use) {
override fun build() = RawResourcePatch(
name,
description,
use,
compatiblePackages,
dependencies,
options,
executionBlock,
finalizeBlock,
)
}
/**
* A [ResourcePatch] builder.
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
*
* @constructor Create a new [ResourcePatch] builder.
*/
class ResourcePatchBuilder internal constructor(
name: String?,
description: String?,
use: Boolean,
) : PatchBuilder<ResourcePatchContext>(name, description, use) {
override fun build() = ResourcePatch(
name,
description,
use,
compatiblePackages,
dependencies,
options,
executionBlock,
finalizeBlock,
)
}
/**
* Builds a [Patch].
*
* @param B The [PatchBuilder] to build the patch with.
* @param block The block to build the patch.
*
* @return The built [Patch].
*/
private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply(block).build()
/**
* Create a new [BytecodePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
*
* @return The created [BytecodePatch].
*/
fun bytecodePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: BytecodePatchBuilder.() -> Unit = {},
) = BytecodePatchBuilder(name, description, use).buildPatch(block) as BytecodePatch
/**
* Create a new [RawResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
* @return The created [RawResourcePatch].
*/
fun rawResourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: RawResourcePatchBuilder.() -> Unit = {},
) = RawResourcePatchBuilder(name, description, use).buildPatch(block) as RawResourcePatch
/**
* Create a new [ResourcePatch].
*
* @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch.
* @param use Weather or not the patch should be used.
* @param block The block to build the patch.
*
* @return The created [ResourcePatch].
*/
fun resourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: ResourcePatchBuilder.() -> Unit = {},
) = ResourcePatchBuilder(name, description, use).buildPatch(block) as ResourcePatch
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}
/**
* A result of executing a [Patch].
*
* @param patch The [Patch] that was executed.
* @param exception The [PatchException] thrown, if any.
*/
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
/**
* A loader for patches.
*
* Loads unnamed patches from JAR or DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
*
* @param byPatchesFile The patches associated by the patches file they were loaded from.
*/
sealed class PatchLoader private constructor(
val byPatchesFile: Map<File, Set<Patch<*>>>,
) : Set<Patch<*>> by byPatchesFile.values.flatten().toSet() {
/**
* @param patchesFiles A set of JAR or DEX files to load the patches from.
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
* @param classLoader The [ClassLoader] to use for loading the classes.
*/
private constructor(
patchesFiles: Set<File>,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
) : this(classLoader.loadPatches(patchesFiles.associateWith { getBinaryClassNames(it).toSet() }))
/**
* A [PatchLoader] for JAR files.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @constructor Create a new [PatchLoader] for JAR files.
*/
class Jar(patchesFiles: Set<File>) :
PatchLoader(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
)
/**
* A [PatchLoader] for [Dex] files.
*
* @param patchesFiles The DEX files to load the patches from.
* @param optimizedDexDirectory The directory to store optimized DEX files in.
* This parameter is deprecated and has no effect since API level 26.
*
* @constructor Create a new [PatchLoader] for [Dex] files.
*/
class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) :
PatchLoader(
patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
this::class.java.classLoader,
),
)
// Companion object required for unit tests.
private companion object {
val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Public static fields that are patches.
*/
private val Class<*>.patchFields
get() = fields.filter { field ->
field.type.isPatch && field.canAccess(null)
}.map { field ->
field.get(null) as Patch<*>
}
/**
* Public static and non-parametrized methods that return patches.
*/
private val Class<*>.patchMethods
get() = methods.filter { method ->
method.returnType.isPatch && method.parameterCount == 0 && method.canAccess(null)
}.map { method ->
method.invoke(null) as Patch<*>
}
/**
* Loads unnamed patches declared as public static fields
* or returned by public static and non-parametrized methods.
*
* @param binaryClassNamesByPatchesFile The binary class name of the classes to load the patches from
* associated by the patches file.
*
* @return The loaded patches associated by the patches file.
*/
private fun ClassLoader.loadPatches(binaryClassNamesByPatchesFile: Map<File, Set<String>>) =
binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) ->
binaryClassNames.asSequence().map {
loadClass(it)
}.flatMap {
it.patchFields + it.patchMethods
}.filter {
it.name != null
}.toSet()
}
}
}
/**
* Loads patches from JAR files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The JAR files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromJar(patchesFiles: Set<File>) =
PatchLoader.Jar(patchesFiles)
/**
* Loads patches from DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded.
*
* @param patchesFiles The DEX files to load the patches from.
*
* @return The loaded patches.
*/
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) =
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)

View file

@ -0,0 +1,9 @@
package app.revanced.patcher.patch
import java.util.function.Supplier
/**
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
*/
sealed interface PatchContext<T> : Supplier<T>

View file

@ -1,12 +0,0 @@
package app.revanced.patcher.patch
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}

View file

@ -1,9 +0,0 @@
package app.revanced.patcher.patch
/**
* A result of executing a [Patch].
*
* @param patch The [Patch] that was executed.
* @param exception The [PatchException] thrown, if any.
*/
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)

View file

@ -1,46 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.data.ResourceContext
import java.io.Closeable
/**
* A [Patch] that accesses a [ResourceContext].
*
* If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by [Patcher].
*
* This type of patch that does not have access to decoded resources.
* Instead, you can read and write arbitrary files in an APK file.
*
* If you want to access decoded resources, use [ResourcePatch] instead.
*/
abstract class RawResourcePatch : Patch<ResourceContext> {
/**
* Create a new [RawResourcePatch].
*/
constructor()
/**
* Create a new [RawResourcePatch].
*
* @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies Other patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/
constructor(
name: String? = null,
description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
requiresIntegrations: Boolean = false,
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
override fun execute(context: PatcherContext) = execute(context.resourceContext)
}

View file

@ -1,46 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchClass
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.data.ResourceContext
import java.io.Closeable
/**
* A [Patch] that accesses a [ResourceContext].
*
* If an implementation of [Patch] also implements [Closeable]
* it will be closed in reverse execution order of patches executed by [Patcher].
*
* This type of patch has access to decoded resources.
* Additionally, you can read and write arbitrary files in an APK file.
*
* If you do not need access to decoded resources, use [RawResourcePatch] instead.
*/
abstract class ResourcePatch : Patch<ResourceContext> {
/**
* Create a new [ResourcePatch].
*/
constructor()
/**
* Create a new [ResourcePatch].
*
* @param name The name of the patch.
* @param description The description of the patch.
* @param compatiblePackages The packages the patch is compatible with.
* @param dependencies Other patches this patch depends on.
* @param use Weather or not the patch should be used.
* @param requiresIntegrations Weather or not the patch requires integrations.
*/
constructor(
name: String? = null,
description: String? = null,
compatiblePackages: Set<CompatiblePackage>? = null,
dependencies: Set<PatchClass>? = null,
use: Boolean = true,
requiresIntegrations: Boolean = false,
) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations)
override fun execute(context: PatcherContext) = execute(context.resourceContext)
}

View file

@ -1,11 +1,10 @@
package app.revanced.patcher.data package app.revanced.patcher.patch
import app.revanced.patcher.InternalApi import app.revanced.patcher.InternalApi
import app.revanced.patcher.PackageMetadata import app.revanced.patcher.PackageMetadata
import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.PatcherResult import app.revanced.patcher.PatcherResult
import app.revanced.patcher.util.Document import app.revanced.patcher.util.Document
import app.revanced.patcher.util.DomFileEditor
import brut.androlib.AaptInvoker import brut.androlib.AaptInvoker
import brut.androlib.ApkDecoder import brut.androlib.ApkDecoder
import brut.androlib.apk.UsesFramework import brut.androlib.apk.UsesFramework
@ -15,33 +14,28 @@ import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.decoder.XmlPullStreamDecoder import brut.androlib.res.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile import brut.directory.ExtFile
import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.nio.file.Files import java.nio.file.Files
import java.util.logging.Logger import java.util.logging.Logger
/** /**
* A context for the patcher containing the current state of the resources. * A context for patches containing the current state of resources.
* *
* @param packageMetadata The [PackageMetadata] of the apk file. * @param packageMetadata The [PackageMetadata] of the apk file.
* @param config The [PatcherConfig] used to create this context. * @param config The [PatcherConfig] used to create this context.
*/ */
class ResourceContext internal constructor( class ResourcePatchContext internal constructor(
private val packageMetadata: PackageMetadata, private val packageMetadata: PackageMetadata,
private val config: PatcherConfig, private val config: PatcherConfig,
) : Context<PatcherResult.PatchedResources?>, Iterable<File> { ) : PatchContext<PatcherResult.PatchedResources?> {
private val logger = Logger.getLogger(ResourceContext::class.java.name) private val logger = Logger.getLogger(ResourcePatchContext::class.java.name)
/** /**
* Read and write documents in the [PatcherConfig.apkFiles]. * Read and write documents in the [PatcherConfig.apkFiles].
*/ */
val document = DocumentOperatable() val document = DocumentOperatable()
@Deprecated("Use document instead.")
@Suppress("DEPRECATION")
val xmlEditor = XmlFileHolder()
/** /**
* Predicate to delete resources from [PatcherConfig.apkFiles]. * Predicate to delete resources from [PatcherConfig.apkFiles].
*/ */
@ -155,7 +149,7 @@ class ResourceContext internal constructor(
// Excluded because present in resources.other. // Excluded because present in resources.other.
// TODO: We are reusing config.apkFiles as a temporarily directory for extracting resources. // TODO: We are reusing config.apkFiles as a temporarily directory for extracting resources.
// This is not ideal as it could conflict with files such as the ones that we filter here. // This is not ideal as it could conflict with files such as the ones that we filter here.
// The problem is that ResourceContext#get returns a File relative to config.apkFiles, // The problem is that ResourcePatchContext#get returns a File relative to config.apkFiles,
// and we need to extract files to that directory. // and we need to extract files to that directory.
// A solution would be to use config.apkFiles as the working directory for the patching process. // A solution would be to use config.apkFiles as the working directory for the patching process.
// Once all patches have been executed, we can move the decoded resources to a new directory. // Once all patches have been executed, we can move the decoded resources to a new directory.
@ -213,12 +207,6 @@ class ResourceContext internal constructor(
*/ */
fun stageDelete(shouldDelete: (String) -> Boolean) = deleteResources.add(shouldDelete) fun stageDelete(shouldDelete: (String) -> Boolean) = deleteResources.add(shouldDelete)
@Deprecated("Use get(String, Boolean) instead.", ReplaceWith("get(path, false)"))
operator fun get(path: String) = get(path, false)
@Deprecated("Use get(String, Boolean) instead.")
override fun iterator(): Iterator<File> = config.apkFiles.listFiles()!!.iterator()
/** /**
* How to handle resources decoding and compiling. * How to handle resources decoding and compiling.
*/ */
@ -243,18 +231,6 @@ class ResourceContext internal constructor(
inner class DocumentOperatable { inner class DocumentOperatable {
operator fun get(inputStream: InputStream) = Document(inputStream) operator fun get(inputStream: InputStream) = Document(inputStream)
@Suppress("DEPRECATION") operator fun get(path: String) = Document(this@ResourcePatchContext[path])
operator fun get(path: String) = Document(this@ResourceContext[path])
}
@Deprecated("Use DocumentOperatable instead.")
inner class XmlFileHolder {
@Suppress("DEPRECATION")
operator fun get(inputStream: InputStream) = DomFileEditor(inputStream)
@Suppress("DEPRECATION")
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path])
}
} }
} }

View file

@ -1,37 +0,0 @@
package app.revanced.patcher.patch.annotation
import java.lang.annotation.Inherited
import kotlin.reflect.KClass
/**
* Annotation for [app.revanced.patcher.patch.Patch] classes.
*
* @param name The name of the patch. If empty, the patch will be unnamed.
* @param description The description of the patch. If empty, no description will be used.
* @param dependencies The patches this patch depends on.
* @param compatiblePackages The packages this patch is compatible with.
* @param use Whether this patch should be used.
* @param requiresIntegrations Whether this patch requires integrations.
*/
@Target(AnnotationTarget.CLASS)
@Inherited
annotation class Patch(
val name: String = "",
val description: String = "",
val dependencies: Array<KClass<out app.revanced.patcher.patch.Patch<*>>> = [],
val compatiblePackages: Array<CompatiblePackage> = [],
val use: Boolean = true,
// TODO: Remove this property, once integrations are coupled with patches.
val requiresIntegrations: Boolean = false,
)
/**
* A package that a [app.revanced.patcher.patch.Patch] is compatible with.
*
* @param name The name of the package.
* @param versions The versions of the package.
*/
annotation class CompatiblePackage(
val name: String,
val versions: Array<String> = [],
)

View file

@ -1,476 +0,0 @@
package app.revanced.patcher.patch.options
import app.revanced.patcher.patch.Patch
import kotlin.reflect.KProperty
/**
* A [Patch] option.
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values identified by their string representation.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param valueType The type of the option value (to handle type erasure).
* @param validator The function to validate the option value.
* @param T The value type of the option.
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
open class PatchOption<T>(
val key: String,
val default: T?,
val values: Map<String, T?>?,
val title: String?,
val description: String?,
val required: Boolean,
val valueType: String,
val validator: PatchOption<T>.(T?) -> Boolean,
) {
/**
* The value of the [PatchOption].
*/
var value: T?
/**
* Set the value of the [PatchOption].
*
* @param value The value to set.
*
* @throws PatchOptionException.ValueRequiredException If the value is required but null.
* @throws PatchOptionException.ValueValidationException If the value is invalid.
*/
set(value) {
assertRequiredButNotNull(value)
assertValid(value)
uncheckedValue = value
}
/**
* Get the value of the [PatchOption].
*
* @return The value.
*
* @throws PatchOptionException.ValueRequiredException If the value is required but null.
* @throws PatchOptionException.ValueValidationException If the value is invalid.
*/
get() {
assertRequiredButNotNull(uncheckedValue)
assertValid(uncheckedValue)
return uncheckedValue
}
// The unchecked value is used to allow setting the value without validation.
private var uncheckedValue = default
/**
* Reset the [PatchOption] to its default value.
* Override this method if you need to mutate the value instead of replacing it.
*/
open fun reset() {
uncheckedValue = default
}
private fun assertRequiredButNotNull(value: T?) {
if (required && value == null) throw PatchOptionException.ValueRequiredException(this)
}
private fun assertValid(value: T?) {
if (!validator(value)) throw PatchOptionException.ValueValidationException(value, this)
}
override fun toString() = value.toString()
operator fun getValue(
thisRef: Any?,
property: KProperty<*>,
) = value
operator fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T?,
) {
this.value = value
}
@Suppress("unused")
companion object PatchExtensions {
/**
* Create a new [PatchOption] with a string value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.stringPatchOption(
key: String,
default: String? = null,
values: Map<String, String?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<String>.(String?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"String",
validator,
)
/**
* Create a new [PatchOption] with an integer value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.intPatchOption(
key: String,
default: Int? = null,
values: Map<String, Int?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Int?>.(Int?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"Int",
validator,
)
/**
* Create a new [PatchOption] with a boolean value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.booleanPatchOption(
key: String,
default: Boolean? = null,
values: Map<String, Boolean?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Boolean?>.(Boolean?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"Boolean",
validator,
)
/**
* Create a new [PatchOption] with a float value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.floatPatchOption(
key: String,
default: Float? = null,
values: Map<String, Float?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Float?>.(Float?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"Float",
validator,
)
/**
* Create a new [PatchOption] with a long value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.longPatchOption(
key: String,
default: Long? = null,
values: Map<String, Long?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Long?>.(Long?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"Long",
validator,
)
/**
* Create a new [PatchOption] with a string array value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.stringArrayPatchOption(
key: String,
default: Array<String>? = null,
values: Map<String, Array<String>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Array<String>?>.(Array<String>?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"StringArray",
validator,
)
/**
* Create a new [PatchOption] with an integer array value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.intArrayPatchOption(
key: String,
default: Array<Int>? = null,
values: Map<String, Array<Int>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Array<Int>?>.(Array<Int>?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"IntArray",
validator,
)
/**
* Create a new [PatchOption] with a boolean array value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.booleanArrayPatchOption(
key: String,
default: Array<Boolean>? = null,
values: Map<String, Array<Boolean>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Array<Boolean>?>.(Array<Boolean>?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"BooleanArray",
validator,
)
/**
* Create a new [PatchOption] with a float array value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.floatArrayPatchOption(
key: String,
default: Array<Float>? = null,
values: Map<String, Array<Float>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Array<Float>?>.(Array<Float>?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"FloatArray",
validator,
)
/**
* Create a new [PatchOption] with a long array value and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>> P.longArrayPatchOption(
key: String,
default: Array<Long>? = null,
values: Map<String, Array<Long>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: PatchOption<Array<Long>?>.(Array<Long>?) -> Boolean = { true },
) = registerNewPatchOption(
key,
default,
values,
title,
description,
required,
"LongArray",
validator,
)
/**
* Create a new [PatchOption] and add it to the current [Patch].
*
* @param key The identifier.
* @param default The default value.
* @param values The set of guaranteed valid values identified by their string representation.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param valueType The type of the option value (to handle type erasure).
* @param validator The function to validate the option value.
*
* @return The created [PatchOption].
*
* @see PatchOption
*/
fun <P : Patch<*>, T> P.registerNewPatchOption(
key: String,
default: T? = null,
values: Map<String, T?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
valueType: String,
validator: PatchOption<T>.(T?) -> Boolean = { true },
) = PatchOption(
key,
default,
values,
title,
description,
required,
valueType,
validator,
).also(options::register)
}
}

View file

@ -1,41 +0,0 @@
package app.revanced.patcher.patch.options
/**
* An exception thrown when using [PatchOption]s.
*
* @param errorMessage The exception message.
*/
sealed class PatchOptionException(errorMessage: String) : Exception(errorMessage, null) {
/**
* An exception thrown when a [PatchOption] is set to an invalid value.
*
* @param invalidType The type of the value that was passed.
* @param expectedType The type of the value that was expected.
*/
class InvalidValueTypeException(invalidType: String, expectedType: String) :
PatchOptionException("Type $expectedType was expected but received type $invalidType")
/**
* An exception thrown when a value did not satisfy the value conditions specified by the [PatchOption].
*
* @param value The value that failed validation.
*/
class ValueValidationException(value: Any?, option: PatchOption<*>) :
PatchOptionException("The option value \"$value\" failed validation for ${option.key}")
/**
* An exception thrown when a value is required but null was passed.
*
* @param option The [PatchOption] that requires a value.
*/
class ValueRequiredException(option: PatchOption<*>) :
PatchOptionException("The option ${option.key} requires a value, but null was passed")
/**
* An exception thrown when a [PatchOption] is not found.
*
* @param key The key of the [PatchOption].
*/
class PatchOptionNotFoundException(key: String) :
PatchOptionException("No option with key $key")
}

View file

@ -1,46 +0,0 @@
package app.revanced.patcher.patch.options
/**
* A map of [PatchOption]s associated by their keys.
*
* @param options The [PatchOption]s to initialize with.
*/
class PatchOptions internal constructor(
private val options: MutableMap<String, PatchOption<*>> = mutableMapOf(),
) : MutableMap<String, PatchOption<*>> by options {
/**
* Register a [PatchOption]. Acts like [MutableMap.put].
* @param value The [PatchOption] to register.
*/
fun register(value: PatchOption<*>) {
options[value.key] = value
}
/**
* Set an option's value.
* @param key The identifier.
* @param value The value.
* @throws PatchOptionException.PatchOptionNotFoundException If the option does not exist.
*/
operator fun <T : Any> set(
key: String,
value: T?,
) {
val option = this[key]
try {
@Suppress("UNCHECKED_CAST")
(option as PatchOption<T>).value = value
} catch (e: ClassCastException) {
throw PatchOptionException.InvalidValueTypeException(
value?.let { it::class.java.name } ?: "null",
option.value?.let { it::class.java.name } ?: "null",
)
}
}
/**
* Get an option.
*/
override operator fun get(key: String) = options[key] ?: throw PatchOptionException.PatchOptionNotFoundException(key)
}

View file

@ -1,7 +1,6 @@
package app.revanced.patcher.util package app.revanced.patcher.util
import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.extensions.or
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
import app.revanced.patcher.util.ClassMerger.Utils.filterAny import app.revanced.patcher.util.ClassMerger.Utils.filterAny
import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
@ -36,7 +35,7 @@ internal object ClassMerger {
*/ */
fun ClassDef.merge( fun ClassDef.merge(
otherClass: ClassDef, otherClass: ClassDef,
context: BytecodeContext, context: BytecodePatchContext,
) = this ) = this
// .fixFieldAccess(otherClass) // .fixFieldAccess(otherClass)
// .fixMethodAccess(otherClass) // .fixMethodAccess(otherClass)
@ -95,7 +94,7 @@ internal object ClassMerger {
*/ */
private fun ClassDef.publicize( private fun ClassDef.publicize(
reference: ClassDef, reference: ClassDef,
context: BytecodeContext, context: BytecodePatchContext,
) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) { ) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
this.asMutableClass().apply { this.asMutableClass().apply {
context.traverseClassHierarchy(this) { context.traverseClassHierarchy(this) {
@ -175,12 +174,12 @@ internal object ClassMerger {
* @param targetClass the class to start traversing the class hierarchy from * @param targetClass the class to start traversing the class hierarchy from
* @param callback function that is called for every class in the hierarchy * @param callback function that is called for every class in the hierarchy
*/ */
fun BytecodeContext.traverseClassHierarchy( fun BytecodePatchContext.traverseClassHierarchy(
targetClass: MutableClass, targetClass: MutableClass,
callback: MutableClass.() -> Unit, callback: MutableClass.() -> Unit,
) { ) {
callback(targetClass) callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { this.classByType(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback) traverseClassHierarchy(it, callback)
} }
} }
@ -199,7 +198,7 @@ internal object ClassMerger {
* *
* @return The new [AccessFlags]. * @return The new [AccessFlags].
*/ */
fun Int.toPublic() = this.or(AccessFlags.PUBLIC).and(AccessFlags.PRIVATE.value.inv()) fun Int.toPublic() = or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
/** /**
* Filter [this] on [needles] matching the given [predicate]. * Filter [this] on [needles] matching the given [predicate].

View file

@ -1,25 +0,0 @@
package app.revanced.patcher.util
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
@Deprecated("Use Document instead.")
class DomFileEditor : Closeable {
val file: Document
internal constructor(
inputStream: InputStream,
) {
file = Document(inputStream)
}
constructor(file: File) {
this.file = Document(file)
}
override fun close() {
file as app.revanced.patcher.util.Document
file.close()
}
}

View file

@ -0,0 +1,109 @@
@file:Suppress("unused")
package app.revanced.patcher.util
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.util.MethodNavigator.NavigateException
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
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.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* A navigator for methods.
*
* @param context The [BytecodePatchContext] to use.
* @param startMethod The [Method] to start navigating from.
*
* @constructor Creates a new [MethodNavigator].
*
* @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference.
*/
class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) {
private var lastNavigatedMethodReference = startMethod
private val lastNavigatedMethodInstructions get() = with(immutable()) {
instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.")
}
/**
* Navigate to the method at the specified index.
*
* @param index The index of the method to navigate to.
*
* @return This [MethodNavigator].
*/
fun at(vararg index: Int): MethodNavigator {
index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
}
return this
}
/**
* Navigate to the method at the specified index that matches the specified predicate.
*
* @param index The index of the method to navigate to.
* @param predicate The predicate to match.
*/
fun at(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index)
return this
}
/**
* Get the method reference at the specified index.
*
* @param index The index of the method reference to get.
*/
private fun Iterable<Instruction>.getMethodReferenceAt(index: Int): MethodReference {
val instruction = elementAt(index) as? ReferenceInstruction
?: throw NavigateException("Instruction at index $index is not a method reference.")
return instruction.reference as MethodReference
}
/**
* Get the last navigated method mutably.
*
* @return The last navigated method mutably.
*/
fun mutable() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
as MutableMethod
/**
* Get the last navigated method immutably.
*
* @return The last navigated method immutably.
*/
fun immutable() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/**
* Predicate to match the class defining the current method reference.
*/
private val matchesCurrentMethodReferenceDefiningClass = { classDef: ClassDef ->
classDef.type == lastNavigatedMethodReference.definingClass
}
/**
* Find the first [lastNavigatedMethodReference] in the class.
*/
private val ClassDef.firstMethodBySignature get() = methods.first {
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
}
/**
* An exception thrown when navigating fails.
*
* @param message The message of the exception.
*/
internal class NavigateException internal constructor(message: String) : Exception(message)
}

View file

@ -4,23 +4,18 @@ import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.ClassDef
/** /**
* A class that represents a set of classes and proxies. * A list of classes and proxies.
* *
* @param classes The classes to be backed by proxies. * @param classes The classes to be backed by proxies.
*/ */
class ProxyClassList internal constructor(classes: MutableSet<ClassDef>) : MutableSet<ClassDef> by classes { class ProxyClassList internal constructor(classes: MutableList<ClassDef>) : MutableList<ClassDef> by classes {
internal val proxies = mutableListOf<ClassProxy>() internal val proxyPool = mutableListOf<ClassProxy>()
/**
* Add a [ClassProxy].
*/
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
/** /**
* Replace all classes with their mutated versions. * Replace all classes with their mutated versions.
*/ */
internal fun replaceClasses() = internal fun replaceClasses() =
proxies.removeIf { proxy -> proxyPool.removeIf { proxy ->
// If the proxy is unused, return false to keep it in the proxies list. // If the proxy is unused, return false to keep it in the proxies list.
if (!proxy.resolved) return@removeIf false if (!proxy.resolved) return@removeIf false

View file

@ -1,60 +0,0 @@
package app.revanced.patcher.util.method
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil
/**
* Find a method from another method via instruction offsets.
* @param bytecodeContext The context to use when resolving the next method reference.
* @param currentMethod The method to start from.
*/
class MethodWalker internal constructor(
private val bytecodeContext: BytecodeContext,
private var currentMethod: Method,
) {
/**
* Get the method which was walked last.
*
* It is possible to cast this method to a [MutableMethod], if the method has been walked mutably.
*
* @return The method which was walked last.
*/
fun getMethod(): Method {
return currentMethod
}
/**
* Walk to a method defined at the offset in the instruction list of the current method.
*
* The current method will be mutable.
*
* @param offset The offset of the instruction. This instruction must be of format 35c.
* @param walkMutable If this is true, the class of the method will be resolved mutably.
* @return The same [MethodWalker] instance with the method at [offset].
*/
fun nextMethod(
offset: Int,
walkMutable: Boolean = false,
): MethodWalker {
currentMethod.implementation?.instructions?.let { instructions ->
val instruction = instructions.elementAt(offset)
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
currentMethod =
methods.first {
return@first MethodUtil.methodSignaturesMatch(it, newMethod)
}
return this
}
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")
}
internal class MethodNotFoundException(exception: String) : Exception(exception)
}

View file

@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.iface.ClassDef
* *
* A class proxy simply holds a reference to the original class * A class proxy simply holds a reference to the original class
* and allocates a mutable clone for the original class if needed. * and allocates a mutable clone for the original class if needed.
*
* @param immutableClass The class to proxy. * @param immutableClass The class to proxy.
*/ */
class ClassProxy internal constructor( class ClassProxy internal constructor(

View file

@ -1,5 +1,6 @@
package app.revanced.patcher.util.smali package app.revanced.patcher.util.smali
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes import com.android.tools.smali.dexlib2.Opcodes
@ -13,7 +14,7 @@ import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.Locale import java.util.*
private const val METHOD_TEMPLATE = """ private const val METHOD_TEMPLATE = """
.class LInlineCompiler; .class LInlineCompiler;
@ -64,7 +65,7 @@ class InlineSmaliCompiler {
val dexGen = smaliTreeWalker(treeStream) val dexGen = smaliTreeWalker(treeStream)
dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault())) dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = dexGen.smali_file() val classDef = dexGen.smali_file()
return classDef.methods.first().implementation!!.instructions.map { it as BuilderInstruction } return classDef.methods.first().instructions.map { it as BuilderInstruction }
} }
} }
} }

View file

@ -0,0 +1,236 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertDoesNotThrow
import java.util.logging.Logger
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
internal object PatcherTest {
private lateinit var patcher: Patcher
@BeforeEach
fun setUp() {
patcher = mockk<Patcher> {
// Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved.
setPrivateField(
"config",
mockk<PatcherConfig> {
every { resourceMode } returns ResourcePatchContext.ResourceMode.NONE
},
)
setPrivateField(
"logger",
Logger.getAnonymousLogger(),
)
every { context.bytecodeContext.classes } returns mockk(relaxed = true)
every { this@mockk() } answers { callOriginal() }
}
}
@Test
fun `executes patches in correct order`() {
val executed = mutableListOf<String>()
val patches = setOf(
bytecodePatch { execute { executed += "1" } },
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "2" }
finalize { executed += "-2" }
},
bytecodePatch { execute { executed += "3" } },
)
execute { executed += "4" }
finalize { executed += "-1" }
},
)
assert(executed.isEmpty())
patches()
assertEquals(
listOf("1", "2", "3", "4", "-1", "-2"),
executed,
"Expected patches to be executed in correct order.",
)
}
@Test
fun `handles execution of patches correctly when exceptions occur`() {
val executed = mutableListOf<String>()
infix fun Patch<*>.produces(equals: List<String>) {
val patches = setOf(this)
patches()
assertEquals(equals, executed, "Expected patches to be executed in correct order.")
executed.clear()
}
// No patches execute successfully,
// because the dependency patch throws an exception inside the execute block.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { throw PatchException("1") }
finalize { executed += "-2" }
},
)
execute { executed += "2" }
finalize { executed += "-1" }
} produces emptyList()
// The dependency patch is executed successfully,
// because only the dependant patch throws an exception inside the finalize block.
// Patches that depend on a failed patch should not be executed,
// but patches that are depended on by a failed patch should be executed.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { executed += "-2" }
},
)
execute { throw PatchException("2") }
finalize { executed += "-1" }
} produces listOf("1", "-2")
// Because the finalize block of the dependency patch is executed after the finalize block of the dependant patch,
// the dependant patch executes successfully, but the dependency patch raises an exception in the finalize block.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { throw PatchException("-2") }
},
)
execute { executed += "2" }
finalize { executed += "-1" }
} produces listOf("1", "2", "-1")
// The dependency patch is executed successfully,
// because the dependant patch raises an exception in the finalize block.
// Patches that depend on a failed patch should not be executed,
// but patches that are depended on by a failed patch should be executed.
bytecodePatch {
dependsOn(
bytecodePatch {
execute { executed += "1" }
finalize { executed += "-2" }
},
)
execute { executed += "2" }
finalize { throw PatchException("-1") }
} produces listOf("1", "2", "-2")
}
@Test
fun `throws if unmatched fingerprint match is delegated`() {
val patch = bytecodePatch {
// Fingerprint can never match.
val match by fingerprint { }
// Manually add the fingerprint.
app.revanced.patcher.fingerprint { }()
execute {
// Throws, because the fingerprint can't be matched.
match.patternMatch
}
}
assertEquals(2, patch.fingerprints.size)
assertTrue(
patch().exception != null,
"Expected an exception because the fingerprint can't match.",
)
}
@Test
fun `matches fingerprint`() {
mockClassWithMethod()
val patches = setOf(bytecodePatch { fingerprint { this returns "V" } })
assertNull(
patches.first().fingerprints.first().match,
"Expected fingerprint to be matched before execution.",
)
patches()
assertDoesNotThrow("Expected fingerprint to be matched.") {
assertEquals(
"V",
patches.first().fingerprints.first().match!!.method.returnType,
"Expected fingerprint to be matched.",
)
}
}
private operator fun Set<Patch<*>>.invoke(): List<PatchResult> {
every { patcher.context.executablePatches } returns toMutableSet()
return runBlocking { patcher().toList() }
}
private operator fun Patch<*>.invoke() = setOf(this)().first()
private fun Any.setPrivateField(field: String, value: Any) {
this::class.java.getDeclaredField(field).apply {
this.isAccessible = true
set(this@setPrivateField, value)
}
}
private fun mockClassWithMethod() {
every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
mutableListOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(
ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
null,
),
),
),
),
)
every { patcher.context.bytecodeContext.lookupMaps } returns LookupMaps(patcher.context.bytecodeContext.classes)
}
}

View file

@ -1,50 +0,0 @@
package app.revanced.patcher.extensions
import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
private object AnnotationExtensionsTest {
@Test
fun `find annotation in annotated class`() {
assertNotNull(TestClasses.Annotation2::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `find annotation`() {
assertNotNull(TestClasses.AnnotatedClass::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `find annotation recursively in super class`() {
assertNotNull(TestClasses.AnnotatedClass2::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `find annotation recursively in super class with annotation`() {
assertNotNull(TestClasses.AnnotatedTestClass3::class.findAnnotationRecursively(TestClasses.Annotation::class))
}
@Test
fun `don't find unknown annotation in annotated class`() {
assertNull(TestClasses.AnnotatedClass::class.findAnnotationRecursively(TestClasses.UnknownAnnotation::class))
}
object TestClasses {
annotation class Annotation
@Annotation
annotation class Annotation2
annotation class UnknownAnnotation
@Annotation
abstract class AnnotatedClass
@Annotation2
class AnnotatedTestClass3
abstract class AnnotatedClass2 : AnnotatedClass()
}
}

View file

@ -1,28 +0,0 @@
package app.revanced.patcher.patch
import app.revanced.patcher.data.ResourceContext
import kotlin.test.Test
import app.revanced.patcher.patch.annotation.Patch as PatchAnnotation
object PatchInitializationTest {
@Test
fun `initialize using constructor`() {
val patch =
object : RawResourcePatch(name = "Resource patch test") {
override fun execute(context: ResourceContext) {}
}
assert(patch.name == "Resource patch test")
}
@Test
fun `initialize using annotation`() {
val patch =
@PatchAnnotation("Resource patch test")
object : RawResourcePatch() {
override fun execute(context: ResourceContext) {}
}
assert(patch.name == "Resource patch test")
}
}

View file

@ -0,0 +1,90 @@
@file:Suppress("unused")
package app.revanced.patcher.patch
import org.junit.jupiter.api.Test
import java.io.File
import kotlin.reflect.KFunction
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredFunctions
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import kotlin.test.assertEquals
// region Test patches.
// Not loaded, because it's unnamed.
val publicUnnamedPatch = bytecodePatch {
}
// Loaded, because it's named.
val publicPatch = bytecodePatch("Public") {
}
// Not loaded, because it's private.
private val privateUnnamedPatch = bytecodePatch {
}
// Not loaded, because it's private.
private val privatePatch = bytecodePatch("Private") {
}
// Not loaded, because it's unnamed.
fun publicUnnamedPatchFunction() = publicUnnamedPatch
// Loaded, because it's named.
fun publicNamedPatchFunction() = bytecodePatch("Public") { }
// Not loaded, because it's parameterized.
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) = publicNamedPatchFunction()
// Not loaded, because it's private.
private fun privateUnnamedPatchFunction() = privateUnnamedPatch
// Not loaded, because it's private.
private fun privateNamedPatchFunction() = privatePatch
// endregion
internal object PatchLoaderTest {
private const val LOAD_PATCHES_FUNCTION_NAME = "loadPatches"
private val TEST_PATCHES_CLASS = ::publicPatch.javaField!!.declaringClass.name
private val TEST_PATCHES_CLASS_LOADER = ::publicPatch.javaClass.classLoader
@Test
fun `loads patches correctly`() {
// Get instance of private PatchLoader.Companion class.
val patchLoaderCompanionObject = getPrivateFieldByType(
PatchLoader::class.java,
PatchLoader::class.companionObject!!.javaObjectType,
)
// Get private PatchLoader.Companion.loadPatches function from PatchLoader.Companion.
@Suppress("UNCHECKED_CAST")
val loadPatchesFunction = getPrivateFunctionByName(
patchLoaderCompanionObject,
LOAD_PATCHES_FUNCTION_NAME,
) as KFunction<Map<File, Set<Patch<*>>>>
// Call private PatchLoader.Companion.loadPatches function.
val patches = loadPatchesFunction.call(
patchLoaderCompanionObject,
TEST_PATCHES_CLASS_LOADER,
mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)),
).values.first()
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
private fun getPrivateFieldByType(cls: Class<*>, fieldType: Class<*>) =
cls.declaredFields.first { it.type == fieldType }.apply { isAccessible = true }.get(null)
private fun getPrivateFunctionByName(obj: Any, @Suppress("SameParameterValue") methodName: String) =
obj::class.declaredFunctions.first { it.name == methodName }.apply { isAccessible = true }
}

View file

@ -0,0 +1,67 @@
package app.revanced.patcher.patch
import app.revanced.patcher.fingerprint
import kotlin.test.Test
import kotlin.test.assertEquals
internal object PatchTest {
@Test
fun `can create patch with name`() {
val patch = bytecodePatch(name = "Test") {}
assertEquals("Test", patch.name)
}
@Test
fun `can create patch with compatible packages`() {
val patch = bytecodePatch(name = "Test") {
compatibleWith(
"compatible.package"("1.0.0"),
)
}
assertEquals(1, patch.compatiblePackages!!.size)
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
}
@Test
fun `can create patch with fingerprints`() {
val externalFingerprint = fingerprint {}
val patch = bytecodePatch(name = "Test") {
val externalFingerprintMatch by externalFingerprint()
val internalFingerprintMatch by fingerprint {}
execute {
externalFingerprintMatch.method
internalFingerprintMatch.method
}
}
assertEquals(2, patch.fingerprints.size)
}
@Test
fun `can create patch with dependencies`() {
val patch = bytecodePatch(name = "Test") {
dependsOn(resourcePatch {})
}
assertEquals(1, patch.dependencies.size)
}
@Test
fun `can create patch with options`() {
val patch = bytecodePatch(name = "Test") {
val print by stringOption("print")
val custom = option<String>("custom")()
execute {
println(print)
println(custom.value)
}
}
assertEquals(2, patch.options.size)
}
}

View file

@ -0,0 +1,128 @@
package app.revanced.patcher.patch.options
import app.revanced.patcher.patch.*
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.reflect.typeOf
import kotlin.test.*
internal object OptionsTest {
private val optionsTestPatch = bytecodePatch {
booleanOption("bool", true)
stringOption("required", "default", required = true)
stringsOption("list", listOf("1", "2"))
stringOption("choices", "value", values = mapOf("Valid option value" to "valid"))
stringOption("validated", "default") { it == "valid" }
stringOption("resettable", null, required = true)
}
@Test
fun `should not fail because default value is unvalidated`() = options {
assertDoesNotThrow { get("required") }
}
@Test
fun `should not allow setting custom value with validation`() = options {
// Getter validation on incorrect value.
assertThrows<OptionException.ValueValidationException> {
set("validated", get("validated"))
}
// Setter validation on incorrect value.
assertThrows<OptionException.ValueValidationException> {
set("validated", "invalid")
}
// Setter validation on correct value.
assertDoesNotThrow {
set("validated", "valid")
}
}
@Test
fun `should throw due to incorrect type`() = options {
assertThrows<OptionException.InvalidValueTypeException> {
set("bool", "not a boolean")
}
}
@Test
fun `should be nullable`() = options {
assertDoesNotThrow {
set("bool", null)
}
}
@Test
fun `option should not be found`() = options {
assertThrows<OptionException.OptionNotFoundException> {
set("this option does not exist", 1)
}
}
@Test
fun `should be able to add options manually`() = options {
assertDoesNotThrow {
bytecodePatch {
get("list")()
}.options["list"]
}
}
@Test
fun `should allow setting value from values`() = options {
@Suppress("UNCHECKED_CAST")
val option = get("choices") as Option<String>
option.value = option.values!!.values.last()
assertTrue(option.value == "valid")
}
@Test
fun `should allow setting custom value`() = options {
assertDoesNotThrow {
set("choices", "unknown")
}
}
@Test
fun `should allow resetting value`() = options {
assertDoesNotThrow {
set("choices", null)
}
assert(get("choices").value == null)
}
@Test
fun `reset should not fail`() = options {
assertDoesNotThrow {
set("resettable", "test")
get("resettable").reset()
}
assertThrows<OptionException.ValueRequiredException> {
get("resettable").value
}
}
@Test
fun `option types should be known`() = options {
assertEquals(typeOf<List<String>>(), get("list").type)
}
@Test
fun `getting default value should work`() = options {
assertDoesNotThrow {
assertNull(get("resettable").default)
}
}
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
}

View file

@ -1,127 +0,0 @@
package app.revanced.patcher.patch.options
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringArrayPatchOption
import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertNull
import kotlin.test.assertTrue
internal class PatchOptionsTest {
@Test
fun `should not fail because default value is unvalidated`() {
assertDoesNotThrow { OptionsTestPatch.requiredStringOption }
}
@Test
fun `should not allow setting custom value with validation`() {
// Getter validation on incorrect value.
assertThrows<PatchOptionException.ValueValidationException> { OptionsTestPatch.validatedOption }
// Setter validation on incorrect value.
assertThrows<PatchOptionException.ValueValidationException> { OptionsTestPatch.validatedOption = "invalid" }
// Setter validation on correct value.
assertDoesNotThrow { OptionsTestPatch.validatedOption = "valid" }
}
@Test
fun `should throw due to incorrect type`() {
assertThrows<PatchOptionException.InvalidValueTypeException> {
OptionsTestPatch.options["bool"] = "not a boolean"
}
}
@Test
fun `should be nullable`() {
OptionsTestPatch.booleanOption = null
}
@Test
fun `option should not be found`() {
assertThrows<PatchOptionException.PatchOptionNotFoundException> {
OptionsTestPatch.options["this option does not exist"] = 1
}
}
@Test
fun `should be able to add options manually`() {
assertThrows<PatchOptionException.InvalidValueTypeException> {
OptionsTestPatch.options["array"] = OptionsTestPatch.stringArrayOption
}
assertDoesNotThrow {
OptionsTestPatch.options.register(OptionsTestPatch.stringArrayOption)
}
}
@Suppress("UNCHECKED_CAST")
@Test
fun `should allow setting value from values`() =
with(OptionsTestPatch.options["choices"] as PatchOption<String>) {
value = values!!.values.last()
assertTrue(value == "valid")
}
@Test
fun `should allow setting custom value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = "unknown" }
@Test
fun `should allow resetting value`() = assertDoesNotThrow { OptionsTestPatch.stringOptionWithChoices = null }
@Test
fun `reset should not fail`() {
assertDoesNotThrow {
OptionsTestPatch.resettableOption.value = "test"
OptionsTestPatch.resettableOption.reset()
}
assertThrows<PatchOptionException.ValueRequiredException> {
OptionsTestPatch.resettableOption.value
}
}
@Test
fun `option types should be known`() = assertTrue(OptionsTestPatch.options["array"].valueType == "StringArray")
@Test
fun `getting default value should work`() = assertDoesNotThrow { assertNull(OptionsTestPatch.resettableOption.default) }
@Suppress("DEPRECATION")
private object OptionsTestPatch : BytecodePatch() {
var booleanOption by booleanPatchOption(
"bool",
true,
)
var requiredStringOption by stringPatchOption(
"required",
"default",
required = true,
)
var stringArrayOption =
stringArrayPatchOption(
"array",
arrayOf("1", "2"),
)
var stringOptionWithChoices by stringPatchOption(
"choices",
"value",
values = mapOf("Valid option value" to "valid"),
)
var validatedOption by stringPatchOption(
"validated",
"default",
) { it == "valid" }
var resettableOption =
stringPatchOption(
"resettable",
null,
required = true,
)
override fun execute(context: BytecodeContext) {}
}
}

View file

@ -1,148 +0,0 @@
package app.revanced.patcher.patch.usage
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.extensions.or
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.annotation.CompatiblePackage
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Format
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction11x
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c
import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableFieldReference
import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference
import com.android.tools.smali.dexlib2.immutable.value.ImmutableFieldEncodedValue
import com.android.tools.smali.dexlib2.util.Preconditions
import com.google.common.collect.ImmutableList
@Suppress("unused")
@Patch(
name = "Example bytecode patch",
description = "Example demonstration of a bytecode patch.",
dependencies = [ExampleResourcePatch::class],
compatiblePackages = [CompatiblePackage("com.example.examplePackage", arrayOf("0.0.1", "0.0.2"))],
)
object ExampleBytecodePatch : BytecodePatch(setOf(ExampleFingerprint)) {
// Entry point of a patch. Supplied fingerprints are resolved at this point.
override fun execute(context: BytecodeContext) {
ExampleFingerprint.result?.let { result ->
// Let's modify it, so it prints "Hello, ReVanced! Editing bytecode."
// Get the start index of our opcode pattern.
// This will be the index of the instruction with the opcode CONST_STRING.
val startIndex = result.scanResult.patternScanResult!!.startIndex
result.mutableMethod.apply {
replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
// Store the fields initial value into the first virtual register.
replaceInstruction(0, "sget-object v0, LTestClass;->dummyField:Ljava/io/PrintStream;")
// Now let's create a new call to our method and print the return value!
// You can also use the smali compiler to create instructions.
// For this sake of example I reuse the TestClass field dummyField inside the virtual register 0.
//
// Control flow instructions are not supported as of now.
addInstructionsWithLabels(
startIndex + 2,
"""
invoke-static { }, LTestClass;->returnHello()Ljava/lang/String;
move-result-object v1
invoke-virtual { v0, v1 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
""",
)
}
// Find the class in which the method matching our fingerprint is defined in.
context.findClass(result.classDef.type)!!.mutableClass.apply {
// Add a new method that returns a string.
methods.add(
ImmutableMethod(
result.classDef.type,
"returnHello",
null,
"Ljava/lang/String;",
AccessFlags.PRIVATE or AccessFlags.STATIC,
null,
null,
ImmutableMethodImplementation(
1,
ImmutableList.of(
BuilderInstruction21c(
Opcode.CONST_STRING,
0,
ImmutableStringReference("Hello, ReVanced! Adding bytecode."),
),
BuilderInstruction11x(Opcode.RETURN_OBJECT, 0),
),
null,
null,
),
).toMutable(),
)
// Add a field in the main class.
// We will use this field in our method below to call println on.
// The field holds the Ljava/io/PrintStream->out; field.
fields.add(
ImmutableField(
type,
"dummyField",
"Ljava/io/PrintStream;",
AccessFlags.PRIVATE or AccessFlags.STATIC,
ImmutableFieldEncodedValue(
ImmutableFieldReference(
"Ljava/lang/System;",
"out",
"Ljava/io/PrintStream;",
),
),
null,
null,
).toMutable(),
)
}
} ?: throw PatchException("Fingerprint failed to resolve.")
}
/**
* Replace an existing instruction with a new one containing a reference to a new string.
* @param index The index of the instruction to replace.
* @param string The replacement string.
*/
private fun MutableMethod.replaceStringAt(
index: Int,
string: String,
) {
val instruction = getInstruction(index)
// Utility method of dexlib2.
Preconditions.checkFormat(instruction.opcode, Format.Format21c)
// Cast this to an instruction of the format 21c.
// The instruction format can be found in the docs at
// https://source.android.com/devices/tech/dalvik/dalvik-bytecode
val strInstruction = instruction as Instruction21c
// In our case we want an instruction with the opcode CONST_STRING
// The format is 21c, so we create a new BuilderInstruction21c
// This instruction will hold the string reference constant in the virtual register of the original instruction
// For that a reference to the string is needed. It can be created with an ImmutableStringReference.
// At last, use the method replaceInstruction to replace it at the given index startIndex.
replaceInstruction(
index,
"const-string ${strInstruction.registerA}, ${ImmutableStringReference(string)}",
)
}
}

View file

@ -1,20 +0,0 @@
package app.revanced.patcher.patch.usage
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@FuzzyPatternScanMethod(2)
object ExampleFingerprint : MethodFingerprint(
"V",
AccessFlags.PUBLIC or AccessFlags.STATIC,
listOf("[L"),
listOf(
Opcode.SGET_OBJECT,
null, // Matching unknown opcodes.
Opcode.INVOKE_STATIC, // This is intentionally wrong to test fuzzy matching.
Opcode.RETURN_VOID,
),
null,
)

View file

@ -1,14 +0,0 @@
package app.revanced.patcher.patch.usage
import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.ResourcePatch
import org.w3c.dom.Element
class ExampleResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
context.document["AndroidManifest.xml"].use { document ->
val element = document.getElementsByTagName("application").item(0) as Element
element.setAttribute("exampleAttribute", "exampleValue")
}
}
}

View file

@ -18,23 +18,24 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
internal class InlineSmaliCompilerTest { internal object InlineSmaliCompilerTest {
@Test @Test
fun `compiler should output valid instruction`() { fun `outputs valid instruction`() {
val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction
val have = "const-string v0, \"Test\"".toInstruction() val have = "const-string v0, \"Test\"".toInstruction()
instructionEquals(want, have)
assertInstructionsEqual(want, have)
} }
@Test @Test
fun `compiler should support branching with own branches`() { fun `supports branching with own branches`() {
val method = createMethod() val method = createMethod()
val insnAmount = 8 val instructionCount = 8
val insnIndex = insnAmount - 2 val instructionIndex = instructionCount - 2
val targetIndex = insnIndex - 1 val targetIndex = instructionIndex - 1
method.addInstructions( method.addInstructions(
arrayOfNulls<String>(insnAmount).also { arrayOfNulls<String>(instructionCount).also {
Arrays.fill(it, "const/4 v0, 0x0") Arrays.fill(it, "const/4 v0, 0x0")
}.joinToString("\n"), }.joinToString("\n"),
) )
@ -47,14 +48,15 @@ internal class InlineSmaliCompilerTest {
""", """,
) )
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex) val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertEquals(targetIndex, insn.target.location.index)
assertEquals(targetIndex, instruction.target.location.index)
} }
@Test @Test
fun `compiler should support branching to outside branches`() { fun `supports branching to outside branches`() {
val method = createMethod() val method = createMethod()
val insnIndex = 3 val instructionIndex = 3
val labelIndex = 1 val labelIndex = 1
method.addInstructions( method.addInstructions(
@ -76,35 +78,30 @@ internal class InlineSmaliCompilerTest {
ExternalLabel("test", method.getInstruction(1)), ExternalLabel("test", method.getInstruction(1)),
) )
val insn = method.getInstruction<BuilderInstruction21t>(insnIndex) val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertTrue(insn.target.isPlaced, "Label was not placed") assertTrue(instruction.target.isPlaced, "Label was not placed")
assertEquals(labelIndex, insn.target.location.index) assertEquals(labelIndex, instruction.target.location.index)
} }
companion object { private fun createMethod(
private fun createMethod( name: String = "dummy",
name: String = "dummy", returnType: String = "V",
returnType: String = "V", accessFlags: Int = AccessFlags.STATIC.value,
accessFlags: Int = AccessFlags.STATIC.value, registerCount: Int = 1,
registerCount: Int = 1, ) = ImmutableMethod(
) = ImmutableMethod( "Ldummy;",
"Ldummy;", name,
name, emptyList(), // parameters
emptyList(), // parameters returnType,
returnType, accessFlags,
accessFlags, emptySet(),
emptySet(), emptySet(),
emptySet(), MutableMethodImplementation(registerCount),
MutableMethodImplementation(registerCount), ).toMutable()
).toMutable()
private fun instructionEquals( private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) {
want: BuilderInstruction, assertEquals(want.opcode, have.opcode)
have: BuilderInstruction, assertEquals(want.format, have.format)
) { assertEquals(want.codeUnits, have.codeUnits)
assertEquals(want.opcode, have.opcode)
assertEquals(want.format, have.format)
assertEquals(want.codeUnits, have.codeUnits)
}
} }
} }