Compare commits

...

370 Commits

Author SHA1 Message Date
oSumAtrIX
91cb464a27 fix nullability 2026-01-11 03:09:49 +01:00
oSumAtrIX
dc8565e8a6 use context params for apis 2026-01-09 19:06:50 +01:00
oSumAtrIX
1a052b9787 contextualize delegate variants and composite api 2026-01-09 18:53:35 +01:00
oSumAtrIX
ed56bf49ad make typealiases public 2026-01-09 16:02:46 +01:00
oSumAtrIX
56fd65d6ce formatting 2026-01-08 00:51:56 +01:00
oSumAtrIX
5674c1f2a2 more completeness 2026-01-08 00:51:17 +01:00
oSumAtrIX
d2461f92aa more or less finish composite api 2026-01-06 04:06:37 +01:00
oSumAtrIX
18570656cc merge matching and tests module with patcher, make builder api context aware and refactor 2025-12-30 00:02:16 +01:00
oSumAtrIX
005c91bc08 Modernize patch api names, deprecate fingerprints, simplify patching code even more, add mutablemethod implementation setter, refactor tests and improve for better coverage 2025-12-29 07:41:07 +01:00
oSumAtrIX
f17fbd8c40 Refactor matching API into separate module, simplify & refactor matching code, convert patcher to functions/DSL, refactor & greatly simplify internal code, refactor & simplify patch api internal code, update deps, fix workflow, add callback for patches files failed to load to be able to be able to load the rest of the patches 2025-12-29 03:41:10 +01:00
oSumAtrIX
2c97de2894 Use Kotlin Multiplatform, update to new binary compatibility plugin, more or less finish composite api, modularize project for separation of APIs, update Gradle wrapper, add new handy declarative predicate APIs, use new publishing plugin, simplify/modernize build system, standardize gitignore, and optimize build properties for performance 2025-12-14 03:17:33 +01:00
oSumAtrIX
2509997432 feat: Fix property delegates & add cache 2025-11-30 19:21:35 +01:00
oSumAtrIX
58ff464192 feat: Simplify composite API 2025-11-28 17:43:51 +01:00
oSumAtrIX
4106ce4070 feat: Composition API 2025-11-28 16:10:58 +01:00
oSumAtrIX
e6eaf6cb73 feat: Infer cache key from unique instance hashcode 2025-11-28 14:25:35 +01:00
oSumAtrIX
c56ac7a81f fix: Everything works now 2025-11-27 00:36:36 +01:00
oSumAtrIX
cdc480acea fix: More bugfixes, patches now compile 2025-11-26 23:12:46 +01:00
oSumAtrIX
14f2eb69e4 fix: Couple more matching fixes 2025-11-26 10:25:19 +01:00
oSumAtrIX
fcdaf324fe fix: Do not create a new matcher instance if provided with one 2025-11-23 16:02:26 +01:00
oSumAtrIX
8653d8304b feat: Appy upstream 2025-11-22 23:20:51 +01:00
oSumAtrIX
e2c781f12c feat: Builds now 2025-11-22 19:23:48 +01:00
oSumAtrIX
0b5e8b791d feat: More progress towards compatibility 2025-11-20 20:08:22 +01:00
oSumAtrIX
cf57726bbb feat: More APIs/adjustments 2025-11-19 20:08:49 +01:00
oSumAtrIX
79d3640186 feat: Modernize APIs 2025-11-19 00:20:41 +01:00
semantic-release-bot
3a8b2ba935 chore: Release v21.1.0-dev.5 [skip ci]
# [21.1.0-dev.5](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.4...v21.1.0-dev.5) (2025-10-16)
2025-10-16 15:03:36 +00:00
dependabot[bot]
39c5a66ce3 build(Needs bump): Bump dependencies 2025-10-16 17:01:58 +02:00
semantic-release-bot
b160a2adc0 chore: Release v21.1.0-dev.4 [skip ci]
# [21.1.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.3...v21.1.0-dev.4) (2025-07-18)

### Bug Fixes

* Correctly save XML files in UTF-8 by using a bufferedWriter ([#356](https://github.com/ReVanced/revanced-patcher/issues/356)) ([33fadcb](33fadcbd0c))
2025-07-18 19:33:42 +00:00
kitadai31
33fadcbd0c fix: Correctly save XML files in UTF-8 by using a bufferedWriter (#356) 2025-07-18 21:31:51 +02:00
semantic-release-bot
68db95b99b chore: Release v21.1.0-dev.3 [skip ci]
# [21.1.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.2...v21.1.0-dev.3) (2025-06-20)

### Bug Fixes

* Encode XML files as UTF-8 to fix compilation of resources ([#339](https://github.com/ReVanced/revanced-patcher/issues/339)) ([4f2ef3c](4f2ef3c47c))
2025-06-20 14:44:18 +00:00
Pg
4f2ef3c47c fix: Encode XML files as UTF-8 to fix compilation of resources (#339)
Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-06-20 16:41:53 +02:00
semantic-release-bot
062ae14936 chore: Release v21.1.0-dev.2 [skip ci]
# [21.1.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.1.0-dev.1...v21.1.0-dev.2) (2025-06-20)

### Bug Fixes

* Add back missing log by naming logger correctly ([#332](https://github.com/ReVanced/revanced-patcher/issues/332)) ([e4e66b0](e4e66b0d8b))
* Support UTF-8 chars when compiling instructions in Smali in non UTF-8 environments ([#331](https://github.com/ReVanced/revanced-patcher/issues/331)) ([bb8771b](bb8771bb8b))

### Features

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

### Performance Improvements

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

### Features

* Add identity hash code to unnamed patches ([88a3252](88a3252574))
2024-12-07 05:20:15 +00:00
oSumAtrIX
88a3252574 feat: Add identity hash code to unnamed patches 2024-12-07 06:19:08 +01:00
semantic-release-bot
ead701bdaf chore: Release v21.0.0 [skip ci]
# [21.0.0](https://github.com/ReVanced/revanced-patcher/compare/v20.0.2...v21.0.0) (2024-11-05)

### Bug Fixes

* Match fingerprint before delegating the match property ([5d996de](5d996def4d))
* Merge extension only when patch executes ([#315](https://github.com/ReVanced/revanced-patcher/issues/315)) ([aa472eb](aa472eb985))

### Features

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

### Performance Improvements

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

### BREAKING CHANGES

* Various APIs have been changed.
* Many APIs have been changed.
2024-11-05 18:18:41 +00:00
oSumAtrIX
0581dcf931 chore: Merge branch dev to main 2024-11-05 19:16:28 +01:00
semantic-release-bot
62191e3c4a chore: Release v21.0.0-dev.4 [skip ci]
# [21.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.3...v21.0.0-dev.4) (2024-11-05)

### Performance Improvements

* Use smallest lookup map for strings ([1358d3f](1358d3fa10))
2024-11-05 13:41:06 +00:00
oSumAtrIX
1358d3fa10 perf: Use smallest lookup map for strings 2024-11-05 14:39:18 +01:00
semantic-release-bot
6712f0ea72 chore: Release v21.0.0-dev.3 [skip ci]
# [21.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.2...v21.0.0-dev.3) (2024-11-05)

### Features

* Move fingerprint match members to fingerprint for ease of access by using context receivers ([0746c22](0746c22743))
2024-11-05 13:25:22 +00:00
oSumAtrIX
0746c22743 feat: Move fingerprint match members to fingerprint for ease of access by using context receivers 2024-11-05 14:23:19 +01:00
semantic-release-bot
7f55868e6f chore: Release v21.0.0-dev.2 [skip ci]
# [21.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v21.0.0-dev.1...v21.0.0-dev.2) (2024-11-01)

### Bug Fixes

* Match fingerprint before delegating the match property ([5d996de](5d996def4d))
2024-11-01 01:49:47 +00:00
oSumAtrIX
5d996def4d fix: Match fingerprint before delegating the match property 2024-11-01 02:47:57 +01:00
semantic-release-bot
49f4570164 chore: Release v21.0.0-dev.1 [skip ci]
# [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](aa472eb985))

### Features

* Improve Fingerprint API ([#316](https://github.com/ReVanced/revanced-patcher/issues/316)) ([0abf1c6](0abf1c6c02))
* Improve various APIs  ([#317](https://github.com/ReVanced/revanced-patcher/issues/317)) ([b824978](b8249789df))

### BREAKING CHANGES

* Various APIs have been changed.
* Many APIs have been changed.
2024-10-27 15:08:13 +00:00
oSumAtrIX
b8249789df feat: Improve various APIs (#317)
Some APIs have been slightly changed, and API docs have been added.

BREAKING CHANGE: Various APIs have been changed.
2024-10-27 16:06:25 +01:00
oSumAtrIX
0abf1c6c02 feat: Improve Fingerprint API (#316)
Fingerprints can now be matched easily without adding them to a patch first.

BREAKING CHANGE: Many APIs have been changed.
2024-10-27 16:04:30 +01:00
oSumAtrIX
aa472eb985 fix: Merge extension only when patch executes (#315) 2024-10-27 16:00:30 +01:00
semantic-release-bot
ab624f04f6 chore: Release v20.0.2 [skip ci]
## [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](a44802ef4e))
2024-10-17 18:03:27 +00:00
oSumAtrIX
21b5c079fb chore: Merge branch dev to main (#314) 2024-10-17 20:01:49 +02:00
semantic-release-bot
5024204046 chore: Release v20.0.2-dev.1 [skip ci]
## [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](a44802ef4e))
2024-10-15 11:41:01 +00:00
LisoUseInAIKyrios
a44802ef4e fix: Make it work on Android 12 and lower by using existing APIs (#312)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-10-15 13:39:33 +02:00
semantic-release-bot
4c1c34ad01 chore: Release v20.0.1 [skip ci]
## [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](69f2f20fd9))
* Make it work on Android by not using APIs from JVM unavailable to Android. ([2be6e97](2be6e97817))
* Use non-nullable type for options ([ea6fc70](ea6fc70caa))

### Performance Improvements

* Free memory earlier and remove negligible lookup maps ([d53aacd](d53aacdad4))
2024-10-13 01:54:23 +00:00
oSumAtrIX
b2aecb726d chore: Merge branch dev to main (#304) 2024-10-13 03:52:31 +02:00
semantic-release-bot
851f9c7885 chore: Release v20.0.1-dev.5 [skip ci]
## [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](ea6fc70caa))
2024-10-11 03:30:03 +00:00
oSumAtrIX
ea6fc70caa fix: Use non-nullable type for options 2024-10-11 05:28:15 +02:00
semantic-release-bot
a2875d1d64 chore: Release v20.0.1-dev.4 [skip ci]
## [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](2be6e97817))
2024-10-07 16:27:20 +00:00
oSumAtrIX
2be6e97817 fix: Make it work on Android by not using APIs from JVM unavailable to Android. 2024-10-07 18:25:43 +02:00
semantic-release-bot
348d0070e7 chore: Release v20.0.1-dev.3 [skip ci]
## [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](d53aacdad4))
2024-10-03 14:08:34 +00:00
oSumAtrIX
d53aacdad4 perf: Free memory earlier and remove negligible lookup maps
Negligible lookup maps used for matching fingerprints have been removed to reduce the likelihood of OOM when the maps are instantiated, commonly observed with 400M RAM. Additionally, lookup maps previously kept for the duration of the patcher instance are now cleared before the classes are compiled. This reduces the likelihood of OOM when compiling classes.
On a related note, a linear increase in memory usage is observed with every compiled class until all classes are compiled implying compiled classes not being freed by GC because they are still referenced. After compiling a class, the class is technically free-able though. The classes are assumed to be referenced in the `multidexlib2` library that takes the list of all classes to compile multiple DEX with and seems to hold the reference to all these classes in memory until all DEX are compiled. A clever fix would involve splitting the list of classes into chunks and getting rid of the list of all classes so that after every DEX compilation, the corresponding split of classes can be freed.
2024-10-03 16:06:42 +02:00
semantic-release-bot
f1615b7ab5 chore: Release v20.0.1-dev.2 [skip ci]
## [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)
2024-10-01 15:31:49 +00:00
oSumAtrIX
ffb1d880d7 ci: Use permissions and regular GitHub token instead of PAT 2024-10-01 17:25:49 +02:00
oSumAtrIX
e95f13ae3e build(Needs bump): Update dependencies 2024-09-30 23:21:44 +02:00
oSumAtrIX
e1b984d601 ci: Adjust release commit message 2024-09-30 22:34:23 +02:00
semantic-release-bot
c2dc29e061 chore(release): 20.0.1-dev.1 [skip ci]
## [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](69f2f20fd9))
2024-09-18 12:38:03 +00:00
oSumAtrIX
69f2f20fd9 fix: Check for class type exactly instead of with contains (#310) 2024-09-18 14:36:15 +02:00
Pg
525beda18e docs: Fix code example (#306) 2024-08-29 08:59:33 +02:00
oSumAtrIX
73d3cbf4ff build: Bump Gradle Wrapper to 8.9 2024-08-06 17:46:39 +02:00
semantic-release-bot
70278dd79d chore(release): 20.0.0 [skip ci]
# [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](5227e98abf))
* Improve exception message wording ([5481d0c](5481d0c54c))
* Make constructor internal as supposed ([7f44174](7f44174d91))
* Merge all extensions before initializing lookup maps ([8c4dd5b](8c4dd5b3a3))
* Use null for compatible package version when adding packages only ([736b3ee](736b3eebbf))

### Features

* Add ability to create options outside of a patch ([d310246](d310246852))
* Convert APIs to Kotlin DSL ([#298](https://github.com/ReVanced/revanced-patcher/issues/298)) ([11a911d](11a911dc67))

### BREAKING CHANGES

* Various old APIs are removed, and DSL APIs are added instead.
2024-08-06 14:56:20 +00:00
oSumAtrIX
5e98e9e30a chore: Merge branch dev to main (#279) 2024-08-06 16:54:38 +02:00
semantic-release-bot
ac1aff5a1a chore(release): 20.0.0-dev.4 [skip ci]
# [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](bd434ceb33))
2024-08-06 16:53:42 +02:00
oSumAtrIX
5481d0c54c fix: Improve exception message wording 2024-08-06 16:53:42 +02:00
semantic-release-bot
4604742d0f chore(release): 20.0.0-dev.3 [skip ci]
# [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](e95fcd1c0b))

### Features

* Add ability to create options outside of a patch ([b8d763a](b8d763a66e))
2024-08-06 16:53:42 +02:00
oSumAtrIX
4beb907a61 refactor: Sort dependencies 2024-08-06 16:53:42 +02:00
oSumAtrIX
7f44174d91 fix: Make constructor internal as supposed 2024-08-06 16:53:42 +02:00
oSumAtrIX
d310246852 feat: Add ability to create options outside of a patch 2024-08-06 16:53:42 +02:00
semantic-release-bot
dcc989243c 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-08-06 16:53:42 +02:00
oSumAtrIX
5227e98abf fix: Downgrade smali to fix dex compilation issue 2024-08-06 16:53:42 +02:00
oSumAtrIX
8c4dd5b3a3 fix: Merge all extensions before initializing lookup maps 2024-08-06 16:53:42 +02:00
oSumAtrIX
736b3eebbf fix: Use null for compatible package version when adding packages only 2024-08-06 16:53:42 +02:00
oSumAtrIX
b41a542952 refactor: Convert method bodies to single expression functions 2024-08-06 16:53:42 +02:00
oSumAtrIX
d21128fe2e build(Needs bump): Bump dependencies 2024-08-06 16:53:42 +02:00
oSumAtrIX
cf4374b8cf docs: Fix syntax issues and improve wording 2024-08-06 16:53:42 +02:00
semantic-release-bot
8a30b0fa10 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-08-06 16:53:42 +02:00
oSumAtrIX
11a911dc67 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-08-06 16:53:42 +02:00
oSumAtrIX
6e3ba7419b ci: Correct usage of repository variable 2024-08-06 16:53:42 +02:00
oSumAtrIX
50a66ccfed docs: Improve issue templates 2024-08-06 16:53:42 +02:00
oSumAtrIX
0be79840b1 chore: Fix spelling mistake 2024-08-06 16:53:42 +02:00
Vologhat
d8b4c60321 refactor: Simplify mapping classes to their names (#290)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-08-06 16:53:42 +02:00
oSumAtrIX
f77e99e817 build: Publish sources 2024-08-06 16:53:42 +02:00
oSumAtrIX
ea26c486c0 build: Set target bytecode level to JVM 11 2024-08-06 16:53:42 +02:00
oSumAtrIX
bebb734608 ci: Update action 2024-08-06 16:53:42 +02:00
oSumAtrIX
d842f82d07 chore: Lint code 2024-08-06 16:53:42 +02:00
oSumAtrIX
82bab58ac2 build: Bump dependencies 2024-08-06 16:53:42 +02:00
oSumAtrIX
90b7631d9e refactor: Properly abstract Patch#execute function 2024-08-06 16:53:42 +02:00
oSumAtrIX
26d449e6d9 docs: Fix broken links 2024-08-06 16:53:42 +02:00
oSumAtrIX
49466060e3 docs: Fix docs link [skip ci] 2024-08-06 16:53:42 +02:00
oSumAtrIX
620ea5b852 docs: Un-indent markdown to fix rendering 2024-08-06 16:53:42 +02:00
oSumAtrIX
3e2168a2b2 chore: Revert using a relative image path
For some reason GitHub does not render them correctly
2024-08-06 16:53:42 +02:00
oSumAtrIX
13c77967b1 build: Bump dependencies 2024-08-06 16:53:42 +02:00
oSumAtrIX
f57e571a14 chore: Merge branch dev to main (#276) 2024-02-25 03:33:57 +01:00
oSumAtrIX
fe616beb22 docs: Add documentation (#278) 2024-02-25 03:30:08 +01:00
oSumAtrIX
41257ee87e chore: Rename issue templates and use relative image paths 2024-02-25 03:26:15 +01:00
oSumAtrIX
33ed5f0aa3 ci: Fix indentation in workflow 2024-02-24 01:15:09 +01:00
oSumAtrIX
d0a57ac00d docs: Add readme and contribution guidelines 2024-02-23 04:00:59 +01:00
oSumAtrIX
b0b2c10665 ci: Split release into a separate PR build workflow
Because the release workflow already runs on dev and main, it is not necessary to also trigger it for PRs.
2024-02-23 02:26:30 +01:00
oSumAtrIX
cc183062ab chore: Fix inline comment 2024-02-15 02:46:14 +01:00
oSumAtrIX
fe8ea9130d build: Remove unnecessary java plugin 2024-02-14 04:32:33 +01:00
oSumAtrIX
f1c60093cf ci: Update GPG passphrase secret name 2024-02-14 02:49:52 +01:00
oSumAtrIX
687b884dc4 ci: Import GPG key as late as possible
It is not necessary to import it earlier.
2024-02-14 01:59:41 +01:00
semantic-release-bot
ceb6fd51c1 chore(release): 19.3.1 [skip ci]
## [19.3.1](https://github.com/ReVanced/revanced-patcher/compare/v19.3.0...v19.3.1) (2024-02-14)
2024-02-14 00:57:51 +00:00
oSumAtrIX
0b223bfe65 chore: Merge branch dev to main (#275) 2024-02-14 01:55:11 +01:00
semantic-release-bot
308e95cf62 chore(release): 19.3.1-dev.1 [skip ci]
## [19.3.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.3.0...v19.3.1-dev.1) (2024-02-14)
2024-02-14 00:51:49 +00:00
oSumAtrIX
db8866212a build: Bump Gradle 2024-02-14 01:50:02 +01:00
oSumAtrIX
608a05d9aa build: Bump dependencies 2024-02-14 01:50:01 +01:00
oSumAtrIX
55746ed705 build(Needs bump): Add missing repository to publish packages to 2024-02-14 01:47:52 +01:00
semantic-release-bot
e33026c538 chore(release): 19.3.0 [skip ci]
# [19.3.0](https://github.com/ReVanced/revanced-patcher/compare/v19.2.0...v19.3.0) (2024-02-14)

### Bug Fixes

* Use `Patch#toString` to get patch class name, when no name available ([c9a8260](c9a82608f7))

### Features

* Read and write arbitrary files in APK files ([f1d7217](f1d7217495))
2024-02-14 00:35:38 +00:00
oSumAtrIX
ff215620bb chore: Merge branch dev to main (#266) 2024-02-14 01:33:41 +01:00
oSumAtrIX
fec31f45da chore: Add note 2024-02-14 01:14:28 +01:00
oSumAtrIX
7684b70324 build: Publish to GitHub Packages
Because Jitpack can not sign artifacts.
2024-02-14 01:12:54 +01:00
oSumAtrIX
55a5d3bd4e chore: Fix warnings 2024-02-13 05:27:50 +01:00
semantic-release-bot
17a4675a8e chore(release): 19.3.0-dev.2 [skip ci]
# [19.3.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v19.3.0-dev.1...v19.3.0-dev.2) (2024-02-13)
2024-02-13 04:18:58 +00:00
oSumAtrIX
98085d1d45 build(Needs bump): Sign publication artifacts 2024-02-13 05:17:15 +01:00
semantic-release-bot
bc5c16f112 chore(release): 19.3.0-dev.1 [skip ci]
# [19.3.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.2.1-dev.1...v19.3.0-dev.1) (2024-02-13)

### Features

* Read and write arbitrary files in APK files ([f1d7217](f1d7217495))
2024-02-13 02:42:39 +00:00
oSumAtrIX
f1d7217495 feat: Read and write arbitrary files in APK files
This commit allows reading and writing arbitrary files in an APK file. Additionally it allows deleting files from APK files. A `RawResourcePatch` class has been added which has access to `ResourceContext` but ReVanced Patcher will not decode APK resources. A regular `ResourcePatch` can read and write arbitrary files from an APK file, unless they are decoded to `PatcherConfig.apkFiles`. On attempt to get a file from `PatcherConfig.apkFiles` if the second parameter is true, it will read and write the raw resource file from the original APK to `PatcherConfig.apkFiles` if it does not exist. With this commit, many APIs have been deprecated as well, such as `DomFileEditor` and instead a `Document` has been added.
2024-02-13 03:40:39 +01:00
semantic-release-bot
64dd1526cd chore(release): 19.2.1-dev.1 [skip ci]
## [19.2.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.2.0...v19.2.1-dev.1) (2024-01-27)

### Bug Fixes

* Use `Patch#toString` to get patch class name, when no name available ([c9a8260](c9a82608f7))
2024-01-27 03:08:34 +00:00
oSumAtrIX
c9a82608f7 fix: Use Patch#toString to get patch class name, when no name available 2024-01-27 04:06:15 +01:00
oSumAtrIX
9fc42e132c ci: Use latest Node.js LTS version to fix builds
(cherry picked from commit 23b3eb1325704c39db1a99bb169047f4879766d1)
2024-01-27 04:06:15 +01:00
oSumAtrIX
efa98ece45 ci: Add dependabot 2024-01-26 01:45:43 +01:00
oSumAtrIX
68e2acebba build: Bump dependencies 2024-01-26 01:42:02 +01:00
semantic-release-bot
7a7a8fc353 chore(release): 19.2.0 [skip ci]
# [19.2.0](https://github.com/ReVanced/revanced-patcher/compare/v19.1.0...v19.2.0) (2023-12-28)

### Bug Fixes

* Accept `PatchSet` in `PatchesConsumer#acceptPatches` ([716825f](716825f232))

### Features

* Add `PatchExtensions#registerNewPatchOption` function to simplify instantiation and registration of patch options ([4a91845](4a9184597b))
2023-12-28 21:35:22 +00:00
oSumAtrIX
f8306ac43d chore: Merge branch dev to main (#264) 2023-12-28 22:33:35 +01:00
semantic-release-bot
d03591b735 chore(release): 19.2.0-dev.1 [skip ci]
# [19.2.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.1.1-dev.1...v19.2.0-dev.1) (2023-12-22)

### Features

* Add `PatchExtensions#registerNewPatchOption` function to simplify instantiation and registration of patch options ([4a91845](4a9184597b))
2023-12-22 02:23:04 +00:00
oSumAtrIX
4a9184597b feat: Add PatchExtensions#registerNewPatchOption function to simplify instantiation and registration of patch options 2023-12-22 03:20:50 +01:00
oSumAtrIX
0a482f8c9a refactor: Reduce duplicate code by using a common function to register options 2023-12-22 03:09:42 +01:00
oSumAtrIX
e7dacfba8c build: Simplify enabling local build cache 2023-12-10 21:57:10 +01:00
semantic-release-bot
2d7fffd4ec chore(release): 19.1.1-dev.1 [skip ci]
## [19.1.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.1.0...v19.1.1-dev.1) (2023-12-01)

### Bug Fixes

* Accept `PatchSet` in `PatchesConsumer#acceptPatches` ([716825f](716825f232))
2023-12-01 22:15:43 +00:00
oSumAtrIX
f8baabbcec build: Bump dependencies 2023-12-01 23:13:49 +01:00
oSumAtrIX
716825f232 fix: Accept PatchSet in PatchesConsumer#acceptPatches
This deprecates accepting `List<Patch<*>>`
2023-12-01 23:10:23 +01:00
semantic-release-bot
58bd46750b chore(release): 19.1.0 [skip ci]
# [19.1.0](https://github.com/ReVanced/revanced-patcher/compare/v19.0.0...v19.1.0) (2023-12-01)

### Features

* Add constructor to initialize patches without annotations ([462fbe2](462fbe2cad))
* Retrieve annotations in super and interface classes ([7aeae93](7aeae93f3d))

### Performance Improvements

* Use a hash set for fast lookup ([f1de9b3](f1de9b39ef))
2023-12-01 00:19:54 +00:00
oSumAtrIX
288240f163 chore: Merge branch dev to main (#260) 2023-12-01 01:17:47 +01:00
semantic-release-bot
ff02452cb8 chore(release): 19.1.0-dev.1 [skip ci]
# [19.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v19.0.0...v19.1.0-dev.1) (2023-11-29)

### Features

* Add constructor to initialize patches without annotations ([462fbe2](462fbe2cad))
* Retrieve annotations in super and interface classes ([7aeae93](7aeae93f3d))

### Performance Improvements

* Use a hash set for fast lookup ([f1de9b3](f1de9b39ef))
2023-11-29 19:28:24 +00:00
oSumAtrIX
462fbe2cad feat: Add constructor to initialize patches without annotations 2023-11-29 20:26:20 +01:00
oSumAtrIX
7aeae93f3d feat: Retrieve annotations in super and interface classes 2023-11-29 01:14:46 +01:00
oSumAtrIX
f1de9b39ef perf: Use a hash set for fast lookup 2023-11-28 20:17:46 +01:00
oSumAtrIX
db5b0ed7be refactor: Convert extension property to member 2023-11-28 20:17:02 +01:00
oSumAtrIX
9f28a01c03 refactor: Group related extension functions together 2023-11-28 19:59:18 +01:00
oSumAtrIX
80407b6102 chore: Lint code 2023-11-26 05:57:41 +01:00
oSumAtrIX
287841d806 chore: Add heading to issue templates 2023-11-23 00:56:53 +01:00
oSumAtrIX
10c3be1195 build: Bump Gradle wrapper 2023-11-22 01:04:49 +01:00
oSumAtrIX
0c0e22013b build: Use dedicated Gradle cache action 2023-11-22 01:04:49 +01:00
oSumAtrIX
f35c8d4446 build: Use Gradle build cache 2023-11-22 01:04:48 +01:00
oSumAtrIX
17418d4b9c ci: Simplify cache paths 2023-11-22 01:04:48 +01:00
oSumAtrIX
ec1fbdf2ae chore: Reword comment for first PR merge 2023-11-22 01:04:48 +01:00
oSumAtrIX
56e5a46fd5 chore: Add a newline between steps 2023-11-22 01:04:48 +01:00
oSumAtrIX
32e86d44a3 chore: Simplify issue templates 2023-11-22 01:04:47 +01:00
oSumAtrIX
7100606dfc build: Bump dependencies 2023-11-22 01:04:47 +01:00
oSumAtrIX
d7eb111460 ci: Update cache key 2023-11-22 01:04:47 +01:00
oSumAtrIX
27ea46653e chore: Update packages 2023-11-22 01:04:46 +01:00
oSumAtrIX
12c43072cb refactor: Include class of method in call expression 2023-11-03 23:31:15 +01:00
semantic-release-bot
671aa6d507 chore(release): 19.0.0 [skip ci]
# [19.0.0](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0...v19.0.0) (2023-10-24)

### Features

* Add `PatchOption#valueType` to handle type erasure ([a46e948](a46e948b5a))

### BREAKING CHANGES

* This changes the signature of the `PatchOption` constructor.
2023-10-24 20:39:50 +00:00
oSumAtrIX
b697bbad2b chore: Merge branch dev to main (#258) 2023-10-24 22:37:05 +02:00
semantic-release-bot
f05a404e48 chore(release): 19.0.0-dev.1 [skip ci]
# [19.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0...v19.0.0-dev.1) (2023-10-24)

### Features

* Add `PatchOption#valueType` to handle type erasure ([a46e948](a46e948b5a))

### BREAKING CHANGES

* This changes the signature of the `PatchOption` constructor.
2023-10-24 20:35:38 +00:00
oSumAtrIX
a46e948b5a feat: Add PatchOption#valueType to handle type erasure
Without this new property, it is not possible to infer the type without abusing `ClassCastException` at runtime to infer the type of the option value.

BREAKING CHANGE: This changes the signature of the `PatchOption` constructor.
2023-10-24 22:33:10 +02:00
semantic-release-bot
dc09ea639f chore(release): 18.0.0 [skip ci]
# [18.0.0](https://github.com/ReVanced/revanced-patcher/compare/v17.0.0...v18.0.0) (2023-10-22)

### Bug Fixes

* Do not set patch fields if they are empty ([a76ac04](a76ac04214))
* Only allow setting `MethodFingerprint#result` privately ([aed1eac](aed1eac315))

### Code Refactoring

* Change `PatchOption` from abstract to open class ([09cd6aa](09cd6aa568))
* Change data classes to actual classes ([6192089](6192089b71))
* Convert extension functions to member functions ([e2ca507](e2ca50729d))
* Move files to simplify package structure ([124a2e9](124a2e9d3e))
* Remove deprecated classes and members ([a4212f6](a4212f6bf9))

* refactor!: Remove `Fingerprint` interface ([54a2f8f](54a2f8f16f))

### Features

* Add function to reset options to their default value ([ebbaafb](ebbaafb78e))
* Add function to reset options to their default value ([e6de90d](e6de90d300))
* Add getter for default option value ([c7922e9](c7922e90d0))
* Make `PatchOption#values` nullable ([56ce9ec](56ce9ec2f9))
* Name patch option value validator property correctly ([caa634f](caa634fac6))
* Remove patch annotation processor ([4456031](4456031459))
* Use a map for `PatchOption#values` ([54ac139](54ac1394a9))

### Performance Improvements

* Run the garbage collector after writing dex files ([d9fb241](d9fb241d57))

### BREAKING CHANGES

* The `MethodFingerprint#result` member can now only be set inside `MethodFingerprint`.
* The `Fingerprint` interface is no longer present.
* Some extension functions are now member functions.
* This gets rid of data class members.
* Some deprecated classes and members are not present anymore.
* Classes and members have changed packages.
* This gets rid of the existing basic implementations of the `PatchOptions` type and moves extension functions.
* This changes the getter name of the property.
* Various patch constructor signatures have changed.
2023-10-22 23:59:31 +00:00
oSumAtrIX
49ed096e85 chore: Merge branch dev to main (#257) 2023-10-23 01:56:40 +02:00
semantic-release-bot
167bd83f4e chore(release): 18.0.0-dev.6 [skip ci]
# [18.0.0-dev.6](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0-dev.5...v18.0.0-dev.6) (2023-10-22)

### Bug Fixes

* Only allow setting `MethodFingerprint#result` privately ([aed1eac](aed1eac315))

### Code Refactoring

* Change data classes to actual classes ([6192089](6192089b71))
* Convert extension functions to member functions ([e2ca507](e2ca50729d))
* Move files to simplify package structure ([124a2e9](124a2e9d3e))
* Remove deprecated classes and members ([a4212f6](a4212f6bf9))

* refactor!: Remove `Fingerprint` interface ([54a2f8f](54a2f8f16f))

### BREAKING CHANGES

* The `MethodFingerprint#result` member can now only be set inside `MethodFingerprint`.
* The `Fingerprint` interface is no longer present.
* Some extension functions are now member functions.
* This gets rid of data class members.
* Some deprecated classes and members are not present anymore.
* Classes and members have changed packages.
2023-10-22 23:54:59 +00:00
oSumAtrIX
aed1eac315 fix: Only allow setting MethodFingerprint#result privately
BREAKING CHANGE: The `MethodFingerprint#result` member can now only be set inside `MethodFingerprint`.
2023-10-23 01:52:43 +02:00
oSumAtrIX
54a2f8f16f refactor!: Remove Fingerprint interface
It served no purpose so far.

BREAKING CHANGE: The `Fingerprint` interface is no longer present.
2023-10-23 01:52:04 +02:00
oSumAtrIX
2ca543ffb9 refactor: Move lookup related classes and members to own file 2023-10-23 01:51:48 +02:00
oSumAtrIX
58e7f815a5 refactor: Move MethodFingerprintResult to own file 2023-10-23 01:51:34 +02:00
oSumAtrIX
15b38fc841 refactor: Simplify method implementation 2023-10-23 01:50:54 +02:00
oSumAtrIX
e2ca50729d refactor: Convert extension functions to member functions
BREAKING CHANGE: Some extension functions are now member functions.
2023-10-23 01:50:53 +02:00
oSumAtrIX
6192089b71 refactor: Change data classes to actual classes
The data class members did not serve any actual purpose.

BREAKING CHANGE: This gets rid of data class members.
2023-10-23 01:49:57 +02:00
oSumAtrIX
a4212f6bf9 refactor: Remove deprecated classes and members
BREAKING CHANGE: Some deprecated classes and members are not present anymore.
2023-10-23 01:49:23 +02:00
oSumAtrIX
124a2e9d3e refactor: Move files to simplify package structure
BREAKING CHANGE: Classes and members have changed packages.
2023-10-23 01:48:52 +02:00
semantic-release-bot
f77624b3b9 chore(release): 18.0.0-dev.5 [skip ci]
# [18.0.0-dev.5](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0-dev.4...v18.0.0-dev.5) (2023-10-22)

### Bug Fixes

* Do not set patch fields if they are empty ([a76ac04](a76ac04214))
2023-10-22 16:10:54 +00:00
oSumAtrIX
a76ac04214 fix: Do not set patch fields if they are empty 2023-10-22 18:08:28 +02:00
semantic-release-bot
0447fa9c28 chore(release): 18.0.0-dev.4 [skip ci]
# [18.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0-dev.3...v18.0.0-dev.4) (2023-10-22)

### Features

* Use a map for `PatchOption#values` ([54ac139](54ac1394a9))
2023-10-22 15:39:57 +00:00
oSumAtrIX
54ac1394a9 feat: Use a map for PatchOption#values
This allows to display a string representation associated to the option value.
2023-10-22 17:07:22 +02:00
oSumAtrIX
0b04c73ac5 build: Bump Kotlin Gradle plugin version 2023-10-22 16:14:46 +02:00
semantic-release-bot
079de45238 chore(release): 18.0.0-dev.3 [skip ci]
# [18.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0-dev.2...v18.0.0-dev.3) (2023-10-22)

### Features

* Make `PatchOption#values` nullable ([56ce9ec](56ce9ec2f9))
2023-10-22 14:09:28 +00:00
oSumAtrIX
56ce9ec2f9 feat: Make PatchOption#values nullable
There is no difference semantically, but this change allows passing null as a parameter which is simpler than having to use `emptySet()`.
2023-10-22 16:07:01 +02:00
semantic-release-bot
1b52e4b0f9 chore(release): 18.0.0-dev.2 [skip ci]
# [18.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v18.0.0-dev.1...v18.0.0-dev.2) (2023-10-22)

### Code Refactoring

* Change `PatchOption` from abstract to open class ([09cd6aa](09cd6aa568))

### Features

* Add function to reset options to their default value ([ebbaafb](ebbaafb78e))
* Add function to reset options to their default value ([e6de90d](e6de90d300))
* Add getter for default option value ([c7922e9](c7922e90d0))
* Name patch option value validator property correctly ([caa634f](caa634fac6))

### BREAKING CHANGES

* This gets rid of the existing basic implementations of the `PatchOptions` type and moves extension functions.
* This changes the getter name of the property.
2023-10-22 00:56:40 +00:00
oSumAtrIX
098c2c1efa chore: Test default option value 2023-10-22 02:50:26 +02:00
oSumAtrIX
64343e5a7c chore: Test resetting options 2023-10-22 02:49:44 +02:00
oSumAtrIX
0caf6caeb9 refactor: Simplify patch option tests 2023-10-22 02:46:51 +02:00
oSumAtrIX
55f6c2a9fc feat!: Add property PatchOption#values 2023-10-22 02:38:12 +02:00
oSumAtrIX
c6095bc38a feat!: Add PatchOption#toString 2023-10-22 02:04:49 +02:00
oSumAtrIX
09cd6aa568 refactor: Change PatchOption from abstract to open class
BREAKING CHANGE: This gets rid of the existing basic implementations of the `PatchOptions` type and moves extension functions.
2023-10-22 01:55:05 +02:00
oSumAtrIX
ebbaafb78e feat: Add function to reset options to their default value 2023-10-22 01:14:48 +02:00
oSumAtrIX
e6de90d300 feat: Add function to reset options to their default value 2023-10-22 01:10:15 +02:00
oSumAtrIX
c7922e90d0 feat: Add getter for default option value 2023-10-22 01:02:43 +02:00
oSumAtrIX
c1f4c0445a chore: Fix inline docs wording 2023-10-21 23:52:47 +02:00
oSumAtrIX
caa634fac6 feat: Name patch option value validator property correctly
BREAKING CHANGE: This changes the getter name of the property.
2023-10-21 23:51:44 +02:00
oSumAtrIX
f28bfe0dbd chore: Add missing inline docs 2023-10-21 23:49:40 +02:00
semantic-release-bot
155e787ff4 chore(release): 18.0.0-dev.1 [skip ci]
# [18.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v17.0.1-dev.1...v18.0.0-dev.1) (2023-10-14)

### Features

* Remove patch annotation processor ([4456031](4456031459))

### BREAKING CHANGES

* Various patch constructor signatures have changed.
2023-10-14 18:43:40 +00:00
oSumAtrIX
c38f0ef42a build: Move subproject to root project 2023-10-14 19:30:10 +02:00
oSumAtrIX
4456031459 feat: Remove patch annotation processor
Unfortunately processing annotations required generating new classes which turned out to be quite cumbersome to work with, especially when trying to publish an API. Therefor, the patch annotation retention is now `RUNTIME`.

BREAKING CHANGE: Various patch constructor signatures have changed.
2023-10-14 19:29:23 +02:00
semantic-release-bot
5fb59a227f chore(release): 17.0.1-dev.1 [skip ci]
## [17.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v17.0.0...v17.0.1-dev.1) (2023-10-10)

### Performance Improvements

* Run the garbage collector after writing dex files ([d9fb241](d9fb241d57))
2023-10-10 18:23:51 +00:00
oSumAtrIX
d9fb241d57 perf: Run the garbage collector after writing dex files
Writing dex files consumes a lot of memory.
2023-10-10 20:06:27 +02:00
oSumAtrIX
642c4ea97e refactor: Use correct class structure 2023-10-10 20:05:46 +02:00
semantic-release-bot
8c8a251626 chore(release): 17.0.0 [skip ci]
# [17.0.0](https://github.com/ReVanced/revanced-patcher/compare/v16.0.2...v17.0.0) (2023-10-09)

### Features

* Add option to use single threaded writer for dex files ([77dbee3](77dbee3d6a))

### BREAKING CHANGES

* This commit gets rid of deprecated constructors.
2023-10-09 13:00:18 +00:00
oSumAtrIX
5953d6cfb5 chore: Merge branch dev to main (#252) 2023-10-09 14:56:10 +02:00
semantic-release-bot
a1962fe600 chore(release): 17.0.0-dev.1 [skip ci]
# [17.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v16.0.2...v17.0.0-dev.1) (2023-10-09)

### Features

* Add option to use single threaded writer for dex files ([77dbee3](77dbee3d6a))

### BREAKING CHANGES

* This commit gets rid of deprecated constructors.
2023-10-09 12:53:08 +00:00
oSumAtrIX
77dbee3d6a feat: Add option to use single threaded writer for dex files
BREAKING CHANGE: This commit gets rid of deprecated constructors.
2023-10-09 14:42:20 +02:00
oSumAtrIX
cb5e39d73e build: Publish artifacts on Jitpack 2023-10-09 14:42:20 +02:00
semantic-release-bot
38ef2f470a chore(release): 16.0.2 [skip ci]
## [16.0.2](https://github.com/ReVanced/revanced-patcher/compare/v16.0.1...v16.0.2) (2023-10-06)

### Performance Improvements

* Use a map to merge integrations classes ([6059d3c](6059d3ca26))
2023-10-06 23:59:10 +00:00
oSumAtrIX
129d84e108 chore: Merge branch dev to main (#250) 2023-10-07 01:55:48 +02:00
semantic-release-bot
affeba76b8 chore(release): 16.0.2-dev.1 [skip ci]
## [16.0.2-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v16.0.1...v16.0.2-dev.1) (2023-10-06)

### Performance Improvements

* Use a map to merge integrations classes ([6059d3c](6059d3ca26))
2023-10-06 23:55:11 +00:00
oSumAtrIX
6059d3ca26 perf: Use a map to merge integrations classes 2023-10-07 01:52:21 +02:00
semantic-release-bot
444dee5a16 chore(release): 16.0.1 [skip ci]
## [16.0.1](https://github.com/ReVanced/revanced-patcher/compare/v16.0.0...v16.0.1) (2023-10-05)

### Bug Fixes

* Merge integrations when required ([06c2b76](06c2b76f11))
2023-10-05 15:31:28 +00:00
oSumAtrIX
d314466ce2 chore: Merge branch dev to main (#249) 2023-10-05 17:28:45 +02:00
semantic-release-bot
fdaf9c21c8 chore(release): 16.0.1-dev.1 [skip ci]
## [16.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v16.0.0...v16.0.1-dev.1) (2023-10-05)

### Bug Fixes

* Merge integrations when required ([06c2b76](06c2b76f11))
2023-10-05 15:27:34 +00:00
oSumAtrIX
06c2b76f11 fix: Merge integrations when required
Previous to this commit integrations were merged when not required.
2023-10-05 17:24:28 +02:00
semantic-release-bot
3896b30738 chore(release): 16.0.0 [skip ci]
# [16.0.0](https://github.com/ReVanced/revanced-patcher/compare/v15.0.3...v16.0.0) (2023-10-04)

### Bug Fixes

* Use correct super class type ([f590436](f590436399))

### BREAKING CHANGES

* This changes the super classes of some `PatchOptionException` classes
2023-10-04 02:18:27 +00:00
oSumAtrIX
2c4b88e1a0 chore: Merge branch dev to main (#248) 2023-10-04 04:15:04 +02:00
semantic-release-bot
dfc7e1596b chore(release): 16.0.0-dev.1 [skip ci]
# [16.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v15.0.3...v16.0.0-dev.1) (2023-10-04)

### Bug Fixes

* Use correct super class type ([f590436](f590436399))

### BREAKING CHANGES

* This changes the super classes of some `PatchOptionException` classes
2023-10-04 02:06:03 +00:00
oSumAtrIX
f590436399 fix: Use correct super class type
BREAKING CHANGE: This changes the super classes of some `PatchOptionException` classes
2023-10-04 04:02:10 +02:00
semantic-release-bot
cbfb9ba02f chore(release): 15.0.3 [skip ci]
## [15.0.3](https://github.com/ReVanced/revanced-patcher/compare/v15.0.2...v15.0.3) (2023-10-01)

### Bug Fixes

* Fix SMALI compilation on devices with RTL language ([#242](https://github.com/ReVanced/revanced-patcher/issues/242)) ([356f1f1](356f1f1553))
2023-10-01 16:58:40 +00:00
oSumAtrIX
b4cfe80ad5 chore: Merge branch dev to main (#243) 2023-10-01 18:56:02 +02:00
semantic-release-bot
b37906fa35 chore(release): 15.0.3-dev.1 [skip ci]
## [15.0.3-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v15.0.2...v15.0.3-dev.1) (2023-09-29)

### Bug Fixes

* Fix SMALI compilation on devices with RTL language ([#242](https://github.com/ReVanced/revanced-patcher/issues/242)) ([356f1f1](356f1f1553))
2023-09-29 13:18:47 +00:00
LisoUseInAIKyrios
356f1f1553 fix: Fix SMALI compilation on devices with RTL language (#242) 2023-09-29 15:15:14 +02:00
semantic-release-bot
e882af74ee chore(release): 15.0.2 [skip ci]
## [15.0.2](https://github.com/ReVanced/revanced-patcher/compare/v15.0.1...v15.0.2) (2023-09-27)

### Performance Improvements

* Do not unnecessary resolve fingeprints twice ([#241](https://github.com/ReVanced/revanced-patcher/issues/241)) ([4d6e08a](4d6e08a650))
2023-09-27 21:27:32 +00:00
oSumAtrIX
46875fb28e chore: Merge branch dev to main (#238) 2023-09-27 23:23:46 +02:00
oSumAtrIX
417c3e4234 ci: Bump checkout action 2023-09-27 18:02:50 +02:00
semantic-release-bot
6d2c28807b chore(release): 15.0.2-dev.1 [skip ci]
## [15.0.2-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v15.0.1...v15.0.2-dev.1) (2023-09-26)

### Performance Improvements

* Do not unnecessary resolve fingeprints twice ([#241](https://github.com/ReVanced/revanced-patcher/issues/241)) ([4d6e08a](4d6e08a650))
2023-09-26 12:25:00 +00:00
LisoUseInAIKyrios
4d6e08a650 perf: Do not unnecessary resolve fingeprints twice (#241) 2023-09-26 14:21:43 +02:00
oSumAtrIX
5cebc1fd30 ci: Use better workflow name and PR message 2023-09-23 18:17:47 +02:00
semantic-release-bot
ac61731dc6 chore(release): 15.0.1 [skip ci]
## [15.0.1](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0...v15.0.1) (2023-09-20)

### Bug Fixes

* Remove log management ([d51bc32](d51bc32e37))
2023-09-20 03:19:14 +00:00
oSumAtrIX
9e4ffabd5c chore: merge branch dev to main (#235) 2023-09-20 05:16:25 +02:00
semantic-release-bot
3f410bd39f chore(release): 15.0.1-dev.1 [skip ci]
## [15.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0...v15.0.1-dev.1) (2023-09-20)

### Bug Fixes

* Remove log management ([d51bc32](d51bc32e37))
2023-09-20 03:16:04 +00:00
oSumAtrIX
d51bc32e37 fix: Remove log management
Changing the logging behaviour caused unintended behaviour outside of the library.
2023-09-19 05:09:19 +02:00
oSumAtrIX
b7f6aa94cc chore: Fix inline docs reference 2023-09-19 05:08:37 +02:00
semantic-release-bot
ff965e6953 chore(release): 15.0.0 [skip ci]
# [15.0.0](https://github.com/ReVanced/revanced-patcher/compare/v14.2.2...v15.0.0) (2023-09-18)

### Bug Fixes

* Account for source patch dependency for tests ([6918418](69184187d9))
* Always make the generated patch depend on the source patch ([8de3063](8de30633ae))
* Catch correct exception ([637d487](637d48746f))
* Delegate `PatchBundleLoader` by mutable set of patches ([9a109c1](9a109c129b))
* Do not resolve the proxied patch to the proxy in the dependency list ([e112837](e11283744a))
* Do not set `CompatiblePackage.versions` if `@CompatiblePackage.versions` is empty ([6b1e0a1](6b1e0a1656))
* Filter for patches correctly ([4bc4b0d](4bc4b0dc01))
* Find dependency in `context.allPatches` ([670f015](670f0153de))
* Log the correct patch names ([9fdb8f0](9fdb8f087f))
* Make `CompatiblePackage.versions` a property ([67b7dff](67b7dff67a))
* Print patch name instead of class name ([4e7811e](4e7811ea07))
* Print stack trace of exception ([aa71146](aa71146b1b))
* Run code-block if `executablePatches` does not yet contain `patch` ([1d7aeca](1d7aeca696))
* Suppress logger when loading patches in `PatchBundleLoader` ([72c9eb2](72c9eb2129))
* Use correct module name ([080fbe9](080fbe9feb))

### Code Refactoring

* Internalize processor constructor ([a802d0d](a802d0df46))

* feat Use `Set` as super type for `PatchBundleLoader` ([4b76d19](4b76d19596))
* feat!: Add patch annotation processor ([3fc6a13](3fc6a139ee))
* feat!: Remove patch annotations ([3b4db3d](3b4db3ddb7))

### Features

* Add patch annotation processor ([#231](https://github.com/ReVanced/revanced-patcher/issues/231)) ([a29931f](a29931f2ec))

### BREAKING CHANGES

* This gets rid of the public constructor.
* `PatchBundleLoader` is not a map anymore
* This renames packages and the Maven package.
* The manifest for patches has been removed, and the properties have been added to patches. Patches are now `OptionsContainer`. The `@Patch` annotation has been removed in favour of the `@Patch` annotation from the annotation processor.
* Patch annotations have been removed. PatcherException is now thrown in various places. PatchBundleLoader is now a map of patches associated by their name. Patches are now instances.
2023-09-18 15:44:01 +00:00
oSumAtrIX
468d5d7421 chore: merge branch dev to main (#232) 2023-09-18 17:40:24 +02:00
semantic-release-bot
fc95b28c49 chore(release): 15.0.0-dev.4 [skip ci]
# [15.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0-dev.3...v15.0.0-dev.4) (2023-09-13)

### Bug Fixes

* Account for source patch dependency for tests ([6918418](69184187d9))
* Always make the generated patch depend on the source patch ([8de3063](8de30633ae))
* Catch correct exception ([637d487](637d48746f))
* Delegate `PatchBundleLoader` by mutable set of patches ([9a109c1](9a109c129b))
* Do not resolve the proxied patch to the proxy in the dependency list ([e112837](e11283744a))
* Do not set `CompatiblePackage.versions` if `@CompatiblePackage.versions` is empty ([6b1e0a1](6b1e0a1656))
* Filter for patches correctly ([4bc4b0d](4bc4b0dc01))
* Find dependency in `context.allPatches` ([670f015](670f0153de))
* Log the correct patch names ([9fdb8f0](9fdb8f087f))
* Print patch name instead of class name ([4e7811e](4e7811ea07))
* Print stack trace of exception ([aa71146](aa71146b1b))
* Run code-block if `executablePatches` does not yet contain `patch` ([1d7aeca](1d7aeca696))
* Suppress logger when loading patches in `PatchBundleLoader` ([72c9eb2](72c9eb2129))

### Code Refactoring

* Internalize processor constructor ([a802d0d](a802d0df46))

### BREAKING CHANGES

* This gets rid of the public constructor.
2023-09-13 03:44:27 +00:00
oSumAtrIX
69184187d9 fix: Account for source patch dependency for tests 2023-09-13 05:40:59 +02:00
oSumAtrIX
a802d0df46 refactor: Internalize processor constructor
BREAKING CHANGE: This gets rid of the public constructor.
2023-09-13 05:32:40 +02:00
oSumAtrIX
8de30633ae fix: Always make the generated patch depend on the source patch 2023-09-13 05:18:28 +02:00
oSumAtrIX
a1fbb7990f refactor: Simplify fingerprint resolution 2023-09-13 04:13:38 +02:00
oSumAtrIX
aa71146b1b fix: Print stack trace of exception
Otherwise the log will not include what originally raised the exception.
2023-09-13 03:58:46 +02:00
oSumAtrIX
9fdb8f087f fix: Log the correct patch names 2023-09-13 03:35:39 +02:00
oSumAtrIX
670f0153de fix: Find dependency in context.allPatches 2023-09-13 03:06:40 +02:00
oSumAtrIX
1d7aeca696 fix: Run code-block if executablePatches does not yet contain patch 2023-09-13 03:06:19 +02:00
oSumAtrIX
4e7811ea07 fix: Print patch name instead of class name 2023-09-13 03:05:37 +02:00
oSumAtrIX
e11283744a fix: Do not resolve the proxied patch to the proxy in the dependency list
If a patch is used as a dependency, it would be present in `dependencyResolutionMap`. If that patch would also be annotated, then the generated patch would depend on itself.
2023-09-13 03:05:06 +02:00
oSumAtrIX
91cdfd53ef refactor: Simplify accessing the patches from PatcherContext.executablePatches 2023-09-13 02:22:31 +02:00
oSumAtrIX
bc7d6b9941 refactor: Rename unused destructed parameter to _ 2023-09-13 02:17:54 +02:00
oSumAtrIX
6b1e0a1656 fix: Do not set CompatiblePackage.versions if @CompatiblePackage.versions is empty 2023-09-13 02:17:24 +02:00
oSumAtrIX
72c9eb2129 fix: Suppress logger when loading patches in PatchBundleLoader 2023-09-13 02:11:51 +02:00
oSumAtrIX
4bc4b0dc01 fix: Filter for patches correctly 2023-09-13 01:52:15 +02:00
oSumAtrIX
637d48746f fix: Catch correct exception 2023-09-13 01:51:46 +02:00
oSumAtrIX
9a109c129b fix: Delegate PatchBundleLoader by mutable set of patches 2023-09-13 01:36:53 +02:00
oSumAtrIX
d49e4ee5ea refactor: Remove unused logger 2023-09-13 00:08:52 +02:00
semantic-release-bot
30f0ea29a3 chore(release): 15.0.0-dev.3 [skip ci]
# [15.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0-dev.2...v15.0.0-dev.3) (2023-09-06)

### Bug Fixes

* Make `CompatiblePackage.versions` a property ([67b7dff](67b7dff67a))
* Use correct module name ([080fbe9](080fbe9feb))

* feat Use `Set` as super type for `PatchBundleLoader` ([4b76d19](4b76d19596))

### BREAKING CHANGES

* `PatchBundleLoader` is not a map anymore
* This renames packages and the Maven package.
2023-09-06 12:32:05 +00:00
oSumAtrIX
49930f6565 chore: Use correct resource names for tests 2023-09-06 14:27:51 +02:00
oSumAtrIX
909d89fa8d chore: Print correct error message 2023-09-06 14:23:26 +02:00
oSumAtrIX
81d1d7f544 refactor: Do not reify generic type 2023-09-06 06:34:38 +02:00
oSumAtrIX
67b7dff67a fix: Make CompatiblePackage.versions a property 2023-09-06 06:12:44 +02:00
oSumAtrIX
4b76d19596 feat Use Set as super type for PatchBundleLoader
BREAKING CHANGE: `PatchBundleLoader` is not a map anymore
2023-09-06 06:12:22 +02:00
oSumAtrIX
080fbe9feb fix: Use correct module name
BREAKING CHANGE: This renames packages and the Maven package.
2023-09-06 06:11:15 +02:00
semantic-release-bot
d3721229bf chore(release): 15.0.0-dev.2 [skip ci]
# [15.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v15.0.0-dev.1...v15.0.0-dev.2) (2023-09-06)
2023-09-06 03:36:03 +00:00
oSumAtrIX
86c1c9c772 build(Needs bump): Use correct Maven repository URL 2023-09-06 05:32:58 +02:00
oSumAtrIX
c299817193 feat!: Use Kotlin DSL for patch options (#234) 2023-09-06 02:53:31 +02:00
semantic-release-bot
fcc1de45ed chore(release): 15.0.0-dev.1 [skip ci]
# [15.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.2.2...v15.0.0-dev.1) (2023-09-04)

* feat!: Add patch annotation processor ([3fc6a13](3fc6a139ee))
* feat!: Remove patch annotations ([3b4db3d](3b4db3ddb7))

### Features

* Add patch annotation processor ([#231](https://github.com/ReVanced/revanced-patcher/issues/231)) ([a29931f](a29931f2ec))

### BREAKING CHANGES

* The manifest for patches has been removed, and the properties have been added to patches. Patches are now `OptionsContainer`. The `@Patch` annotation has been removed in favour of the `@Patch` annotation from the annotation processor.
* Patch annotations have been removed. PatcherException is now thrown in various places. PatchBundleLoader is now a map of patches associated by their name. Patches are now instances.
2023-09-04 03:51:04 +00:00
oSumAtrIX
a29931f2ec feat: Add patch annotation processor (#231) 2023-09-04 05:48:20 +02:00
oSumAtrIX
3fc6a139ee feat!: Add patch annotation processor
This commit introduces an annotation processor for patches. Patches can use the `@Patch` instead of super constructor parameters.

BREAKING CHANGE: The manifest for patches has been removed, and the properties have been added to patches. Patches are now `OptionsContainer`. The `@Patch` annotation has been removed in favour of the `@Patch` annotation from the annotation processor.
2023-09-04 05:43:41 +02:00
oSumAtrIX
4dd04975d9 refactor: Move ReVanced Patcher to sub-project
This allows other sub-projects to exist.
2023-09-04 05:37:13 +02:00
oSumAtrIX
3b4db3ddb7 feat!: Remove patch annotations
Annotations required reflection and working with them turned out to be rather cumbersome. The annotations have been replaced with properties for the most part.

BREAKING CHANGE: Patch annotations have been removed. PatcherException is now thrown in various places. PatchBundleLoader is now a map of patches associated by their name. Patches are now instances.
2023-09-04 05:24:29 +02:00
semantic-release-bot
c4a7117ee8 chore(release): 14.2.2 [skip ci]
## [14.2.2](https://github.com/ReVanced/revanced-patcher/compare/v14.2.1...v14.2.2) (2023-08-30)

### Bug Fixes

* allow setting `DexClassLoader.optimizedDirectory` ([11a3378](11a3378659))
2023-08-30 17:25:12 +00:00
oSumAtrIX
b4e900fde8 chore: merge branch dev to main (#226) 2023-08-30 19:22:46 +02:00
semantic-release-bot
9818d730e4 chore(release): 14.2.2-dev.1 [skip ci]
## [14.2.2-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.2.1...v14.2.2-dev.1) (2023-08-29)

### Bug Fixes

* allow setting `DexClassLoader.optimizedDirectory` ([11a3378](11a3378659))
2023-08-29 02:12:54 +00:00
oSumAtrIX
11a3378659 fix: allow setting DexClassLoader.optimizedDirectory
This is required for API level below 26, otherwise an NPE is thrown
2023-08-29 04:09:17 +02:00
oSumAtrIX
1bb05f22d3 chore: add initial API dump
This is necessary for the binary compatibility validation.
2023-08-29 04:05:18 +02:00
oSumAtrIX
26b70554c4 build: use binary compatibility validator
This prevents releasing breaking changes accidentally.
2023-08-29 03:47:21 +02:00
oSumAtrIX
93b29d2e83 chore: assert instead of printing in tests 2023-08-29 03:38:11 +02:00
semantic-release-bot
072986374a chore(release): 14.2.1 [skip ci]
## [14.2.1](https://github.com/ReVanced/revanced-patcher/compare/v14.2.0...v14.2.1) (2023-08-27)

### Bug Fixes

* do not flag resource table as sparse when main package is not loaded ([b832812](b832812767))
2023-08-27 20:23:09 +00:00
oSumAtrIX
2c590d212a chore: merge branch dev to main (#225) 2023-08-27 22:21:36 +02:00
semantic-release-bot
6cc863efb3 chore(release): 14.2.1-dev.1 [skip ci]
## [14.2.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.2.0...v14.2.1-dev.1) (2023-08-27)

### Bug Fixes

* do not flag resource table as sparse when main package is not loaded ([b832812](b832812767))
2023-08-27 20:17:31 +00:00
oSumAtrIX
b832812767 fix: do not flag resource table as sparse when main package is not loaded 2023-08-27 22:14:43 +02:00
oSumAtrIX
c44558cacd build: bump dependencies
This fixes an issue with flagging the resource table with sparse incorrectly.
2023-08-27 22:14:11 +02:00
semantic-release-bot
6d83a720cd chore(release): 14.2.0 [skip ci]
# [14.2.0](https://github.com/ReVanced/revanced-patcher/compare/v14.1.0...v14.2.0) (2023-08-27)

### Features

* load patches in lexicographical order ([e8f2087](e8f2087a6f))
* log when merging integrations ([983563e](983563efb6))

### Performance Improvements

* compare types of classes ([55d6945](55d694579a))
2023-08-27 00:55:36 +00:00
oSumAtrIX
8d0dd9c448 chore: merge branch dev to main (#222) 2023-08-27 02:53:47 +02:00
oSumAtrIX
64020eec49 chore: add multi-threading opportunity notice 2023-08-26 20:58:19 +02:00
semantic-release-bot
4dedfb85cb chore(release): 14.2.0-dev.3 [skip ci]
# [14.2.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v14.2.0-dev.2...v14.2.0-dev.3) (2023-08-26)

### Performance Improvements

* compare types of classes ([55d6945](55d694579a))
2023-08-26 17:44:12 +00:00
oSumAtrIX
55d694579a perf: compare types of classes 2023-08-26 19:42:33 +02:00
semantic-release-bot
86db64edff chore(release): 14.2.0-dev.2 [skip ci]
# [14.2.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v14.2.0-dev.1...v14.2.0-dev.2) (2023-08-26)

### Features

* log when merging integrations ([983563e](983563efb6))
2023-08-26 17:16:08 +00:00
oSumAtrIX
983563efb6 feat: log when merging integrations 2023-08-26 19:14:29 +02:00
oSumAtrIX
37abb2db99 build: package the Java sources 2023-08-26 19:10:29 +02:00
semantic-release-bot
5ba0b47e60 chore(release): 14.2.0-dev.1 [skip ci]
# [14.2.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.1.0...v14.2.0-dev.1) (2023-08-25)

### Features

* load patches in lexicographical order ([e8f2087](e8f2087a6f))
2023-08-25 15:12:46 +00:00
oSumAtrIX
e8f2087a6f feat: load patches in lexicographical order
This feature was lost in past commits
2023-08-25 17:10:44 +02:00
semantic-release-bot
6ce99f5cdf chore(release): 14.1.0 [skip ci]
# [14.1.0](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0...v14.1.0) (2023-08-24)

### Bug Fixes

* move version properties file to correct package ([e985676](e985676c2d))

### Features

* properly make use of logging facade ([ba56a6a](ba56a6a2ee))
2023-08-24 02:50:15 +00:00
oSumAtrIX
13c0c9cdd3 chore: merge branch dev to main (#220) 2023-08-24 04:48:39 +02:00
semantic-release-bot
58ffdb60d7 chore(release): 14.1.0-dev.1 [skip ci]
# [14.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.0.1-dev.1...v14.1.0-dev.1) (2023-08-24)

### Features

* properly make use of logging facade ([ba56a6a](ba56a6a2ee))
2023-08-24 02:45:16 +00:00
oSumAtrIX
ba56a6a2ee feat: properly make use of logging facade
This deprecates the primary constructor of `PatcherOptions` with the `logger` parameter
2023-08-24 04:43:16 +02:00
oSumAtrIX
ccccf5b1d2 build: migrate dependencies to version catalogs 2023-08-23 04:30:31 +02:00
semantic-release-bot
b507ac0a54 chore(release): 14.0.1-dev.1 [skip ci]
## [14.0.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0...v14.0.1-dev.1) (2023-08-23)

### Bug Fixes

* move version properties file to correct package ([e985676](e985676c2d))
2023-08-23 01:39:26 +00:00
oSumAtrIX
e985676c2d fix: move version properties file to correct package 2023-08-23 03:36:48 +02:00
oSumAtrIX
f7f4ba6c55 build: update dependencies 2023-08-23 03:36:48 +02:00
semantic-release-bot
4292f43814 chore(release): 14.0.0 [skip ci]
# [14.0.0](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0...v14.0.0) (2023-08-22)

### Bug Fixes

* log decoding resources after logging deleting resource cache directory ([db62a16](db62a1607b))
* only emit closed patches that did not throw an exception with the `@Patch` annotation ([5938f6b](5938f6b7ea))
* supply the parent classloader to `DexClassLoader` ([0f15077](0f15077225))

### Code Refactoring

* improve structure and public API ([6b8977f](6b8977f178))

### Features

* do not log instantiation of ReVanced Patcher ([273dd8d](273dd8d388))

### BREAKING CHANGES

* Various public APIs have been changed. The `Version` annotation has been removed. Patches do not return anything anymore and instead throw `PatchException`. Multiple patch bundles can now be loaded in a single ClassLoader to bypass class loader isolation.
2023-08-22 17:16:51 +00:00
oSumAtrIX
30bd4fd9fe chore: merge branch dev to main (#217) 2023-08-22 19:15:09 +02:00
semantic-release-bot
76de39369d chore(release): 14.0.0-dev.4 [skip ci]
# [14.0.0-dev.4](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.3...v14.0.0-dev.4) (2023-08-22)

### Bug Fixes

* only emit closed patches that did not throw an exception with the `@Patch` annotation ([5938f6b](5938f6b7ea))
2023-08-22 17:04:25 +00:00
oSumAtrIX
88a703ce36 build: bump dependencies 2023-08-22 19:01:59 +02:00
oSumAtrIX
5938f6b7ea fix: only emit closed patches that did not throw an exception with the @Patch annotation 2023-08-22 19:00:34 +02:00
semantic-release-bot
5c0c0d6c37 chore(release): 14.0.0-dev.3 [skip ci]
# [14.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.2...v14.0.0-dev.3) (2023-08-20)

### Bug Fixes

* supply the parent classloader to `DexClassLoader` ([0f15077](0f15077225))

### Features

* do not log instantiation of ReVanced Patcher ([273dd8d](273dd8d388))
2023-08-20 17:16:00 +00:00
oSumAtrIX
0f15077225 fix: supply the parent classloader to DexClassLoader 2023-08-20 19:14:10 +02:00
oSumAtrIX
273dd8d388 feat: do not log instantiation of ReVanced Patcher 2023-08-20 19:14:09 +02:00
semantic-release-bot
1795f376ef chore(release): 14.0.0-dev.2 [skip ci]
# [14.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v14.0.0-dev.1...v14.0.0-dev.2) (2023-08-19)
2023-08-19 15:26:18 +00:00
oSumAtrIX
e7360a7692 build(Needs bump): Bump dependencies
This fixes an issue with a library not working on Android
2023-08-19 17:23:31 +02:00
semantic-release-bot
e1fc86934f chore(release): 14.0.0-dev.1 [skip ci]
# [14.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0...v14.0.0-dev.1) (2023-08-18)

### Bug Fixes

* log decoding resources after logging deleting resource cache directory ([db62a16](db62a1607b))

### Code Refactoring

* improve structure and public API ([6b8977f](6b8977f178))

### BREAKING CHANGES

* Various public APIs have been changed. The `Version` annotation has been removed. Patches do not return anything anymore and instead throw `PatchException`. Multiple patch bundles can now be loaded in a single ClassLoader to bypass class loader isolation.
2023-08-18 23:47:18 +00:00
oSumAtrIX
6b8977f178 refactor: improve structure and public API
This commit introduces a couple changes besides the refactor. Executing patches can be cancelled, multiple bundles loaded into the same class loader and `Patch.execute` does not have to return anymore.

BREAKING CHANGE: Various public APIs have been changed. The `Version` annotation has been removed. Patches do not return anything anymore and instead throw `PatchException`. Multiple patch bundles can now be loaded in a single ClassLoader to bypass class loader isolation.
2023-08-19 01:45:27 +02:00
oSumAtrIX
12c6c73de0 build: add mavenLocal to repositories 2023-08-16 16:53:47 +02:00
oSumAtrIX
db62a1607b fix: log decoding resources after logging deleting resource cache directory 2023-08-16 16:53:45 +02:00
semantic-release-bot
58bb879ef5 chore(release): 13.0.0 [skip ci]
# [13.0.0](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1...v13.0.0) (2023-08-14)

### Bug Fixes

* decode in correct order ([8fb2f2d](8fb2f2dc1d))
* disable correct loggers ([c2d89c6](c2d89c622e))
* get framework ids to compile resources ([f2cb7ee](f2cb7ee7df))
* only enable logging for ReVanced ([783ccf8](783ccf8529))
* set package metadata correctly ([02d6ff1](02d6ff15fe))

* build(Needs bump)!: Bump dependencies ([d5f89a9](d5f89a903f))

### BREAKING CHANGES

* This bump updates smali, a crucial dependency
2023-08-14 02:11:55 +00:00
oSumAtrIX
254912438a chore: merge branch dev to main (#213) 2023-08-14 04:10:17 +02:00
semantic-release-bot
0e48918bcc chore(release): 13.0.0-dev.3 [skip ci]
# [13.0.0-dev.3](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0-dev.2...v13.0.0-dev.3) (2023-08-14)

### Bug Fixes

* decode in correct order ([8fb2f2d](8fb2f2dc1d))
* only enable logging for ReVanced ([783ccf8](783ccf8529))
2023-08-14 02:04:54 +00:00
oSumAtrIX
783ccf8529 fix: only enable logging for ReVanced 2023-08-14 04:02:39 +02:00
oSumAtrIX
8fb2f2dc1d fix: decode in correct order 2023-08-14 04:02:24 +02:00
semantic-release-bot
2a8cc283c7 chore(release): 13.0.0-dev.2 [skip ci]
# [13.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v13.0.0-dev.1...v13.0.0-dev.2) (2023-08-12)

### Bug Fixes

* disable correct loggers ([c2d89c6](c2d89c622e))
* get framework ids to compile resources ([f2cb7ee](f2cb7ee7df))
* set package metadata correctly ([02d6ff1](02d6ff15fe))
2023-08-12 00:25:18 +00:00
oSumAtrIX
433fe3af9f build(Needs bump): Bump dependencies 2023-08-12 02:23:03 +02:00
oSumAtrIX
c2d89c622e fix: disable correct loggers 2023-08-12 02:22:52 +02:00
oSumAtrIX
02d6ff15fe fix: set package metadata correctly 2023-08-12 02:19:28 +02:00
oSumAtrIX
f2cb7ee7df fix: get framework ids to compile resources 2023-08-12 02:18:43 +02:00
oSumAtrIX
a2ac44dcc1 chore: use more generic inline docs 2023-08-12 02:15:26 +02:00
semantic-release-bot
3cf9d74efa chore(release): 13.0.0-dev.1 [skip ci]
# [13.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1...v13.0.0-dev.1) (2023-08-11)

* build(Needs bump)!: Bump dependencies ([d5f89a9](d5f89a903f))

### BREAKING CHANGES

* This bump updates smali, a crucial dependency
2023-08-11 00:53:46 +00:00
oSumAtrIX
d5f89a903f build(Needs bump)!: Bump dependencies
BREAKING CHANGE: This bump updates smali, a crucial dependency
2023-08-11 02:51:37 +02:00
semantic-release-bot
496c2242bc chore(release): 12.1.1 [skip ci]
## [12.1.1](https://github.com/ReVanced/revanced-patcher/compare/v12.1.0...v12.1.1) (2023-08-03)

### Bug Fixes

* clear method lookup maps before initializing them ([#210](https://github.com/ReVanced/revanced-patcher/issues/210)) ([746544f](746544f9d5))
2023-08-03 18:34:22 +00:00
oSumAtrIX
98fbff87df chore: merge branch dev to main (#211) 2023-08-03 20:32:45 +02:00
semantic-release-bot
ddb51a1c45 chore(release): 12.1.1-dev.2 [skip ci]
## [12.1.1-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v12.1.1-dev.1...v12.1.1-dev.2) (2023-08-03)
2023-08-03 18:16:10 +00:00
oSumAtrIX
8df1155215 build(Needs bump): Bump compatibility 2023-08-03 20:14:01 +02:00
semantic-release-bot
53f2a61409 chore(release): 12.1.1-dev.1 [skip ci]
## [12.1.1-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v12.1.0...v12.1.1-dev.1) (2023-08-03)

### Bug Fixes

* clear method lookup maps before initializing them ([#210](https://github.com/ReVanced/revanced-patcher/issues/210)) ([746544f](746544f9d5))
2023-08-03 11:43:51 +00:00
aAbed
746544f9d5 fix: clear method lookup maps before initializing them (#210)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-08-03 13:42:00 +02:00
semantic-release-bot
c65c3df11c chore(release): 12.1.0 [skip ci]
# [12.1.0](https://github.com/ReVanced/revanced-patcher/compare/v12.0.0...v12.1.0) (2023-08-03)

### Features

* add `MutableMethod.getInstructions` extension function ([fae4029](fae4029cfc))
2023-08-03 02:43:21 +00:00
oSumAtrIX
b29b8f12b3 chore: merge branch dev to main (#209) 2023-08-03 04:18:23 +02:00
semantic-release-bot
d6945677c4 chore(release): 12.1.0-dev.2 [skip ci]
# [12.1.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v12.1.0-dev.1...v12.1.0-dev.2) (2023-08-03)
2023-08-03 02:16:55 +00:00
oSumAtrIX
aedf4aea08 build(Needs bump): Update dependencies 2023-08-03 04:15:09 +02:00
semantic-release-bot
dc28d414dc chore(release): 12.1.0-dev.1 [skip ci]
# [12.1.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v12.0.0...v12.1.0-dev.1) (2023-08-01)

### Features

* add `MutableMethod.getInstructions` extension function ([fae4029](fae4029cfc))
2023-08-01 22:14:09 +00:00
oSumAtrIX
9755bab298 refactor: remove unnecessary annotation 2023-08-02 00:12:24 +02:00
oSumAtrIX
fae4029cfc feat: add MutableMethod.getInstructions extension function 2023-08-02 00:11:56 +02:00
oSumAtrIX
1790f0d706 ci: Change bumping commit scope 2023-07-30 02:50:41 +02:00
semantic-release-bot
0ba2c51676 chore(release): 12.0.0 [skip ci]
# [12.0.0](https://github.com/ReVanced/revanced-patcher/compare/v11.0.4...v12.0.0) (2023-07-30)

### Bug Fixes

* correct access flags of `PackageMetadata` ([416d691](416d69142f))
* set resource table via resource decoder ([e0f8e1b](e0f8e1b71a))

### Features

* Deprecate `Version` annotation ([c9bbcf2](c9bbcf2bf2))
* remove `Path` option ([#202](https://github.com/ReVanced/revanced-patcher/issues/202)) ([69e4a49](69e4a49065))

### BREAKING CHANGES

* This removes the previously available `Path` option
2023-07-30 00:10:57 +00:00
oSumAtrIX
03cd97b49c chore: merge branch dev to main (#203) 2023-07-30 02:09:12 +02:00
semantic-release-bot
16a162c1dd chore(release): 12.0.0-dev.2 [skip ci]
# [12.0.0-dev.2](https://github.com/ReVanced/revanced-patcher/compare/v12.0.0-dev.1...v12.0.0-dev.2) (2023-07-28)

### Features

* Deprecate `Version` annotation ([400442f](400442f70e))
2023-07-28 20:19:02 +02:00
oSumAtrIX
c9bbcf2bf2 feat: Deprecate Version annotation 2023-07-28 20:19:01 +02:00
semantic-release-bot
86e1bf6078 chore(release): 12.0.0-dev.1 [skip ci]
# [12.0.0-dev.1](https://github.com/ReVanced/revanced-patcher/compare/v11.0.4...v12.0.0-dev.1) (2023-07-26)

### Bug Fixes

* correct access flags of `PackageMetadata` ([416d691](416d69142f))
* set resource table via resource decoder ([e0f8e1b](e0f8e1b71a))

### Features

* remove `Path` option ([#202](https://github.com/ReVanced/revanced-patcher/issues/202)) ([69e4a49](69e4a49065))

### BREAKING CHANGES

* This removes the previously available `Path` option
2023-07-26 04:30:48 +00:00
oSumAtrIX
1bca84ef0b refactor: move code out of try block 2023-07-26 06:28:48 +02:00
oSumAtrIX
e0f8e1b71a fix: set resource table via resource decoder 2023-07-26 06:28:48 +02:00
oSumAtrIX
416d69142f fix: correct access flags of PackageMetadata 2023-07-26 06:28:48 +02:00
oSumAtrIX
426807aeaa refactor: remove unnecessary changes to default config 2023-07-26 06:28:47 +02:00
oSumAtrIX
90cb075a97 build(needs-bump): update dependencies 2023-07-26 06:28:47 +02:00
oSumAtrIX
ac2ca8fbd3 ci: bump on scope needs-bump 2023-07-26 06:28:47 +02:00
Palm
69e4a49065 feat: remove Path option (#202)
BREAKING CHANGE: This removes the previously available `Path` option
2023-07-26 04:11:21 +02:00
oSumAtrIX
a4a030f2b2 build: update gradle 2023-07-06 18:15:40 +02:00
semantic-release-bot
dcc4ecd237 chore(release): 11.0.4 [skip ci]
## [11.0.4](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4) (2023-07-01)

### Bug Fixes

* clear method lookup maps ([#198](https://github.com/revanced/revanced-patcher/issues/198)) ([9d81baf](9d81baf4b4))
2023-07-01 00:42:50 +00:00
oSumAtrIX
57f3036a96 chore: merge branch dev to main (#199) 2023-07-01 02:41:21 +02:00
semantic-release-bot
753e55dfc3 chore(release): 11.0.4-dev.1 [skip ci]
## [11.0.4-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.3...v11.0.4-dev.1) (2023-07-01)

### Bug Fixes

* clear method lookup maps ([#198](https://github.com/revanced/revanced-patcher/issues/198)) ([9d81baf](9d81baf4b4))
2023-07-01 00:37:30 +00:00
LisoUseInAIKyrios
9d81baf4b4 fix: clear method lookup maps (#198) 2023-07-01 02:35:49 +02:00
semantic-release-bot
7cb4d4c596 chore(release): 11.0.3 [skip ci]
## [11.0.3](https://github.com/revanced/revanced-patcher/compare/v11.0.2...v11.0.3) (2023-06-30)

### Bug Fixes

* NPE on method lookup ([#195](https://github.com/revanced/revanced-patcher/issues/195)) ([fcef434](fcef4342e8))
2023-06-30 23:56:27 +00:00
oSumAtrIX
2c8565508e chore: merge branch dev to main (#196) 2023-07-01 01:54:46 +02:00
semantic-release-bot
c7f156e4c9 chore(release): 11.0.3-dev.1 [skip ci]
## [11.0.3-dev.1](https://github.com/revanced/revanced-patcher/compare/v11.0.2...v11.0.3-dev.1) (2023-06-28)

### Bug Fixes

* NPE on method lookup ([#195](https://github.com/revanced/revanced-patcher/issues/195)) ([fcef434](fcef4342e8))
2023-06-28 19:57:40 +00:00
LisoUseInAIKyrios
fcef4342e8 fix: NPE on method lookup (#195) 2023-06-28 21:56:03 +02:00
semantic-release-bot
72783a5e74 chore(release): 11.0.2 [skip ci]
## [11.0.2](https://github.com/revanced/revanced-patcher/compare/v11.0.1...v11.0.2) (2023-06-27)

### Bug Fixes

* catch exceptions from closing patches ([d5d6f85](d5d6f85084))
* do not load annotations as patches ([519359a](519359a9eb))
* only close succeeded patches ([b8151eb](b8151ebccb))
* use `versionCode` if `versionName` is unavailable ([6e1b647](6e1b6479b6))

### Performance Improvements

* resolve fingerprints using method maps ([#185](https://github.com/revanced/revanced-patcher/issues/185)) ([d718134](d718134ab2))
2023-06-27 02:32:47 +00:00
oSumAtrIX
a379b69eeb chore: merge branch dev to main (#191) 2023-06-27 04:31:09 +02:00
semantic-release-bot
0a8ccba33e chore(release): 11.0.2-dev.4 [skip ci]
## [11.0.2-dev.4](https://github.com/revanced/revanced-patcher/compare/v11.0.2-dev.3...v11.0.2-dev.4) (2023-06-27)

### Bug Fixes

* do not load annotations as patches ([519359a](519359a9eb))
2023-06-27 02:11:33 +00:00
oSumAtrIX
519359a9eb fix: do not load annotations as patches 2023-06-27 04:09:03 +02:00
157 changed files with 17792 additions and 7546 deletions

9
.gitattributes vendored Normal file
View File

@@ -0,0 +1,9 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf

View File

@@ -1,72 +0,0 @@
name: 🐞 Bug report
description: Report a very clearly broken issue.
title: 'bug: <title>'
labels: [bug]
body:
- type: markdown
attributes:
value: |
# ReVanced bug report
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-patcher/labels/bug).
- type: dropdown
attributes:
label: Type
options:
- Crash
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: Add the steps to reproduce this bug including your environment.
placeholder: Step 1. Download some files. Step 2. ...
validations:
required: true
- type: textarea
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: true
- type: textarea
attributes:
label: Screenshots or videos
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution.
validations:
required: false
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
required: true

109
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: 🐞 Bug report
description: Report a bug or an issue.
title: "bug: "
labels: ["Bug report"]
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# ReVanced Patcher bug report
Before creating a new bug report, please keep the following in mind:
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patcher/issues?q=label%3A%22Bug+report%22).
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patcher/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
validations:
required: true
- type: textarea
attributes:
label: Error logs
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
render: shell
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution to the bug.
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed bug reports and this is not a duplicate.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

View File

@@ -1,58 +0,0 @@
name: ⭐ Feature request
description: Create a detailed feature request.
title: 'feat: <title>'
labels: [feature-request]
body:
- type: markdown
attributes:
value: |
# ReVanced feature request
Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead.
Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-patcher/labels/feature-request).
- type: dropdown
attributes:
label: Type
options:
- Functionality
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Issue
description: What is the current problem. Why does it require a feature request?
validations:
required: true
- type: textarea
attributes:
label: Feature
description: Describe your feature in detail. How does it solve the issue?
validations:
required: true
- type: textarea
attributes:
label: Motivation
description: Why should your feature should be considered?
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
validations:
required: false
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I filled out all of the requested information in this issue properly.
required: true

View File

@@ -0,0 +1,107 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: "feat: "
labels: ["Feature request"]
body:
- type: markdown
attributes:
value: |
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# ReVanced Patcher feature request
Before creating a new feature request, please keep the following in mind:
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patcher/issues?q=label%3A%22Feature+request%22).
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patcher/blob/main/CONTRIBUTING.md).
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
- type: textarea
attributes:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- Add the target application name in case you request a new patch
- type: textarea
attributes:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed feature requests and this is not a duplicate.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

2
.github/config.yml vendored
View File

@@ -1,2 +1,2 @@
firstPRMergeComment: >
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.

22
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
version: 2
updates:
- package-ecosystem: github-actions
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: npm
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly
- package-ecosystem: gradle
labels: []
directory: /
target-branch: dev
schedule:
interval: monthly

View File

@@ -0,0 +1,25 @@
name: Build pull request
on:
workflow_dispatch:
pull_request:
branches:
- dev
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build --no-daemon

View File

@@ -1,4 +1,4 @@
name: PR to main
name: Open a PR to main
on:
push:
@@ -7,7 +7,7 @@ on:
workflow_dispatch:
env:
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
@@ -15,7 +15,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Open pull request
uses: repo-sync/pull-request@v2
with:

View File

@@ -6,40 +6,45 @@ on:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
release:
name: Release
permissions:
contents: write
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Cache
uses: actions/cache@v3
with:
path: |
${{ runner.home }}/.gradle/caches
${{ runner.home }}/.gradle/wrapper
.gradle
build
node_modules
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
- name: Build with Gradle
uses: actions/checkout@v5
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v3
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew clean --no-daemon
- name: Setup semantic-release
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build clean
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "lts/*"
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
uses: cycjimmy/semantic-release-action@v4
env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,19 @@
name: Update documentation
on:
push:
paths:
- docs/**
jobs:
trigger:
runs-on: ubuntu-latest
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation
event-type: update-documentation
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'

133
.gitignore vendored
View File

@@ -1,124 +1,19 @@
### Java template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
.idea/artifacts
.idea/compiler.xml
.idea/jarRepositories.xml
.idea/modules.xml
.idea/*.iml
.idea/modules
*.iml
*.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
.idea/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Gradle template
.kotlin
.gradle
**/build/
xcuserdata
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
# Avoid ignoring test resources
!src/test/resources/*
# Dependency directories
local.properties
.idea
.DS_Store
captures
.externalNativeBuild
.cxx
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcodeproj/project.xcworkspace/
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
# Gradle props, to avoid sharing the gpr key
gradle.properties

View File

@@ -7,7 +7,13 @@
}
],
"plugins": [
"@semantic-release/commit-analyzer",
[
"@semantic-release/commit-analyzer", {
"releaseRules": [
{ "type": "build", "scope": "Needs bump", "release": "patch" }
]
}
],
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"gradle-semantic-release-plugin",
@@ -17,7 +23,8 @@
"assets": [
"CHANGELOG.md",
"gradle.properties"
]
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
[

File diff suppressed because it is too large Load Diff

99
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,99 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 👋 Contribution guidelines
This document describes how to contribute to ReVanced Patcher.
## 📖 Resources to help you get started
- The [documentation](https://github.com/ReVanced/revanced-patcher/tree/docs/docs) contains the fundamentals
of ReVanced Patcher and how to use ReVanced Patcher to create patches
- [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
- [Issues](https://github.com/ReVanced/revanced-patcher/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-patcher/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
> **Note**
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patcher.
> Good motivation has to be provided for a request to be accepted.
## 🐞 Submitting a bug report
If you encounter a bug while using ReVanced Patcher, open an issue using the
[Bug report issue template](https://github.com/ReVanced/revanced-patcher/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
## 📝 How to contribute
1. Before contributing, it is recommended to open an issue to discuss your change
with the maintainers of ReVanced Patcher. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes
4. Submit a pull request to the `dev` branch of the repository and reference issues
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of ReVanced Patcher
❤️ Thank you for considering contributing to ReVanced Patcher,
ReVanced

122
README.md
View File

@@ -1,3 +1,125 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 💉 ReVanced Patcher
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-patcher/release.yml)
![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)
ReVanced Patcher used to patch Android applications.
## ❓ About
ReVanced Patcher is a library that is used to patch Android applications.
It powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager),
[ReVanced CLI](https://github.com/ReVanced/revanced-cli)
and [ReVanced Library](https://github.com/ReVanced/revanced-library) and a rich set of patches have been developed
using ReVanced Patcher in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository.
## 💪 Features
Some of the features the ReVanced Patcher provides are:
- 🔧 **Patch Dalvik VM bytecode**: Disassemble and assemble Dalvik bytecode
- 📦 **Patch APK resources**: Decode and build Android APK resources
- 📂 **Patch arbitrary APK files**: Read and write arbitrary files directly from and to APK files
- 🧩 **Write modular patches**: Extensive API to write modular patches that can patch Dalvik VM bytecode,
APK resources and arbitrary APK files
## 🚀 How to get started
To use ReVanced Patcher in your project, follow these steps:
1. [Add the repository](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
to your project
2. Add the dependency to your project:
```kt
dependencies {
implementation("app.revanced:revanced-patcher:{$version}")
}
```
For a minimal project configuration,
see [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template).
## 📚 Everything else
### 📙 Contributing
Thank you for considering contributing to ReVanced Patcher.
You can find the contribution guidelines [here](CONTRIBUTING.md).
### 🛠️ Building
To build ReVanced Patcher,
you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
### 📃 Documentation
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).
## 📜 Licence
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.
Any modifications to ReVanced Patcher must also be made available under the GPL,
along with build & install instructions.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,71 +1,5 @@
plugins {
kotlin("jvm") version "1.8.10"
`maven-publish`
}
group = "app.revanced"
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
repositories {
mavenCentral()
maven {
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
credentials {
username = githubUsername
password = githubPassword
}
}
}
dependencies {
implementation("xpp3:xpp3:1.1.4c")
implementation("app.revanced:smali:2.5.3-a3836654")
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
implementation("app.revanced:apktool-lib:2.7.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
}
tasks {
test {
useJUnitPlatform()
testLogging {
events("PASSED", "SKIPPED", "FAILED")
}
}
processResources {
expand("projectVersion" to project.version)
}
}
java {
withSourcesJar()
}
kotlin {
jvmToolchain(11)
}
publishing {
repositories {
if (System.getenv("GITHUB_ACTOR") != null)
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
else
mavenLocal()
}
publications {
register<MavenPublication>("gpr") {
from(components["java"])
}
}
}
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.vanniktech.mavenPublish) apply false
}

111
docs/1_patcher_intro.md Normal file
View File

@@ -0,0 +1,111 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 💉 Introduction to ReVanced Patcher
To create patches for Android apps, it is recommended to know the basic concept of ReVanced Patcher.
## 📙 How it works
ReVanced Patcher is a library that allows modifying Android apps by applying patches.
It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool)
for resource decoding and encoding.
ReVanced Patcher receives a list of patches and applies them to a given APK file.
It then returns the modified components of the APK file, such as modified dex files and resources,
that can be repackaged into a new APK file.
ReVanced Patcher has a simple API that allows you to load patches from RVP (JAR or DEX container) files
and apply them to an APK file. Later on, you will learn how to create patches.
```kt
val patches = loadPatchesFromJar(setOf(File("revanced-patches.rvp")))
val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { patcher ->
// Here you can access metadata about the APK file through patcher.context.packageMetadata
// such as package name, version code, version name, etc.
// Add patches.
patcher += patches
// Execute the patches.
runBlocking {
patcher().collect { patchResult ->
if (patchResult.exception != null)
logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" }
else
logger.info { "\"${patchResult.patch}\" succeeded" }
}
}
// Compile and save the patched APK file components.
patcher.get()
}
// The result of the patcher contains the modified components of the APK file that can be repackaged into a new APK file.
val dexFiles = patcherResult.dexFiles
val resources = patcherResult.resources
```
## ⏭️ What's next
The next page teaches the fundamentals of ReVanced Patches.
Continue: [🧩 Introduction to ReVanced Patches](2_patches_intro.md)

112
docs/2_1_setup.md Normal file
View File

@@ -0,0 +1,112 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 👶 Setting up a development environment
To start developing patches with ReVanced Patcher, you must prepare a development environment.
## 📝 Prerequisites
- A Java IDE with Kotlin support, such as [IntelliJ IDEA](https://www.jetbrains.com/idea/)
- Knowledge of Java, [Kotlin](https://kotlinlang.org), and [Dalvik bytecode](https://source.android.com/docs/core/runtime/dalvik-bytecode)
- Android reverse engineering skills and tools such as [jadx](https://github.com/skylot/jadx)
## 🏃 Prepare the environment
Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.
> [!NOTE]
> To start a fresh project,
> you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template).
1. Clone the repository
```bash
git clone https://github.com/revanced/revanced-patches && cd revanced-patches
```
2. Build the project
```bash
./gradlew build
```
> [!NOTE]
> If the build fails due to authentication, you may need to authenticate to GitHub Packages.
> Create a PAT with the scope `read:packages` [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced) and add your token to ~/.gradle/gradle.properties.
>
> Example `gradle.properties` file:
>
> ```properties
> gpr.user = user
> gpr.key = key
> ```
3. Open the project in your IDE
> [!TIP]
> It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches
> by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
## ⏭️ What's next
The next page will go into details about a ReVanced patch.
Continue: [🧩 Anatomy of a patch](2_2_patch_anatomy.md)

View File

@@ -0,0 +1,296 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 🔎 Fingerprinting
In the context of ReVanced, a fingerprint is a partial description of a method.
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.
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
access flags, an opcode pattern, strings, and more.
## ⛳️ Example fingerprint
An example fingerprint is shown below:
```kt
package app.revanced.patches.ads.fingerprints
fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Z")
parameters("Z")
opcodes(Opcode.RETURN)
strings("pro")
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
}
```
## 🔎 Reconstructing the original code from the example fingerprint from above
The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
The fingerprint contains the following information:
- Method signature:
```kt
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("Z")
parameters("Z")
```
- Method implementation:
```kt
opcodes(Opcode.RETURN)
strings("pro")
```
- Package and class name:
```kt
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
```
With this information, the original code can be reconstructed:
```java
package com.some.app.ads;
<accessFlags>
class AdsLoader {
public final boolean <methodName>(boolean <parameter>)
{
// ...
var userStatus = "pro";
// ...
return <returnValue >;
}
}
```
Using that fingerprint, this method can be matched uniquely from all other methods.
> [!TIP]
> A fingerprint should contain information about a method likely to remain the same across updates.
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated
> app.
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
> same.
## 🔨 How to use fingerprints
After declaring a fingerprint, it can be used in a patch to find the method it matches to:
```kt
val fingerprint = fingerprint {
// ...
}
val patch = bytecodePatch {
execute {
fingerprint.method
}
}
```
The fingerprint won't be matched again, if it has already been matched once, for performance reasons.
This makes it useful, to share fingerprints between multiple patches,
and let the first executing patch match the fingerprint:
```kt
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
val mainActivityPatch1 = bytecodePatch {
execute {
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]
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
> function to fuzzy match the pattern.
> `null` can be used as a wildcard to match any opcode:
>
> ```kt
> fingerprint(fuzzyPatternScanThreshhold = 2) {
> opcodes(
> Opcode.ICONST_0,
> null,
> Opcode.ICONST_1,
> Opcode.IRETURN,
> )
>}
> ```
The following properties can be accessed in a fingerprint:
- `originalClassDef`: The original class definition the fingerprint matches to.
- `originalClassDefOrNull`: The original class definition the fingerprint matches to.
- `originalMethod`: The original method the fingerprint matches to.
- `originalMethodOrNull`: The original method the fingerprint matches to.
- `classDef`: The class the fingerprint matches to.
- `classDefOrNull`: The class the fingerprint matches to.
- `method`: The method the fingerprint matches to. If no match is found, an exception is raised.
- `methodOrNull`: The method the fingerprint matches to.
The difference between the `original` and non-`original` properties is that the `original` properties return the
original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
The mutable copies can be modified. They are lazy properties, so they are only computed
and only then will effectively replace the `original` method or class definition when accessed.
> [!TIP]
> If only read-only access to the class or method is needed,
> the `originalClassDef` and `originalMethod` properties should be used,
> to avoid making a mutable copy of the class or method.
## 🏹 Manually matching fingerprints
By default, a fingerprint is matched automatically against all classes
when one of the fingerprint's properties is accessed.
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function:
- In a **list of classes**, if the fingerprint can match in a known subset of classes
If you have a known list of classes you know the fingerprint can match in,
you can match the fingerprint on the list of classes:
```kt
execute {
val match = showAdsFingerprint(classes)
}
```
- In a **single class**, if the fingerprint can match in a single known class
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
```kt
execute {
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }
val match = showAdsFingerprint.match(adsLoaderClass)
}
```
Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.
```kt
execute {
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
}
```
- Match a **single method**, to extract certain information about it
The match of a fingerprint contains useful information about the method,
such as the start and end index of an opcode pattern or the indices of the instructions with certain string
references.
A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
```kt
execute {
val currentPlanFingerprint = fingerprint {
strings("free", "trial")
}
currentPlanFingerprint.match(adsFingerprint.method).let { match ->
match.stringMatches.forEach { match ->
println("The index of the string '${match.string}' is ${match.index}")
}
}
}
```
> [!WARNING]
> If the fingerprint can not be matched to any method, calling `match` will raise an
> exception.
> Instead, the `orNull` overloads can be used to return `null` if no match is found.
> [!TIP]
> To see real-world examples of fingerprints,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
## ⏭️ What's next
The next page discusses the structure and conventions of patches.
Continue: [📜 Project structure and conventions](3_structure_and_conventions.md)

262
docs/2_2_patch_anatomy.md Normal file
View File

@@ -0,0 +1,262 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 🧩 Anatomy of a ReVanced patch
Learn the API to create patches using ReVanced Patcher.
## ⛳️ Example patch
The following example patch disables ads in an app.
In the following sections, each part of the patch will be explained in detail.
```kt
package app.revanced.patches.ads
val disableAdsPatch = bytecodePatch(
name = "Disable ads",
description = "Disable ads in the app.",
) {
compatibleWith("com.some.app"("1.0.0"))
// Patches can depend on other patches, executing them first.
dependsOn(disableAdsResourcePatch)
// Merge precompiled DEX files into the patched app, before the patch is executed.
extendWith("disable-ads.rve")
// Business logic of the patch to disable ads in the app.
execute {
// Fingerprint to find the method to patch.
val showAdsFingerprint = fingerprint {
// More about fingerprints on the next page of the documentation.
}
// In the method that shows ads,
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
// to enable or disable ads.
showAdsFingerprint.method.addInstructions(
0,
"""
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
move-result v0
return v0
"""
)
}
}
```
> [!TIP]
> To see real-world examples of patches,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
## 🧩 Patch API
### ⚙️ Patch options
Patches can have options to get and set before a patch is executed.
Options are useful for making patches configurable.
After loading the patches using `PatchLoader`, options can be set for a patch.
Multiple types are already built into ReVanced Patcher and are supported by any application that uses ReVanced Patcher.
To define an option, use the available `option` functions:
```kt
val patch = bytecodePatch(name = "Patch") {
// Add an inbuilt option and delegate it to a property.
val value by stringOption(name = "Inbuilt option")
// Add an option with a custom type and delegate it to a property.
val string by option<String>(name = "String option")
execute {
println(value)
println(string)
}
}
```
Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch:
```kt
loadPatchesJar(patches).apply {
// Type is checked at runtime.
first { it.name == "Patch" }.options["Option"] = "Value"
}
```
The type of an option can be obtained from the `type` property of the option:
```kt
option.type // The KType of the option. Captures the full type information of the option.
```
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
An extension is a precompiled DEX file merged into the patched app before a patch is executed.
While patches are compile-time constructs, extensions are runtime constructs
that extend the patched app with additional classes.
Assume you want to add a complex feature to an app that would need multiple classes and methods:
```java
public class ComplexPatch {
public static void doSomething() {
// ...
}
}
```
After compiling the above code as a DEX file, you can add the DEX file as a resource in the patches file
and use it in a patch:
```kt
val patch = bytecodePatch(name = "Complex patch") {
extendWith("complex-patch.rve")
execute {
fingerprint.method.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
}
}
```
ReVanced Patcher merges the classes from the extension into `context.classes` before executing the patch.
When the patch is executed, it can reference the classes and methods from the extension.
> [!NOTE]
>
> The [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template) repository
> is a template project to create patches and extensions.
> [!TIP]
> To see real-world examples of extensions,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
### ♻️ Finalization
Patches can have a finalization block called after all patches have been executed, in reverse order of patch execution.
The finalization block is called after all patches that depend on the patch have been executed.
This is useful for doing post-processing tasks.
A simple real-world example would be a patch that opens a resource file of the app for writing.
Other patches that depend on this patch can write to the file, and the finalization block can close the file.
```kt
val patch = bytecodePatch(name = "Patch") {
dependsOn(
bytecodePatch(name = "Dependency") {
execute {
print("1")
}
finalize {
print("4")
}
}
)
execute {
print("2")
}
finalize {
print("3")
}
}
```
Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`.
Finalization blocks are called in reverse order of patch execution, which means,
first, the finalization block of `Patch`, then the finalization block of `Dependency` is called.
The output after executing the patch above would be `1234`.
The same order is followed for multiple patches depending on the patch.
## 💡 Additional tips
- When using `PatchLoader` to load patches, only patches with a name are loaded.
Refer to the inline documentation of `PatchLoader` for detailed information.
- Patches can depend on others. Dependencies are executed first.
The dependent patch will not be executed if a dependency raises an exception while executing.
- A patch can declare compatibility with specific packages and versions,
but patches can still be executed on any package or version.
It is recommended that compatibility is specified to present known compatible packages and versions.
- If `compatibleWith` is not used, the patch is treated as compatible with any package
- If a package is specified with no versions, the patch is compatible with any version of the package
- If an empty array of versions is specified, the patch is not compatible with any version of the package.
This is useful for declaring incompatibility with a specific package.
- A patch can raise a `PatchException` at any time of execution to indicate that the patch failed to execute.
## ⏭️ What's next
The next page explains the concept of fingerprinting in ReVanced Patcher.
Continue: [🔎 Fingerprinting](2_2_1_fingerprinting.md)

126
docs/2_patches_intro.md Normal file
View File

@@ -0,0 +1,126 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 🧩 Introduction to ReVanced Patches
Learn the basic concepts of ReVanced Patcher and how to create patches.
## 📙 Fundamentals
A patch is a piece of code that modifies an Android application.
There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode,
the APK resources, or arbitrary files in the APK:
- A `BytecodePatch` modifies the Dalvik VM bytecode
- A `ResourcePatch` modifies (decoded) resources
- A `RawResourcePatch` modifies arbitrary files
Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies
before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way.
The `execute` function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed.
The `execute` function receives an instance of a context object that provides access to the APK.
The patch can use this context to modify the APK.
Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs
to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources.
The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources
if it is supplied a `ResourcePatch` for execution or if any patch depends on a `ResourcePatch`
and will not decode the resources before executing `RawResourcePatch`.
Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK,
whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case.
Decoding and building resources is a time- and resource-consuming,
so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`.
Example of patches:
```kt
@Surpress("unused")
val bytecodePatch = bytecodePatch {
execute {
// More about this on the next page of the documentation.
}
}
@Surpress("unused")
val rawResourcePatch = rawResourcePatch {
execute {
// More about this on the next page of the documentation.
}
}
@Surpress("unused")
val resourcePatch = resourcePatch {
execute {
// More about this on the next page of the documentation.
}
}
```
> [!TIP]
> To see real-world examples of patches,
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
## ⏭️ Whats next
The next page will guide you through creating a development environment for creating patches.
Continue: [👶 Setting up a development environment](2_1_setup.md)

View File

@@ -0,0 +1,105 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 📜 Project structure and conventions
Over time, a specific project structure and conventions have been established.
## 📁 File structure
Patches are organized in a specific way. The file structure looks as follows:
```text
📦your.patches.app.category
├ 🔍Fingerprints.kt
└ 🧩SomePatch.kt
```
> [!NOTE]
> Moving fingerprints to a separate file isn't strictly necessary, but it helps the organization when a patch uses multiple fingerprints.
## 📙 Conventions
- 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `Remove ads`.
If a patch changes the color of a button, name it `Change button color`
- 🔥 Write the patch description in the third person, present tense, and end it with a period.
If a patch removes ads, the description can be omitted because of redundancy,
but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
- 🔥 Write patches with modularity and reusability in mind. Patches can depend on each other,
so it is important to write patches in a way that can be used in different contexts.
- 🔥🔥 Keep patches as minimal as possible. This reduces the risk of failing patches.
Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch,
you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app
before this patch is executed.
Patches can then reference methods and classes from extensions.
A real-world example of extensions can be found in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository
- 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change.
In the example of an obfuscated method, it's better to fingerprint the method by its return type
and parameters rather than its name because the name is likely to change. An intelligent selection
of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates.
- 🔥🔥🔥 Document your patches. Patches are abstract, so it is important to document parts of the code
that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks
of instructions that are modified or added to a method
## ⏭️ What's next
The next page discusses useful APIs for patch development.
Continue: [💪 Advanced APIs](4_apis.md)

117
docs/4_apis.md Normal file
View File

@@ -0,0 +1,117 @@
# 💪 Advanced APIs
A handful of APIs are available to make patch development easier and more efficient.
## 📙 Overview
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
3. 🏃‍ Navigate method calls recursively by index with `navigate(Method)`
4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)`
5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)`
### 🧰 APIs
#### 👹 `proxy(ClassDef)`
By default, the classes are immutable, meaning they cannot be modified.
To make a class mutable, use the `proxy(ClassDef)` function.
This function creates a lazy mutable copy of the class definition.
Accessing the property will replace the original class definition with the mutable copy,
thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
```kt
execute {
val mutableClass = proxy(classDef)
mutableClass.methods.add(Method())
}
```
#### 🔍 `classBy(Predicate)`
The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate.
It automatically proxies the class definition, making it mutable.
```kt
execute {
// Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef
val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef
}
```
#### 🏃‍ `navigate(Method).at(index)`
The `navigate(Method)` function allows you to navigate method calls recursively by index.
```kt
execute {
// Sequentially navigate to the instructions at index 1 within 'someMethod'.
val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
// Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
// stop() returns the mutable copy of the method.
val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
// Alternatively, to stop(), you can delegate the method to a variable.
val method by navigate(someMethod).to(1)
// You can chain multiple calls to at() to navigate deeper into the method.
val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
}
```
#### 💾 `get(String, Boolean)` and `delete(String)`
The `get(String, Boolean)` function returns a `File` object that can be used to read and write resource files.
```kt
execute {
val file = get("res/values/strings.xml")
val content = file.readText()
file.writeText(content)
}
```
The `delete` function can mark files for deletion when the APK is rebuilt.
```kt
execute {
delete("res/values/strings.xml")
}
```
#### 📃 `document(String)` and `document(InputStream)`
The `document` function is used to read and write DOM files.
```kt
execute {
document("res/values/strings.xml").use { document ->
val element = doc.createElement("string").apply {
textContent = "Hello, World!"
}
document.documentElement.appendChild(element)
}
}
```
You can also read documents from an `InputStream`:
```kt
execute {
val inputStream = classLoader.getResourceAsStream("some.xml")
document(inputStream).use { document ->
// ...
}
}
```
## 🎉 Afterword
ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches
that outlive app updates. Patches make up ReVanced; without you, the community of patch developers,
ReVanced would not be what it is today. We hope that this documentation has been helpful to you
and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help,
talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or
feature request,
ReVanced

73
docs/README.md Normal file
View File

@@ -0,0 +1,73 @@
<p align="center">
<picture>
<source
width="256px"
media="(prefers-color-scheme: dark)"
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
>
<img
width="256px"
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
>
</picture>
<br>
<a href="https://revanced.app/">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://github.com/ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="http://revanced.app/discord">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://reddit.com/r/revancedapp">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://t.me/app_revanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://x.com/revancedapp">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;
<a href="https://www.youtube.com/@ReVanced">
<picture>
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
</picture>
</a>
<br>
<br>
Continuing the legacy of Vanced
</p>
# 💉 Documentation of ReVanced Patcher
This documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches
## 📖 Table of content
1. [💉 Introduction to ReVanced Patcher](1_patcher_intro.md)
2. [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
1. [👶 Setting up a development environment](2_1_setup.md)
2. [🧩 Anatomy of a ReVanced patch](2_2_patch_anatomy.md)
1. [🔎 Fingerprinting](2_2_1_fingerprinting.md)
3. [📜 Project structure and conventions](3_structure_and_conventions.md)
4. [💪 Advanced APIs](4_apis.md)

View File

@@ -1,2 +1,11 @@
kotlin.code.style = official
version = 11.0.2-dev.3
version = 22.0.0-local
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
org.gradle.caching=true
org.gradle.configuration-cache=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true

28
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,28 @@
[versions]
agp = "8.12.3"
android-compileSdk = "36"
android-minSdk = "26"
kotlin = "2.3.0"
apktool-lib = "2.10.1.1"
mockk = "1.14.7"
multidexlib2 = "3.0.3.r3"
# Tracking https://github.com/google/smali/issues/64.
#noinspection GradleDependency
smali = "3.0.9"
xpp3 = "1.1.4c"
vanniktechMavenPublish = "0.35.0"
[libraries]
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
[plugins]
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }

Binary file not shown.

View File

@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

58
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/bin/sh
#
# Copyright <EFBFBD> 2015-2021 the original authors.
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -32,10 +34,10 @@
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions <EFBFBD>$var<EFBFBD>, <EFBFBD>${var}<EFBFBD>, <EFBFBD>${var:-default}<EFBFBD>, <EFBFBD>${var+SET}<EFBFBD>,
# <EFBFBD>${var#prefix}<EFBFBD>, <EFBFBD>${var%suffix}<EFBFBD>, and <EFBFBD>$( cmd )<EFBFBD>;
# * compound commands having a testable exit status, especially <EFBFBD>case<EFBFBD>;
# * various built-in commands including <EFBFBD>command<EFBFBD>, <EFBFBD>set<EFBFBD>, and <EFBFBD>ulimit<EFBFBD>.
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,11 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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"'
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -133,22 +132,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -165,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# 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"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

40
gradlew.bat vendored
View File

@@ -13,8 +13,10 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

7350
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"devDependencies": {
"@saithodev/semantic-release-backmerge": "^3.1.0",
"@semantic-release/changelog": "^6.0.2",
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.7.6",
"semantic-release": "^20.1.0"
"gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^24.2.9"
}
}

1108
patcher/api/android/core.api Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1108
patcher/api/jvm/core.api Normal file

File diff suppressed because it is too large Load Diff

1218
patcher/api/jvm/patcher.api Normal file

File diff suppressed because it is too large Load Diff

110
patcher/build.gradle.kts Normal file
View File

@@ -0,0 +1,110 @@
import com.android.build.api.dsl.androidLibrary
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.android.kotlin.multiplatform.library)
alias(libs.plugins.vanniktech.mavenPublish)
}
group = "app.revanced"
kotlin {
@OptIn(ExperimentalAbiValidation::class)
abiValidation {
enabled = true
}
jvm()
androidLibrary {
namespace = "app.revanced.patcher"
compileSdk = libs.versions.android.compileSdk.get().toInt()
minSdk = libs.versions.android.minSdk.get().toInt()
withHostTestBuilder {}.configure {}
withDeviceTestBuilder {
sourceSetTreeName = "test"
}
compilations.configureEach {
compilerOptions.configure {
jvmTarget.set(
JvmTarget.JVM_11
)
}
}
}
sourceSets {
commonMain.dependencies {
implementation(libs.apktool.lib)
implementation(libs.kotlin.reflect)
implementation(libs.multidexlib2)
implementation(libs.smali)
implementation(libs.xpp3)
}
jvmTest.dependencies {
implementation(libs.mockk)
implementation(libs.kotlin.test)
}
}
compilerOptions {
freeCompilerArgs.addAll(
"-Xexplicit-backing-fields",
"-Xcontext-parameters"
)
}
}
tasks {
named<Test>("jvmTest") {
useJUnitPlatform()
}
}
mavenPublishing {
publishing {
repositories {
maven {
name = "githubPackages"
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
credentials(PasswordCredentials::class)
}
}
}
signAllPublications()
extensions.getByType<SigningExtension>().useGpgCmd()
coordinates(group.toString(), project.name, version.toString())
pom {
name = "ReVanced Patcher"
description = "Patcher used by ReVanced."
inceptionYear = "2022"
url = "https://revanced.app"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
}
}
developers {
developer {
id = "ReVanced"
name = "ReVanced"
email = "contact@revanced.app"
}
}
scm {
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
url = "https://github.com/revanced/revanced-patcher"
}
}
}

View File

@@ -0,0 +1,39 @@
package app.revanced.patcher.patch
import dalvik.system.DexClassLoader
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.MultiDexIO
import java.io.File
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Loads patches from DEX files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded. If a patches file fails to load,
* the [onFailedToLoad] callback is invoked with the file and the throwable
* and the loading continues for the other files.
*
* @param patchesFiles The DEX files to load the patches from.
* @param onFailedToLoad A callback invoked when a patches file fails to load.
*
* @return The loaded patches.
*/
actual fun loadPatches(
vararg patchesFiles: File,
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit,
) = loadPatches(
patchesFiles = patchesFiles,
{ patchBundle ->
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
.map { classDef ->
classDef.type.substring(1, classDef.length - 1)
}
},
DexClassLoader(
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
null,
null, null
),
onFailedToLoad
)

View File

@@ -0,0 +1,7 @@
package collections
actual fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V
) = merge(key, value, remappingFunction)

View File

@@ -0,0 +1,8 @@
package java.io
import java.nio.charset.Charset
internal actual fun File.kmpResolve(child: String) = resolve(child)
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
internal actual fun File.kmpInputStream() = inputStream()
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)

View File

@@ -0,0 +1,363 @@
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
package app.revanced.patcher
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.PatchException
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.util.MethodUtil
@Deprecated("Use the matcher API instead.")
class Fingerprint internal constructor(
internal val accessFlags: Int?,
internal val returnType: String?,
internal val parameters: List<String>?,
internal val opcodes: List<Opcode?>?,
internal val strings: List<String>?,
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
private val fuzzyPatternScanThreshold: Int,
) {
@Suppress("ktlint:standard:backing-property-naming")
// Backing field needed for lazy initialization.
private var _matchOrNull: FingerprintMatch? = null
context(_: BytecodePatchContext)
private val matchOrNull: FingerprintMatch?
get() = matchOrNull()
context(context: BytecodePatchContext)
internal fun matchOrNull(): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
var match = strings?.mapNotNull {
context.classDefs.methodsByString[it]
}?.minByOrNull { it.size }?.let { methodClasses ->
methodClasses.forEach { method ->
val match = matchOrNull(method, context.classDefs[method.definingClass]!!)
if (match != null) return@let match
}
null
}
if (match != null) return match
context.classDefs.forEach { classDef ->
match = matchOrNull(classDef)
if (match != null) return match
}
return null
}
context(_: BytecodePatchContext)
fun matchOrNull(
classDef: ClassDef,
): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
for (method in classDef.methods) {
val match = matchOrNull(method, classDef)
if (match != null) return match
}
return null
}
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
context(context: BytecodePatchContext)
fun matchOrNull(
method: Method,
classDef: ClassDef,
): FingerprintMatch? {
if (_matchOrNull != null) return _matchOrNull
if (returnType != null && !method.returnType.startsWith(returnType)) {
return null
}
if (accessFlags != null && accessFlags != method.accessFlags) {
return null
}
fun parametersEqual(
parameters1: Iterable<CharSequence>,
parameters2: Iterable<CharSequence>,
): Boolean {
if (parameters1.count() != parameters2.count()) return false
val iterator1 = parameters1.iterator()
parameters2.forEach {
if (!it.startsWith(iterator1.next())) return false
}
return true
}
// TODO: parseParameters()
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
return null
}
if (custom != null && !custom.invoke(method, classDef)) {
return null
}
val stringMatches: List<FingerprintMatch.StringMatch>? =
if (strings != null) {
buildList {
val instructions = method.instructionsOrNull ?: return null
val stringsList = strings.toMutableList()
instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) {
return@forEachIndexed
}
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(FingerprintMatch.StringMatch(string, instructionIndex))
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return null
}
} else {
null
}
val patternMatch = if (opcodes != null) {
val instructions = method.instructionsOrNull ?: return null
fun patternScan(): FingerprintMatch.PatternMatch? {
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
val instructionLength = instructions.count()
val patternLength = opcodes.size
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = opcodes.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// Reaching maximum threshold (0) means,
// the pattern does not match to the current instructions.
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// If the entire pattern has not been scanned yet, continue the scan.
patternIndex++
continue
}
// The entire pattern has been scanned.
return FingerprintMatch.PatternMatch(
index,
index + patternIndex,
)
}
}
return null
}
patternScan() ?: return null
} else {
null
}
_matchOrNull = FingerprintMatch(
context,
classDef,
method,
patternMatch,
stringMatches,
)
return _matchOrNull
}
private val exception get() = PatchException("Failed to match the fingerprint: $this")
context(_: BytecodePatchContext)
private val match
get() = matchOrNull ?: throw exception
context(_: BytecodePatchContext)
fun match(
classDef: ClassDef,
) = matchOrNull(classDef) ?: throw exception
context(_: BytecodePatchContext)
fun match(
method: Method,
) = matchOrNull(method) ?: throw exception
context(_: BytecodePatchContext)
fun match(
method: Method,
classDef: ClassDef,
) = matchOrNull(method, classDef) ?: throw exception
context(_: BytecodePatchContext)
val originalClassDefOrNull
get() = matchOrNull?.originalClassDef
context(_: BytecodePatchContext)
val originalMethodOrNull
get() = matchOrNull?.originalMethod
context(_: BytecodePatchContext)
val classDefOrNull
get() = matchOrNull?.classDef
context(_: BytecodePatchContext)
val methodOrNull
get() = matchOrNull?.method
context(_: BytecodePatchContext)
val patternMatchOrNull
get() = matchOrNull?.patternMatch
context(_: BytecodePatchContext)
val stringMatchesOrNull
get() = matchOrNull?.stringMatches
context(_: BytecodePatchContext)
val originalClassDef
get() = match.originalClassDef
context(_: BytecodePatchContext)
val originalMethod
get() = match.originalMethod
context(_: BytecodePatchContext)
val classDef
get() = match.classDef
context(_: BytecodePatchContext)
val method
get() = match.method
context(_: BytecodePatchContext)
val patternMatch
get() = match.patternMatch
context(_: BytecodePatchContext)
val stringMatches
get() = match.stringMatches
}
@Deprecated("Use the matcher API instead.")
class FingerprintMatch internal constructor(
val context: BytecodePatchContext,
val originalClassDef: ClassDef,
val originalMethod: Method,
val patternMatch: PatternMatch?,
val stringMatches: List<StringMatch>?,
) {
val classDef by lazy {
val classDef = context.classDefs[originalClassDef.type]!!
context.classDefs.getOrReplaceMutable(classDef)
}
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
class PatternMatch internal constructor(
val startIndex: Int,
val endIndex: Int,
)
class StringMatch internal constructor(val string: String, val index: Int)
}
@Deprecated("Use the matcher API instead.")
class FingerprintBuilder internal constructor(
private val fuzzyPatternScanThreshold: Int = 0,
) {
private var accessFlags: Int? = null
private var returnType: String? = null
private var parameters: List<String>? = null
private var opcodes: List<Opcode?>? = null
private var strings: List<String>? = null
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
fun accessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun accessFlags(vararg accessFlags: AccessFlags) {
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
}
fun returns(returnType: String) {
this.returnType = returnType
}
fun parameters(vararg parameters: String) {
this.parameters = parameters.toList()
}
fun opcodes(vararg opcodes: Opcode?) {
this.opcodes = opcodes.toList()
}
fun opcodes(instructions: String) {
this.opcodes = instructions.trimIndent().split("\n").filter {
it.isNotBlank()
}.map {
// Remove any operands.
val name = it.split(" ", limit = 1).first().trim()
if (name == "null") return@map null
opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
}
}
fun strings(vararg strings: String) {
this.strings = strings.toList()
}
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
this.customBlock = customBlock
}
internal fun build() = Fingerprint(
accessFlags,
returnType,
parameters,
opcodes,
strings,
customBlock,
fuzzyPatternScanThreshold,
)
private companion object {
val opcodesByName = Opcode.entries.associateBy { it.name }
}
}
@Deprecated("Use the matcher API instead.")
fun fingerprint(
fuzzyPatternScanThreshold: Int = 0,
block: FingerprintBuilder.() -> Unit,
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
package app.revanced.patcher
import app.revanced.patcher.patch.*
import java.io.File
import java.io.InputStream
import java.io.deleteRecursively
import java.io.resolve
import java.util.logging.Logger
fun patcher(
apkFile: File,
temporaryFilesPath: File = File("revanced-patcher-temporary-files"),
aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null,
getPatches: (packageName: String, versionName: String) -> Set<Patch>,
): (emit: (PatchResult) -> Unit) -> PatchesResult {
val logger = Logger.getLogger("Patcher")
if (temporaryFilesPath.exists()) {
logger.info("Deleting existing temporary files directory")
if (!temporaryFilesPath.deleteRecursively())
logger.severe("Failed to delete existing temporary files directory")
}
val apkFilesPath = temporaryFilesPath.resolve("apk").also { it.mkdirs() }
val patchedFilesPath = temporaryFilesPath.resolve("patched").also { it.mkdirs() }
val resourcePatchContext = ResourcePatchContext(
apkFile,
apkFilesPath,
patchedFilesPath,
aaptBinaryPath,
frameworkFileDirectory
)
val (packageName, versionName) = resourcePatchContext.decodeManifest()
val patches = getPatches(packageName, versionName)
return { emit: (PatchResult) -> Unit ->
if (patches.any { patch -> patch.patchesResources }) resourcePatchContext.decodeResources()
// After initializing the resource context, to keep memory usage time low.
val bytecodePatchContext = BytecodePatchContext(
apkFile,
patchedFilesPath
)
logger.info("Warming up the cache")
bytecodePatchContext.classDefs.initializeCache()
logger.info("Applying patches")
patches.apply(bytecodePatchContext, resourcePatchContext, emit)
}
}
// Public for testing.
fun Set<Patch>.apply(
bytecodePatchContext: BytecodePatchContext,
resourcePatchContext: ResourcePatchContext,
emit: (PatchResult) -> Unit
): PatchesResult {
val appliedPatches = LinkedHashMap<Patch, PatchResult>()
sortedBy { it.name }.forEach { patch ->
fun Patch.apply(): PatchResult {
val result = appliedPatches[this]
return if (result == null) {
val failedDependency = dependencies.asSequence().map { it.apply() }.firstOrNull { it.exception != null }
if (failedDependency != null) return patchResult(
"The dependant patch \"$failedDependency\" of the patch \"$this\" raised an exception:\n" +
failedDependency.exception!!.stackTraceToString(),
)
val exception = runCatching { apply(bytecodePatchContext, resourcePatchContext) }
.exceptionOrNull() as? Exception
patchResult(exception).also { result -> appliedPatches[this] = result }
} else if (result.exception == null) result
else patchResult("The patch '$this' has failed previously")
}
val patchResult = patch.apply()
// If an exception occurred or the patch has no finalize block, emit the result.
if (patchResult.exception != null || patch.afterDependents == null) {
emit(patchResult)
}
}
val succeededPatchesWithFinalizeBlock = appliedPatches.values.filter {
it.exception == null && it.patch.afterDependents != null
}
succeededPatchesWithFinalizeBlock.asReversed().forEach { result ->
val patch = result.patch
runCatching { patch.afterDependents!!.invoke(bytecodePatchContext, resourcePatchContext) }.fold(
{ emit(result) },
{
emit(
PatchResult(
patch,
PatchException(
"The patch \"$patch\" raised an exception:\n" + it.stackTraceToString(),
it,
),
)
)
}
)
}
return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get())
}
/**
* The result of applying patches.
*
* @param dexFiles The patched dex files.
* @param resources The patched resources.
*/
class PatchesResult internal constructor(
val dexFiles: Set<PatchedDexFile>,
val resources: PatchedResources?,
) {
/**
* A dex file.
*
* @param name The original name of the dex file.
* @param stream The dex file as [InputStream].
*/
class PatchedDexFile internal constructor(val name: String, val stream: InputStream)
/**
* The resources of a patched apk.
*
* @param resourcesApk The compiled resources.apk file.
* @param otherResources The directory containing other resources files.
* @param doNotCompress List of files that should not be compressed.
* @param deleteResources List of resources that should be deleted.
*/
class PatchedResources internal constructor(
val resourcesApk: File?,
val otherResources: File?,
val doNotCompress: Set<String>,
val deleteResources: Set<String>,
)
}

View File

@@ -0,0 +1,115 @@
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.*
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
import com.android.tools.smali.smali.smaliFlexLexer
import com.android.tools.smali.smali.smaliParser
import com.android.tools.smali.smali.smaliTreeWalker
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.TokenSource
import org.antlr.runtime.tree.CommonTreeNodeStream
import java.io.StringReader
private inline fun <reified T : Reference> Instruction.reference(): T? =
(this as? ReferenceInstruction)?.reference as? T
val Instruction.reference: Reference?
get() = reference()
val Instruction.methodReference
get() = reference<MethodReference>()
val Instruction.fieldReference
get() = reference<FieldReference>()
val Instruction.typeReference
get() = reference<TypeReference>()
val Instruction.stringReference
get() = reference<StringReference>()
/** TODO: This is technically missing for consistency:
private inline fun <reified T : Reference> Instruction.reference2(): T? =
(this as? DualReferenceInstruction)?.reference2 as? T
val Instruction.reference2: Reference?
get() = reference2()
val Instruction.methodReference2
get() = reference2<MethodReference>()
val Instruction.fieldReference2
get() = reference2<FieldReference>()
val Instruction.typeReference2
get() = reference2<TypeReference>()
val Instruction.stringReference2
get() = reference2<StringReference>()
**/
val Instruction.type
get() = typeReference?.type
val Instruction.string
get() = stringReference?.string
val Instruction.wideLiteral
get() = (this as? NarrowLiteralInstruction)?.wideLiteral
private const val CLASS_HEADER = ".class LInlineCompiler;\n.super Ljava/lang/Object;\n"
private const val STATIC_HEADER = "$CLASS_HEADER.method public static dummyMethod("
private const val HEADER = "$CLASS_HEADER.method public dummyMethod("
private val sb by lazy { StringBuilder(512) }
/**
* Compile lines of Smali code to a list of instructions.
*
* Note: Adding compiled instructions to an existing method with
* offset instructions WITHOUT specifying a parent method will not work.
* @param templateMethod The method to compile the instructions against.
* @returns A list of instructions.
*/
fun String.toInstructions(templateMethod: com.android.tools.smali.dexlib2.mutable.MutableMethod? = null): List<BuilderInstruction> {
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
sb.setLength(0) // reset
if (isStatic) sb.append(STATIC_HEADER) else sb.append(HEADER)
sb.append(parameters).append(")V\n")
sb.append(" .registers ").append(registers).append("\n")
sb.append(trimIndent()).append("\n")
sb.append(".end method")
val reader = StringReader(sb.toString())
val lexer = smaliFlexLexer(reader, 15)
val tokens = CommonTokenStream(lexer as TokenSource)
val parser = smaliParser(tokens)
val fileTree = parser.smali_file()
if (lexer.numberOfSyntaxErrors > 0 || parser.numberOfSyntaxErrors > 0) {
throw IllegalStateException(
"Lexer errors: ${lexer.numberOfSyntaxErrors}, Parser errors: ${parser.numberOfSyntaxErrors}"
)
}
val treeStream = CommonTreeNodeStream(fileTree.tree).apply {
tokenStream = tokens
}
val walker = smaliTreeWalker(treeStream)
walker.setDexBuilder(DexBuilder(Opcodes.getDefault()))
val classDef = walker.smali_file()
return classDef.methods.first().instructions.map { it as BuilderInstruction }
}

View File

@@ -0,0 +1,446 @@
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.Label
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.*
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.MethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.mutable.MutableMethod
fun Method.accessFlags(vararg flags: AccessFlags) =
accessFlags.and(flags.map { it.ordinal }.reduce { acc, i -> acc or i }) != 0
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param instructions The instructions to add.
*/
fun MutableMethodImplementation.addInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = instructions.asReversed().forEach { addInstruction(index, it) }
/**
* Add instructions to a method.
* The instructions will be added at the end of the method.
*
* @param instructions The instructions to add.
*/
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) =
instructions.forEach { addInstruction(it) }
/**
* Remove instructions from a method at the given index.
*
* @param index The index to remove the instructions at.
* @param count The amount of instructions to remove.
*/
fun MutableMethodImplementation.removeInstructions(
index: Int,
count: Int,
) = repeat(count) {
removeInstruction(index)
}
/**
* Remove the first instructions from a method.
*
* @param count The amount of instructions to remove.
*/
fun MutableMethodImplementation.removeInstructions(count: Int) = removeInstructions(0, count)
/**
* Replace instructions at the given index with the given instructions.
* The amount of instructions to replace is the amount of instructions in the given list.
*
* @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with.
*/
fun MutableMethodImplementation.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = instructions.forEachIndexed { i, instruction -> replaceInstruction(index + i, instruction) }
/**
* Add an instruction to a method at the given index.
*
* @param index The index to add the instruction at.
* @param instruction The instruction to add.
*/
fun MutableMethod.addInstruction(
index: Int,
instruction: BuilderInstruction,
) = implementation!!.addInstruction(index, instruction)
/**
* Add an instruction to a method.
*
* @param instruction The instructions to add.
*/
fun MutableMethod.addInstruction(instruction: BuilderInstruction) = implementation!!.addInstruction(instruction)
/**
* Add an instruction to a method at the given index.
*
* @param index The index to add the instruction at.
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(
index: Int,
smaliInstructions: String,
) = implementation!!.addInstruction(index, smaliInstructions.toInstructions(this).first())
/**
* Add an instruction to a method.
*
* @param smaliInstructions The instruction to add.
*/
fun MutableMethod.addInstruction(smaliInstructions: String) =
implementation!!.addInstruction(smaliInstructions.toInstructions(this).first())
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.addInstructions(index, instructions)
/**
* Add instructions to a method.
*
* @param instructions The instructions to add.
*/
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) =
implementation!!.addInstructions(instructions)
/**
* Add instructions to a method.
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(
index: Int,
smaliInstructions: String,
) = implementation!!.addInstructions(index, smaliInstructions.toInstructions(this))
/**
* Add instructions to a method.
*
* @param smaliInstructions The instructions to add.
*/
fun MutableMethod.addInstructions(smaliInstructions: String) =
implementation!!.addInstructions(smaliInstructions.toInstructions(this))
/**
* Add instructions to a method at the given index.
*
* @param index The index to add the instructions at.
* @param smaliInstructions The instructions to add.
* @param externalLabels A list of [ExternalLabel] for instructions outside of [smaliInstructions].
*/
// Special function for adding instructions with external labels.
fun MutableMethod.addInstructionsWithLabels(
index: Int,
smaliInstructions: String,
vararg externalLabels: ExternalLabel,
) {
// Create reference dummy instructions for the instructions.
val nopSmali =
StringBuilder(smaliInstructions).also { builder ->
externalLabels.forEach { (name, _) ->
builder.append("\n:$name\nnop")
}
}.toString()
// Compile the instructions with the dummy labels
val compiledInstructions = nopSmali.toInstructions(this)
// Add the compiled list of instructions to the method.
addInstructions(
index,
compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size),
)
implementation!!.apply {
this@apply.instructions.subList(index, index + compiledInstructions.size - externalLabels.size)
.forEachIndexed { compiledInstructionIndex, compiledInstruction ->
// If the compiled instruction is not an offset instruction, skip it.
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
/**
* Create a new label for the instruction
* and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex].
*/
fun Instruction.makeNewLabel() {
fun replaceOffset(
i: BuilderOffsetInstruction,
label: Label,
): BuilderOffsetInstruction {
return when (i) {
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
is BuilderInstruction22t ->
BuilderInstruction22t(
i.opcode,
i.registerA,
i.registerB,
label,
)
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
else -> throw IllegalStateException(
"A non-offset instruction was given, this should never happen!",
)
}
}
// Create the final label.
val label = newLabelForIndex(this@apply.instructions.indexOf(this))
// Create the final instruction with the new label.
val newInstruction =
replaceOffset(
compiledInstruction,
label,
)
// Replace the instruction pointing to the dummy label
// with the new instruction pointing to the real instruction.
replaceInstruction(index + compiledInstructionIndex, newInstruction)
}
// If the compiled instruction targets its own instruction,
// which means it points to some of its own, simply an offset has to be applied.
val labelIndex = compiledInstruction.target.location.index
if (labelIndex < compiledInstructions.size - externalLabels.size) {
// Get the targets index (insertion index + the index of the dummy instruction).
this.instructions[index + labelIndex].makeNewLabel()
return@forEachIndexed
}
// Since the compiled instruction points to a dummy instruction,
// we can find the real instruction which it was created for by calculation.
// Get the index of the instruction in the externalLabels list
// which the dummy instruction was created for.
// This works because we created the dummy instructions in the same order as the externalLabels list.
val (_, instruction) = externalLabels[(compiledInstructions.size - 1) - labelIndex]
instruction.makeNewLabel()
}
}
}
/**
* Remove an instruction at the given index.
*
* @param index The index to remove the instruction at.
*/
fun MutableMethod.removeInstruction(index: Int) = implementation!!.removeInstruction(index)
/**
* Remove instructions at the given index.
*
* @param index The index to remove the instructions at.
* @param count The amount of instructions to remove.
*/
fun MutableMethod.removeInstructions(
index: Int,
count: Int,
) = implementation!!.removeInstructions(index, count)
/**
* Remove instructions at the given index.
*
* @param count The amount of instructions to remove.
*/
fun MutableMethod.removeInstructions(count: Int) = implementation!!.removeInstructions(count)
/**
* Replace an instruction at the given index.
*
* @param index The index to replace the instruction at.
* @param instruction The instruction to replace the instruction with.
*/
fun MutableMethod.replaceInstruction(
index: Int,
instruction: BuilderInstruction,
) = implementation!!.replaceInstruction(index, instruction)
/**
* Replace an instruction at the given index.
*
* @param index The index to replace the instruction at.
* @param smaliInstruction The smali instruction to replace the instruction with.
*/
fun MutableMethod.replaceInstruction(
index: Int,
smaliInstruction: String,
) = implementation!!.replaceInstruction(index, smaliInstruction.toInstructions(this).first())
/**
* Replace instructions at the given index.
*
* @param index The index to replace the instructions at.
* @param instructions The instructions to replace the instructions with.
*/
fun MutableMethod.replaceInstructions(
index: Int,
instructions: List<BuilderInstruction>,
) = implementation!!.replaceInstructions(index, instructions)
/**
* Replace instructions at the given index.
*
* @param index The index to replace the instructions at.
* @param smaliInstructions The smali instructions to replace the instructions with.
*/
fun MutableMethod.replaceInstructions(
index: Int,
smaliInstructions: String,
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MethodImplementation.getInstruction(index: Int) = instructions.elementAt(index)
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethodImplementation.getInstruction(index: Int): BuilderInstruction = instructions[index]
/**
* Get an instruction at the given index.
*
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
@Suppress("UNCHECKED_CAST")
fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun Method.getInstructionOrNull(index: Int): Instruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun Method.getInstruction(index: Int): Instruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> Method.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction or null if the method has no implementation.
*/
fun MutableMethod.getInstructionOrNull(index: Int): BuilderInstruction? = implementation?.getInstruction(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @return The instruction.
*/
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = getInstructionOrNull(index)!!
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction or null if the method has no implementation.
*/
fun <T> MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
/**
* Get an instruction at the given index.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> MutableMethod.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val Method.instructionsOrNull: Iterable<Instruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val Method.instructions: Iterable<Instruction> get() = instructionsOrNull!!
/**
* The instructions of a method.
* @return The instructions or null if the method has no implementation.
*/
val MutableMethod.instructionsOrNull: MutableList<BuilderInstruction>? get() = implementation?.instructions
/**
* The instructions of a method.
* @return The instructions.
*/
val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instructionsOrNull!!
/**
* Create a label for the instruction at given index.
*
* @param index The index to create the label for the instruction at.
* @return The label.
*/
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
/**
* A class that represents a label for an instruction.
* @param name The label name.
* @param instruction The instruction that this label is for.
*/
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)

View File

@@ -0,0 +1,264 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchesResult
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.extensions.string
import app.revanced.patcher.util.ClassMerger.merge
import app.revanced.patcher.util.MethodNavigator
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import lanchon.multidexlib2.RawDexIO
import java.io.*
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A context for patches containing the current state of the bytecode.
*
* @param apkFile The apk [File] to patch.
* @param patchedFilesPath The path to the temporary apk files directory.
*/
@Suppress("MemberVisibilityCanBePrivate")
class BytecodePatchContext internal constructor(
internal val apkFile: File,
internal val patchedFilesPath: File,
) : PatchContext<Set<PatchesResult.PatchedDexFile>> {
private val logger = Logger.getLogger(this::class.jvmName)
inner class ClassDefs private constructor(
dexFile: DexFile,
private val classDefs: MutableSet<ClassDef> = dexFile.classes.toMutableSet()
) :
MutableSet<ClassDef> by classDefs {
private val byType = mutableMapOf<String, ClassDef>()
operator fun get(name: String): ClassDef? = byType[name]
// Better performance according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/9b6d95d4f414a35ed68da37b0ecd8549df1ef63a
private val _methodsByStrings =
LinkedHashMap<String, MutableSet<Method>>(2 * size, 0.5f)
val methodsByString: Map<String, Set<Method>> = _methodsByStrings
// Can have a use-case in the future:
// private val _methodsWithString = methodsByString.values.flatten().toMutableSet()
// val methodsWithString: Set<Method> = _methodsWithString
constructor() : this(
MultiDexIO.readDexFile(
true,
apkFile,
BasicDexFileNamer(),
null,
null
)
)
internal val opcodes = dexFile.opcodes
override fun add(element: ClassDef): Boolean {
val added = classDefs.add(element)
if (added) addCache(element)
return added
}
override fun addAll(elements: Collection<ClassDef>): Boolean {
var anyAdded = false
elements.forEach { element ->
val added = classDefs.add(element)
if (added) {
addCache(element)
anyAdded = true
}
}
return anyAdded
}
// TODO: There is one default method "removeIf" in MutableSet, which we cannot override in the common module.
// The method must be overloaded with a NotImplementedException to avoid cache desynchronization.
override fun clear() {
classDefs.clear()
byType.clear()
_methodsByStrings.clear()
}
override fun remove(element: ClassDef): Boolean {
val removed = classDefs.remove(element)
if (removed) removeCache(element)
return removed
}
override fun removeAll(elements: Collection<ClassDef>): Boolean {
var anyRemoved = false
elements.forEach { element ->
val removed = classDefs.remove(element)
if (removed) {
removeCache(element)
anyRemoved = true
}
}
return anyRemoved
}
override fun retainAll(elements: Collection<ClassDef>) =
removeAll(classDefs.asSequence().filter { it !in elements })
private fun addCache(classDef: ClassDef) {
byType[classDef.type] = classDef
classDef.forEachString { method, string ->
_methodsByStrings.getOrPut(string) {
// Maybe adjusting load factor/ initial size can improve performance.
mutableSetOf()
} += method
}
}
private fun removeCache(classDef: ClassDef) {
byType -= classDef.type
classDef.forEachString { method, string ->
if (_methodsByStrings[string]?.also { it -= method }?.isEmpty() == true)
_methodsByStrings -= string
}
}
private fun ClassDef.forEachString(action: (Method, String) -> Unit) {
methods.asSequence().forEach { method ->
method.instructionsOrNull?.asSequence()
?.mapNotNull { it.string }
?.forEach { string -> action(method, string) }
}
}
/**
* Get a mutable version of the given [classDef], replacing it in the set if necessary.
*
* @param classDef The [ClassDef] to get or replace.
* @return The mutable version of the [classDef].
* @see MutableClassDef
* @see toMutable
*/
fun getOrReplaceMutable(classDef: ClassDef): MutableClassDef {
if (classDef !is MutableClassDef) {
val mutableClassDef = classDef.toMutable()
this -= classDef
this += mutableClassDef
return mutableClassDef
}
return classDef
}
internal fun initializeCache() = classDefs.forEach(::addCache)
internal fun clearCache() {
byType.clear()
_methodsByStrings.clear()
}
}
/**
* The list of classes.
*/
val classDefs = ClassDefs()
/**
* Extend this [BytecodePatchContext] with [extensionInputStream].
*
* @param extensionInputStream The input stream for an extension dex file.
*/
internal fun extendWith(extensionInputStream: InputStream) {
RawDexIO.readRawDexFile(
extensionInputStream, 0, null
).classes.forEach { classDef ->
val existingClass = classDefs[classDef.type] ?: run {
logger.fine { "Adding class \"$classDef\"" }
classDefs += classDef
return@forEach
}
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
// If the class was merged, replace the original class with the merged class.
if (mergedClass === existingClass) {
return@let
}
classDefs -= existingClass
classDefs += mergedClass
}
}
extensionInputStream.close()
}
/**
* Navigate a method.
*
* @param method The method to navigate.
*
* @return A [MethodNavigator] for the method.
*/
fun navigate(method: MethodReference) = MethodNavigator(method)
/**
* Compile bytecode from the [BytecodePatchContext].
*
* @return The compiled bytecode.
*/
override fun get(): Set<PatchesResult.PatchedDexFile> {
logger.info("Compiling patched dex files")
classDefs.clearCache()
System.gc()
val patchedDexFileResults =
patchedFilesPath.resolve("dex").also {
it.deleteRecursively() // Make sure the directory is empty.
it.mkdirs()
}.apply {
MultiDexIO.writeDexFile(
true,
-1,
this,
BasicDexFileNamer(),
object : DexFile {
override fun getClasses() = classDefs.let {
// More performant according to
// https://github.com/LisoUseInAIKyrios/revanced-patcher/
// commit/8c26ad08457fb1565ea5794b7930da42a1c81cf1
// #diff-be698366d9868784ecf7da3fd4ac9d2b335b0bb637f9f618fbe067dbd6830b8fR197
// TODO: Benchmark, if actually faster.
HashSet<ClassDef>(it.size * 3 / 2).apply { addAll(it) }
}
override fun getOpcodes() = classDefs.opcodes
},
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
}.listFiles { it.isFile }!!.map {
PatchesResult.PatchedDexFile(it.name, it.inputStream())
}.toSet()
return patchedDexFileResults
}
}

View File

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

View File

@@ -0,0 +1,273 @@
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
package app.revanced.patcher.patch
import java.io.File
import java.io.InputStream
import java.lang.reflect.Member
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.util.function.Supplier
import kotlin.properties.ReadOnlyProperty
typealias PackageName = String
typealias VersionName = String
typealias Package = Pair<PackageName, Set<VersionName>?>
enum class PatchType(internal val prefix: String) {
BYTECODE("Bytecode"),
RAW_RESOURCE("RawResource"),
RESOURCE("Resource")
}
internal val Patch.patchesResources: Boolean get() = type == PatchType.RESOURCE || dependencies.any { it.patchesResources }
open class Patch internal constructor(
val name: String?,
val description: String?,
val use: Boolean,
val dependencies: Set<Patch>,
val compatiblePackages: Set<Package>?,
options: Set<Option<*>>,
internal val apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
// Must be nullable, so that Patcher.invoke can check,
// if a patch has an "afterDependents" in order to not emit it twice.
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
internal val type: PatchType,
) {
val options = Options(options)
override fun toString() = name ?: "${type.prefix}Patch@${System.identityHashCode(this)}"
}
sealed class PatchBuilder<C : PatchContext<*>>(
private val type: PatchType,
private val getPatchContext: context(BytecodePatchContext, ResourcePatchContext) () -> C
) {
private var compatiblePackages: MutableSet<Package>? = null
private val dependencies = mutableSetOf<Patch>()
private val options = mutableSetOf<Option<*>>()
internal var apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
context(_: BytecodePatchContext, _: ResourcePatchContext)
private val patchContext get() = getPatchContext()
fun apply(block: C.() -> Unit) {
apply = { block(patchContext) }
}
fun afterDependents(block: C.() -> Unit) {
afterDependents = { block(patchContext) }
}
operator fun <T> Option<T>.invoke() = apply {
options += this
}
operator fun String.invoke(vararg versions: VersionName) = invoke(versions.toSet())
private operator fun String.invoke(versions: Set<VersionName>? = null): Package = this to versions
fun compatibleWith(vararg packages: Package) {
if (compatiblePackages == null) {
compatiblePackages = mutableSetOf()
}
compatiblePackages!! += packages
}
fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray())
fun dependsOn(vararg patches: Patch) {
dependencies += patches
}
fun build(name: String?, description: String?, use: Boolean) = Patch(
name,
description,
use,
dependencies,
compatiblePackages,
options,
apply,
afterDependents,
type,
)
}
class BytecodePatchBuilder private constructor(
private var extensionInputStream: InputStream? = null
) : PatchBuilder<BytecodePatchContext>(
PatchType.BYTECODE,
{
// Extend the context with the extension, before returning it to the patch before applying it.
contextOf<BytecodePatchContext>().apply {
if (extensionInputStream != null) extendWith(extensionInputStream)
}
}
) {
internal constructor() : this(null)
fun extendWith(extension: String) {
// Should be the classloader which loaded the patch class.
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
extensionInputStream = classLoader.getResourceAsStream(extension)
?: throw PatchException("Extension \"$extension\" not found")
}
}
open class ResourcePatchBuilder internal constructor(type: PatchType) : PatchBuilder<ResourcePatchContext>(
type,
{ contextOf<ResourcePatchContext>() }
) {
internal constructor() : this(PatchType.RESOURCE)
}
class RawResourcePatchBuilder internal constructor() : ResourcePatchBuilder()
fun bytecodePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: BytecodePatchBuilder.() -> Unit
) = BytecodePatchBuilder().apply(block).build(name, description, use)
fun resourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: ResourcePatchBuilder.() -> Unit
) = ResourcePatchBuilder().apply(block).build(name, description, use)
fun rawResourcePatch(
name: String? = null,
description: String? = null,
use: Boolean = true,
block: RawResourcePatchBuilder.() -> Unit
) = RawResourcePatchBuilder().apply(block).build(name, description, use)
private fun <B : PatchBuilder<*>> creatingPatch(
description: String? = null,
use: Boolean = true,
block: B.() -> Unit,
patchSupplier: (String?, String?, Boolean, B.() -> Unit) -> Patch
) = ReadOnlyProperty<Any?, Patch> { _, property -> patchSupplier(property.name, description, use, block) }
fun creatingBytecodePatch(
description: String? = null,
use: Boolean = true,
block: BytecodePatchBuilder.() -> Unit,
) = creatingPatch(description, use, block) { name, description, use, block ->
bytecodePatch(name, description, use, block)
}
fun creatingResourcePatch(
description: String? = null,
use: Boolean = true,
block: ResourcePatchBuilder.() -> Unit,
) = creatingPatch(description, use, block) { name, description, use, block ->
resourcePatch(name, description, use, block)
}
fun creatingRawResourcePatch(
description: String? = null,
use: Boolean = true,
block: RawResourcePatchBuilder.() -> Unit,
) = creatingPatch(description, use, block) { name, description, use, block ->
rawResourcePatch(name, description, use, block)
}
/**
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
*/
sealed interface PatchContext<T> : Supplier<T>
/**
* An exception thrown when patching.
*
* @param errorMessage The exception message.
* @param cause The corresponding [Throwable].
*/
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
constructor(errorMessage: String) : this(errorMessage, null)
constructor(cause: Throwable) : this(cause.message, cause)
}
/**
* A result of applying a [Patch].
*
* @param patch The [Patch] that ran.
* @param exception The [PatchException] thrown, if any.
*/
class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null)
/**
* Creates a [PatchResult] for this [Patch].
*
* @param exception The [PatchException] thrown, if any.
* @return The created [PatchResult].
*/
internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException())
/**
* Creates a [PatchResult] for this [Patch] with the given error message.
*
* @param errorMessage The error message.
* @return The created [PatchResult].
*/
internal fun Patch.patchResult(errorMessage: String) = PatchResult(this, PatchException(errorMessage))
private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this)
/**
* A collection of patches loaded from patches files.
*
* @property patchesByFile The patches mapped by their patches file.
*/
class Patches internal constructor(val patchesByFile: Map<File, Set<Patch>>) : Set<Patch>
by patchesByFile.values.flatten().toSet()
// Must be internal and a separate function for testing.
@Suppress("MISSING_DEPENDENCY_IN_INFERRED_TYPE_ANNOTATION_WARNING")
internal fun getPatches(classNames: List<String>, classLoader: ClassLoader): Set<Patch> {
fun Member.isUsable() =
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount == 0)
fun Class<*>.getPatchFields() = fields
.filter { it.type.isPatch && it.isUsable() }
.map { it.get(null) as Patch }
fun Class<*>.getPatchMethods() = methods
.filter { it.returnType.isPatch && it.parameterCount == 0 && it.isUsable() }
.map { it.invoke(null) as Patch }
return classNames
.map { classLoader.loadClass(it) }
.flatMap { it.getPatchMethods() + it.getPatchFields() }
.filter { it.name != null }
.toSet()
}
internal fun loadPatches(
vararg patchesFiles: File,
getBinaryClassNames: (patchesFile: File) -> List<String>,
classLoader: ClassLoader,
onFailedToLoad: (File, Throwable) -> Unit
) = Patches(patchesFiles.map { file ->
file to getBinaryClassNames(file)
}.mapNotNull { (file, classNames) ->
runCatching { file to getPatches(classNames, classLoader) }
.onFailure { onFailedToLoad(file, it) }.getOrNull()
}.toMap())
expect fun loadPatches(
vararg patchesFiles: File,
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit = { _, _ -> },
): Patches
internal expect val Class<*>.isPatch: Boolean

View File

@@ -0,0 +1,229 @@
package app.revanced.patcher.patch
import app.revanced.patcher.PatchesResult
import app.revanced.patcher.util.Document
import brut.androlib.AaptInvoker
import brut.androlib.ApkDecoder
import brut.androlib.Config
import brut.androlib.apk.ApkInfo
import brut.androlib.apk.UsesFramework
import brut.androlib.res.Framework
import brut.androlib.res.ResourcesDecoder
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
import brut.androlib.res.decoder.AndroidManifestResourceParser
import brut.androlib.res.xml.ResXmlUtils
import brut.directory.ExtFile
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.io.resolve
import java.nio.file.Files
import java.util.logging.Logger
import kotlin.reflect.jvm.jvmName
/**
* A context for patches containing the current state of resources.
*
* @param apkFile The apk file to patch.
* @param apkFilesPath The path to the temporary apk files directory.
* @param patchedFilesPath The path to the temporary patched files directory.
* @param aaptBinaryPath The path to a custom aapt binary.
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
*/
class ResourcePatchContext internal constructor(
private val apkFile: File,
private val apkFilesPath: File,
private val patchedFilesPath: File,
aaptBinaryPath: File? = null,
frameworkFileDirectory: String? = null,
) : PatchContext<PatchesResult.PatchedResources?> {
private val apkInfo = ApkInfo(ExtFile(apkFile))
private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName)
private val resourceConfig = Config.getDefaultConfig().apply {
aaptBinary = aaptBinaryPath
frameworkDirectory = frameworkFileDirectory
}
internal var decodingMode = ResourceDecodingMode.MANIFEST
/**
* Read a document from an [InputStream].
*/
fun document(inputStream: InputStream) = Document(inputStream)
/**
* Read and write documents in the [apkFile].
*/
fun document(path: String) = Document(get(path))
/**
* Set of resources from [apkFile] to delete.
*/
private val deleteResources = mutableSetOf<String>()
internal fun decodeManifest(): Pair<PackageName, VersionName> {
logger.info("Decoding manifest")
val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo)
// Decode manually instead of using resourceDecoder.decodeManifest
// because it does not support decoding to an OutputStream.
AndroidManifestPullStreamDecoder(
AndroidManifestResourceParser(resourcesDecoder.resTable),
resourcesDecoder.newXmlSerializer(),
).decode(
apkInfo.apkFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) { // Do nothing.
}
},
)
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
// The call to AndroidManifestPullStreamDecoder.decode() above sets apkInfo.
val packageName = resourcesDecoder.resTable.packageRenamed
val packageVersion =
apkInfo.versionInfo.versionName ?: apkInfo.versionInfo.versionCode
/*
When the main resource package is not loaded, the ResTable is flagged as sparse.
Because ResourcesDecoder.decodeResources loads the main package and is not called here,
set sparseResources to false again to prevent the ResTable from being flagged as sparse falsely,
in case ResourcesDecoder.decodeResources is not later used in the patching process
to set sparseResources correctly.
See ARSCDecoder.readTableType for more info.
*/
apkInfo.sparseResources = false
return packageName to packageVersion
}
internal fun decodeResources() {
logger.info("Decoding resources")
val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo).also {
it.decodeResources(apkFilesPath)
it.decodeManifest(apkFilesPath)
}
// Record uncompressed files to preserve their state when recompiling.
ApkDecoder(apkInfo, resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping)
// Get the ids of the used framework packages to include them for reference when recompiling.
apkInfo.usesFramework = UsesFramework().apply {
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
}
}
/**
* Compile resources in [apkFilesPath].
*
* @return The [PatchesResult.PatchedResources].
*/
override fun get(): PatchesResult.PatchedResources {
logger.info("Compiling patched resources")
val resourcesPath = patchedFilesPath.resolve("resources").also { it.mkdirs() }
val resourcesApkFile = if (decodingMode == ResourceDecodingMode.ALL) {
val resourcesApkFile = resourcesPath.resolve("resources.apk").also { it.createNewFile() }
val manifestFile = apkFilesPath.resolve("AndroidManifest.xml").also {
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
}
val resPath = apkFilesPath.resolve("res")
val frameworkApkFiles = with(Framework(resourceConfig)) {
apkInfo.usesFramework.ids.map { id -> getFrameworkApk(id, null) }
}.toTypedArray()
AaptInvoker(
resourceConfig,
apkInfo
).invoke(resourcesApkFile, manifestFile, resPath, null, null, frameworkApkFiles)
resourcesApkFile
} else null
val otherFiles = apkFilesPath.listFiles()!!.filter {
// Excluded because present in resources.other.
// TODO: We are reusing apkFiles as a temporarily directory for extracting resources.
// This is not ideal as it could conflict with files such as the ones that are filtered here.
// The problem is that ResourcePatchContext#get returns a File relative to apkFiles,
// and we need to extract files to that directory.
// A solution would be to use apkFiles as the working directory for the patching process.
// Once all patches have been executed, we can move the decoded resources to a new directory.
// The filters wouldn't be needed anymore.
// For now, we assume that the files we filter here are not needed for the patching process.
it.name != "AndroidManifest.xml" &&
it.name != "res" &&
// Generated by Androlib.
it.name != "build"
}
val otherResourceFiles = if (otherFiles.isNotEmpty()) {
// Move the other resources files.
resourcesPath.resolve("other").also { it.mkdirs() }.apply {
otherFiles.forEach { file ->
Files.move(file.toPath(), resolve(file.name).toPath())
}
}
} else null
return PatchesResult.PatchedResources(
resourcesApkFile,
otherResourceFiles,
apkInfo.doNotCompress?.toSet() ?: emptySet(),
deleteResources,
)
}
/**
* Get a file from [apkFilesPath].
*
* @param path The path of the file.
* @param copy Whether to copy the file from [apkFile] if it does not exist yet in [apkFilesPath].
*/
operator fun get(
path: String,
copy: Boolean = true,
) = apkFilesPath.resolve(path).apply {
if (copy && !exists()) {
with(ExtFile(apkFile).directory) {
if (containsFile(path) || containsDir(path)) {
copyToDir(apkFilesPath, path)
}
}
}
}
/**
* Mark a file for deletion when the APK is rebuilt.
*
* @param name The name of the file to delete.
*/
fun delete(name: String) = deleteResources.add(name)
/**
* How to handle resources decoding and compiling.
*/
internal enum class ResourceDecodingMode {
/**
* Decode and compile all resources.
*/
ALL,
/**
* Do not decode or compile any resources.
*/
NONE,
/**
* Do not decode or compile any resources.
*/
MANIFEST,
}
}

View File

@@ -0,0 +1,239 @@
package app.revanced.patcher.util
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableField
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
import app.revanced.patcher.util.ClassMerger.Utils.filterAny
import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
import app.revanced.patcher.util.ClassMerger.Utils.isPublic
import app.revanced.patcher.util.ClassMerger.Utils.toPublic
import app.revanced.patcher.util.ClassMerger.Utils.traverseClassHierarchy
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.util.MethodUtil
import java.util.logging.Logger
import kotlin.reflect.KFunction2
import kotlin.reflect.jvm.jvmName
/**
* Experimental class to merge a [ClassDef] with another.
* Note: This will not consider method implementations or if the class is missing a superclass or interfaces.
*/
internal object ClassMerger {
private val logger = Logger.getLogger(ClassMerger::class.jvmName)
/**
* Merge a class with [otherClass].
*
* @param otherClass The class to merge with
* @param context The context to traverse the class hierarchy in.
* @return The merged class or the original class if no merge was needed.
*/
fun ClassDef.merge(
otherClass: ClassDef,
context: BytecodePatchContext,
) = this
// .fixFieldAccess(otherClass)
// .fixMethodAccess(otherClass)
.addMissingFields(otherClass)
.addMissingMethods(otherClass)
.publicize(otherClass, context)
/**
* Add methods which are missing but existing in [fromClass].
*
* @param fromClass The class to add missing methods from.
*/
private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef {
val missingMethods =
fromClass.methods.let { fromMethods ->
methods.filterNot { method ->
fromMethods.any { fromMethod ->
MethodUtil.methodSignaturesMatch(fromMethod, method)
}
}
}
if (missingMethods.isEmpty()) return this
logger.fine { "Found ${missingMethods.size} missing methods" }
return asMutableClass().apply {
methods.addAll(missingMethods.map { it.toMutable() })
}
}
/**
* Add fields which are missing but existing in [fromClass].
*
* @param fromClass The class to add missing fields from.
*/
private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef {
val missingFields =
fields.filterNotAny(fromClass.fields) { field, fromField ->
fromField.name == field.name
}
if (missingFields.isEmpty()) return this
logger.fine { "Found ${missingFields.size} missing fields" }
return asMutableClass().apply {
fields.addAll(missingFields.map { it.toMutable() })
}
}
/**
* Make a class and its super class public recursively.
* @param reference The class to check the [AccessFlags] of.
* @param context The context to traverse the class hierarchy in.
*/
private fun ClassDef.publicize(
reference: ClassDef,
context: BytecodePatchContext,
) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
this.asMutableClass().apply {
context.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy
logger.fine { "Publicizing ${this.type}" }
accessFlags = accessFlags.toPublic()
}
}
} else {
this
}
/**
* Publicize fields if they are public in [reference].
*
* @param reference The class to check the [AccessFlags] of the fields in.
*/
private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef {
val brokenFields =
fields.filterAny(reference.fields) { field, referenceField ->
if (field.name != referenceField.name) return@filterAny false
referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
}
if (brokenFields.isEmpty()) return this
logger.fine { "Found ${brokenFields.size} broken fields" }
/**
* Make a field public.
*/
fun MutableField.publicize() {
accessFlags = accessFlags.toPublic()
}
return asMutableClass().apply {
fields.filter { brokenFields.contains(it) }.forEach(MutableField::publicize)
}
}
/**
* Publicize methods if they are public in [reference].
*
* @param reference The class to check the [AccessFlags] of the methods in.
*/
private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef {
val brokenMethods =
methods.filterAny(reference.methods) { method, referenceMethod ->
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
}
if (brokenMethods.isEmpty()) return this
logger.fine { "Found ${brokenMethods.size} methods" }
/**
* Make a method public.
*/
fun MutableMethod.publicize() {
accessFlags = accessFlags.toPublic()
}
return asMutableClass().apply {
methods.filter { brokenMethods.contains(it) }.forEach(MutableMethod::publicize)
}
}
private object Utils {
/**
* traverse the class hierarchy starting from the given root class
*
* @param targetClass the class to start traversing the class hierarchy from
* @param callback function that is called for every class in the hierarchy
*/
fun BytecodePatchContext.traverseClassHierarchy(
targetClass: MutableClassDef,
callback: MutableClassDef.() -> Unit,
) {
callback(targetClass)
classDefs[targetClass.superclass ?: return]?.let { classDef ->
traverseClassHierarchy(classDefs.getOrReplaceMutable(classDef), callback)
}
}
fun ClassDef.asMutableClass() = this as? MutableClassDef ?: this.toMutable()
/**
* Check if the [AccessFlags.PUBLIC] flag is set.
*
* @return True, if the flag is set.
*/
fun Int.isPublic() = AccessFlags.PUBLIC.isSet(this)
/**
* Make [AccessFlags] public.
*
* @return The new [AccessFlags].
*/
fun Int.toPublic() = or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
/**
* Filter [this] on [needles] matching the given [predicate].
*
* @param needles The needles to filter [this] with.
* @param predicate The filter.
* @return The [this] filtered on [needles] matching the given [predicate].
*/
fun <HayType, NeedleType> Iterable<HayType>.filterAny(
needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = Iterable<HayType>::filter.any(this, needles, predicate)
/**
* Filter [this] on [needles] not matching the given [predicate].
*
* @param needles The needles to filter [this] with.
* @param predicate The filter.
* @return The [this] filtered on [needles] not matching the given [predicate].
*/
fun <HayType, NeedleType> Iterable<HayType>.filterNotAny(
needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = Iterable<HayType>::filterNot.any(this, needles, predicate)
fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any(
haystack: Iterable<HayType>,
needles: Iterable<NeedleType>,
predicate: (HayType, NeedleType) -> Boolean,
) = this(haystack) { hay ->
needles.any { needle ->
predicate(hay, needle)
}
}
}
}

View File

@@ -0,0 +1,61 @@
package app.revanced.patcher.util
import collections.merge
import com.google.common.base.Charsets
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.bufferedWriter
import java.io.inputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import kotlin.use
class Document internal constructor(
inputStream: InputStream,
) : Document by DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream), Closeable {
private var file: File? = null
init {
normalize()
}
internal constructor(file: File) : this(file.inputStream()) {
this.file = file
readerCount.merge(file, 1, Int::plus)
}
override fun close() {
file?.let {
if (readerCount[it]!! > 1) {
throw IllegalStateException(
"Two or more instances are currently reading $it." +
"To be able to close this instance, no other instances may be reading $it at the same time.",
)
} else {
readerCount.remove(it)
}
val transformer = TransformerFactory.newInstance().newTransformer()
// Set to UTF-16 to prevent surrogate pairs from being escaped to invalid numeric character references, but save as UTF-8.
if (isAndroid) {
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16")
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
val writer = it.bufferedWriter(charset = Charsets.UTF_8)
transformer.transform(DOMSource(this), StreamResult(writer))
writer.close()
} else {
transformer.transform(DOMSource(this), StreamResult(it))
}
}
}
private companion object {
private val readerCount = mutableMapOf<File, Int>()
private val isAndroid = System.getProperty("java.runtime.name") == "Android Runtime"
}
}

View File

@@ -0,0 +1,119 @@
@file:Suppress("unused")
package app.revanced.patcher.util
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import app.revanced.patcher.extensions.instructionsOrNull
import app.revanced.patcher.patch.BytecodePatchContext
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.util.MethodUtil
import kotlin.reflect.KProperty
/**
* A navigator for methods.
*
* @param startMethod The [Method] to start navigating from.
*
* @constructor Creates a new [MethodNavigator].
*
* @throws NavigateException If the method does not have an implementation.
* @throws NavigateException If the instruction at the specified index is not a method reference.
*/
class MethodNavigator internal constructor(
private var startMethod: MethodReference,
) {
private var lastNavigatedMethodReference = startMethod
context(_: BytecodePatchContext)
private val lastNavigatedMethodInstructions
get() = with(original()) {
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
}
/**
* Navigate to the method at the specified index.
*
* @param index The index of the method to navigate to.
*
* @return This [MethodNavigator].
*/
context(_: BytecodePatchContext)
fun to(vararg index: Int): MethodNavigator {
index.forEach {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
}
return this
}
/**
* Navigate to the method at the specified index that matches the specified predicate.
*
* @param index The index of the method to navigate to.
* @param predicate The predicate to match.
*/
context(_: BytecodePatchContext)
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
.filter(predicate).asIterable().getMethodReferenceAt(index)
return this
}
/**
* Get the method reference at the specified index.
*
* @param index The index of the method reference to get.
*/
private fun Iterable<Instruction>.getMethodReferenceAt(index: Int): MethodReference {
val instruction = elementAt(index) as? ReferenceInstruction
?: throw NavigateException("Instruction at index $index is not a method reference.")
return instruction.reference as MethodReference
}
/**
* Get the last navigated method mutably.
*
* @return The last navigated method mutably.
*/
context(context: BytecodePatchContext)
fun stop() = context.classDefs[lastNavigatedMethodReference.definingClass]!!
.firstMethodBySignature as MutableMethod
/**
* Get the last navigated method mutably.
*
* @return The last navigated method mutably.
*/
operator fun getValue(context: BytecodePatchContext?, property: KProperty<*>) =
context(requireNotNull(context)) { stop() }
/**
* Get the last navigated method immutably.
*
* @return The last navigated method immutably.
*/
context(context: BytecodePatchContext)
fun original(): Method = context.classDefs[lastNavigatedMethodReference.definingClass]!!.firstMethodBySignature
/**
* Find the first [lastNavigatedMethodReference] in the class.
*/
private val ClassDef.firstMethodBySignature
get() = methods.first {
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
}
/**
* An exception thrown when navigating fails.
*
* @param message The message of the exception.
*/
internal class NavigateException internal constructor(message: String) : Exception(message)
}

View File

@@ -0,0 +1,13 @@
package collections
internal expect fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V,
)
internal fun <K, V> MutableMap<K, V>.merge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V,
) = kmpMerge(key, value, remappingFunction)

View File

@@ -0,0 +1,26 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
BaseAnnotationEncodedValue(),
MutableEncodedValue {
private var type = annotationEncodedValue.type
private val _elements by lazy {
annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
}
fun setType(type: String) {
this.type = type
}
override fun getType() = this.type
override fun getElements() = _elements
companion object {
fun AnnotationEncodedValue.toMutable() = MutableAnnotationEncodedValue(this)
}
}

View File

@@ -0,0 +1,16 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) : BaseArrayEncodedValue(), MutableEncodedValue {
private val _value by lazy {
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
}
override fun getValue() = _value
companion object {
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
}
}

View File

@@ -0,0 +1,19 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
BaseBooleanEncodedValue(),
MutableEncodedValue {
private var value = booleanEncodedValue.value
fun setValue(value: Boolean) {
this.value = value
}
override fun getValue(): Boolean = this.value
companion object {
fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue = MutableBooleanEncodedValue(this)
}
}

View File

@@ -1,22 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.base.value.BaseByteEncodedValue
import org.jf.dexlib2.iface.value.ByteEncodedValue
import com.android.tools.smali.dexlib2.base.value.BaseByteEncodedValue
class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) : BaseByteEncodedValue(), MutableEncodedValue {
private var value = byteEncodedValue.value
override fun getValue(): Byte {
return this.value
}
fun setValue(value: Byte) {
this.value = value
}
override fun getValue(): Byte = this.value
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue {
return MutableByteEncodedValue(this)
}
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}
}

View File

@@ -1,22 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.base.value.BaseCharEncodedValue
import org.jf.dexlib2.iface.value.CharEncodedValue
import com.android.tools.smali.dexlib2.base.value.BaseCharEncodedValue
class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) : BaseCharEncodedValue(), MutableEncodedValue {
private var value = charEncodedValue.value
override fun getValue(): Char {
return this.value
}
fun setValue(value: Char) {
this.value = value
}
override fun getValue(): Char = this.value
companion object {
fun CharEncodedValue.toMutable(): MutableCharEncodedValue {
return MutableCharEncodedValue(this)
}
fun CharEncodedValue.toMutable(): MutableCharEncodedValue = MutableCharEncodedValue(this)
}
}

View File

@@ -0,0 +1,19 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
BaseDoubleEncodedValue(),
MutableEncodedValue {
private var value = doubleEncodedValue.value
fun setValue(value: Double) {
this.value = value
}
override fun getValue(): Double = this.value
companion object {
fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue = MutableDoubleEncodedValue(this)
}
}

View File

@@ -1,7 +1,6 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.ValueType
import org.jf.dexlib2.iface.value.*
import com.android.tools.smali.dexlib2.ValueType
interface MutableEncodedValue : EncodedValue {
companion object {
@@ -29,4 +28,4 @@ interface MutableEncodedValue : EncodedValue {
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseEnumEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) : BaseEnumEncodedValue(), MutableEncodedValue {
private var value = enumEncodedValue.value
fun setValue(value: FieldReference) {
this.value = value
}
override fun getValue(): FieldReference = this.value
companion object {
fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue = MutableEnumEncodedValue(this)
}
}

View File

@@ -0,0 +1,21 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.ValueType
import com.android.tools.smali.dexlib2.base.value.BaseFieldEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) : BaseFieldEncodedValue(), MutableEncodedValue {
private var value = fieldEncodedValue.value
fun setValue(value: FieldReference) {
this.value = value
}
override fun getValueType(): Int = ValueType.FIELD
override fun getValue(): FieldReference = this.value
companion object {
fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue = MutableFieldEncodedValue(this)
}
}

View File

@@ -1,22 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.base.value.BaseFloatEncodedValue
import org.jf.dexlib2.iface.value.FloatEncodedValue
import com.android.tools.smali.dexlib2.base.value.BaseFloatEncodedValue
class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) : BaseFloatEncodedValue(), MutableEncodedValue {
private var value = floatEncodedValue.value
override fun getValue(): Float {
return this.value
}
fun setValue(value: Float) {
this.value = value
}
override fun getValue(): Float = this.value
companion object {
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue {
return MutableFloatEncodedValue(this)
}
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue = MutableFloatEncodedValue(this)
}
}
}

View File

@@ -1,22 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.base.value.BaseIntEncodedValue
import org.jf.dexlib2.iface.value.IntEncodedValue
import com.android.tools.smali.dexlib2.base.value.BaseIntEncodedValue
class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) : BaseIntEncodedValue(), MutableEncodedValue {
private var value = intEncodedValue.value
override fun getValue(): Int {
return this.value
}
fun setValue(value: Int) {
this.value = value
}
override fun getValue(): Int = this.value
companion object {
fun IntEncodedValue.toMutable(): MutableIntEncodedValue {
return MutableIntEncodedValue(this)
}
fun IntEncodedValue.toMutable(): MutableIntEncodedValue = MutableIntEncodedValue(this)
}
}
}

View File

@@ -1,22 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.base.value.BaseLongEncodedValue
import org.jf.dexlib2.iface.value.LongEncodedValue
import com.android.tools.smali.dexlib2.base.value.BaseLongEncodedValue
class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) : BaseLongEncodedValue(), MutableEncodedValue {
private var value = longEncodedValue.value
override fun getValue(): Long {
return this.value
}
fun setValue(value: Long) {
this.value = value
}
override fun getValue(): Long = this.value
companion object {
fun LongEncodedValue.toMutable(): MutableLongEncodedValue {
return MutableLongEncodedValue(this)
}
fun LongEncodedValue.toMutable(): MutableLongEncodedValue = MutableLongEncodedValue(this)
}
}

View File

@@ -0,0 +1,20 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
BaseMethodEncodedValue(),
MutableEncodedValue {
private var value = methodEncodedValue.value
fun setValue(value: MethodReference) {
this.value = value
}
override fun getValue(): MethodReference = this.value
companion object {
fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue = MutableMethodEncodedValue(this)
}
}

View File

@@ -0,0 +1,20 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodHandleEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodHandleReference
class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) :
BaseMethodHandleEncodedValue(),
MutableEncodedValue {
private var value = methodHandleEncodedValue.value
fun setValue(value: MethodHandleReference) {
this.value = value
}
override fun getValue(): MethodHandleReference = this.value
companion object {
fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue = MutableMethodHandleEncodedValue(this)
}
}

View File

@@ -0,0 +1,18 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), MutableEncodedValue {
private var value = methodTypeEncodedValue.value
fun setValue(value: MethodProtoReference) {
this.value = value
}
override fun getValue(): MethodProtoReference = this.value
companion object {
fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue = MutableMethodTypeEncodedValue(this)
}
}

View File

@@ -0,0 +1,9 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
class MutableNullEncodedValue : BaseNullEncodedValue(), MutableEncodedValue {
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}

View File

@@ -0,0 +1,19 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseShortEncodedValue
class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) :
BaseShortEncodedValue(),
MutableEncodedValue {
private var value = shortEncodedValue.value
fun setValue(value: Short) {
this.value = value
}
override fun getValue(): Short = this.value
companion object {
fun ShortEncodedValue.toMutable(): MutableShortEncodedValue = MutableShortEncodedValue(this)
}
}

View File

@@ -0,0 +1,19 @@
package com.android.tools.smali.dexlib2.iface.value
import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
BaseStringEncodedValue(),
MutableEncodedValue {
private var value = stringEncodedValue.value
fun setValue(value: String) {
this.value = value
}
override fun getValue(): String = this.value
companion object {
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
}
}

View File

@@ -1,22 +1,17 @@
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
package com.android.tools.smali.dexlib2.iface.value
import org.jf.dexlib2.base.value.BaseTypeEncodedValue
import org.jf.dexlib2.iface.value.TypeEncodedValue
import com.android.tools.smali.dexlib2.base.value.BaseTypeEncodedValue
class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) : BaseTypeEncodedValue(), MutableEncodedValue {
private var value = typeEncodedValue.value
override fun getValue(): String {
return this.value
}
fun setValue(value: String) {
this.value = value
}
override fun getValue() = this.value
companion object {
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue {
return MutableTypeEncodedValue(this)
}
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue = MutableTypeEncodedValue(this)
}
}

View File

@@ -0,0 +1,21 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.BaseAnnotation
import com.android.tools.smali.dexlib2.iface.Annotation
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
class MutableAnnotation(annotation: Annotation) : BaseAnnotation() {
private val visibility = annotation.visibility
private val type = annotation.type
private val _elements by lazy { annotation.elements.map { element -> element.toMutable() }.toMutableSet() }
override fun getType() = type
override fun getElements() = _elements
override fun getVisibility() = visibility
companion object {
fun Annotation.toMutable() = MutableAnnotation(this)
}
}

View File

@@ -0,0 +1,27 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
import com.android.tools.smali.dexlib2.iface.AnnotationElement
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() {
private var name = annotationElement.name
private var value = annotationElement.value.toMutable()
fun setName(name: String) {
this.name = name
}
fun setValue(value: MutableEncodedValue) {
this.value = value
}
override fun getName() = name
override fun getValue() = value
companion object {
fun AnnotationElement.toMutable() = MutableAnnotationElement(this)
}
}

View File

@@ -0,0 +1,76 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
import com.android.tools.smali.dexlib2.iface.ClassDef
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import com.android.tools.smali.dexlib2.util.FieldUtil
import com.android.tools.smali.dexlib2.util.MethodUtil
class MutableClassDef(classDef: ClassDef) : BaseTypeReference(), ClassDef {
// Class
private var type = classDef.type
private var sourceFile = classDef.sourceFile
private var accessFlags = classDef.accessFlags
private var superclass = classDef.superclass
private val _interfaces by lazy { classDef.interfaces.toMutableList() }
private val _annotations by lazy {
classDef.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
}
// Methods
private val _methods by lazy { classDef.methods.map { method -> method.toMutable() }.toMutableSet() }
private val _directMethods by lazy { methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
private val _virtualMethods by lazy { methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
// Fields
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
private val _staticFields by lazy { _fields.filter { field -> FieldUtil.isStatic(field) }.toMutableSet() }
private val _instanceFields by lazy { _fields.filter { field -> !FieldUtil.isStatic(field) }.toMutableSet() }
fun setType(type: String) {
this.type = type
}
fun setSourceFile(sourceFile: String?) {
this.sourceFile = sourceFile
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setSuperClass(superclass: String?) {
this.superclass = superclass
}
override fun getType() = type
override fun getAccessFlags() = accessFlags
override fun getSourceFile() = sourceFile
override fun getSuperclass() = superclass
override fun getInterfaces() = _interfaces
override fun getAnnotations() = _annotations
override fun getStaticFields() = _staticFields
override fun getInstanceFields() = _instanceFields
override fun getFields() = _fields
override fun getDirectMethods() = _directMethods
override fun getVirtualMethods() = _virtualMethods
override fun getMethods() = _methods
companion object {
fun ClassDef.toMutable(): MutableClassDef = MutableClassDef(this)
}
}

View File

@@ -0,0 +1,56 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.reference.BaseFieldReference
import com.android.tools.smali.dexlib2.iface.Field
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
class MutableField(field: Field) : BaseFieldReference(), Field {
private var definingClass = field.definingClass
private var name = field.name
private var type = field.type
private var accessFlags = field.accessFlags
private var initialValue = field.initialValue?.toMutable()
private val _annotations by lazy { field.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _hiddenApiRestrictions by lazy { field.hiddenApiRestrictions }
fun setDefiningClass(definingClass: String) {
this.definingClass = definingClass
}
fun setName(name: String) {
this.name = name
}
fun setType(type: String) {
this.type = type
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setInitialValue(initialValue: MutableEncodedValue?) {
this.initialValue = initialValue
}
override fun getDefiningClass() = this.definingClass
override fun getName() = this.name
override fun getType() = this.type
override fun getAnnotations() = this._annotations
override fun getAccessFlags() = this.accessFlags
override fun getHiddenApiRestrictions() = this._hiddenApiRestrictions
override fun getInitialValue() = this.initialValue
companion object {
fun Field.toMutable() = MutableField(this)
}
}

View File

@@ -0,0 +1,63 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
import com.android.tools.smali.dexlib2.mutable.MutableMethodParameter.Companion.toMutable
class MutableMethod(method: Method) : BaseMethodReference(), Method {
private var definingClass = method.definingClass
private var name = method.name
private var accessFlags = method.accessFlags
private var returnType = method.returnType
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count).
private var implementation = method.implementation?.let(::MutableMethodImplementation)
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions }
fun setDefiningClass(definingClass: String) {
this.definingClass = definingClass
}
fun setName(name: String) {
this.name = name
}
fun setAccessFlags(accessFlags: Int) {
this.accessFlags = accessFlags
}
fun setReturnType(returnType: String) {
this.returnType = returnType
}
fun setImplementation(implementation: MutableMethodImplementation?) {
this.implementation = implementation
}
override fun getDefiningClass() = definingClass
override fun getName() = name
override fun getParameterTypes() = _parameterTypes
override fun getReturnType() = returnType
override fun getAnnotations() = _annotations
override fun getAccessFlags() = accessFlags
override fun getHiddenApiRestrictions() = _hiddenApiRestrictions
override fun getParameters() = _parameters
override fun getImplementation() = implementation
companion object {
fun Method.toMutable() = MutableMethod(this)
}
}

View File

@@ -0,0 +1,26 @@
package com.android.tools.smali.dexlib2.mutable
import com.android.tools.smali.dexlib2.base.BaseMethodParameter
import com.android.tools.smali.dexlib2.iface.MethodParameter
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
class MutableMethodParameter(parameter: MethodParameter) : BaseMethodParameter(), MethodParameter {
private var type = parameter.type
private var name = parameter.name
private var signature = parameter.signature
private val _annotations by lazy {
parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
}
override fun getType() = type
override fun getName() = name
override fun getSignature() = signature
override fun getAnnotations() = _annotations
companion object {
fun MethodParameter.toMutable() = MutableMethodParameter(this)
}
}

View File

@@ -0,0 +1,17 @@
package java.io
internal expect fun File.kmpResolve(child: String): File
internal fun File.resolve(child: String) = kmpResolve(child)
internal expect fun File.kmpDeleteRecursively(): Boolean
internal fun File.deleteRecursively() = kmpDeleteRecursively()
internal expect fun File.kmpInputStream(): InputStream
internal fun File.inputStream() = kmpInputStream()
internal expect fun File.kmpBufferedWriter(charset: java.nio.charset.Charset): BufferedWriter
internal fun File.bufferedWriter(charset: java.nio.charset.Charset) = kmpBufferedWriter(charset)

View File

@@ -0,0 +1,32 @@
package app.revanced.patcher.patch
import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
/**
* Loads patches from JAR files declared as public static fields
* or returned by public static and non-parametrized methods.
* Patches with no name are not loaded. If a patches file fails to load,
* the [onFailedToLoad] callback is invoked with the file and the throwable
* and the loading continues for the other files.
*
* @param patchesFiles The JAR files to load the patches from.
* @param onFailedToLoad A callback invoked when a patches file fails to load.
*
* @return The loaded patches.
*/
actual fun loadPatches(
vararg patchesFiles: File,
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit,
) = loadPatches(
patchesFiles = patchesFiles,
{ file ->
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
.map { it.name.substringBeforeLast('.').replace('/', '.') }
},
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
onFailedToLoad = onFailedToLoad
)

View File

@@ -0,0 +1,7 @@
package collections
internal actual fun <K, V> MutableMap<K, V>.kmpMerge(
key: K,
value: V,
remappingFunction: (oldValue: V, newValue: V) -> V
) = merge(key, value, remappingFunction)

View File

@@ -0,0 +1,9 @@
package java.io
import java.nio.charset.Charset
internal actual fun File.kmpResolve(child: String) = resolve(child)
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
internal actual fun File.kmpInputStream() = inputStream()
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)

View File

@@ -0,0 +1,27 @@
package app.revanced.patcher
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class FingerprintTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `matches fingerprints correctly`() {
with(bytecodePatchContext) {
assertNotNull(
fingerprint { returns("V") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
assertNull(
fingerprint { returns("does not exist") }.originalMethodOrNull,
"Fingerprints should match correctly."
)
}
}
}

View File

@@ -0,0 +1,165 @@
package app.revanced.patcher
import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethod
import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethodDeclarativelyOrNull
import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertDoesNotThrow
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class MatchingTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `finds via builder api`() {
fun firstMethodComposite(fail: Boolean = false) = firstMethodComposite {
name("method")
definingClass("class")
if (fail) returnType("doesnt exist")
instructions(
at(Opcode.CONST_STRING()),
`is`<TwoRegisterInstruction>(),
noneOf(registers()),
string("test", String::contains),
after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))),
allOf(),
type("PrintStream;", String::endsWith)
)
}
with(bytecodePatchContext) {
assertNotNull(firstMethodComposite().methodOrNull) { "Expected to find a method" }
Assertions.assertNull(firstMethodComposite(fail = true).immutableMethodOrNull) { "Expected to not find a method" }
Assertions.assertNotNull(
firstMethodComposite().match(classDefs.first()).methodOrNull
) { "Expected to find a method matching in a specific class" }
}
}
@Test
fun `finds via declarative api`() {
bytecodePatch {
apply {
val method = firstMethodDeclarativelyOrNull {
anyOf {
predicate { name == "method" }
add { false }
}
allOf {
predicate { returnType == "V" }
}
predicate { definingClass == "class" }
}
assertNotNull(method) { "Expected to find a method" }
}
}()
}
@Test
fun `predicate api works correctly`() {
bytecodePatch {
apply {
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
}
}
}
@Test
fun `matcher finds indices correctly`() {
val iterable = (1..10).toList()
val matcher = indexedMatcher<Int>()
matcher.apply {
+at<Int> { this > 5 }
}
assertFalse(
matcher(iterable),
"Should not match at any other index than first"
)
matcher.clear()
matcher.apply { +at<Int> { this == 1 } }(iterable)
assertEquals(
listOf(0),
matcher.indices,
"Should match at first index."
)
matcher.clear()
matcher.apply { add { _, _, _ -> this > 0 } }(iterable)
assertEquals(1, matcher.indices.size, "Should only match once.")
matcher.clear()
matcher.apply { add { _, _, _ -> this == 2 } }(iterable)
assertEquals(
listOf(1),
matcher.indices,
"Should find the index correctly."
)
matcher.clear()
matcher.apply {
+at<Int> { this == 1 }
add { _, _, _ -> this == 2 }
add { _, _, _ -> this == 4 }
}(iterable)
assertEquals(
listOf(0, 1, 3),
matcher.indices,
"Should match 1, 2 and 4 at indices 0, 1 and 3."
)
matcher.clear()
matcher.apply {
+after<Int> { this == 1 }
}(iterable)
assertEquals(
listOf(0),
matcher.indices,
"Should match index 0 after nothing"
)
matcher.clear()
matcher.apply {
+after<Int>(2..Int.MAX_VALUE) { this == 1 }
}
assertFalse(
matcher(iterable),
"Should not match, because 1 is out of range"
)
matcher.clear()
matcher.apply {
+after<Int>(1..1) { this == 2 }
}
assertFalse(
matcher(iterable),
"Should not match, because 2 is at index 1"
)
matcher.clear()
matcher.apply {
+at<Int> { this == 1 }
+after<Int>(2..5) { this == 4 }
add { _, _, _ -> this == 8 }
add { _, _, _ -> this == 9 }
}(iterable)
assertEquals(
listOf(0, 3, 7, 8),
matcher.indices,
"Should match indices correctly."
)
}
}

View File

@@ -0,0 +1,80 @@
package app.revanced.patcher
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
import kotlin.test.Test
import kotlin.test.assertEquals
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class PatcherTest : PatcherTestBase() {
@BeforeAll
fun setup() = setupMock()
@Test
fun `applies patches in correct order`() {
val applied = mutableListOf<String>()
infix fun Patch.resultsIn(equals: List<String>) = this to equals
infix fun Pair<Patch, List<String>>.because(reason: String) {
runCatching { setOf(first)() }
assertEquals(second, applied, reason)
applied.clear()
}
bytecodePatch {
dependsOn(
bytecodePatch {
apply { applied += "1" }
afterDependents { applied += "-2" }
},
bytecodePatch { apply { applied += "2" } },
)
apply { applied += "3" }
afterDependents { applied += "-1" }
} resultsIn listOf("1", "2", "3", "-1", "-2") because
"Patches should apply in post-order and afterDependents in pre-order."
bytecodePatch {
dependsOn(
bytecodePatch {
apply { throw PatchException("1") }
afterDependents { applied += "-2" }
},
)
apply { applied += "2" }
afterDependents { applied += "-1" }
} resultsIn emptyList() because
"Patches that depend on a patched that failed to apply should not be applied."
bytecodePatch {
dependsOn(
bytecodePatch {
apply { applied += "1" }
afterDependents { applied += "-2" }
},
)
apply { throw PatchException("2") }
afterDependents { applied += "-1" }
} resultsIn listOf("1", "-2") because
"afterDependents of a patch should not be called if it failed to apply."
bytecodePatch {
dependsOn(
bytecodePatch {
apply { applied += "1" }
afterDependents { applied += "-2" }
},
)
apply { applied += "2" }
afterDependents { throw PatchException("-1") }
} resultsIn listOf("1", "2", "-2") because
"afterDependents of a patch should be called " +
"regardless of dependant patches failing."
}
}

View File

@@ -0,0 +1,105 @@
package app.revanced.patcher
import app.revanced.patcher.extensions.toInstructions
import app.revanced.patcher.patch.BytecodePatchContext
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.ResourcePatchContext
import com.android.tools.smali.dexlib2.Opcodes
import com.android.tools.smali.dexlib2.iface.DexFile
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
import io.mockk.every
import io.mockk.justRun
import io.mockk.mockk
import io.mockk.mockkStatic
import lanchon.multidexlib2.MultiDexIO
import java.io.File
import java.io.InputStream
abstract class PatcherTestBase {
protected lateinit var bytecodePatchContext: BytecodePatchContext
protected lateinit var resourcePatchContext: ResourcePatchContext
protected fun setupMock(
method: ImmutableMethod = ImmutableMethod(
"class",
"method",
emptyList(),
"V",
0,
null,
null,
ImmutableMethodImplementation(
2,
"""
const-string v0, "Hello, World!"
iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
return-void
const-string v0, "This is a test."
return-object v0
invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J
check-cast p0, Ljava/io/PrintStream;
""".toInstructions(),
null,
null
),
),
) {
resourcePatchContext = mockk<ResourcePatchContext>(relaxed = true)
bytecodePatchContext = mockk<BytecodePatchContext> bytecodePatchContext@{
mockkStatic(MultiDexIO::readDexFile)
every {
MultiDexIO.readDexFile(
any(),
any(),
any(),
any(),
any()
)
} returns mockk<DexFile> {
every { classes } returns mutableSetOf(
ImmutableClassDef(
"class",
0,
null,
null,
null,
null,
null,
listOf(method),
)
)
every { opcodes } returns Opcodes.getDefault()
}
every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk<File>()
every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {
javaClass.getDeclaredMethod($$"initializeCache$patcher").apply {
isAccessible = true
}.invoke(this)
}
every { get() } returns emptySet()
justRun { this@bytecodePatchContext["extendWith"](any<InputStream>()) }
}
}
protected operator fun Set<Patch>.invoke() {
runCatching {
apply(
bytecodePatchContext,
resourcePatchContext
) { }
}.fold(
{ it.dexFiles },
{ it.printStackTrace() }
)
}
protected operator fun Patch.invoke() = setOf(this)()
}

View File

@@ -0,0 +1,253 @@
package app.revanced.patcher.extensions
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import kotlin.test.assertEquals
internal class MethodExtensionsTest {
private val testInstructions = (0..9).map { i -> TestInstruction(i) }
private var method = ImmutableMethod(
"TestClass;",
"testMethod",
null,
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(16)
).toMutable()
@BeforeEach
fun setup() {
method.instructions.clear()
method.addInstructions(testInstructions)
}
@Test
fun addInstructionsToImplementationIndexed() =
applyToImplementation {
addInstructions(5, getTestInstructions(5..6)).also {
assertRegisterIs(5, 5)
assertRegisterIs(6, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addInstructionsToImplementation() =
applyToImplementation {
addInstructions(getTestInstructions(10..11)).also {
assertRegisterIs(10, 10)
assertRegisterIs(11, 11)
}
}
@Test
fun removeInstructionsFromImplementationIndexed() =
applyToImplementation {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
}
@Test
fun removeInstructionsFromImplementation() =
applyToImplementation {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionsInImplementationIndexed() =
applyToImplementation {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
@Test
fun addInstructionToMethodIndexed() =
applyToMethod {
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun addInstructionToMethod() =
applyToMethod {
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addSmaliInstructionToMethodIndexed() =
applyToMethod {
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun addSmaliInstructionToMethod() =
applyToMethod {
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
}
@Test
fun addInstructionsToMethodIndexed() =
applyToMethod {
addInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addInstructionsToMethod() =
applyToMethod {
addInstructions(getTestInstructions(0..1)).also {
assertRegisterIs(0, 10)
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsToMethodIndexed() =
applyToMethod {
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 7)
}
}
@Test
fun addSmaliInstructionsToMethod() =
applyToMethod {
addInstructions(getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 10)
assertRegisterIs(1, 11)
assertRegisterIs(9, 9)
}
}
@Test
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() =
applyToMethod {
val label = ExternalLabel("testLabel", getInstruction(5))
addInstructionsWithLabels(
5,
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
label,
).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(5, 8)
val gotoTarget =
getInstruction<BuilderOffsetInstruction>(7)
.target.location.instruction as OneRegisterInstruction
assertEquals(5, gotoTarget.registerA)
}
}
@Test
fun removeInstructionFromMethodIndexed() =
applyToMethod {
removeInstruction(5).also {
assertRegisterIs(4, 4)
assertRegisterIs(6, 5)
}
}
@Test
fun removeInstructionsFromMethodIndexed() =
applyToMethod {
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
}
@Test
fun removeInstructionsFromMethod() =
applyToMethod {
removeInstructions(0).also { assertRegisterIs(9, 9) }
removeInstructions(1).also { assertRegisterIs(1, 0) }
removeInstructions(2).also { assertRegisterIs(3, 0) }
}
@Test
fun replaceInstructionInMethodIndexed() =
applyToMethod {
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
}
@Test
fun replaceInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
@Test
fun replaceSmaliInstructionsInMethodIndexed() =
applyToMethod {
replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
assertRegisterIs(0, 5)
assertRegisterIs(1, 6)
assertRegisterIs(7, 7)
}
}
// region Helper methods
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
method.implementation!!.apply(block)
}
private fun applyToMethod(block: MutableMethod.() -> Unit) {
method.apply(block)
}
private fun MutableMethodImplementation.assertRegisterIs(
register: Int,
atIndex: Int,
) = assertEquals(
register,
getInstruction<OneRegisterInstruction>(atIndex).registerA,
)
private fun MutableMethod.assertRegisterIs(
register: Int,
atIndex: Int,
) = implementation!!.assertRegisterIs(register, atIndex)
private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
private fun getTestSmaliInstructions(range: IntRange) =
range.joinToString("\n") {
getTestSmaliInstruction(it)
}
// endregion
private class TestInstruction(register: Int) : BuilderInstruction21s(Opcode.CONST_16, register, 0)
}

View File

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

View File

@@ -0,0 +1,85 @@
package app.revanced.patcher.patch
import kotlin.reflect.jvm.javaField
import kotlin.test.Test
import kotlin.test.assertEquals
internal object PatchTest {
@Test
fun `can create patch with name`() {
val patch = bytecodePatch(name = "Test") {}
assertEquals("Test", patch.name)
}
@Test
fun `can create patch with compatible packages`() {
val patch = bytecodePatch(name = "Test") {
compatibleWith(
"compatible.package"("1.0.0"),
)
}
assertEquals(1, patch.compatiblePackages!!.size)
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
}
@Test
fun `can create patch with dependencies`() {
val patch = bytecodePatch(name = "Test") {
dependsOn(resourcePatch {})
}
assertEquals(1, patch.dependencies.size)
}
@Test
fun `can create patch with options`() {
val patch = bytecodePatch(name = "Test") {
val print by stringOption("print")
val custom = option<String>("custom")()
this.apply {
println(print)
println(custom.value)
}
}
assertEquals(2, patch.options.size)
}
@Test
fun `loads patches correctly`() {
val patchesClass = ::Public.javaField!!.declaringClass.name
val classLoader = ::Public.javaClass.classLoader
val patches = getPatches(listOf(patchesClass), classLoader)
assertEquals(
2,
patches.size,
"Expected 2 patches to be loaded, " +
"because there's only two named patches declared as public static fields " +
"or returned by public static and non-parametrized methods.",
)
}
}
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
val Public by creatingBytecodePatch {} // Loaded, because it's named.
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
publicNamedPatchFunction() // Not loaded, because it's parameterized.
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.

View File

@@ -0,0 +1,76 @@
package app.revanced.patcher.util
import app.revanced.patcher.extensions.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
import org.junit.jupiter.api.BeforeEach
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
internal class SmaliTest {
val method = ImmutableMethod(
"Ldummy;",
"name",
emptyList(), // parameters
"V",
AccessFlags.PUBLIC.value,
null,
null,
MutableMethodImplementation(1),
).toMutable()
@BeforeEach
fun setup() {
method.instructions.clear()
}
@Test
fun `own branches work`() {
method.addInstructionsWithLabels(
0,
"""
:test
const/4 v0, 0x1
if-eqz v0, :test
""",
)
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(1).target.location.index
assertEquals(0, targetLocationIndex, "Label should point to index 0")
}
@Test
fun `external branches work`() {
val instructionIndex = 3
val labelIndex = 1
method.addInstructions(
"""
const/4 v0, 0x1
const/4 v0, 0x0
""",
)
method.addInstructionsWithLabels(
method.instructions.size,
"""
const/4 v0, 0x1
if-eqz v0, :test
return-void
""",
ExternalLabel("test", method.getInstruction(1)),
)
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
assertTrue(instruction.target.isPlaced, "Label should be placed")
assertEquals(labelIndex, instruction.target.location.index)
}
}

View File

@@ -1 +1,24 @@
rootProject.name = "revanced-patcher"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
mavenCentral()
google()
}
}
dependencyResolutionManagement {
repositories {
mavenCentral()
google()
maven {
name = "githubPackages"
// A repository must be specified for some reason. "registry" is a dummy.
url = uri("https://maven.pkg.github.com/revanced/registry")
credentials(PasswordCredentials::class)
}
}
}
include(":patcher")

View File

@@ -1,417 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolveUsingLookupMap
import app.revanced.patcher.patch.*
import app.revanced.patcher.util.VersionReader
import brut.androlib.Androlib
import brut.androlib.meta.UsesFramework
import brut.androlib.options.BuildOptions
import brut.androlib.res.AndrolibResources
import brut.androlib.res.data.ResPackage
import brut.androlib.res.decoder.AXmlResourceParser
import brut.androlib.res.decoder.ResAttrDecoder
import brut.androlib.res.decoder.XmlPullStreamDecoder
import brut.androlib.res.xml.ResXmlPatcher
import brut.directory.ExtFile
import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.Closeable
import java.io.File
import java.io.OutputStream
import java.nio.file.Files
internal val NAMER = BasicDexFileNamer()
/**
* The ReVanced Patcher.
* @param options The options for the patcher.
*/
class Patcher(private val options: PatcherOptions) {
private val logger = options.logger
private val opcodes: Opcodes
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
private var mergeIntegrations = false
val context: PatcherContext
companion object {
@JvmStatic
val version = VersionReader.read()
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
this.aaptPath = options.aaptPath
this.useAapt2 = true
this.frameworkFolderLocation = options.frameworkFolderLocation
}
}
init {
logger.info("Reading dex files")
// read dex files
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
// get the opcodes
opcodes = dexFile.opcodes
// finally create patcher context
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
// decode manifest file
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
}
/**
* Add integrations to be merged by the patcher.
* The integrations will only be merged, if necessary.
*
* @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files.
* @param callback The callback for [integrations] which are being added.
*/
fun addIntegrations(
integrations: List<File>,
callback: (File) -> Unit
) {
context.integrations.apply integrations@{
add(integrations)
this@integrations.callback = callback
}
}
/**
* Save the patched dex file.
*/
fun save(): PatcherResult {
val packageMetadata = context.packageMetadata
val metaInfo = packageMetadata.metaInfo
var resourceFile: File? = null
when (resourceDecodingMode) {
ResourceDecodingMode.FULL -> {
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
try {
val androlibResources = AndrolibResources().also { resources ->
resources.buildOptions = BuildOptions().also { buildOptions ->
buildOptions.setBuildOptions(options)
buildOptions.isFramework = metaInfo.isFrameworkApk
buildOptions.resourcesAreCompressed = metaInfo.compressionType
buildOptions.doNotCompress = metaInfo.doNotCompress
}
resources.setSdkInfo(metaInfo.sdkInfo)
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
}
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
val aaptFile = cacheDirectory.resolve("aapt_temp_file")
// delete if it exists
Files.deleteIfExists(aaptFile.toPath())
val resDirectory = cacheDirectory.resolve("res")
val includedFiles = metaInfo.usesFramework.ids.map { id ->
androlibResources.getFrameworkApk(
id, metaInfo.usesFramework.tag
)
}.toTypedArray()
logger.info("Compiling resources")
androlibResources.aaptPackage(
aaptFile, manifestFile, resDirectory, null, null, includedFiles
)
resourceFile = aaptFile
} finally {
cacheDirectory.close()
}
}
else -> logger.info("Not compiling resources because resource patching is not required")
}
logger.trace("Creating new dex file")
val newDexFile = object : DexFile {
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
override fun getOpcodes() = this@Patcher.opcodes
}
// write modified dex files
logger.info("Writing modified dex files")
val dexFiles = mutableMapOf<String, MemoryDataStore>()
MultiDexIO.writeDexFile(
true, -1, // core count
dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
)
return PatcherResult(
dexFiles.map {
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
},
metaInfo.doNotCompress?.toList(),
resourceFile
)
}
/**
* Add [Patch]es to the patcher.
* @param patches [Patch]es The patches to add.
*/
fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
/**
* Returns true if at least one patches or its dependencies matches the given predicate.
*/
fun Class<out Patch<Context>>.anyRecursively(predicate: (Class<out Patch<Context>>) -> Boolean): Boolean =
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
// Determine if resource patching is required.
for (patch in patches) {
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
resourceDecodingMode = ResourceDecodingMode.FULL
break
}
}
// Determine if merging integrations is required.
for (patch in patches) {
if (patch.anyRecursively { it.requiresIntegrations }) {
mergeIntegrations = true
break
}
}
context.patches.addAll(patches)
}
/**
* Decode resources for the patcher.
*
* @param mode The [ResourceDecodingMode] to use when decoding.
*/
private fun decodeResources(mode: ResourceDecodingMode) {
val extInputFile = ExtFile(options.inputFile)
try {
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
val resourceTable = androlib.getResTable(extInputFile, true)
when (mode) {
ResourceDecodingMode.FULL -> {
val outDir = File(options.resourceCacheDirectory)
if (outDir.exists()) {
logger.info("Deleting existing resource cache directory")
if (!outDir.deleteRecursively()) {
logger.error("Failed to delete existing resource cache directory")
}
}
outDir.mkdirs()
logger.info("Decoding resources")
// decode resources to cache directory
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
// read additional metadata from the resource table
context.packageMetadata.let { metadata ->
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
}
// read files to not compress
metadata.metaInfo.doNotCompress = buildList {
androlib.recordUncompressedFiles(extInputFile, this)
}
}
}
ResourceDecodingMode.MANIFEST_ONLY -> {
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
// create decoder for the resource table
val decoder = ResAttrDecoder()
decoder.currentPackage = ResPackage(resourceTable, 0, null)
// create xml parser with the decoder
val axmlParser = AXmlResourceParser()
axmlParser.attrDecoder = decoder
// parse package information with the decoder and parser which will set required values in the resource table
// instead of decodeManifest another more low level solution can be created to make it faster/better
XmlPullStreamDecoder(
axmlParser, AndrolibResources().resXmlSerializer
).decodeManifest(
extInputFile.directory.getFileInput("AndroidManifest.xml"),
// Older Android versions do not support OutputStream.nullOutputStream()
object : OutputStream() {
override fun write(b: Int) {
// do nothing
}
}
)
}
}
// read of the resourceTable which is created by reading the manifest file
context.packageMetadata.let { metadata ->
metadata.packageName = resourceTable.currentResPackage.name
metadata.packageVersion = resourceTable.versionInfo.versionName ?: resourceTable.versionInfo.versionCode
metadata.metaInfo.versionInfo = resourceTable.versionInfo
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
}
} finally {
extInputFile.close()
}
}
/**
* Execute patches added the patcher.
*
* @param stopOnError If true, the patches will stop on the first error.
* @return A pair of the name of the [Patch] and its [PatchResult].
*/
fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
/**
* Execute a [Patch] and its dependencies recursively.
*
* @param patchClass The [Patch] to execute.
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
* @return The result of executing the [Patch].
*/
fun executePatch(
patchClass: Class<out Patch<Context>>,
executedPatches: LinkedHashMap<String, ExecutedPatch>
): PatchResult {
val patchName = patchClass.patchName
// if the patch has already applied silently skip it
if (executedPatches.contains(patchName)) {
if (!executedPatches[patchName]!!.success)
return PatchResultError("'$patchName' did not succeed previously")
logger.trace("Skipping '$patchName' because it has already been applied")
return PatchResultSuccess()
}
// recursively execute all dependency patches
patchClass.dependencies?.forEach { dependencyClass ->
val dependency = dependencyClass.java
val result = executePatch(dependency, executedPatches)
if (result.isSuccess()) return@forEach
return PatchResultError(
"'$patchName' depends on '${dependency.patchName}' but the following error was raised: " +
result.error()!!.let { it.cause?.stackTraceToString() ?: it.message }
)
}
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
// TODO: implement this in a more polymorphic way
val patchContext = if (isResourcePatch) {
context.resourceContext
} else {
context.bytecodeContext.also { context ->
(patchInstance as BytecodePatch).fingerprints?.resolveUsingLookupMap(context)
}
}
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
return try {
patchInstance.execute(patchContext).also {
executedPatches[patchName] = ExecutedPatch(patchInstance, it.isSuccess())
}
} catch (e: Exception) {
PatchResultError(e).also {
executedPatches[patchName] = ExecutedPatch(patchInstance, false)
}
}
}
return sequence {
if (mergeIntegrations) context.integrations.merge(logger)
logger.trace("Initialize lookup maps for method MethodFingerprint resolution")
MethodFingerprint.initializeFingerprintResolutionLookupMaps(context.bytecodeContext)
// prevent from decoding the manifest twice if it is not needed
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
logger.info("Executing patches")
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
context.patches.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
// TODO: This prints before the patch really finishes in case it is a Closeable
// because the Closeable is closed after all patches are executed.
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) return@sequence
}
executedPatches.values
.filter(ExecutedPatch::success)
.map(ExecutedPatch::patchInstance)
.filterIsInstance(Closeable::class.java)
.asReversed().forEach {
try {
it.close()
} catch (exception: Exception) {
val patchName = (it as Patch<Context>).javaClass.patchName
logger.error("Failed to close '$patchName': ${exception.stackTraceToString()}")
yield(patchName to Result.failure(exception))
// This is not failsafe. If a patch throws an exception while closing,
// the other patches that depend on it may fail.
if (stopOnError) return@sequence
}
}
}
}
/**
* The type of decoding the resources.
*/
private enum class ResourceDecodingMode {
/**
* Decode all resources.
*/
FULL,
/**
* Decode the manifest file only.
*/
MANIFEST_ONLY,
}
}
/**
* A result of executing a [Patch].
*
* @param patchInstance The instance of the [Patch] that was applied.
* @param success The result of the [Patch].
*/
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)

View File

@@ -1,64 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.data.*
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.ClassMerger.merge
import org.jf.dexlib2.iface.ClassDef
import java.io.File
data class PatcherContext(
val classes: MutableList<ClassDef>,
val resourceCacheDirectory: File,
) {
val packageMetadata = PackageMetadata()
internal val patches = mutableListOf<Class<out Patch<Context>>>()
internal val integrations = Integrations(this)
internal val bytecodeContext = BytecodeContext(classes)
internal val resourceContext = ResourceContext(resourceCacheDirectory)
internal class Integrations(val context: PatcherContext) {
var callback: ((File) -> Unit)? = null
private val integrations: MutableList<File> = mutableListOf()
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
/**
* Merge integrations.
* @param logger A logger.
*/
fun merge(logger: Logger) {
with(context.bytecodeContext.classes) {
for (integrations in integrations) {
callback?.let { it(integrations) }
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
true,
integrations,
NAMER,
null,
null
).classes) {
val type = classDef.type
val result = classes.findIndexed { it.type == type }
if (result == null) {
logger.trace("Merging type $type")
classes.add(classDef)
continue
}
val (existingClass, existingClassIndex) = result
logger.trace("Type $type exists. Adding missing methods and fields.")
existingClass.merge(classDef, context, logger).let { mergedClass ->
if (mergedClass !== existingClass) // referential equality check
classes[existingClassIndex] = mergedClass
}
}
}
}
}
}
}

View File

@@ -1,21 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.logging.Logger
import app.revanced.patcher.logging.impl.NopLogger
import java.io.File
/**
* Options for the [Patcher].
* @param inputFile The input file (usually an apk file).
* @param resourceCacheDirectory Directory to cache resources.
* @param aaptPath Optional path to a custom aapt binary.
* @param frameworkFolderLocation Optional path to a custom framework folder.
* @param logger Custom logger implementation for the [Patcher].
*/
data class PatcherOptions(
internal val inputFile: File,
internal val resourceCacheDirectory: String,
internal val aaptPath: String? = null,
internal val frameworkFolderLocation: String? = null,
internal val logger: Logger = NopLogger
)

View File

@@ -1,16 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.util.dex.DexFile
import java.io.File
/**
* The result of a patcher.
* @param dexFiles The patched dex files.
* @param doNotCompress List of relative paths to files to exclude from compressing.
* @param resourceFile File containing resources that need to be extracted into the APK.
*/
data class PatcherResult(
val dexFiles: List<DexFile>,
val doNotCompress: List<String>? = null,
val resourceFile: File?
)

View File

@@ -1,23 +0,0 @@
package app.revanced.patcher.annotation
import app.revanced.patcher.patch.Patch
/**
* Annotation to constrain a [Patch] to compatible packages.
* @param compatiblePackages A list of packages a [Patch] is compatible with.
*/
@Target(AnnotationTarget.CLASS)
annotation class Compatibility(
val compatiblePackages: Array<Package>,
)
/**
* Annotation to represent packages a patch can be compatible with.
* @param name The package identifier name.
* @param versions The versions of the package the [Patch] is compatible with.
*/
@Target()
annotation class Package(
val name: String,
val versions: Array<String> = [],
)

Some files were not shown because too many files have changed in this diff Show More