Compare commits

...

126 Commits

Author SHA1 Message Date
semantic-release-bot
2432fde6bf chore(release): 7.1.0-dev.2 [skip ci]
# [7.1.0-dev.2](https://github.com/revanced/revanced-patcher/compare/v7.1.0-dev.1...v7.1.0-dev.2) (2023-05-05)

### Features

* add overload to get instruction as type ([49c173d](49c173dc14))
2023-05-05 21:38:29 +00:00
oSumAtrIX
49c173dc14 feat: add overload to get instruction as type 2023-05-05 23:36:30 +02:00
oSumAtrIX
d83e9372bb chore: update gradle and dependencies 2023-04-30 05:27:22 +02:00
semantic-release-bot
7e8cd3bede chore(release): 7.1.0-dev.1 [skip ci]
# [7.1.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v7.0.0...v7.1.0-dev.1) (2023-04-30)

### Features

* add appreciation message for new contributors ([d674362](d67436271d))
2023-04-30 01:16:45 +00:00
oSumAtrIX
d67436271d feat: add appreciation message for new contributors 2023-04-30 03:15:12 +02:00
oSumAtrIX
aa07f35f06 chore: bump dependencies 2023-04-03 03:04:05 +02:00
oSumAtrIX
77e0536838 build: update Kotlin to 1.8.10 2023-03-31 17:19:40 +02:00
oSumAtrIX
a49e78234b chore: remove project files
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-03-02 19:23:41 +01:00
semantic-release-bot
a3ae825e48 chore(release): 7.0.0 [skip ci]
# [7.0.0](https://github.com/revanced/revanced-patcher/compare/v6.4.3...v7.0.0) (2023-02-26)

* feat!: merge integrations only when necessary ([6e24a85](6e24a85eab))

### BREAKING CHANGES

* `Patcher.addFiles` is now renamed to `Patcher.addIntegrations`

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-26 23:42:48 +00:00
oSumAtrIX
146c8504ed chore: merge branch dev to main (#159) 2023-02-27 00:41:27 +01:00
semantic-release-bot
2eb125ad69 chore(release): 7.0.0-dev.1 [skip ci]
# [7.0.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.3...v7.0.0-dev.1) (2023-02-26)

* feat!: merge integrations only when necessary ([6e24a85](6e24a85eab))

### BREAKING CHANGES

* `Patcher.addFiles` is now renamed to `Patcher.addIntegrations`

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-26 23:24:38 +00:00
oSumAtrIX
6e24a85eab feat!: merge integrations only when necessary
BREAKING CHANGE: `Patcher.addFiles` is now renamed to `Patcher.addIntegrations`

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-27 00:23:27 +01:00
oSumAtrIX
e4c3e9ffc5 refactor: remove unnecessary annotations
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-26 23:18:35 +01:00
Javier Flores
4c1778a62f chore: update gradle wrapper to 8.0.1 (#160) 2023-02-26 03:46:33 +01:00
oSumAtrIX
d99261cdbb ci: fix backmerge direction
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-22 16:39:50 +01:00
oSumAtrIX
ac1c0f2773 ci: add backmerge target branch
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-22 06:45:16 +01:00
oSumAtrIX
eddd4ec7ac ci: use new semantic-release-backmerge option
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-18 21:55:40 +01:00
Palm
07a2829c65 ci: update actions (#158) 2023-02-13 22:29:20 +01:00
semantic-release-bot
3d77e299d9 chore(release): 6.4.3 [skip ci]
## [6.4.3](https://github.com/revanced/revanced-patcher/compare/v6.4.2...v6.4.3) (2023-02-10)

### Bug Fixes

* check `CONST_STRING_JUMP` instructions for matching string ([058d292](058d292ad5))
2023-02-10 04:28:15 +00:00
oSumAtrIX
f1336f89e4 chore: merge branch dev to main (#156) 2023-02-10 05:26:43 +01:00
semantic-release-bot
0502f84c20 chore(release): 6.4.3-dev.1 [skip ci]
## [6.4.3-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.2...v6.4.3-dev.1) (2023-02-10)

### Bug Fixes

* check `CONST_STRING_JUMP` instructions for matching string ([058d292](058d292ad5))
2023-02-10 04:20:46 +00:00
oSumAtrIX
058d292ad5 fix: check CONST_STRING_JUMP instructions for matching string
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-10 05:18:38 +01:00
oSumAtrIX
1029d56a52 refactor: remove dead code
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-10 05:17:36 +01:00
oSumAtrIX
709b5a0fec chore: fix typo
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2023-02-10 05:16:00 +01:00
semantic-release-bot
e1accc5041 chore(release): 6.4.2 [skip ci]
## [6.4.2](https://github.com/revanced/revanced-patcher/compare/v6.4.1...v6.4.2) (2023-01-17)

### Bug Fixes

* resolve failing builds ([a263fdf](a263fdfd41))
2023-01-17 23:29:34 +00:00
oSumAtrIX
6dbbf2e03e chore: merge branch dev to main (#153) 2023-01-18 00:14:38 +01:00
semantic-release-bot
16557eeab0 chore(release): 6.4.2-dev.1 [skip ci]
## [6.4.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.1...v6.4.2-dev.1) (2023-01-17)

### Bug Fixes

* resolve failing builds ([a263fdf](a263fdfd41))
2023-01-17 23:09:26 +00:00
oSumAtrIX
6bca3e2bb5 build: update dependencies 2023-01-17 23:57:19 +01:00
oSumAtrIX
a263fdfd41 fix: resolve failing builds
This updates dependencies, because the previous ones were missing or outdated.
2023-01-17 01:32:11 +01:00
semantic-release-bot
e4b4bacae8 chore(release): 6.4.1 [skip ci]
## [6.4.1](https://github.com/revanced/revanced-patcher/compare/v6.4.0...v6.4.1) (2023-01-15)

### Bug Fixes

* update dependency `app.revanced:multidexlib2` ([#150](https://github.com/revanced/revanced-patcher/issues/150)) ([dd7dd38](dd7dd38357))
2023-01-15 05:13:48 +00:00
semantic-release-bot
cbc97af155 chore(release): 6.4.1 [skip ci]
## [6.4.1](https://github.com/revanced/revanced-patcher/compare/v6.4.0...v6.4.1) (2023-01-15)

### Bug Fixes

* update dependency `app.revanced:multidexlib2` ([#150](https://github.com/revanced/revanced-patcher/issues/150)) ([dd7dd38](dd7dd38357))
2023-01-15 04:32:33 +00:00
oSumAtrIX
d5533788e2 chore: merge branch dev to main (#152) 2023-01-15 05:31:12 +01:00
oSumAtrIX
5a4ea5cd7d ci: add missing PAT in step Release 2023-01-15 05:29:50 +01:00
oSumAtrIX
70f3c8b38c chore: merge branch dev to main (#151) 2023-01-15 05:24:25 +01:00
semantic-release-bot
6b410a0eea chore(release): 6.4.1-dev.1 [skip ci]
## [6.4.1-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.0...v6.4.1-dev.1) (2023-01-15)

### Bug Fixes

* update dependency `app.revanced:multidexlib2` ([#150](https://github.com/revanced/revanced-patcher/issues/150)) ([dd7dd38](dd7dd38357))
2023-01-15 03:07:43 +00:00
oSumAtrIX
73a013d75b ci: use PAT in step Release 2023-01-15 04:06:45 +01:00
oSumAtrIX
7159f3db4c ci: downgrade semantic release and plugins to v19 2023-01-15 04:06:45 +01:00
oSumAtrIX
7d5ecf095c ci: give jobs names 2023-01-15 04:06:44 +01:00
oSumAtrIX
fa015a424d refactor: fix formatting 2023-01-15 04:06:44 +01:00
oSumAtrIX
dd7dd38357 fix: update dependency app.revanced:multidexlib2 (#150)
This commit addresses https://github.com/revanced/multidexlib2/issues/2.
2023-01-15 04:06:43 +01:00
semantic-release-bot
22356f2d26 chore(release): 6.4.0 [skip ci]
# [6.4.0](https://github.com/revanced/revanced-patcher/compare/v6.3.2...v6.4.0) (2023-01-02)

### Features

* add missing setter to `MutableMethod` ([8f3ecc3](8f3ecc318c))
* do not fix methods or methods in class merger ([4102f43](4102f43b8a))
* fix method and field access when merging classes ([5c09ef7](5c09ef7837))
* make `aaptPath` nullable ([#146](https://github.com/revanced/revanced-patcher/issues/146)) ([9f0a09a](9f0a09a756))
2023-01-02 08:07:08 +00:00
oSumAtrIX
66701f6076 chore: merge branch dev to main (#147) 2023-01-02 09:05:19 +01:00
semantic-release-bot
6a6ded084e chore(release): 6.4.0-dev.2 [skip ci]
# [6.4.0-dev.2](https://github.com/revanced/revanced-patcher/compare/v6.4.0-dev.1...v6.4.0-dev.2) (2023-01-02)

### Features

* add missing setter to `MutableMethod` ([8f3ecc3](8f3ecc318c))
* do not fix methods or methods in class merger ([4102f43](4102f43b8a))
* fix method and field access when merging classes ([5c09ef7](5c09ef7837))
2023-01-02 07:58:55 +00:00
oSumAtrIX
5887c69bde refactor: move merging classes code to own class (#149) 2023-01-02 08:58:05 +01:00
oSumAtrIX
4102f43b8a feat: do not fix methods or methods in class merger 2023-01-02 08:50:08 +01:00
oSumAtrIX
5c09ef7837 feat: fix method and field access when merging classes 2023-01-02 07:57:50 +01:00
oSumAtrIX
3e0bf8c863 refactor: move merging classes code to own class 2023-01-02 07:57:48 +01:00
oSumAtrIX
8f3ecc318c feat: add missing setter to MutableMethod 2023-01-02 07:09:58 +01:00
oSumAtrIX
365da96e2b build: do not comment on successful releases 2022-12-31 21:12:03 +01:00
oSumAtrIX
cd68ec4803 ci: do not escape in environment variable MESSAGE 2022-12-29 19:07:31 +01:00
semantic-release-bot
35265e029c chore(release): 6.4.0-dev.1 [skip ci]
# [6.4.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.2...v6.4.0-dev.1) (2022-12-20)

### Features

* make `aaptPath` nullable ([#146](https://github.com/revanced/revanced-patcher/issues/146)) ([9f0a09a](9f0a09a756))
2022-12-20 19:05:47 +00:00
Canny
9f0a09a756 feat: make aaptPath nullable (#146) 2022-12-20 20:04:15 +01:00
semantic-release-bot
e802141df5 chore(release): 6.3.2 [skip ci]
## [6.3.2](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2) (2022-12-18)

### Bug Fixes

* check if fingerprint string is substring of any string references ([c5de9e2](c5de9e2988))
* print full exception when patch fails ([7cf79e6](7cf79e68e0))
2022-12-18 21:11:48 +00:00
oSumAtrIX
abebc0862c chore: merge branch dev to main (#144) 2022-12-18 22:10:56 +01:00
semantic-release-bot
96ef150e89 chore(release): 6.3.2-dev.1 [skip ci]
## [6.3.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2-dev.1) (2022-12-18)

### Bug Fixes

* check if fingerprint string is substring of any string references ([c5de9e2](c5de9e2988))
* print full exception when patch fails ([7cf79e6](7cf79e68e0))
2022-12-18 21:08:40 +00:00
oSumAtrIX
c5de9e2988 fix: check if fingerprint string is substring of any string references 2022-12-18 22:05:05 +01:00
semantic-release-bot
c391ca648b chore(release): 6.3.2-dev.1 [skip ci]
## [6.3.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2-dev.1) (2022-12-17)

### Bug Fixes

* print full exception when patch fails ([27a8401](27a8401d81))
2022-12-18 09:02:48 +01:00
oSumAtrIX
7cf79e68e0 fix: print full exception when patch fails 2022-12-18 09:02:48 +01:00
oSumAtrIX
f07db3c214 chore: merge branch dev to main (#143) 2022-12-15 21:44:59 +01:00
oSumAtrIX
88bb3a8845 ci: do not release on build commit type 2022-12-15 19:47:31 +01:00
oSumAtrIX
b9e6bd6775 chore: merge branch dev to main (#141) 2022-12-15 19:36:43 +01:00
oSumAtrIX
cd1b72e078 ci: remove unnecessary step 2022-12-15 01:08:00 +01:00
oSumAtrIX
6b889557ab ci: stash before rebasing 2022-12-15 01:07:48 +01:00
oSumAtrIX
4b1be8c647 ci: only back-merge from main branch to dev 2022-12-15 01:04:16 +01:00
oSumAtrIX
73c893c6e7 chore: merge branch dev to main (#140)
chore: merge branch `dev` to `main`
2022-12-14 00:13:25 +01:00
oSumAtrIX
75b36823b8 ci: back-merge releases back into dev branch 2022-12-14 00:02:52 +01:00
semantic-release-bot
d2d93cd075 chore(release): 6.3.1 [skip ci]
## [6.3.1](https://github.com/revanced/revanced-patcher/compare/v6.3.0...v6.3.1) (2022-12-13)

### Bug Fixes

* publicize types when merging files if necessary ([#137](https://github.com/revanced/revanced-patcher/issues/137)) ([9ec720e](9ec720e983))
2022-12-13 22:52:11 +00:00
oSumAtrIX
26b8621ac8 chore: merge branch dev to main (#139) 2022-12-13 23:50:28 +01:00
semantic-release-bot
f365a41741 chore(release): 6.3.1-dev.1 [skip ci]
## [6.3.1-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.0...v6.3.1-dev.1) (2022-12-13)

### Bug Fixes

* publicize types when merging files if necessary ([#137](https://github.com/revanced/revanced-patcher/issues/137)) ([9ec720e](9ec720e983))
2022-12-13 22:39:34 +00:00
oSumAtrIX
9ec720e983 fix: publicize types when merging files if necessary (#137) 2022-12-13 23:36:47 +01:00
oSumAtrIX
0f432b3fdd ci: escape backticks in message environment variable 2022-12-13 23:36:47 +01:00
oSumAtrIX
96cd5618dd ci: open pull requests to merge dev to main (#131) 2022-12-13 23:36:47 +01:00
oSumAtrIX
c2a5a55e67 refactor: remove unnecessary test 2022-12-11 03:01:41 +01:00
oSumAtrIX
6c5de8b414 ci: refactor release workflow 2022-12-11 03:01:40 +01:00
semantic-release-bot
ea773cfa56 chore(release): 6.3.0 [skip ci]
# [6.3.0](https://github.com/revanced/revanced-patcher/compare/v6.2.0...v6.3.0) (2022-12-02)

### Features

* sort patches in lexicographical order ([a306561](a306561b55)), closes [#125](https://github.com/revanced/revanced-patcher/issues/125)
2022-12-02 02:32:49 +00:00
oSumAtrIX
a306561b55 feat: sort patches in lexicographical order
Closes #125
2022-12-02 03:31:25 +01:00
semantic-release-bot
b6dcd88495 chore(release): 6.2.0 [skip ci]
# [6.2.0](https://github.com/revanced/revanced-patcher/compare/v6.1.1...v6.2.0) (2022-12-02)

### Features

* merge classes on addition ([#127](https://github.com/revanced/revanced-patcher/issues/127)) ([a925650](a925650044))
2022-12-02 01:33:08 +00:00
oSumAtrIX
a925650044 feat: merge classes on addition (#127) 2022-12-02 02:31:47 +01:00
semantic-release-bot
77bbf6be1f chore(release): 6.1.1 [skip ci]
## [6.1.1](https://github.com/revanced/revanced-patcher/compare/v6.1.0...v6.1.1) (2022-11-25)

### Bug Fixes

* use `MethodUtil.methodSignaturesMatch` instead of `Method.softCompareTo` ([bd053b7](bd053b7e99))
2022-11-25 09:25:48 +00:00
oSumAtrIX
bd053b7e99 fix: use MethodUtil.methodSignaturesMatch instead of Method.softCompareTo 2022-11-25 10:24:13 +01:00
semantic-release-bot
fd742eba63 chore(release): 6.1.0 [skip ci]
# [6.1.0](https://github.com/revanced/revanced-patcher/compare/v6.0.2...v6.1.0) (2022-11-22)

### Features

* apply changes from ReVanced Patcher ([ba9d998](ba9d998681))
2022-11-22 23:30:33 +00:00
oSumAtrIX
ba9d998681 feat: apply changes from ReVanced Patcher
BREAKING-CHANGE: Some annotations have been removed regarding fingerprints and patches.
2022-11-23 00:29:12 +01:00
semantic-release-bot
75df245ec3 chore(release): 6.0.2 [skip ci]
## [6.0.2](https://github.com/revanced/revanced-patcher/compare/v6.0.1...v6.0.2) (2022-11-18)

### Bug Fixes

* fallback to patch class name instead of `java.lang.Class` class name ([4164cb0](4164cb0dea))
2022-11-18 01:20:19 +00:00
oSumAtrIX
4164cb0dea fix: fallback to patch class name instead of java.lang.Class class name 2022-11-18 02:14:20 +01:00
semantic-release-bot
18fe35ae73 chore(release): 6.0.1 [skip ci]
## [6.0.1](https://github.com/revanced/revanced-patcher/compare/v6.0.0...v6.0.1) (2022-11-14)

### Bug Fixes

* remove unnecessary dummy nop instructions ([#111](https://github.com/revanced/revanced-patcher/issues/111)) ([f9bc95f](f9bc95f220))
2022-11-14 15:59:22 +00:00
MewtR
f9bc95f220 fix: remove unnecessary dummy nop instructions (#111) 2022-11-14 16:57:13 +01:00
Nico Mexis
d2f91a8545 build: update workflow actions (#121) [skip ci] 2022-11-05 15:27:56 +01:00
semantic-release-bot
4016bdc37f chore(release): 6.0.0 [skip ci]
# [6.0.0](https://github.com/revanced/revanced-patcher/compare/v5.1.2...v6.0.0) (2022-10-05)

### Code Refactoring

* improve structuring of classes and their implementations ([4aa14bb](4aa14bbb85))

### Features

* remove unused annotation `DirectPatternScanMethod` ([538b2a8](538b2a8599))
* remove unused annotation `SincePatcher` ([4ae9ad0](4ae9ad09d6))
* remove unused extension `dependsOn` ([797286b](797286b758))
* remove unused patch extensions ([5583904](5583904994))

### BREAKING CHANGES

* various changes in which packages classes previously where and their implementation
* These extensions do not exist anymore and any use should be removed
* The extension does not exist anymore and any use should be removed
* The annotation does not exist anymore and any use should be removed
2022-10-05 02:11:19 +00:00
oSumAtrIX
538b2a8599 feat: remove unused annotation DirectPatternScanMethod 2022-10-05 03:57:40 +02:00
oSumAtrIX
4aa14bbb85 refactor: improve structuring of classes and their implementations
BREAKING CHANGE: various changes in which packages classes previously where and their implementation
2022-10-05 03:57:40 +02:00
oSumAtrIX
d37452997b refactor: move OptionsContainer to own class file
BREAKING-CHANGE: this changes the package
2022-10-04 08:32:50 +02:00
oSumAtrIX
db21d5e953 refactor: remove unused import 2022-10-04 08:30:17 +02:00
oSumAtrIX
4d581811db feat!: seal abstract class Patch as an interface 2022-10-04 08:30:17 +02:00
oSumAtrIX
8c502448be feat!: remove unused annotation MatchingMethod 2022-10-04 08:30:16 +02:00
oSumAtrIX
fec16d9442 refactor: remove line break 2022-10-04 08:18:52 +02:00
oSumAtrIX
5583904994 feat: remove unused patch extensions
BREAKING CHANGE: These extensions do not exist anymore and any use should be removed
2022-10-04 08:18:43 +02:00
oSumAtrIX
797286b758 feat: remove unused extension dependsOn
BREAKING CHANGE: The extension does not exist anymore and any use should be removed
2022-10-04 08:18:09 +02:00
oSumAtrIX
4ae9ad09d6 feat: remove unused annotation SincePatcher
BREAKING CHANGE: The annotation does not exist anymore and any use should be removed
2022-10-04 08:16:58 +02:00
semantic-release-bot
447e1ad30e chore(release): 5.1.2 [skip ci]
## [5.1.2](https://github.com/revanced/revanced-patcher/compare/v5.1.1...v5.1.2) (2022-09-29)

### Bug Fixes

* check dependencies for resource patches ([9c07ffc](9c07ffcc7a))
* use instruction index instead of strings list index for `StringMatch` ([843e62a](843e62ad29))
2022-09-29 19:29:55 +00:00
oSumAtrIX
843e62ad29 fix: use instruction index instead of strings list index for StringMatch 2022-09-29 21:27:56 +02:00
oSumAtrIX
9c07ffcc7a fix: check dependencies for resource patches 2022-09-29 21:27:12 +02:00
semantic-release-bot
438321330e chore(release): 5.1.1 [skip ci]
## [5.1.1](https://github.com/revanced/revanced-patcher/compare/v5.1.0...v5.1.1) (2022-09-26)

### Performance Improvements

* decode resources only when necessary ([3ba4be2](3ba4be240b))
2022-09-26 06:59:37 +00:00
oSumAtrIX
3ba4be240b perf: decode resources only when necessary 2022-09-26 08:57:39 +02:00
semantic-release-bot
98ce0abfa9 chore(release): 5.1.0 [skip ci]
# [5.1.0](https://github.com/revanced/revanced-patcher/compare/v5.0.1...v5.1.0) (2022-09-26)

### Features

* RwLock for opening files in `DomFileEditor` ([db4348c](db4348c4fa))
2022-09-26 01:22:58 +00:00
oSumAtrIX
db4348c4fa feat: RwLock for opening files in DomFileEditor 2022-09-26 03:21:13 +02:00
semantic-release-bot
4839f87519 chore(release): 5.0.1 [skip ci]
## [5.0.1](https://github.com/revanced/revanced-patcher/compare/v5.0.0...v5.0.1) (2022-09-23)

### Reverts

* revert breaking changes ([#106](https://github.com/revanced/revanced-patcher/issues/106)) ([124332f](124332f0e9))
2022-09-23 04:21:53 +00:00
oSumAtrIX
809862c997 build: update apktool-lib dependency 2022-09-23 06:20:00 +02:00
oSumAtrIX
fd5c878cee fix!: revert reverting changes
BREAKING-CHANGE: Imports will have to be updated from `MethodFingerprintUtils` to `MethodFingerprint.Companion`.
2022-09-21 16:45:16 +02:00
bogadana
124332f0e9 revert: revert breaking changes (#106) 2022-09-21 15:22:55 +02:00
semantic-release-bot
d4cf0cea52 chore(release): 5.0.0 [skip ci]
# [5.0.0](https://github.com/revanced/revanced-patcher/compare/v4.5.0...v5.0.0) (2022-09-21)

### Bug Fixes

* **tests:** access `patternScanResult` through `scanResult` ([76676fb](76676fb567))

* refactor!: move utility methods from `MethodFingerprintUtils` `MethodFingerprint` ([d802ef8](d802ef844e))
* feat(fingerprint)!: `StringsScanResult` for `MethodFingerprint` ([3813e28](3813e28ac2))

### BREAKING CHANGES

* Imports will have to be updated from `MethodFingerprintUtils` to `MethodFingerprint.Companion`.

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
* `MethodFingerprint` now has a field for `MethodFingerprintScanResult`. `MethodFingerprintScanResult` now holds the previous field `MethodFingerprint.patternScanResult`.

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-21 01:43:54 +00:00
oSumAtrIX
76676fb567 fix(tests): access patternScanResult through scanResult 2022-09-21 03:42:29 +02:00
oSumAtrIX
d802ef844e refactor!: move utility methods from MethodFingerprintUtils MethodFingerprint
BREAKING CHANGE: Imports will have to be updated from `MethodFingerprintUtils` to `MethodFingerprint.Companion`.

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-21 01:38:49 +02:00
oSumAtrIX
90fc547673 refactor: suppress member visibility and unnecessary null assertion warnings
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-21 01:38:49 +02:00
oSumAtrIX
3813e28ac2 feat(fingerprint)!: StringsScanResult for MethodFingerprint
BREAKING CHANGE: `MethodFingerprint` now has a field for `MethodFingerprintScanResult`. `MethodFingerprintScanResult` now holds the previous field `MethodFingerprint.patternScanResult`.

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-21 01:38:49 +02:00
semantic-release-bot
a2bb4004c7 chore(release): 4.5.0 [skip ci]
# [4.5.0](https://github.com/revanced/revanced-patcher/compare/v4.4.2...v4.5.0) (2022-09-20)

### Features

* section `acknowledgements` for issue templates ([a0cb449](a0cb449c60))
2022-09-20 22:37:38 +00:00
oSumAtrIX
a0cb449c60 feat: section acknowledgements for issue templates
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-21 00:36:25 +02:00
semantic-release-bot
e0271790b8 chore(release): 4.4.2 [skip ci]
## [4.4.2](https://github.com/revanced/revanced-patcher/compare/v4.4.1...v4.4.2) (2022-09-18)

### Bug Fixes

* **fingerprint:** do not throw on `MethodFingerprint.result` getter ([2f7e62e](2f7e62ef65))

### Performance Improvements

* **fingerprint:** do not resolve already resolved fingerprints ([4bfd7eb](4bfd7ebff8))
2022-09-18 06:12:38 +00:00
oSumAtrIX
4bfd7ebff8 perf(fingerprint): do not resolve already resolved fingerprints
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-18 07:35:08 +02:00
oSumAtrIX
2f7e62ef65 fix(fingerprint): do not throw on MethodFingerprint.result getter
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
2022-09-18 07:34:32 +02:00
semantic-release-bot
4485af8036 chore(release): 4.4.1 [skip ci]
## [4.4.1](https://github.com/revanced/revanced-patcher/compare/v4.4.0...v4.4.1) (2022-09-14)

### Bug Fixes

* compare any methods parameters ([#101](https://github.com/revanced/revanced-patcher/issues/101)) ([085a3a4](085a3a479d))
2022-09-14 16:36:34 +00:00
d4rkk3y
085a3a479d fix: compare any methods parameters (#101) 2022-09-14 18:34:58 +02:00
semantic-release-bot
f75c9a78b8 chore(release): 4.4.0 [skip ci]
# [4.4.0](https://github.com/revanced/revanced-patcher/compare/v4.3.0...v4.4.0) (2022-09-09)

### Features

* add PathOption back ([172655b](172655bde0))
2022-09-09 14:24:48 +00:00
Sculas
172655bde0 feat: add PathOption back
Now backed by a String.
2022-09-09 16:23:21 +02:00
semantic-release-bot
456db7289a chore(release): 4.3.0 [skip ci]
# [4.3.0](https://github.com/revanced/revanced-patcher/compare/v4.2.3...v4.3.0) (2022-09-09)

### Features

* improved Patch Options ([e722e3f](e722e3f4f9))
2022-09-09 14:11:58 +00:00
Sculas
e722e3f4f9 feat: improved Patch Options
Removed a lot of the type mess. There's still some duplicated code PatchOption.kt, but I'm afraid there's nothing I can do about that. It's not a big deal anyway.
2022-09-09 16:10:30 +02:00
Sculas
c348c1f0a0 refactor: remove PathOption and FileOption 2022-09-09 15:04:17 +02:00
61 changed files with 8310 additions and 1134 deletions

View File

@@ -58,3 +58,15 @@ body:
description: Add additional context here. description: Add additional context here.
validations: validations:
required: false 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

@@ -43,4 +43,16 @@ body:
label: Additional context label: Additional context
description: Add additional context here. description: Add additional context here.
validations: validations:
required: false 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

2
.github/config.yml vendored Normal file
View File

@@ -0,0 +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.

25
.github/workflows/pull_request.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: PR to main
on:
push:
branches:
- dev
workflow_dispatch:
env:
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
jobs:
pull-request:
name: Open pull request
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Open pull request
uses: repo-sync/pull-request@v2
with:
destination_branch: 'main'
pr_title: 'chore: ${{ env.MESSAGE }}'
pr_body: 'This pull request will ${{ env.MESSAGE }}.'
pr_draft: true

View File

@@ -1,4 +1,5 @@
name: Release name: Release
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@@ -9,32 +10,33 @@ on:
branches: branches:
- main - main
- dev - dev
jobs: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: 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 fetch-depth: 0
- name: Setup JDK - name: Setup JDK
uses: actions/setup-java@v2 uses: actions/setup-java@v3
with: with:
java-version: '17' java-version: '17'
distribution: 'adopt' distribution: 'zulu'
cache: gradle cache: gradle
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v3
with: with:
node-version: "lts/*" node-version: "18"
- name: Build with Gradle cache: 'npm'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build
- name: Setup semantic-release - name: Setup semantic-release
run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog gradle-semantic-release-plugin -D run: npm install
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npx semantic-release run: npm exec semantic-release

7
.gitignore vendored
View File

@@ -74,6 +74,7 @@ cmake-build-*/
# IntelliJ # IntelliJ
out/ out/
.idea/
# mpeltonen/sbt-idea plugin # mpeltonen/sbt-idea plugin
.idea_modules/ .idea_modules/
@@ -115,3 +116,9 @@ gradle-app.setting
# Avoid ignoring test resources # Avoid ignoring test resources
!src/test/resources/* !src/test/resources/*
# Dependency directories
node_modules/
# Gradle props, to avoid sharing the gpr key
gradle.properties

9
.idea/.gitignore generated vendored
View File

@@ -1,9 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
/kotlinc.xml

View File

@@ -1,10 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

7
.idea/discord.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UnusedSymbol" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

10
.idea/misc.xml generated
View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="azul-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

12
.idea/vcs.xml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@@ -7,11 +7,7 @@
} }
], ],
"plugins": [ "plugins": [
["@semantic-release/commit-analyzer", { "@semantic-release/commit-analyzer",
"releaseRules": [
{"type": "build", "release": "patch"}
]
}],
"@semantic-release/release-notes-generator", "@semantic-release/release-notes-generator",
"@semantic-release/changelog", "@semantic-release/changelog",
"gradle-semantic-release-plugin", "gradle-semantic-release-plugin",
@@ -24,6 +20,18 @@
] ]
} }
], ],
"@semantic-release/github" [
"@saithodev/semantic-release-backmerge",
{
backmergeBranches: [{"from": "main", "to": "dev"}],
clearWorkspace: true
}
],
[
"@semantic-release/github",
{
successComment: false
}
]
] ]
} }

View File

@@ -1,3 +1,308 @@
# [7.1.0-dev.2](https://github.com/revanced/revanced-patcher/compare/v7.1.0-dev.1...v7.1.0-dev.2) (2023-05-05)
### Features
* add overload to get instruction as type ([49c173d](https://github.com/revanced/revanced-patcher/commit/49c173dc14137ddd198a611e9882dc71300831ee))
# [7.1.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v7.0.0...v7.1.0-dev.1) (2023-04-30)
### Features
* add appreciation message for new contributors ([d674362](https://github.com/revanced/revanced-patcher/commit/d67436271ddca9ccfe008272c1ca82c6123ae7ee))
# [7.0.0](https://github.com/revanced/revanced-patcher/compare/v6.4.3...v7.0.0) (2023-02-26)
* feat!: merge integrations only when necessary ([6e24a85](https://github.com/revanced/revanced-patcher/commit/6e24a85eabd1e7a1484fad229d5ba55c3ba1f1b4))
### BREAKING CHANGES
* `Patcher.addFiles` is now renamed to `Patcher.addIntegrations`
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
# [7.0.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.3...v7.0.0-dev.1) (2023-02-26)
* feat!: merge integrations only when necessary ([6e24a85](https://github.com/revanced/revanced-patcher/commit/6e24a85eabd1e7a1484fad229d5ba55c3ba1f1b4))
### BREAKING CHANGES
* `Patcher.addFiles` is now renamed to `Patcher.addIntegrations`
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
## [6.4.3](https://github.com/revanced/revanced-patcher/compare/v6.4.2...v6.4.3) (2023-02-10)
### Bug Fixes
* check `CONST_STRING_JUMP` instructions for matching string ([058d292](https://github.com/revanced/revanced-patcher/commit/058d292ad5e297f4c652ff543c13e77a39f7fb1b))
## [6.4.3-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.2...v6.4.3-dev.1) (2023-02-10)
### Bug Fixes
* check `CONST_STRING_JUMP` instructions for matching string ([058d292](https://github.com/revanced/revanced-patcher/commit/058d292ad5e297f4c652ff543c13e77a39f7fb1b))
## [6.4.2](https://github.com/revanced/revanced-patcher/compare/v6.4.1...v6.4.2) (2023-01-17)
### Bug Fixes
* resolve failing builds ([a263fdf](https://github.com/revanced/revanced-patcher/commit/a263fdfd413fc05098e28d4800e36ce7d313085b))
## [6.4.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.1...v6.4.2-dev.1) (2023-01-17)
### Bug Fixes
* resolve failing builds ([a263fdf](https://github.com/revanced/revanced-patcher/commit/a263fdfd413fc05098e28d4800e36ce7d313085b))
## [6.4.1](https://github.com/revanced/revanced-patcher/compare/v6.4.0...v6.4.1) (2023-01-15)
### Bug Fixes
* update dependency `app.revanced:multidexlib2` ([#150](https://github.com/revanced/revanced-patcher/issues/150)) ([dd7dd38](https://github.com/revanced/revanced-patcher/commit/dd7dd383577dcfc95e97f77b446a89b41b589dc0))
## [6.4.1](https://github.com/revanced/revanced-patcher/compare/v6.4.0...v6.4.1) (2023-01-15)
### Bug Fixes
* update dependency `app.revanced:multidexlib2` ([#150](https://github.com/revanced/revanced-patcher/issues/150)) ([dd7dd38](https://github.com/revanced/revanced-patcher/commit/dd7dd383577dcfc95e97f77b446a89b41b589dc0))
## [6.4.1-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.4.0...v6.4.1-dev.1) (2023-01-15)
### Bug Fixes
* update dependency `app.revanced:multidexlib2` ([#150](https://github.com/revanced/revanced-patcher/issues/150)) ([dd7dd38](https://github.com/revanced/revanced-patcher/commit/dd7dd383577dcfc95e97f77b446a89b41b589dc0))
# [6.4.0](https://github.com/revanced/revanced-patcher/compare/v6.3.2...v6.4.0) (2023-01-02)
### Features
* add missing setter to `MutableMethod` ([8f3ecc3](https://github.com/revanced/revanced-patcher/commit/8f3ecc318c39f0270aff53efdee7a1c8d82af421))
* do not fix methods or methods in class merger ([4102f43](https://github.com/revanced/revanced-patcher/commit/4102f43b8a9473fd0ee96c5d4fb8f6e9b4e30e70))
* fix method and field access when merging classes ([5c09ef7](https://github.com/revanced/revanced-patcher/commit/5c09ef7837f9b731e137b66c19da77f63c007595))
* make `aaptPath` nullable ([#146](https://github.com/revanced/revanced-patcher/issues/146)) ([9f0a09a](https://github.com/revanced/revanced-patcher/commit/9f0a09a7569fd5dd78afa27cb66a73d1662edc69))
# [6.4.0-dev.2](https://github.com/revanced/revanced-patcher/compare/v6.4.0-dev.1...v6.4.0-dev.2) (2023-01-02)
### Features
* add missing setter to `MutableMethod` ([8f3ecc3](https://github.com/revanced/revanced-patcher/commit/8f3ecc318c39f0270aff53efdee7a1c8d82af421))
* do not fix methods or methods in class merger ([4102f43](https://github.com/revanced/revanced-patcher/commit/4102f43b8a9473fd0ee96c5d4fb8f6e9b4e30e70))
* fix method and field access when merging classes ([5c09ef7](https://github.com/revanced/revanced-patcher/commit/5c09ef7837f9b731e137b66c19da77f63c007595))
# [6.4.0-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.2...v6.4.0-dev.1) (2022-12-20)
### Features
* make `aaptPath` nullable ([#146](https://github.com/revanced/revanced-patcher/issues/146)) ([9f0a09a](https://github.com/revanced/revanced-patcher/commit/9f0a09a7569fd5dd78afa27cb66a73d1662edc69))
## [6.3.2](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2) (2022-12-18)
### Bug Fixes
* check if fingerprint string is substring of any string references ([c5de9e2](https://github.com/revanced/revanced-patcher/commit/c5de9e29889dffd18b31e62a892881cc48e8b607))
* print full exception when patch fails ([7cf79e6](https://github.com/revanced/revanced-patcher/commit/7cf79e68e0e9dfd9faddee33139b127b71882d3e))
## [6.3.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2-dev.1) (2022-12-18)
### Bug Fixes
* check if fingerprint string is substring of any string references ([c5de9e2](https://github.com/revanced/revanced-patcher/commit/c5de9e29889dffd18b31e62a892881cc48e8b607))
* print full exception when patch fails ([7cf79e6](https://github.com/revanced/revanced-patcher/commit/7cf79e68e0e9dfd9faddee33139b127b71882d3e))
## [6.3.2-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.1...v6.3.2-dev.1) (2022-12-17)
### Bug Fixes
* print full exception when patch fails ([27a8401](https://github.com/revanced/revanced-patcher/commit/27a8401d81e078e0303f7ddcb0ac6f342f8e4def))
## [6.3.1](https://github.com/revanced/revanced-patcher/compare/v6.3.0...v6.3.1) (2022-12-13)
### Bug Fixes
* publicize types when merging files if necessary ([#137](https://github.com/revanced/revanced-patcher/issues/137)) ([9ec720e](https://github.com/revanced/revanced-patcher/commit/9ec720e983785d8b1dde330cc0e0e0f914c1803c))
## [6.3.1-dev.1](https://github.com/revanced/revanced-patcher/compare/v6.3.0...v6.3.1-dev.1) (2022-12-13)
### Bug Fixes
* publicize types when merging files if necessary ([#137](https://github.com/revanced/revanced-patcher/issues/137)) ([9ec720e](https://github.com/revanced/revanced-patcher/commit/9ec720e983785d8b1dde330cc0e0e0f914c1803c))
# [6.3.0](https://github.com/revanced/revanced-patcher/compare/v6.2.0...v6.3.0) (2022-12-02)
### Features
* sort patches in lexicographical order ([a306561](https://github.com/revanced/revanced-patcher/commit/a306561b55ac848792046378f582a036f7ffab03)), closes [#125](https://github.com/revanced/revanced-patcher/issues/125)
# [6.2.0](https://github.com/revanced/revanced-patcher/compare/v6.1.1...v6.2.0) (2022-12-02)
### Features
* merge classes on addition ([#127](https://github.com/revanced/revanced-patcher/issues/127)) ([a925650](https://github.com/revanced/revanced-patcher/commit/a9256500440f9b4117f1b8813ba0097dafee4ebb))
## [6.1.1](https://github.com/revanced/revanced-patcher/compare/v6.1.0...v6.1.1) (2022-11-25)
### Bug Fixes
* use `MethodUtil.methodSignaturesMatch` instead of `Method.softCompareTo` ([bd053b7](https://github.com/revanced/revanced-patcher/commit/bd053b7e9974c0282d56e6762459db7070452e4a))
# [6.1.0](https://github.com/revanced/revanced-patcher/compare/v6.0.2...v6.1.0) (2022-11-22)
### Features
* apply changes from ReVanced Patcher ([ba9d998](https://github.com/revanced/revanced-patcher/commit/ba9d99868103406fe36b9aa0cfaa0ed5023edfab))
## [6.0.2](https://github.com/revanced/revanced-patcher/compare/v6.0.1...v6.0.2) (2022-11-18)
### Bug Fixes
* fallback to patch class name instead of `java.lang.Class` class name ([4164cb0](https://github.com/revanced/revanced-patcher/commit/4164cb0deacc7e1eed9fce63dab030180f28b762))
## [6.0.1](https://github.com/revanced/revanced-patcher/compare/v6.0.0...v6.0.1) (2022-11-14)
### Bug Fixes
* remove unnecessary dummy nop instructions ([#111](https://github.com/revanced/revanced-patcher/issues/111)) ([f9bc95f](https://github.com/revanced/revanced-patcher/commit/f9bc95f220aa434308ce6950ba6ad2e7efac9c8a))
# [6.0.0](https://github.com/revanced/revanced-patcher/compare/v5.1.2...v6.0.0) (2022-10-05)
### Code Refactoring
* improve structuring of classes and their implementations ([4aa14bb](https://github.com/revanced/revanced-patcher/commit/4aa14bbb858af9253eae9328b759f3298b65a215))
### Features
* remove unused annotation `DirectPatternScanMethod` ([538b2a8](https://github.com/revanced/revanced-patcher/commit/538b2a859962570c700362afc88704ed3611aa87))
* remove unused annotation `SincePatcher` ([4ae9ad0](https://github.com/revanced/revanced-patcher/commit/4ae9ad09d64a3f69512ccb037f816cb847d7350f))
* remove unused extension `dependsOn` ([797286b](https://github.com/revanced/revanced-patcher/commit/797286b7588646272dea2fd35e8e78b0ffb18a0f))
* remove unused patch extensions ([5583904](https://github.com/revanced/revanced-patcher/commit/55839049948033ad02414517fd3ba03619216aec))
### BREAKING CHANGES
* various changes in which packages classes previously where and their implementation
* These extensions do not exist anymore and any use should be removed
* The extension does not exist anymore and any use should be removed
* The annotation does not exist anymore and any use should be removed
## [5.1.2](https://github.com/revanced/revanced-patcher/compare/v5.1.1...v5.1.2) (2022-09-29)
### Bug Fixes
* check dependencies for resource patches ([9c07ffc](https://github.com/revanced/revanced-patcher/commit/9c07ffcc7af9f088426528561f4321c5cc6b5b15))
* use instruction index instead of strings list index for `StringMatch` ([843e62a](https://github.com/revanced/revanced-patcher/commit/843e62ad290ee0a707be9322ee943921da3ea420))
## [5.1.1](https://github.com/revanced/revanced-patcher/compare/v5.1.0...v5.1.1) (2022-09-26)
### Performance Improvements
* decode resources only when necessary ([3ba4be2](https://github.com/revanced/revanced-patcher/commit/3ba4be240bf0a424e4bbfbaca9605644fda0984e))
# [5.1.0](https://github.com/revanced/revanced-patcher/compare/v5.0.1...v5.1.0) (2022-09-26)
### Features
* RwLock for opening files in `DomFileEditor` ([db4348c](https://github.com/revanced/revanced-patcher/commit/db4348c4faf51bfe29678baacfbe76ba645ec0b9))
## [5.0.1](https://github.com/revanced/revanced-patcher/compare/v5.0.0...v5.0.1) (2022-09-23)
### Reverts
* revert breaking changes ([#106](https://github.com/revanced/revanced-patcher/issues/106)) ([124332f](https://github.com/revanced/revanced-patcher/commit/124332f0e9bbdaf4f1aeeb6a31333093eeba1642))
# [5.0.0](https://github.com/revanced/revanced-patcher/compare/v4.5.0...v5.0.0) (2022-09-21)
### Bug Fixes
* **tests:** access `patternScanResult` through `scanResult` ([76676fb](https://github.com/revanced/revanced-patcher/commit/76676fb5673a9e92517ee3a13943cdc98dd5102a))
* refactor!: move utility methods from `MethodFingerprintUtils` `MethodFingerprint` ([d802ef8](https://github.com/revanced/revanced-patcher/commit/d802ef844edf65d4d26328d6ca72e3ddd5a52b15))
* feat(fingerprint)!: `StringsScanResult` for `MethodFingerprint` ([3813e28](https://github.com/revanced/revanced-patcher/commit/3813e28ac2ad6710d8d935526ca679e7b1b5980e))
### BREAKING CHANGES
* Imports will have to be updated from `MethodFingerprintUtils` to `MethodFingerprint.Companion`.
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
* `MethodFingerprint` now has a field for `MethodFingerprintScanResult`. `MethodFingerprintScanResult` now holds the previous field `MethodFingerprint.patternScanResult`.
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
# [4.5.0](https://github.com/revanced/revanced-patcher/compare/v4.4.2...v4.5.0) (2022-09-20)
### Features
* section `acknowledgements` for issue templates ([a0cb449](https://github.com/revanced/revanced-patcher/commit/a0cb449c60310917141e2809abaa16b4174dc002))
## [4.4.2](https://github.com/revanced/revanced-patcher/compare/v4.4.1...v4.4.2) (2022-09-18)
### Bug Fixes
* **fingerprint:** do not throw on `MethodFingerprint.result` getter ([2f7e62e](https://github.com/revanced/revanced-patcher/commit/2f7e62ef65422f2c75ef8b09b9cd27076e172b30))
### Performance Improvements
* **fingerprint:** do not resolve already resolved fingerprints ([4bfd7eb](https://github.com/revanced/revanced-patcher/commit/4bfd7ebff8b6623b0da4a46d6048bed08c5070d4))
## [4.4.1](https://github.com/revanced/revanced-patcher/compare/v4.4.0...v4.4.1) (2022-09-14)
### Bug Fixes
* compare any methods parameters ([#101](https://github.com/revanced/revanced-patcher/issues/101)) ([085a3a4](https://github.com/revanced/revanced-patcher/commit/085a3a479d7bd411dcb0492b283daca538c824a1))
# [4.4.0](https://github.com/revanced/revanced-patcher/compare/v4.3.0...v4.4.0) (2022-09-09)
### Features
* add PathOption back ([172655b](https://github.com/revanced/revanced-patcher/commit/172655bde06efdb0955431b44d269e6a64fe317a))
# [4.3.0](https://github.com/revanced/revanced-patcher/compare/v4.2.3...v4.3.0) (2022-09-09)
### Features
* improved Patch Options ([e722e3f](https://github.com/revanced/revanced-patcher/commit/e722e3f4f9dc64acf53595802a0a83cf46ee96b8))
## [4.2.3](https://github.com/revanced/revanced-patcher/compare/v4.2.2...v4.2.3) (2022-09-08) ## [4.2.3](https://github.com/revanced/revanced-patcher/compare/v4.2.2...v4.2.3) (2022-09-08)

View File

@@ -1,6 +1,5 @@
plugins { plugins {
kotlin("jvm") version "1.7.0" kotlin("jvm") version "1.8.10"
java
`maven-publish` `maven-publish`
} }
@@ -22,12 +21,12 @@ repositories {
dependencies { dependencies {
implementation("xpp3:xpp3:1.1.4c") implementation("xpp3:xpp3:1.1.4c")
implementation("org.smali:smali:2.5.2") implementation("app.revanced:smali:2.5.3-a3836654")
implementation("app.revanced:multidexlib2:2.5.2.r2") implementation("app.revanced:multidexlib2:2.5.3-a3836654")
implementation("org.apktool:apktool-lib:2.7.0-SNAPSHOT") implementation("app.revanced:apktool-lib:2.7.0")
implementation(kotlin("reflect")) implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
testImplementation(kotlin("test")) testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
} }
tasks { tasks {

View File

@@ -1,2 +1,2 @@
kotlin.code.style = official kotlin.code.style = official
version = 4.2.3 version = 7.1.0-dev.2

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

6580
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

9
package.json Normal file
View File

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

View File

@@ -1,21 +1,12 @@
package app.revanced.patcher package app.revanced.patcher
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.data.PackageMetadata
import app.revanced.patcher.data.impl.findIndexed
import app.revanced.patcher.extensions.PatchExtensions.dependencies import app.revanced.patcher.extensions.PatchExtensions.dependencies
import app.revanced.patcher.extensions.PatchExtensions.deprecated
import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.extensions.PatchExtensions.sincePatcherVersion import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
import app.revanced.patcher.extensions.nullOutputStream import app.revanced.patcher.extensions.nullOutputStream
import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils.resolve import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultError
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.impl.BytecodePatch
import app.revanced.patcher.patch.impl.ResourcePatch
import app.revanced.patcher.util.ListBackedSet
import app.revanced.patcher.util.VersionReader import app.revanced.patcher.util.VersionReader
import brut.androlib.Androlib import brut.androlib.Androlib
import brut.androlib.meta.UsesFramework import brut.androlib.meta.UsesFramework
@@ -31,14 +22,12 @@ import lanchon.multidexlib2.BasicDexFileNamer
import lanchon.multidexlib2.DexIO import lanchon.multidexlib2.DexIO
import lanchon.multidexlib2.MultiDexIO import lanchon.multidexlib2.MultiDexIO
import org.jf.dexlib2.Opcodes import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.DexFile import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.writer.io.MemoryDataStore import org.jf.dexlib2.writer.io.MemoryDataStore
import java.io.Closeable
import java.io.File import java.io.File
import java.nio.file.Files import java.nio.file.Files
private val NAMER = BasicDexFileNamer() internal val NAMER = BasicDexFileNamer()
/** /**
* The ReVanced Patcher. * The ReVanced Patcher.
@@ -47,123 +36,47 @@ private val NAMER = BasicDexFileNamer()
class Patcher(private val options: PatcherOptions) { class Patcher(private val options: PatcherOptions) {
private val logger = options.logger private val logger = options.logger
private val opcodes: Opcodes private val opcodes: Opcodes
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
val data: PatcherData private var mergeIntegrations = false
val context: PatcherContext
companion object { companion object {
@JvmStatic @JvmStatic
val version = VersionReader.read() val version = VersionReader.read()
} private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
this.aaptPath = options.aaptPath
init { this.useAapt2 = true
val extInputFile = ExtFile(options.inputFile) this.frameworkFolderLocation = options.frameworkFolderLocation
try {
val outDir = File(options.resourceCacheDirectory)
if (outDir.exists()) {
logger.info("Deleting existing resource cache directory")
outDir.deleteRecursively()
}
outDir.mkdirs()
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
val resourceTable = androlib.getResTable(extInputFile, true)
val packageMetadata = PackageMetadata()
if (options.patchResources) {
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
packageMetadata.metaInfo.usesFramework = UsesFramework().also { framework ->
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
}
packageMetadata.metaInfo.doNotCompress = buildList {
androlib.recordUncompressedFiles(extInputFile, this)
}
} else {
logger.info("Only decoding AndroidManifest.xml because resource patching is disabled")
// 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"), nullOutputStream
)
}
packageMetadata.packageName = resourceTable.currentResPackage.name
packageMetadata.packageVersion = resourceTable.versionInfo.versionName
packageMetadata.metaInfo.versionInfo = resourceTable.versionInfo
packageMetadata.metaInfo.sdkInfo = resourceTable.sdkInfo
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 data
data = PatcherData(
dexFile.classes.toMutableList(), options.resourceCacheDirectory, packageMetadata
)
} finally {
extInputFile.close()
} }
} }
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 additional dex file container to the patcher. * Add integrations to be merged by the patcher.
* @param files The dex file containers to add to the patcher. * The integrations will only be merged, if necessary.
* @param allowedOverwrites A list of class types that are allowed to be overwritten. *
* @param throwOnDuplicates If this is set to true, the patcher will throw an exception if a duplicate class has been found. * @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 addFiles( fun addIntegrations(
files: List<File>, integrations: List<File>,
allowedOverwrites: Iterable<String> = emptyList(),
throwOnDuplicates: Boolean = false,
callback: (File) -> Unit callback: (File) -> Unit
) { ) {
for (file in files) { context.integrations.apply integrations@{
var modified = false add(integrations)
for (classDef in MultiDexIO.readDexFile(true, file, NAMER, null, null).classes) { this@integrations.callback = callback
val type = classDef.type
val existingClass = data.bytecodeData.classes.internalClasses.findIndexed { it.type == type }
if (existingClass == null) {
if (throwOnDuplicates) throw Exception("Class $type has already been added to the patcher")
logger.trace("Merging $type")
data.bytecodeData.classes.internalClasses.add(classDef)
modified = true
continue
}
if (!allowedOverwrites.contains(type)) continue
logger.trace("Overwriting $type")
val index = existingClass.second
data.bytecodeData.classes.internalClasses[index] = classDef
modified = true
}
if (modified) callback(file)
} }
} }
@@ -171,64 +84,62 @@ class Patcher(private val options: PatcherOptions) {
* Save the patched dex file. * Save the patched dex file.
*/ */
fun save(): PatcherResult { fun save(): PatcherResult {
val packageMetadata = data.packageMetadata val packageMetadata = context.packageMetadata
val metaInfo = packageMetadata.metaInfo val metaInfo = packageMetadata.metaInfo
var resourceFile: File? = null var resourceFile: File? = null
if (options.patchResources) { when (resourceDecodingMode) {
val cacheDirectory = ExtFile(options.resourceCacheDirectory) ResourceDecodingMode.FULL -> {
try { val cacheDirectory = ExtFile(options.resourceCacheDirectory)
val androlibResources = AndrolibResources().also { resources -> try {
resources.buildOptions = BuildOptions().also { buildOptions -> val androlibResources = AndrolibResources().also { resources ->
buildOptions.setBuildOptions(options) resources.buildOptions = BuildOptions().also { buildOptions ->
buildOptions.isFramework = metaInfo.isFrameworkApk buildOptions.setBuildOptions(options)
buildOptions.resourcesAreCompressed = metaInfo.compressionType buildOptions.isFramework = metaInfo.isFrameworkApk
buildOptions.doNotCompress = metaInfo.doNotCompress buildOptions.resourcesAreCompressed = metaInfo.compressionType
buildOptions.doNotCompress = metaInfo.doNotCompress
}
resources.setSdkInfo(metaInfo.sdkInfo)
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
} }
resources.setSdkInfo(metaInfo.sdkInfo) val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
resources.setVersionInfo(metaInfo.versionInfo)
resources.setSharedLibrary(metaInfo.sharedLibrary)
resources.setSparseResources(metaInfo.sparseResources)
}
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml") ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile) val aaptFile = cacheDirectory.resolve("aapt_temp_file")
val aaptFile = cacheDirectory.resolve("aapt_temp_file") // delete if it exists
Files.deleteIfExists(aaptFile.toPath())
// delete if it exists val resDirectory = cacheDirectory.resolve("res")
Files.deleteIfExists(aaptFile.toPath()) val includedFiles = metaInfo.usesFramework.ids.map { id ->
androlibResources.getFrameworkApk(
id, metaInfo.usesFramework.tag
)
}.toTypedArray()
val resDirectory = cacheDirectory.resolve("res") logger.info("Compiling resources")
val includedFiles = metaInfo.usesFramework.ids.map { id -> androlibResources.aaptPackage(
androlibResources.getFrameworkApk( aaptFile, manifestFile, resDirectory, null, null, includedFiles
id, metaInfo.usesFramework.tag
) )
}.toTypedArray()
logger.info("Compiling resources") resourceFile = aaptFile
androlibResources.aaptPackage( } finally {
aaptFile, manifestFile, resDirectory, null, null, includedFiles cacheDirectory.close()
) }
resourceFile = aaptFile
} finally {
cacheDirectory.close()
} }
else -> logger.info("Not compiling resources because resource patching is not required")
} }
logger.trace("Creating new dex file") logger.trace("Creating new dex file")
val newDexFile = object : DexFile { val newDexFile = object : DexFile {
override fun getClasses(): Set<ClassDef> { override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
data.bytecodeData.classes.applyProxies() override fun getOpcodes() = this@Patcher.opcodes
return ListBackedSet(data.bytecodeData.classes.internalClasses)
}
override fun getOpcodes(): Opcodes {
return this@Patcher.opcodes
}
} }
// write modified dex files // write modified dex files
@@ -242,7 +153,9 @@ class Patcher(private val options: PatcherOptions) {
return PatcherResult( return PatcherResult(
dexFiles.map { dexFiles.map {
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0)) app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
}, metaInfo.doNotCompress?.toList(), resourceFile },
metaInfo.doNotCompress?.toList(),
resourceFile
) )
} }
@@ -250,132 +163,229 @@ class Patcher(private val options: PatcherOptions) {
* Add [Patch]es to the patcher. * Add [Patch]es to the patcher.
* @param patches [Patch]es The patches to add. * @param patches [Patch]es The patches to add.
*/ */
fun addPatches(patches: Iterable<Class<out Patch<Data>>>) { 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) { for (patch in patches) {
val needsVersion = patch.sincePatcherVersion if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
if (needsVersion != null && needsVersion > version) { resourceDecodingMode = ResourceDecodingMode.FULL
logger.error("Patch '${patch.patchName}' requires Patcher version $needsVersion or higher") break
logger.error("Current Patcher version is $version")
logger.warn("Skipping '${patch.patchName}'!")
continue // TODO: continue or halt/throw?
} }
} }
data.patches.addAll(patches)
// Determine if merging integrations is required.
for (patch in patches) {
if (patch.anyRecursively { it.requiresIntegrations }) {
mergeIntegrations = true
break
}
}
context.patches.addAll(patches)
} }
/** /**
* Apply a [patch] and its dependencies recursively. * Decode resources for the patcher.
* @param patch The [patch] to apply. *
* @param appliedPatches A map of [patch]es paired to a boolean indicating their success, to prevent infinite recursion. * @param mode The [ResourceDecodingMode] to use when decoding.
* @return The result of executing the [patch].
*/ */
private fun applyPatch( private fun decodeResources(mode: ResourceDecodingMode) {
patch: Class<out Patch<Data>>, val extInputFile = ExtFile(options.inputFile)
appliedPatches: LinkedHashMap<String, AppliedPatch> try {
): PatchResult { val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
val patchName = patch.patchName 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()
// if the patch has already applied silently skip it logger.info("Decoding resources")
if (appliedPatches.contains(patchName)) {
if (!appliedPatches[patchName]!!.success)
return PatchResultError("'$patchName' did not succeed previously")
logger.trace("Skipping '$patchName' because it has already been applied") // decode resources to cache directory
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
return PatchResultSuccess() // 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()
}
// recursively apply all dependency patches // read files to not compress
patch.dependencies?.forEach { metadata.metaInfo.doNotCompress = buildList {
val patchDependency = it.java androlib.recordUncompressedFiles(extInputFile, this)
}
}
val result = applyPatch(patchDependency, appliedPatches) }
if (result.isSuccess()) return@forEach ResourceDecodingMode.MANIFEST_ONLY -> {
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
val error = result.error()!! // create decoder for the resource table
val errorMessage = error.cause ?: error.message val decoder = ResAttrDecoder()
return PatchResultError("'$patchName' depends on '${patchDependency.patchName}' but the following error was raised: $errorMessage") decoder.currentPackage = ResPackage(resourceTable, 0, null)
}
val patchInstance = patch.getDeclaredConstructor().newInstance() // create xml parser with the decoder
val axmlParser = AXmlResourceParser()
axmlParser.attrDecoder = decoder
// if the current patch is a resource patch but resource patching is disabled, return an error // parse package information with the decoder and parser which will set required values in the resource table
val isResourcePatch = patchInstance is ResourcePatch // instead of decodeManifest another more low level solution can be created to make it faster/better
if (!options.patchResources && isResourcePatch) { XmlPullStreamDecoder(
return PatchResultError("'$patchName' is a resource patch, but resource patching is disabled") axmlParser, AndrolibResources().resXmlSerializer
} ).decodeManifest(
extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
)
}
}
patch.deprecated?.let { (reason, replacement) -> // read of the resourceTable which is created by reading the manifest file
logger.warn("'$patchName' is deprecated: $reason") context.packageMetadata.let { metadata ->
if (replacement != null) logger.warn("Use '${replacement.java.patchName}' instead") metadata.packageName = resourceTable.currentResPackage.name
} metadata.packageVersion = resourceTable.versionInfo.versionName
metadata.metaInfo.versionInfo = resourceTable.versionInfo
// TODO: find a solution for this metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
val data = if (isResourcePatch) { }
data.resourceData } finally {
} else { extInputFile.close()
val bytecodeData = data.bytecodeData
(patchInstance as BytecodePatch).fingerprints?.resolve(bytecodeData, bytecodeData.classes.internalClasses)
bytecodeData
}
logger.trace("Executing '$patchName' of type: ${if (isResourcePatch) "resource" else "bytecode"}")
return try {
val result = patchInstance.execute(data)
appliedPatches[patchName] = AppliedPatch(patchInstance, result.isSuccess())
result
} catch (e: Exception) {
appliedPatches[patchName] = AppliedPatch(patchInstance, false)
PatchResultError(e)
} }
} }
/** /**
* Apply patches loaded into the patcher. * Execute patches added the patcher.
*
* @param stopOnError If true, the patches will stop on the first error. * @param stopOnError If true, the patches will stop on the first error.
* @return A pair of the name of the [Patch] and its [PatchResult]. * @return A pair of the name of the [Patch] and its [PatchResult].
*/ */
fun applyPatches(stopOnError: Boolean = false) = sequence { fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
logger.trace("Applying all patches") /**
* 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
val appliedPatches = LinkedHashMap<String, AppliedPatch>() // first is name // if the patch has already applied silently skip it
if (executedPatches.contains(patchName)) {
if (!executedPatches[patchName]!!.success)
return PatchResultError("'$patchName' did not succeed previously")
try { logger.trace("Skipping '$patchName' because it has already been applied")
for (patch in data.patches) {
val patchResult = applyPatch(patch, appliedPatches)
val result = if (patchResult.isSuccess()) { return PatchResultSuccess()
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) break
} }
} finally {
// close all closeable patches in order
for ((patch, _) in appliedPatches.values.reversed()) {
if (patch !is Closeable) continue
patch.close() // 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?.resolve(
context,
context.classes.classes
)
}
}
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)
// prevent from decoding the manifest twice if it is not needed
if (resourceDecodingMode == ResourceDecodingMode.FULL) decodeResources(ResourceDecodingMode.FULL)
logger.trace("Executing all patches")
val executedPatches = LinkedHashMap<String, ExecutedPatch>() // first is name
try {
context.patches.forEach { patch ->
val patchResult = executePatch(patch, executedPatches)
val result = if (patchResult.isSuccess()) {
Result.success(patchResult.success()!!)
} else {
Result.failure(patchResult.error()!!)
}
yield(patch.patchName to result)
if (stopOnError && patchResult.isError()) return@sequence
}
} finally {
executedPatches.values.reversed().forEach { (patch, _) ->
patch.close()
}
}
}
}
/**
* The type of decoding the resources.
*/
private enum class ResourceDecodingMode {
/**
* Decode all resources.
*/
FULL,
/**
* Decode the manifest file only.
*/
MANIFEST_ONLY,
} }
} }
/** /**
* A result of applying a [Patch]. * A result of executing a [Patch].
* *
* @param patchInstance The instance of the [Patch] that was applied. * @param patchInstance The instance of the [Patch] that was applied.
* @param success The result of the [Patch]. * @param success The result of the [Patch].
*/ */
internal data class AppliedPatch(val patchInstance: Patch<Data>, val success: Boolean) internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
this.aaptPath = options.aaptPath
this.useAapt2 = true
this.frameworkFolderLocation = options.frameworkFolderLocation
}

View File

@@ -0,0 +1,64 @@
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,19 +0,0 @@
package app.revanced.patcher
import app.revanced.patcher.data.Data
import app.revanced.patcher.data.PackageMetadata
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.data.impl.ResourceData
import app.revanced.patcher.patch.Patch
import org.jf.dexlib2.iface.ClassDef
import java.io.File
data class PatcherData(
internal val internalClasses: MutableList<ClassDef>,
internal val resourceCacheDirectory: String,
val packageMetadata: PackageMetadata
) {
internal val patches = mutableListOf<Class<out Patch<Data>>>()
internal val bytecodeData = BytecodeData(internalClasses)
internal val resourceData = ResourceData(File(resourceCacheDirectory))
}

View File

@@ -8,7 +8,6 @@ import java.io.File
* Options for the [Patcher]. * Options for the [Patcher].
* @param inputFile The input file (usually an apk file). * @param inputFile The input file (usually an apk file).
* @param resourceCacheDirectory Directory to cache resources. * @param resourceCacheDirectory Directory to cache resources.
* @param patchResources Weather to use the resource patcher. Resources will still need to be decoded.
* @param aaptPath Optional path to a custom aapt binary. * @param aaptPath Optional path to a custom aapt binary.
* @param frameworkFolderLocation Optional path to a custom framework folder. * @param frameworkFolderLocation Optional path to a custom framework folder.
* @param logger Custom logger implementation for the [Patcher]. * @param logger Custom logger implementation for the [Patcher].
@@ -16,8 +15,7 @@ import java.io.File
data class PatcherOptions( data class PatcherOptions(
internal val inputFile: File, internal val inputFile: File,
internal val resourceCacheDirectory: String, internal val resourceCacheDirectory: String,
internal val patchResources: Boolean = false, internal val aaptPath: String? = null,
internal val aaptPath: String = "",
internal val frameworkFolderLocation: String? = null, internal val frameworkFolderLocation: String? = null,
internal val logger: Logger = NopLogger internal val logger: Logger = NopLogger
) )

View File

@@ -1,15 +1,12 @@
package app.revanced.patcher.annotation package app.revanced.patcher.annotation
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
/** /**
* Annotation to constrain a [Patch] or [MethodFingerprint] to compatible packages. * Annotation to constrain a [Patch] to compatible packages.
* @param compatiblePackages A list of packages a [Patch] or [MethodFingerprint] is compatible with. * @param compatiblePackages A list of packages a [Patch] is compatible with.
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Compatibility( annotation class Compatibility(
val compatiblePackages: Array<Package>, val compatiblePackages: Array<Package>,
) )
@@ -17,11 +14,9 @@ annotation class Compatibility(
/** /**
* Annotation to represent packages a patch can be compatible with. * Annotation to represent packages a patch can be compatible with.
* @param name The package identifier name. * @param name The package identifier name.
* @param versions The versions of the package the [Patch] or [MethodFingerprint]is compatible with. * @param versions The versions of the package the [Patch] is compatible with.
*/ */
@Target() @Target()
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Package( annotation class Package(
val name: String, val name: String,
val versions: Array<String> = [], val versions: Array<String> = [],

View File

@@ -1,19 +0,0 @@
package app.revanced.patcher.annotation
import app.revanced.patcher.data.Data
import app.revanced.patcher.patch.Patch
import kotlin.reflect.KClass
/**
* Declares a [Patch] deprecated for removal.
* @param reason The reason why the patch is deprecated.
* @param replacement The replacement for the deprecated patch, if any.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class PatchDeprecated(
val reason: String,
val replacement: KClass<out Patch<Data>> = Patch::class
// Values cannot be nullable in annotations, so this will have to do.
)

View File

@@ -1,38 +1,31 @@
package app.revanced.patcher.annotation package app.revanced.patcher.annotation
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
/** /**
* Annotation to name a [Patch] or [MethodFingerprint]. * Annotation to name a [Patch].
* @param name A suggestive name for the [Patch] or [MethodFingerprint]. * @param name A suggestive name for the [Patch].
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Name( annotation class Name(
val name: String, val name: String,
) )
/** /**
* Annotation to describe a [Patch] or [MethodFingerprint]. * Annotation to describe a [Patch].
* @param description A description for the [Patch] or [MethodFingerprint]. * @param description A description for the [Patch].
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Description( annotation class Description(
val description: String, val description: String,
) )
/** /**
* Annotation to version a [Patch] or [MethodFingerprint]. * Annotation to version a [Patch].
* @param version The version of a [Patch] or [MethodFingerprint]. * @param version The version of a [Patch].
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Version( annotation class Version(
val version: String, val version: String,
) )

View File

@@ -1,13 +0,0 @@
package app.revanced.patcher.annotation
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.Patcher
/**
* Declares a [Patch] deprecated for removal.
* @param version The minimum version of the [Patcher] this [Patch] supports.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class SincePatcher(val version: String)

View File

@@ -0,0 +1,181 @@
package app.revanced.patcher.data
import app.revanced.patcher.util.ProxyBackedClassList
import app.revanced.patcher.util.method.MethodWalker
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
/**
* A common interface to constrain [Context] to [BytecodeContext] and [ResourceContext].
*/
sealed interface Context
class BytecodeContext internal constructor(classes: MutableList<ClassDef>) : Context {
/**
* The list of classes.
*/
val classes = ProxyBackedClassList(classes)
/**
* Find a class by a given class name.
*
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
*
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.add(proxy)
}
return proxy
}
private companion object {
inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
for (element in this) {
if (predicate(element)) {
return element
}
}
return null
}
}
}
/**
* Create a [MethodWalker] instance for the current [BytecodeContext].
*
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun BytecodeContext.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod)
}
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
for ((index, element) in this.withIndex()) {
if (predicate(element)) {
return element to index
}
}
return null
}
class ResourceContext internal constructor(private val resourceCacheDirectory: File) : Context, Iterable<File> {
val xmlEditor = XmlFileHolder()
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
inner class XmlFileHolder {
operator fun get(inputStream: InputStream) =
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceContext[path])
}
}
}
/**
* Wrapper for a file that can be edited as a dom document.
*
* This constructor does not check for locks to the file when writing.
* Use the secondary constructor.
*
* @param inputStream the input stream to read the xml file from.
* @param outputStream the output stream to write the xml file to. If null, the file will be read only.
*
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val outputStream: Lazy<OutputStream>? = null,
) : Closeable {
// path to the xml file to unlock the resource when closing the editor
private var filePath: String? = null
private var closed: Boolean = false
/**
* The document of the xml file
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
// increase the lock
locks.merge(file.path, 1, Integer::sum)
filePath = file.path
}
/**
* Closes the editor. Write backs and decreases the lock count.
*
* Will not write back to the file if the file is still locked.
*/
override fun close() {
if (closed) return
inputStream.close()
// if the output stream is not null, do not close it
outputStream?.let {
// prevent writing to same file, if it is being locked
// isLocked will be false if the editor was created through a stream
val isLocked = filePath?.let { path ->
val isLocked = locks[path]!! > 1
// decrease the lock count if the editor was opened for a file
locks.merge(path, -1, Integer::sum)
isLocked
} ?: false
// if unlocked, write back to the file
if (!isLocked) {
it.value.use { stream ->
val result = StreamResult(stream)
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
}
it.value.close()
return
}
}
closed = true
}
private companion object {
// map of concurrent open files
val locks = mutableMapOf<String, Int>()
}
}

View File

@@ -1,9 +0,0 @@
package app.revanced.patcher.data
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.data.impl.ResourceData
/**
* Constraint interface for [BytecodeData] and [ResourceData]
*/
interface Data

View File

@@ -1,69 +0,0 @@
package app.revanced.patcher.data.impl
import app.revanced.patcher.data.Data
import app.revanced.patcher.util.ProxyBackedClassList
import app.revanced.patcher.util.method.MethodWalker
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
class BytecodeData(
internalClasses: MutableList<ClassDef>
) : Data {
val classes = ProxyBackedClassList(internalClasses)
/**
* Find a class by a given class name.
* @param className The name of the class.
* @return A proxy for the first class that matches the class name.
*/
fun findClass(className: String) = findClass { it.type.contains(className) }
/**
* Find a class by a given predicate.
* @param predicate A predicate to match the class.
* @return A proxy for the first class that matches the predicate.
*/
fun findClass(predicate: (ClassDef) -> Boolean) =
// if we already proxied the class matching the predicate...
classes.proxies.firstOrNull { predicate(it.immutableClass) } ?:
// else resolve the class to a proxy and return it, if the predicate is matching a class
classes.find(predicate)?.let { proxy(it) }
fun proxy(classDef: ClassDef): app.revanced.patcher.util.proxy.ClassProxy {
var proxy = this.classes.proxies.find { it.immutableClass.type == classDef.type }
if (proxy == null) {
proxy = app.revanced.patcher.util.proxy.ClassProxy(classDef)
this.classes.add(proxy)
}
return proxy
}
}
internal class MethodNotFoundException(s: String) : Exception(s)
internal inline fun <reified T> Iterable<T>.find(predicate: (T) -> Boolean): T? {
for (element in this) {
if (predicate(element)) {
return element
}
}
return null
}
/**
* Create a [MethodWalker] instance for the current [BytecodeData].
* @param startMethod The method to start at.
* @return A [MethodWalker] instance.
*/
fun BytecodeData.toMethodWalker(startMethod: Method): MethodWalker {
return MethodWalker(this, startMethod)
}
internal inline fun <T> Iterable<T>.findIndexed(predicate: (T) -> Boolean): Pair<T, Int>? {
for ((index, element) in this.withIndex()) {
if (predicate(element)) {
return element to index
}
}
return null
}

View File

@@ -1,84 +0,0 @@
package app.revanced.patcher.data.impl
import app.revanced.patcher.data.Data
import org.w3c.dom.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
class ResourceData(private val resourceCacheDirectory: File) : Data, Iterable<File> {
val xmlEditor = XmlFileHolder()
operator fun get(path: String) = resourceCacheDirectory.resolve(path)
override fun iterator() = resourceCacheDirectory.walkTopDown().iterator()
inner class XmlFileHolder {
operator fun get(inputStream: InputStream) =
DomFileEditor(inputStream)
operator fun get(path: String): DomFileEditor {
return DomFileEditor(this@ResourceData[path])
}
}
}
/**
* DomFileEditor is a wrapper for a file that can be edited as a dom document.
*
* @param inputStream the input stream to read the xml file from.
* @param outputStream the output stream to write the xml file to. If null, the file will not be written.
*/
class DomFileEditor internal constructor(
private val inputStream: InputStream,
private val outputStream: Lazy<OutputStream>? = null,
) : Closeable {
// path to the xml file to unlock the resource when closing the editor
private var filePath: String? = null
/**
* The document of the xml file
*/
val file: Document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream)
.also(Document::normalize)
// lazily open an output stream
// this is required because when constructing a DomFileEditor the output stream is created along with the input stream, which is not allowed
// the workaround is to lazily create the output stream. This way it would be used after the input stream is closed, which happens in the constructor
constructor(file: File) : this(file.inputStream(), lazy { file.outputStream() }) {
filePath = file.path
// prevent sharing mutability of the same file between multiple instances of DomFileEditor
if (locks.contains(filePath))
throw IllegalStateException("Can not create a DomFileEditor for that file because it is already locked by another instance of DomFileEditor.")
locks.add(filePath!!)
}
override fun close() {
inputStream.close()
// if the output stream is not null, do not close it
outputStream?.let {
val result = StreamResult(it.value)
TransformerFactory.newInstance().newTransformer().transform(DOMSource(file), result)
it.value.close()
}
// remove the lock, if it exists
filePath?.let {
locks.remove(it)
}
}
private companion object {
// list of locked file paths
val locks = mutableListOf<String>()
}
}

View File

@@ -1,11 +1,14 @@
package app.revanced.patcher.extensions package app.revanced.patcher.extensions
import app.revanced.patcher.annotation.* import app.revanced.patcher.annotation.*
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.OptionsContainer
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchOptions import app.revanced.patcher.patch.PatchOptions
import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.RequiresIntegrations
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KVisibility import kotlin.reflect.KVisibility
import kotlin.reflect.full.companionObject import kotlin.reflect.full.companionObject
@@ -16,7 +19,7 @@ import kotlin.reflect.full.companionObjectInstance
* @param targetAnnotation The annotation to find. * @param targetAnnotation The annotation to find.
* @return The annotation. * @return The annotation.
*/ */
private fun <T : Annotation> Class<*>.recursiveAnnotation(targetAnnotation: KClass<T>) = private fun <T : Annotation> Class<*>.findAnnotationRecursively(targetAnnotation: KClass<T>) =
this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf()) this.findAnnotationRecursively(targetAnnotation.java, mutableSetOf())
@@ -38,42 +41,43 @@ private fun <T : Annotation> Class<*>.findAnnotationRecursively(
} }
object PatchExtensions { object PatchExtensions {
val Class<*>.patchName: String val Class<out Patch<Context>>.patchName: String
get() = recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName get() = findAnnotationRecursively(Name::class)?.name ?: this.simpleName
val Class<out Patch<Data>>.version get() = recursiveAnnotation(Version::class)?.version
val Class<out Patch<Data>>.include get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.Patch::class)!!.include val Class<out Patch<Context>>.version
val Class<out Patch<Data>>.description get() = recursiveAnnotation(Description::class)?.description get() = findAnnotationRecursively(Version::class)?.version
val Class<out Patch<Data>>.dependencies get() = recursiveAnnotation(app.revanced.patcher.patch.annotations.DependsOn::class)?.dependencies
val Class<out Patch<Data>>.compatiblePackages get() = recursiveAnnotation(Compatibility::class)?.compatiblePackages val Class<out Patch<Context>>.include
val Class<out Patch<Data>>.options: PatchOptions? get() = findAnnotationRecursively(app.revanced.patcher.patch.annotations.Patch::class)!!.include
val Class<out Patch<Context>>.description
get() = findAnnotationRecursively(Description::class)?.description
val Class<out Patch<Context>>.dependencies
get() = findAnnotationRecursively(DependsOn::class)?.dependencies
val Class<out Patch<Context>>.compatiblePackages
get() = findAnnotationRecursively(Compatibility::class)?.compatiblePackages
internal val Class<out Patch<Context>>.requiresIntegrations
get() = findAnnotationRecursively(RequiresIntegrations::class) != null
val Class<out Patch<Context>>.options: PatchOptions?
get() = kotlin.companionObject?.let { cl -> get() = kotlin.companionObject?.let { cl ->
if (cl.visibility != KVisibility.PUBLIC) return null if (cl.visibility != KVisibility.PUBLIC) return null
kotlin.companionObjectInstance?.let { kotlin.companionObjectInstance?.let {
(it as? OptionsContainer)?.options (it as? OptionsContainer)?.options
} }
} }
val Class<out Patch<Data>>.deprecated: Pair<String, KClass<out Patch<Data>>?>?
get() = recursiveAnnotation(PatchDeprecated::class)?.let {
it.reason to it.replacement.let { cl ->
if (cl == Patch::class) null else cl
}
}
val Class<out Patch<Data>>.sincePatcherVersion get() = recursiveAnnotation(SincePatcher::class)?.version
@JvmStatic
fun Class<out Patch<Data>>.dependsOn(patch: Class<out Patch<Data>>): Boolean {
if (this.patchName == patch.patchName) throw IllegalArgumentException("thisval and patch may not be the same")
return this.dependencies?.any { it.java.patchName == this@dependsOn.patchName } == true
}
} }
object MethodFingerprintExtensions { object MethodFingerprintExtensions {
val MethodFingerprint.name: String val MethodFingerprint.name: String
get() = javaClass.recursiveAnnotation(Name::class)?.name ?: this.javaClass.simpleName get() = this.javaClass.simpleName
val MethodFingerprint.version get() = javaClass.recursiveAnnotation(Version::class)?.version ?: "0.0.1"
val MethodFingerprint.description get() = javaClass.recursiveAnnotation(Description::class)?.description val MethodFingerprint.fuzzyPatternScanMethod
val MethodFingerprint.compatiblePackages get() = javaClass.recursiveAnnotation(Compatibility::class)?.compatiblePackages get() = javaClass.findAnnotationRecursively(FuzzyPatternScanMethod::class)
val MethodFingerprint.matchingMethod get() = javaClass.recursiveAnnotation(app.revanced.patcher.fingerprint.method.annotation.MatchingMethod::class)
val MethodFingerprint.fuzzyPatternScanMethod get() = javaClass.recursiveAnnotation(app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod::class) val MethodFingerprint.fuzzyScanThreshold
val MethodFingerprint.fuzzyScanThreshold get() = fuzzyPatternScanMethod?.threshold ?: 0 get() = fuzzyPatternScanMethod?.threshold ?: 0
} }

View File

@@ -13,10 +13,8 @@ import org.jf.dexlib2.builder.MutableMethodImplementation
import org.jf.dexlib2.builder.instruction.* import org.jf.dexlib2.builder.instruction.*
import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.Instruction import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.reference.MethodReference
import org.jf.dexlib2.immutable.ImmutableMethod import org.jf.dexlib2.immutable.ImmutableMethod
import org.jf.dexlib2.immutable.ImmutableMethodImplementation import org.jf.dexlib2.immutable.ImmutableMethodImplementation
import org.jf.dexlib2.util.MethodUtil
import java.io.OutputStream import java.io.OutputStream
infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value infix fun AccessFlags.or(other: AccessFlags) = this.value or other.value
@@ -46,19 +44,6 @@ fun MutableMethodImplementation.removeInstructions(index: Int, count: Int) {
} }
} }
/**
* Compare a method to another, considering constructors and parameters.
* @param otherMethod The method to compare against.
* @return True if the methods match given the conditions.
*/
fun Method.softCompareTo(otherMethod: MethodReference): Boolean {
if (MethodUtil.isConstructor(this) && !parametersEqual(
this.parameterTypes, otherMethod.parameterTypes
)
) return false
return this.name == otherMethod.name
}
/** /**
* Clones the method. * Clones the method.
* @param registerCount This parameter allows you to change the register count of the method. * @param registerCount This parameter allows you to change the register count of the method.
@@ -116,12 +101,20 @@ fun MutableMethod.removeInstruction(index: Int) = this.implementation!!.removeIn
fun MutableMethod.label(index: Int) = this.implementation!!.newLabelForIndex(index) fun MutableMethod.label(index: Int) = this.implementation!!.newLabelForIndex(index)
/** /**
* Get the instruction at given index in the method's implementation. * Get an instruction at the given index in the method's implementation.
* @param index The index to get the instruction at. * @param index The index to get the instruction at.
* @return The instruction. * @return The instruction.
*/ */
fun MutableMethod.instruction(index: Int): BuilderInstruction = this.implementation!!.instructions[index] fun MutableMethod.instruction(index: Int): BuilderInstruction = this.implementation!!.instructions[index]
/**
* Get an instruction at the given index in the method's implementation.
* @param index The index to get the instruction at.
* @param T The type of instruction to return.
* @return The instruction.
*/
fun <T> MutableMethod.instruction(index: Int): T = instruction(index) as T
/** /**
* Add smali instructions to the method. * Add smali instructions to the method.
* @param index The index to insert the instructions at. * @param index The index to insert the instructions at.
@@ -142,10 +135,10 @@ fun MutableMethod.addInstructions(index: Int, smali: String, externalLabels: Lis
// Add the compiled list of instructions to the method. // Add the compiled list of instructions to the method.
val methodImplementation = this.implementation!! val methodImplementation = this.implementation!!
methodImplementation.addInstructions(index, compiledInstructions) methodImplementation.addInstructions(index, compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size))
val methodInstructions = methodImplementation.instructions val methodInstructions = methodImplementation.instructions
methodInstructions.subList(index, index + compiledInstructions.size) methodInstructions.subList(index, index + compiledInstructions.size - externalLabels.size)
.forEachIndexed { compiledInstructionIndex, compiledInstruction -> .forEachIndexed { compiledInstructionIndex, compiledInstruction ->
// If the compiled instruction is not an offset instruction, skip it. // If the compiled instruction is not an offset instruction, skip it.
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
@@ -242,21 +235,4 @@ internal fun parametersEqual(
internal val nullOutputStream = object : OutputStream() { internal val nullOutputStream = object : OutputStream() {
override fun write(b: Int) {} override fun write(b: Int) {}
}
/**
* Should be used to parse a list of parameters represented by their first letter,
* or in the case of arrays prefixed with an unspecified amount of '[' character.
*/
internal fun String.parseParameters(): List<String> {
val parameters = mutableListOf<String>()
var parameter = ""
for (char in this.toCharArray()) {
parameter += char
if (char == '[') continue
parameters.add(parameter)
parameter = ""
}
return parameters
} }

View File

@@ -2,31 +2,11 @@ package app.revanced.patcher.fingerprint.method.annotation
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
/**
* Annotations for a method which matches to a [MethodFingerprint].
* @param definingClass The defining class name of the method.
* @param name A suggestive name for the method which the [MethodFingerprint] was created for.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class MatchingMethod(
val definingClass: String = "L<unspecified-class>;",
val name: String = "<unspecified-method>"
)
/** /**
* Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm. * Annotations to scan a pattern [MethodFingerprint] with fuzzy algorithm.
* @param threshold if [threshold] or more of the opcodes do not match, skip. * @param threshold if [threshold] or more of the opcodes do not match, skip.
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class FuzzyPatternScanMethod( annotation class FuzzyPatternScanMethod(
val threshold: Int = 1 val threshold: Int = 1
) )
/**
* Annotations to scan a pattern [MethodFingerprint] directly.
*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class DirectPatternScanMethod

View File

@@ -1,15 +1,19 @@
package app.revanced.patcher.fingerprint.method.impl package app.revanced.patcher.fingerprint.method.impl
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.impl.MethodNotFoundException import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.extensions.MethodFingerprintExtensions.name import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyScanThreshold
import app.revanced.patcher.extensions.softCompareTo import app.revanced.patcher.extensions.parametersEqual
import app.revanced.patcher.fingerprint.Fingerprint import app.revanced.patcher.fingerprint.Fingerprint
import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.util.proxy.ClassProxy import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.Opcode import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.util.MethodUtil
/** /**
* Represents the [MethodFingerprint] for a method. * Represents the [MethodFingerprint] for a method.
@@ -31,32 +35,262 @@ abstract class MethodFingerprint(
) : Fingerprint { ) : Fingerprint {
/** /**
* The result of the [MethodFingerprint] the [Method]. * The result of the [MethodFingerprint] the [Method].
* @throws MethodNotFoundException If the resolution of the [Method] has not happened.
*/ */
var result: MethodFingerprintResult? = null var result: MethodFingerprintResult? = null
get() = field ?: throw Exception("${this.name} has not been resolved yet.")
companion object {
/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
*
* @param classes The classes on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun Iterable<MethodFingerprint>.resolve(context: BytecodeContext, classes: Iterable<ClassDef>) {
for (fingerprint in this) // For each fingerprint
classes@ for (classDef in classes) // search through all classes for the fingerprint
if (fingerprint.resolve(context, classDef))
break@classes // if the resolution succeeded, continue with the next fingerprint
}
/**
* Resolve a [MethodFingerprint] against a [ClassDef].
*
* @param forClass The class on which to resolve the [MethodFingerprint] in.
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean {
for (method in forClass.methods)
if (this.resolve(context, method, forClass))
return true
return false
}
/**
* Resolve a [MethodFingerprint] against a [Method].
*
* @param method The class on which to resolve the [MethodFingerprint] in.
* @param forClass The class on which to resolve the [MethodFingerprint].
* @param context The [BytecodeContext] to host proxies.
* @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise.
*/
fun MethodFingerprint.resolve(context: BytecodeContext, method: Method, forClass: ClassDef): Boolean {
val methodFingerprint = this
if (methodFingerprint.result != null) return true
if (methodFingerprint.returnType != null && !method.returnType.startsWith(methodFingerprint.returnType))
return false
if (methodFingerprint.access != null && methodFingerprint.access != method.accessFlags)
return false
if (methodFingerprint.parameters != null && !parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters()
method.parameterTypes
)
) return false
@Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(method))
return false
val stringsScanResult: StringsScanResult? =
if (methodFingerprint.strings != null) {
StringsScanResult(
buildList {
val implementation = method.implementation ?: return false
val stringsList = methodFingerprint.strings.toMutableList()
implementation.instructions.forEachIndexed { instructionIndex, instruction ->
if (
instruction.opcode != Opcode.CONST_STRING &&
instruction.opcode != Opcode.CONST_STRING_JUMBO
) return@forEachIndexed
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst(string::contains)
if (index == -1) return@forEachIndexed
add(
StringMatch(
string,
instructionIndex
)
)
stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return false
}
)
} else null
val patternScanResult = if (methodFingerprint.opcodes != null) {
method.implementation?.instructions ?: return false
method.patternScan(methodFingerprint) ?: return false
} else null
methodFingerprint.result = MethodFingerprintResult(
method,
forClass,
MethodFingerprintResult.MethodFingerprintScanResult(
patternScanResult,
stringsScanResult
),
context
)
return true
}
private fun Method.patternScan(
fingerprint: MethodFingerprint
): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyScanThreshold
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result =
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult(
index,
index + patternIndex
)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.createWarnings(pattern, instructions)
return result
}
}
return null
}
private fun MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.createWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {
for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(
MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult.Warning(
originalOpcode,
patternOpcode,
instructionIndex,
patternIndex
)
)
}
}
}
} }
private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch
private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult
/** /**
* Represents the result of a [MethodFingerprintUtils]. * Represents the result of a [MethodFingerprintResult].
*
* @param method The matching method. * @param method The matching method.
* @param classDef The [ClassDef] that contains the matching [method]. * @param classDef The [ClassDef] that contains the matching [method].
* @param patternScanResult Opcodes pattern scan result. * @param scanResult The result of scanning for the [MethodFingerprint].
* @param data The [BytecodeData] this [MethodFingerprintResult] is attached to, to create proxies. * @param context The [BytecodeContext] this [MethodFingerprintResult] is attached to, to create proxies.
*/ */
data class MethodFingerprintResult( data class MethodFingerprintResult(
val method: Method, val method: Method,
val classDef: ClassDef, val classDef: ClassDef,
val patternScanResult: PatternScanResult?, val scanResult: MethodFingerprintScanResult,
internal val data: BytecodeData internal val context: BytecodeContext
) { ) {
/**
* The result of scanning on the [MethodFingerprint].
* @param patternScanResult The result of the pattern scan.
* @param stringsScanResult The result of the string scan.
*/
data class MethodFingerprintScanResult(
val patternScanResult: PatternScanResult?,
val stringsScanResult: StringsScanResult?
) {
/**
* The result of scanning strings on the [MethodFingerprint].
* @param matches The list of strings that were matched.
*/
data class StringsScanResult(val matches: List<StringMatch>) {
/**
* Represents a match for a string at an index.
* @param string The string that was matched.
* @param index The index of the string.
*/
data class StringMatch(val string: String, val index: Int)
}
/**
* The result of a pattern scan.
* @param startIndex The start index of the instructions where to which this pattern matches.
* @param endIndex The end index of the instructions where to which this pattern matches.
* @param warnings A list of warnings considering this [PatternScanResult].
*/
data class PatternScanResult(
val startIndex: Int,
val endIndex: Int,
var warnings: List<Warning>? = null
) {
/**
* Represents warnings of the pattern scan.
* @param correctOpcode The opcode the instruction list has.
* @param wrongOpcode The opcode the pattern list of the signature currently has.
* @param instructionIndex The index of the opcode relative to the instruction list.
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
*/
data class Warning(
val correctOpcode: Opcode,
val wrongOpcode: Opcode,
val instructionIndex: Int,
val patternIndex: Int,
)
}
}
/** /**
* Returns a mutable clone of [classDef] * Returns a mutable clone of [classDef]
* *
* Please note, this method allocates a [ClassProxy]. * Please note, this method allocates a [ClassProxy].
* Use [classDef] where possible. * Use [classDef] where possible.
*/ */
val mutableClass by lazy { data.proxy(classDef).resolve() } @Suppress("MemberVisibilityCanBePrivate")
val mutableClass by lazy { context.proxy(classDef).mutableClass }
/** /**
* Returns a mutable clone of [method] * Returns a mutable clone of [method]
@@ -66,33 +300,7 @@ data class MethodFingerprintResult(
*/ */
val mutableMethod by lazy { val mutableMethod by lazy {
mutableClass.methods.first { mutableClass.methods.first {
it.softCompareTo(this.method) MethodUtil.methodSignaturesMatch(it, this.method)
} }
} }
}
/**
* The result of a pattern scan.
* @param startIndex The start index of the instructions where to which this pattern matches.
* @param endIndex The end index of the instructions where to which this pattern matches.
* @param warnings A list of warnings considering this [PatternScanResult].
*/
data class PatternScanResult(
val startIndex: Int,
val endIndex: Int,
var warnings: List<Warning>? = null
) {
/**
* Represents warnings of the pattern scan.
* @param correctOpcode The opcode the instruction list has.
* @param wrongOpcode The opcode the pattern list of the signature currently has.
* @param instructionIndex The index of the opcode relative to the instruction list.
* @param patternIndex The index of the opcode relative to the pattern list from the signature.
*/
data class Warning(
val correctOpcode: Opcode,
val wrongOpcode: Opcode,
val instructionIndex: Int,
val patternIndex: Int,
)
} }

View File

@@ -1,158 +0,0 @@
package app.revanced.patcher.fingerprint.method.utils
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyPatternScanMethod
import app.revanced.patcher.extensions.MethodFingerprintExtensions.fuzzyScanThreshold
import app.revanced.patcher.extensions.parametersEqual
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult
import app.revanced.patcher.fingerprint.method.impl.PatternScanResult
import org.jf.dexlib2.Opcode
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.Instruction
import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.StringReference
/**
* Utility class for [MethodFingerprint]
*/
object MethodFingerprintUtils {
/**
* Resolve a list of [MethodFingerprint] against a list of [ClassDef].
* @param context The classes on which to resolve the [MethodFingerprint].
* @param forData The [BytecodeData] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun Iterable<MethodFingerprint>.resolve(forData: BytecodeData, context: Iterable<ClassDef>) {
for (fingerprint in this) // For each fingerprint
classes@ for (classDef in context) // search through all classes for the fingerprint
if (fingerprint.resolve(forData, classDef))
break@classes // if the resolution succeeded, continue with the next fingerprint
}
/**
* Resolve a [MethodFingerprint] against a [ClassDef].
* @param context The class on which to resolve the [MethodFingerprint].
* @param forData The [BytecodeData] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolve(forData: BytecodeData, context: ClassDef): Boolean {
for (method in context.methods)
if (this.resolve(forData, method, context))
return true
return false
}
/**
* Resolve a [MethodFingerprint] against a [Method].
* @param context The context on which to resolve the [MethodFingerprint].
* @param classDef The class of the matching [Method].
* @param forData The [BytecodeData] to host proxies.
* @return True if the resolution was successful, false otherwise.
*/
fun MethodFingerprint.resolve(forData: BytecodeData, context: Method, classDef: ClassDef): Boolean {
val methodFingerprint = this
if (methodFingerprint.returnType != null && !context.returnType.startsWith(methodFingerprint.returnType))
return false
if (methodFingerprint.access != null && methodFingerprint.access != context.accessFlags)
return false
if (methodFingerprint.parameters != null && !parametersEqual(
methodFingerprint.parameters, // TODO: parseParameters()
context.parameterTypes
)
) return false
if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!(context))
return false
if (methodFingerprint.strings != null) {
val implementation = context.implementation ?: return false
val stringsList = methodFingerprint.strings.toMutableList()
implementation.instructions.forEach { instruction ->
if (instruction.opcode.ordinal != Opcode.CONST_STRING.ordinal) return@forEach
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
val index = stringsList.indexOfFirst { it == string }
if (index != -1) stringsList.removeAt(index)
}
if (stringsList.isNotEmpty()) return false
}
val patternScanResult = if (methodFingerprint.opcodes != null) {
context.implementation?.instructions ?: return false
context.patternScan(methodFingerprint) ?: return false
} else null
methodFingerprint.result = MethodFingerprintResult(context, classDef, patternScanResult, forData)
return true
}
private fun Method.patternScan(
fingerprint: MethodFingerprint
): PatternScanResult? {
val instructions = this.implementation!!.instructions
val fingerprintFuzzyPatternScanThreshold = fingerprint.fuzzyScanThreshold
val pattern = fingerprint.opcodes!!
val instructionLength = instructions.count()
val patternLength = pattern.count()
for (index in 0 until instructionLength) {
var patternIndex = 0
var threshold = fingerprintFuzzyPatternScanThreshold
while (index + patternIndex < instructionLength) {
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
// reaching maximum threshold (0) means,
// the pattern does not match to the current instructions
if (threshold-- == 0) break
}
if (patternIndex < patternLength - 1) {
// if the entire pattern has not been scanned yet
// continue the scan
patternIndex++
continue
}
// the pattern is valid, generate warnings if fuzzyPatternScanMethod is FuzzyPatternScanMethod
val result = PatternScanResult(index, index + patternIndex)
if (fingerprint.fuzzyPatternScanMethod !is FuzzyPatternScanMethod) return result
result.warnings = result.createWarnings(pattern, instructions)
return result
}
}
return null
}
}
private fun PatternScanResult.createWarnings(
pattern: Iterable<Opcode?>, instructions: Iterable<Instruction>
) = buildList {
for ((patternIndex, instructionIndex) in (this@createWarnings.startIndex until this@createWarnings.endIndex).withIndex()) {
val originalOpcode = instructions.elementAt(instructionIndex).opcode
val patternOpcode = pattern.elementAt(patternIndex)
if (patternOpcode == null || patternOpcode.ordinal == originalOpcode.ordinal) continue
this.add(PatternScanResult.Warning(originalOpcode, patternOpcode, instructionIndex, patternIndex))
}
}
private operator fun ClassDef.component1() = this
private operator fun ClassDef.component2() = this.methods

View File

@@ -0,0 +1,18 @@
package app.revanced.patcher.patch
/**
* A container for patch options.
*/
abstract class OptionsContainer {
/**
* A list of [PatchOption]s.
* @see PatchOptions
*/
@Suppress("MemberVisibilityCanBePrivate")
val options = PatchOptions()
protected fun <T> option(opt: PatchOption<T>): PatchOption<T> {
options.register(opt)
return opt
}
}

View File

@@ -1,34 +1,44 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import app.revanced.patcher.data.Data import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.patch.impl.BytecodePatch import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.impl.ResourcePatch import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import java.io.Closeable import java.io.Closeable
/** /**
* A ReVanced patch. * A ReVanced patch.
* *
* Can either be a [ResourcePatch] or a [BytecodePatch].
* If it implements [Closeable], it will be closed after all patches have been executed. * If it implements [Closeable], it will be closed after all patches have been executed.
* Closing will be done in reverse execution order. * Closing will be done in reverse execution order.
*/ */
abstract class Patch<out T : Data> { sealed interface Patch<out T : Context> : Closeable {
/** /**
* The main function of the [Patch] which the patcher will call. * The main function of the [Patch] which the patcher will call.
*
* @param context The [Context] the patch will work on.
* @return The result of executing the patch.
*/ */
abstract fun execute(data: @UnsafeVariance T): PatchResult fun execute(context: @UnsafeVariance T): PatchResult
/**
* The closing function for this patch.
*
* This can be treated like popping the patch from the current patch stack.
*/
override fun close() {}
} }
abstract class OptionsContainer { /**
/** * Resource patch for the Patcher.
* A list of [PatchOption]s. */
* @see PatchOptions interface ResourcePatch : Patch<ResourceContext>
*/
@Suppress("MemberVisibilityCanBePrivate")
val options = PatchOptions()
protected fun option(opt: PatchOption<*>): PatchOption<*> { /**
options.register(opt) * Bytecode patch for the Patcher.
return opt *
} * @param fingerprints A list of [MethodFingerprint] this patch relies on.
} */
abstract class BytecodePatch(
internal val fingerprints: Iterable<MethodFingerprint>? = null
) : Patch<BytecodeContext>

View File

@@ -2,8 +2,8 @@
package app.revanced.patcher.patch package app.revanced.patcher.patch
import java.io.File
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.pathString
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
class NoSuchOptionException(val option: String) : Exception("No such option: $option") class NoSuchOptionException(val option: String) : Exception("No such option: $option")
@@ -35,8 +35,22 @@ class PatchOptions(vararg options: PatchOption<*>) : Iterable<PatchOption<*>> {
* Get a [PatchOption] by its key. * Get a [PatchOption] by its key.
* @param key The key of the [PatchOption]. * @param key The key of the [PatchOption].
*/ */
@JvmName("getUntyped")
operator fun get(key: String) = register[key] ?: throw NoSuchOptionException(key) operator fun get(key: String) = register[key] ?: throw NoSuchOptionException(key)
/**
* Get a [PatchOption] by its key.
* @param key The key of the [PatchOption].
*/
inline operator fun <reified T> get(key: String): PatchOption<T> {
val opt = get(key)
if (opt.value !is T) throw InvalidTypeException(
opt.value?.let { it::class.java.canonicalName } ?: "null",
T::class.java.canonicalName
)
return opt as PatchOption<T>
}
/** /**
* Set the value of a [PatchOption]. * Set the value of a [PatchOption].
* @param key The key of the [PatchOption]. * @param key The key of the [PatchOption].
@@ -44,7 +58,7 @@ class PatchOptions(vararg options: PatchOption<*>) : Iterable<PatchOption<*>> {
* Please note that using the wrong value type results in a runtime error. * Please note that using the wrong value type results in a runtime error.
*/ */
inline operator fun <reified T> set(key: String, value: T) { inline operator fun <reified T> set(key: String, value: T) {
@Suppress("UNCHECKED_CAST") val opt = get(key) as PatchOption<T> val opt = get<T>(key)
if (opt.value !is T) throw InvalidTypeException( if (opt.value !is T) throw InvalidTypeException(
T::class.java.canonicalName, T::class.java.canonicalName,
opt.value?.let { it::class.java.canonicalName } ?: "null" opt.value?.let { it::class.java.canonicalName } ?: "null"
@@ -101,7 +115,8 @@ sealed class PatchOption<T>(
* Gets the value of the option. * Gets the value of the option.
* Please note that using the wrong value type results in a runtime error. * Please note that using the wrong value type results in a runtime error.
*/ */
inline operator fun <reified V> getValue(thisRef: Any?, property: KProperty<*>): V? { @JvmName("getValueTyped")
inline operator fun <reified V> getValue(thisRef: Nothing?, property: KProperty<*>): V? {
if (value !is V?) throw InvalidTypeException( if (value !is V?) throw InvalidTypeException(
V::class.java.canonicalName, V::class.java.canonicalName,
value?.let { it::class.java.canonicalName } ?: "null" value?.let { it::class.java.canonicalName } ?: "null"
@@ -109,11 +124,14 @@ sealed class PatchOption<T>(
return value as? V? return value as? V?
} }
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
/** /**
* Gets the value of the option. * Gets the value of the option.
* Please note that using the wrong value type results in a runtime error. * Please note that using the wrong value type results in a runtime error.
*/ */
inline operator fun <reified V> setValue(thisRef: Any?, property: KProperty<*>, new: V) { @JvmName("setValueTyped")
inline operator fun <reified V> setValue(thisRef: Nothing?, property: KProperty<*>, new: V) {
if (value !is V) throw InvalidTypeException( if (value !is V) throw InvalidTypeException(
V::class.java.canonicalName, V::class.java.canonicalName,
value?.let { it::class.java.canonicalName } ?: "null" value?.let { it::class.java.canonicalName } ?: "null"
@@ -121,6 +139,10 @@ sealed class PatchOption<T>(
value = new as T value = new as T
} }
operator fun setValue(thisRef: Any?, property: KProperty<*>, new: T?) {
value = new
}
/** /**
* A [PatchOption] representing a [String]. * A [PatchOption] representing a [String].
* @see PatchOption * @see PatchOption
@@ -209,34 +231,18 @@ sealed class PatchOption<T>(
) )
/** /**
* A [PatchOption] representing a [Path]. * A [PatchOption] representing a [Path], backed by a [String].
* The validator passes a [String], if you need a [Path] you will have to convert it yourself.
* @see PatchOption * @see PatchOption
*/ */
open class PathOption( class PathOption(
key: String, key: String,
default: Path?, default: Path?,
title: String, title: String,
description: String, description: String,
required: Boolean = false, required: Boolean = false,
validator: (Path?) -> Boolean = { true } validator: (String?) -> Boolean = { true }
) : PatchOption<Path>( ) : PatchOption<String>(
key, default, title, description, required, validator key, default?.pathString, title, description, required, validator
)
/**
* A [PathOption] of type [File].
* @see PathOption
*/
class FileOption(
key: String,
default: File?,
title: String,
description: String,
required: Boolean = false,
validator: (File?) -> Boolean = { true }
) : PathOption(
key, default?.toPath(), title, description, required, {
validator(it?.toFile())
}
) )
} }

View File

@@ -1,24 +1,27 @@
package app.revanced.patcher.patch.annotations package app.revanced.patcher.patch.annotations
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* Annotation to mark a Class as a patch. * Annotation to mark a class as a patch.
* @param include If false, the patch should be treated as optional by default. * @param include If false, the patch should be treated as optional by default.
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class Patch(val include: Boolean = true) annotation class Patch(val include: Boolean = true)
/** /**
* Annotation for dependencies of [Patch]es . * Annotation for dependencies of [Patch]es.
*/ */
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
annotation class DependsOn( annotation class DependsOn(
val dependencies: Array<KClass<out Patch<Data>>> = [] val dependencies: Array<KClass<out Patch<Context>>> = []
) )
/**
* Annotation to mark [Patch]es which depend on integrations.
*/
@Target(AnnotationTarget.CLASS)
annotation class RequiresIntegrations // required because integrations are decoupled from patches

View File

@@ -1,13 +0,0 @@
package app.revanced.patcher.patch.impl
import app.revanced.patcher.data.impl.BytecodeData
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.patch.Patch
/**
* Bytecode patch for the Patcher.
* @param fingerprints A list of [MethodFingerprint] this patch relies on.
*/
abstract class BytecodePatch(
internal val fingerprints: Iterable<MethodFingerprint>? = null
) : Patch<BytecodeData>()

View File

@@ -1,9 +0,0 @@
package app.revanced.patcher.patch.impl
import app.revanced.patcher.data.impl.ResourceData
import app.revanced.patcher.patch.Patch
/**
* Resource patch for the Patcher.
*/
abstract class ResourcePatch : Patch<ResourceData>()

View File

@@ -0,0 +1,214 @@
package app.revanced.patcher.util
import app.revanced.patcher.PatcherContext
import app.revanced.patcher.extensions.or
import app.revanced.patcher.logging.Logger
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.TypeUtil.traverseClassHierarchy
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableField
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.iface.ClassDef
import org.jf.dexlib2.util.MethodUtil
import kotlin.reflect.KFunction2
/**
* 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 {
/**
* Merge a class with [otherClass].
*
* @param otherClass The class to merge with
* @param context The context to traverse the class hierarchy in.
* @param logger A logger.
*/
fun ClassDef.merge(otherClass: ClassDef, context: PatcherContext, logger: Logger? = null) = this
//.fixFieldAccess(otherClass, logger)
//.fixMethodAccess(otherClass, logger)
.addMissingFields(otherClass, logger)
.addMissingMethods(otherClass, logger)
.publicize(otherClass, context, logger)
/**
* Add methods which are missing but existing in [fromClass].
*
* @param fromClass The class to add missing methods from.
* @param logger A logger.
*/
private fun ClassDef.addMissingMethods(fromClass: ClassDef, logger: Logger? = null): ClassDef {
val missingMethods = fromClass.methods.let { fromMethods ->
methods.filterNot { method ->
fromMethods.any { fromMethod ->
MethodUtil.methodSignaturesMatch(fromMethod, method)
}
}
}
if (missingMethods.isEmpty()) return this
logger?.trace("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.
* @param logger A logger.
*/
private fun ClassDef.addMissingFields(fromClass: ClassDef, logger: Logger? = null): ClassDef {
val missingFields = fields.filterNotAny(fromClass.fields) { field, fromField ->
fromField.name == field.name
}
if (missingFields.isEmpty()) return this
logger?.trace("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.
* @param logger A logger.
*/
private fun ClassDef.publicize(reference: ClassDef, context: PatcherContext, logger: Logger? = null) =
if (reference.accessFlags.isPublic() && !accessFlags.isPublic())
this.asMutableClass().apply {
context.bytecodeContext.traverseClassHierarchy(this) {
if (accessFlags.isPublic()) return@traverseClassHierarchy
logger?.trace("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.
* @param logger A logger.
*/
private fun ClassDef.fixFieldAccess(reference: ClassDef, logger: Logger? = null): 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?.trace("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.
* @param logger A logger.
*/
private fun ClassDef.fixMethodAccess(reference: ClassDef, logger: Logger? = null): 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?.trace("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 {
fun ClassDef.asMutableClass() = if (this is MutableClass) this else 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() = this.or(AccessFlags.PUBLIC).and(AccessFlags.PRIVATE.value.inv())
/**
* Filter [this] on [needles] matching the given [predicate].
*
* @param this The hay to filter for [needles].
* @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 this The hay to filter for [needles].
* @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

@@ -3,40 +3,44 @@ package app.revanced.patcher.util
import app.revanced.patcher.util.proxy.ClassProxy import app.revanced.patcher.util.proxy.ClassProxy
import org.jf.dexlib2.iface.ClassDef import org.jf.dexlib2.iface.ClassDef
class ProxyBackedClassList(internal val internalClasses: MutableList<ClassDef>) : List<ClassDef> { /**
private val internalProxies = mutableListOf<ClassProxy>() * A class that represents a set of classes and proxies.
internal val proxies: List<ClassProxy> = internalProxies *
* @param classes The classes to be backed by proxies.
fun add(classDef: ClassDef) = internalClasses.add(classDef) */
fun add(classProxy: ClassProxy) = internalProxies.add(classProxy) class ProxyBackedClassList(internal val classes: MutableList<ClassDef>) : Set<ClassDef> {
internal val proxies = mutableListOf<ClassProxy>()
/** /**
* Apply all resolved classes into [internalClasses] and clean the [proxies] list. * Add a [ClassDef].
*/ */
internal fun applyProxies() { fun add(classDef: ClassDef) = classes.add(classDef)
// FIXME: check if this could cause issues when multiple patches use the same proxy
internalProxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list
if (!proxy.proxyUsed) return@removeIf false
// if it has been used, replace the internal class which it proxied /**
val index = internalClasses.indexOfFirst { it.type == proxy.immutableClass.type } * Add a [ClassProxy].
internalClasses[index] = proxy.mutatedClass */
fun add(classProxy: ClassProxy) = proxies.add(classProxy)
/**
* Replace all classes with their mutated versions.
*/
internal fun replaceClasses() =
proxies.removeIf { proxy ->
// if the proxy is unused, keep it in the list
if (!proxy.resolved) return@removeIf false
// if it has been used, replace the original class with the new class
val index = classes.indexOfFirst { it.type == proxy.immutableClass.type }
classes[index] = proxy.mutableClass
// return true to remove it from the proxies list // return true to remove it from the proxies list
return@removeIf true return@removeIf true
} }
}
override val size get() = internalClasses.size
override fun contains(element: ClassDef) = internalClasses.contains(element) override val size get() = classes.size
override fun containsAll(elements: Collection<ClassDef>) = internalClasses.containsAll(elements) override fun contains(element: ClassDef) = classes.contains(element)
override fun get(index: Int) = internalClasses[index] override fun containsAll(elements: Collection<ClassDef>) = classes.containsAll(elements)
override fun indexOf(element: ClassDef) = internalClasses.indexOf(element) override fun isEmpty() = classes.isEmpty()
override fun isEmpty() = internalClasses.isEmpty() override fun iterator() = classes.iterator()
override fun iterator() = internalClasses.iterator()
override fun lastIndexOf(element: ClassDef) = internalClasses.lastIndexOf(element)
override fun listIterator() = internalClasses.listIterator()
override fun listIterator(index: Int) = internalClasses.listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int) = internalClasses.subList(fromIndex, toIndex)
} }

View File

@@ -0,0 +1,19 @@
package app.revanced.patcher.util
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
object TypeUtil {
/**
* 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 BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: MutableClass.() -> Unit) {
callback(targetClass)
this.findClass(targetClass.superclass ?: return)?.mutableClass?.let {
traverseClassHierarchy(it, callback)
}
}
}

View File

@@ -1,25 +1,27 @@
package app.revanced.patcher.util.method package app.revanced.patcher.util.method
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.data.impl.MethodNotFoundException
import app.revanced.patcher.extensions.softCompareTo
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import org.jf.dexlib2.iface.Method import org.jf.dexlib2.iface.Method
import org.jf.dexlib2.iface.instruction.ReferenceInstruction import org.jf.dexlib2.iface.instruction.ReferenceInstruction
import org.jf.dexlib2.iface.reference.MethodReference import org.jf.dexlib2.iface.reference.MethodReference
import org.jf.dexlib2.util.MethodUtil
/** /**
* Find a method from another method via instruction offsets. * Find a method from another method via instruction offsets.
* @param bytecodeData The bytecodeData to use when resolving the next method reference. * @param bytecodeContext The context to use when resolving the next method reference.
* @param currentMethod The method to start from. * @param currentMethod The method to start from.
*/ */
class MethodWalker internal constructor( class MethodWalker internal constructor(
private val bytecodeData: BytecodeData, private val bytecodeContext: BytecodeContext,
private var currentMethod: Method private var currentMethod: Method
) { ) {
/** /**
* Get the method which was walked last. * Get the method which was walked last.
*
* It is possible to cast this method to a [MutableMethod], if the method has been walked mutably. * It is possible to cast this method to a [MutableMethod], if the method has been walked mutably.
*
* @return The method which was walked last.
*/ */
fun getMethod(): Method { fun getMethod(): Method {
return currentMethod return currentMethod
@@ -27,25 +29,28 @@ class MethodWalker internal constructor(
/** /**
* Walk to a method defined at the offset in the instruction list of the current method. * Walk to a method defined at the offset in the instruction list of the current method.
*
* The current method will be mutable.
*
* @param offset The offset of the instruction. This instruction must be of format 35c. * @param offset The offset of the instruction. This instruction must be of format 35c.
* @param walkMutable If this is true, the class of the method will be resolved mutably. * @param walkMutable If this is true, the class of the method will be resolved mutably.
* The current method will be mutable. * @return The same [MethodWalker] instance with the method at [offset].
*/ */
fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker { fun nextMethod(offset: Int, walkMutable: Boolean = false): MethodWalker {
currentMethod.implementation?.instructions?.let { instructions -> currentMethod.implementation?.instructions?.let { instructions ->
val instruction = instructions.elementAt(offset) val instruction = instructions.elementAt(offset)
val newMethod = (instruction as ReferenceInstruction).reference as MethodReference val newMethod = (instruction as ReferenceInstruction).reference as MethodReference
val proxy = bytecodeData.findClass(newMethod.definingClass)!! val proxy = bytecodeContext.findClass(newMethod.definingClass)!!
val methods = if (walkMutable) proxy.resolve().methods else proxy.immutableClass.methods val methods = if (walkMutable) proxy.mutableClass.methods else proxy.immutableClass.methods
currentMethod = methods.first { it -> currentMethod = methods.first {
return@first it.softCompareTo(newMethod) return@first MethodUtil.methodSignaturesMatch(it, newMethod)
} }
return this return this
} }
throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}") throw MethodNotFoundException("This method can not be walked at offset $offset inside the method ${currentMethod.name}")
} }
internal class MethodNotFoundException(exception: String) : Exception(exception)
} }

View File

@@ -1,18 +1,75 @@
@file:Suppress("unused")
package app.revanced.patcher.util.patch package app.revanced.patcher.util.patch
import app.revanced.patcher.data.Data import app.revanced.patcher.data.Context
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.Patch
import org.jf.dexlib2.DexFileFactory
import java.io.File import java.io.File
import java.net.URLClassLoader
import java.util.jar.JarFile
/** /**
* A patch bundle.
* @param path The path to the patch bundle. * @param path The path to the patch bundle.
*/ */
abstract class PatchBundle(path: String) : File(path) { sealed class PatchBundle(path: String) : File(path) {
internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList { internal fun loadPatches(classLoader: ClassLoader, classNames: Iterator<String>) = buildList {
for (className in classNames) { classNames.forEach { className ->
val clazz = classLoader.loadClass(className) val clazz = classLoader.loadClass(className)
if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) continue if (!clazz.isAnnotationPresent(app.revanced.patcher.patch.annotations.Patch::class.java)) return@forEach
@Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Data>>) @Suppress("UNCHECKED_CAST") this.add(clazz as Class<out Patch<Context>>)
} }
}.sortedBy { it.patchName }
/**
* A patch bundle of type [Jar].
*
* @param patchBundlePath The path to the patch bundle.
*/
class Jar(patchBundlePath: String) : PatchBundle(patchBundlePath) {
/**
* Load patches from the patch bundle.
*
* Patches will be loaded with a new [URLClassLoader].
*/
fun loadPatches() = loadPatches(
URLClassLoader(
arrayOf(this.toURI().toURL()),
Thread.currentThread().contextClassLoader // TODO: find out why this is required
),
StringIterator(
JarFile(this)
.entries()
.toList() // TODO: find a cleaner solution than that to filter non class files
.filter {
it.name.endsWith(".class") && !it.name.contains("$")
}
.iterator()
) {
it.realName.replace('/', '.').replace(".class", "")
}
)
}
/**
* A patch bundle of type [Dex] format.
*
* @param patchBundlePath The path to a patch bundle of dex format.
* @param dexClassLoader The dex class loader.
*/
class Dex(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
/**
* Load patches from the patch bundle.
*
* Patches will be loaded to the provided [dexClassLoader].
*/
fun loadPatches() = loadPatches(dexClassLoader,
StringIterator(DexFileFactory.loadDexFile(path, null).classes.iterator()) { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
})
} }
} }

View File

@@ -1,17 +0,0 @@
package app.revanced.patcher.util.patch.impl
import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.patcher.util.patch.StringIterator
import org.jf.dexlib2.DexFileFactory
/**
* A patch bundle of the ReVanced [DexPatchBundle] format.
* @param patchBundlePath The path to a patch bundle of dex format.
* @param dexClassLoader The dex class loader.
*/
class DexPatchBundle(patchBundlePath: String, private val dexClassLoader: ClassLoader) : PatchBundle(patchBundlePath) {
fun loadPatches() = loadPatches(dexClassLoader,
StringIterator(DexFileFactory.loadDexFile(path, null).classes.iterator()) { classDef ->
classDef.type.substring(1, classDef.length - 1).replace('/', '.')
})
}

View File

@@ -1,30 +0,0 @@
package app.revanced.patcher.util.patch.impl
import app.revanced.patcher.util.patch.PatchBundle
import app.revanced.patcher.util.patch.StringIterator
import java.net.URLClassLoader
import java.util.jar.JarFile
/**
* A patch bundle of the ReVanced [JarPatchBundle] format.
* @param patchBundlePath The path to the patch bundle.
*/
class JarPatchBundle(patchBundlePath: String) : PatchBundle(patchBundlePath) {
fun loadPatches() = loadPatches(
URLClassLoader(
arrayOf(this.toURI().toURL()),
Thread.currentThread().contextClassLoader // TODO: find out why this is required
),
StringIterator(
JarFile(this)
.entries()
.toList() // TODO: find a cleaner solution than that to filter non class files
.filter {
it.name.endsWith(".class") && !it.name.contains("$")
}
.iterator()
) {
it.realName.replace('/', '.').replace(".class", "")
}
)
}

View File

@@ -8,34 +8,26 @@ import org.jf.dexlib2.iface.ClassDef
* *
* A class proxy simply holds a reference to the original class * A class proxy simply holds a reference to the original class
* and allocates a mutable clone for the original class if needed. * and allocates a mutable clone for the original class if needed.
* @param immutableClass The class to proxy * @param immutableClass The class to proxy.
*/ */
class ClassProxy( class ClassProxy internal constructor(
val immutableClass: ClassDef, val immutableClass: ClassDef,
) { ) {
internal var proxyUsed = false /**
internal lateinit var mutatedClass: MutableClass * Weather the proxy was actually used.
*/
init { internal var resolved = false
// in the instance, that a [MutableClass] is being proxied,
// do not create an additional clone and reuse the [MutableClass] instance
if (immutableClass is MutableClass) {
mutatedClass = immutableClass
proxyUsed = true
}
}
/** /**
* Allocates and returns a mutable clone of the original class. * The mutable clone of the original class.
* A patch should always use the original immutable class reference *
* to avoid unnecessary allocations for the mutable class. * Note: This is only allocated if the proxy is actually used.
* @return A mutable clone of the original class.
*/ */
fun resolve(): MutableClass { val mutableClass by lazy {
if (!proxyUsed) { resolved = true
proxyUsed = true if (immutableClass is MutableClass) {
mutatedClass = MutableClass(immutableClass) immutableClass
} } else
return mutatedClass MutableClass(immutableClass)
} }
} }

View File

@@ -20,6 +20,22 @@ class MutableMethod(method: Method) : Method, BaseMethodReference() {
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() } private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions } 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
}
override fun getDefiningClass(): String { override fun getDefiningClass(): String {
return definingClass return definingClass
} }

View File

@@ -3,7 +3,8 @@ package app.revanced.patcher.patch
import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch import app.revanced.patcher.usage.bytecode.ExampleBytecodePatch
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import java.io.File import kotlin.io.path.Path
import kotlin.io.path.pathString
import kotlin.test.assertNotEquals import kotlin.test.assertNotEquals
internal class PatchOptionsTest { internal class PatchOptionsTest {
@@ -36,11 +37,13 @@ internal class PatchOptionsTest {
} }
is PatchOption.PathOption -> { is PatchOption.PathOption -> {
option.value = File("test.txt").toPath() option.value = Path("test.txt").pathString
} }
} }
} }
val option = options["key1"] val option = options.get<String>("key1")
// or: val option: String? by options["key1"]
// then you won't need `.value` every time
println(option.value) println(option.value)
options["key1"] = "Hello, world!" options["key1"] = "Hello, world!"
println(option.value) println(option.value)
@@ -60,6 +63,9 @@ internal class PatchOptionsTest {
// > options["key2"] = null // > options["key2"] = null
// is not possible because Kotlin // is not possible because Kotlin
// cannot reify the type "Nothing?". // cannot reify the type "Nothing?".
// So we have to do this instead:
options["key2"] = null as Any?
// This is a cleaner replacement for the above:
options.nullify("key2") options.nullify("key2")
} }
@@ -71,12 +77,19 @@ internal class PatchOptionsTest {
} }
@Test @Test
fun `should fail because of invalid value type`() { fun `should fail because of invalid value type when setting an option`() {
assertThrows<InvalidTypeException> { assertThrows<InvalidTypeException> {
options["key1"] = 123 options["key1"] = 123
} }
} }
@Test
fun `should fail because of invalid value type when getting an option`() {
assertThrows<InvalidTypeException> {
options.get<Int>("key1")
}
}
@Test @Test
fun `should fail because of an illegal value`() { fun `should fail because of an illegal value`() {
assertThrows<IllegalValueException> { assertThrows<IllegalValueException> {
@@ -94,7 +107,7 @@ internal class PatchOptionsTest {
@Test @Test
fun `should fail because getting a non-initialized option is illegal`() { fun `should fail because getting a non-initialized option is illegal`() {
assertThrows<RequirementNotMetException> { assertThrows<RequirementNotMetException> {
println(options["key6"].value) println(options["key5"].value)
} }
} }
} }

View File

@@ -9,6 +9,5 @@ import app.revanced.patcher.annotation.Package
)] )]
) )
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ExampleBytecodeCompatibility internal annotation class ExampleBytecodeCompatibility

View File

@@ -3,18 +3,15 @@ package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.BytecodeData import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.addInstructions import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.extensions.replaceInstruction import app.revanced.patcher.extensions.replaceInstruction
import app.revanced.patcher.patch.OptionsContainer import app.revanced.patcher.patch.*
import app.revanced.patcher.patch.PatchOption
import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.annotations.DependsOn import app.revanced.patcher.patch.annotations.DependsOn
import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.patch.impl.BytecodePatch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import app.revanced.patcher.usage.resource.patch.ExampleResourcePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
@@ -32,19 +29,18 @@ import org.jf.dexlib2.immutable.reference.ImmutableFieldReference
import org.jf.dexlib2.immutable.reference.ImmutableStringReference import org.jf.dexlib2.immutable.reference.ImmutableStringReference
import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue import org.jf.dexlib2.immutable.value.ImmutableFieldEncodedValue
import org.jf.dexlib2.util.Preconditions import org.jf.dexlib2.util.Preconditions
import java.io.File import kotlin.io.path.Path
import java.nio.file.Path
@Patch @Patch
@Name("example-bytecode-patch") @Name("example-bytecode-patch")
@Description("Example demonstration of a bytecode patch.") @Description("Example demonstration of a bytecode patch.")
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1") @Version("0.0.1")
@DependsOn([ExampleBytecodePatch::class]) @DependsOn([ExampleResourcePatch::class])
class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) { class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// This function will be executed by the patcher. // This function will be executed by the patcher.
// You can treat it as a constructor // You can treat it as a constructor
override fun execute(data: BytecodeData): PatchResult { override fun execute(context: BytecodeContext): PatchResult {
// Get the resolved method by its fingerprint from the resolver cache // Get the resolved method by its fingerprint from the resolver cache
val result = ExampleFingerprint.result!! val result = ExampleFingerprint.result!!
@@ -59,14 +55,14 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
// Let's modify it, so it prints "Hello, ReVanced! Editing bytecode." // Let's modify it, so it prints "Hello, ReVanced! Editing bytecode."
// Get the start index of our opcode pattern. // Get the start index of our opcode pattern.
// This will be the index of the instruction with the opcode CONST_STRING. // This will be the index of the instruction with the opcode CONST_STRING.
val startIndex = result.patternScanResult!!.startIndex val startIndex = result.scanResult.patternScanResult!!.startIndex
implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.") implementation.replaceStringAt(startIndex, "Hello, ReVanced! Editing bytecode.")
// Get the class in which the method matching our fingerprint is defined in. // Get the class in which the method matching our fingerprint is defined in.
val mainClass = data.findClass { val mainClass = context.findClass {
it.type == result.classDef.type it.type == result.classDef.type
}!!.resolve() }!!.mutableClass
// Add a new method returning a string // Add a new method returning a string
mainClass.methods.add( mainClass.methods.add(
@@ -170,35 +166,36 @@ class ExampleBytecodePatch : BytecodePatch(listOf(ExampleFingerprint)) {
) )
} }
@Suppress("unused")
companion object : OptionsContainer() { companion object : OptionsContainer() {
private var key1: String? by option( private var key1 by option(
PatchOption.StringOption( PatchOption.StringOption(
"key1", "default", "title", "description", true "key1", "default", "title", "description", true
) )
) )
private var key2: Boolean? by option( private var key2 by option(
PatchOption.BooleanOption( PatchOption.BooleanOption(
"key2", true, "title", "description" // required defaults to false "key2", true, "title", "description" // required defaults to false
) )
) )
private var key3: String? by option( private var key3 by option(
PatchOption.StringListOption( PatchOption.StringListOption(
"key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description" "key3", "TEST", listOf("TEST", "TEST1", "TEST2"), "title", "description"
) )
) )
private var key4: Int? by option( private var key4 by option(
PatchOption.IntListOption( PatchOption.IntListOption(
"key4", 1, listOf(1, 2, 3), "title", "description" "key4", 1, listOf(1, 2, 3), "title", "description"
) )
) )
private var key5: Path? by option( private var key5 by option(
PatchOption.PathOption( PatchOption.StringOption(
"key5", File("test.txt").toPath(), "title", "description" "key5", null, "title", "description", true
) )
) )
private var key6: String? by option( private var key6 by option(
PatchOption.StringOption( PatchOption.PathOption(
"key6", null, "title", "description", true "key6", Path("test.txt"), "title", "description", true
) )
) )
} }

View File

@@ -1,23 +1,11 @@
package app.revanced.patcher.usage.bytecode package app.revanced.patcher.usage.bytecode
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.extensions.or import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod import app.revanced.patcher.fingerprint.method.annotation.FuzzyPatternScanMethod
import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
import app.revanced.patcher.usage.bytecode.ExampleBytecodeCompatibility
import org.jf.dexlib2.AccessFlags import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode import org.jf.dexlib2.Opcode
@Name("example-fingerprint")
@MatchingMethod(
"LexampleClass;",
"exampleMehod"
)
@FuzzyPatternScanMethod(2) @FuzzyPatternScanMethod(2)
@ExampleBytecodeCompatibility
@Version("0.0.1")
object ExampleFingerprint : MethodFingerprint( object ExampleFingerprint : MethodFingerprint(
"V", "V",
AccessFlags.PUBLIC or AccessFlags.STATIC, AccessFlags.PUBLIC or AccessFlags.STATIC,

View File

@@ -9,6 +9,5 @@ import app.revanced.patcher.annotation.Package
)] )]
) )
@Target(AnnotationTarget.CLASS) @Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
internal annotation class ExampleResourceCompatibility internal annotation class ExampleResourceCompatibility

View File

@@ -3,11 +3,11 @@ package app.revanced.patcher.usage.resource.patch
import app.revanced.patcher.annotation.Description import app.revanced.patcher.annotation.Description
import app.revanced.patcher.annotation.Name import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.ResourceData import app.revanced.patcher.data.ResourceContext
import app.revanced.patcher.patch.PatchResult import app.revanced.patcher.patch.PatchResult
import app.revanced.patcher.patch.PatchResultSuccess import app.revanced.patcher.patch.PatchResultSuccess
import app.revanced.patcher.patch.ResourcePatch
import app.revanced.patcher.patch.annotations.Patch import app.revanced.patcher.patch.annotations.Patch
import app.revanced.patcher.patch.impl.ResourcePatch
import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility import app.revanced.patcher.usage.resource.annotation.ExampleResourceCompatibility
import org.w3c.dom.Element import org.w3c.dom.Element
@@ -16,9 +16,9 @@ import org.w3c.dom.Element
@Description("Example demonstration of a resource patch.") @Description("Example demonstration of a resource patch.")
@ExampleResourceCompatibility @ExampleResourceCompatibility
@Version("0.0.1") @Version("0.0.1")
class ExampleResourcePatch : ResourcePatch() { class ExampleResourcePatch : ResourcePatch {
override fun execute(data: ResourceData): PatchResult { override fun execute(context: ResourceContext): PatchResult {
data.xmlEditor["AndroidManifest.xml"].use { editor -> context.xmlEditor["AndroidManifest.xml"].use { editor ->
val element = editor // regular DomFileEditor val element = editor // regular DomFileEditor
.file .file
.getElementsByTagName("application") .getElementsByTagName("application")

View File

@@ -1,20 +0,0 @@
package app.revanced.patcher.util
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
internal class VersionReaderTest {
@Test
fun read() {
val version = VersionReader.read()
assertNotNull(version)
assertTrue(version.isNotEmpty())
val parts = version.split(".")
assertEquals(3, parts.size)
parts.forEach {
assertTrue(it.toInt() >= 0)
}
println(version)
}
}

View File

@@ -44,7 +44,7 @@ internal class InlineSmaliCompilerTest {
""" """
) )
val insn = method.instruction(insnIndex) as BuilderInstruction21t val insn = method.instruction<BuilderInstruction21t>(insnIndex)
assertEquals(targetIndex, insn.target.location.index) assertEquals(targetIndex, insn.target.location.index)
} }
@@ -73,7 +73,7 @@ internal class InlineSmaliCompilerTest {
) )
) )
val insn = method.instruction(insnIndex) as BuilderInstruction21t val insn = method.instruction<BuilderInstruction21t>(insnIndex)
assertTrue(insn.target.isPlaced, "Label was not placed") assertTrue(insn.target.isPlaced, "Label was not placed")
assertEquals(labelIndex, insn.target.location.index) assertEquals(labelIndex, insn.target.location.index)
} }