Compare commits

..

24 Commits

Author SHA1 Message Date
semantic-release-bot
76c262ff12 chore(release): 20.0.0-dev.2 [skip ci]
# [20.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0-dev.1...v20.0.0-dev.2) (2024-07-31)

### Bug Fixes

* Downgrade smali to fix dex compilation issue ([714447d](714447de70))
* Merge all extensions before initializing lookup maps ([328aa87](328aa876d8))
* Use null for compatible package version when adding packages only ([a8e8fa4](a8e8fa4093))
2024-07-31 00:27:24 +00:00
oSumAtrIX
714447de70 fix: Downgrade smali to fix dex compilation issue 2024-07-31 02:25:32 +02:00
oSumAtrIX
328aa876d8 fix: Merge all extensions before initializing lookup maps 2024-07-26 04:45:31 +02:00
oSumAtrIX
a8e8fa4093 fix: Use null for compatible package version when adding packages only 2024-07-26 03:41:32 +02:00
oSumAtrIX
c482dff17c refactor: Convert method bodies to single expression functions 2024-07-26 03:10:08 +02:00
oSumAtrIX
e6de1f6b4c build(Needs bump): Bump dependencies 2024-07-26 03:09:32 +02:00
oSumAtrIX
82a2b3c371 docs: Fix syntax issues and improve wording 2024-07-23 19:55:30 +02:00
semantic-release-bot
9ce772a597 chore(release): 20.0.0-dev.1 [skip ci]
# [20.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.3.1...v20.0.0-dev.1) (2024-07-22)

### Features

* Convert APIs to Kotlin DSL ([#298](https://github.com/ReVanced/revanced-patcher/issues/298)) ([3f9cbd2](3f9cbd2408))

### BREAKING CHANGES

* Various old APIs are removed, and DSL APIs are added instead.
2024-07-22 01:11:38 +00:00
oSumAtrIX
3f9cbd2408 feat: Convert APIs to Kotlin DSL (#298)
This commit converts various APIs to Kotlin DSL.

BREAKING CHANGE: Various old APIs are removed, and DSL APIs are added instead.
2024-07-21 22:45:45 +02:00
oSumAtrIX
1ff0830249 ci: Correct usage of repository variable 2024-07-13 00:44:51 +02:00
oSumAtrIX
7be131d348 docs: Improve issue templates 2024-05-26 00:43:39 +02:00
oSumAtrIX
1d78d690bb chore: Fix spelling mistake 2024-04-07 18:22:00 +02:00
Vologhat
042f554d75 refactor: Simplify mapping classes to their names (#290)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-04-07 17:54:37 +02:00
oSumAtrIX
f63302feab build: Publish sources 2024-03-14 12:21:48 +01:00
oSumAtrIX
1614d7f9f4 build: Set target bytecode level to JVM 11 2024-03-04 19:15:07 +01:00
oSumAtrIX
d64776c933 ci: Update action 2024-03-04 15:36:36 +01:00
oSumAtrIX
03cd9f7f54 chore: Lint code 2024-03-03 00:25:28 +01:00
oSumAtrIX
b69226dd26 build: Bump dependencies 2024-03-02 08:29:38 +01:00
oSumAtrIX
8e1117ed3f refactor: Properly abstract Patch#execute function 2024-02-26 16:45:03 +01:00
oSumAtrIX
29adcd5aad docs: Fix broken links 2024-02-26 04:37:45 +01:00
oSumAtrIX
6b2bc5ef4d docs: Fix docs link [skip ci] 2024-02-25 19:49:40 +01:00
oSumAtrIX
d862d61386 docs: Un-indent markdown to fix rendering 2024-02-25 04:50:44 +01:00
oSumAtrIX
a4e18334bc chore: Revert using a relative image path
For some reason GitHub does not render them correctly
2024-02-25 03:40:20 +01:00
oSumAtrIX
5bef74adb5 build: Bump dependencies 2024-02-25 03:40:20 +01:00
37 changed files with 2266 additions and 4003 deletions

View File

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

View File

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

View File

@@ -10,13 +10,10 @@ on:
jobs: jobs:
release: release:
name: Release name: Release
permissions:
contents: write
packages: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
# Make sure the release step uses its own credentials: # Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages # https://github.com/cycjimmy/semantic-release-action#private-packages
@@ -24,7 +21,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Cache Gradle - name: Cache Gradle
uses: burrunan/gradle-cache-action@v3 uses: burrunan/gradle-cache-action@v1
- name: Build - name: Build
env: env:
@@ -32,7 +29,7 @@ jobs:
run: ./gradlew build clean run: ./gradlew build clean
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v4
with: with:
node-version: "lts/*" node-version: "lts/*"
cache: 'npm' cache: 'npm'
@@ -49,5 +46,5 @@ jobs:
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release run: npm exec semantic-release

View File

@@ -23,8 +23,7 @@
"assets": [ "assets": [
"CHANGELOG.md", "CHANGELOG.md",
"gradle.properties" "gradle.properties"
], ]
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
} }
], ],
[ [

View File

@@ -1,209 +1,3 @@
# [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)
### 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)
### Bug Fixes
* Make it work on Android 12 and lower by using existing APIs ([#312](https://github.com/ReVanced/revanced-patcher/issues/312)) ([a44802e](https://github.com/ReVanced/revanced-patcher/commit/a44802ef4ebf59ae47213854ba761c81dadc51f3))
## [20.0.2-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1...v20.0.2-dev.1) (2024-10-15)
### Bug Fixes
* Make it work on Android 12 and lower by using existing APIs ([#312](https://github.com/ReVanced/revanced-patcher/issues/312)) ([a44802e](https://github.com/ReVanced/revanced-patcher/commit/a44802ef4ebf59ae47213854ba761c81dadc51f3))
## [20.0.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0...v20.0.1) (2024-10-13)
### Bug Fixes
* Check for class type exactly instead of with contains ([#310](https://github.com/ReVanced/revanced-patcher/issues/310)) ([69f2f20](https://github.com/ReVanced/revanced-patcher/commit/69f2f20fd99162f91cd9c531dfe47d00d3152ead))
* Make it work on Android by not using APIs from JVM unavailable to Android. ([2be6e97](https://github.com/ReVanced/revanced-patcher/commit/2be6e97817437f40e17893dfff3bea2cd4c3ff9e))
* Use non-nullable type for options ([ea6fc70](https://github.com/ReVanced/revanced-patcher/commit/ea6fc70caab055251ad4d0d3f1b5cf53865abb85))
### Performance Improvements
* Free memory earlier and remove negligible lookup maps ([d53aacd](https://github.com/ReVanced/revanced-patcher/commit/d53aacdad4ed3750ddae526fb307577ea36e6171))
## [20.0.1-dev.5](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.4...v20.0.1-dev.5) (2024-10-11)
### Bug Fixes
* Use non-nullable type for options ([ea6fc70](https://github.com/ReVanced/revanced-patcher/commit/ea6fc70caab055251ad4d0d3f1b5cf53865abb85))
## [20.0.1-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.3...v20.0.1-dev.4) (2024-10-07)
### Bug Fixes
* Make it work on Android by not using APIs from JVM unavailable to Android. ([2be6e97](https://github.com/ReVanced/revanced-patcher/commit/2be6e97817437f40e17893dfff3bea2cd4c3ff9e))
## [20.0.1-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.2...v20.0.1-dev.3) (2024-10-03)
### Performance Improvements
* Free memory earlier and remove negligible lookup maps ([d53aacd](https://github.com/ReVanced/revanced-patcher/commit/d53aacdad4ed3750ddae526fb307577ea36e6171))
## [20.0.1-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.1-dev.1...v20.0.1-dev.2) (2024-10-01)
## [20.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0...v20.0.1-dev.1) (2024-09-18)
### Bug Fixes
* Check for class type exactly instead of with contains ([#310](https://github.com/ReVanced/revanced-patcher/issues/310)) ([69f2f20](https://github.com/ReVanced/revanced-patcher/commit/69f2f20fd99162f91cd9c531dfe47d00d3152ead))
# [20.0.0](https://github.com/ReVanced/revanced-patcher/compare/v19.3.1...v20.0.0) (2024-08-06)
### Bug Fixes
* Downgrade smali to fix dex compilation issue ([5227e98](https://github.com/ReVanced/revanced-patcher/commit/5227e98abfaa2ff1204eb20a0f2671f58c489930))
* Improve exception message wording ([5481d0c](https://github.com/ReVanced/revanced-patcher/commit/5481d0c54ccecc91cd8d15af1ba2d3285a33e5ab))
* Make constructor internal as supposed ([7f44174](https://github.com/ReVanced/revanced-patcher/commit/7f44174d91f0af0d50a83d80a7103c779241e094))
* Merge all extensions before initializing lookup maps ([8c4dd5b](https://github.com/ReVanced/revanced-patcher/commit/8c4dd5b3a309077fa9a3827b4931fc28b0517809))
* Use null for compatible package version when adding packages only ([736b3ee](https://github.com/ReVanced/revanced-patcher/commit/736b3eebbfdd7279b8d5fcfc5c46c9e3aadbee12))
### Features
* Add ability to create options outside of a patch ([d310246](https://github.com/ReVanced/revanced-patcher/commit/d310246852504b08a15f6376bbf25ac7c6fae76f))
* Convert APIs to Kotlin DSL ([#298](https://github.com/ReVanced/revanced-patcher/issues/298)) ([11a911d](https://github.com/ReVanced/revanced-patcher/commit/11a911dc674eb0801649949dd3f28dfeb00efe97))
### BREAKING CHANGES
* Various old APIs are removed, and DSL APIs are added instead.
# [20.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0-dev.3...v20.0.0-dev.4) (2024-08-06)
### Bug Fixes
* Improve exception message wording ([bd434ce](https://github.com/ReVanced/revanced-patcher/commit/bd434ceb3394d1d5292e8b94e5bfd6da0e4e9c72))
# [20.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0-dev.2...v20.0.0-dev.3) (2024-08-01)
### Bug Fixes
* Make constructor internal as supposed ([e95fcd1](https://github.com/ReVanced/revanced-patcher/commit/e95fcd1c0b641164bbf0840ec7e562aeb3bacc3e))
### Features
* Add ability to create options outside of a patch ([b8d763a](https://github.com/ReVanced/revanced-patcher/commit/b8d763a66e0601627dd71c8c24247726aa300146))
# [20.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0-dev.1...v20.0.0-dev.2) (2024-07-31) # [20.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v20.0.0-dev.1...v20.0.0-dev.2) (2024-07-31)

View File

@@ -117,9 +117,9 @@ you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced
The documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches. The documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches.
You can find it [here](https://github.com/ReVanced/revanced-patcher/tree/main/docs). You can find it [here](https://github.com/ReVanced/revanced-patcher/tree/main/docs).
## 📜 License ## 📜 Licence
ReVanced Patcher is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information. ReVanced Patcher is licensed under the GPLv3 license. Please see the [licence file](LICENSE) for more information.
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patcher as long as you track changes/dates in source files. [tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patcher as long as you track changes/dates in source files.
Any modifications to ReVanced Patcher must also be made available under the GPL, Any modifications to ReVanced Patcher must also be made available under the GPL,
along with build & install instructions. along with build & install instructions.

View File

@@ -1,22 +1,7 @@
public final class app/revanced/patcher/Fingerprint { 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 getMatch ()Lapp/revanced/patcher/Match;
public final fun getClassDefOrNull (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z
public final fun getMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; public final fun match (Lapp/revanced/patcher/patch/BytecodePatchContext;Lcom/android/tools/smali/dexlib2/iface/Method;)Z
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 { 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 final class app/revanced/patcher/FingerprintKt {
public static final fun fingerprint (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/Fingerprint; 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 (ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/Fingerprint;
public static synthetic fun fingerprint$default (Lapp/revanced/patcher/patch/BytecodePatchBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatchBuilder$InvokedFingerprint;
} }
public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation { public abstract interface annotation class app/revanced/patcher/InternalApi : java/lang/annotation/Annotation {
} }
public final class app/revanced/patcher/Match { public final class app/revanced/patcher/Match {
public final fun getClassDef ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; 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 getMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; public final fun getClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef;
public final fun getOriginalClassDef ()Lcom/android/tools/smali/dexlib2/iface/ClassDef; public final fun getMethod ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun getOriginalMethod ()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 getPatternMatch ()Lapp/revanced/patcher/Match$PatternMatch;
public final fun getStringMatches ()Ljava/util/List; public final fun getStringMatches ()Ljava/util/List;
} }
public final class app/revanced/patcher/Match$PatternMatch { public final class app/revanced/patcher/Match$PatternMatch {
public fun <init> (II)V
public final fun getEndIndex ()I public final fun getEndIndex ()I
public final fun getStartIndex ()I public final fun getStartIndex ()I
} }
public final class app/revanced/patcher/Match$StringMatch { public final class app/revanced/patcher/Match$StringMatch {
public fun <init> (Ljava/lang/String;I)V
public final fun getIndex ()I public final fun getIndex ()I
public final fun getString ()Ljava/lang/String; 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 final class app/revanced/patcher/PatcherConfig {
public fun <init> (Ljava/io/File;Ljava/io/File;Ljava/io/File;Ljava/lang/String;)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/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public synthetic fun <init> (Ljava/io/File;Ljava/io/File;Ljava/lang/String;Ljava/lang/String;ZILkotlin/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 final class app/revanced/patcher/PatcherContext : java/io/Closeable { public final class app/revanced/patcher/PatcherContext : java/io/Closeable {
@@ -147,36 +135,41 @@ public final class app/revanced/patcher/extensions/InstructionExtensions {
} }
public final class app/revanced/patcher/patch/BytecodePatch : app/revanced/patcher/patch/Patch { 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 fun toString ()Ljava/lang/String;
} }
public final class app/revanced/patcher/patch/BytecodePatchBuilder : app/revanced/patcher/patch/PatchBuilder { 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 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 extendWith (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatchBuilder;
public final fun getExtensionInputStream ()Ljava/util/function/Supplier; public final fun getExtension ()Ljava/io/InputStream;
public final fun setExtensionInputStream (Ljava/util/function/Supplier;)V 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 fun <init> (Lapp/revanced/patcher/Fingerprint;)V
public final fun getValue (Ljava/lang/Void;Lkotlin/reflect/KProperty;)Lapp/revanced/patcher/Match;
} }
public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable { public final class app/revanced/patcher/patch/BytecodePatchContext : app/revanced/patcher/patch/PatchContext, java/io/Closeable {
public final fun classBy (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/ClassProxy; public final fun classBy (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 fun close ()V
public synthetic fun get ()Ljava/lang/Object; public synthetic fun get ()Ljava/lang/Object;
public fun get ()Ljava/util/Set; public fun get ()Ljava/util/Set;
public final fun getClasses ()Lapp/revanced/patcher/util/ProxyClassList; 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 fun proxy (Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Lapp/revanced/patcher/util/proxy/ClassProxy;
} }
public final class app/revanced/patcher/patch/Option { 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 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 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 getDefault ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String; public final fun getDescription ()Ljava/lang/String;
public final fun getKey ()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 getRequired ()Z
public final fun getTitle ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String;
public final fun getType ()Lkotlin/reflect/KType; public final fun getType ()Lkotlin/reflect/KType;
@@ -212,43 +205,25 @@ public final class app/revanced/patcher/patch/OptionException$ValueValidationExc
public final class app/revanced/patcher/patch/OptionKt { public final class app/revanced/patcher/patch/OptionKt {
public static final fun booleanOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun booleanOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun booleanOption (Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleanOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun booleanOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleanOption$default (Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun booleansOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun booleansOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun booleansOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleansOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun booleansOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun booleansOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun floatOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun floatOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun floatOption (Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun floatOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatOption$default (Ljava/lang/String;Ljava/lang/Float;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun floatsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun floatsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun floatsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun floatsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun intOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun intOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun intOption (Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun intOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intOption$default (Ljava/lang/String;Ljava/lang/Integer;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun intsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun intsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun intsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun intsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun intsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun longOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun longOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun longOption (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun longOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longOption$default (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun longsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun longsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun longsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun longsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun longsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun stringOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun stringOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun stringOption (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun stringOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringOption$default (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static final fun stringsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option; public static final fun stringsOption (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static final fun stringsOption (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option; public static synthetic fun stringsOption$default (Lapp/revanced/patcher/patch/PatchBuilder;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
public static synthetic fun stringsOption$default (Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/Option;
} }
public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker { public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jvm/internal/markers/KMappedMarker {
@@ -294,7 +269,7 @@ public final class app/revanced/patcher/patch/Options : java/util/Map, kotlin/jv
} }
public abstract class app/revanced/patcher/patch/Patch { public abstract class app/revanced/patcher/patch/Patch {
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;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 execute (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V public final fun finalize (Lapp/revanced/patcher/patch/PatchContext;)V
public final fun getCompatiblePackages ()Ljava/util/Set; public final fun getCompatiblePackages ()Ljava/util/Set;
@@ -311,13 +286,13 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun compatibleWith ([Ljava/lang/String;)V public final fun compatibleWith ([Ljava/lang/String;)V
public final fun compatibleWith ([Lkotlin/Pair;)V public final fun compatibleWith ([Lkotlin/Pair;)V
public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V public final fun dependsOn ([Lapp/revanced/patcher/patch/Patch;)V
public final fun execute (Lkotlin/jvm/functions/Function1;)V public final fun execute (Lkotlin/jvm/functions/Function2;)V
public final fun finalize (Lkotlin/jvm/functions/Function1;)V public final fun finalize (Lkotlin/jvm/functions/Function2;)V
protected final fun getCompatiblePackages ()Ljava/util/Set; protected final fun getCompatiblePackages ()Ljava/util/Set;
protected final fun getDependencies ()Ljava/util/Set; protected final fun getDependencies ()Ljava/util/Set;
protected final fun getDescription ()Ljava/lang/String; protected final fun getDescription ()Ljava/lang/String;
protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function1; protected final fun getExecutionBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function1; protected final fun getFinalizeBlock ()Lkotlin/jvm/functions/Function2;
protected final fun getName ()Ljava/lang/String; protected final fun getName ()Ljava/lang/String;
protected final fun getOptions ()Ljava/util/Set; protected final fun getOptions ()Ljava/util/Set;
protected final fun getUse ()Z protected final fun getUse ()Z
@@ -325,8 +300,8 @@ public abstract class app/revanced/patcher/patch/PatchBuilder {
public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair; public final fun invoke (Ljava/lang/String;[Ljava/lang/String;)Lkotlin/Pair;
protected final fun setCompatiblePackages (Ljava/util/Set;)V protected final fun setCompatiblePackages (Ljava/util/Set;)V
protected final fun setDependencies (Ljava/util/Set;)V protected final fun setDependencies (Ljava/util/Set;)V
protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function1;)V protected final fun setExecutionBlock (Lkotlin/jvm/functions/Function2;)V
protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function1;)V protected final fun setFinalizeBlock (Lkotlin/jvm/functions/Function2;)V
} }
public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier { public abstract interface class app/revanced/patcher/patch/PatchContext : java/util/function/Supplier {
@@ -403,13 +378,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 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 fun get ()Lapp/revanced/patcher/PatcherResult$PatchedResources;
public synthetic fun get ()Ljava/lang/Object; public synthetic fun get ()Ljava/lang/Object;
public final fun get (Ljava/lang/String;Z)Ljava/io/File; 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 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 { public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w3c/dom/Document {
@@ -485,12 +465,11 @@ public final class app/revanced/patcher/util/Document : java/io/Closeable, org/w
} }
public final class app/revanced/patcher/util/MethodNavigator { 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 at (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator;
public final fun original ()Lcom/android/tools/smali/dexlib2/iface/Method; public final fun at ([I)Lapp/revanced/patcher/util/MethodNavigator;
public final fun stop ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; 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 to (ILkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/MethodNavigator; public final fun immutable ()Lcom/android/tools/smali/dexlib2/iface/Method;
public final fun to ([I)Lapp/revanced/patcher/util/MethodNavigator; public final fun mutable ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;
public static synthetic fun to$default (Lapp/revanced/patcher/util/MethodNavigator;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/MethodNavigator;
} }
public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList { public final class app/revanced/patcher/util/ProxyClassList : java/util/List, kotlin/jvm/internal/markers/KMutableList {

View File

@@ -36,28 +36,25 @@ repositories {
} }
dependencies { dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.xpp3)
implementation(libs.smali)
implementation(libs.multidexlib2)
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
// TODO: Convert project to KMP. // TODO: Convert project to KMP.
compileOnly(libs.android) { compileOnly(libs.android) {
// Exclude, otherwise the org.w3c.dom API breaks. // Exclude, otherwise the org.w3c.dom API breaks.
exclude(group = "xerces", module = "xmlParserAPIs") exclude(group = "xerces", module = "xmlParserAPIs")
} }
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(libs.xpp3)
testImplementation(libs.mockk)
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
testImplementation(libs.mockk)
} }
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(JvmTarget.JVM_11) 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 { runBlocking {
patcher().collect { patchResult -> patcher().collect { patchResult ->
if (patchResult.exception != null) if (patchResult.exception != null)
logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" } logger.info("\"${patchResult.patch}\" failed:\n${patchResult.exception}")
else 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. 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 1. Clone the repository
```bash ```bash

View File

@@ -60,16 +60,14 @@
# 🔎 Fingerprinting # 🔎 Fingerprinting
In the context of ReVanced, a fingerprint is a partial description of a method. In the context of ReVanced, fingerprinting is primarily used to match methods with a limited amount of known information.
It is used to uniquely match a method by its characteristics.
Fingerprinting is used to match methods with a limited amount of known information.
Methods with obfuscated names that change with each update are primary candidates for fingerprinting. Methods with obfuscated names that change with each update are primary candidates for fingerprinting.
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type, The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
access flags, an opcode pattern, strings, and more. access flags, an opcode pattern, strings, and more.
## ⛳️ Example fingerprint ## ⛳️ Example fingerprint
An example fingerprint is shown below: Throughout the documentation, the following example will be used to demonstrate the concepts of fingerprints:
```kt ```kt
@@ -81,11 +79,11 @@ fingerprint {
parameters("Z") parameters("Z")
opcodes(Opcode.RETURN) opcodes(Opcode.RETURN)
strings("pro") strings("pro")
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" } custom { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;" }
} }
``` ```
## 🔎 Reconstructing the original code from the example fingerprint from above ## 🔎 Reconstructing the original code from a fingerprint
The following code is reconstructed from the fingerprint to understand how a fingerprint is created. The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
@@ -109,43 +107,36 @@ The fingerprint contains the following information:
- Package and class name: - Package and class name:
```kt ```kt
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" } custom = { (method, classDef) -> method.definingClass == "Lcom/some/app/ads/AdsLoader;"}
``` ```
With this information, the original code can be reconstructed: With this information, the original code can be reconstructed:
```java ```java
package com.some.app.ads; package com.some.app.ads;
<accessFlags> <accessFlags> class AdsLoader {
public final boolean <methodName>(boolean <parameter>) {
class AdsLoader {
public final boolean <methodName>(boolean <parameter>)
{
// ... // ...
var userStatus = "pro"; var userStatus = "pro";
// ... // ...
return <returnValue >; return <returnValue>;
}
} }
}
``` ```
Using that fingerprint, this method can be matched uniquely from all other methods.
> [!TIP] > [!TIP]
> A fingerprint should contain information about a method likely to remain the same across updates. > A fingerprint should contain information about a method likely to remain the same across updates.
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated > A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated app.
> app. > In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the same.
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
> same.
## 🔨 How to use fingerprints ## 🔨 How to use fingerprints
After declaring a fingerprint, it can be used in a patch to find the method it matches to: Fingerprints can be added to a patch by directly creating and adding them or by invoking them manually.
Fingerprints added to a patch are matched by ReVanced Patcher before the patch is executed.
```kt ```kt
val fingerprint = fingerprint { val fingerprint = fingerprint {
@@ -153,35 +144,18 @@ val fingerprint = fingerprint {
} }
val patch = bytecodePatch { val patch = bytecodePatch {
execute { // Directly create and add a fingerprint.
fingerprint.method fingerprint {
// ...
} }
// Add a fingerprint manually by invoking it.
fingerprint()
} }
``` ```
The fingerprint won't be matched again, if it has already been matched once, for performance reasons. > [!TIP]
This makes it useful, to share fingerprints between multiple patches, > Multiple patches can share fingerprints. If a fingerprint is matched once, it will not be matched again.
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 {
mainActivityOnCreateFingerprint.method
}
}
val mainActivityPatch2 = bytecodePatch {
execute {
mainActivityOnCreateFingerprint.method
}
}
```
> [!WARNING]
> 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] > [!TIP]
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode` > If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
@@ -199,42 +173,62 @@ val mainActivityPatch2 = bytecodePatch {
>} >}
> ``` > ```
The following properties can be accessed in a fingerprint: Once the fingerprint is matched, the match can be used in the patch:
- `originalClassDef`: The original class definition the fingerprint matches to. ```kt
- `originalClassDefOrNull`: The original class definition the fingerprint matches to. val patch = bytecodePatch {
- `originalMethod`: The original method the fingerprint matches to. // Add a fingerprint and delegate its match to a variable.
- `originalMethodOrNull`: The original method the fingerprint matches to. val match by showAdsFingerprint()
- `classDef`: The class the fingerprint matches to. val match2 by fingerprint {
- `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 difference between the `original` and non-`original` properties is that the `original` properties return the execute {
original class or method definition, while the non-`original` properties return a mutable copy of the class or method. val method = match.method
The mutable copies can be modified. They are lazy properties, so they are only computed val method2 = match2.method
and only then will effectively replace the `original` method or class definition when accessed. }
}
```
> [!TIP] > [!WARNING]
> If only read-only access to the class or method is needed, > If the fingerprint can not be matched to any method, the match of a fingerprint is `null`. If such a match is delegated
> the `originalClassDef` and `originalMethod` properties should be used, > to a variable, accessing it will raise an exception.
> to avoid making a mutable copy of the class or method.
## 🏹 Manually matching fingerprints The match of a fingerprint contains mutable and immutable references to the method and the class it matches to.
By default, a fingerprint is matched automatically against all classes ```kt
when one of the fingerprint's properties is accessed. class Match(
val method: Method,
val classDef: ClassDef,
val patternMatch: Match.PatternMatch?,
val stringMatches: List<Match.StringMatch>?,
// ...
) {
val mutableClass by lazy { /* ... */ }
val mutableMethod by lazy { /* ... */ }
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function: // ...
}
```
## 🏹 Manual matching of fingerprints
Unless a fingerprint is added to a patch, the fingerprint will not be matched automatically by ReVanced Patcher
before the patch is executed.
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function.
You can match a fingerprint the following ways:
- In a **list of classes**, if the fingerprint can match in a known subset of classes - In a **list of classes**, if the fingerprint can match in a known subset of classes
If you have a known list of classes you know the fingerprint can match in, 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 ```kt
execute { execute { context ->
val match = showAdsFingerprint(classes) val match = showAdsFingerprint.apply {
match(context, context.classes)
}.match ?: throw PatchException("No match found")
} }
``` ```
@@ -243,48 +237,37 @@ Instead, the fingerprint can be matched manually using various overloads of a fi
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class: If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
```kt ```kt
execute { execute { context ->
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" } val adsLoaderClass = context.classes.single { it.name == "Lcom/some/app/ads/Loader;" }
val match = showAdsFingerprint.match(adsLoaderClass) val match = showAdsFingerprint.apply {
} match(context, adsLoaderClass)
``` }.match ?: throw PatchException("No match found")
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 = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
} }
``` ```
- Match a **single method**, to extract certain information about it - Match a **single method**, to extract certain information about it
The match of a fingerprint contains useful information about the method, The match of a fingerprint contains useful information about the method, such as the start and end index of an opcode pattern
such as the start and end index of an opcode pattern or the indices of the instructions with certain string or the indices of the instructions with certain string references.
references.
A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out: A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
```kt ```kt
execute { execute { context ->
val currentPlanFingerprint = fingerprint { val proStringsFingerprint = fingerprint {
strings("free", "trial") strings("free", "trial")
} }
currentPlanFingerprint.match(adsFingerprint.method).let { match -> proStringsFingerprint.apply {
match(context, adsFingerprintMatch.method)
}.match?.let { match ->
match.stringMatches.forEach { match -> match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}") println("The index of the string '${match.string}' is ${match.index}")
} }
} } ?: throw PatchException("No match found")
} }
``` ```
> [!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] > [!TIP]
> To see real-world examples of fingerprints, > To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches). > check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).

View File

@@ -76,23 +76,23 @@ val disableAdsPatch = bytecodePatch(
) { ) {
compatibleWith("com.some.app"("1.0.0")) 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) 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") extendWith("disable-ads.rve")
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch. // Fingerprint to find the method to patch.
val showAdsFingerprint = fingerprint { val showAdsMatch by showAdsFingerprint {
// More about fingerprints on the next page of the documentation. // More about fingerprints on the next page of the documentation.
} }
// Business logic of the patch to disable ads in the app.
execute {
// In the method that shows ads, // In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file) // call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads. // to enable or disable ads.
showAdsFingerprint.method.addInstructions( showAdsMatch.mutableMethod.addInstructions(
0, 0,
""" """
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
@@ -122,10 +122,10 @@ To define an option, use the available `option` functions:
```kt ```kt
val patch = bytecodePatch(name = "Patch") { val patch = bytecodePatch(name = "Patch") {
// Add an inbuilt option and delegate it to a property. // 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. // 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 { execute {
println(value) println(value)
@@ -139,28 +139,16 @@ Options of a patch can be set after loading the patches with `PatchLoader` by ob
```kt ```kt
loadPatchesJar(patches).apply { loadPatchesJar(patches).apply {
// Type is checked at runtime. // 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: The type of an option can be obtained from the `type` property of the option:
```kt ```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:
```kt
val option = stringOption(name = "Option")
bytecodePatch(name = "Patch") {
val value by option()
}
```
This is useful when the same option is referenced in multiple patches.
### 🧩 Extensions ### 🧩 Extensions
An extension is a precompiled DEX file merged into the patched app before a patch is executed. An extension is a precompiled DEX file merged into the patched app before a patch is executed.
@@ -184,8 +172,10 @@ and use it in a patch:
val patch = bytecodePatch(name = "Complex patch") { val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve") extendWith("complex-patch.rve")
val match by methodFingerprint()
execute { execute {
fingerprint.method.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V") match.mutableMethod.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
} }
} }
``` ```

View File

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

View File

@@ -4,107 +4,18 @@ A handful of APIs are available to make patch development easier and more effici
## 📙 Overview ## 📙 Overview
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)` 1. 👹 Mutate classes with `context.proxy(ClassDef)`
2. 🔍 Find and create mutable replaces with `classBy(Predicate)` 2. 🔍 Find and proxy existing classes with `classBy(Predicate)` and `classByType(String)`
3. 🏃‍ Navigate method calls recursively by index with `navigate(Method)` 3. 🏃‍ Easily access referenced methods recursively by index with `MethodNavigator`
4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)` 4. 🔨 Make use of extension functions from `BytecodeUtils` and `ResourceUtils` with certain applications
5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)` (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 ### 🧰 APIs
#### 👹 `proxy(ClassDef)` > [!WARNING]
> This section is still under construction and may be incomplete.
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 ->
// ...
}
}
```
## 🎉 Afterword ## 🎉 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, 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 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, 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 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,
feature request,
ReVanced ReVanced

View File

@@ -1,3 +1,3 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
version = 21.1.0-dev.5 version = 20.0.0-dev.2

View File

@@ -1,22 +1,22 @@
[versions] [versions]
android = "4.1.1.4" android = "4.1.1.4"
apktool-lib = "2.10.1.1" apktool-lib = "2.9.3"
binary-compatibility-validator = "0.18.1" kotlin = "2.0.0"
kotlin = "2.0.20" kotlinx-coroutines-core = "1.8.1"
kotlinx-coroutines-core = "1.10.2" mockk = "1.13.10"
mockk = "1.14.5"
multidexlib2 = "3.0.3.r3" multidexlib2 = "3.0.3.r3"
# Tracking https://github.com/google/smali/issues/64. # Tracking https://github.com/google/smali/issues/64.
#noinspection GradleDependency #noinspection GradleDependency
smali = "3.0.9" smali = "3.0.5"
binary-compatibility-validator = "0.15.1"
xpp3 = "1.1.4c" xpp3 = "1.1.4c"
[libraries] [libraries]
android = { module = "com.google.android:android", version.ref = "android" } android = { module = "com.google.android:android", version.ref = "android" }
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" } apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" } multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }

Binary file not shown.

View File

@@ -1,8 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dist

22
gradlew vendored
View File

@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -85,9 +83,7 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -148,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -156,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -205,11 +201,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command;
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# and any embedded shellness will be escaped. # shell script including quotes and variable substitutions, so put them in
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # double quotes to make sure that they get re-expanded; and
# treated as '${Hostname}' itself on the command line. # * put everything else in single quotes, so that it's not re-expanded.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail

4043
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
"@saithodev/semantic-release-backmerge": "^4.0.1", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.10.1", "gradle-semantic-release-plugin": "^1.9.1",
"semantic-release": "^24.2.9" "semantic-release": "^23.0.2"
} }
} }

View File

@@ -3,8 +3,10 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull import app.revanced.patcher.extensions.InstructionExtensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.BytecodePatchContext import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps.Companion.appendParameters
import app.revanced.patcher.patch.MethodClassPairs
import app.revanced.patcher.util.proxy.ClassProxy import app.revanced.patcher.util.proxy.ClassProxy
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
@@ -15,17 +17,7 @@ import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
/** /**
* A fingerprint for a method. A fingerprint is a partial description of a method. * A fingerprint.
* It is used to uniquely match a method by its characteristics.
*
* An example fingerprint for a public method that takes a single string parameter and returns void:
* ```
* fingerprint {
* accessFlags(AccessFlags.PUBLIC)
* returns("V")
* parameters("Ljava/lang/String;")
* }
* ```
* *
* @param accessFlags The exact access flags using values of [AccessFlags]. * @param accessFlags The exact access flags using values of [AccessFlags].
* @param returnType The return type. Compared using [String.startsWith]. * @param returnType The return type. Compared using [String.startsWith].
@@ -44,21 +36,16 @@ class Fingerprint internal constructor(
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?, internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int, 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. * The match for this [Fingerprint]. Null if unmatched.
*/ */
context(BytecodePatchContext) var match: Match? = null
private val matchOrNull: Match? private set
get() = matchOrNull()
/** /**
* Match using [BytecodePatchContext.lookupMaps]. * Match using [BytecodePatchContext.LookupMaps].
* *
* Generally faster than the other [matchOrNull] overloads when there are many methods to check for a match. * Generally faster than the other [match] overloads when there are many methods to check for a match.
* *
* Fingerprints can be optimized for performance: * Fingerprints can be optimized for performance:
* - Slowest: Specify [custom] or [opcodes] and nothing else. * - Slowest: Specify [custom] or [opcodes] and nothing else.
@@ -66,50 +53,69 @@ class Fingerprint internal constructor(
* - Faster: Specify [accessFlags], [returnType] and [parameters]. * - Faster: Specify [accessFlags], [returnType] and [parameters].
* - Fastest: Specify [strings], with at least one string being an exact (non-partial) match. * - Fastest: Specify [strings], with at least one string being an exact (non-partial) match.
* *
* @return The [Match] if a match was found or if the fingerprint is already matched to a method, null otherwise. * @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/ */
context(BytecodePatchContext) internal fun match(context: BytecodePatchContext): Boolean {
internal fun matchOrNull(): Match? { val lookupMaps = context.lookupMaps
if (_matchOrNull != null) return _matchOrNull
var match = strings?.mapNotNull { fun Fingerprint.match(methodClasses: MethodClassPairs): Boolean {
lookupMaps.methodsByStrings[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
methodClasses.forEach { (classDef, method) -> methodClasses.forEach { (classDef, method) ->
val match = matchOrNull(classDef, method) if (match(context, classDef, method)) return true
if (match != null) return@let match }
return false
} }
null // TODO: If only one string is necessary, why not use a single string for every fingerprint?
} fun Fingerprint.lookupByStrings() = strings?.firstNotNullOfOrNull { lookupMaps.methodsByStrings[it] }
if (match != null) return match if (lookupByStrings()?.let(::match) == true) {
return true
classes.forEach { classDef ->
match = matchOrNull(classDef)
if (match != null) return match
} }
return null // No strings declared or none matched (partial matches are allowed).
// Use signature matching.
fun Fingerprint.lookupBySignature(): MethodClassPairs {
if (accessFlags == null) return lookupMaps.allMethods
var returnTypeValue = returnType
if (returnTypeValue == null) {
if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) {
// Constructors always have void return type.
returnTypeValue = "V"
} else {
return lookupMaps.allMethods
}
}
val signature =
buildString {
append(accessFlags)
append(returnTypeValue.first())
appendParameters(parameters ?: return@buildString)
}
return lookupMaps.methodsBySignature[signature] ?: return MethodClassPairs()
}
return match(lookupBySignature())
} }
/** /**
* Match using a [ClassDef]. * Match using a [ClassDef].
* *
* @param classDef The class to match against. * @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. * @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/ */
context(BytecodePatchContext) fun match(
fun matchOrNull( context: BytecodePatchContext,
classDef: ClassDef, classDef: ClassDef,
): Match? { ): Boolean {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) { for (method in classDef.methods) {
val match = matchOrNull(method, classDef) if (match(context, method, classDef)) {
if (match != null) return match return true
} }
}
return null return false
} }
/** /**
@@ -117,33 +123,35 @@ class Fingerprint internal constructor(
* The class is retrieved from the method. * The class is retrieved from the method.
* *
* @param method The method to match against. * @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. * @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/ */
context(BytecodePatchContext) fun match(
fun matchOrNull( context: BytecodePatchContext,
method: Method, method: Method,
) = matchOrNull(method, classBy { method.definingClass == it.type }!!.immutableClass) ) = match(context, method, context.classByType(method.definingClass)!!.immutableClass)
/** /**
* Match using a [Method]. * Match using a [Method].
* *
* @param method The method to match against. * @param method The method to match against.
* @param classDef The class the method is a member of. * @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. * @param context The context to create mutable proxies for the matched method and its class.
* @return True if a match was found or if the fingerprint is already matched to a method, false otherwise.
*/ */
context(BytecodePatchContext) internal fun match(
fun matchOrNull( context: BytecodePatchContext,
method: Method, method: Method,
classDef: ClassDef, classDef: ClassDef,
): Match? { ): Boolean {
if (_matchOrNull != null) return _matchOrNull if (match != null) return true
if (returnType != null && !method.returnType.startsWith(returnType)) { if (returnType != null && !method.returnType.startsWith(returnType)) {
return null return false
} }
if (accessFlags != null && accessFlags != method.accessFlags) { if (accessFlags != null && accessFlags != method.accessFlags) {
return null return false
} }
fun parametersEqual( fun parametersEqual(
@@ -160,17 +168,17 @@ class Fingerprint internal constructor(
// TODO: parseParameters() // TODO: parseParameters()
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) { if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
return null return false
} }
if (custom != null && !custom.invoke(method, classDef)) { if (custom != null && !custom.invoke(method, classDef)) {
return null return false
} }
val stringMatches: List<Match.StringMatch>? = val stringMatches: List<Match.StringMatch>? =
if (strings != null) { if (strings != null) {
buildList { buildList {
val instructions = method.instructionsOrNull ?: return null val instructions = method.instructionsOrNull ?: return false
val stringsList = strings.toMutableList() val stringsList = strings.toMutableList()
@@ -190,14 +198,14 @@ class Fingerprint internal constructor(
stringsList.removeAt(index) stringsList.removeAt(index)
} }
if (stringsList.isNotEmpty()) return null if (stringsList.isNotEmpty()) return false
} }
} else { } else {
null null
} }
val patternMatch = if (opcodes != null) { val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return null val instructions = method.instructionsOrNull ?: return false
fun patternScan(): Match.PatternMatch? { fun patternScan(): Match.PatternMatch? {
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
@@ -236,217 +244,61 @@ class Fingerprint internal constructor(
return null return null
} }
patternScan() ?: return null patternScan() ?: return false
} else { } else {
null null
} }
_matchOrNull = Match( match = Match(
method, method,
classDef,
patternMatch, patternMatch,
stringMatches, stringMatches,
classDef, context,
) )
return _matchOrNull return true
} }
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 of a [Fingerprint]. * A match for a [Fingerprint].
* *
* @param originalClassDef The class the matching method is a member of. * @param method The matching method.
* @param originalMethod The matching method. * @param classDef The class the matching method is a member of.
* @param patternMatch The match for the opcode pattern. * @param patternMatch The match for the opcode pattern.
* @param stringMatches The matches for the strings. * @param stringMatches The matches for the strings.
* @param context The context to create mutable proxies in.
*/ */
context(BytecodePatchContext) class Match(
class Match internal constructor( val method: Method,
val originalMethod: Method, val classDef: ClassDef,
val patternMatch: PatternMatch?, val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?, val stringMatches: List<StringMatch>?,
val originalClassDef: ClassDef, internal val context: BytecodePatchContext,
) { ) {
/** /**
* The mutable version of [originalClassDef]. * The mutable version of [classDef].
* *
* Accessing this property allocates a [ClassProxy]. * Accessing this property allocates a [ClassProxy].
* Use [originalClassDef] if mutable access is not required. * Use [classDef] if mutable access is not required.
*/ */
val classDef by lazy { proxy(originalClassDef).mutableClass } val mutableClass by lazy { context.proxy(classDef).mutableClass }
/** /**
* The mutable version of [originalMethod]. * The mutable version of [method].
* *
* Accessing this property allocates a [ClassProxy]. * Accessing this property allocates a [ClassProxy].
* Use [originalMethod] if mutable access is not required. * Use [method] if mutable access is not required.
*/ */
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } } val mutableMethod by lazy { mutableClass.methods.first { MethodUtil.methodSignaturesMatch(it, method) } }
/** /**
* A match for an opcode pattern. * A match for an opcode pattern.
* @param startIndex The index of the first opcode of the pattern in the method. * @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. * @param endIndex The index of the last opcode of the pattern in the method.
*/ */
class PatternMatch internal constructor( class PatternMatch(
val startIndex: Int, val startIndex: Int,
val endIndex: Int, val endIndex: Int,
) )
@@ -457,7 +309,7 @@ class Match internal constructor(
* @param string The string that matched. * @param string The string that matched.
* @param index The index of the instruction in the method. * @param index The index of the instruction in the method.
*/ */
class StringMatch internal constructor(val string: String, val index: Int) class StringMatch(val string: String, val index: Int)
} }
/** /**
@@ -506,7 +358,7 @@ class FingerprintBuilder internal constructor(
* *
* @param returnType The return type compared using [String.startsWith]. * @param returnType The return type compared using [String.startsWith].
*/ */
fun returns(returnType: String) { infix fun returns(returnType: String) {
this.returnType = returnType this.returnType = returnType
} }
@@ -597,3 +449,19 @@ fun fingerprint(
fuzzyPatternScanThreshold: Int = 0, fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit, block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build() ) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()
/**
* Create a [Fingerprint] and add it to the set of fingerprints.
*
* @param fuzzyPatternScanThreshold The threshold for fuzzy pattern scanning. Default is 0.
* @param block The block to build the [Fingerprint].
*
* @return The created [Fingerprint].
*/
fun BytecodePatchBuilder.fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,
) = app.revanced.patcher.fingerprint(
fuzzyPatternScanThreshold,
block,
)() // Invoke to add it.

View File

@@ -91,14 +91,20 @@ class Patcher(private val config: PatcherConfig) : Closeable {
}.also { executedPatches[this] = it } }.also { executedPatches[this] = it }
} }
// Prevent decoding the app manifest twice if it is not needed. // Prevent from decoding the app manifest twice if it is not needed.
if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) { if (config.resourceMode != ResourcePatchContext.ResourceMode.NONE) {
context.resourceContext.decodeResources(config.resourceMode) context.resourceContext.decodeResources(config.resourceMode)
} }
logger.info("Initializing lookup maps") logger.info("Merging extensions")
// Accessing the lazy lookup maps to initialize them. context.executablePatches.forEachRecursively { patch ->
if (patch is BytecodePatch && patch.extension != null) {
context.bytecodeContext.merge(patch.extension)
}
}
// Initialize lookup maps.
context.bytecodeContext.lookupMaps context.bytecodeContext.lookupMaps
logger.info("Executing patches") logger.info("Executing patches")

View File

@@ -12,32 +12,16 @@ import java.util.logging.Logger
* @param temporaryFilesPath A path to a folder to store temporary files in. * @param temporaryFilesPath A path to a folder to store temporary files in.
* @param aaptBinaryPath A path to a custom aapt binary. * @param aaptBinaryPath A path to a custom aapt binary.
* @param frameworkFileDirectory A path to the directory to cache the framework file in. * @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( class PatcherConfig(
internal val apkFile: File, internal val apkFile: File,
private val temporaryFilesPath: File = File("revanced-temporary-files"), private val temporaryFilesPath: File = File("revanced-temporary-files"),
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, aaptBinaryPath: String? = null,
frameworkFileDirectory: String? = null, frameworkFileDirectory: String? = null,
) : this(apkFile, temporaryFilesPath, aaptBinaryPath?.let { File(it) }, frameworkFileDirectory) internal val multithreadingDexFileWriter: Boolean = false,
) {
private val logger = Logger.getLogger(PatcherConfig::class.java.name) private val logger = Logger.getLogger(PatcherConfig::class.java.name)
/** /**
@@ -52,7 +36,8 @@ class PatcherConfig(
*/ */
internal val resourceConfig = internal val resourceConfig =
Config.getDefaultConfig().apply { Config.getDefaultConfig().apply {
aaptBinary = aaptBinaryPath useAapt2 = true
aaptPath = aaptBinaryPath ?: ""
frameworkDirectory = frameworkFileDirectory frameworkDirectory = frameworkFileDirectory
} }

View File

@@ -29,12 +29,12 @@ class PatcherResult internal constructor(
* @param resourcesApk The compiled resources.apk file. * @param resourcesApk The compiled resources.apk file.
* @param otherResources The directory containing other resources files. * @param otherResources The directory containing other resources files.
* @param doNotCompress List of files that should not be compressed. * @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( class PatchedResources internal constructor(
val resourcesApk: File?, val resourcesApk: File?,
val otherResources: File?, val otherResources: File?,
val doNotCompress: Set<String>, 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.DexFile
import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction 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 com.android.tools.smali.dexlib2.iface.reference.StringReference
import lanchon.multidexlib2.BasicDexFileNamer import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.DexIO
@@ -22,6 +21,7 @@ import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO import lanchon.multidexlib2.RawDexIO
import java.io.Closeable import java.io.Closeable
import java.io.FileFilter import java.io.FileFilter
import java.io.InputStream
import java.util.* import java.util.*
import java.util.logging.Logger import java.util.logging.Logger
@@ -34,7 +34,7 @@ import java.util.logging.Logger
class BytecodePatchContext internal constructor(private val config: PatcherConfig) : class BytecodePatchContext internal constructor(private val config: PatcherConfig) :
PatchContext<Set<PatcherResult.PatchedDexFile>>, PatchContext<Set<PatcherResult.PatchedDexFile>>,
Closeable { 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]. * [Opcodes] of the supplied [PatcherConfig.apkFile].
@@ -60,24 +60,31 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
internal val lookupMaps by lazy { LookupMaps(classes) } internal val lookupMaps by lazy { LookupMaps(classes) }
/** /**
* Merge the extension of [bytecodePatch] into the [BytecodePatchContext]. * A map for lookup by [merge].
* If no extension is present, the function will return early.
*
* @param bytecodePatch The [BytecodePatch] to merge the extension of.
*/ */
internal fun mergeExtension(bytecodePatch: BytecodePatch) { internal val classesByType = mutableMapOf<String, ClassDef>().apply {
bytecodePatch.extensionInputStream?.get()?.use { extensionStream -> classes.forEach { classDef -> put(classDef.type, classDef) }
RawDexIO.readRawDexFile(extensionStream, 0, null).classes.forEach { classDef -> }
val existingClass = lookupMaps.classesByType[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" } /**
* Merge an extension to [classes].
*
* @param extensionInputStream The input stream of the extension to merge.
*/
internal fun merge(extensionInputStream: InputStream) {
val extension = extensionInputStream.readAllBytes()
RawDexIO.readRawDexFile(extension, 0, null).classes.forEach { classDef ->
val existingClass = classesByType[classDef.type] ?: run {
logger.fine("Adding class \"$classDef\"")
classes += classDef classes += classDef
lookupMaps.classesByType[classDef.type] = classDef classesByType[classDef.type] = classDef
return@forEach 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 -> existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class. // If the class was merged, replace the original class with the merged class.
@@ -89,9 +96,16 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
classes += mergedClass classes += mergedClass
} }
} }
} ?: 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.
*/
fun classByType(type: String) = classBy { type in it.type }
/** /**
* Find a class with a predicate. * Find a class with a predicate.
* *
@@ -108,9 +122,9 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* *
* @return A proxy for the class. * @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 it.immutableClass.type == classDef.type
} ?: ClassProxy(classDef).also { classes.proxyPool.add(it) } } ?: ClassProxy(classDef).also { this@BytecodePatchContext.classes.proxyPool.add(it) }
/** /**
* Navigate a method. * Navigate a method.
@@ -119,7 +133,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* *
* @return A [MethodNavigator] for the method. * @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]. * Compile bytecode from the [BytecodePatchContext].
@@ -130,9 +144,6 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
override fun get(): Set<PatcherResult.PatchedDexFile> { override fun get(): Set<PatcherResult.PatchedDexFile> {
logger.info("Compiling patched dex files") logger.info("Compiling patched dex files")
// Free up memory before compiling the dex files.
lookupMaps.close()
val patchedDexFileResults = val patchedDexFileResults =
config.patchedFiles.resolve("dex").also { config.patchedFiles.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty. it.deleteRecursively() // Make sure the directory is empty.
@@ -140,7 +151,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
}.apply { }.apply {
MultiDexIO.writeDexFile( MultiDexIO.writeDexFile(
true, true,
-1, if (config.multithreadingDexFileWriter) -1 else 1,
this, this,
BasicDexFileNamer(), BasicDexFileNamer(),
object : DexFile { object : DexFile {
@@ -150,7 +161,7 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
override fun getOpcodes() = this@BytecodePatchContext.opcodes override fun getOpcodes() = this@BytecodePatchContext.opcodes
}, },
DexIO.DEFAULT_MAX_DEX_POOL_SIZE, DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } } ) { _, entryName, _ -> logger.info("Compiled $entryName") }
}.listFiles(FileFilter { it.isFile })!!.map { }.listFiles(FileFilter { it.isFile })!!.map {
PatcherResult.PatchedDexFile(it.name, it.inputStream()) PatcherResult.PatchedDexFile(it.name, it.inputStream())
}.toSet() }.toSet()
@@ -166,21 +177,47 @@ class BytecodePatchContext internal constructor(private val config: PatcherConfi
* @param classes The list of classes to create the lookup maps from. * @param classes The list of classes to create the lookup maps from.
*/ */
internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable { internal class LookupMaps internal constructor(classes: List<ClassDef>) : Closeable {
/**
* Classes associated by their type.
*/
internal val classesByType = classes.associateBy { it.type }.toMutableMap()
/**
* All methods and the class they are a member of.
*/
internal val allMethods = MethodClassPairs()
/**
* Methods associated by its access flags, return type and parameter.
*/
internal val methodsBySignature = MethodClassPairsLookupMap()
/** /**
* Methods associated by strings referenced in it. * Methods associated by strings referenced in it.
*/ */
internal val methodsByStrings = MethodClassPairsLookupMap() internal val methodsByStrings = MethodClassPairsLookupMap()
// Lookup map for fast checking if a class exists by its type.
val classesByType = mutableMapOf<String, ClassDef>().apply {
classes.forEach { classDef -> put(classDef.type, classDef) }
}
init { init {
classes.forEach { classDef -> classes.forEach { classDef ->
classDef.methods.forEach { method -> classDef.methods.forEach { method ->
val methodClassPair: MethodClassPair = method to classDef val methodClassPair: MethodClassPair = method to classDef
// For fingerprints with no access or return type specified.
allMethods += methodClassPair
val accessFlagsReturnKey = method.accessFlags.toString() + method.returnType.first()
// Add <access><returnType> as the key.
methodsBySignature[accessFlagsReturnKey] = methodClassPair
// Add <access><returnType>[parameters] as the key.
methodsBySignature[
buildString {
append(accessFlagsReturnKey)
appendParameters(method.parameterTypes)
},
] = methodClassPair
// Add strings contained in the method as the key. // Add strings contained in the method as the key.
method.instructionsOrNull?.forEach instructions@{ instruction -> method.instructionsOrNull?.forEach instructions@{ instruction ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) { if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
@@ -198,14 +235,38 @@ 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() { override fun close() {
allMethods.clear()
methodsBySignature.clear()
methodsByStrings.clear() methodsByStrings.clear()
classesByType.clear()
} }
} }
override fun close() { override fun close() {
lookupMaps.close() lookupMaps.close()
classesByType.clear()
classes.clear() classes.clear()
} }
} }

View File

@@ -20,51 +20,16 @@ import kotlin.reflect.typeOf
* @constructor Create a new [Option]. * @constructor Create a new [Option].
*/ */
@Suppress("MemberVisibilityCanBePrivate", "unused") @Suppress("MemberVisibilityCanBePrivate", "unused")
class Option<T> class Option<T> @PublishedApi internal constructor(
@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 key: String,
val default: T? = null, val default: T? = null,
val values: Map<String, T?>? = null, val values: Map<String, T?>? = null,
@Deprecated("Use the name property instead.")
val title: String? = null, val title: String? = null,
val description: String? = null, val description: String? = null,
val required: Boolean = false, val required: Boolean = false,
val type: KType, val type: KType,
val validator: Option<T>.(T?) -> Boolean = { true }, 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]. * The value of the [Option].
*/ */
@@ -135,7 +100,7 @@ internal constructor(
} }
/** /**
* A collection of [Option]s where options can be set and retrieved by their key. * A collection of [Option]s where options can be set and retrieved by key.
* *
* @param options The options. * @param options The options.
* *
@@ -144,7 +109,7 @@ internal constructor(
class Options internal constructor( class Options internal constructor(
private val options: Map<String, Option<*>>, private val options: Map<String, Option<*>>,
) : Map<String, Option<*>> by options { ) : 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. * Set an option's value.
@@ -178,39 +143,6 @@ class Options internal constructor(
override fun get(key: String) = options[key] ?: throw OptionException.OptionNotFoundException(key) override fun get(key: String) = options[key] ?: throw OptionException.OptionNotFoundException(key)
} }
/**
* Create a new [Option] with a string value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun stringOption(
key: String,
default: String? = null,
values: Map<String, String?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<String>.(String?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/** /**
* Create a new [Option] with a string value and add it to the current [PatchBuilder]. * Create a new [Option] with a string value and add it to the current [PatchBuilder].
* *
@@ -244,39 +176,6 @@ fun PatchBuilder<*>.stringOption(
validator, validator,
) )
/**
* Create a new [Option] with an integer value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun intOption(
key: String,
default: Int? = null,
values: Map<String, Int?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Int>.(Int?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/** /**
* Create a new [Option] with an integer value and add it to the current [PatchBuilder]. * Create a new [Option] with an integer value and add it to the current [PatchBuilder].
* *
@@ -299,40 +198,7 @@ fun PatchBuilder<*>.intOption(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: Option<Int>.(Int?) -> Boolean = { true }, validator: Option<Int?>.(Int?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a boolean value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun booleanOption(
key: String,
default: Boolean? = null,
values: Map<String, Boolean?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Boolean>.(Boolean?) -> Boolean = { true },
) = option( ) = option(
key, key,
default, default,
@@ -365,40 +231,7 @@ fun PatchBuilder<*>.booleanOption(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: Option<Boolean>.(Boolean?) -> Boolean = { true }, validator: Option<Boolean?>.(Boolean?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a float value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun floatOption(
key: String,
default: Float? = null,
values: Map<String, Float?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Float>.(Float?) -> Boolean = { true },
) = option( ) = option(
key, key,
default, default,
@@ -431,40 +264,7 @@ fun PatchBuilder<*>.floatOption(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: Option<Float>.(Float?) -> Boolean = { true }, validator: Option<Float?>.(Float?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a long value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun longOption(
key: String,
default: Long? = null,
values: Map<String, Long?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<Long>.(Long?) -> Boolean = { true },
) = option( ) = option(
key, key,
default, default,
@@ -497,40 +297,7 @@ fun PatchBuilder<*>.longOption(
title: String? = null, title: String? = null,
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
validator: Option<Long>.(Long?) -> Boolean = { true }, validator: Option<Long?>.(Long?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/**
* Create a new [Option] with a string list value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun stringsOption(
key: String,
default: List<String>? = null,
values: Map<String, List<String>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
) = option( ) = option(
key, key,
default, default,
@@ -574,39 +341,6 @@ fun PatchBuilder<*>.stringsOption(
validator, validator,
) )
/**
* Create a new [Option] with an integer list value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun intsOption(
key: String,
default: List<Int>? = null,
values: Map<String, List<Int>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/** /**
* Create a new [Option] with an integer list value and add it to the current [PatchBuilder]. * Create a new [Option] with an integer list value and add it to the current [PatchBuilder].
* *
@@ -640,39 +374,6 @@ fun PatchBuilder<*>.intsOption(
validator, validator,
) )
/**
* Create a new [Option] with a boolean list value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun booleansOption(
key: String,
default: List<Boolean>? = null,
values: Map<String, List<Boolean>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/** /**
* Create a new [Option] with a boolean list value and add it to the current [PatchBuilder]. * Create a new [Option] with a boolean list value and add it to the current [PatchBuilder].
* *
@@ -739,39 +440,6 @@ fun PatchBuilder<*>.floatsOption(
validator, validator,
) )
/**
* Create a new [Option] with a long list value.
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
fun longsOption(
key: String,
default: List<Long>? = null,
values: Map<String, List<Long>?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
) = option(
key,
default,
values,
title,
description,
required,
validator,
)
/** /**
* Create a new [Option] with a long list value and add it to the current [PatchBuilder]. * Create a new [Option] with a long list value and add it to the current [PatchBuilder].
* *
@@ -805,40 +473,6 @@ fun PatchBuilder<*>.longsOption(
validator, validator,
) )
/**
* Create a new [Option].
*
* @param key The key.
* @param default The default value.
* @param values Eligible option values mapped to a human-readable name.
* @param title The title.
* @param description A description.
* @param required Whether the option is required.
* @param validator The function to validate the option value.
*
* @return The created [Option].
*
* @see Option
*/
inline fun <reified T> option(
key: String,
default: T? = null,
values: Map<String, T?>? = null,
title: String? = null,
description: String? = null,
required: Boolean = false,
noinline validator: Option<T>.(T?) -> Boolean = { true },
) = Option(
key,
default,
values,
title,
description,
required,
typeOf<T>(),
validator,
)
/** /**
* Create a new [Option] and add it to the current [PatchBuilder]. * Create a new [Option] and add it to the current [PatchBuilder].
* *
@@ -862,13 +496,14 @@ inline fun <reified T> PatchBuilder<*>.option(
description: String? = null, description: String? = null,
required: Boolean = false, required: Boolean = false,
noinline validator: Option<T>.(T?) -> Boolean = { true }, noinline validator: Option<T>.(T?) -> Boolean = { true },
) = app.revanced.patcher.patch.option( ) = Option(
key, key,
default, default,
values, values,
title, title,
description, description,
required, required,
typeOf<T>(),
validator, validator,
).also { it() } ).also { it() }
@@ -884,26 +519,30 @@ sealed class OptionException(errorMessage: String) : Exception(errorMessage, nul
* @param invalidType The type of the value that was passed. * @param invalidType The type of the value that was passed.
* @param expectedType The type of the value that was expected. * @param expectedType The type of the value that was expected.
*/ */
class InvalidValueTypeException(invalidType: String, expectedType: String) : OptionException("Type $expectedType was expected but received type $invalidType") class InvalidValueTypeException(invalidType: String, expectedType: String) :
OptionException("Type $expectedType was expected but received type $invalidType")
/** /**
* An exception thrown when a value did not satisfy the value conditions specified by the [Option]. * An exception thrown when a value did not satisfy the value conditions specified by the [Option].
* *
* @param value The value that failed validation. * @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. * An exception thrown when a value is required but null was passed.
* *
* @param option The [Option] that requires a value. * @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 null was passed")
/** /**
* An exception thrown when a [Option] is not found. * An exception thrown when a [Option] is not found.
* *
* @param key The key of the [Option]. * @param key The key of the [Option].
*/ */
class OptionNotFoundException(key: String) : OptionException("No option with key $key") class OptionNotFoundException(key: String) :
OptionException("No option with key $key")
} }

View File

@@ -2,6 +2,7 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.Fingerprint
import app.revanced.patcher.Patcher import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherContext import app.revanced.patcher.PatcherContext
import dalvik.system.DexClassLoader import dalvik.system.DexClassLoader
@@ -9,12 +10,9 @@ import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO import lanchon.multidexlib2.MultiDexIO
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.net.URLClassLoader import java.net.URLClassLoader
import java.util.function.Supplier
import java.util.jar.JarFile import java.util.jar.JarFile
import kotlin.reflect.KProperty
typealias PackageName = String typealias PackageName = String
typealias VersionName = String typealias VersionName = String
@@ -27,7 +25,7 @@ typealias Package = Pair<PackageName, Set<VersionName>?>
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param dependencies Other patches this patch depends on. * @param dependencies Other patches this patch depends on.
* @param compatiblePackages The packages the patch is compatible with. * @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages. * If null, the patch is compatible with all packages.
@@ -45,50 +43,46 @@ sealed class Patch<C : PatchContext<*>>(
val dependencies: Set<Patch<*>>, val dependencies: Set<Patch<*>>,
val compatiblePackages: Set<Package>?, val compatiblePackages: Set<Package>?,
options: Set<Option<*>>, 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, // 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. // 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.
*/
val options = Options(options) val options = Options(options)
/** /**
* Calls the execution block of the patch. * Runs the execution block of the patch.
* This function is called by [Patcher.invoke]. * Called by [Patcher].
* *
* @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with. * @param context The [PatcherContext] to get the [PatchContext] from to execute the patch with.
*/ */
internal abstract fun execute(context: PatcherContext) internal abstract fun execute(context: PatcherContext)
/** /**
* Calls the execution block of the patch. * Runs the execution block of the patch.
* *
* @param context The [PatchContext] to execute the patch with. * @param context The [PatchContext] to execute the patch with.
*/ */
fun execute(context: C) = executeBlock(context) fun execute(context: C) = executeBlock(context)
/** /**
* Calls the finalizing block of the patch. * Runs the finalizing block of the patch.
* This function is called by [Patcher.invoke]. * Called by [Patcher].
* *
* @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with. * @param context The [PatcherContext] to get the [PatchContext] from to finalize the patch with.
*/ */
internal abstract fun finalize(context: PatcherContext) 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. * @param context The [PatchContext] to finalize the patch with.
*/ */
fun finalize(context: C) { fun finalize(context: C) {
finalizeBlock?.invoke(context) finalizeBlock?.invoke(this, context)
} }
override fun toString() = name ?: override fun toString() = name ?: "Patch"
"Patch@${System.identityHashCode(this)}"
} }
internal fun Patch<*>.anyRecursively( internal fun Patch<*>.anyRecursively(
@@ -122,12 +116,13 @@ internal fun Iterable<Patch<*>>.forEachRecursively(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with. * @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages. * If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on. * @param dependencies Other patches this patch depends on.
* @param options The options of the patch. * @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. * 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 executeBlock The execution block of the patch.
* @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed, * @param finalizeBlock The finalizing block of the patch. Called after all patches have been executed,
@@ -142,9 +137,10 @@ class BytecodePatch internal constructor(
compatiblePackages: Set<Package>?, compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>, dependencies: Set<Patch<*>>,
options: Set<Option<*>>, options: Set<Option<*>>,
val extensionInputStream: Supplier<InputStream>?, val fingerprints: Set<Fingerprint>,
executeBlock: (BytecodePatchContext) -> Unit, val extension: InputStream?,
finalizeBlock: ((BytecodePatchContext) -> Unit)?, executeBlock: Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit,
finalizeBlock: (Patch<BytecodePatchContext>.(BytecodePatchContext) -> Unit)?,
) : Patch<BytecodePatchContext>( ) : Patch<BytecodePatchContext>(
name, name,
description, description,
@@ -156,13 +152,14 @@ class BytecodePatch internal constructor(
finalizeBlock, finalizeBlock,
) { ) {
override fun execute(context: PatcherContext) = with(context.bytecodeContext) { override fun execute(context: PatcherContext) = with(context.bytecodeContext) {
mergeExtension(this@BytecodePatch) fingerprints.forEach { it.match(this) }
execute(this) execute(this)
} }
override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext) override fun finalize(context: PatcherContext) = finalize(context.bytecodeContext)
override fun toString() = name ?: "Bytecode${super.toString()}" override fun toString() = name ?: "BytecodePatch"
} }
/** /**
@@ -171,7 +168,7 @@ class BytecodePatch internal constructor(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with. * @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages. * If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on. * @param dependencies Other patches this patch depends on.
@@ -189,8 +186,8 @@ class RawResourcePatch internal constructor(
compatiblePackages: Set<Package>?, compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>, dependencies: Set<Patch<*>>,
options: Set<Option<*>>, options: Set<Option<*>>,
executeBlock: (ResourcePatchContext) -> Unit, executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
finalizeBlock: ((ResourcePatchContext) -> Unit)?, finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>( ) : Patch<ResourcePatchContext>(
name, name,
description, description,
@@ -205,7 +202,7 @@ class RawResourcePatch internal constructor(
override fun finalize(context: PatcherContext) = finalize(context.resourceContext) override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "RawResource${super.toString()}" override fun toString() = name ?: "RawResourcePatch"
} }
/** /**
@@ -214,7 +211,7 @@ class RawResourcePatch internal constructor(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param compatiblePackages The packages the patch is compatible with. * @param compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages. * If null, the patch is compatible with all packages.
* @param dependencies Other patches this patch depends on. * @param dependencies Other patches this patch depends on.
@@ -232,8 +229,8 @@ class ResourcePatch internal constructor(
compatiblePackages: Set<Package>?, compatiblePackages: Set<Package>?,
dependencies: Set<Patch<*>>, dependencies: Set<Patch<*>>,
options: Set<Option<*>>, options: Set<Option<*>>,
executeBlock: (ResourcePatchContext) -> Unit, executeBlock: Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit,
finalizeBlock: ((ResourcePatchContext) -> Unit)?, finalizeBlock: (Patch<ResourcePatchContext>.(ResourcePatchContext) -> Unit)?,
) : Patch<ResourcePatchContext>( ) : Patch<ResourcePatchContext>(
name, name,
description, description,
@@ -248,7 +245,7 @@ class ResourcePatch internal constructor(
override fun finalize(context: PatcherContext) = finalize(context.resourceContext) override fun finalize(context: PatcherContext) = finalize(context.resourceContext)
override fun toString() = name ?: "Resource${super.toString()}" override fun toString() = name ?: "ResourcePatch"
} }
/** /**
@@ -258,7 +255,7 @@ class ResourcePatch internal constructor(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @property compatiblePackages The packages the patch is compatible with. * @property compatiblePackages The packages the patch is compatible with.
* If null, the patch is compatible with all packages. * If null, the patch is compatible with all packages.
* @property dependencies Other patches this patch depends on. * @property dependencies Other patches this patch depends on.
@@ -278,8 +275,8 @@ sealed class PatchBuilder<C : PatchContext<*>>(
protected var dependencies = mutableSetOf<Patch<*>>() protected var dependencies = mutableSetOf<Patch<*>>()
protected val options = mutableSetOf<Option<*>>() protected val options = mutableSetOf<Option<*>>()
protected var executionBlock: ((C) -> Unit) = { } protected var executionBlock: (Patch<C>.(C) -> Unit) = { }
protected var finalizeBlock: ((C) -> Unit)? = null protected var finalizeBlock: (Patch<C>.(C) -> Unit)? = null
/** /**
* Add an option to the patch. * Add an option to the patch.
@@ -338,7 +335,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
* *
* @param block The execution block of the patch. * @param block The execution block of the patch.
*/ */
fun execute(block: C.() -> Unit) { fun execute(block: Patch<C>.(C) -> Unit) {
executionBlock = block executionBlock = block
} }
@@ -347,7 +344,7 @@ sealed class PatchBuilder<C : PatchContext<*>>(
* *
* @param block The finalizing block of the patch. * @param block The finalizing block of the patch.
*/ */
fun finalize(block: C.() -> Unit) { fun finalize(block: Patch<C>.(C) -> Unit) {
finalizeBlock = block finalizeBlock = block
} }
@@ -375,8 +372,9 @@ private fun <B : PatchBuilder<*>> B.buildPatch(block: B.() -> Unit = {}) = apply
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @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. * 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. * @constructor Create a new [BytecodePatchBuilder] builder.
@@ -386,9 +384,27 @@ class BytecodePatchBuilder internal constructor(
description: String?, description: String?,
use: Boolean, use: Boolean,
) : PatchBuilder<BytecodePatchContext>(name, description, use) { ) : PatchBuilder<BytecodePatchContext>(name, description, use) {
private val fingerprints = mutableSetOf<Fingerprint>()
/**
* Add the fingerprint to the patch.
*
* @return A wrapper for the fingerprint with the ability to delegate the match to the fingerprint.
*/
operator fun Fingerprint.invoke() = InvokedFingerprint(also { fingerprints.add(it) })
class InvokedFingerprint(private val fingerprint: Fingerprint) {
// The reason getValue isn't extending the Fingerprint class is
// because delegating makes only sense if the fingerprint was previously added to the patch by invoking it.
// It may be likely to forget invoking it. By wrapping the fingerprint into this class,
// the compiler will throw an error if the fingerprint was not invoked if attempting to delegate the match.
operator fun getValue(nothing: Nothing?, property: KProperty<*>) = fingerprint.match
?: throw PatchException("No fingerprint match to delegate to \"${property.name}\".")
}
// Must be internal for the inlined function "extendWith". // Must be internal for the inlined function "extendWith".
@PublishedApi @PublishedApi
internal var extensionInputStream: Supplier<InputStream>? = null internal var extension: InputStream? = null
// Inlining is necessary to get the class loader that loaded the patch // Inlining is necessary to get the class loader that loaded the patch
// to load the extension from the resources. // to load the extension from the resources.
@@ -399,11 +415,8 @@ class BytecodePatchBuilder internal constructor(
*/ */
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun extendWith(extension: String) = apply { inline fun extendWith(extension: String) = apply {
val classLoader = object {}.javaClass.classLoader this.extension = object {}.javaClass.classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension \"$extension\" not found")
extensionInputStream = Supplier {
classLoader.getResourceAsStream(extension) ?: throw PatchException("Extension \"$extension\" not found")
}
} }
override fun build() = BytecodePatch( override fun build() = BytecodePatch(
@@ -413,7 +426,8 @@ class BytecodePatchBuilder internal constructor(
compatiblePackages, compatiblePackages,
dependencies, dependencies,
options, options,
extensionInputStream, fingerprints,
extension,
executionBlock, executionBlock,
finalizeBlock, finalizeBlock,
) )
@@ -425,7 +439,7 @@ class BytecodePatchBuilder internal constructor(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param block The block to build the patch. * @param block The block to build the patch.
* *
* @return The created [BytecodePatch]. * @return The created [BytecodePatch].
@@ -443,7 +457,7 @@ fun bytecodePatch(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* *
* @constructor Create a new [RawResourcePatch] builder. * @constructor Create a new [RawResourcePatch] builder.
*/ */
@@ -470,7 +484,7 @@ class RawResourcePatchBuilder internal constructor(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param block The block to build the patch. * @param block The block to build the patch.
* @return The created [RawResourcePatch]. * @return The created [RawResourcePatch].
*/ */
@@ -487,7 +501,7 @@ fun rawResourcePatch(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* *
* @constructor Create a new [ResourcePatch] builder. * @constructor Create a new [ResourcePatch] builder.
*/ */
@@ -514,7 +528,7 @@ class ResourcePatchBuilder internal constructor(
* @param name The name of the patch. * @param name The name of the patch.
* If null, the patch is named "Patch" and will not be loaded by [PatchLoader]. * If null, the patch is named "Patch" and will not be loaded by [PatchLoader].
* @param description The description of the patch. * @param description The description of the patch.
* @param use Whether or not the patch should be used. * @param use Weather or not the patch should be used.
* @param block The block to build the patch. * @param block The block to build the patch.
* *
* @return The created [ResourcePatch]. * @return The created [ResourcePatch].
@@ -619,7 +633,7 @@ sealed class PatchLoader private constructor(
*/ */
private val Class<*>.patchFields private val Class<*>.patchFields
get() = fields.filter { field -> get() = fields.filter { field ->
field.type.isPatch && field.canAccess() field.type.isPatch && field.canAccess(null)
}.map { field -> }.map { field ->
field.get(null) as Patch<*> field.get(null) as Patch<*>
} }
@@ -629,7 +643,7 @@ sealed class PatchLoader private constructor(
*/ */
private val Class<*>.patchMethods private val Class<*>.patchMethods
get() = methods.filter { method -> get() = methods.filter { method ->
method.returnType.isPatch && method.parameterCount == 0 && method.canAccess() method.returnType.isPatch && method.parameterCount == 0 && method.canAccess(null)
}.map { method -> }.map { method ->
method.invoke(null) as Patch<*> method.invoke(null) as Patch<*>
} }
@@ -653,12 +667,6 @@ sealed class PatchLoader private constructor(
it.name != null it.name != null
}.toSet() }.toSet()
} }
private fun Member.canAccess(): Boolean {
if (this is Method && parameterCount != 0) return false
return Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)
}
} }
} }

View File

@@ -10,9 +10,9 @@ import brut.androlib.ApkDecoder
import brut.androlib.apk.UsesFramework import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser 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 brut.directory.ExtFile
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@@ -31,27 +31,23 @@ class ResourcePatchContext internal constructor(
) : PatchContext<PatcherResult.PatchedResources?> { ) : PatchContext<PatcherResult.PatchedResources?> {
private val logger = Logger.getLogger(ResourcePatchContext::class.java.name) 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]. * 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]. * Decode resources of [PatcherConfig.apkFile].
* *
* @param mode The [ResourceMode] to use. * @param mode The [ResourceMode] to use.
*/ */
internal fun decodeResources(mode: ResourceMode) = with(packageMetadata.apkInfo) { internal fun decodeResources(mode: ResourceMode) =
with(packageMetadata.apkInfo) {
config.initializeTemporaryFilesDirectories() config.initializeTemporaryFilesDirectories()
// Needed to decode resources. // Needed to decode resources.
@@ -64,7 +60,8 @@ class ResourcePatchContext internal constructor(
resourcesDecoder.decodeManifest(config.apkFiles) resourcesDecoder.decodeManifest(config.apkFiles)
// Needed to record uncompressed files. // Needed to record uncompressed files.
ApkDecoder(this, config.resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping) val apkDecoder = ApkDecoder(config.resourceConfig, this)
apkDecoder.recordUncompressedFiles(resourcesDecoder.resFileMapping)
usesFramework = usesFramework =
UsesFramework().apply { UsesFramework().apply {
@@ -75,10 +72,10 @@ class ResourcePatchContext internal constructor(
// Decode manually instead of using resourceDecoder.decodeManifest // Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream. // because it does not support decoding to an OutputStream.
AndroidManifestPullStreamDecoder( XmlPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable), AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.newXmlSerializer(), resourcesDecoder.resXmlSerializer,
).decode( ).decodeManifest(
apkFile.directory.getFileInput("AndroidManifest.xml"), apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream() // Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() { object : OutputStream() {
@@ -88,7 +85,7 @@ class ResourcePatchContext internal constructor(
) )
// Get the package name and version from the manifest using the XmlPullStreamDecoder. // Get the package name and version from the manifest using the XmlPullStreamDecoder.
// AndroidManifestPullStreamDecoder.decode() sets metadata.apkInfo. // XmlPullStreamDecoder.decodeManifest() sets metadata.apkInfo.
packageMetadata.let { metadata -> packageMetadata.let { metadata ->
metadata.packageName = resourcesDecoder.resTable.packageRenamed metadata.packageName = resourcesDecoder.resTable.packageRenamed
versionInfo.let { versionInfo.let {
@@ -98,7 +95,7 @@ class ResourcePatchContext internal constructor(
/* /*
The ResTable if flagged as sparse if the main package is not loaded, which is the case here, 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 because ResourcesDecoder.decodeResources loads the main package
and not AndroidManifestPullStreamDecoder.decode. and not XmlPullStreamDecoder.decodeManifest.
See ARSCDecoder.readTableType for more info. See ARSCDecoder.readTableType for more info.
Set this to false again to prevent the ResTable from being flagged as sparse falsely. Set this to false again to prevent the ResTable from being flagged as sparse falsely.
@@ -128,10 +125,10 @@ class ResourcePatchContext internal constructor(
AaptInvoker( AaptInvoker(
config.resourceConfig, config.resourceConfig,
packageMetadata.apkInfo, packageMetadata.apkInfo,
).invoke( ).invokeAapt(
resources.resolve("resources.apk"), resources.resolve("resources.apk"),
config.apkFiles.resolve("AndroidManifest.xml").also { config.apkFiles.resolve("AndroidManifest.xml").also {
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it) ResXmlPatcher.fixingPublicAttrsInProviderAttributes(it)
}, },
config.apkFiles.resolve("res"), config.apkFiles.resolve("res"),
null, 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. * How to handle resources decoding and compiling.
@@ -230,4 +227,10 @@ class ResourcePatchContext internal constructor(
*/ */
NONE, 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 if (missingMethods.isEmpty()) return this
logger.fine { "Found ${missingMethods.size} missing methods" } logger.fine("Found ${missingMethods.size} missing methods")
return asMutableClass().apply { return asMutableClass().apply {
methods.addAll(missingMethods.map { it.toMutable() }) methods.addAll(missingMethods.map { it.toMutable() })
@@ -80,7 +80,7 @@ internal object ClassMerger {
if (missingFields.isEmpty()) return this if (missingFields.isEmpty()) return this
logger.fine { "Found ${missingFields.size} missing fields" } logger.fine("Found ${missingFields.size} missing fields")
return asMutableClass().apply { return asMutableClass().apply {
fields.addAll(missingFields.map { it.toMutable() }) fields.addAll(missingFields.map { it.toMutable() })
@@ -100,7 +100,7 @@ internal object ClassMerger {
context.traverseClassHierarchy(this) { context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy if (accessFlags.isPublic()) return@traverseClassHierarchy
logger.fine { "Publicizing ${this.type}" } logger.fine("Publicizing ${this.type}")
accessFlags = accessFlags.toPublic() accessFlags = accessFlags.toPublic()
} }
@@ -124,7 +124,7 @@ internal object ClassMerger {
if (brokenFields.isEmpty()) return this if (brokenFields.isEmpty()) return this
logger.fine { "Found ${brokenFields.size} broken fields" } logger.fine("Found ${brokenFields.size} broken fields")
/** /**
* Make a field public. * Make a field public.
@@ -153,7 +153,7 @@ internal object ClassMerger {
if (brokenMethods.isEmpty()) return this if (brokenMethods.isEmpty()) return this
logger.fine { "Found ${brokenMethods.size} methods" } logger.fine("Found ${brokenMethods.size} methods")
/** /**
* Make a method public. * Make a method public.
@@ -179,9 +179,7 @@ internal object ClassMerger {
callback: MutableClass.() -> Unit, callback: MutableClass.() -> Unit,
) { ) {
callback(targetClass) callback(targetClass)
this.classByType(targetClass.superclass ?: return)?.mutableClass?.let {
targetClass.superclass ?: return
this.classBy { targetClass.superclass == it.type }?.mutableClass?.let {
traverseClassHierarchy(it, callback) traverseClassHierarchy(it, callback)
} }
} }

View File

@@ -5,7 +5,6 @@ import java.io.Closeable
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import javax.xml.parsers.DocumentBuilderFactory import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult import javax.xml.transform.stream.StreamResult
@@ -35,22 +34,15 @@ class Document internal constructor(
readerCount.remove(it) readerCount.remove(it)
} }
val transformer = TransformerFactory.newInstance().newTransformer() it.outputStream().use { stream ->
// Set to UTF-16 to prevent surrogate pairs from being escaped to invalid numeric character references, but save as UTF-8. TransformerFactory.newInstance()
if (isAndroid) { .newTransformer()
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16") .transform(DOMSource(this), StreamResult(stream))
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 companion object {
private val readerCount = mutableMapOf<File, Int>() 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.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil import com.android.tools.smali.dexlib2.util.MethodUtil
import kotlin.reflect.KProperty
/** /**
* A navigator for methods. * A navigator for methods.
* *
* @param context The [BytecodePatchContext] to use.
* @param startMethod The [Method] to start navigating from. * @param startMethod The [Method] to start navigating from.
* *
* @constructor Creates a new [MethodNavigator]. * @constructor Creates a new [MethodNavigator].
@@ -24,15 +24,11 @@ import kotlin.reflect.KProperty
* @throws NavigateException If the method does not have an implementation. * @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference. * @throws NavigateException If the instruction at the specified index is not a method reference.
*/ */
context(BytecodePatchContext) class MethodNavigator internal constructor(private val context: BytecodePatchContext, private var startMethod: MethodReference) {
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
private var lastNavigatedMethodReference = startMethod private var lastNavigatedMethodReference = startMethod
private val lastNavigatedMethodInstructions private val lastNavigatedMethodInstructions get() = with(immutable()) {
get() = with(original()) { instructionsOrNull ?: throw NavigateException("Method $definingClass.$name does not have an implementation.")
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
} }
/** /**
@@ -42,7 +38,7 @@ class MethodNavigator internal constructor(
* *
* @return This [MethodNavigator]. * @return This [MethodNavigator].
*/ */
fun to(vararg index: Int): MethodNavigator { fun at(vararg index: Int): MethodNavigator {
index.forEach { index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it) lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
} }
@@ -56,7 +52,7 @@ class MethodNavigator internal constructor(
* @param index The index of the method to navigate to. * @param index The index of the method to navigate to.
* @param predicate The predicate to match. * @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() lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index) .filter(predicate).asIterable().getMethodReferenceAt(index)
@@ -80,22 +76,15 @@ class MethodNavigator internal constructor(
* *
* @return The last navigated method mutably. * @return The last navigated method mutably.
*/ */
fun stop() = classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature fun mutable() = context.classBy(matchesCurrentMethodReferenceDefiningClass)!!.mutableClass.firstMethodBySignature
as MutableMethod 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. * Get the last navigated method immutably.
* *
* @return 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. * Predicate to match the class defining the current method reference.
@@ -107,8 +96,7 @@ class MethodNavigator internal constructor(
/** /**
* Find the first [lastNavigatedMethodReference] in the class. * Find the first [lastNavigatedMethodReference] in the class.
*/ */
private val ClassDef.firstMethodBySignature private val ClassDef.firstMethodBySignature get() = methods.first {
get() = methods.first {
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference) MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
} }

View File

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

View File

@@ -5,15 +5,16 @@ import app.revanced.patcher.patch.BytecodePatchContext.LookupMaps
import app.revanced.patcher.util.ProxyClassList import app.revanced.patcher.util.ProxyClassList
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import io.mockk.* import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.assertAll import org.junit.jupiter.api.assertDoesNotThrow
import java.util.logging.Logger import java.util.logging.Logger
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertNotNull import kotlin.test.assertNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
internal object PatcherTest { internal object PatcherTest {
@@ -148,15 +149,19 @@ internal object PatcherTest {
@Test @Test
fun `throws if unmatched fingerprint match is delegated`() { fun `throws if unmatched fingerprint match is delegated`() {
val patch = bytecodePatch { val patch = bytecodePatch {
execute {
// Fingerprint can never match. // Fingerprint can never match.
val fingerprint = fingerprint { } val match by fingerprint { }
// Manually add the fingerprint.
app.revanced.patcher.fingerprint { }()
execute {
// Throws, because the fingerprint can't be matched. // Throws, because the fingerprint can't be matched.
fingerprint.patternMatch match.patternMatch
} }
} }
assertEquals(2, patch.fingerprints.size)
assertTrue( assertTrue(
patch().exception != null, patch().exception != null,
"Expected an exception because the fingerprint can't match.", "Expected an exception because the fingerprint can't match.",
@@ -165,6 +170,43 @@ internal object PatcherTest {
@Test @Test
fun `matches fingerprint`() { 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)
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( every { patcher.context.bytecodeContext.classes } returns ProxyClassList(
mutableListOf( mutableListOf(
ImmutableClassDef( ImmutableClassDef(
@@ -190,47 +232,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 { 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 package app.revanced.patcher.patch
import app.revanced.patcher.fingerprint
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
@@ -23,6 +24,23 @@ internal object PatchTest {
assertEquals("compatible.package", patch.compatiblePackages!!.first().first) 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 @Test
fun `can create patch with dependencies`() { fun `can create patch with dependencies`() {
val patch = bytecodePatch(name = "Test") { val patch = bytecodePatch(name = "Test") {

View File

@@ -7,11 +7,7 @@ import kotlin.reflect.typeOf
import kotlin.test.* import kotlin.test.*
internal object OptionsTest { internal object OptionsTest {
private val externalOption = stringOption("external", "default")
private val optionsTestPatch = bytecodePatch { private val optionsTestPatch = bytecodePatch {
externalOption()
booleanOption("bool", true) booleanOption("bool", true)
stringOption("required", "default", required = true) stringOption("required", "default", required = true)
@@ -128,17 +124,5 @@ internal object OptionsTest {
} }
} }
@Test
fun `external option should be accessible`() {
assertDoesNotThrow {
externalOption.value = "test"
}
}
@Test
fun `should allow getting the external option from the patch`() {
assertEquals(optionsTestPatch.options["external"].value, externalOption.value)
}
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block) private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
} }