Compare commits

...

26 Commits

Author SHA1 Message Date
oSumAtrIX
926ec96291 fix test 2025-10-16 23:16:43 +02:00
oSumAtrIX
e74498aff5 feat: Add callback for patch loading exceptions 2025-10-16 22:46:51 +02:00
semantic-release-bot
3a8b2ba935 chore: Release v21.1.0-dev.5 [skip ci]
# [21.1.0-dev.5](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.4...v21.1.0-dev.5) (2025-10-16)
2025-10-16 15:03:36 +00:00
dependabot[bot]
39c5a66ce3 build(Needs bump): Bump dependencies 2025-10-16 17:01:58 +02:00
semantic-release-bot
b160a2adc0 chore: Release v21.1.0-dev.4 [skip ci]
# [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](33fadcbd0c))
2025-07-18 19:33:42 +00:00
kitadai31
33fadcbd0c fix: Correctly save XML files in UTF-8 by using a bufferedWriter (#356) 2025-07-18 21:31:51 +02:00
semantic-release-bot
68db95b99b chore: Release v21.1.0-dev.3 [skip ci]
# [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](4f2ef3c47c))
2025-06-20 14:44:18 +00:00
Pg
4f2ef3c47c fix: Encode XML files as UTF-8 to fix compilation of resources (#339)
Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-06-20 16:41:53 +02:00
semantic-release-bot
062ae14936 chore: Release v21.1.0-dev.2 [skip ci]
# [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](e4e66b0d8b))
* Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments ([#331](https://github.com/ReVanced/revanced-patcher/issues/331)) ([bb8771b](bb8771bb8b))

### Features

* Use option name as key for simplicity and consistency ([754b02e](754b02e4ca))

### Performance Improvements

* Use a buffered writer to reduce IO overhead ([#347](https://github.com/ReVanced/revanced-patcher/issues/347)) ([99f4318](99f431897e))
2025-06-20 13:28:31 +00:00
Pg
99f431897e perf: Use a buffered writer to reduce IO overhead (#347) 2025-06-20 15:26:10 +02:00
oSumAtrIX
d80abbcd17 docs: Correct API usage of fingerprints 2025-03-10 13:52:09 +01:00
oSumAtrIX
509ecc81e1 docs: Correct API usage of fingerprints 2025-03-10 13:47:55 +01:00
kitadai31
e4e66b0d8b fix: Add back missing log by naming logger correctly (#332) 2025-01-20 00:40:26 +01:00
Vologhat
bb8771bb8b fix: Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments (#331) 2025-01-07 01:30:21 +01:00
oSumAtrIX
754b02e4ca feat: Use option name as key for simplicity and consistency 2024-12-24 16:47:48 +01:00
oSumAtrIX
fe5fb736cb build: Bump dependencies 2024-12-17 04:20:28 +01:00
semantic-release-bot
fc505a8726 chore: Release v21.1.0-dev.1 [skip ci]
# [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](88a3252574))
2024-12-07 05:20:15 +00:00
oSumAtrIX
88a3252574 feat: Add identity hash code to unnamed patches 2024-12-07 06:19:08 +01:00
semantic-release-bot
ead701bdaf chore: Release v21.0.0 [skip ci]
# [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](5d996def4d))
* Merge extension only when patch executes ([#315](https://github.com/ReVanced/revanced-patcher/issues/315)) ([aa472eb](aa472eb985))

### Features

* Improve Fingerprint API ([#316](https://github.com/ReVanced/revanced-patcher/issues/316)) ([0abf1c6](0abf1c6c02))
* Improve various APIs  ([#317](https://github.com/ReVanced/revanced-patcher/issues/317)) ([b824978](b8249789df))
* Move fingerprint match members to fingerprint for ease of access by using context receivers ([0746c22](0746c22743))

### Performance Improvements

* Use smallest lookup map for strings ([1358d3f](1358d3fa10))

### BREAKING CHANGES

* Various APIs have been changed.
* Many APIs have been changed.
2024-11-05 18:18:41 +00:00
oSumAtrIX
0581dcf931 chore: Merge branch dev to main 2024-11-05 19:16:28 +01:00
semantic-release-bot
62191e3c4a chore: Release v21.0.0-dev.4 [skip ci]
# [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](1358d3fa10))
2024-11-05 13:41:06 +00:00
oSumAtrIX
1358d3fa10 perf: Use smallest lookup map for strings 2024-11-05 14:39:18 +01:00
semantic-release-bot
6712f0ea72 chore: Release v21.0.0-dev.3 [skip ci]
# [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](0746c22743))
2024-11-05 13:25:22 +00:00
oSumAtrIX
0746c22743 feat: Move fingerprint match members to fingerprint for ease of access by using context receivers 2024-11-05 14:23:19 +01:00
semantic-release-bot
7f55868e6f chore: Release v21.0.0-dev.2 [skip ci]
# [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](5d996def4d))
2024-11-01 01:49:47 +00:00
oSumAtrIX
5d996def4d fix: Match fingerprint before delegating the match property 2024-11-01 02:47:57 +01:00
24 changed files with 1471 additions and 967 deletions

View File

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

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- 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@v4
uses: actions/checkout@v5
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@v1
uses: burrunan/gradle-cache-action@v3
- name: Build
env:
@@ -32,7 +32,7 @@ jobs:
run: ./gradlew build clean
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: "lts/*"
cache: 'npm'

View File

@@ -1,3 +1,91 @@
# [21.1.0-dev.5](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.4...v21.1.0-dev.5) (2025-10-16)
# [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)

View File

@@ -1,4 +1,22 @@
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 class app/revanced/patcher/FingerprintBuilder {
@@ -31,13 +49,11 @@ public final class app/revanced/patcher/Match {
}
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;
}
@@ -57,6 +73,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
}
@@ -146,10 +164,6 @@ public final class app/revanced/patcher/patch/BytecodePatchContext : app/revance
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 getMatch (Lapp/revanced/patcher/Fingerprint;)Lapp/revanced/patcher/Match;
public final fun getValue (Lapp/revanced/patcher/Fingerprint;Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/Match;
public final fun match (Lapp/revanced/patcher/Fingerprint;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/Match;
public final fun navigate (Lcom/android/tools/smali/dexlib2/iface/reference/MethodReference;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
}
@@ -157,9 +171,12 @@ public final class app/revanced/patcher/patch/BytecodePatchContext : app/revance
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;
@@ -325,8 +342,12 @@ 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 final fun loadPatchesFromDex (Ljava/util/Set;Ljava/io/File;Lkotlin/jvm/functions/Function2;)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 synthetic fun loadPatchesFromDex$default (Ljava/util/Set;Ljava/io/File;Lkotlin/jvm/functions/Function2;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 loadPatchesFromJar (Ljava/util/Set;Lkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/PatchLoader$Jar;
public static synthetic fun loadPatchesFromJar$default (Ljava/util/Set;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)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;
@@ -335,7 +356,7 @@ public final class app/revanced/patcher/patch/PatchKt {
public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, kotlin/jvm/internal/markers/KMappedMarker {
public synthetic fun <init> (Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/util/Set;Lkotlin/jvm/functions/Function1;Ljava/lang/ClassLoader;Lkotlin/jvm/functions/Function2;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
@@ -356,12 +377,9 @@ public abstract class app/revanced/patcher/patch/PatchLoader : java/util/Set, ko
}
public final class app/revanced/patcher/patch/PatchLoader$Dex : app/revanced/patcher/patch/PatchLoader {
public fun <init> (Ljava/util/Set;Ljava/io/File;)V
public synthetic fun <init> (Ljava/util/Set;Ljava/io/File;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class app/revanced/patcher/patch/PatchLoader$Jar : app/revanced/patcher/patch/PatchLoader {
public fun <init> (Ljava/util/Set;)V
}
public final class app/revanced/patcher/patch/PatchResult {
@@ -468,12 +486,12 @@ public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w
}
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 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 class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList {

View File

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

View File

@@ -117,15 +117,19 @@ With this information, the original code can be reconstructed:
```java
package com.some.app.ads;
<accessFlags> class AdsLoader {
public final boolean <methodName>(boolean <parameter>) {
<accessFlags>
class AdsLoader {
public final boolean <methodName>(boolean <parameter>)
{
// ...
var userStatus = "pro";
// ...
return <returnValue>;
return <returnValue >;
}
}
```
@@ -134,13 +138,14 @@ Using that fingerprint, this method can be matched uniquely from all other metho
> [!TIP]
> A fingerprint should contain information about a method likely to remain the same across updates.
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app.
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same.
> A 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
A fingerprint is matched to a method,
once the `match` property of the fingerprint is accessed in a patch's `execute` scope:
After declaring a fingerprint, it can be used in a patch to find the method it matches to:
```kt
val fingerprint = fingerprint {
@@ -149,52 +154,34 @@ val fingerprint = fingerprint {
val patch = bytecodePatch {
execute {
val match = fingerprint.match!!
fingerprint.method
}
}
```
The fingerprint won't be matched again, if it has already been matched once.
This makes it useful, to share fingerprints between multiple patches, and let the first patch match the fingerprint:
The fingerprint won't be matched again, if it has already been matched once, for performance reasons.
This makes it useful, to share fingerprints between multiple patches,
and let the first executing patch match the fingerprint:
```kt
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
val mainActivityPatch1 = bytecodePatch {
execute {
val match = mainActivityOnCreateFingerprint.match!!
mainActivityOnCreateFingerprint.method
}
}
val mainActivityPatch2 = bytecodePatch {
execute {
val match = mainActivityOnCreateFingerprint.match!!
}
}
```
A fingerprint match can also be delegated to a variable for convenience without the need to check for `null`:
```kt
val fingerprint = fingerprint {
// ...
}
val patch = bytecodePatch {
execute {
// Alternative to fingerprint.match ?: throw PatchException("No match found")
val match by fingerprint.match
try {
match.method
} catch (e: PatchException) {
// Handle the exception for example.
}
mainActivityOnCreateFingerprint.method
}
}
```
> [!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.
> If the fingerprint can not be matched to any method,
> accessing certain properties of the fingerprint will raise an exception.
> Instead, the `orNull` properties can be used to return `null` if no match is found.
> [!TIP]
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
@@ -211,47 +198,43 @@ val patch = bytecodePatch {
> )
>}
> ```
>
The match of a fingerprint contains references to the original method and class definition of the method:
```kt
class Match(
val originalMethod: Method,
val originalClassDef: ClassDef,
val patternMatch: Match.PatternMatch?,
val stringMatches: List<Match.StringMatch>?,
// ...
) {
val classDef by lazy { /* ... */ }
val method by lazy { /* ... */ }
The following properties can be accessed in a fingerprint:
// ...
}
```
- `originalClassDef`: The original class definition the fingerprint matches to.
- `originalClassDefOrNull`: The original class definition the fingerprint matches to.
- `originalMethod`: The original method the fingerprint matches to.
- `originalMethodOrNull`: The original method the fingerprint matches to.
- `classDef`: The class the fingerprint matches to.
- `classDefOrNull`: The class the fingerprint matches to.
- `method`: The method the fingerprint matches to. If no match is found, an exception is raised.
- `methodOrNull`: The method the fingerprint matches to.
The `classDef` and `method` properties can be used to make changes to the class or method.
They are lazy properties, so they are only computed
and will effectively replace the original method or class definition when accessed.
The difference between the `original` and non-`original` properties is that the `original` properties return the
original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
The mutable copies can be modified. They are lazy properties, so they are only computed
and only then will effectively replace the `original` method or class definition when accessed.
> [!TIP]
> If only read-only access to the class or method is needed,
> the `originalClassDef` and `originalMethod` properties can be used,
> If only read-only access to the class or method is needed,
> the `originalClassDef` and `originalMethod` properties should be used,
> to avoid making a mutable copy of the class or method.
## 🏹 Manually matching fingerprints
By default, a fingerprint is matched automatically against all classes when the `match` property is accessed.
By default, a fingerprint is matched automatically against all classes
when one of the fingerprint's properties is accessed.
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function:
- In a **list of classes**, if the fingerprint can match in a known subset of classes
If you have a known list of classes you know the fingerprint can match in,
you can match the fingerprint on the list of classes:
you can match the fingerprint on the list of classes:
```kt
execute {
val match = showAdsFingerprint.match(classes) ?: throw PatchException("No match found")
val match = showAdsFingerprint(classes)
}
```
@@ -263,23 +246,24 @@ you can match the fingerprint on the list of classes:
execute {
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }
val match = showAdsFingerprint.match(context, adsLoaderClass) ?: throw PatchException("No match found")
val match = showAdsFingerprint.match(adsLoaderClass)
}
```
Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.
```kt
execute {
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
val match by showAdsFingerprint.match(adsLoaderClassFingerprint.match!!.classDef)
val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
}
```
- Match a **single method**, to extract certain information about it
The match of a fingerprint contains useful information about the method,
such as the start and end index of an opcode pattern or the indices of the instructions with certain string references.
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
@@ -288,14 +272,19 @@ you can match the fingerprint on the list of classes:
strings("free", "trial")
}
currentPlanFingerprint.match(adsFingerprintMatch.method)?.let { match ->
currentPlanFingerprint.match(adsFingerprint.method).let { match ->
match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}")
}
} ?: throw PatchException("No match found")
}
}
```
> [!WARNING]
> If the fingerprint can not be matched to any method, calling `match` will raise an
> exception.
> Instead, the `orNull` overloads can be used to return `null` if no match is found.
> [!TIP]
> To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).

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.
dependsOn(disableAdsResourcePatch)
// Merge precompiled DEX files into the patched app, before the patch is executed.
extendWith("disable-ads.rve")
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch.
val showAdsMatch by showAdsFingerprint {
// More about fingerprints on the next page of the documentation.
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.
showAdsMatch.method.addInstructions(
showAdsFingerprint.method.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(key = "option")
val value by stringOption(name = "Inbuilt option")
// Add an option with a custom type and delegate it to a property.
val string by option<String>(key = "string")
val string by option<String>(name = "String option")
execute {
println(value)
println(string)
@@ -139,7 +139,7 @@ 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"
}
```
@@ -152,7 +152,7 @@ option.type // The KType of the option. Captures the full type information of th
Options can be declared outside a patch and added to a patch manually:
```kt
val option = stringOption(key = "option")
val option = stringOption(name = "Option")
bytecodePatch(name = "Patch") {
val value by option()
@@ -183,18 +183,18 @@ and use it in a patch:
```kt
val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve")
execute {
fingerprint.match!!.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
execute {
fingerprint.method.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 +211,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 +249,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

@@ -46,17 +46,17 @@ The `navigate(Method)` function allows you to navigate method calls recursively
```kt
execute {
// Sequentially navigate to the instructions at index 1 within 'someMethod'.
val method = navigate(someMethod).at(1).original() // original() returns the original immutable method.
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).at(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
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).at(1)
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).at(1).at(2, 3, 4).at(5)
val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
}
```
@@ -85,7 +85,7 @@ execute {
The `document` function is used to read and write DOM files.
```kt
execute {
execute {
document("res/values/strings.xml").use { document ->
val element = doc.createElement("string").apply {
textContent = "Hello, World!"
@@ -112,5 +112,6 @@ 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.0.0-dev.1
version = 21.1.0-dev.5

View File

@@ -1,14 +1,14 @@
[versions]
android = "4.1.1.4"
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"
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"
multidexlib2 = "3.0.3.r3"
# Tracking https://github.com/google/smali/issues/64.
#noinspection GradleDependency
smali = "3.0.5"
smali = "3.0.9"
xpp3 = "1.1.4c"
[libraries]

1391
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.1.2"
"semantic-release": "^24.2.9"
}
}

View File

@@ -3,8 +3,8 @@
package app.revanced.patcher
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.MethodClassPairs
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@@ -44,17 +44,21 @@ class Fingerprint internal constructor(
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: Match? = null
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
// Backing property for "match" extension in BytecodePatchContext.
@Suppress("ktlint:standard:backing-property-naming", "PropertyName")
internal var _match: Match? = null
context(BytecodePatchContext)
private val matchOrNull: Match?
get() = matchOrNull()
/**
* Match using [BytecodePatchContext.LookupMaps].
* Match using [BytecodePatchContext.lookupMaps].
*
* Generally faster than the other [_match] overloads when there are many methods to check for a match.
* Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match.
*
* Fingerprints can be optimized for performance:
* - Slowest: Specify [custom] or [opcodes] and nothing else.
@@ -62,29 +66,26 @@ class Fingerprint internal constructor(
* - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
*
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
internal fun match(context: BytecodePatchContext): Match? {
if (_match != null) return _match
context(BytecodePatchContext)
internal fun matchOrNull(): Match? {
if (_matchOrNull != null) return _matchOrNull
val lookupMaps = context.lookupMaps
fun Fingerprint.match(methodClasses: MethodClassPairs): Match? {
var match = strings?.mapNotNull {
lookupMaps.methodsByStrings[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
methodClasses.forEach { (classDef, method) ->
val match = match(context, classDef, method)
if (match != null) return match
val match = matchOrNull(classDef, method)
if (match != null) return@let match
}
return null
null
}
// TODO: If only one string is necessary, why not use a single string for every fingerprint?
val match = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }?.let(::match)
if (match != null) return match
context.classes.forEach { classDef ->
val match = match(context, classDef)
classes.forEach { classDef ->
match = matchOrNull(classDef)
if (match != null) return match
}
@@ -95,18 +96,17 @@ class Fingerprint internal constructor(
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
internal fun match(
context: BytecodePatchContext,
context(BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): Match? {
if (_match != null) return _match
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = match(context, method, classDef)
if (match != null)return match
val match = matchOrNull(method, classDef)
if (match != null) return match
}
return null
@@ -117,28 +117,26 @@ class Fingerprint internal constructor(
* The class is retrieved from the method.
*
* @param method The method to match against.
* @param context The [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
internal fun match(
context: BytecodePatchContext,
context(BytecodePatchContext)
fun matchOrNull(
method: Method,
) = match(context, method, context.classBy { method.definingClass == it.type }!!.immutableClass)
) = matchOrNull(method, classBy { method.definingClass == it.type }!!.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 [BytecodePatchContext] to match against [BytecodePatchContext.classes].
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
internal fun match(
context: BytecodePatchContext,
context(BytecodePatchContext)
fun matchOrNull(
method: Method,
classDef: ClassDef,
): Match? {
if (_match != null) return _match
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
return null
@@ -243,33 +241,189 @@ class Fingerprint internal constructor(
null
}
_match = Match(
classDef,
_matchOrNull = Match(
method,
patternMatch,
stringMatches,
context,
classDef,
)
return _match
return _matchOrNull
}
private val exception get() = PatchException("Failed to match the fingerprint: $this")
/**
* The match for this [Fingerprint].
*
* @throws PatchException If the [Fingerprint] has not been matched.
*/
context(BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
/**
* Match using a [Method].
*
* @param method The method to match against.
* @param classDef The class the method is a member of.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
/**
* The class the matching method is a member of.
*/
context(BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
/**
* The matching method.
*/
context(BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
/**
* The mutable version of [originalClassDefOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDefOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val classDefOrNull
get() = matchOrNull?.classDef
/**
* The mutable version of [originalMethodOrNull].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethodOrNull] if mutable access is not required.
*/
context(BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
/**
* The match for the opcode pattern.
*/
context(BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
/**
* The matches for the strings.
*/
context(BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
/**
* The class the matching method is a member of.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val originalClassDef
get() = match.originalClassDef
/**
* The matching method.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val originalMethod
get() = match.originalMethod
/**
* The mutable version of [originalClassDef].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val classDef
get() = match.classDef
/**
* The mutable version of [originalMethod].
*
* Accessing this property allocates a [ClassProxy].
* Use [originalMethod] if mutable access is not required.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val method
get() = match.method
/**
* The match for the opcode pattern.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val patternMatch
get() = match.patternMatch
/**
* The matches for the strings.
*
* @throws PatchException If the fingerprint has not been matched.
*/
context(BytecodePatchContext)
val stringMatches
get() = match.stringMatches
}
/**
* A match for a [Fingerprint].
* A match of a [Fingerprint].
*
* @param originalClassDef The class the matching method is a member of.
* @param originalMethod The matching method.
* @param patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings.
* @param context The context to create mutable proxies in.
*/
context(BytecodePatchContext)
class Match internal constructor(
val originalClassDef: ClassDef,
val originalMethod: Method,
val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?,
internal val context: BytecodePatchContext,
val originalClassDef: ClassDef,
) {
/**
* The mutable version of [originalClassDef].
@@ -277,7 +431,7 @@ class Match internal constructor(
* Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required.
*/
val classDef by lazy { context.proxy(originalClassDef).mutableClass }
val classDef by lazy { proxy(originalClassDef).mutableClass }
/**
* The mutable version of [originalMethod].
@@ -292,7 +446,7 @@ class Match internal constructor(
* @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(
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
@@ -303,7 +457,7 @@ class Match internal constructor(
* @param string The string that matched.
* @param index The index of the instruction in the method.
*/
class StringMatch(val string: String, val index: Int)
class StringMatch internal constructor(val string: String, val index: Int)
}
/**

View File

@@ -16,9 +16,28 @@ import java.util.logging.Logger
class PatcherConfig(
internal val apkFile: File,
private val temporaryFilesPath: File = File("revanced-temporary-files"),
aaptBinaryPath: String? = null,
aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null,
) {
/**
* 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)
/**
@@ -33,8 +52,7 @@ class PatcherConfig(
*/
internal val resourceConfig =
Config.getDefaultConfig().apply {
useAapt2 = true
aaptPath = aaptBinaryPath ?: ""
aaptBinary = aaptBinaryPath
frameworkDirectory = frameworkFileDirectory
}

View File

@@ -1,6 +1,8 @@
package app.revanced.patcher.patch
import app.revanced.patcher.*
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
@@ -22,7 +24,6 @@ import java.io.Closeable
import java.io.FileFilter
import java.util.*
import java.util.logging.Logger
import kotlin.reflect.KProperty
/**
* A context for patches containing the current state of the bytecode.
@@ -33,7 +34,7 @@ import kotlin.reflect.KProperty
class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable {
private val logger = Logger.getLogger(BytecodePatchContext::class.java.name)
private val logger = Logger.getLogger(this::class.java.name)
/**
* [Opcodes] of the supplied [PatcherConfig.apkFile].
@@ -53,36 +54,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
).also { opcodes = it.opcodes }.classes.toMutableList(),
)
/**
* The match for this [Fingerprint]. Null if unmatched.
*/
val Fingerprint.match get() = match(this@BytecodePatchContext)
/**
* Match using a [ClassDef].
*
* @param classDef The class to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
fun Fingerprint.match(classDef: ClassDef) = match(this@BytecodePatchContext, classDef)
/**
* Match using a [Method].
* The class is retrieved from the method.
*
* @param method The method to match against.
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise.
*/
fun Fingerprint.match(method: Method) = match(this@BytecodePatchContext, method)
/**
* Get the match for this [Fingerprint].
*
* @throws IllegalStateException If the [Fingerprint] has not been matched.
*/
operator fun Fingerprint.getValue(nothing: Nothing?, property: KProperty<*>): Match = _match
?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".")
/**
* The lookup maps for methods and the class they are a member of from the [classes].
*/
@@ -137,9 +108,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
*
* @return A proxy for the class.
*/
fun proxy(classDef: ClassDef) = this@BytecodePatchContext.classes.proxyPool.find {
fun proxy(classDef: ClassDef) = classes.proxyPool.find {
it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) }
} ?: ClassProxy(classDef).also { classes.proxyPool.add(it) }
/**
* Navigate a method.
@@ -148,7 +119,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(this@BytecodePatchContext, method)
fun navigate(method: MethodReference) = MethodNavigator(method)
/**
* Compile bytecode from the [BytecodePatchContext].
@@ -227,28 +198,6 @@ 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,16 +20,51 @@ import kotlin.reflect.typeOf
* @constructor Create a new [Option].
*/
@Suppress("MemberVisibilityCanBePrivate", "unused")
class Option<T> @PublishedApi internal constructor(
class Option<T>
@PublishedApi
@Deprecated("Use the constructor with the name instead of a key instead.")
internal constructor(
@Deprecated("Use the name property instead.")
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].
*/
@@ -109,7 +144,7 @@ class Option<T> @PublishedApi 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.key })
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.name })
/**
* Set an option's value.
@@ -856,14 +891,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.key}")
class ValueValidationException(value: Any?, option: Option<*>) : OptionException("The option value \"$value\" failed validation for ${option.name}")
/**
* 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 the value was null")
class ValueRequiredException(option: Option<*>) : OptionException("The option ${option.name} requires a value, but the value was null")
/**
* An exception thrown when a [Option] is not found.

View File

@@ -87,7 +87,7 @@ sealed class Patch<C : PatchContext<*>>(
finalizeBlock?.invoke(context)
}
override fun toString() = name ?: "Patch"
override fun toString() = name ?: "Patch@${System.identityHashCode(this)}"
}
internal fun Patch<*>.anyRecursively(
@@ -161,7 +161,7 @@ class BytecodePatch internal constructor(
override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext)
override fun toString() = name ?: "BytecodePatch"
override fun toString() = name ?: "Bytecode${super.toString()}"
}
/**
@@ -204,7 +204,7 @@ class RawResourcePatch internal constructor(
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "RawResourcePatch"
override fun toString() = name ?: "RawResource${super.toString()}"
}
/**
@@ -247,7 +247,7 @@ class ResourcePatch internal constructor(
override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "ResourcePatch"
override fun toString() = name ?: "Resource${super.toString()}"
}
/**
@@ -528,11 +528,11 @@ fun resourcePatch(
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
* @param message The exception message.
* @param cause The cause of the exception.
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
class PatchException(message: String?, cause: Throwable?) : Exception(message, cause) {
constructor(message: String) : this(message, null)
constructor(cause: Throwable) : this(cause.message, cause)
}
@@ -542,6 +542,7 @@ class PatchException(errorMessage: String?, cause: Throwable?) : Exception(error
* @param patch The [Patch] that was executed.
* @param exception The [PatchException] thrown, if any.
*/
@Deprecated("This class is not used anymore. Instead a callback is used")
class PatchResult internal constructor(val patch: Patch<*>, val exception: PatchException? = null)
/**
@@ -552,36 +553,47 @@ class PatchResult internal constructor(val patch: Patch<*>, val exception: Patch
*
* @param byPatchesFile The patches associated by the patches file they were loaded from.
*/
sealed class PatchLoader private constructor(
sealed class PatchLoader(
val byPatchesFile: Map<File, Set<Patch<*>>>,
) : Set<Patch<*>> by byPatchesFile.values.flatten().toSet() {
/**
* @param patchesFiles A set of JAR or DEX files to load the patches from.
* @param getBinaryClassNames A function that returns the binary names of all classes accessible by the class loader.
* @param classLoader The [ClassLoader] to use for loading the classes.
* @param onLoadPatchesException The callback for patches that could not be loaded.
*/
private constructor(
constructor(
patchesFiles: Set<File>,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
) : this(classLoader.loadPatches(patchesFiles.associateWith { getBinaryClassNames(it).toSet() }))
onLoadPatchesException: (message: String, cause: Throwable) -> Unit
) : this(
classLoader.loadPatches(
patchesFiles.associateWith { getBinaryClassNames(it).toSet() },
onLoadPatchesException
)
)
/**
* A [PatchLoader] for JAR files.
*
* @param patchesFiles The JAR files to load the patches from.
* @param onLoadPatchesException The callback for patches that could not be loaded.
*
* @constructor Create a new [PatchLoader] for JAR files.
*/
class Jar(patchesFiles: Set<File>) :
PatchLoader(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
)
class Jar internal constructor(
patchesFiles: Set<File>,
onLoadPatchesException: (message: String, cause: Throwable) -> Unit
) : PatchLoader(
patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
onLoadPatchesException
)
/**
* A [PatchLoader] for [Dex] files.
@@ -589,25 +601,30 @@ sealed class PatchLoader private constructor(
* @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.
* @param onLoadPatchesException The callback for patches that could not be loaded.
*
* @constructor Create a new [PatchLoader] for [Dex] files.
*/
class Dex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) :
PatchLoader(
patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
optimizedDexDirectory?.absolutePath,
null,
this::class.java.classLoader,
),
)
class Dex internal constructor(
patchesFiles: Set<File>,
optimizedDexDirectory: File? = null,
onLoadPatchesException: (message: String, cause: Throwable) -> Unit
) : 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,
),
onLoadPatchesException
)
// Companion object required for unit tests.
private companion object {
@@ -639,19 +656,24 @@ sealed class PatchLoader private constructor(
*
* @param binaryClassNamesByPatchesFile The binary class name of the classes to load the patches from
* associated by the patches file.
* @param onLoadPatchesException The callback for patches that could not be loaded.
*
* @return The loaded patches associated by the patches file.
*/
private fun ClassLoader.loadPatches(binaryClassNamesByPatchesFile: Map<File, Set<String>>) =
binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) ->
binaryClassNames.asSequence().map {
loadClass(it)
}.flatMap {
it.patchFields + it.patchMethods
}.filter {
it.name != null
}.toSet()
}
private fun ClassLoader.loadPatches(
binaryClassNamesByPatchesFile: Map<File, Set<String>>,
onLoadPatchesException: (message: String, cause: Throwable) -> Unit
) = binaryClassNamesByPatchesFile.mapValues { (_, binaryClassNames) ->
binaryClassNames.asSequence().mapNotNull {
runCatching { loadClass(it) }.onFailure { exception ->
onLoadPatchesException("Failed to load patch class $it", exception)
}.getOrNull()
}.flatMap {
it.patchFields + it.patchMethods
}.filter {
it.name != null
}.toSet()
}
private fun Member.canAccess(): Boolean {
if (this is Method && parameterCount != 0) return false
@@ -667,11 +689,22 @@ sealed class PatchLoader private constructor(
* Patches with no name are not loaded.
*
* @param patchesFiles The JAR files to load the patches from.
* @param onLoadPatchesException The callback for patches that could not be loaded.
*
* @return The loaded patches.
*/
fun loadPatchesFromJar(patchesFiles: Set<File>) =
PatchLoader.Jar(patchesFiles)
fun loadPatchesFromJar(
patchesFiles: Set<File>,
onLoadPatchesException: ((message: String, cause: Throwable) -> Unit)? = null
) = PatchLoader.Jar(patchesFiles, onLoadPatchesException ?: { message, cause -> })
@Deprecated(
"Use the function with the onLoadPatchesException overload",
replaceWith = ReplaceWith("loadPatchesFromJar(patchesFiles, null)")
)
fun loadPatchesFromJar(
patchesFiles: Set<File>
) = loadPatchesFromJar(patchesFiles, null)
/**
* Loads patches from DEX files declared as public static fields
@@ -679,8 +712,21 @@ fun loadPatchesFromJar(patchesFiles: Set<File>) =
* Patches with no name are not loaded.
*
* @param patchesFiles The DEX files to load the patches from.
* @param onLoadPatchesException The callback for patches that could not be loaded.
*
* @return The loaded patches.
*/
fun loadPatchesFromDex(patchesFiles: Set<File>, optimizedDexDirectory: File? = null) =
PatchLoader.Dex(patchesFiles, optimizedDexDirectory)
fun loadPatchesFromDex(
patchesFiles: Set<File>,
optimizedDexDirectory: File? = null,
onLoadPatchesException: ((message: String, cause: Throwable) -> Unit)? = null
) = PatchLoader.Dex(patchesFiles, optimizedDexDirectory, onLoadPatchesException ?: { message, cause -> })
@Deprecated(
"Use the function with the onLoadPatchesException overload",
replaceWith = ReplaceWith("loadPatchesFromJar(patchesFiles, optimizedDexDirectory, null)")
)
fun loadPatchesFromDex(
patchesFiles: Set<File>,
optimizedDexDirectory: File? = null,
) = loadPatchesFromDex(patchesFiles, optimizedDexDirectory, null)

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.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher
import brut.androlib.res.xml.ResXmlUtils
import brut.directory.ExtFile
import java.io.InputStream
import java.io.OutputStream
@@ -51,64 +51,62 @@ class ResourcePatchContext internal constructor(
*
* @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.
val apkDecoder = ApkDecoder(config.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
// Needed to record uncompressed files.
ApkDecoder(this, config.resourceConfig).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.
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
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.
}
},
)
// 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
}
/*
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].
@@ -130,10 +128,10 @@ class ResourcePatchContext internal constructor(
AaptInvoker(
config.resourceConfig,
packageMetadata.apkInfo,
).invokeAapt(
).invoke(
resources.resolve("resources.apk"),
config.apkFiles.resolve("AndroidManifest.xml").also {
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
},
config.apkFiles.resolve("res"),
null,

View File

@@ -5,6 +5,7 @@ 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
@@ -34,15 +35,22 @@ class Document internal constructor(
readerCount.remove(it)
}
it.outputStream().use { stream ->
TransformerFactory.newInstance()
.newTransformer()
.transform(DOMSource(this), StreamResult(stream))
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))
}
}
}
private companion object {
private val readerCount = mutableMapOf<File, Int>()
private val isAndroid = System.getProperty("java.runtime.name") == "Android Runtime"
}
}

View File

@@ -17,7 +17,6 @@ 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].
@@ -25,12 +24,16 @@ 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.
*/
class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) {
context(BytecodePatchContext)
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
private var lastNavigatedMethodReference = startMethod
private val lastNavigatedMethodInstructions get() = with(original()) {
instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.")
}
private val lastNavigatedMethodInstructions
get() = with(original()) {
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
}
/**
* Navigate to the method at the specified index.
@@ -39,7 +42,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
*
* @return This [MethodNavigator].
*/
fun at(vararg index: Int): MethodNavigator {
fun to(vararg index: Int): MethodNavigator {
index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
}
@@ -53,7 +56,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
* @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 {
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index)
@@ -77,7 +80,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
*
* @return The last navigated method mutably.
*/
fun stop() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
as MutableMethod
/**
@@ -92,7 +95,7 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
*
* @return The last navigated method immutably.
*/
fun original() = context.classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
fun original(): Method = classes.first(matchesCurrentMethodReferenceDefiningClass).firstMethodBySignature
/**
* Predicate to match the class defining the current method reference.
@@ -104,9 +107,10 @@ class MethodNavigator internal constructor(private val context: BytecodePatchCon
/**
* 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())
val reader = InputStreamReader(input.byteInputStream(), Charsets.UTF_8)
val lexer: LexerErrorInterface = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)

View File

@@ -3,21 +3,18 @@ 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.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import jdk.internal.module.ModuleBootstrap.patcher
import io.mockk.*
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll
import java.util.logging.Logger
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
internal object PatcherTest {
private lateinit var patcher: Patcher
@@ -153,10 +150,10 @@ internal object PatcherTest {
val patch = bytecodePatch {
execute {
// Fingerprint can never match.
val match by fingerprint { }
val fingerprint = fingerprint { }
// Throws, because the fingerprint can't be matched.
match.patternMatch
fingerprint.patternMatch
}
}
@@ -193,11 +190,6 @@ internal object PatcherTest {
),
),
)
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match } } answers { callOriginal() }
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<ClassDef>()) } } answers { callOriginal() }
every { with(patcher.context.bytecodeContext) { any<Fingerprint>().match(any<Method>()) } } answers { callOriginal() }
every { patcher.context.bytecodeContext.classBy(any()) } answers { callOriginal() }
every { patcher.context.bytecodeContext.proxy(any()) } answers { callOriginal() }
val fingerprint = fingerprint { returns("V") }
val fingerprint2 = fingerprint { returns("V") }
@@ -208,19 +200,21 @@ internal object PatcherTest {
execute {
fingerprint.match(classes.first().methods.first())
fingerprint2.match(classes.first())
fingerprint3.match
fingerprint3.originalClassDef
}
},
)
patches()
assertAll(
"Expected fingerprints to match.",
{ assertNotNull(fingerprint._match) },
{ assertNotNull(fingerprint2._match) },
{ assertNotNull(fingerprint3._match) },
)
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> {

View File

@@ -71,6 +71,7 @@ internal object PatchLoaderTest {
patchLoaderCompanionObject,
TEST_PATCHES_CLASS_LOADER,
mapOf(File("patchesFile") to setOf(TEST_PATCHES_CLASS)),
null
).values.first()
assertEquals(