1
mirror of https://github.com/revanced/revanced-patcher synced 2025-09-06 16:38:50 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
oSumAtrIX
1f0eab36d9 fix build 2024-10-27 04:32:40 +01:00
oSumAtrIX
7be0cd8548 fix: Merge extension only when patch executes 2024-10-26 23:23:36 +02:00
29 changed files with 1163 additions and 1697 deletions

View File

@@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3
uses: burrunan/gradle-cache-action@v1
- name: Build
env:

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Open pull request
uses: repo-sync/pull-request@v2

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
@@ -24,7 +24,7 @@ jobs:
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3
uses: burrunan/gradle-cache-action@v1
- name: Build
env:

View File

@@ -1,108 +1,3 @@
# [21.1.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.3...v21.1.0-dev.4) (2025-07-18)
### Bug Fixes
* Correctly save XML files in UTF-8 by using a bufferedWriter ([#356](https://github.com/ReVanced/revanced-patcher/issues/356)) ([33fadcb](https://github.com/ReVanced/revanced-patcher/commit/33fadcbd0c7076b848bdca4d62a9c684d5781232))
# [21.1.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.2...v21.1.0-dev.3) (2025-06-20)
### Bug Fixes
* Encode XML files as UTF-8 to fix compilation of resources ([#339](https://github.com/ReVanced/revanced-patcher/issues/339)) ([4f2ef3c](https://github.com/ReVanced/revanced-patcher/commit/4f2ef3c47cea76a26c464cfb45d4bb57fe7198b5))
# [21.1.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.1...v21.1.0-dev.2) (2025-06-20)
### Bug Fixes
* Add back missing log by naming logger correctly ([#332](https://github.com/ReVanced/revanced-patcher/issues/332)) ([e4e66b0](https://github.com/ReVanced/revanced-patcher/commit/e4e66b0d8bb0986b79fb150b9c15da35b8e11561))
* Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments ([#331](https://github.com/ReVanced/revanced-patcher/issues/331)) ([bb8771b](https://github.com/ReVanced/revanced-patcher/commit/bb8771bb8b8ab1724d957e56f4de88c02684d87b))
### Features
* Use option name as key for simplicity and consistency ([754b02e](https://github.com/ReVanced/revanced-patcher/commit/754b02e4ca66ec10764d5205c6643f2d86d0c6a2))
### Performance Improvements
* Use a buffered writer to reduce IO overhead ([#347](https://github.com/ReVanced/revanced-patcher/issues/347)) ([99f4318](https://github.com/ReVanced/revanced-patcher/commit/99f431897eb9e607987fd5d09b879d7eda442f3e))
# [21.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0...v21.1.0-dev.1) (2024-12-07)
### Features
* Add identity hash code to unnamed patches ([88a3252](https://github.com/ReVanced/revanced-patcher/commit/88a325257494939a79fb30dd51d60c5c52546755))
# [21.0.0](https://github.com/ReVanced/revanced-patcher/compare/v20.0.2...v21.0.0) (2024-11-05)
### Bug Fixes
* Match fingerprint before delegating the match property ([5d996de](https://github.com/ReVanced/revanced-patcher/commit/5d996def4d3de4e2bfc34562e5a6c7d89a8cddf0))
* Merge extension only when patch executes ([#315](https://github.com/ReVanced/revanced-patcher/issues/315)) ([aa472eb](https://github.com/ReVanced/revanced-patcher/commit/aa472eb9857145b53b49f843406a9764fbb7e5ce))
### Features
* Improve Fingerprint API ([#316](https://github.com/ReVanced/revanced-patcher/issues/316)) ([0abf1c6](https://github.com/ReVanced/revanced-patcher/commit/0abf1c6c0279708fdef5cb66b141d07d17682693))
* Improve various APIs ([#317](https://github.com/ReVanced/revanced-patcher/issues/317)) ([b824978](https://github.com/ReVanced/revanced-patcher/commit/b8249789df8b90129f7b7ad0e523a8d0ceaab848))
* Move fingerprint match members to fingerprint for ease of access by using context receivers ([0746c22](https://github.com/ReVanced/revanced-patcher/commit/0746c22743a9561bae2284d234b151f2f8511ca5))
### Performance Improvements
* Use smallest lookup map for strings ([1358d3f](https://github.com/ReVanced/revanced-patcher/commit/1358d3fa10cb8ba011b6b89cfe3684ecf9849d2f))
### BREAKING CHANGES
* Various APIs have been changed.
* Many APIs have been changed.
# [21.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.3...v21.0.0-dev.4) (2024-11-05)
### Performance Improvements
* Use smallest lookup map for strings ([1358d3f](https://github.com/ReVanced/revanced-patcher/commit/1358d3fa10cb8ba011b6b89cfe3684ecf9849d2f))
# [21.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.2...v21.0.0-dev.3) (2024-11-05)
### Features
* Move fingerprint match members to fingerprint for ease of access by using context receivers ([0746c22](https://github.com/ReVanced/revanced-patcher/commit/0746c22743a9561bae2284d234b151f2f8511ca5))
# [21.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.1...v21.0.0-dev.2) (2024-11-01)
### Bug Fixes
* Match fingerprint before delegating the match property ([5d996de](https://github.com/ReVanced/revanced-patcher/commit/5d996def4d3de4e2bfc34562e5a6c7d89a8cddf0))
# [21.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.2...v21.0.0-dev.1) (2024-10-27)
### Bug Fixes
* Merge extension only when patch executes ([#315](https://github.com/ReVanced/revanced-patcher/issues/315)) ([aa472eb](https://github.com/ReVanced/revanced-patcher/commit/aa472eb9857145b53b49f843406a9764fbb7e5ce))
### Features
* Improve Fingerprint API ([#316](https://github.com/ReVanced/revanced-patcher/issues/316)) ([0abf1c6](https://github.com/ReVanced/revanced-patcher/commit/0abf1c6c0279708fdef5cb66b141d07d17682693))
* Improve various APIs ([#317](https://github.com/ReVanced/revanced-patcher/issues/317)) ([b824978](https://github.com/ReVanced/revanced-patcher/commit/b8249789df8b90129f7b7ad0e523a8d0ceaab848))
### BREAKING CHANGES
* Various APIs have been changed.
* Many APIs have been changed.
## [20.0.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1...v20.0.2) (2024-10-17)

View File

@@ -1,22 +1,7 @@
public final class app/revanced/patcher/Fingerprint {
public final fun getClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getOriginalMethodOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getPatternMatch (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getPatternMatchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun getStringMatchesOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Ljava/util/List;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun matchOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun getMatch ()Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z
}
public final class app/revanced/patcher/FingerprintBuilder {
@@ -33,27 +18,32 @@ public final class app/revanced/patcher/FingerprintBuilder {
public final class app/revanced/patcher/FingerprintKt {
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint;
public static final fun fingerprint (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public static synthetic fun fingerprint$default (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
public static synthetic fun fingerprint$default (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
}
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
}
public final class app/revanced/patcher/Match {
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public fun <init> (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lapp/revanced/patcher/Match$PatternMatch;Ljava/util/List;Lapp/revanced/patcher/patch/BytecodePatchContext;)V
public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;
public final fun getMutableMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List;
}
public final class app/revanced/patcher/Match$PatternMatch {
public fun <init> (II)V
public final fun getEndIndex ()I
public final fun getStartIndex ()I
}
public final class app/revanced/patcher/Match$StringMatch {
public fun <init> (Ljava/lang/String;I)V
public final fun getIndex ()I
public final fun getString ()Ljava/lang/String;
}
@@ -73,10 +63,8 @@ public final class app/revanced/patcher/Patcher : java/io/Closeable {
}
public final class app/revanced/patcher/PatcherConfig {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;Z)V
public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
@@ -147,36 +135,40 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
}
public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch {
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public final fun getExtension ()Ljava/io/InputStream;
public final fun getFingerprints ()Ljava/util/Set;
public fun toString ()Ljava/lang/String;
}
public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder {
public synthetic fun build$revanced_patcher ()Lapp/revanced/patcher/patch/Patch;
public final fun extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public final fun getExtensionInputStream ()Ljava/util/function/Supplier;
public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V
public final fun getExtension ()Ljava/io/InputStream;
public final fun invoke (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
public final fun setExtension (Ljava/io/InputStream;)V
}
public final class app/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint {
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
}
public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public final fun classByType (Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/ClassProxy;
public fun close ()V
public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/util/Set;
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator;
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/patch/Option {
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/String;ZLkotlin/reflect/KType;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Object;Ljava/util/Map;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 getName ()Ljava/lang/String;
public final fun getRequired ()Z
public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Lkotlin/reflect/KType;
@@ -294,7 +286,7 @@ public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jv
}
public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZLjava/util/Set;Ljava/util/Set;Ljava/util/Set;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun execute (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun getCompatiblePackages ()Ljava/util/Set;
@@ -311,13 +303,13 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun compatibleWith ([Ljava/lang/String;)V
public final fun compatibleWith ([Lkotlin/Pair;)V
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
public final fun execute (Lkotlin/jvm/functions/Function1;)V
public final fun finalize (Lkotlin/jvm/functions/Function1;)V
public final fun execute (Lkotlin/jvm/functions/Function2;)V
public final fun finalize (Lkotlin/jvm/functions/Function2;)V
protected final fun getCompatiblePackages ()Ljava/util/Set;
protected final fun getDependencies ()Ljava/util/Set;
protected final fun getDescription ()Ljava/lang/String;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set;
protected final fun getUse ()Z
@@ -325,8 +317,8 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
protected final fun setCompatiblePackages (Ljava/util/Set;)V
protected final fun setDependencies (Ljava/util/Set;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V
}
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
@@ -403,13 +395,18 @@ public final class app/revanced/patcher/patch/ResourcePatchBuilder : app/revance
}
public final class app/revanced/patcher/patch/ResourcePatchContext : app/revanced/patcher/patch/PatchContext {
public final fun delete (Ljava/lang/String;)Z
public final fun document (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document;
public final fun document (Ljava/lang/String;)Lapp/revanced/patcher/util/Document;
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/ResourcePatchContext$DocumentOperatable {
public fun <init> (Lapp/revanced/patcher/patch/ResourcePatchContext;)V
public final fun get (Ljava/io/InputStream;)Lapp/revanced/patcher/util/Document;
public final fun get (Ljava/lang/String;)Lapp/revanced/patcher/util/Document;
}
public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document {
@@ -485,12 +482,11 @@ public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w
}
public final class app/revanced/patcher/util/MethodNavigator {
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public final fun to (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun to ([I)Lapp/revanced/patcher/util/MethodNavigator;
public static synthetic fun to$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
public final 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/List, kotlin/jvm/internal/markers/KMutableList {

View File

@@ -56,8 +56,6 @@ dependencies {
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
freeCompilerArgs = listOf("-Xcontext-receivers")
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -73,26 +73,26 @@ package app.revanced.patches.ads
val disableAdsPatch = bytecodePatch(
name = "Disable ads",
description = "Disable ads in the app.",
) {
) {
compatibleWith("com.some.app"("1.0.0"))
// Patches can depend on other patches, executing them first.
// Resource patch disables ads by patching resource files.
dependsOn(disableAdsResourcePatch)
// Merge precompiled DEX files into the patched app, before the patch is executed.
// Precompiled DEX file to be merged into the patched app.
extendWith("disable-ads.rve")
// Fingerprint to find the method to patch.
val showAdsMatch by showAdsFingerprint {
// More about fingerprints on the next page of the documentation.
}
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch.
val showAdsFingerprint = fingerprint {
// More about fingerprints on the next page of the documentation.
}
// In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads.
showAdsFingerprint.method.addInstructions(
showAdsMatch.mutableMethod.addInstructions(
0,
"""
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
@@ -122,11 +122,11 @@ To define an option, use the available `option` functions:
```kt
val patch = bytecodePatch(name = "Patch") {
// Add an inbuilt option and delegate it to a property.
val value by stringOption(name = "Inbuilt option")
val value by stringOption(key = "option")
// Add an option with a custom type and delegate it to a property.
val string by option<String>(name = "String option")
val string by option<String>(key = "string")
execute {
println(value)
println(string)
@@ -139,20 +139,20 @@ Options of a patch can be set after loading the patches with `PatchLoader` by ob
```kt
loadPatchesJar(patches).apply {
// Type is checked at runtime.
first { it.name == "Patch" }.options["Option"] = "Value"
first { it.name == "Patch" }.options["option"] = "Value"
}
```
The type of an option can be obtained from the `type` property of the option:
```kt
option.type // The KType of the option. Captures the full type information of the option.
option.type // The KType of the option.
```
Options can be declared outside a patch and added to a patch manually:
Options can be declared outside of a patch and added to a patch manually:
```kt
val option = stringOption(name = "Option")
val option = stringOption(key = "option")
bytecodePatch(name = "Patch") {
val value by option()
@@ -183,18 +183,20 @@ and use it in a patch:
```kt
val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve")
execute {
fingerprint.method.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
val match by methodFingerprint()
execute {
match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
}
}
```
ReVanced Patcher merges the classes from the extension into `context.classes` before executing the patch.
ReVanced Patcher merges the classes from the extension into `context.classes` before executing the patch.
When the patch is executed, it can reference the classes and methods from the extension.
> [!NOTE]
>
>
> The [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template) repository
> is a template project to create patches and extensions.
@@ -211,9 +213,9 @@ A simple real-world example would be a patch that opens a resource file of the a
Other patches that depend on this patch can write to the file, and the finalization block can close the file.
```kt
val patch = bytecodePatch(name = "Patch") {
val patch = bytecodePatch(name = "Patch") {
dependsOn(
bytecodePatch(name = "Dependency") {
bytecodePatch(name = "Dependency") {
execute {
print("1")
}
@@ -249,10 +251,10 @@ The same order is followed for multiple patches depending on the patch.
- A patch can declare compatibility with specific packages and versions,
but patches can still be executed on any package or version.
It is recommended that compatibility is specified to present known compatible packages and versions.
- If `compatibleWith` is not used, the patch is treated as compatible with any package
- 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.
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

View File

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

View File

@@ -4,107 +4,18 @@ A handful of APIs are available to make patch development easier and more effici
## 📙 Overview
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
3. 🏃‍ Navigate method calls recursively by index with `navigate(Method)`
4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)`
5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)`
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
#### 👹 `proxy(ClassDef)`
By default, the classes are immutable, meaning they cannot be modified.
To make a class mutable, use the `proxy(ClassDef)` function.
This function creates a lazy mutable copy of the class definition.
Accessing the property will replace the original class definition with the mutable copy,
thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
```kt
execute {
val mutableClass = proxy(classDef)
mutableClass.methods.add(Method())
}
```
#### 🔍 `classBy(Predicate)`
The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate.
It automatically proxies the class definition, making it mutable.
```kt
execute {
// Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef
val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef
}
```
#### 🏃‍ `navigate(Method).at(index)`
The `navigate(Method)` function allows you to navigate method calls recursively by index.
```kt
execute {
// Sequentially navigate to the instructions at index 1 within 'someMethod'.
val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
// Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
// stop() returns the mutable copy of the method.
val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
// Alternatively, to stop(), you can delegate the method to a variable.
val method by navigate(someMethod).to(1)
// You can chain multiple calls to at() to navigate deeper into the method.
val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
}
```
#### 💾 `get(String, Boolean)` and `delete(String)`
The `get(String, Boolean)` function returns a `File` object that can be used to read and write resource files.
```kt
execute {
val file = get("res/values/strings.xml")
val content = file.readText()
file.writeText(content)
}
```
The `delete` function can mark files for deletion when the APK is rebuilt.
```kt
execute {
delete("res/values/strings.xml")
}
```
#### 📃 `document(String)` and `document(InputStream)`
The `document` function is used to read and write DOM files.
```kt
execute {
document("res/values/strings.xml").use { document ->
val element = doc.createElement("string").apply {
textContent = "Hello, World!"
}
document.documentElement.appendChild(element)
}
}
```
You can also read documents from an `InputStream`:
```kt
execute {
val inputStream = classLoader.getResourceAsStream("some.xml")
document(inputStream).use { document ->
// ...
}
}
```
> [!WARNING]
> This section is still under construction and may be incomplete.
## 🎉 Afterword
@@ -112,6 +23,5 @@ ReVanced Patcher is a powerful library to patch Android applications, offering a
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,
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

View File

@@ -1,3 +1,3 @@
org.gradle.parallel = true
org.gradle.caching = true
version = 21.1.0-dev.4
version = 20.0.2

View File

@@ -1,14 +1,14 @@
[versions]
android = "4.1.1.4"
apktool-lib = "2.10.1.1"
binary-compatibility-validator = "0.18.1"
kotlin = "2.0.20"
kotlinx-coroutines-core = "1.10.2"
mockk = "1.14.5"
apktool-lib = "2.9.3"
binary-compatibility-validator = "0.15.1"
kotlin = "2.0.0"
kotlinx-coroutines-core = "1.8.1"
mockk = "1.13.10"
multidexlib2 = "3.0.3.r3"
# Tracking https://github.com/google/smali/issues/64.
#noinspection GradleDependency
smali = "3.0.9"
smali = "3.0.5"
xpp3 = "1.1.4c"
[libraries]

1375
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,6 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^24.2.7"
"semantic-release": "^24.1.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,32 +12,17 @@ import java.util.logging.Logger
* @param temporaryFilesPath A path to a folder to store temporary files in.
* @param aaptBinaryPath A path to a custom aapt binary.
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
* @param multithreadingDexFileWriter Whether to use multiple threads for writing dex files.
* This has impact on memory usage and performance.
*/
class PatcherConfig(
internal val apkFile: File,
private val temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: File? = null,
aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null,
@Deprecated("This is going to be removed in the future because it is not needed anymore.")
internal val multithreadingDexFileWriter: Boolean = false,
) {
/**
* The configuration for the patcher.
*
* @param apkFile The apk file to patch.
* @param temporaryFilesPath A path to a folder to store temporary files in.
* @param aaptBinaryPath A path to a custom aapt binary.
* @param frameworkFileDirectory A path to the directory to cache the framework file in.
*/
@Deprecated(
"Use the constructor with a File for aaptBinaryPath instead.",
ReplaceWith("PatcherConfig(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)"),
)
constructor(
apkFile: File,
temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null,
) : this(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory)
private val logger = Logger.getLogger(PatcherConfig::class.java.name)
/**
@@ -52,7 +37,8 @@ class PatcherConfig(
*/
internal val resourceConfig =
Config.getDefaultConfig().apply {
aaptBinary = aaptBinaryPath
useAapt2 = true
aaptPath = aaptBinaryPath ?: ""
frameworkDirectory = frameworkFileDirectory
}

View File

@@ -29,12 +29,12 @@ class PatcherResult internal constructor(
* @param resourcesApk The compiled resources.apk file.
* @param otherResources The directory containing other resources files.
* @param doNotCompress List of files that should not be compressed.
* @param deleteResources List of resources that should be deleted.
* @param deleteResources List of predicates about resources that should be deleted.
*/
class PatchedResources internal constructor(
val resourcesApk: File?,
val otherResources: File?,
val doNotCompress: Set<String>,
val deleteResources: Set<String>,
val deleteResources: Set<(String) -> Boolean>,
)
}

View File

@@ -14,7 +14,6 @@ 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.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
@@ -34,7 +33,7 @@ import java.util.logging.Logger
class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(BytecodePatchContext::class.java.name)
/**
* [Opcodes] of the supplied [PatcherConfig.apkFile].
@@ -60,16 +59,13 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal val lookupMaps by lazy { LookupMaps(classes) }
/**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext].
* If no extension is present, the function will return early.
*
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
* Merge the extension of this patch.
*/
internal fun mergeExtension(bytecodePatch: BytecodePatch) {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream ->
internal fun BytecodePatch.mergeExtension() {
extension?.use { extensionStream ->
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef ->
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
logger.fine("Adding class \"$classDef\"")
classes += classDef
lookupMaps.classesByType[classDef.type] = classDef
@@ -77,7 +73,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
logger.fine("Class \"$classDef\" exists already. Adding missing methods and fields.")
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
@@ -89,9 +85,18 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
classes += mergedClass
}
}
} ?: logger.fine("No extension to merge")
} ?: return logger.fine("No extension to merge")
}
/**
* Find a class by its type using a contains check.
*
* @param type The type of the class.
* @return A proxy for the first class that matches the type.
*/
@Deprecated("Use classBy { type in it.type } instead.", ReplaceWith("classBy { type in it.type }"))
fun classByType(type: String) = classBy { type in it.type }
/**
* Find a class with a predicate.
*
@@ -108,9 +113,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
*
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = classes.proxyPool.find {
fun proxy(classDef: ClassDef) = this@BytecodePatchContext.classes.proxyPool.find {
it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
} ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) }
/**
* Navigate a method.
@@ -119,7 +124,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(method)
fun navigate(method: Method) = MethodNavigator(this@BytecodePatchContext, method)
/**
* Compile bytecode from the [BytecodePatchContext].
@@ -140,7 +145,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
if (config.multithreadingDexFileWriter) -1 else 1,
this,
BasicDexFileNamer(),
object : DexFile {
@@ -150,7 +155,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
override fun getOpcodes() = this@BytecodePatchContext.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
@@ -198,6 +203,28 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
}
}
internal companion object {
/**
* Appends a string based on the parameter reference types of this method.
*/
internal fun StringBuilder.appendParameters(parameters: Iterable<CharSequence>) {
// Maximum parameters to use in the signature key.
// Some apps have methods with an incredible number of parameters (over 100 parameters have been seen).
// To keep the signature map from becoming needlessly bloated,
// group together in the same map entry all methods with the same access/return and 5 or more parameters.
// The value of 5 was chosen based on local performance testing and is not set in stone.
val maxSignatureParameters = 5
// Must append a unique value before the parameters to distinguish this key includes the parameters.
// If this is not appended, then methods with no parameters
// will collide with different keys that specify access/return but omit the parameters.
append("p:")
parameters.forEachIndexed { index, parameter ->
if (index >= maxSignatureParameters) return
append(parameter.first())
}
}
}
override fun close() {
methodsByStrings.clear()
classesByType.clear()

View File

@@ -20,51 +20,16 @@ import kotlin.reflect.typeOf
* @constructor Create a new [Option].
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
class Option<T>
@PublishedApi
@Deprecated("Use the constructor with the name instead of a key instead.")
internal constructor(
@Deprecated("Use the name property instead.")
class Option<T> @PublishedApi internal constructor(
val key: String,
val default: T? = null,
val values: Map<String, T?>? = null,
@Deprecated("Use the name property instead.")
val title: String? = null,
val description: String? = null,
val required: Boolean = false,
val type: KType,
val validator: Option<T>.(T?) -> Boolean = { true },
) {
/**
* The name.
*/
val name = key
/**
* An option.
*
* @param T The value type of the option.
* @param name The name.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @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].
*/
@PublishedApi
internal constructor(
name: String,
default: T? = null,
values: Map<String, T?>? = null,
description: String? = null,
required: Boolean = false,
type: KType,
validator: Option<T>.(T?) -> Boolean = { true },
) : this(name, default, values, name, description, required, type, validator)
/**
* The value of the [Option].
*/
@@ -144,7 +109,7 @@ internal constructor(
class Options internal constructor(
private val options: Map<String, Option<*>>,
) : Map<String, Option<*>> by options {
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.name })
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.key })
/**
* Set an option's value.
@@ -891,14 +856,14 @@ sealed class OptionException(errorMessage: String) : Exception(errorMessage, nul
*
* @param value The value that failed validation.
*/
class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.name}")
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.name} requires a value, but the value was null")
class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.key} requires a value, but the value was null")
/**
* An exception thrown when a [Option] is not found.

View File

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

View File

@@ -10,9 +10,9 @@ import brut.androlib.ApkDecoder
import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.xml.ResXmlUtils
import brut.androlib.res.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile
import java.io.InputStream
import java.io.OutputStream
@@ -31,82 +31,79 @@ class ResourcePatchContext internal constructor(
) : PatchContext<PatcherResult.PatchedResources?> {
private val logger = Logger.getLogger(ResourcePatchContext::class.java.name)
/**
* Read a document from an [InputStream].
*/
fun document(inputStream: InputStream) = Document(inputStream)
/**
* Read and write documents in the [PatcherConfig.apkFiles].
*/
fun document(path: String) = Document(get(path))
val document = DocumentOperatable()
/**
* Set of resources from [PatcherConfig.apkFiles] to delete.
* Predicate to delete resources from [PatcherConfig.apkFiles].
*/
private val deleteResources = mutableSetOf<String>()
private val deleteResources = mutableSetOf<(String) -> Boolean>()
/**
* Decode resources of [PatcherConfig.apkFile].
*
* @param mode The [ResourceMode] to use.
*/
internal fun decodeResources(mode: ResourceMode) = with(packageMetadata.apkInfo) {
config.initializeTemporaryFilesDirectories()
internal fun decodeResources(mode: ResourceMode) =
with(packageMetadata.apkInfo) {
config.initializeTemporaryFilesDirectories()
// Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
// Needed to decode resources.
val resourcesDecoder = ResourcesDecoder(config.resourceConfig, this)
if (mode == ResourceMode.FULL) {
logger.info("Decoding resources")
if (mode == ResourceMode.FULL) {
logger.info("Decoding resources")
resourcesDecoder.decodeResources(config.apkFiles)
resourcesDecoder.decodeManifest(config.apkFiles)
resourcesDecoder.decodeResources(config.apkFiles)
resourcesDecoder.decodeManifest(config.apkFiles)
// Needed to record uncompressed files.
ApkDecoder(this, config.resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping)
// Needed to record uncompressed files.
val apkDecoder = ApkDecoder(config.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework =
UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
} else {
logger.info("Decoding app manifest")
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
AndroidManifestPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.newXmlSerializer(),
).decode(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { // Do nothing.
usesFramework =
UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
},
)
} else {
logger.info("Decoding app manifest")
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// AndroidManifestPullStreamDecoder.decode() sets metadata.apkInfo.
packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.resXmlSerializer,
).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { // Do nothing.
}
},
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let {
metadata.packageVersion = it.versionName ?: it.versionCode
}
/*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
because ResourcesDecoder.decodeResources loads the main package
and not XmlPullStreamDecoder.decodeManifest.
See ARSCDecoder.readTableType for more info.
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/
metadata.apkInfo.sparseResources = false
}
/*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here,
because ResourcesDecoder.decodeResources loads the main package
and not AndroidManifestPullStreamDecoder.decode.
See ARSCDecoder.readTableType for more info.
Set this to false again to prevent the ResTable from being flagged as sparse falsely.
*/
metadata.apkInfo.sparseResources = false
}
}
}
/**
* Compile resources in [PatcherConfig.apkFiles].
@@ -128,10 +125,10 @@ class ResourcePatchContext internal constructor(
AaptInvoker(
config.resourceConfig,
packageMetadata.apkInfo,
).invoke(
).invokeAapt(
resources.resolve("resources.apk"),
config.apkFiles.resolve("AndroidManifest.xml").also {
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
},
config.apkFiles.resolve("res"),
null,
@@ -204,11 +201,11 @@ class ResourcePatchContext internal constructor(
}
/**
* Mark a file for deletion when the APK is rebuilt.
* Stage a file to be deleted from [PatcherConfig.apkFile].
*
* @param name The name of the file to delete.
* @param shouldDelete The predicate to stage the file for deletion given its name.
*/
fun delete(name: String) = deleteResources.add(name)
fun stageDelete(shouldDelete: (String) -> Boolean) = deleteResources.add(shouldDelete)
/**
* How to handle resources decoding and compiling.
@@ -230,4 +227,10 @@ class ResourcePatchContext internal constructor(
*/
NONE,
}
inner class DocumentOperatable {
operator fun get(inputStream: InputStream) = Document(inputStream)
operator fun get(path: String) = Document(this@ResourcePatchContext[path])
}
}

View File

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

View File

@@ -5,7 +5,6 @@ import java.io.Closeable
import java.io.File
import java.io.InputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
@@ -35,22 +34,15 @@ class Document internal constructor(
readerCount.remove(it)
}
val transformer = TransformerFactory.newInstance().newTransformer()
// Set to UTF-16 to prevent surrogate pairs from being escaped to invalid numeric character references, but save as UTF-8.
if (isAndroid) {
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16")
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
it.bufferedWriter(charset = Charsets.UTF_8).use { writer ->
transformer.transform(DOMSource(this), StreamResult(writer))
}
} else {
transformer.transform(DOMSource(this), StreamResult(it))
it.outputStream().use { stream ->
TransformerFactory.newInstance()
.newTransformer()
.transform(DOMSource(this), StreamResult(stream))
}
}
}
private companion object {
private val readerCount = mutableMapOf<File, Int>()
private val isAndroid = System.getProperty("java.runtime.name") == "Android Runtime"
}
}

View File

@@ -12,11 +12,11 @@ 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
import kotlin.reflect.KProperty
/**
* A navigator for methods.
*
* @param context The [BytecodePatchContext] to use.
* @param startMethod The [Method] to start navigating from.
*
* @constructor Creates a new [MethodNavigator].
@@ -24,16 +24,12 @@ import kotlin.reflect.KProperty
* @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference.
*/
context(BytecodePatchContext)
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) {
private var lastNavigatedMethodReference = startMethod
private val lastNavigatedMethodInstructions
get() = with(original()) {
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
}
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.
@@ -42,7 +38,7 @@ class MethodNavigator internal constructor(
*
* @return This [MethodNavigator].
*/
fun to(vararg index: Int): MethodNavigator {
fun at(vararg index: Int): MethodNavigator {
index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
}
@@ -56,7 +52,7 @@ class MethodNavigator internal constructor(
* @param index The index of the method to navigate to.
* @param predicate The predicate to match.
*/
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
fun at(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index)
@@ -80,22 +76,15 @@ class MethodNavigator internal constructor(
*
* @return The last navigated method mutably.
*/
fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
fun mutable() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
as MutableMethod
/**
* Get the last navigated method mutably.
*
* @return The last navigated method mutably.
*/
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = stop()
/**
* Get the last navigated method immutably.
*
* @return The last navigated method immutably.
*/
fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
fun immutable() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/**
* Predicate to match the class defining the current method reference.
@@ -107,10 +96,9 @@ class MethodNavigator internal constructor(
/**
* Find the first [lastNavigatedMethodReference] in the class.
*/
private val ClassDef.firstMethodBySignature
get() = methods.first {
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
}
private val ClassDef.firstMethodBySignature get() = methods.first {
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
}
/**
* An exception thrown when navigating fails.

View File

@@ -50,7 +50,7 @@ class InlineSmaliCompiler {
registers,
instructions,
)
val reader = InputStreamReader(input.byteInputStream(), Charsets.UTF_8)
val reader = InputStreamReader(input.byteInputStream())
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)

View File

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

View File

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