diff --git a/api/revanced-patcher.api b/api/revanced-patcher.api index 40c862ab..77c31d31 100644 --- a/api/revanced-patcher.api +++ b/api/revanced-patcher.api @@ -1,62 +1,65 @@ -public abstract interface class app/revanced/patcher/IntegrationsConsumer { - public abstract fun acceptIntegrations (Ljava/util/List;)V - public abstract fun acceptIntegrations (Ljava/util/Set;)V +public final class app/revanced/patcher/Fingerprint { + public final fun getMatch ()Lapp/revanced/patcher/Match; + public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z + public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z } -public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation { +public final class app/revanced/patcher/FingerprintBuilder { + public fun ()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/PackageMetadata { - public final fun getPackageName ()Ljava/lang/String; - public final fun getPackageVersion ()Ljava/lang/String; +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 class app/revanced/patcher/PatchBundleLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker { - public synthetic fun (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 abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation { } -public final class app/revanced/patcher/PatchBundleLoader$Dex : app/revanced/patcher/PatchBundleLoader { - public fun ([Ljava/io/File;)V - public fun ([Ljava/io/File;Ljava/io/File;)V - public synthetic fun ([Ljava/io/File;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class app/revanced/patcher/Match { + public fun (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/PatchBundleLoader$Jar : app/revanced/patcher/PatchBundleLoader { - public fun ([Ljava/io/File;)V +public final class app/revanced/patcher/Match$PatternMatch { + public fun (II)V + public final fun getEndIndex ()I + public final fun getStartIndex ()I } -public abstract interface class app/revanced/patcher/PatchExecutorFunction : java/util/function/Function { +public final class app/revanced/patcher/Match$StringMatch { + public fun (Ljava/lang/String;I)V + public final fun getIndex ()I + public final fun getString ()Ljava/lang/String; } -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 final class app/revanced/patcher/PackageMetadata { + public final fun getPackageName ()Ljava/lang/String; + public final fun getPackageVersion ()Ljava/lang/String; +} + +public final class app/revanced/patcher/Patcher : java/io/Closeable { public fun (Lapp/revanced/patcher/PatcherConfig;)V - public fun (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 get ()Lapp/revanced/patcher/PatcherResult; - public synthetic fun get ()Ljava/lang/Object; + public final fun get ()Lapp/revanced/patcher/PatcherResult; 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 { @@ -68,45 +71,12 @@ public final class app/revanced/patcher/PatcherContext { public final fun getPackageMetadata ()Lapp/revanced/patcher/PackageMetadata; } -public abstract class app/revanced/patcher/PatcherException : java/lang/Exception { - public synthetic fun (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (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 (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (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 fun (Ljava/util/List;Ljava/io/File;Ljava/util/List;)V - public synthetic fun (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 getDoNotCompress ()Ljava/util/List; - public final fun getResourceFile ()Ljava/io/File; 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 fun (Ljava/lang/String;Ljava/io/InputStream;)V public final fun getName ()Ljava/lang/String; 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 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 (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 (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 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 { @@ -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 (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/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 (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 removeInstructions (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)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 class app/revanced/patcher/extensions/MethodFingerprintExtensions { - public static final field INSTANCE Lapp/revanced/patcher/extensions/MethodFingerprintExtensions; - public final fun getFuzzyPatternScanMethod (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod; +public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { + public final fun getExtension ()Ljava/io/InputStream; + public final fun getFingerprints ()Ljava/util/Set; + public fun toString ()Ljava/lang/String; } -public abstract class app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field Companion Lapp/revanced/patcher/fingerprint/MethodFingerprint$Companion; - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (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 getFuzzyPatternScanMethod ()Lapp/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod; - 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/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder { + public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch; + public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder; + public final fun getExtension ()Ljava/io/InputStream; + public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint; + public final fun setExtension (Ljava/io/InputStream;)V } -public final class app/revanced/patcher/fingerprint/MethodFingerprint$Companion { - public final fun resolve (Ljava/lang/Iterable;Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/Iterable;)V +public final class app/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint { + public fun (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 fun (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 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 getScanResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult; +public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext { + public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy; + public final fun classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy; + public synthetic fun get ()Ljava/lang/Object; + public fun get ()Ljava/util/Set; + public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList; + 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 fun (Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult;Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult;)V - public final fun getPatternScanResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult; - public final fun getStringsScanResult ()Lapp/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult; +public final class app/revanced/patcher/patch/Option { + public fun (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (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 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 fun (IILjava/util/List;)V - public synthetic fun (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 abstract class app/revanced/patcher/patch/OptionException : java/lang/Exception { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } -public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$PatternScanResult$Warning { - public fun (Lcom/android/tools/smali/dexlib2/Opcode;Lcom/android/tools/smali/dexlib2/Opcode;II)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/patch/OptionException$InvalidValueTypeException : app/revanced/patcher/patch/OptionException { + public fun (Ljava/lang/String;Ljava/lang/String;)V } -public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult { - public fun (Ljava/util/List;)V - public final fun getMatches ()Ljava/util/List; +public final class app/revanced/patcher/patch/OptionException$OptionNotFoundException : app/revanced/patcher/patch/OptionException { + public fun (Ljava/lang/String;)V } -public final class app/revanced/patcher/fingerprint/MethodFingerprintResult$MethodFingerprintScanResult$StringsScanResult$StringMatch { - public fun (Ljava/lang/String;I)V - public final fun getIndex ()I - public final fun getString ()Ljava/lang/String; +public final class app/revanced/patcher/patch/OptionException$ValueRequiredException : app/revanced/patcher/patch/OptionException { + public fun (Lapp/revanced/patcher/patch/Option;)V } -public abstract interface annotation class app/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod : java/lang/annotation/Annotation { - public abstract fun threshold ()I +public final class app/revanced/patcher/patch/OptionException$ValueValidationException : app/revanced/patcher/patch/OptionException { + public fun (Ljava/lang/Object;Lapp/revanced/patcher/patch/Option;)V } -public abstract class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLjava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLjava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/util/Set;)V - public synthetic fun (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class app/revanced/patcher/patch/OptionKt { + 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 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 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 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 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 synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public abstract fun execute (Lapp/revanced/patcher/data/Context;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V + public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V public final fun getCompatiblePackages ()Ljava/util/Set; public final fun getDependencies ()Ljava/util/Set; public final fun getDescription ()Ljava/lang/String; public final fun getName ()Ljava/lang/String; - public final fun getOptions ()Lapp/revanced/patcher/patch/options/PatchOptions; - public final fun getRequiresIntegrations ()Z + public final fun getOptions ()Lapp/revanced/patcher/patch/Options; public final fun getUse ()Z - public fun hashCode ()I public fun toString ()Ljava/lang/String; } -public final class app/revanced/patcher/patch/Patch$CompatiblePackage { - public fun (Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getName ()Ljava/lang/String; - public final fun getVersions ()Ljava/util/Set; +public abstract class app/revanced/patcher/patch/PatchBuilder { + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun compatibleWith ([Ljava/lang/String;)V + public final fun compatibleWith ([Lkotlin/Pair;)V + public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V + public final fun execute (Lkotlin/jvm/functions/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 { @@ -304,128 +311,83 @@ public final class app/revanced/patcher/patch/PatchException : java/lang/Excepti public fun (Ljava/lang/Throwable;)V } -public final class app/revanced/patcher/patch/PatchResult { - public final fun getException ()Lapp/revanced/patcher/patch/PatchException; - public final fun getPatch ()Lapp/revanced/patcher/patch/Patch; +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/RawResourcePatch : app/revanced/patcher/patch/Patch { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V +public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker { + public synthetic fun (Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (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 abstract class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader { + public fun (Ljava/util/Set;Ljava/io/File;)V + public synthetic fun (Ljava/util/Set;Ljava/io/File;ILkotlin/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 final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader { + public fun (Ljava/util/Set;)V } -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 final class app/revanced/patcher/patch/PatchResult { + public final fun getException ()Lapp/revanced/patcher/patch/PatchException; + public final fun getPatch ()Lapp/revanced/patcher/patch/Patch; } -public class app/revanced/patcher/patch/options/PatchOption { - public static final field PatchExtensions Lapp/revanced/patcher/patch/options/PatchOption$PatchExtensions; - public fun (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 final class app/revanced/patcher/patch/RawResourcePatch : app/revanced/patcher/patch/Patch { public fun toString ()Ljava/lang/String; } -public final class app/revanced/patcher/patch/options/PatchOption$PatchExtensions { - 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 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 synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class app/revanced/patcher/patch/options/PatchOptionException$InvalidValueTypeException : app/revanced/patcher/patch/options/PatchOptionException { - public fun (Ljava/lang/String;Ljava/lang/String;)V +public final class app/revanced/patcher/patch/RawResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder { + 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 fun (Ljava/lang/String;)V +public final class app/revanced/patcher/patch/ResourcePatch : app/revanced/patcher/patch/Patch { + public fun toString ()Ljava/lang/String; } -public final class app/revanced/patcher/patch/options/PatchOptionException$ValueRequiredException : app/revanced/patcher/patch/options/PatchOptionException { - public fun (Lapp/revanced/patcher/patch/options/PatchOption;)V +public final class app/revanced/patcher/patch/ResourcePatchBuilder : app/revanced/patcher/patch/PatchBuilder { + public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch; } -public final class app/revanced/patcher/patch/options/PatchOptionException$ValueValidationException : app/revanced/patcher/patch/options/PatchOptionException { - public fun (Ljava/lang/Object;Lapp/revanced/patcher/patch/options/PatchOption;)V +public final class app/revanced/patcher/patch/ResourcePatchContext : app/revanced/patcher/patch/PatchContext { + 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/PatchOptions : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { - public fun ()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/patch/ResourcePatchContext$DocumentOperatable { + public fun (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/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 final class app/revanced/patcher/util/DomFileEditor : java/io/Closeable { - public fun (Ljava/io/File;)V - public fun close ()V - public final fun getFile ()Lorg/w3c/dom/Document; +public final class app/revanced/patcher/util/MethodNavigator { + public final fun at (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator; + public final fun at ([I)Lapp/revanced/patcher/util/MethodNavigator; + 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 fun add (Lapp/revanced/patcher/util/proxy/ClassProxy;)Z +public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList { + public fun add (ILcom/android/tools/smali/dexlib2/iface/ClassDef;)V + public synthetic fun add (ILjava/lang/Object;)V public fun add (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z public synthetic fun add (Ljava/lang/Object;)Z + public fun addAll (ILjava/util/Collection;)Z public fun addAll (Ljava/util/Collection;)Z public fun clear ()V public fun contains (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z public final fun contains (Ljava/lang/Object;)Z public fun containsAll (Ljava/util/Collection;)Z + public fun get (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public synthetic fun get (I)Ljava/lang/Object; public fun getSize ()I + public fun indexOf (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)I + public final fun indexOf (Ljava/lang/Object;)I public fun isEmpty ()Z public fun iterator ()Ljava/util/Iterator; + public fun lastIndexOf (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)I + public final fun lastIndexOf (Ljava/lang/Object;)I + public fun listIterator ()Ljava/util/ListIterator; + public fun listIterator (I)Ljava/util/ListIterator; + public final fun remove (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public synthetic fun remove (I)Ljava/lang/Object; public fun remove (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z public final fun remove (Ljava/lang/Object;)Z public fun removeAll (Ljava/util/Collection;)Z + public fun removeAt (I)Lcom/android/tools/smali/dexlib2/iface/ClassDef; public fun retainAll (Ljava/util/Collection;)Z + public fun set (ILcom/android/tools/smali/dexlib2/iface/ClassDef;)Lcom/android/tools/smali/dexlib2/iface/ClassDef; + public synthetic fun set (ILjava/lang/Object;)Ljava/lang/Object; public final fun size ()I + public fun subList (II)Ljava/util/List; public fun toArray ()[Ljava/lang/Object; public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; } -public final class app/revanced/patcher/util/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 fun getImmutableClass ()Lcom/android/tools/smali/dexlib2/iface/ClassDef; public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; diff --git a/build.gradle.kts b/build.gradle.kts index b02892c7..28a3cbf5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,6 @@ tasks { repositories { mavenCentral() - mavenLocal() google() maven { // 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(group = "xerces", module = "xmlParserAPIs") } - testImplementation(libs.kotlin.test) + testImplementation(libs.mockk) } kotlin { diff --git a/docs/1_patcher_intro.md b/docs/1_patcher_intro.md index 983d7582..8664236c 100644 --- a/docs/1_patcher_intro.md +++ b/docs/1_patcher_intro.md @@ -60,40 +60,43 @@ # 💉 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 -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 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. +ReVanced Patcher is a library that allows modifying Android apps by applying patches. +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. -Later on, you will learn how to create patches. +ReVanced Patcher receives a list of patches 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. + +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 - // Executed patches do not necessarily reset their state. - // 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> */ = PatchBundleLoader.Jar(File("revanced-patches.jar")) -val integrations = setOf(File("integrations.apk")) +val patches = loadPatchesFromJar(setOf(File("revanced-patches.rvp"))) + +val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { patcher -> + // Here you can access metadata about the APK file through patcher.context.packageMetadata + // such as package name, version code, version name, etc. -// Instantiating the patcher will decode the manifest of the APK file to read the package and version name. -val patcherConfig = PatcherConfig(apkFile = File("some.apk")) -val patcherResult = Patcher(patcherConfig).use { patcher -> - patcher.apply { - acceptIntegrations(integrations) - acceptPatches(patches) + // Add patches. + patcher += patches - // Execute patches. - runBlocking { - patcher.apply(returnOnError = false).collect { patchResult -> - if (patchResult.exception != null) - println("${patchResult.patchName} failed:\n${patchResult.exception}") - else - println("${patchResult.patchName} succeeded") - } + // Execute the patches. + runBlocking { + patcher().collect { patchResult -> + 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. diff --git a/docs/2_1_setup.md b/docs/2_1_setup.md index f4b8fb65..b333f9d0 100644 --- a/docs/2_1_setup.md +++ b/docs/2_1_setup.md @@ -98,7 +98,8 @@ Throughout the documentation, [ReVanced Patches](https://github.com/revanced/rev 3. Open the project in your IDE > [!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 diff --git a/docs/2_2_1_fingerprinting.md b/docs/2_2_1_fingerprinting.md index 5ce35068..b93372db 100644 --- a/docs/2_2_1_fingerprinting.md +++ b/docs/2_2_1_fingerprinting.md @@ -60,9 +60,10 @@ # 🔎 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. -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 @@ -72,14 +73,14 @@ Throughout the documentation, the following example will be used to demonstrate package app.revanced.patches.ads.fingerprints -object ShowAdsFingerprint : MethodFingerprint( - returnType = "Z", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Z"), - opcodes = listOf(Opcode.RETURN), - strings = listOf("pro"), - customFingerprint = { (methodDef, classDef) -> methodDef.definingClass == "Lcom/some/app/ads/AdsLoader;" } -) +fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters("Z") + opcodes(Opcode.RETURN) + strings("pro") + custom { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;" } +} ``` ## 🔎 Reconstructing the original code from a fingerprint @@ -91,22 +92,22 @@ The fingerprint contains the following information: - Method signature: ```kt - returnType = "Z", - access = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Z"), + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters("Z") ``` - Method implementation: ```kt - opcodes = listOf(Opcode.RETURN) - strings = listOf("pro"), + opcodes(Opcode.RETURN) + strings("pro") ``` - Package and class name: ```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: @@ -129,56 +130,78 @@ With this information, the original code can be reconstructed: > [!TIP] > A fingerprint should contain information about a method likely to remain the same across updates. -> A method's name is not included in the fingerprint because it 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 -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 -object DisableAdsPatch : BytecodePatch( - setOf(ShowAdsFingerprint) -) { +val fingerprint = fingerprint { // ... - } -``` +} -> [!NOTE] -> Fingerprints passed to the constructor of `BytecodePatch` are resolved by ReVanced Patcher before the patch is executed. +val patch = bytecodePatch { + // Directly create and add a fingerprint. + fingerprint { + // ... + } -> [!TIP] -> Multiple patches can share fingerprints. If a fingerprint is resolved once, it will not be resolved again. + // Add a fingerprint manually by invoking it. + fingerprint() +} +``` > [!TIP] -> 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`. +> Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again. -Once the fingerprint is resolved, the result can be used in the patch: +> [!TIP] +> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode` +> function to fuzzy match the pattern. +> `null` can be used as a wildcard to match any opcode: +> +> ```kt +> fingerprint(fuzzyPatternScanThreshhold = 2) { +> opcodes( +> Opcode.ICONST_0, +> null, +> Opcode.ICONST_1, +> Opcode.IRETURN, +> ) +>} +> ``` + +Once the fingerprint is matched, the match can be used in the patch: ```kt -object DisableAdsPatch : BytecodePatch( - setOf(ShowAdsFingerprint) -) { - override fun execute(context: BytecodeContext) { - val result = ShowAdsFingerprint.result - ?: throw PatchException("ShowAdsFingerprint not found") - +val patch = bytecodePatch { + // Add a fingerprint and delegate its match to a variable. + val match by showAdsFingerprint() + val match2 by fingerprint { // ... } + + execute { + val method = match.method + val method2 = match2.method + } } ``` -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 -class MethodFingerprintResult( +class Match( val method: Method, val classDef: ClassDef, - val scanResult: MethodFingerprintScanResult, + val patternMatch: Match.PatternMatch?, + val stringMatches: List?, // ... ) { val mutableClass by lazy { /* ... */ } @@ -186,87 +209,68 @@ class MethodFingerprintResult( // ... } - -class MethodFingerprintScanResult( - val patternScanResult: PatternScanResult?, - val stringsScanResult: StringsScanResult?, -) { - class StringsScanResult(val matches: List) { - 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. -Instead, the fingerprint can be resolved manually using various overloads of the `resolve` function of a fingerprint. +Unless a fingerprint is added to a patch, the fingerprint will not be matched automatically by ReVanced Patcher +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 - override fun execute(context: BytecodeContext) { - val result = ShowAdsFingerprint.also { it.resolve(context, context.classes) }.result - ?: throw PatchException("ShowAdsFingerprint not found") - - // ... - } + execute { context -> + val match = showAdsFingerprint.apply { + 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 - override fun execute(context: BytecodeContext) { + execute { context -> val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" } - val result = ShowAdsFingerprint.also { it.resolve(context, adsLoaderClass) }.result - ?: throw PatchException("ShowAdsFingerprint not found") - - // ... + val match = showAdsFingerprint.apply { + 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: ```kt - override fun execute(context: BytecodeContext) { - val adsFingerprintResult = ShowAdsFingerprint.result - ?: throw PatchException("ShowAdsFingerprint not found") - - val proStringsFingerprint = object : MethodFingerprint( - strings = listOf("free", "trial") - ) {} - - proStringsFingerprint.also { - it.resolve(context, adsFingerprintResult.method) - }.result?.let { result -> - result.scanResult.stringsScanResult!!.matches.forEach { match -> + execute { context -> + val proStringsFingerprint = fingerprint { + strings("free", "trial") + } + + proStringsFingerprint.apply { + match(context, adsFingerprintMatch.method) + }.match?.let { match -> + match.stringMatches.forEach { match -> println("The index of the string '${match.string}' is ${match.index}") } - - } ?: throw PatchException("pro strings fingerprint not found") + } ?: throw PatchException("No match found") } ``` > [!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 diff --git a/docs/2_2_patch_anatomy.md b/docs/2_2_patch_anatomy.md index 9b941f3f..2f31a2be 100644 --- a/docs/2_2_patch_anatomy.md +++ b/docs/2_2_patch_anatomy.md @@ -64,145 +64,186 @@ Learn the API to create patches using ReVanced Patcher. ## ⛳️ 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 package app.revanced.patches.ads -@Patch( +val disableAdsPatch = bytecodePatch( name = "Disable ads", description = "Disable ads in the app.", - dependencies = [DisableAdsResourcePatch::class], - compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] -) -object DisableAdsPatch : BytecodePatch( - setOf(ShowAdsFingerprint) -) { - override fun execute(context: BytecodeContext) { - ShowAdsFingerprint.result?.let { result -> - result.mutableMethod.addInstructions( - 0, - """ - # Return false. - const/4 v0, 0x0 - return v0 - """ - ) - } ?: throw PatchException("ShowAdsFingerprint not found") +) { + compatibleWith("com.some.app"("1.0.0")) + + // Resource patch disables ads by patching resource files. + dependsOn(disableAdsResourcePatch) + + // Precompiled DEX file to be merged into the patched app. + extendWith("disable-ads.rve") + + // Fingerprint to find the method to patch. + val showAdsMatch by showAdsFingerprint { + // More about fingerprints on the next page of the documentation. + } + + // Business logic of the patch to disable ads in the app. + execute { + // 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). + +## 🧩 Patch API -The example patch consists of the following parts: +### ⚙️ Patch options -### 📝 Patch annotation +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 -@Patch( - name = "Disable ads", - description = "Disable ads in the app.", - dependencies = [DisableAdsResourcePatch::class], - compatiblePackages = [CompatiblePackage("com.some.app", ["1.3.0"])] -) +val patch = bytecodePatch(name = "Patch") { + // Add an inbuilt option and delegate it to a property. + val value by stringOption(key = "option") + + // Add an option with a custom type and delegate it to a property. + val string by option(key = "string") + + execute { + println(value) + println(string) + } +} ``` -The `@Patch` annotation is used to provide metadata about the patch. +Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch: -Notable annotation parameters are: +```kt +loadPatchesJar(patches).apply { + // Type is checked at runtime. + first { it.name == "Patch" }.options["option"] = "Value" +} +``` -- `name`: The name of the patch. This is used as an identifier for the patch. - If this parameter is not set, `PatchBundleLoader` will not load the patch. - 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 +The type of an option can be obtained from the `type` property of the option: + +```kt +option.type // The KType of the option. +``` -> [!WARNING] -> Circular dependencies are not allowed. If a patch depends on another patch, the other patch cannot depend on the first patch. +### 🧩 Extensions -> [!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. +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. -> [!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. +Assume you want to add a complex feature to an app that would need multiple classes and methods: -> [!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 -> ) : BytecodePatch( -> name = "Disable ads", -> description = "Disable ads in the app.", -> fingerprints -> ) { -> // ... -> } -> ``` -> -> Remember that this constructor has precedence over the `@Patch` annotation. - -### 🏗️ Patch class +```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 -object DisableAdsPatch : BytecodePatch( /* Parameters */ ) { - // ... +val patch = bytecodePatch(name = "Complex patch") { + extendWith("complex-patch.rve") + + val match by methodFingerprint() + + execute { + match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V") + } } ``` -Each patch class extends off a base class that implements the `Patch` interface. -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). +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. -> [!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. +> [!NOTE] +> +> The [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template) repository +> is a template project to create patches and extensions. -### 🏁 The `execute` function +> [!TIP] +> To see real-world examples of extensions, +> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). -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). +### ♻️ Finalization -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: +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 result = LoadAdsFingerprint.result - ?: throw PatchException("LoadAdsFingerprint not found") - -result.mutableMethod.addInstructions( - 0, - """ - # Return false. - const/4 v0, 0x0 - return v0 - """ -) -``` - -> [!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`. -> Fingerprints will be explained in more detail on the next page. +val patch = bytecodePatch(name = "Patch") { + dependsOn( + bytecodePatch(name = "Dependency") { + execute { + print("1") + } + + finalize { + print("4") + } + } + ) + + execute { + print("2") + } -> [!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. -> 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. -> ReVanced Patcher will handle any exception raised by a patch. + finalize { + print("3") + } +} +``` -> [!TIP] -> To see real-world examples of patches, check out the [ReVanced Patches](https://github.com/revanced/revanced-patches) repository. +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 diff --git a/docs/2_patches_intro.md b/docs/2_patches_intro.md index bcb3ab22..c80a4696 100644 --- a/docs/2_patches_intro.md +++ b/docs/2_patches_intro.md @@ -65,61 +65,62 @@ Learn the basic concepts of ReVanced Patcher and how to create patches. ## 📙 Fundamentals 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 `ResourcePatch` modifies (decoded) resources - 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. -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 is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed. +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 a `BytecodePatch`: +Example of patches: ```kt @Surpress("unused") -object MyPatch : BytecodePatch() { - override fun execute(context: BytecodeContext) { - // Your patch code here - } +val bytecodePatch = bytecodePatch { + execute { + // TODO + } } -``` -Example of a `ResourcePatch`: - -```kt @Surpress("unused") -object MyPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { - // Your patch code here - } +val rawResourcePatch = rawResourcePatch { + execute { + // TODO + } } -``` - -Example of a `RawResourcePatch`: -```kt @Surpress("unused") -object MyPatch : RawResourcePatch() { - override fun execute(context: ResourceContext) { - // Your patch code here - } +val resourcePatch = rawResourcePatch { + execute { + // TODO + } } ``` > [!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 -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) diff --git a/docs/3_structure_and_conventions.md b/docs/3_structure_and_conventions.md index 03ad9fa9..3c8e27e3 100644 --- a/docs/3_structure_and_conventions.md +++ b/docs/3_structure_and_conventions.md @@ -64,31 +64,39 @@ Over time, a specific project structure and conventions have been established. ## 📁 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 📦your.patches.app.category - ├ 📂fingerprints - ├ ├ 🔍SomeFingerprintA.kt - ├ └ 🔍SomeFingerprintB.kt + ├ 🔍Fingerprints.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 -- 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `RemoveAdsPatch`. - If a patch changes the color of a button, name it `ChangeButtonColorPatch` +- 🔥 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 `Change button color` - 🔥 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._ -- 🔥 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. + 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._ +- 🔥 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. 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). - Patches can then reference methods and classes from integrations. - A real-world example of integrations can be found in the [ReVanced Integrations](https://github.com/ReVanced/revanced-integrations) repository + you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app + before this patch is executed. + Patches can then reference methods and classes from extensions. + A real-world example of extensions can be found in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository - 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change. - In the example of an obfuscated method, it's better to fingerprint the method by its return type and parameters rather than its name because the name is likely to change. An intelligent selection 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 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 + 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. +- 🔥🔥🔥 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 diff --git a/docs/4_apis.md b/docs/4_apis.md index 21c1cf7a..a2368cd7 100644 --- a/docs/4_apis.md +++ b/docs/4_apis.md @@ -4,13 +4,13 @@ A handful of APIs are available to make patch development easier and more effici ## 📙 Overview -1. 👹 Create new mutable classes with `context.proxy(ClassDef)` -2. 🔍 Find and proxy existing classes with `BytecodeContext.findClass(Predicate)` -3. 🏃‍ Easily access referenced methods recursively by index with `BytecodeContext.toMethodWalker(Method)` -4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications (Available in ReVanced Patches) -5. 💾 Read and write (decoded) resources with `ResourceContext.get(Path, Boolean) ` -6. 📃 Read and write DOM files using `ResourceContext.document` -7. 🔧 Equip patches with configurable options using `Patch.options` +1. 👹 Mutate classes with `context.proxy(ClassDef)` +2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)` +3. 🏃‍ Easily access referenced methods recursively by index with `MethodNavigator` +4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications +(Available in ReVanced Patches) +5. 💾 Read and write (decoded) resources with `ResourcePatchContext.get(Path, Boolean)` +6. 📃 Read and write DOM files using `ResourcePatchContext.document` ### 🧰 APIs @@ -19,5 +19,9 @@ A handful of APIs are available to make patch development easier and more effici ## 🎉 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e903aad0..ac82d74f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ android = "4.1.1.4" apktool-lib = "2.9.3" kotlin = "1.9.22" kotlinx-coroutines-core = "1.7.3" +mockk = "1.13.10" multidexlib2 = "3.0.3.r3" smali = "3.0.5" binary-compatibility-validator = "0.14.0" @@ -14,10 +15,11 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" } smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" } [plugins] binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } \ No newline at end of file +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 37f0d020..b4d1a122 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1 @@ rootProject.name = "revanced-patcher" - -buildCache { - local { - isEnabled = "CI" !in System.getenv() - } -} diff --git a/src/main/kotlin/app/revanced/patcher/Fingerprint.kt b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt new file mode 100644 index 00000000..8493bfae --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/Fingerprint.kt @@ -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?, + internal val opcodes: List?, + internal val strings: List?, + 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, + parameters2: Iterable, + ): Boolean { + if (parameters1.count() != parameters2.count()) return false + val iterator1 = parameters1.iterator() + parameters2.forEach { + if (!it.startsWith(iterator1.next())) return false + } + return true + } + + // TODO: parseParameters() + if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) { + return false + } + + if (custom != null && !custom.invoke(method, classDef)) { + return false + } + + val stringMatches: List? = + 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?, + 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? = null + private var opcodes: List? = null + private var strings: List? = 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. diff --git a/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt b/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt deleted file mode 100644 index d50a212e..00000000 --- a/src/main/kotlin/app/revanced/patcher/IntegrationsConsumer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.patcher - -import java.io.File - -@FunctionalInterface -interface IntegrationsConsumer { - fun acceptIntegrations(integrations: Set) - - @Deprecated("Use acceptIntegrations(Set) instead.") - fun acceptIntegrations(integrations: List) -} diff --git a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt b/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt deleted file mode 100644 index 08de7dd4..00000000 --- a/src/main/kotlin/app/revanced/patcher/PatchBundleLoader.kt +++ /dev/null @@ -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> - -/** - * A [Patch] class. - */ -typealias PatchClass = KClass> - -/** - * 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, - getBinaryClassNames: (patchBundle: File) -> List, - // 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> = 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) - } -} diff --git a/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt b/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt deleted file mode 100644 index da5aaa61..00000000 --- a/src/main/kotlin/app/revanced/patcher/PatchExecutorFunction.kt +++ /dev/null @@ -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> diff --git a/src/main/kotlin/app/revanced/patcher/Patcher.kt b/src/main/kotlin/app/revanced/patcher/Patcher.kt index e0d8cc36..214f7b47 100644 --- a/src/main/kotlin/app/revanced/patcher/Patcher.kt +++ b/src/main/kotlin/app/revanced/patcher/Patcher.kt @@ -1,13 +1,8 @@ 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 kotlinx.coroutines.flow.flow import java.io.Closeable -import java.io.File -import java.util.function.Supplier import java.util.logging.Logger /** @@ -15,243 +10,149 @@ import java.util.logging.Logger * * @param config The configuration to use for the patcher. */ -class Patcher( - private val config: PatcherConfig, -) : PatchExecutorFunction, PatchesConsumer, IntegrationsConsumer, Supplier, Closeable { - private val logger = Logger.getLogger(Patcher::class.java.name) +class Patcher(private val config: PatcherConfig) : Closeable { + private val logger = Logger.getLogger(this::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) - @Suppress("DEPRECATION") - @Deprecated("Use Patcher(PatcherConfig) instead.") - constructor( - patcherOptions: PatcherOptions, - ) : this( - PatcherConfig( - patcherOptions.inputFile, - patcherOptions.resourceCachePath, - patcherOptions.aaptBinaryPath, - patcherOptions.frameworkFileDirectory, - patcherOptions.multithreadingDexFileWriter, - ), - ) - 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") - override fun acceptPatches(patches: PatchSet) { - /** - * 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)!! - context.allPatches[this] = dependency - - dependency.dependencies?.forEach { it.putDependenciesRecursively() } - } + operator fun plusAssign(patches: Set>) { + // Add all patches to the executablePatches set. + context.executablePatches += patches - // Add all patches and their dependencies to the context. + // Add all patches and their dependencies to the allPatches set. patches.forEach { patch -> - context.executablePatches.putIfAbsent(patch::class, patch) ?: run { - context.allPatches[patch::class] = patch + fun Patch<*>.addRecursively() = + 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 = - predicate(this) || dependencies?.any { dependency -> - context.allPatches[dependency]!!.anyRecursively(predicate) - } ?: false - - context.allPatches.values.let { patches -> - // Determine the resource mode. - - config.resourceMode = if (patches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) { - ResourceContext.ResourceMode.FULL - } else if (patches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) { - ResourceContext.ResourceMode.RAW_ONLY + predicate(this) || dependencies.any { dependency -> dependency.anyRecursively(predicate) } + + context.allPatches.let { allPatches -> + // Check, if what kind of resource mode is required. + config.resourceMode = if (allPatches.any { patch -> patch.anyRecursively { it is ResourcePatch } }) { + ResourcePatchContext.ResourceMode.FULL + } else if (allPatches.any { patch -> patch.anyRecursively { it is RawResourcePatch } }) { + ResourcePatchContext.ResourceMode.RAW_ONLY } 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]. - * - * @param integrations The integrations to add. Must be a DEX file or container of DEX files. - */ - override fun acceptIntegrations(integrations: Set) { - context.bytecodeContext.integrations.addAll(integrations) - } - - @Deprecated( - "Use acceptIntegrations(Set) instead.", - ReplaceWith("acceptIntegrations(integrations.toSet())"), - ) - override fun acceptIntegrations(integrations: List) = acceptIntegrations(integrations.toSet()) - - /** - * Execute [Patch]es that were added to ReVanced [Patcher]. + * Execute added patches. * - * @param returnOnError If true, ReVanced [Patcher] will return immediately if a [Patch] fails. - * @return A pair of the name of the [Patch] and its [PatchResult]. + * @return A flow of [PatchResult]s. */ - override fun apply(returnOnError: Boolean) = - 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, 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. - patch.dependencies?.forEach { dependencyClass -> - val dependency = context.allPatches[dependencyClass]!! - val result = executePatch(dependency, executedPatches) + operator fun invoke() = flow { + fun Patch<*>.execute( + executedPatches: LinkedHashMap, PatchResult>, + ): PatchResult { + // If the patch was executed before or failed, return it's the result. + executedPatches[this]?.let { patchResult -> + patchResult.exception ?: return patchResult + + return PatchResult(this, PatchException("The patch '$this' failed previously")) + } - result.exception?.let { - return PatchResult( - patch, - PatchException( - "'$patchName' depends on '${dependency.name ?: dependency}' " + - "that raised an exception:\n${it.stackTraceToString()}", - ), - ) - } + // Recursively execute all dependency patches. + dependencies.forEach { dependency -> + dependency.execute(executedPatches).exception?.let { + return PatchResult( + this, + PatchException( + "The patch \"$this\" depends on \"$dependency\", which raised an exception:\n${it.stackTraceToString()}", + ), + ) } - - return try { - patch.execute(context) - - PatchResult(patch) - } catch (exception: PatchException) { - PatchResult(patch, exception) - } catch (exception: Exception) { - PatchResult(patch, PatchException(exception)) - }.also { executedPatches[patch] = it } } - if (context.bytecodeContext.integrations.merge) context.bytecodeContext.integrations.flush() - - LookupMap.initializeLookupMaps(context.bytecodeContext) + // Execute the patch. + return try { + execute(context) - // 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") + PatchResult(this) + } catch (exception: PatchException) { + PatchResult(this, exception) + } catch (exception: Exception) { + PatchResult(this, PatchException(exception)) + }.also { executedPatches[this] = it } + } - val executedPatches = LinkedHashMap, PatchResult>() // Key is name. + // Prevent from decoding the app manifest twice if it is not needed. + if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) { + context.resourceContext.decodeResources(config.resourceMode) + } - context.executablePatches.values.sortedBy { it.name }.forEach { patch -> - val patchResult = executePatch(patch, executedPatches) + logger.info("Executing patches") - // 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) + val executedPatches = LinkedHashMap, PatchResult>() - if (returnOnError) return@flow - } ?: run { - if (patch is Closeable) return@run + context.executablePatches.sortedBy { it.name }.forEach { patch -> + val patchResult = patch.execute(executedPatches) - emit(patchResult) - } + // If an exception occurred or the patch has no finalize block, emit the result. + if (patchResult.exception != null || patch.finalizeBlock == null) { + emit(patchResult) } + } - 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)) - } + val succeededPatchesWithFinalizeBlock = executedPatches.values.filter { + it.exception == null && it.patch.finalizeBlock != null + } - result.exception?.let { - emit( - PatchResult( - patch, - PatchException( - "'$patch' raised an exception while being closed: ${it.stackTraceToString()}", - result.exception, - ), - ), - ) + succeededPatchesWithFinalizeBlock.asReversed().forEach { executionResult -> + val patch = executionResult.patch - if (returnOnError) return@flow - } ?: run { - patch.name ?: return@run + val result = + try { + patch.finalize(context) - emit(result) - } + executionResult + } catch (exception: PatchException) { + PatchResult(patch, exception) + } catch (exception: Exception) { + PatchResult(patch, PatchException(exception)) } + + 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) + } } + } - 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) - override fun get() = - PatcherResult( - context.bytecodeContext.get(), - context.resourceContext.get(), - ) + fun get() = PatcherResult(context.bytecodeContext.get(), context.resourceContext.get()) } diff --git a/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt b/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt index ca47654b..cb2698f4 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherConfig.kt @@ -1,6 +1,6 @@ package app.revanced.patcher -import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.ResourcePatchContext import brut.androlib.Config import java.io.File import java.util.logging.Logger @@ -27,9 +27,9 @@ class PatcherConfig( /** * 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. diff --git a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt index 7824b4c9..54e6ddc6 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherContext.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherContext.kt @@ -1,8 +1,8 @@ package app.revanced.patcher -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.ResourcePatchContext import brut.androlib.apk.ApkInfo import brut.directory.ExtFile @@ -19,22 +19,22 @@ class PatcherContext internal constructor(config: PatcherConfig) { 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>() + internal val executablePatches = mutableSetOf>() /** - * 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>() + internal val allPatches = mutableSetOf>() /** - * 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) } diff --git a/src/main/kotlin/app/revanced/patcher/PatcherException.kt b/src/main/kotlin/app/revanced/patcher/PatcherException.kt deleted file mode 100644 index 9f92a011..00000000 --- a/src/main/kotlin/app/revanced/patcher/PatcherException.kt +++ /dev/null @@ -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", - ) -} diff --git a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt b/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt deleted file mode 100644 index f2061cc6..00000000 --- a/src/main/kotlin/app/revanced/patcher/PatcherOptions.kt +++ /dev/null @@ -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 - } -} diff --git a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt index acd797da..0334f9fb 100644 --- a/src/main/kotlin/app/revanced/patcher/PatcherResult.kt +++ b/src/main/kotlin/app/revanced/patcher/PatcherResult.kt @@ -2,7 +2,6 @@ package app.revanced.patcher import java.io.File import java.io.InputStream -import kotlin.jvm.internal.Intrinsics /** * The result of a patcher. @@ -15,87 +14,6 @@ class PatcherResult internal constructor( val dexFiles: Set, val resources: PatchedResources?, ) { - @Deprecated("This method is not used anymore") - constructor( - dexFiles: List, - resourceFile: File?, - doNotCompress: List? = null, - ) : this(dexFiles.toSet(), PatchedResources(resourceFile, null, doNotCompress?.toSet() ?: emptySet(), emptySet())) - - @Deprecated("This method is not used anymore") - fun component1(): List { - 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? { - return resources?.doNotCompress?.toList() - } - - @Deprecated("This method is not used anymore") - fun copy( - dexFiles: List, - resourceFile: File?, - doNotCompress: List? = 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. @@ -103,10 +21,7 @@ class PatcherResult internal constructor( * @param name The original name of the dex file. * @param stream The dex file as [InputStream]. */ - class PatchedDexFile - // TODO: Add internal modifier. - @Deprecated("This constructor will be removed in the future.") - constructor(val name: String, val stream: InputStream) + class PatchedDexFile internal constructor(val name: String, val stream: InputStream) /** * The resources of a patched apk. diff --git a/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt b/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt deleted file mode 100644 index 24533bbc..00000000 --- a/src/main/kotlin/app/revanced/patcher/PatchesConsumer.kt +++ /dev/null @@ -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>) = acceptPatches(patches.toSet()) - fun acceptPatches(patches: PatchSet) -} diff --git a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt b/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt deleted file mode 100644 index 50d8d638..00000000 --- a/src/main/kotlin/app/revanced/patcher/data/BytecodeContext.kt +++ /dev/null @@ -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> { - 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 { - 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 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() - } - } -} diff --git a/src/main/kotlin/app/revanced/patcher/data/Context.kt b/src/main/kotlin/app/revanced/patcher/data/Context.kt deleted file mode 100644 index 50c152ca..00000000 --- a/src/main/kotlin/app/revanced/patcher/data/Context.kt +++ /dev/null @@ -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 : Supplier diff --git a/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt deleted file mode 100644 index 3c0e25e8..00000000 --- a/src/main/kotlin/app/revanced/patcher/extensions/AnnotationExtensions.kt +++ /dev/null @@ -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 Class<*>.findAnnotationRecursively( - targetAnnotationClass: Class, - searchedClasses: HashSet = 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 KClass<*>.findAnnotationRecursively(targetAnnotation: KClass) = - java.findAnnotationRecursively(targetAnnotation.java) -} diff --git a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt index cc61e9f7..3be8f63d 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/Extensions.kt @@ -1,7 +1,6 @@ package app.revanced.patcher.extensions import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import com.android.tools.smali.dexlib2.AccessFlags /** * Create a label for the instruction at given index. @@ -10,24 +9,3 @@ import com.android.tools.smali.dexlib2.AccessFlags * @return The label. */ 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 diff --git a/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt index 736146bf..315a8513 100644 --- a/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt +++ b/src/main/kotlin/app/revanced/patcher/extensions/InstructionExtensions.kt @@ -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.MutableMethodImplementation 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 object InstructionExtensions { @@ -30,7 +32,7 @@ object InstructionExtensions { * @param instructions The instructions to add. */ fun MutableMethodImplementation.addInstructions(instructions: List) = - instructions.forEach { this.addInstruction(it) } + instructions.forEach { addInstruction(it) } /** * Remove instructions from a method at the given index. @@ -178,8 +180,8 @@ object InstructionExtensions { if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed /** - * Creates a new label for the instruction - * and replaces it with the label of the [compiledInstruction] at [compiledInstructionIndex]. + * Create a new label for the instruction + * and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex]. */ fun Instruction.makeNewLabel() { fun replaceOffset( @@ -310,6 +312,24 @@ object InstructionExtensions { smaliInstructions: String, ) = 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 MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T + /** * Get an instruction at the given index. * @@ -328,12 +348,27 @@ object InstructionExtensions { @Suppress("UNCHECKED_CAST") fun 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. * @param index The index to get the instruction at. * @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 Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction(index) /** * Get an instruction at the given index. @@ -341,11 +376,59 @@ object InstructionExtensions { * @param T The type of instruction to return. * @return The instruction. */ - fun MutableMethod.getInstruction(index: Int): T = implementation!!.getInstruction(index) + fun Method.getInstruction(index: Int): T = getInstructionOrNull(index)!! + + /** + * 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 MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction(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 MutableMethod.getInstruction(index: Int): T = getInstructionOrNull(index)!! + + /** + * The instructions of a method. + * @return The instructions or null if the method has no implementation. + */ + val Method.instructionsOrNull: Iterable? get() = implementation?.instructions + + /** + * The instructions of a method. + * @return The instructions. + */ + val Method.instructions: Iterable get() = instructionsOrNull!! + + /** + * The instructions of a method. + * @return The instructions or null if the method has no implementation. + */ + val MutableMethod.instructionsOrNull: MutableList? get() = implementation?.instructions /** - * Get the instructions of a method. + * The instructions of a method. * @return The instructions. */ - fun MutableMethod.getInstructions(): MutableList = implementation!!.instructions + val MutableMethod.instructions: MutableList get() = instructionsOrNull!! } diff --git a/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt b/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt deleted file mode 100644 index 413f1fd1..00000000 --- a/src/main/kotlin/app/revanced/patcher/extensions/MethodFingerprintExtensions.kt +++ /dev/null @@ -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 -} diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt deleted file mode 100644 index 6497cd5c..00000000 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/LookupMap.kt +++ /dev/null @@ -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 - -/** - * Lookup map for methods. - */ -internal class LookupMap : MutableMap 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() - - 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 as the key. - methodSignatureLookupMap.add(accessFlagsReturnKey, methodClassPair) - - // Add [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) { - // 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()) - } - } - } -} diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt deleted file mode 100644 index 7e16df7f..00000000 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprint.kt +++ /dev/null @@ -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? = null, - internal val opcodes: Iterable? = null, - internal val strings: Iterable? = 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, - parameters2: Iterable, - ): Boolean { - if (parameters1.count() != parameters2.count()) return false - val iterator1 = parameters1.iterator() - parameters2.forEach { - if (!it.startsWith(iterator1.next())) return false - } - return true - } - - 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, - instructions: Iterable, - ) = 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.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.resolve( - context: BytecodeContext, - classes: Iterable, - ) = forEach { fingerprint -> - for (classDef in classes) { - if (fingerprint.resolve(context, classDef)) break - } - } - } -} diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt deleted file mode 100644 index 25050034..00000000 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/MethodFingerprintResult.kt +++ /dev/null @@ -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) { - /** - * 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? = 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, - ) - } - } -} diff --git a/src/main/kotlin/app/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod.kt b/src/main/kotlin/app/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod.kt deleted file mode 100644 index dbaebac4..00000000 --- a/src/main/kotlin/app/revanced/patcher/fingerprint/annotation/FuzzyPatternScanMethod.kt +++ /dev/null @@ -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, -) diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt deleted file mode 100644 index 74c7d463..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatch.kt +++ /dev/null @@ -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 { - /** - * The fingerprints to resolve before executing the patch. - */ - internal val fingerprints: Set - - /** - * Create a new [BytecodePatch]. - * - * @param fingerprints The fingerprints to resolve before executing the patch. - */ - constructor(fingerprints: Set = 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? = null, - dependencies: Set? = null, - use: Boolean = true, - requiresIntegrations: Boolean = false, - fingerprints: Set = 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) - } -} diff --git a/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt new file mode 100644 index 00000000..b5671eaa --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/BytecodePatchContext.kt @@ -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> { + 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 { + 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) : 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 as the key. + methodsBySignature[accessFlagsReturnKey] = methodClassPair + + // Add [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) { + // 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 + +/** + * A list of [MethodClassPair]s. + */ +internal typealias MethodClassPairs = LinkedList + +/** + * A lookup map for [MethodClassPairs]s. + * The key is a string and the value is a list of [MethodClassPair]s. + */ +internal class MethodClassPairsLookupMap : MutableMap 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) } +} diff --git a/src/main/kotlin/app/revanced/patcher/patch/Option.kt b/src/main/kotlin/app/revanced/patcher/patch/Option.kt new file mode 100644 index 00000000..aa02b302 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/Option.kt @@ -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 @PublishedApi internal constructor( + val key: String, + val default: T? = null, + val values: Map? = null, + val title: String? = null, + val description: String? = null, + val required: Boolean = false, + val type: KType, + val validator: Option.(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>, +) : Map> by options { + internal constructor(options: Set>) : 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 set(key: String, value: T?) { + val option = this[key] + + try { + @Suppress("UNCHECKED_CAST") + (option as Option).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? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(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? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(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? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(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? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(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? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option.(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? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> 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? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> 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? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> 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? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> 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? = null, + values: Map?>? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + validator: Option>.(List?) -> 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 PatchBuilder<*>.option( + key: String, + default: T? = null, + values: Map? = null, + title: String? = null, + description: String? = null, + required: Boolean = false, + noinline validator: Option.(T?) -> Boolean = { true }, +) = Option( + key, + default, + values, + title, + description, + required, + typeOf(), + 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") +} diff --git a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt index 73bbd77c..acf45cc1 100644 --- a/src/main/kotlin/app/revanced/patcher/patch/Patch.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/Patch.kt @@ -1,133 +1,663 @@ -@file:Suppress("MemberVisibilityCanBePrivate") +@file:Suppress("MemberVisibilityCanBePrivate", "unused") package app.revanced.patcher.patch -import app.revanced.patcher.PatchClass +import app.revanced.patcher.Fingerprint import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherContext -import app.revanced.patcher.data.Context -import app.revanced.patcher.extensions.AnnotationExtensions.findAnnotationRecursively -import app.revanced.patcher.patch.options.PatchOptions -import java.io.Closeable +import dalvik.system.DexClassLoader +import lanchon.multidexlib2.BasicDexFileNamer +import lanchon.multidexlib2.MultiDexIO +import java.io.File +import java.io.InputStream +import java.net.URLClassLoader +import java.util.jar.JarFile +import kotlin.reflect.KProperty + +typealias PackageName = String +typealias VersionName = String +typealias Package = Pair?> /** * A patch. * - * If an implementation of [Patch] also implements [Closeable] - * it will be closed in reverse execution order of patches executed by [Patcher]. + * @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. + * @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> { +sealed class Patch>( + val name: String?, + val description: String?, + val use: Boolean, + val dependencies: Set>, + val compatiblePackages: Set?, + options: Set>, + private val executeBlock: Patch.(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) -> Unit)?, +) { + val options = Options(options) + /** - * The name of the patch. + * Runs the execution block of the patch. + * Called by [Patcher]. + * + * @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with. */ - var name: String? = null - private set + internal abstract fun execute(context: PatcherContext) /** - * The description of the patch. + * Runs the execution block of the patch. + * + * @param context The [PatchContext] to execute the patch with. */ - var description: String? = null - private set + fun execute(context: C) = executeBlock(context) /** - * The packages the patch is compatible with. + * Runs the finalizing block of the patch. + * Called by [Patcher]. + * + * @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with. */ - var compatiblePackages: Set? = null - private set + internal abstract fun finalize(context: PatcherContext) /** - * Other patches this patch depends on. + * Runs the finalizing block of the patch. + * + * @param context The [PatchContext] to finalize the patch with. */ - var dependencies: Set? = null - private set + fun finalize(context: C) { + finalizeBlock?.invoke(this, context) + } + + override fun toString() = name ?: "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?, + dependencies: Set>, + options: Set>, + val fingerprints: Set, + val extension: InputStream?, + executeBlock: Patch.(BytecodePatchContext) -> Unit, + finalizeBlock: (Patch.(BytecodePatchContext) -> Unit)?, +) : Patch( + name, + description, + use, + dependencies, + compatiblePackages, + options, + executeBlock, + finalizeBlock, +) { + override fun execute(context: PatcherContext) = with(context.bytecodeContext) { + extension?.let(::merge) + fingerprints.forEach { it.match(this) } + + 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?, + dependencies: Set>, + options: Set>, + executeBlock: Patch.(ResourcePatchContext) -> Unit, + finalizeBlock: (Patch.(ResourcePatchContext) -> Unit)?, +) : Patch( + 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?, + dependencies: Set>, + options: Set>, + executeBlock: Patch.(ResourcePatchContext) -> Unit, + finalizeBlock: (Patch.(ResourcePatchContext) -> Unit)?, +) : Patch( + 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>( + protected val name: String?, + protected val description: String?, + protected val use: Boolean, +) { + protected var compatiblePackages: MutableSet? = null + protected var dependencies = mutableSetOf>() + protected val options = mutableSetOf>() + + protected var executionBlock: (Patch.(C) -> Unit) = { } + protected var finalizeBlock: (Patch.(C) -> Unit)? = null /** - * Weather or not the patch should be used. + * Add an option to the patch. + * + * @return The added option. */ - var use = true - private set + operator fun Option.invoke() = apply { + options += this + } - // TODO: Remove this property, once integrations are coupled with patches. /** - * Weather or not the patch requires integrations. + * Create a package a patch is compatible with. + * + * @param versions The versions of the package. */ - var requiresIntegrations = false - private set - - constructor( - name: String?, - description: String?, - compatiblePackages: Set?, - dependencies: Set?, - use: Boolean, - requiresIntegrations: Boolean, - ) { - this.name = name - this.description = description - this.compatiblePackages = compatiblePackages - this.dependencies = dependencies - this.use = use - this.requiresIntegrations = requiresIntegrations - } + operator fun String.invoke(vararg versions: String) = this to versions.toSet() - 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 + /** + * 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 } /** - * The options of the patch associated by the options key. + * Set the compatible packages of the patch. + * + * @param packages The packages the patch is compatible with. */ - val options = PatchOptions() + fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray()) /** - * The execution function of the patch. - * This function is called by [Patcher]. + * Add dependencies to the patch. * - * @param context The [PatcherContext] the patch will work on. + * @param patches The patches the patch depends on. */ - internal abstract fun execute(context: PatcherContext) + fun dependsOn(vararg patches: Patch<*>) { + dependencies += patches + } /** - * The execution function of the patch. + * Set the execution block of the patch. * - * @param context The [Context] the patch will work on. - * @return The result of executing the patch. + * @param block The execution block of the patch. */ - abstract fun execute(context: @UnsafeVariance T) + fun execute(block: Patch.(C) -> Unit) { + executionBlock = block + } - override fun hashCode() = name.hashCode() + /** + * Set the finalizing block of the patch. + * + * @param block The finalizing block of the patch. + */ + fun finalize(block: Patch.(C) -> Unit) { + finalizeBlock = block + } - override fun toString() = name ?: this::class.simpleName ?: "Unnamed patch" + /** + * Build the patch. + * + * @return The built patch. + */ + internal abstract fun build(): Patch +} - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false +/** + * 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(name, description, use) { + private val fingerprints = mutableSetOf() - other as Patch<*> + /** + * 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) }) - return name == other.name + 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. /** - * A package a [Patch] is compatible with. + * Set the extension of the patch. * - * @param name The name of the package. - * @param versions The versions of the package. + * @param extension The name of the extension resource. */ - class CompatiblePackage( - val name: String, - val versions: Set? = null, + 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(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(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.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>>, +) : Set> 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, + getBinaryClassNames: (patchesFile: File) -> List, + 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) : + 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, 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>) = + 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) = + 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, optimizedDexDirectory: File? = null) = + PatchLoader.Dex(patchesFiles, optimizedDexDirectory) diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchContext.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchContext.kt new file mode 100644 index 00000000..33309a07 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/patch/PatchContext.kt @@ -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 : Supplier diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt deleted file mode 100644 index 544859f7..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchException.kt +++ /dev/null @@ -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) -} diff --git a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt b/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt deleted file mode 100644 index 790f419f..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/PatchResult.kt +++ /dev/null @@ -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) diff --git a/src/main/kotlin/app/revanced/patcher/patch/RawResourcePatch.kt b/src/main/kotlin/app/revanced/patcher/patch/RawResourcePatch.kt deleted file mode 100644 index a767ebb9..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/RawResourcePatch.kt +++ /dev/null @@ -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 { - /** - * 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? = null, - dependencies: Set? = null, - use: Boolean = true, - requiresIntegrations: Boolean = false, - ) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations) - - override fun execute(context: PatcherContext) = execute(context.resourceContext) -} diff --git a/src/main/kotlin/app/revanced/patcher/patch/ResourcePatch.kt b/src/main/kotlin/app/revanced/patcher/patch/ResourcePatch.kt deleted file mode 100644 index d25f1bb2..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/ResourcePatch.kt +++ /dev/null @@ -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 { - /** - * 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? = null, - dependencies: Set? = null, - use: Boolean = true, - requiresIntegrations: Boolean = false, - ) : super(name, description, compatiblePackages, dependencies, use, requiresIntegrations) - - override fun execute(context: PatcherContext) = execute(context.resourceContext) -} diff --git a/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt b/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt similarity index 86% rename from src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt rename to src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt index 71f935bc..5f9edfdb 100644 --- a/src/main/kotlin/app/revanced/patcher/data/ResourceContext.kt +++ b/src/main/kotlin/app/revanced/patcher/patch/ResourcePatchContext.kt @@ -1,11 +1,10 @@ -package app.revanced.patcher.data +package app.revanced.patcher.patch import app.revanced.patcher.InternalApi import app.revanced.patcher.PackageMetadata import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherResult import app.revanced.patcher.util.Document -import app.revanced.patcher.util.DomFileEditor import brut.androlib.AaptInvoker import brut.androlib.ApkDecoder 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.xml.ResXmlPatcher import brut.directory.ExtFile -import java.io.File import java.io.InputStream import java.io.OutputStream import java.nio.file.Files 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 config The [PatcherConfig] used to create this context. */ -class ResourceContext internal constructor( +class ResourcePatchContext internal constructor( private val packageMetadata: PackageMetadata, private val config: PatcherConfig, -) : Context, Iterable { - private val logger = Logger.getLogger(ResourceContext::class.java.name) +) : PatchContext { + private val logger = Logger.getLogger(ResourcePatchContext::class.java.name) /** * Read and write documents in the [PatcherConfig.apkFiles]. */ val document = DocumentOperatable() - @Deprecated("Use document instead.") - @Suppress("DEPRECATION") - val xmlEditor = XmlFileHolder() - /** * Predicate to delete resources from [PatcherConfig.apkFiles]. */ @@ -155,7 +149,7 @@ class ResourceContext internal constructor( // Excluded because present in resources.other. // 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. - // 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. // 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. @@ -213,12 +207,6 @@ class ResourceContext internal constructor( */ 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 = config.apkFiles.listFiles()!!.iterator() - /** * How to handle resources decoding and compiling. */ @@ -243,18 +231,6 @@ class ResourceContext internal constructor( inner class DocumentOperatable { operator fun get(inputStream: InputStream) = Document(inputStream) - @Suppress("DEPRECATION") - 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]) - } + operator fun get(path: String) = Document(this@ResourcePatchContext[path]) } } diff --git a/src/main/kotlin/app/revanced/patcher/patch/annotation/PatchAnnotations.kt b/src/main/kotlin/app/revanced/patcher/patch/annotation/PatchAnnotations.kt deleted file mode 100644 index 2e81f5ab..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/annotation/PatchAnnotations.kt +++ /dev/null @@ -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>> = [], - val compatiblePackages: Array = [], - 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 = [], -) diff --git a/src/main/kotlin/app/revanced/patcher/patch/options/PatchOption.kt b/src/main/kotlin/app/revanced/patcher/patch/options/PatchOption.kt deleted file mode 100644 index d9409505..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/options/PatchOption.kt +++ /dev/null @@ -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( - val key: String, - val default: T?, - val values: Map?, - val title: String?, - val description: String?, - val required: Boolean, - val valueType: String, - val validator: PatchOption.(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.stringPatchOption( - key: String, - default: String? = null, - values: Map? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption.(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.intPatchOption( - key: String, - default: Int? = null, - values: Map? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption.(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.booleanPatchOption( - key: String, - default: Boolean? = null, - values: Map? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption.(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.floatPatchOption( - key: String, - default: Float? = null, - values: Map? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption.(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.longPatchOption( - key: String, - default: Long? = null, - values: Map? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption.(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.stringArrayPatchOption( - key: String, - default: Array? = null, - values: Map?>? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption?>.(Array?) -> 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.intArrayPatchOption( - key: String, - default: Array? = null, - values: Map?>? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption?>.(Array?) -> 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.booleanArrayPatchOption( - key: String, - default: Array? = null, - values: Map?>? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption?>.(Array?) -> 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.floatArrayPatchOption( - key: String, - default: Array? = null, - values: Map?>? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption?>.(Array?) -> 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.longArrayPatchOption( - key: String, - default: Array? = null, - values: Map?>? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - validator: PatchOption?>.(Array?) -> 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

, T> P.registerNewPatchOption( - key: String, - default: T? = null, - values: Map? = null, - title: String? = null, - description: String? = null, - required: Boolean = false, - valueType: String, - validator: PatchOption.(T?) -> Boolean = { true }, - ) = PatchOption( - key, - default, - values, - title, - description, - required, - valueType, - validator, - ).also(options::register) - } -} diff --git a/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptionException.kt b/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptionException.kt deleted file mode 100644 index 4b5dd997..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptionException.kt +++ /dev/null @@ -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") -} diff --git a/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptions.kt b/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptions.kt deleted file mode 100644 index 69f30eab..00000000 --- a/src/main/kotlin/app/revanced/patcher/patch/options/PatchOptions.kt +++ /dev/null @@ -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> = mutableMapOf(), -) : MutableMap> 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 set( - key: String, - value: T?, - ) { - val option = this[key] - - try { - @Suppress("UNCHECKED_CAST") - (option as PatchOption).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) -} diff --git a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt index 87789b1a..6f917bb5 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt +++ b/src/main/kotlin/app/revanced/patcher/util/ClassMerger.kt @@ -1,7 +1,6 @@ package app.revanced.patcher.util -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.or +import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass import app.revanced.patcher.util.ClassMerger.Utils.filterAny import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny @@ -36,7 +35,7 @@ internal object ClassMerger { */ fun ClassDef.merge( otherClass: ClassDef, - context: BytecodeContext, + context: BytecodePatchContext, ) = this // .fixFieldAccess(otherClass) // .fixMethodAccess(otherClass) @@ -95,7 +94,7 @@ internal object ClassMerger { */ private fun ClassDef.publicize( reference: ClassDef, - context: BytecodeContext, + context: BytecodePatchContext, ) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) { this.asMutableClass().apply { context.traverseClassHierarchy(this) { @@ -175,12 +174,12 @@ internal object ClassMerger { * @param targetClass the class to start traversing the class hierarchy from * @param callback function that is called for every class in the hierarchy */ - fun BytecodeContext.traverseClassHierarchy( + fun BytecodePatchContext.traverseClassHierarchy( targetClass: MutableClass, callback: MutableClass.() -> Unit, ) { callback(targetClass) - this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { + this.classByType(targetClass.superclass ?: return)?.mutableClass?.let { traverseClassHierarchy(it, callback) } } @@ -199,7 +198,7 @@ internal object ClassMerger { * * @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]. diff --git a/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt b/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt deleted file mode 100644 index b7324f67..00000000 --- a/src/main/kotlin/app/revanced/patcher/util/DomFileEditor.kt +++ /dev/null @@ -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() - } -} diff --git a/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt new file mode 100644 index 00000000..ae780c49 --- /dev/null +++ b/src/main/kotlin/app/revanced/patcher/util/MethodNavigator.kt @@ -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.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) +} diff --git a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt index a05b7b84..cdc334f8 100644 --- a/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt +++ b/src/main/kotlin/app/revanced/patcher/util/ProxyClassList.kt @@ -4,23 +4,18 @@ import app.revanced.patcher.util.proxy.ClassProxy 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. */ -class ProxyClassList internal constructor(classes: MutableSet) : MutableSet by classes { - internal val proxies = mutableListOf() - - /** - * Add a [ClassProxy]. - */ - fun add(classProxy: ClassProxy) = proxies.add(classProxy) +class ProxyClassList internal constructor(classes: MutableList) : MutableList by classes { + internal val proxyPool = mutableListOf() /** * Replace all classes with their mutated versions. */ internal fun replaceClasses() = - proxies.removeIf { proxy -> + proxyPool.removeIf { proxy -> // If the proxy is unused, return false to keep it in the proxies list. if (!proxy.resolved) return@removeIf false diff --git a/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt b/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt deleted file mode 100644 index 0f27511c..00000000 --- a/src/main/kotlin/app/revanced/patcher/util/method/MethodWalker.kt +++ /dev/null @@ -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) -} diff --git a/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt b/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt index b20c0b70..ccd8abd1 100644 --- a/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt +++ b/src/main/kotlin/app/revanced/patcher/util/proxy/ClassProxy.kt @@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.iface.ClassDef * * A class proxy simply holds a reference to the original class * and allocates a mutable clone for the original class if needed. + * * @param immutableClass The class to proxy. */ class ClassProxy internal constructor( diff --git a/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt b/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt index f2e15038..12d97d75 100644 --- a/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt +++ b/src/main/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompiler.kt @@ -1,5 +1,6 @@ package app.revanced.patcher.util.smali +import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import com.android.tools.smali.dexlib2.AccessFlags 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.tree.CommonTreeNodeStream import java.io.InputStreamReader -import java.util.Locale +import java.util.* private const val METHOD_TEMPLATE = """ .class LInlineCompiler; @@ -64,7 +65,7 @@ class InlineSmaliCompiler { val dexGen = smaliTreeWalker(treeStream) dexGen.setDexBuilder(DexBuilder(Opcodes.getDefault())) val classDef = dexGen.smali_file() - return classDef.methods.first().implementation!!.instructions.map { it as BuilderInstruction } + return classDef.methods.first().instructions.map { it as BuilderInstruction } } } } diff --git a/src/test/kotlin/app/revanced/patcher/PatcherTest.kt b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt new file mode 100644 index 00000000..4ebf1f07 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/PatcherTest.kt @@ -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 { + // Can't mock private fields, until https://github.com/mockk/mockk/issues/1244 is resolved. + setPrivateField( + "config", + mockk { + 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() + + 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() + + infix fun Patch<*>.produces(equals: List) { + 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>.invoke(): List { + 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) + } +} diff --git a/src/test/kotlin/app/revanced/patcher/extensions/AnnotationExtensionsTest.kt b/src/test/kotlin/app/revanced/patcher/extensions/AnnotationExtensionsTest.kt deleted file mode 100644 index 57872018..00000000 --- a/src/test/kotlin/app/revanced/patcher/extensions/AnnotationExtensionsTest.kt +++ /dev/null @@ -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() - } -} diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchInitializationTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchInitializationTest.kt deleted file mode 100644 index 60362669..00000000 --- a/src/test/kotlin/app/revanced/patcher/patch/PatchInitializationTest.kt +++ /dev/null @@ -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") - } -} diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt new file mode 100644 index 00000000..8f90fef3 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/patch/PatchLoaderTest.kt @@ -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>>> + + // 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 } +} diff --git a/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt b/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt new file mode 100644 index 00000000..97a711c1 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/patch/PatchTest.kt @@ -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("custom")() + + execute { + println(print) + println(custom.value) + } + } + + assertEquals(2, patch.options.size) + } +} diff --git a/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt b/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt new file mode 100644 index 00000000..788b68e7 --- /dev/null +++ b/src/test/kotlin/app/revanced/patcher/patch/options/OptionsTest.kt @@ -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 { + set("validated", get("validated")) + } + + // Setter validation on incorrect value. + assertThrows { + set("validated", "invalid") + } + + // Setter validation on correct value. + assertDoesNotThrow { + set("validated", "valid") + } + } + + @Test + fun `should throw due to incorrect type`() = options { + assertThrows { + set("bool", "not a boolean") + } + } + + @Test + fun `should be nullable`() = options { + assertDoesNotThrow { + set("bool", null) + } + } + + @Test + fun `option should not be found`() = options { + assertThrows { + 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 + + 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 { + get("resettable").value + } + } + + @Test + fun `option types should be known`() = options { + assertEquals(typeOf>(), 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) +} diff --git a/src/test/kotlin/app/revanced/patcher/patch/options/PatchOptionsTest.kt b/src/test/kotlin/app/revanced/patcher/patch/options/PatchOptionsTest.kt deleted file mode 100644 index 6b896021..00000000 --- a/src/test/kotlin/app/revanced/patcher/patch/options/PatchOptionsTest.kt +++ /dev/null @@ -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 { OptionsTestPatch.validatedOption } - - // Setter validation on incorrect value. - assertThrows { OptionsTestPatch.validatedOption = "invalid" } - - // Setter validation on correct value. - assertDoesNotThrow { OptionsTestPatch.validatedOption = "valid" } - } - - @Test - fun `should throw due to incorrect type`() { - assertThrows { - OptionsTestPatch.options["bool"] = "not a boolean" - } - } - - @Test - fun `should be nullable`() { - OptionsTestPatch.booleanOption = null - } - - @Test - fun `option should not be found`() { - assertThrows { - OptionsTestPatch.options["this option does not exist"] = 1 - } - } - - @Test - fun `should be able to add options manually`() { - assertThrows { - 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) { - 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 { - 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) {} - } -} diff --git a/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleBytecodePatch.kt b/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleBytecodePatch.kt deleted file mode 100644 index 2f4b5f2d..00000000 --- a/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleBytecodePatch.kt +++ /dev/null @@ -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)}", - ) - } -} diff --git a/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleFingerprint.kt b/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleFingerprint.kt deleted file mode 100644 index 64bd62d0..00000000 --- a/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleFingerprint.kt +++ /dev/null @@ -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, -) diff --git a/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleResourcePatch.kt b/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleResourcePatch.kt deleted file mode 100644 index 7cd85c2d..00000000 --- a/src/test/kotlin/app/revanced/patcher/patch/usage/ExampleResourcePatch.kt +++ /dev/null @@ -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") - } - } -} diff --git a/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt b/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt index 44219323..c71b41ad 100644 --- a/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt +++ b/src/test/kotlin/app/revanced/patcher/util/smali/InlineSmaliCompilerTest.kt @@ -18,23 +18,24 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -internal class InlineSmaliCompilerTest { +internal object InlineSmaliCompilerTest { @Test - fun `compiler should output valid instruction`() { + fun `outputs valid instruction`() { val want = BuilderInstruction21c(Opcode.CONST_STRING, 0, ImmutableStringReference("Test")) as BuilderInstruction val have = "const-string v0, \"Test\"".toInstruction() - instructionEquals(want, have) + + assertInstructionsEqual(want, have) } @Test - fun `compiler should support branching with own branches`() { + fun `supports branching with own branches`() { val method = createMethod() - val insnAmount = 8 - val insnIndex = insnAmount - 2 - val targetIndex = insnIndex - 1 + val instructionCount = 8 + val instructionIndex = instructionCount - 2 + val targetIndex = instructionIndex - 1 method.addInstructions( - arrayOfNulls(insnAmount).also { + arrayOfNulls(instructionCount).also { Arrays.fill(it, "const/4 v0, 0x0") }.joinToString("\n"), ) @@ -47,14 +48,15 @@ internal class InlineSmaliCompilerTest { """, ) - val insn = method.getInstruction(insnIndex) - assertEquals(targetIndex, insn.target.location.index) + val instruction = method.getInstruction(instructionIndex) + + assertEquals(targetIndex, instruction.target.location.index) } @Test - fun `compiler should support branching to outside branches`() { + fun `supports branching to outside branches`() { val method = createMethod() - val insnIndex = 3 + val instructionIndex = 3 val labelIndex = 1 method.addInstructions( @@ -76,35 +78,30 @@ internal class InlineSmaliCompilerTest { ExternalLabel("test", method.getInstruction(1)), ) - val insn = method.getInstruction(insnIndex) - assertTrue(insn.target.isPlaced, "Label was not placed") - assertEquals(labelIndex, insn.target.location.index) + val instruction = method.getInstruction(instructionIndex) + assertTrue(instruction.target.isPlaced, "Label was not placed") + assertEquals(labelIndex, instruction.target.location.index) } - companion object { - private fun createMethod( - name: String = "dummy", - returnType: String = "V", - accessFlags: Int = AccessFlags.STATIC.value, - registerCount: Int = 1, - ) = ImmutableMethod( - "Ldummy;", - name, - emptyList(), // parameters - returnType, - accessFlags, - emptySet(), - emptySet(), - MutableMethodImplementation(registerCount), - ).toMutable() + private fun createMethod( + name: String = "dummy", + returnType: String = "V", + accessFlags: Int = AccessFlags.STATIC.value, + registerCount: Int = 1, + ) = ImmutableMethod( + "Ldummy;", + name, + emptyList(), // parameters + returnType, + accessFlags, + emptySet(), + emptySet(), + MutableMethodImplementation(registerCount), + ).toMutable() - private fun instructionEquals( - want: BuilderInstruction, - have: BuilderInstruction, - ) { - assertEquals(want.opcode, have.opcode) - assertEquals(want.format, have.format) - assertEquals(want.codeUnits, have.codeUnits) - } + private fun assertInstructionsEqual(want: BuilderInstruction, have: BuilderInstruction) { + assertEquals(want.opcode, have.opcode) + assertEquals(want.format, have.format) + assertEquals(want.codeUnits, have.codeUnits) } }