mirror of
https://github.com/ReVanced/revanced-patcher.git
synced 2026-01-11 22:06:18 +00:00
Compare commits
417 Commits
v7.1.0-dev
...
feat/moder
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91cb464a27 | ||
|
|
dc8565e8a6 | ||
|
|
1a052b9787 | ||
|
|
ed56bf49ad | ||
|
|
56fd65d6ce | ||
|
|
5674c1f2a2 | ||
|
|
d2461f92aa | ||
|
|
18570656cc | ||
|
|
005c91bc08 | ||
|
|
f17fbd8c40 | ||
|
|
2c97de2894 | ||
|
|
2509997432 | ||
|
|
58ff464192 | ||
|
|
4106ce4070 | ||
|
|
e6eaf6cb73 | ||
|
|
c56ac7a81f | ||
|
|
cdc480acea | ||
|
|
14f2eb69e4 | ||
|
|
fcdaf324fe | ||
|
|
8653d8304b | ||
|
|
e2c781f12c | ||
|
|
0b5e8b791d | ||
|
|
cf57726bbb | ||
|
|
79d3640186 | ||
|
|
3a8b2ba935 | ||
|
|
39c5a66ce3 | ||
|
|
b160a2adc0 | ||
|
|
33fadcbd0c | ||
|
|
68db95b99b | ||
|
|
4f2ef3c47c | ||
|
|
062ae14936 | ||
|
|
99f431897e | ||
|
|
d80abbcd17 | ||
|
|
509ecc81e1 | ||
|
|
e4e66b0d8b | ||
|
|
bb8771bb8b | ||
|
|
754b02e4ca | ||
|
|
fe5fb736cb | ||
|
|
fc505a8726 | ||
|
|
88a3252574 | ||
|
|
ead701bdaf | ||
|
|
0581dcf931 | ||
|
|
62191e3c4a | ||
|
|
1358d3fa10 | ||
|
|
6712f0ea72 | ||
|
|
0746c22743 | ||
|
|
7f55868e6f | ||
|
|
5d996def4d | ||
|
|
49f4570164 | ||
|
|
b8249789df | ||
|
|
0abf1c6c02 | ||
|
|
aa472eb985 | ||
|
|
ab624f04f6 | ||
|
|
21b5c079fb | ||
|
|
5024204046 | ||
|
|
a44802ef4e | ||
|
|
4c1c34ad01 | ||
|
|
b2aecb726d | ||
|
|
851f9c7885 | ||
|
|
ea6fc70caa | ||
|
|
a2875d1d64 | ||
|
|
2be6e97817 | ||
|
|
348d0070e7 | ||
|
|
d53aacdad4 | ||
|
|
f1615b7ab5 | ||
|
|
ffb1d880d7 | ||
|
|
e95f13ae3e | ||
|
|
e1b984d601 | ||
|
|
c2dc29e061 | ||
|
|
69f2f20fd9 | ||
|
|
525beda18e | ||
|
|
73d3cbf4ff | ||
|
|
70278dd79d | ||
|
|
5e98e9e30a | ||
|
|
ac1aff5a1a | ||
|
|
5481d0c54c | ||
|
|
4604742d0f | ||
|
|
4beb907a61 | ||
|
|
7f44174d91 | ||
|
|
d310246852 | ||
|
|
dcc989243c | ||
|
|
5227e98abf | ||
|
|
8c4dd5b3a3 | ||
|
|
736b3eebbf | ||
|
|
b41a542952 | ||
|
|
d21128fe2e | ||
|
|
cf4374b8cf | ||
|
|
8a30b0fa10 | ||
|
|
11a911dc67 | ||
|
|
6e3ba7419b | ||
|
|
50a66ccfed | ||
|
|
0be79840b1 | ||
|
|
d8b4c60321 | ||
|
|
f77e99e817 | ||
|
|
ea26c486c0 | ||
|
|
bebb734608 | ||
|
|
d842f82d07 | ||
|
|
82bab58ac2 | ||
|
|
90b7631d9e | ||
|
|
26d449e6d9 | ||
|
|
49466060e3 | ||
|
|
620ea5b852 | ||
|
|
3e2168a2b2 | ||
|
|
13c77967b1 | ||
|
|
f57e571a14 | ||
|
|
fe616beb22 | ||
|
|
41257ee87e | ||
|
|
33ed5f0aa3 | ||
|
|
d0a57ac00d | ||
|
|
b0b2c10665 | ||
|
|
cc183062ab | ||
|
|
fe8ea9130d | ||
|
|
f1c60093cf | ||
|
|
687b884dc4 | ||
|
|
ceb6fd51c1 | ||
|
|
0b223bfe65 | ||
|
|
308e95cf62 | ||
|
|
db8866212a | ||
|
|
608a05d9aa | ||
|
|
55746ed705 | ||
|
|
e33026c538 | ||
|
|
ff215620bb | ||
|
|
fec31f45da | ||
|
|
7684b70324 | ||
|
|
55a5d3bd4e | ||
|
|
17a4675a8e | ||
|
|
98085d1d45 | ||
|
|
bc5c16f112 | ||
|
|
f1d7217495 | ||
|
|
64dd1526cd | ||
|
|
c9a82608f7 | ||
|
|
9fc42e132c | ||
|
|
efa98ece45 | ||
|
|
68e2acebba | ||
|
|
7a7a8fc353 | ||
|
|
f8306ac43d | ||
|
|
d03591b735 | ||
|
|
4a9184597b | ||
|
|
0a482f8c9a | ||
|
|
e7dacfba8c | ||
|
|
2d7fffd4ec | ||
|
|
f8baabbcec | ||
|
|
716825f232 | ||
|
|
58bd46750b | ||
|
|
288240f163 | ||
|
|
ff02452cb8 | ||
|
|
462fbe2cad | ||
|
|
7aeae93f3d | ||
|
|
f1de9b39ef | ||
|
|
db5b0ed7be | ||
|
|
9f28a01c03 | ||
|
|
80407b6102 | ||
|
|
287841d806 | ||
|
|
10c3be1195 | ||
|
|
0c0e22013b | ||
|
|
f35c8d4446 | ||
|
|
17418d4b9c | ||
|
|
ec1fbdf2ae | ||
|
|
56e5a46fd5 | ||
|
|
32e86d44a3 | ||
|
|
7100606dfc | ||
|
|
d7eb111460 | ||
|
|
27ea46653e | ||
|
|
12c43072cb | ||
|
|
671aa6d507 | ||
|
|
b697bbad2b | ||
|
|
f05a404e48 | ||
|
|
a46e948b5a | ||
|
|
dc09ea639f | ||
|
|
49ed096e85 | ||
|
|
167bd83f4e | ||
|
|
aed1eac315 | ||
|
|
54a2f8f16f | ||
|
|
2ca543ffb9 | ||
|
|
58e7f815a5 | ||
|
|
15b38fc841 | ||
|
|
e2ca50729d | ||
|
|
6192089b71 | ||
|
|
a4212f6bf9 | ||
|
|
124a2e9d3e | ||
|
|
f77624b3b9 | ||
|
|
a76ac04214 | ||
|
|
0447fa9c28 | ||
|
|
54ac1394a9 | ||
|
|
0b04c73ac5 | ||
|
|
079de45238 | ||
|
|
56ce9ec2f9 | ||
|
|
1b52e4b0f9 | ||
|
|
098c2c1efa | ||
|
|
64343e5a7c | ||
|
|
0caf6caeb9 | ||
|
|
55f6c2a9fc | ||
|
|
c6095bc38a | ||
|
|
09cd6aa568 | ||
|
|
ebbaafb78e | ||
|
|
e6de90d300 | ||
|
|
c7922e90d0 | ||
|
|
c1f4c0445a | ||
|
|
caa634fac6 | ||
|
|
f28bfe0dbd | ||
|
|
155e787ff4 | ||
|
|
c38f0ef42a | ||
|
|
4456031459 | ||
|
|
5fb59a227f | ||
|
|
d9fb241d57 | ||
|
|
642c4ea97e | ||
|
|
8c8a251626 | ||
|
|
5953d6cfb5 | ||
|
|
a1962fe600 | ||
|
|
77dbee3d6a | ||
|
|
cb5e39d73e | ||
|
|
38ef2f470a | ||
|
|
129d84e108 | ||
|
|
affeba76b8 | ||
|
|
6059d3ca26 | ||
|
|
444dee5a16 | ||
|
|
d314466ce2 | ||
|
|
fdaf9c21c8 | ||
|
|
06c2b76f11 | ||
|
|
3896b30738 | ||
|
|
2c4b88e1a0 | ||
|
|
dfc7e1596b | ||
|
|
f590436399 | ||
|
|
cbfb9ba02f | ||
|
|
b4cfe80ad5 | ||
|
|
b37906fa35 | ||
|
|
356f1f1553 | ||
|
|
e882af74ee | ||
|
|
46875fb28e | ||
|
|
417c3e4234 | ||
|
|
6d2c28807b | ||
|
|
4d6e08a650 | ||
|
|
5cebc1fd30 | ||
|
|
ac61731dc6 | ||
|
|
9e4ffabd5c | ||
|
|
3f410bd39f | ||
|
|
d51bc32e37 | ||
|
|
b7f6aa94cc | ||
|
|
ff965e6953 | ||
|
|
468d5d7421 | ||
|
|
fc95b28c49 | ||
|
|
69184187d9 | ||
|
|
a802d0df46 | ||
|
|
8de30633ae | ||
|
|
a1fbb7990f | ||
|
|
aa71146b1b | ||
|
|
9fdb8f087f | ||
|
|
670f0153de | ||
|
|
1d7aeca696 | ||
|
|
4e7811ea07 | ||
|
|
e11283744a | ||
|
|
91cdfd53ef | ||
|
|
bc7d6b9941 | ||
|
|
6b1e0a1656 | ||
|
|
72c9eb2129 | ||
|
|
4bc4b0dc01 | ||
|
|
637d48746f | ||
|
|
9a109c129b | ||
|
|
d49e4ee5ea | ||
|
|
30f0ea29a3 | ||
|
|
49930f6565 | ||
|
|
909d89fa8d | ||
|
|
81d1d7f544 | ||
|
|
67b7dff67a | ||
|
|
4b76d19596 | ||
|
|
080fbe9feb | ||
|
|
d3721229bf | ||
|
|
86c1c9c772 | ||
|
|
c299817193 | ||
|
|
fcc1de45ed | ||
|
|
a29931f2ec | ||
|
|
3fc6a139ee | ||
|
|
4dd04975d9 | ||
|
|
3b4db3ddb7 | ||
|
|
c4a7117ee8 | ||
|
|
b4e900fde8 | ||
|
|
9818d730e4 | ||
|
|
11a3378659 | ||
|
|
1bb05f22d3 | ||
|
|
26b70554c4 | ||
|
|
93b29d2e83 | ||
|
|
072986374a | ||
|
|
2c590d212a | ||
|
|
6cc863efb3 | ||
|
|
b832812767 | ||
|
|
c44558cacd | ||
|
|
6d83a720cd | ||
|
|
8d0dd9c448 | ||
|
|
64020eec49 | ||
|
|
4dedfb85cb | ||
|
|
55d694579a | ||
|
|
86db64edff | ||
|
|
983563efb6 | ||
|
|
37abb2db99 | ||
|
|
5ba0b47e60 | ||
|
|
e8f2087a6f | ||
|
|
6ce99f5cdf | ||
|
|
13c0c9cdd3 | ||
|
|
58ffdb60d7 | ||
|
|
ba56a6a2ee | ||
|
|
ccccf5b1d2 | ||
|
|
b507ac0a54 | ||
|
|
e985676c2d | ||
|
|
f7f4ba6c55 | ||
|
|
4292f43814 | ||
|
|
30bd4fd9fe | ||
|
|
76de39369d | ||
|
|
88a703ce36 | ||
|
|
5938f6b7ea | ||
|
|
5c0c0d6c37 | ||
|
|
0f15077225 | ||
|
|
273dd8d388 | ||
|
|
1795f376ef | ||
|
|
e7360a7692 | ||
|
|
e1fc86934f | ||
|
|
6b8977f178 | ||
|
|
12c6c73de0 | ||
|
|
db62a1607b | ||
|
|
58bb879ef5 | ||
|
|
254912438a | ||
|
|
0e48918bcc | ||
|
|
783ccf8529 | ||
|
|
8fb2f2dc1d | ||
|
|
2a8cc283c7 | ||
|
|
433fe3af9f | ||
|
|
c2d89c622e | ||
|
|
02d6ff15fe | ||
|
|
f2cb7ee7df | ||
|
|
a2ac44dcc1 | ||
|
|
3cf9d74efa | ||
|
|
d5f89a903f | ||
|
|
496c2242bc | ||
|
|
98fbff87df | ||
|
|
ddb51a1c45 | ||
|
|
8df1155215 | ||
|
|
53f2a61409 | ||
|
|
746544f9d5 | ||
|
|
c65c3df11c | ||
|
|
b29b8f12b3 | ||
|
|
d6945677c4 | ||
|
|
aedf4aea08 | ||
|
|
dc28d414dc | ||
|
|
9755bab298 | ||
|
|
fae4029cfc | ||
|
|
1790f0d706 | ||
|
|
0ba2c51676 | ||
|
|
03cd97b49c | ||
|
|
16a162c1dd | ||
|
|
c9bbcf2bf2 | ||
|
|
86e1bf6078 | ||
|
|
1bca84ef0b | ||
|
|
e0f8e1b71a | ||
|
|
416d69142f | ||
|
|
426807aeaa | ||
|
|
90cb075a97 | ||
|
|
ac2ca8fbd3 | ||
|
|
69e4a49065 | ||
|
|
a4a030f2b2 | ||
|
|
dcc4ecd237 | ||
|
|
57f3036a96 | ||
|
|
753e55dfc3 | ||
|
|
9d81baf4b4 | ||
|
|
7cb4d4c596 | ||
|
|
2c8565508e | ||
|
|
c7f156e4c9 | ||
|
|
fcef4342e8 | ||
|
|
72783a5e74 | ||
|
|
a379b69eeb | ||
|
|
0a8ccba33e | ||
|
|
519359a9eb | ||
|
|
b615ed6aab | ||
|
|
d718134ab2 | ||
|
|
5e681ed381 | ||
|
|
6e1b6479b6 | ||
|
|
f3c9e28a62 | ||
|
|
d5d6f85084 | ||
|
|
b8151ebccb | ||
|
|
5650e34432 | ||
|
|
c893d16d52 | ||
|
|
34f08bf206 | ||
|
|
f02a42610b | ||
|
|
c95e6fa92f | ||
|
|
fd738e723b | ||
|
|
b1d1956323 | ||
|
|
725a8012ac | ||
|
|
bb9a73e53b | ||
|
|
ef2de35a74 | ||
|
|
2a453d51a8 | ||
|
|
43d6868d1f | ||
|
|
cea9379b32 | ||
|
|
a12fe7dd9e | ||
|
|
efdd01a988 | ||
|
|
eafe1c631f | ||
|
|
aacf900764 | ||
|
|
f82494e9bb | ||
|
|
1e0ffa176e | ||
|
|
b7eb2d2249 | ||
|
|
b6d6a7591b | ||
|
|
8f1c835299 | ||
|
|
a188c16a99 | ||
|
|
3e6804f06c | ||
|
|
526a3d7c35 | ||
|
|
28fc6a2ddd | ||
|
|
d4f08d7bff | ||
|
|
ca9fe322eb | ||
|
|
239ea0bcaa | ||
|
|
7f02b8df48 | ||
|
|
a2052202b2 | ||
|
|
223cea7021 | ||
|
|
ac9337f694 | ||
|
|
549651d04a | ||
|
|
966bbd902e | ||
|
|
81e6f8784e | ||
|
|
9c53877888 | ||
|
|
98f8eedecd | ||
|
|
4ed429d25c | ||
|
|
119d05f469 |
9
.gitattributes
vendored
Normal file
9
.gitattributes
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# Linux start script should use lf
|
||||
/gradlew text eol=lf
|
||||
|
||||
# These are Windows script files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
@@ -1,72 +0,0 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a very clearly broken issue.
|
||||
title: 'bug: <title>'
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced bug report
|
||||
|
||||
Important to note that your issue may have already been reported before. Please check for existing issues [here](https://github.com/revanced/revanced-patcher/labels/bug).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Crash
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: How did you find the bug? Any additional details that might help?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Add the steps to reproduce this bug including your environment.
|
||||
placeholder: Step 1. Download some files. Step 2. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or videos
|
||||
description: Add screenshots or videos that show the bug here.
|
||||
placeholder: Drag and drop the screenshots/videos into this box.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: If applicable, add a possible solution.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I filled out all of the requested information in this issue properly.
|
||||
required: true
|
||||
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
109
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
name: 🐞 Bug report
|
||||
description: Report a bug or an issue.
|
||||
title: "bug: "
|
||||
labels: ["Bug report"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Patcher bug report
|
||||
|
||||
Before creating a new bug report, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patcher/issues?q=label%3A%22Bug+report%22).
|
||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patcher/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: |
|
||||
- Describe your bug in detail
|
||||
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
|
||||
- Add images and videos if possible
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Error logs
|
||||
description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell.
|
||||
render: shell
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Solution
|
||||
description: If applicable, add a possible solution to the bug.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
58
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
58
.github/ISSUE_TEMPLATE/feature-issue.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed feature request.
|
||||
title: 'feat: <title>'
|
||||
labels: [feature-request]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# ReVanced feature request
|
||||
|
||||
Do not submit requests for patches here. Please submit them [here](https://github.com/orgs/revanced/discussions/categories/patches) instead.
|
||||
Important to note that your feature request may have already been made before. Please check for existing feature requests [here](https://github.com/revanced/revanced-patcher/labels/feature-request).
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Functionality
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Issue
|
||||
description: What is the current problem. Why does it require a feature request?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature
|
||||
description: Describe your feature in detail. How does it solve the issue?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: Why should your feature should be considered?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add additional context here.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new and no duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I filled out all of the requested information in this issue properly.
|
||||
required: true
|
||||
107
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
107
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: ⭐ Feature request
|
||||
description: Create a detailed request for a new feature.
|
||||
title: "feat: "
|
||||
labels: ["Feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="https://raw.githubusercontent.com/revanced/revanced-patcher/main/assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# ReVanced Patcher feature request
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patcher/issues?q=label%3A%22Feature+request%22).
|
||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patcher/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Feature description
|
||||
description: |
|
||||
- Describe your feature in detail
|
||||
- Add images, videos, links, examples, references, etc. if possible
|
||||
- Add the target application name in case you request a new patch
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation
|
||||
description: |
|
||||
A strong motivation is necessary for a feature request to be considered.
|
||||
|
||||
- Why should this feature be implemented?
|
||||
- What is the explicit use case?
|
||||
- What are the benefits?
|
||||
- What makes this feature important?
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
- label: All requested information has been provided properly.
|
||||
required: true
|
||||
2
.github/config.yml
vendored
2
.github/config.yml
vendored
@@ -1,2 +1,2 @@
|
||||
firstPRMergeComment: >
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role.
|
||||
Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution.
|
||||
|
||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
||||
- package-ecosystem: npm
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
|
||||
- package-ecosystem: gradle
|
||||
labels: []
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: monthly
|
||||
25
.github/workflows/build_pull_request.yml
vendored
Normal file
25
.github/workflows/build_pull_request.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Build pull request
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build --no-daemon
|
||||
@@ -1,4 +1,4 @@
|
||||
name: PR to main
|
||||
name: Open a PR to main
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
MESSAGE: merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main`
|
||||
|
||||
jobs:
|
||||
pull-request:
|
||||
@@ -15,7 +15,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Open pull request
|
||||
uses: repo-sync/pull-request@v2
|
||||
with:
|
||||
60
.github/workflows/release.yml
vendored
60
.github/workflows/release.yml
vendored
@@ -6,37 +6,45 @@ on:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
# Make sure the release step uses its own credentials:
|
||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'zulu'
|
||||
cache: gradle
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
cache: 'npm'
|
||||
- name: Setup semantic-release
|
||||
run: npm install
|
||||
- name: Release
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: burrunan/gradle-cache-action@v3
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||
run: npm exec semantic-release
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew build clean
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Import GPG key
|
||||
uses: crazy-max/ghaction-import-gpg@v6
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Release
|
||||
uses: cycjimmy/semantic-release-action@v4
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ env.GITHUB_ACTOR }}
|
||||
ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
19
.github/workflows/update_documentation.yml
vendored
Normal file
19
.github/workflows/update_documentation.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Update documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- docs/**
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
runs-on: ubuntu-latest
|
||||
name: Dispatch event to documentation repository
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
|
||||
repository: revanced/revanced-documentation
|
||||
event-type: update-documentation
|
||||
client-payload: '{"repo": "${{ github.event.repository.name }}", "ref": "${{ github.ref }}"}'
|
||||
133
.gitignore
vendored
133
.gitignore
vendored
@@ -1,124 +1,19 @@
|
||||
### Java template
|
||||
# Compiled class file
|
||||
*.class
|
||||
|
||||
# Log file
|
||||
*.log
|
||||
|
||||
# BlueJ files
|
||||
*.ctxt
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
.mtj.tmp/
|
||||
|
||||
# Package Files #
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
|
||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
### JetBrains template
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
.idea/artifacts
|
||||
.idea/compiler.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/modules.xml
|
||||
.idea/*.iml
|
||||
.idea/modules
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
.idea/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### Gradle template
|
||||
.kotlin
|
||||
.gradle
|
||||
**/build/
|
||||
xcuserdata
|
||||
!src/**/build/
|
||||
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||
!gradle-wrapper.jar
|
||||
|
||||
# Cache of project
|
||||
.gradletasknamecache
|
||||
|
||||
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
|
||||
# gradle/wrapper/gradle-wrapper.properties
|
||||
|
||||
# Avoid ignoring test resources
|
||||
!src/test/resources/*
|
||||
|
||||
# Dependency directories
|
||||
local.properties
|
||||
.idea
|
||||
.DS_Store
|
||||
captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
node_modules/
|
||||
|
||||
# Gradle props, to avoid sharing the gpr key
|
||||
gradle.properties
|
||||
|
||||
11
.releaserc
11
.releaserc
@@ -7,7 +7,13 @@
|
||||
}
|
||||
],
|
||||
"plugins": [
|
||||
"@semantic-release/commit-analyzer",
|
||||
[
|
||||
"@semantic-release/commit-analyzer", {
|
||||
"releaseRules": [
|
||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"@semantic-release/release-notes-generator",
|
||||
"@semantic-release/changelog",
|
||||
"gradle-semantic-release-plugin",
|
||||
@@ -17,7 +23,8 @@
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
]
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
1181
CHANGELOG.md
1181
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
99
CONTRIBUTING.md
Normal file
99
CONTRIBUTING.md
Normal file
@@ -0,0 +1,99 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 👋 Contribution guidelines
|
||||
|
||||
This document describes how to contribute to ReVanced Patcher.
|
||||
|
||||
## 📖 Resources to help you get started
|
||||
|
||||
- The [documentation](https://github.com/ReVanced/revanced-patcher/tree/docs/docs) contains the fundamentals
|
||||
of ReVanced Patcher and how to use ReVanced Patcher to create patches
|
||||
- [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
|
||||
- [Issues](https://github.com/ReVanced/revanced-patcher/issues) are where we keep track of bugs and feature requests
|
||||
|
||||
## 🙏 Submitting a feature request
|
||||
|
||||
Features can be requested by opening an issue using the
|
||||
[Feature request issue template](https://github.com/ReVanced/revanced-patcher/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
|
||||
|
||||
> **Note**
|
||||
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced Patcher.
|
||||
> Good motivation has to be provided for a request to be accepted.
|
||||
|
||||
## 🐞 Submitting a bug report
|
||||
|
||||
If you encounter a bug while using ReVanced Patcher, open an issue using the
|
||||
[Bug report issue template](https://github.com/ReVanced/revanced-patcher/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
|
||||
|
||||
## 📝 How to contribute
|
||||
|
||||
1. Before contributing, it is recommended to open an issue to discuss your change
|
||||
with the maintainers of ReVanced Patcher. This will help you determine whether your change is acceptable
|
||||
and whether it is worth your time to implement it
|
||||
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
|
||||
3. Commit your changes
|
||||
4. Submit a pull request to the `dev` branch of the repository and reference issues
|
||||
that your pull request closes in the description of your pull request
|
||||
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
|
||||
it will be merged into the `dev` branch and will be included in the next release of ReVanced Patcher
|
||||
|
||||
❤️ Thank you for considering contributing to ReVanced Patcher,
|
||||
ReVanced
|
||||
127
README.md
127
README.md
@@ -1,2 +1,125 @@
|
||||
# Patcher
|
||||
Patcher framework used in the ReVanced project.
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 💉 ReVanced Patcher
|
||||
|
||||

|
||||

|
||||
|
||||
ReVanced Patcher used to patch Android applications.
|
||||
|
||||
## ❓ About
|
||||
|
||||
ReVanced Patcher is a library that is used to patch Android applications.
|
||||
It powers [ReVanced Manager](https://github.com/ReVanced/revanced-manager),
|
||||
[ReVanced CLI](https://github.com/ReVanced/revanced-cli)
|
||||
and [ReVanced Library](https://github.com/ReVanced/revanced-library) and a rich set of patches have been developed
|
||||
using ReVanced Patcher in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository.
|
||||
|
||||
## 💪 Features
|
||||
|
||||
Some of the features the ReVanced Patcher provides are:
|
||||
|
||||
- 🔧 **Patch Dalvik VM bytecode**: Disassemble and assemble Dalvik bytecode
|
||||
- 📦 **Patch APK resources**: Decode and build Android APK resources
|
||||
- 📂 **Patch arbitrary APK files**: Read and write arbitrary files directly from and to APK files
|
||||
- 🧩 **Write modular patches**: Extensive API to write modular patches that can patch Dalvik VM bytecode,
|
||||
APK resources and arbitrary APK files
|
||||
|
||||
## 🚀 How to get started
|
||||
|
||||
To use ReVanced Patcher in your project, follow these steps:
|
||||
|
||||
1. [Add the repository](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry#using-a-published-package)
|
||||
to your project
|
||||
2. Add the dependency to your project:
|
||||
|
||||
```kt
|
||||
dependencies {
|
||||
implementation("app.revanced:revanced-patcher:{$version}")
|
||||
}
|
||||
```
|
||||
|
||||
For a minimal project configuration,
|
||||
see [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template).
|
||||
|
||||
## 📚 Everything else
|
||||
|
||||
### 📙 Contributing
|
||||
|
||||
Thank you for considering contributing to ReVanced Patcher.
|
||||
You can find the contribution guidelines [here](CONTRIBUTING.md).
|
||||
|
||||
### 🛠️ Building
|
||||
|
||||
To build ReVanced Patcher,
|
||||
you can follow the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||
|
||||
### 📃 Documentation
|
||||
|
||||
The documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches.
|
||||
You can find it [here](https://github.com/ReVanced/revanced-patcher/tree/main/docs).
|
||||
|
||||
## 📜 Licence
|
||||
|
||||
ReVanced Patcher is licensed under the GPLv3 license. Please see the [licence file](LICENSE) for more information.
|
||||
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced Patcher as long as you track changes/dates in source files.
|
||||
Any modifications to ReVanced Patcher must also be made available under the GPL,
|
||||
along with build & install instructions.
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 11 KiB |
1
assets/revanced-logo/revanced-logo.svg
Normal file
1
assets/revanced-logo/revanced-logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 800 800" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="Logo"><g id="Ring"><circle id="Ring-Background" serif:id="Ring Background" cx="400" cy="400" r="400" style="fill:#1b1b1b;"/><path id="Ring1" serif:id="Ring" d="M400,0c220.766,0 400,179.234 400,400c-0,220.766 -179.234,400 -400,400c-220.766,-0 -400,-179.234 -400,-400c0,-220.766 179.234,-400 400,-400Zm-0,36c200.897,-0 364,163.103 364,364c0,200.897 -163.103,364 -364,364c-200.897,0 -364,-163.103 -364,-364c-0,-200.897 163.103,-364 364,-364Z" style="fill:url(#_Linear1);"/></g><g id="Shape"><path id="V-Shape" serif:id="V Shape" d="M538.74,269.872c1.481,-3.382 1.157,-7.283 -0.863,-10.373c-2.021,-3.091 -5.464,-4.954 -9.156,-4.954c-5.148,0 -10.435,0 -14.165,0c-3.1,0 -5.907,1.834 -7.153,4.672c-12.468,28.396 -78.273,178.273 -100.25,228.328c-1.246,2.838 -4.053,4.671 -7.154,4.671c-3.1,0 -5.907,-1.833 -7.153,-4.671c-21.977,-50.055 -87.782,-199.932 -100.25,-228.328c-1.246,-2.838 -4.053,-4.672 -7.153,-4.672c-3.73,0 -9.017,0 -14.164,0c-3.693,0 -7.135,1.863 -9.156,4.954c-2.02,3.09 -2.344,6.991 -0.863,10.373c23.557,53.766 101.872,232.519 117.871,269.034c1.743,3.979 5.674,6.549 10.018,6.549c6.293,-0 15.408,-0 21.701,-0c4.344,-0 8.275,-2.57 10.018,-6.549c15.999,-36.515 94.315,-215.268 117.872,-269.034Z" style="fill:#fff;"/><path id="Diamond" d="M408.119,395.312c-1.675,2.901 -4.77,4.688 -8.119,4.688c-3.349,-0 -6.444,-1.787 -8.119,-4.688c-16.997,-29.44 -56.156,-97.264 -73.153,-126.704c-1.675,-2.901 -1.675,-6.474 0,-9.375c1.675,-2.901 4.77,-4.688 8.119,-4.688c33.995,0 112.311,0 146.306,0c3.349,0 6.444,1.787 8.119,4.688c1.675,2.901 1.675,6.474 -0,9.375c-16.997,29.44 -56.156,97.264 -73.153,126.704Z" style="fill:url(#_Linear2);"/></g></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.89859e-14,800,-800,4.89859e-14,400.001,3.31681e-10)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient><linearGradient id="_Linear2" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.77155e-14,289.317,-282.535,1.73003e-14,400,254.545)"><stop offset="0" style="stop-color:#f04e98;stop-opacity:1"/><stop offset="0.5" style="stop-color:#5f65d4;stop-opacity:1"/><stop offset="1" style="stop-color:#4e98f0;stop-opacity:1"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,67 +1,5 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.8.10"
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
|
||||
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = uri("https://maven.pkg.github.com/revanced/multidexlib2")
|
||||
credentials {
|
||||
username = githubUsername
|
||||
password = githubPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("xpp3:xpp3:1.1.4c")
|
||||
implementation("app.revanced:smali:2.5.3-a3836654")
|
||||
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
|
||||
implementation("app.revanced:apktool-lib:2.7.0")
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
||||
}
|
||||
|
||||
tasks {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("PASSED", "SKIPPED", "FAILED")
|
||||
}
|
||||
}
|
||||
processResources {
|
||||
expand("projectVersion" to project.version)
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("GITHUB_ACTOR") != null)
|
||||
maven {
|
||||
name = "GitHubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials {
|
||||
username = System.getenv("GITHUB_ACTOR")
|
||||
password = System.getenv("GITHUB_TOKEN")
|
||||
}
|
||||
}
|
||||
else
|
||||
mavenLocal()
|
||||
}
|
||||
publications {
|
||||
register<MavenPublication>("gpr") {
|
||||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
alias(libs.plugins.android.kotlin.multiplatform.library) apply false
|
||||
alias(libs.plugins.kotlinMultiplatform) apply false
|
||||
alias(libs.plugins.vanniktech.mavenPublish) apply false
|
||||
}
|
||||
111
docs/1_patcher_intro.md
Normal file
111
docs/1_patcher_intro.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 💉 Introduction to ReVanced Patcher
|
||||
|
||||
To create patches for Android apps, it is recommended to know the basic concept of ReVanced Patcher.
|
||||
|
||||
## 📙 How it works
|
||||
|
||||
ReVanced Patcher is a library that allows modifying Android apps by applying patches.
|
||||
It is built on top of [Smali](https://github.com/google/smali) for bytecode manipulation and [Androlib (Apktool)](https://github.com/iBotPeaches/Apktool)
|
||||
for resource decoding and encoding.
|
||||
|
||||
ReVanced Patcher receives a list of patches and applies them to a given APK file.
|
||||
It then returns the modified components of the APK file, such as modified dex files and resources,
|
||||
that can be repackaged into a new APK file.
|
||||
|
||||
ReVanced Patcher has a simple API that allows you to load patches from RVP (JAR or DEX container) files
|
||||
and apply them to an APK file. Later on, you will learn how to create patches.
|
||||
|
||||
```kt
|
||||
val patches = loadPatchesFromJar(setOf(File("revanced-patches.rvp")))
|
||||
|
||||
val patcherResult = Patcher(PatcherConfig(apkFile = File("some.apk"))).use { patcher ->
|
||||
// Here you can access metadata about the APK file through patcher.context.packageMetadata
|
||||
// such as package name, version code, version name, etc.
|
||||
|
||||
// Add patches.
|
||||
patcher += patches
|
||||
|
||||
// Execute the patches.
|
||||
runBlocking {
|
||||
patcher().collect { patchResult ->
|
||||
if (patchResult.exception != null)
|
||||
logger.info { "\"${patchResult.patch}\" failed:\n${patchResult.exception}" }
|
||||
else
|
||||
logger.info { "\"${patchResult.patch}\" succeeded" }
|
||||
}
|
||||
}
|
||||
|
||||
// Compile and save the patched APK file components.
|
||||
patcher.get()
|
||||
}
|
||||
|
||||
// The result of the patcher contains the modified components of the APK file that can be repackaged into a new APK file.
|
||||
val dexFiles = patcherResult.dexFiles
|
||||
val resources = patcherResult.resources
|
||||
```
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page teaches the fundamentals of ReVanced Patches.
|
||||
|
||||
Continue: [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
|
||||
112
docs/2_1_setup.md
Normal file
112
docs/2_1_setup.md
Normal file
@@ -0,0 +1,112 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 👶 Setting up a development environment
|
||||
|
||||
To start developing patches with ReVanced Patcher, you must prepare a development environment.
|
||||
|
||||
## 📝 Prerequisites
|
||||
|
||||
- A Java IDE with Kotlin support, such as [IntelliJ IDEA](https://www.jetbrains.com/idea/)
|
||||
- Knowledge of Java, [Kotlin](https://kotlinlang.org), and [Dalvik bytecode](https://source.android.com/docs/core/runtime/dalvik-bytecode)
|
||||
- Android reverse engineering skills and tools such as [jadx](https://github.com/skylot/jadx)
|
||||
|
||||
## 🏃 Prepare the environment
|
||||
|
||||
Throughout the documentation, [ReVanced Patches](https://github.com/revanced/revanced-patches) will be used as an example project.
|
||||
|
||||
> [!NOTE]
|
||||
> To start a fresh project,
|
||||
> you can use the [ReVanced Patches template](https://github.com/revanced/revanced-patches-template).
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/revanced/revanced-patches && cd revanced-patches
|
||||
```
|
||||
|
||||
2. Build the project
|
||||
|
||||
```bash
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If the build fails due to authentication, you may need to authenticate to GitHub Packages.
|
||||
> Create a PAT with the scope `read:packages` [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced) and add your token to ~/.gradle/gradle.properties.
|
||||
>
|
||||
> Example `gradle.properties` file:
|
||||
>
|
||||
> ```properties
|
||||
> gpr.user = user
|
||||
> gpr.key = key
|
||||
> ```
|
||||
|
||||
3. Open the project in your IDE
|
||||
|
||||
> [!TIP]
|
||||
> It is a good idea to set up a complete development environment for ReVanced, so that you can also test your patches
|
||||
> by following the [ReVanced documentation](https://github.com/ReVanced/revanced-documentation).
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page will go into details about a ReVanced patch.
|
||||
|
||||
Continue: [🧩 Anatomy of a patch](2_2_patch_anatomy.md)
|
||||
296
docs/2_2_1_fingerprinting.md
Normal file
296
docs/2_2_1_fingerprinting.md
Normal file
@@ -0,0 +1,296 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 🔎 Fingerprinting
|
||||
|
||||
In the context of ReVanced, a fingerprint is a partial description of a method.
|
||||
It is used to uniquely match a method by its characteristics.
|
||||
Fingerprinting is used to match methods with a limited amount of known information.
|
||||
Methods with obfuscated names that change with each update are primary candidates for fingerprinting.
|
||||
The goal of fingerprinting is to uniquely identify a method by capturing various attributes, such as the return type,
|
||||
access flags, an opcode pattern, strings, and more.
|
||||
|
||||
## ⛳️ Example fingerprint
|
||||
|
||||
An example fingerprint is shown below:
|
||||
|
||||
```kt
|
||||
|
||||
package app.revanced.patches.ads.fingerprints
|
||||
|
||||
fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("Z")
|
||||
parameters("Z")
|
||||
opcodes(Opcode.RETURN)
|
||||
strings("pro")
|
||||
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
|
||||
}
|
||||
```
|
||||
|
||||
## 🔎 Reconstructing the original code from the example fingerprint from above
|
||||
|
||||
The following code is reconstructed from the fingerprint to understand how a fingerprint is created.
|
||||
|
||||
The fingerprint contains the following information:
|
||||
|
||||
- Method signature:
|
||||
|
||||
```kt
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("Z")
|
||||
parameters("Z")
|
||||
```
|
||||
|
||||
- Method implementation:
|
||||
|
||||
```kt
|
||||
opcodes(Opcode.RETURN)
|
||||
strings("pro")
|
||||
```
|
||||
|
||||
- Package and class name:
|
||||
|
||||
```kt
|
||||
custom { (method, classDef) -> classDef == "Lcom/some/app/ads/AdsLoader;" }
|
||||
```
|
||||
|
||||
With this information, the original code can be reconstructed:
|
||||
|
||||
```java
|
||||
package com.some.app.ads;
|
||||
|
||||
<accessFlags>
|
||||
|
||||
class AdsLoader {
|
||||
public final boolean <methodName>(boolean <parameter>)
|
||||
|
||||
{
|
||||
// ...
|
||||
|
||||
var userStatus = "pro";
|
||||
|
||||
// ...
|
||||
|
||||
return <returnValue >;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Using that fingerprint, this method can be matched uniquely from all other methods.
|
||||
|
||||
> [!TIP]
|
||||
> A fingerprint should contain information about a method likely to remain the same across updates.
|
||||
> A method's name is not included in the fingerprint because it will likely change with each update in an obfuscated
|
||||
> app.
|
||||
> In contrast, the return type, access flags, parameters, patterns of opcodes, and strings are likely to remain the
|
||||
> same.
|
||||
|
||||
## 🔨 How to use fingerprints
|
||||
|
||||
After declaring a fingerprint, it can be used in a patch to find the method it matches to:
|
||||
|
||||
```kt
|
||||
val fingerprint = fingerprint {
|
||||
// ...
|
||||
}
|
||||
|
||||
val patch = bytecodePatch {
|
||||
execute {
|
||||
fingerprint.method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The fingerprint won't be matched again, if it has already been matched once, for performance reasons.
|
||||
This makes it useful, to share fingerprints between multiple patches,
|
||||
and let the first executing patch match the fingerprint:
|
||||
|
||||
```kt
|
||||
// Either of these two patches will match the fingerprint first and the other patch can reuse the match:
|
||||
val mainActivityPatch1 = bytecodePatch {
|
||||
execute {
|
||||
mainActivityOnCreateFingerprint.method
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivityPatch2 = bytecodePatch {
|
||||
execute {
|
||||
mainActivityOnCreateFingerprint.method
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> If the fingerprint can not be matched to any method,
|
||||
> accessing certain properties of the fingerprint will raise an exception.
|
||||
> Instead, the `orNull` properties can be used to return `null` if no match is found.
|
||||
|
||||
> [!TIP]
|
||||
> If a fingerprint has an opcode pattern, you can use the `fuzzyPatternScanThreshhold` parameter of the `opcode`
|
||||
> function to fuzzy match the pattern.
|
||||
> `null` can be used as a wildcard to match any opcode:
|
||||
>
|
||||
> ```kt
|
||||
> fingerprint(fuzzyPatternScanThreshhold = 2) {
|
||||
> opcodes(
|
||||
> Opcode.ICONST_0,
|
||||
> null,
|
||||
> Opcode.ICONST_1,
|
||||
> Opcode.IRETURN,
|
||||
> )
|
||||
>}
|
||||
> ```
|
||||
|
||||
The following properties can be accessed in a fingerprint:
|
||||
|
||||
- `originalClassDef`: The original class definition the fingerprint matches to.
|
||||
- `originalClassDefOrNull`: The original class definition the fingerprint matches to.
|
||||
- `originalMethod`: The original method the fingerprint matches to.
|
||||
- `originalMethodOrNull`: The original method the fingerprint matches to.
|
||||
- `classDef`: The class the fingerprint matches to.
|
||||
- `classDefOrNull`: The class the fingerprint matches to.
|
||||
- `method`: The method the fingerprint matches to. If no match is found, an exception is raised.
|
||||
- `methodOrNull`: The method the fingerprint matches to.
|
||||
|
||||
The difference between the `original` and non-`original` properties is that the `original` properties return the
|
||||
original class or method definition, while the non-`original` properties return a mutable copy of the class or method.
|
||||
The mutable copies can be modified. They are lazy properties, so they are only computed
|
||||
and only then will effectively replace the `original` method or class definition when accessed.
|
||||
|
||||
> [!TIP]
|
||||
> If only read-only access to the class or method is needed,
|
||||
> the `originalClassDef` and `originalMethod` properties should be used,
|
||||
> to avoid making a mutable copy of the class or method.
|
||||
|
||||
## 🏹 Manually matching fingerprints
|
||||
|
||||
By default, a fingerprint is matched automatically against all classes
|
||||
when one of the fingerprint's properties is accessed.
|
||||
|
||||
Instead, the fingerprint can be matched manually using various overloads of a fingerprint's `match` function:
|
||||
|
||||
- In a **list of classes**, if the fingerprint can match in a known subset of classes
|
||||
|
||||
If you have a known list of classes you know the fingerprint can match in,
|
||||
you can match the fingerprint on the list of classes:
|
||||
|
||||
```kt
|
||||
execute {
|
||||
val match = showAdsFingerprint(classes)
|
||||
}
|
||||
```
|
||||
|
||||
- In a **single class**, if the fingerprint can match in a single known class
|
||||
|
||||
If you know the fingerprint can match a method in a specific class, you can match the fingerprint in the class:
|
||||
|
||||
```kt
|
||||
execute {
|
||||
val adsLoaderClass = classes.single { it.name == "Lcom/some/app/ads/Loader;" }
|
||||
|
||||
val match = showAdsFingerprint.match(adsLoaderClass)
|
||||
}
|
||||
```
|
||||
|
||||
Another common usecase is to use a fingerprint to reduce the search space of a method to a single class.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
// Match showAdsFingerprint in the class of the ads loader found by adsLoaderClassFingerprint.
|
||||
val match = showAdsFingerprint.match(adsLoaderClassFingerprint.classDef)
|
||||
}
|
||||
```
|
||||
|
||||
- Match a **single method**, to extract certain information about it
|
||||
|
||||
The match of a fingerprint contains useful information about the method,
|
||||
such as the start and end index of an opcode pattern or the indices of the instructions with certain string
|
||||
references.
|
||||
A fingerprint can be leveraged to extract such information from a method instead of manually figuring it out:
|
||||
|
||||
```kt
|
||||
execute {
|
||||
val currentPlanFingerprint = fingerprint {
|
||||
strings("free", "trial")
|
||||
}
|
||||
|
||||
currentPlanFingerprint.match(adsFingerprint.method).let { match ->
|
||||
match.stringMatches.forEach { match ->
|
||||
println("The index of the string '${match.string}' is ${match.index}")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> If the fingerprint can not be matched to any method, calling `match` will raise an
|
||||
> exception.
|
||||
> Instead, the `orNull` overloads can be used to return `null` if no match is found.
|
||||
|
||||
> [!TIP]
|
||||
> To see real-world examples of fingerprints,
|
||||
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page discusses the structure and conventions of patches.
|
||||
|
||||
Continue: [📜 Project structure and conventions](3_structure_and_conventions.md)
|
||||
262
docs/2_2_patch_anatomy.md
Normal file
262
docs/2_2_patch_anatomy.md
Normal file
@@ -0,0 +1,262 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 🧩 Anatomy of a ReVanced patch
|
||||
|
||||
Learn the API to create patches using ReVanced Patcher.
|
||||
|
||||
## ⛳️ Example patch
|
||||
|
||||
The following example patch disables ads in an app.
|
||||
In the following sections, each part of the patch will be explained in detail.
|
||||
|
||||
```kt
|
||||
package app.revanced.patches.ads
|
||||
|
||||
val disableAdsPatch = bytecodePatch(
|
||||
name = "Disable ads",
|
||||
description = "Disable ads in the app.",
|
||||
) {
|
||||
compatibleWith("com.some.app"("1.0.0"))
|
||||
|
||||
// Patches can depend on other patches, executing them first.
|
||||
dependsOn(disableAdsResourcePatch)
|
||||
|
||||
// Merge precompiled DEX files into the patched app, before the patch is executed.
|
||||
extendWith("disable-ads.rve")
|
||||
|
||||
// Business logic of the patch to disable ads in the app.
|
||||
execute {
|
||||
// Fingerprint to find the method to patch.
|
||||
val showAdsFingerprint = fingerprint {
|
||||
// More about fingerprints on the next page of the documentation.
|
||||
}
|
||||
|
||||
// In the method that shows ads,
|
||||
// call DisableAdsPatch.shouldDisableAds() from the extension (precompiled DEX file)
|
||||
// to enable or disable ads.
|
||||
showAdsFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static {}, LDisableAdsPatch;->shouldDisableAds()Z
|
||||
move-result v0
|
||||
return v0
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> To see real-world examples of patches,
|
||||
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
|
||||
|
||||
## 🧩 Patch API
|
||||
|
||||
### ⚙️ Patch options
|
||||
|
||||
Patches can have options to get and set before a patch is executed.
|
||||
Options are useful for making patches configurable.
|
||||
After loading the patches using `PatchLoader`, options can be set for a patch.
|
||||
Multiple types are already built into ReVanced Patcher and are supported by any application that uses ReVanced Patcher.
|
||||
|
||||
To define an option, use the available `option` functions:
|
||||
|
||||
```kt
|
||||
val patch = bytecodePatch(name = "Patch") {
|
||||
// Add an inbuilt option and delegate it to a property.
|
||||
val value by stringOption(name = "Inbuilt option")
|
||||
|
||||
// Add an option with a custom type and delegate it to a property.
|
||||
val string by option<String>(name = "String option")
|
||||
|
||||
execute {
|
||||
println(value)
|
||||
println(string)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Options of a patch can be set after loading the patches with `PatchLoader` by obtaining the instance for the patch:
|
||||
|
||||
```kt
|
||||
loadPatchesJar(patches).apply {
|
||||
// Type is checked at runtime.
|
||||
first { it.name == "Patch" }.options["Option"] = "Value"
|
||||
}
|
||||
```
|
||||
|
||||
The type of an option can be obtained from the `type` property of the option:
|
||||
|
||||
```kt
|
||||
option.type // The KType of the option. Captures the full type information of the option.
|
||||
```
|
||||
|
||||
Options can be declared outside a patch and added to a patch manually:
|
||||
|
||||
```kt
|
||||
val option = stringOption(name = "Option")
|
||||
|
||||
bytecodePatch(name = "Patch") {
|
||||
val value by option()
|
||||
}
|
||||
```
|
||||
|
||||
This is useful when the same option is referenced in multiple patches.
|
||||
|
||||
### 🧩 Extensions
|
||||
|
||||
An extension is a precompiled DEX file merged into the patched app before a patch is executed.
|
||||
While patches are compile-time constructs, extensions are runtime constructs
|
||||
that extend the patched app with additional classes.
|
||||
|
||||
Assume you want to add a complex feature to an app that would need multiple classes and methods:
|
||||
|
||||
```java
|
||||
public class ComplexPatch {
|
||||
public static void doSomething() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After compiling the above code as a DEX file, you can add the DEX file as a resource in the patches file
|
||||
and use it in a patch:
|
||||
|
||||
```kt
|
||||
val patch = bytecodePatch(name = "Complex patch") {
|
||||
extendWith("complex-patch.rve")
|
||||
|
||||
execute {
|
||||
fingerprint.method.addInstructions(0, "invoke-static { }, LComplexPatch;->doSomething()V")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
ReVanced Patcher merges the classes from the extension into `context.classes` before executing the patch.
|
||||
When the patch is executed, it can reference the classes and methods from the extension.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> The [ReVanced Patches template](https://github.com/ReVanced/revanced-patches-template) repository
|
||||
> is a template project to create patches and extensions.
|
||||
|
||||
> [!TIP]
|
||||
> To see real-world examples of extensions,
|
||||
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
|
||||
|
||||
### ♻️ Finalization
|
||||
|
||||
Patches can have a finalization block called after all patches have been executed, in reverse order of patch execution.
|
||||
The finalization block is called after all patches that depend on the patch have been executed.
|
||||
This is useful for doing post-processing tasks.
|
||||
A simple real-world example would be a patch that opens a resource file of the app for writing.
|
||||
Other patches that depend on this patch can write to the file, and the finalization block can close the file.
|
||||
|
||||
```kt
|
||||
val patch = bytecodePatch(name = "Patch") {
|
||||
dependsOn(
|
||||
bytecodePatch(name = "Dependency") {
|
||||
execute {
|
||||
print("1")
|
||||
}
|
||||
|
||||
finalize {
|
||||
print("4")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
execute {
|
||||
print("2")
|
||||
}
|
||||
|
||||
finalize {
|
||||
print("3")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because `Patch` depends on `Dependency`, first `Dependency` is executed, then `Patch`.
|
||||
Finalization blocks are called in reverse order of patch execution, which means,
|
||||
first, the finalization block of `Patch`, then the finalization block of `Dependency` is called.
|
||||
The output after executing the patch above would be `1234`.
|
||||
The same order is followed for multiple patches depending on the patch.
|
||||
|
||||
## 💡 Additional tips
|
||||
|
||||
- When using `PatchLoader` to load patches, only patches with a name are loaded.
|
||||
Refer to the inline documentation of `PatchLoader` for detailed information.
|
||||
- Patches can depend on others. Dependencies are executed first.
|
||||
The dependent patch will not be executed if a dependency raises an exception while executing.
|
||||
- A patch can declare compatibility with specific packages and versions,
|
||||
but patches can still be executed on any package or version.
|
||||
It is recommended that compatibility is specified to present known compatible packages and versions.
|
||||
- If `compatibleWith` is not used, the patch is treated as compatible with any package
|
||||
- If a package is specified with no versions, the patch is compatible with any version of the package
|
||||
- If an empty array of versions is specified, the patch is not compatible with any version of the package.
|
||||
This is useful for declaring incompatibility with a specific package.
|
||||
- A patch can raise a `PatchException` at any time of execution to indicate that the patch failed to execute.
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page explains the concept of fingerprinting in ReVanced Patcher.
|
||||
|
||||
Continue: [🔎 Fingerprinting](2_2_1_fingerprinting.md)
|
||||
126
docs/2_patches_intro.md
Normal file
126
docs/2_patches_intro.md
Normal file
@@ -0,0 +1,126 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 🧩 Introduction to ReVanced Patches
|
||||
|
||||
Learn the basic concepts of ReVanced Patcher and how to create patches.
|
||||
|
||||
## 📙 Fundamentals
|
||||
|
||||
A patch is a piece of code that modifies an Android application.
|
||||
There are multiple types of patches. Each type can modify a different part of the APK, such as the Dalvik VM bytecode,
|
||||
the APK resources, or arbitrary files in the APK:
|
||||
|
||||
- A `BytecodePatch` modifies the Dalvik VM bytecode
|
||||
- A `ResourcePatch` modifies (decoded) resources
|
||||
- A `RawResourcePatch` modifies arbitrary files
|
||||
|
||||
Each patch can declare a set of dependencies on other patches. ReVanced Patcher will first execute dependencies
|
||||
before executing the patch itself. This way, multiple patches can work together for abstract purposes in a modular way.
|
||||
|
||||
The `execute` function is the entry point for a patch. It is called by ReVanced Patcher when the patch is executed.
|
||||
The `execute` function receives an instance of a context object that provides access to the APK.
|
||||
The patch can use this context to modify the APK.
|
||||
|
||||
Each type of context provides different APIs to modify the APK. For example, the `BytecodePatchContext` provides APIs
|
||||
to modify the Dalvik VM bytecode, while the `ResourcePatchContext` provides APIs to modify resources.
|
||||
|
||||
The difference between `ResourcePatch` and `RawResourcePatch` is that ReVanced Patcher will decode the resources
|
||||
if it is supplied a `ResourcePatch` for execution or if any patch depends on a `ResourcePatch`
|
||||
and will not decode the resources before executing `RawResourcePatch`.
|
||||
Both, `ResourcePatch` and `RawResourcePatch` can modify arbitrary files in the APK,
|
||||
whereas only `ResourcePatch` can modify decoded resources. The choice of which type to use depends on the use case.
|
||||
Decoding and building resources is a time- and resource-consuming,
|
||||
so if the patch does not need to modify decoded resources, it is better to use `RawResourcePatch` or `BytecodePatch`.
|
||||
|
||||
Example of patches:
|
||||
|
||||
```kt
|
||||
@Surpress("unused")
|
||||
val bytecodePatch = bytecodePatch {
|
||||
execute {
|
||||
// More about this on the next page of the documentation.
|
||||
}
|
||||
}
|
||||
|
||||
@Surpress("unused")
|
||||
val rawResourcePatch = rawResourcePatch {
|
||||
execute {
|
||||
// More about this on the next page of the documentation.
|
||||
}
|
||||
}
|
||||
|
||||
@Surpress("unused")
|
||||
val resourcePatch = resourcePatch {
|
||||
execute {
|
||||
// More about this on the next page of the documentation.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> To see real-world examples of patches,
|
||||
> check out the repository for [ReVanced Patches](https://github.com/revanced/revanced-patches).
|
||||
|
||||
## ⏭️ Whats next
|
||||
|
||||
The next page will guide you through creating a development environment for creating patches.
|
||||
|
||||
Continue: [👶 Setting up a development environment](2_1_setup.md)
|
||||
105
docs/3_structure_and_conventions.md
Normal file
105
docs/3_structure_and_conventions.md
Normal file
@@ -0,0 +1,105 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 📜 Project structure and conventions
|
||||
|
||||
Over time, a specific project structure and conventions have been established.
|
||||
|
||||
## 📁 File structure
|
||||
|
||||
Patches are organized in a specific way. The file structure looks as follows:
|
||||
|
||||
```text
|
||||
📦your.patches.app.category
|
||||
├ 🔍Fingerprints.kt
|
||||
└ 🧩SomePatch.kt
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Moving fingerprints to a separate file isn't strictly necessary, but it helps the organization when a patch uses multiple fingerprints.
|
||||
|
||||
## 📙 Conventions
|
||||
|
||||
- 🔥 Name a patch after what it does. For example, if a patch removes ads, name it `Remove ads`.
|
||||
If a patch changes the color of a button, name it `Change button color`
|
||||
- 🔥 Write the patch description in the third person, present tense, and end it with a period.
|
||||
If a patch removes ads, the description can be omitted because of redundancy,
|
||||
but if a patch changes the color of a button, the description can be _Changes the color of the resume button to red._
|
||||
- 🔥 Write patches with modularity and reusability in mind. Patches can depend on each other,
|
||||
so it is important to write patches in a way that can be used in different contexts.
|
||||
- 🔥🔥 Keep patches as minimal as possible. This reduces the risk of failing patches.
|
||||
Instead of involving many abstract changes in one patch or writing entire methods or classes in a patch,
|
||||
you can write code in extensions. An extension is a precompiled DEX file that is merged into the patched app
|
||||
before this patch is executed.
|
||||
Patches can then reference methods and classes from extensions.
|
||||
A real-world example of extensions can be found in the [ReVanced Patches](https://github.com/ReVanced/revanced-patches) repository
|
||||
- 🔥🔥🔥 Do not overload a fingerprint with information about a method that's likely to change.
|
||||
In the example of an obfuscated method, it's better to fingerprint the method by its return type
|
||||
and parameters rather than its name because the name is likely to change. An intelligent selection
|
||||
of an opcode pattern or strings in a method can result in a strong fingerprint dynamic to app updates.
|
||||
- 🔥🔥🔥 Document your patches. Patches are abstract, so it is important to document parts of the code
|
||||
that are not self-explanatory. For example, explain why and how a certain method is patched or large blocks
|
||||
of instructions that are modified or added to a method
|
||||
|
||||
## ⏭️ What's next
|
||||
|
||||
The next page discusses useful APIs for patch development.
|
||||
|
||||
Continue: [💪 Advanced APIs](4_apis.md)
|
||||
117
docs/4_apis.md
Normal file
117
docs/4_apis.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# 💪 Advanced APIs
|
||||
|
||||
A handful of APIs are available to make patch development easier and more efficient.
|
||||
|
||||
## 📙 Overview
|
||||
|
||||
1. 👹 Create mutable replacements of classes with `proxy(ClassDef)`
|
||||
2. 🔍 Find and create mutable replaces with `classBy(Predicate)`
|
||||
3. 🏃 Navigate method calls recursively by index with `navigate(Method)`
|
||||
4. 💾 Read and write resource files with `get(String, Boolean)` and `delete(String)`
|
||||
5. 📃 Read and write DOM files using `document(String)` and `document(InputStream)`
|
||||
|
||||
### 🧰 APIs
|
||||
|
||||
#### 👹 `proxy(ClassDef)`
|
||||
|
||||
By default, the classes are immutable, meaning they cannot be modified.
|
||||
To make a class mutable, use the `proxy(ClassDef)` function.
|
||||
This function creates a lazy mutable copy of the class definition.
|
||||
Accessing the property will replace the original class definition with the mutable copy,
|
||||
thus allowing you to make changes to the class. Subsequent accesses will return the same mutable copy.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
val mutableClass = proxy(classDef)
|
||||
mutableClass.methods.add(Method())
|
||||
}
|
||||
```
|
||||
|
||||
#### 🔍 `classBy(Predicate)`
|
||||
|
||||
The `classBy(Predicate)` function is an alternative to finding and creating mutable classes by a predicate.
|
||||
It automatically proxies the class definition, making it mutable.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
// Alternative to proxy(classes.find { it.name == "Lcom/example/MyClass;" })?.classDef
|
||||
val classDef = classBy { it.name == "Lcom/example/MyClass;" }?.classDef
|
||||
}
|
||||
```
|
||||
|
||||
#### 🏃 `navigate(Method).at(index)`
|
||||
|
||||
The `navigate(Method)` function allows you to navigate method calls recursively by index.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
// Sequentially navigate to the instructions at index 1 within 'someMethod'.
|
||||
val method = navigate(someMethod).to(1).original() // original() returns the original immutable method.
|
||||
|
||||
// Further navigate to the second occurrence where the instruction's opcode is 'INVOKEVIRTUAL'.
|
||||
// stop() returns the mutable copy of the method.
|
||||
val method = navigate(someMethod).to(2) { instruction -> instruction.opcode == Opcode.INVOKEVIRTUAL }.stop()
|
||||
|
||||
// Alternatively, to stop(), you can delegate the method to a variable.
|
||||
val method by navigate(someMethod).to(1)
|
||||
|
||||
// You can chain multiple calls to at() to navigate deeper into the method.
|
||||
val method by navigate(someMethod).to(1).to(2, 3, 4).to(5)
|
||||
}
|
||||
```
|
||||
|
||||
#### 💾 `get(String, Boolean)` and `delete(String)`
|
||||
|
||||
The `get(String, Boolean)` function returns a `File` object that can be used to read and write resource files.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
val file = get("res/values/strings.xml")
|
||||
val content = file.readText()
|
||||
file.writeText(content)
|
||||
}
|
||||
```
|
||||
|
||||
The `delete` function can mark files for deletion when the APK is rebuilt.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
delete("res/values/strings.xml")
|
||||
}
|
||||
```
|
||||
|
||||
#### 📃 `document(String)` and `document(InputStream)`
|
||||
|
||||
The `document` function is used to read and write DOM files.
|
||||
|
||||
```kt
|
||||
execute {
|
||||
document("res/values/strings.xml").use { document ->
|
||||
val element = doc.createElement("string").apply {
|
||||
textContent = "Hello, World!"
|
||||
}
|
||||
document.documentElement.appendChild(element)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also read documents from an `InputStream`:
|
||||
|
||||
```kt
|
||||
execute {
|
||||
val inputStream = classLoader.getResourceAsStream("some.xml")
|
||||
document(inputStream).use { document ->
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎉 Afterword
|
||||
|
||||
ReVanced Patcher is a powerful library to patch Android applications, offering a rich set of APIs to develop patches
|
||||
that outlive app updates. Patches make up ReVanced; without you, the community of patch developers,
|
||||
ReVanced would not be what it is today. We hope that this documentation has been helpful to you
|
||||
and are excited to see what you will create with ReVanced Patcher. If you have any questions or need help,
|
||||
talk to us on one of our platforms linked on [revanced.app](https://revanced.app) or open an issue in case of a bug or
|
||||
feature request,
|
||||
ReVanced
|
||||
73
docs/README.md
Normal file
73
docs/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="256px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="../assets/revanced-headline/revanced-headline-vertical-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="256px"
|
||||
src="../assets/revanced-headline/revanced-headline-vertical-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revanced.app/">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="../assets/revanced-logo/revanced-logo.svg" />
|
||||
<img height="24px" src="../assets/revanced-logo/revanced-logo.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="24px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="http://revanced.app/discord">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://reddit.com/r/revancedapp">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032351-9d9d5619-8ef7-470a-9eec-2744ece54553.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://t.me/app_revanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032213-faf25ab8-0bc3-4a94-a730-b524c96df124.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://x.com/revancedapp">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/93124920/270180600-7c1b38bf-889b-4d68-bd5e-b9d86f91421a.png">
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/93124920/270108715-d80743fa-b330-4809-b1e6-79fbdc60d09c.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ReVanced">
|
||||
<picture>
|
||||
<source height="24px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
<img height="24px" src="https://user-images.githubusercontent.com/13122796/178032714-c51c7492-0666-44ac-99c2-f003a695ab50.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<br>
|
||||
<br>
|
||||
Continuing the legacy of Vanced
|
||||
</p>
|
||||
|
||||
# 💉 Documentation of ReVanced Patcher
|
||||
|
||||
This documentation contains the fundamentals of ReVanced Patcher and how to use ReVanced Patcher to create patches
|
||||
|
||||
## 📖 Table of content
|
||||
|
||||
1. [💉 Introduction to ReVanced Patcher](1_patcher_intro.md)
|
||||
2. [🧩 Introduction to ReVanced Patches](2_patches_intro.md)
|
||||
1. [👶 Setting up a development environment](2_1_setup.md)
|
||||
2. [🧩 Anatomy of a ReVanced patch](2_2_patch_anatomy.md)
|
||||
1. [🔎 Fingerprinting](2_2_1_fingerprinting.md)
|
||||
3. [📜 Project structure and conventions](3_structure_and_conventions.md)
|
||||
4. [💪 Advanced APIs](4_apis.md)
|
||||
@@ -1,2 +1,11 @@
|
||||
kotlin.code.style = official
|
||||
version = 7.1.0-dev.2
|
||||
version = 22.0.0-local
|
||||
#Kotlin
|
||||
kotlin.code.style=official
|
||||
kotlin.daemon.jvmargs=-Xmx3072M
|
||||
#Gradle
|
||||
org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
#Android
|
||||
android.nonTransitiveRClass=true
|
||||
android.useAndroidX=true
|
||||
|
||||
28
gradle/libs.versions.toml
Normal file
28
gradle/libs.versions.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
[versions]
|
||||
agp = "8.12.3"
|
||||
android-compileSdk = "36"
|
||||
android-minSdk = "26"
|
||||
kotlin = "2.3.0"
|
||||
apktool-lib = "2.10.1.1"
|
||||
mockk = "1.14.7"
|
||||
multidexlib2 = "3.0.3.r3"
|
||||
# Tracking https://github.com/google/smali/issues/64.
|
||||
#noinspection GradleDependency
|
||||
smali = "3.0.9"
|
||||
xpp3 = "1.1.4c"
|
||||
vanniktechMavenPublish = "0.35.0"
|
||||
|
||||
[libraries]
|
||||
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
||||
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
||||
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
||||
|
||||
[plugins]
|
||||
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
|
||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||
vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" }
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
58
gradlew
vendored
58
gradlew
vendored
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright <EFBFBD> 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -32,10 +34,10 @@
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions <EFBFBD>$var<EFBFBD>, <EFBFBD>${var}<EFBFBD>, <EFBFBD>${var:-default}<EFBFBD>, <EFBFBD>${var+SET}<EFBFBD>,
|
||||
# <EFBFBD>${var#prefix}<EFBFBD>, <EFBFBD>${var%suffix}<EFBFBD>, and <EFBFBD>$( cmd )<EFBFBD>;
|
||||
# * compound commands having a testable exit status, especially <EFBFBD>case<EFBFBD>;
|
||||
# * various built-in commands including <EFBFBD>command<EFBFBD>, <EFBFBD>set<EFBFBD>, and <EFBFBD>ulimit<EFBFBD>.
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -80,13 +82,11 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -114,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -133,22 +132,29 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -165,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -193,18 +198,27 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
||||
40
gradlew.bat
vendored
40
gradlew.bat
vendored
@@ -13,8 +13,10 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +27,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -56,32 +59,33 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
7350
package-lock.json
generated
7350
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@saithodev/semantic-release-backmerge": "^3.1.0",
|
||||
"@semantic-release/changelog": "^6.0.2",
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.7.6",
|
||||
"semantic-release": "^20.1.0"
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.2.9"
|
||||
}
|
||||
}
|
||||
|
||||
1108
patcher/api/android/core.api
Normal file
1108
patcher/api/android/core.api
Normal file
File diff suppressed because it is too large
Load Diff
1222
patcher/api/android/patcher.api
Normal file
1222
patcher/api/android/patcher.api
Normal file
File diff suppressed because it is too large
Load Diff
1108
patcher/api/jvm/core.api
Normal file
1108
patcher/api/jvm/core.api
Normal file
File diff suppressed because it is too large
Load Diff
1218
patcher/api/jvm/patcher.api
Normal file
1218
patcher/api/jvm/patcher.api
Normal file
File diff suppressed because it is too large
Load Diff
110
patcher/build.gradle.kts
Normal file
110
patcher/build.gradle.kts
Normal file
@@ -0,0 +1,110 @@
|
||||
import com.android.build.api.dsl.androidLibrary
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.kotlinMultiplatform)
|
||||
alias(libs.plugins.android.kotlin.multiplatform.library)
|
||||
alias(libs.plugins.vanniktech.mavenPublish)
|
||||
}
|
||||
|
||||
group = "app.revanced"
|
||||
|
||||
kotlin {
|
||||
@OptIn(ExperimentalAbiValidation::class)
|
||||
abiValidation {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
jvm()
|
||||
|
||||
androidLibrary {
|
||||
namespace = "app.revanced.patcher"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
minSdk = libs.versions.android.minSdk.get().toInt()
|
||||
|
||||
withHostTestBuilder {}.configure {}
|
||||
withDeviceTestBuilder {
|
||||
sourceSetTreeName = "test"
|
||||
}
|
||||
|
||||
compilations.configureEach {
|
||||
compilerOptions.configure {
|
||||
jvmTarget.set(
|
||||
JvmTarget.JVM_11
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.apktool.lib)
|
||||
implementation(libs.kotlin.reflect)
|
||||
implementation(libs.multidexlib2)
|
||||
implementation(libs.smali)
|
||||
implementation(libs.xpp3)
|
||||
}
|
||||
|
||||
jvmTest.dependencies {
|
||||
implementation(libs.mockk)
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
|
||||
compilerOptions {
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xexplicit-backing-fields",
|
||||
"-Xcontext-parameters"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
named<Test>("jvmTest") {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
mavenPublishing {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "githubPackages"
|
||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||
credentials(PasswordCredentials::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
signAllPublications()
|
||||
extensions.getByType<SigningExtension>().useGpgCmd()
|
||||
|
||||
coordinates(group.toString(), project.name, version.toString())
|
||||
|
||||
pom {
|
||||
name = "ReVanced Patcher"
|
||||
description = "Patcher used by ReVanced."
|
||||
inceptionYear = "2022"
|
||||
url = "https://revanced.app"
|
||||
licenses {
|
||||
license {
|
||||
name = "GNU General Public License v3.0"
|
||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
||||
}
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
id = "ReVanced"
|
||||
name = "ReVanced"
|
||||
email = "contact@revanced.app"
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
|
||||
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
|
||||
url = "https://github.com/revanced/revanced-patcher"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import dalvik.system.DexClassLoader
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
|
||||
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
|
||||
|
||||
/**
|
||||
* Loads patches from DEX files declared as public static fields
|
||||
* or returned by public static and non-parametrized methods.
|
||||
* Patches with no name are not loaded. If a patches file fails to load,
|
||||
* the [onFailedToLoad] callback is invoked with the file and the throwable
|
||||
* and the loading continues for the other files.
|
||||
*
|
||||
* @param patchesFiles The DEX files to load the patches from.
|
||||
* @param onFailedToLoad A callback invoked when a patches file fails to load.
|
||||
*
|
||||
* @return The loaded patches.
|
||||
*/
|
||||
actual fun loadPatches(
|
||||
vararg patchesFiles: File,
|
||||
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit,
|
||||
) = loadPatches(
|
||||
patchesFiles = patchesFiles,
|
||||
{ patchBundle ->
|
||||
MultiDexIO.readDexFile(true, patchBundle, BasicDexFileNamer(), null, null).classes
|
||||
.map { classDef ->
|
||||
classDef.type.substring(1, classDef.length - 1)
|
||||
}
|
||||
},
|
||||
DexClassLoader(
|
||||
patchesFiles.joinToString(File.pathSeparator) { it.absolutePath },
|
||||
null,
|
||||
null, null
|
||||
),
|
||||
onFailedToLoad
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package collections
|
||||
|
||||
actual fun <K, V> MutableMap<K, V>.kmpMerge(
|
||||
key: K,
|
||||
value: V,
|
||||
remappingFunction: (oldValue: V, newValue: V) -> V
|
||||
) = merge(key, value, remappingFunction)
|
||||
8
patcher/src/androidMain/kotlin/java/io/File.android.kt
Normal file
8
patcher/src/androidMain/kotlin/java/io/File.android.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package java.io
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
internal actual fun File.kmpResolve(child: String) = resolve(child)
|
||||
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
|
||||
internal actual fun File.kmpInputStream() = inputStream()
|
||||
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)
|
||||
@@ -0,0 +1,363 @@
|
||||
@file:Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.extensions.instructionsOrNull
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
@Deprecated("Use the matcher API instead.")
|
||||
class Fingerprint internal constructor(
|
||||
internal val accessFlags: Int?,
|
||||
internal val returnType: String?,
|
||||
internal val parameters: List<String>?,
|
||||
internal val opcodes: List<Opcode?>?,
|
||||
internal val strings: List<String>?,
|
||||
internal val custom: ((method: Method, classDef: ClassDef) -> Boolean)?,
|
||||
private val fuzzyPatternScanThreshold: Int,
|
||||
) {
|
||||
@Suppress("ktlint:standard:backing-property-naming")
|
||||
// Backing field needed for lazy initialization.
|
||||
private var _matchOrNull: FingerprintMatch? = null
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
private val matchOrNull: FingerprintMatch?
|
||||
get() = matchOrNull()
|
||||
|
||||
context(context: BytecodePatchContext)
|
||||
internal fun matchOrNull(): FingerprintMatch? {
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
var match = strings?.mapNotNull {
|
||||
context.classDefs.methodsByString[it]
|
||||
}?.minByOrNull { it.size }?.let { methodClasses ->
|
||||
methodClasses.forEach { method ->
|
||||
val match = matchOrNull(method, context.classDefs[method.definingClass]!!)
|
||||
if (match != null) return@let match
|
||||
}
|
||||
|
||||
null
|
||||
}
|
||||
if (match != null) return match
|
||||
context.classDefs.forEach { classDef ->
|
||||
match = matchOrNull(classDef)
|
||||
if (match != null) return match
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
classDef: ClassDef,
|
||||
): FingerprintMatch? {
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
for (method in classDef.methods) {
|
||||
val match = matchOrNull(method, classDef)
|
||||
if (match != null) return match
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
context(context: BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
method: Method,
|
||||
) = matchOrNull(method, context.classDefs[method.definingClass]!!)
|
||||
|
||||
context(context: BytecodePatchContext)
|
||||
fun matchOrNull(
|
||||
method: Method,
|
||||
classDef: ClassDef,
|
||||
): FingerprintMatch? {
|
||||
if (_matchOrNull != null) return _matchOrNull
|
||||
|
||||
if (returnType != null && !method.returnType.startsWith(returnType)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (accessFlags != null && accessFlags != method.accessFlags) {
|
||||
return null
|
||||
}
|
||||
|
||||
fun parametersEqual(
|
||||
parameters1: Iterable<CharSequence>,
|
||||
parameters2: Iterable<CharSequence>,
|
||||
): Boolean {
|
||||
if (parameters1.count() != parameters2.count()) return false
|
||||
val iterator1 = parameters1.iterator()
|
||||
parameters2.forEach {
|
||||
if (!it.startsWith(iterator1.next())) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: parseParameters()
|
||||
if (parameters != null && !parametersEqual(parameters, method.parameterTypes)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (custom != null && !custom.invoke(method, classDef)) {
|
||||
return null
|
||||
}
|
||||
|
||||
val stringMatches: List<FingerprintMatch.StringMatch>? =
|
||||
if (strings != null) {
|
||||
buildList {
|
||||
val instructions = method.instructionsOrNull ?: return null
|
||||
|
||||
val stringsList = strings.toMutableList()
|
||||
|
||||
instructions.forEachIndexed { instructionIndex, instruction ->
|
||||
if (
|
||||
instruction.opcode != Opcode.CONST_STRING &&
|
||||
instruction.opcode != Opcode.CONST_STRING_JUMBO
|
||||
) {
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
val string = ((instruction as ReferenceInstruction).reference as StringReference).string
|
||||
val index = stringsList.indexOfFirst(string::contains)
|
||||
if (index == -1) return@forEachIndexed
|
||||
|
||||
add(FingerprintMatch.StringMatch(string, instructionIndex))
|
||||
stringsList.removeAt(index)
|
||||
}
|
||||
|
||||
if (stringsList.isNotEmpty()) return null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val patternMatch = if (opcodes != null) {
|
||||
val instructions = method.instructionsOrNull ?: return null
|
||||
|
||||
fun patternScan(): FingerprintMatch.PatternMatch? {
|
||||
val fingerprintFuzzyPatternScanThreshold = fuzzyPatternScanThreshold
|
||||
|
||||
val instructionLength = instructions.count()
|
||||
val patternLength = opcodes.size
|
||||
|
||||
for (index in 0 until instructionLength) {
|
||||
var patternIndex = 0
|
||||
var threshold = fingerprintFuzzyPatternScanThreshold
|
||||
|
||||
while (index + patternIndex < instructionLength) {
|
||||
val originalOpcode = instructions.elementAt(index + patternIndex).opcode
|
||||
val patternOpcode = opcodes.elementAt(patternIndex)
|
||||
|
||||
if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) {
|
||||
// Reaching maximum threshold (0) means,
|
||||
// the pattern does not match to the current instructions.
|
||||
if (threshold-- == 0) break
|
||||
}
|
||||
|
||||
if (patternIndex < patternLength - 1) {
|
||||
// If the entire pattern has not been scanned yet, continue the scan.
|
||||
patternIndex++
|
||||
continue
|
||||
}
|
||||
|
||||
// The entire pattern has been scanned.
|
||||
return FingerprintMatch.PatternMatch(
|
||||
index,
|
||||
index + patternIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
patternScan() ?: return null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
_matchOrNull = FingerprintMatch(
|
||||
context,
|
||||
classDef,
|
||||
method,
|
||||
patternMatch,
|
||||
stringMatches,
|
||||
)
|
||||
|
||||
return _matchOrNull
|
||||
}
|
||||
|
||||
private val exception get() = PatchException("Failed to match the fingerprint: $this")
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
private val match
|
||||
get() = matchOrNull ?: throw exception
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
fun match(
|
||||
classDef: ClassDef,
|
||||
) = matchOrNull(classDef) ?: throw exception
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
fun match(
|
||||
method: Method,
|
||||
) = matchOrNull(method) ?: throw exception
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
fun match(
|
||||
method: Method,
|
||||
classDef: ClassDef,
|
||||
) = matchOrNull(method, classDef) ?: throw exception
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val originalClassDefOrNull
|
||||
get() = matchOrNull?.originalClassDef
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val originalMethodOrNull
|
||||
get() = matchOrNull?.originalMethod
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val classDefOrNull
|
||||
get() = matchOrNull?.classDef
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val methodOrNull
|
||||
get() = matchOrNull?.method
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val patternMatchOrNull
|
||||
get() = matchOrNull?.patternMatch
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val stringMatchesOrNull
|
||||
get() = matchOrNull?.stringMatches
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val originalClassDef
|
||||
get() = match.originalClassDef
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val originalMethod
|
||||
get() = match.originalMethod
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val classDef
|
||||
get() = match.classDef
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val method
|
||||
get() = match.method
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val patternMatch
|
||||
get() = match.patternMatch
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
val stringMatches
|
||||
get() = match.stringMatches
|
||||
}
|
||||
|
||||
@Deprecated("Use the matcher API instead.")
|
||||
class FingerprintMatch internal constructor(
|
||||
val context: BytecodePatchContext,
|
||||
val originalClassDef: ClassDef,
|
||||
val originalMethod: Method,
|
||||
val patternMatch: PatternMatch?,
|
||||
val stringMatches: List<StringMatch>?,
|
||||
) {
|
||||
|
||||
val classDef by lazy {
|
||||
val classDef = context.classDefs[originalClassDef.type]!!
|
||||
|
||||
context.classDefs.getOrReplaceMutable(classDef)
|
||||
}
|
||||
|
||||
val method by lazy { classDef.methods.first { MethodUtil.methodSignaturesMatch(it, originalMethod) } }
|
||||
|
||||
class PatternMatch internal constructor(
|
||||
val startIndex: Int,
|
||||
val endIndex: Int,
|
||||
)
|
||||
|
||||
class StringMatch internal constructor(val string: String, val index: Int)
|
||||
}
|
||||
|
||||
@Deprecated("Use the matcher API instead.")
|
||||
class FingerprintBuilder internal constructor(
|
||||
private val fuzzyPatternScanThreshold: Int = 0,
|
||||
) {
|
||||
private var accessFlags: Int? = null
|
||||
private var returnType: String? = null
|
||||
private var parameters: List<String>? = null
|
||||
private var opcodes: List<Opcode?>? = null
|
||||
private var strings: List<String>? = null
|
||||
private var customBlock: ((method: Method, classDef: ClassDef) -> Boolean)? = null
|
||||
|
||||
fun accessFlags(accessFlags: Int) {
|
||||
this.accessFlags = accessFlags
|
||||
}
|
||||
|
||||
fun accessFlags(vararg accessFlags: AccessFlags) {
|
||||
this.accessFlags = accessFlags.fold(0) { acc, it -> acc or it.value }
|
||||
}
|
||||
|
||||
fun returns(returnType: String) {
|
||||
this.returnType = returnType
|
||||
}
|
||||
|
||||
fun parameters(vararg parameters: String) {
|
||||
this.parameters = parameters.toList()
|
||||
}
|
||||
|
||||
fun opcodes(vararg opcodes: Opcode?) {
|
||||
this.opcodes = opcodes.toList()
|
||||
}
|
||||
|
||||
fun opcodes(instructions: String) {
|
||||
this.opcodes = instructions.trimIndent().split("\n").filter {
|
||||
it.isNotBlank()
|
||||
}.map {
|
||||
// Remove any operands.
|
||||
val name = it.split(" ", limit = 1).first().trim()
|
||||
if (name == "null") return@map null
|
||||
|
||||
opcodesByName[name] ?: throw Exception("Unknown opcode: $name")
|
||||
}
|
||||
}
|
||||
|
||||
fun strings(vararg strings: String) {
|
||||
this.strings = strings.toList()
|
||||
}
|
||||
|
||||
fun custom(customBlock: (method: Method, classDef: ClassDef) -> Boolean) {
|
||||
this.customBlock = customBlock
|
||||
}
|
||||
|
||||
internal fun build() = Fingerprint(
|
||||
accessFlags,
|
||||
returnType,
|
||||
parameters,
|
||||
opcodes,
|
||||
strings,
|
||||
customBlock,
|
||||
fuzzyPatternScanThreshold,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val opcodesByName = Opcode.entries.associateBy { it.name }
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use the matcher API instead.")
|
||||
fun fingerprint(
|
||||
fuzzyPatternScanThreshold: Int = 0,
|
||||
block: FingerprintBuilder.() -> Unit,
|
||||
) = FingerprintBuilder(fuzzyPatternScanThreshold).apply(block).build()
|
||||
1040
patcher/src/commonMain/kotlin/app/revanced/patcher/Matching.kt
Normal file
1040
patcher/src/commonMain/kotlin/app/revanced/patcher/Matching.kt
Normal file
File diff suppressed because it is too large
Load Diff
152
patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt
Normal file
152
patcher/src/commonMain/kotlin/app/revanced/patcher/Patching.kt
Normal file
@@ -0,0 +1,152 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.*
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.deleteRecursively
|
||||
import java.io.resolve
|
||||
import java.util.logging.Logger
|
||||
|
||||
fun patcher(
|
||||
apkFile: File,
|
||||
temporaryFilesPath: File = File("revanced-patcher-temporary-files"),
|
||||
aaptBinaryPath: File? = null,
|
||||
frameworkFileDirectory: String? = null,
|
||||
getPatches: (packageName: String, versionName: String) -> Set<Patch>,
|
||||
): (emit: (PatchResult) -> Unit) -> PatchesResult {
|
||||
val logger = Logger.getLogger("Patcher")
|
||||
|
||||
if (temporaryFilesPath.exists()) {
|
||||
logger.info("Deleting existing temporary files directory")
|
||||
|
||||
if (!temporaryFilesPath.deleteRecursively())
|
||||
logger.severe("Failed to delete existing temporary files directory")
|
||||
}
|
||||
|
||||
val apkFilesPath = temporaryFilesPath.resolve("apk").also { it.mkdirs() }
|
||||
val patchedFilesPath = temporaryFilesPath.resolve("patched").also { it.mkdirs() }
|
||||
|
||||
val resourcePatchContext = ResourcePatchContext(
|
||||
apkFile,
|
||||
apkFilesPath,
|
||||
patchedFilesPath,
|
||||
aaptBinaryPath,
|
||||
frameworkFileDirectory
|
||||
)
|
||||
|
||||
val (packageName, versionName) = resourcePatchContext.decodeManifest()
|
||||
val patches = getPatches(packageName, versionName)
|
||||
|
||||
return { emit: (PatchResult) -> Unit ->
|
||||
if (patches.any { patch -> patch.patchesResources }) resourcePatchContext.decodeResources()
|
||||
|
||||
// After initializing the resource context, to keep memory usage time low.
|
||||
val bytecodePatchContext = BytecodePatchContext(
|
||||
apkFile,
|
||||
patchedFilesPath
|
||||
)
|
||||
|
||||
logger.info("Warming up the cache")
|
||||
|
||||
bytecodePatchContext.classDefs.initializeCache()
|
||||
|
||||
logger.info("Applying patches")
|
||||
|
||||
patches.apply(bytecodePatchContext, resourcePatchContext, emit)
|
||||
}
|
||||
}
|
||||
|
||||
// Public for testing.
|
||||
fun Set<Patch>.apply(
|
||||
bytecodePatchContext: BytecodePatchContext,
|
||||
resourcePatchContext: ResourcePatchContext,
|
||||
emit: (PatchResult) -> Unit
|
||||
): PatchesResult {
|
||||
val appliedPatches = LinkedHashMap<Patch, PatchResult>()
|
||||
|
||||
sortedBy { it.name }.forEach { patch ->
|
||||
fun Patch.apply(): PatchResult {
|
||||
val result = appliedPatches[this]
|
||||
|
||||
return if (result == null) {
|
||||
val failedDependency = dependencies.asSequence().map { it.apply() }.firstOrNull { it.exception != null }
|
||||
if (failedDependency != null) return patchResult(
|
||||
"The dependant patch \"$failedDependency\" of the patch \"$this\" raised an exception:\n" +
|
||||
failedDependency.exception!!.stackTraceToString(),
|
||||
)
|
||||
|
||||
val exception = runCatching { apply(bytecodePatchContext, resourcePatchContext) }
|
||||
.exceptionOrNull() as? Exception
|
||||
|
||||
patchResult(exception).also { result -> appliedPatches[this] = result }
|
||||
} else if (result.exception == null) result
|
||||
else patchResult("The patch '$this' has failed previously")
|
||||
}
|
||||
|
||||
val patchResult = patch.apply()
|
||||
|
||||
// If an exception occurred or the patch has no finalize block, emit the result.
|
||||
if (patchResult.exception != null || patch.afterDependents == null) {
|
||||
emit(patchResult)
|
||||
}
|
||||
}
|
||||
|
||||
val succeededPatchesWithFinalizeBlock = appliedPatches.values.filter {
|
||||
it.exception == null && it.patch.afterDependents != null
|
||||
}
|
||||
|
||||
succeededPatchesWithFinalizeBlock.asReversed().forEach { result ->
|
||||
val patch = result.patch
|
||||
runCatching { patch.afterDependents!!.invoke(bytecodePatchContext, resourcePatchContext) }.fold(
|
||||
{ emit(result) },
|
||||
{
|
||||
emit(
|
||||
PatchResult(
|
||||
patch,
|
||||
PatchException(
|
||||
"The patch \"$patch\" raised an exception:\n" + it.stackTraceToString(),
|
||||
it,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return PatchesResult(bytecodePatchContext.get(), resourcePatchContext.get())
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of applying patches.
|
||||
*
|
||||
* @param dexFiles The patched dex files.
|
||||
* @param resources The patched resources.
|
||||
*/
|
||||
class PatchesResult internal constructor(
|
||||
val dexFiles: Set<PatchedDexFile>,
|
||||
val resources: PatchedResources?,
|
||||
) {
|
||||
|
||||
/**
|
||||
* A dex file.
|
||||
*
|
||||
* @param name The original name of the dex file.
|
||||
* @param stream The dex file as [InputStream].
|
||||
*/
|
||||
class PatchedDexFile internal constructor(val name: String, val stream: InputStream)
|
||||
|
||||
/**
|
||||
* The resources of a patched apk.
|
||||
*
|
||||
* @param resourcesApk The compiled resources.apk file.
|
||||
* @param otherResources The directory containing other resources files.
|
||||
* @param doNotCompress List of files that should not be compressed.
|
||||
* @param deleteResources List of resources that should be deleted.
|
||||
*/
|
||||
class PatchedResources internal constructor(
|
||||
val resourcesApk: File?,
|
||||
val otherResources: File?,
|
||||
val doNotCompress: Set<String>,
|
||||
val deleteResources: Set<String>,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcodes
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.*
|
||||
import com.android.tools.smali.dexlib2.writer.builder.DexBuilder
|
||||
import com.android.tools.smali.smali.smaliFlexLexer
|
||||
import com.android.tools.smali.smali.smaliParser
|
||||
import com.android.tools.smali.smali.smaliTreeWalker
|
||||
import org.antlr.runtime.CommonTokenStream
|
||||
import org.antlr.runtime.TokenSource
|
||||
import org.antlr.runtime.tree.CommonTreeNodeStream
|
||||
import java.io.StringReader
|
||||
|
||||
|
||||
private inline fun <reified T : Reference> Instruction.reference(): T? =
|
||||
(this as? ReferenceInstruction)?.reference as? T
|
||||
|
||||
val Instruction.reference: Reference?
|
||||
get() = reference()
|
||||
|
||||
val Instruction.methodReference
|
||||
get() = reference<MethodReference>()
|
||||
|
||||
val Instruction.fieldReference
|
||||
get() = reference<FieldReference>()
|
||||
|
||||
val Instruction.typeReference
|
||||
get() = reference<TypeReference>()
|
||||
|
||||
val Instruction.stringReference
|
||||
get() = reference<StringReference>()
|
||||
|
||||
/** TODO: This is technically missing for consistency:
|
||||
|
||||
private inline fun <reified T : Reference> Instruction.reference2(): T? =
|
||||
(this as? DualReferenceInstruction)?.reference2 as? T
|
||||
|
||||
val Instruction.reference2: Reference?
|
||||
get() = reference2()
|
||||
|
||||
val Instruction.methodReference2
|
||||
get() = reference2<MethodReference>()
|
||||
|
||||
val Instruction.fieldReference2
|
||||
get() = reference2<FieldReference>()
|
||||
|
||||
val Instruction.typeReference2
|
||||
get() = reference2<TypeReference>()
|
||||
|
||||
val Instruction.stringReference2
|
||||
get() = reference2<StringReference>()
|
||||
**/
|
||||
|
||||
val Instruction.type
|
||||
get() = typeReference?.type
|
||||
|
||||
val Instruction.string
|
||||
get() = stringReference?.string
|
||||
val Instruction.wideLiteral
|
||||
get() = (this as? NarrowLiteralInstruction)?.wideLiteral
|
||||
|
||||
|
||||
private const val CLASS_HEADER = ".class LInlineCompiler;\n.super Ljava/lang/Object;\n"
|
||||
private const val STATIC_HEADER = "$CLASS_HEADER.method public static dummyMethod("
|
||||
private const val HEADER = "$CLASS_HEADER.method public dummyMethod("
|
||||
private val sb by lazy { StringBuilder(512) }
|
||||
|
||||
/**
|
||||
* Compile lines of Smali code to a list of instructions.
|
||||
*
|
||||
* Note: Adding compiled instructions to an existing method with
|
||||
* offset instructions WITHOUT specifying a parent method will not work.
|
||||
* @param templateMethod The method to compile the instructions against.
|
||||
* @returns A list of instructions.
|
||||
*/
|
||||
fun String.toInstructions(templateMethod: com.android.tools.smali.dexlib2.mutable.MutableMethod? = null): List<BuilderInstruction> {
|
||||
val parameters = templateMethod?.parameterTypes?.joinToString("") { it } ?: ""
|
||||
val registers = templateMethod?.implementation?.registerCount ?: 1 // TODO: Should this be 0?
|
||||
val isStatic = templateMethod?.let { AccessFlags.STATIC.isSet(it.accessFlags) } ?: true
|
||||
|
||||
sb.setLength(0) // reset
|
||||
|
||||
if (isStatic) sb.append(STATIC_HEADER) else sb.append(HEADER)
|
||||
sb.append(parameters).append(")V\n")
|
||||
sb.append(" .registers ").append(registers).append("\n")
|
||||
sb.append(trimIndent()).append("\n")
|
||||
sb.append(".end method")
|
||||
|
||||
val reader = StringReader(sb.toString())
|
||||
val lexer = smaliFlexLexer(reader, 15)
|
||||
val tokens = CommonTokenStream(lexer as TokenSource)
|
||||
val parser = smaliParser(tokens)
|
||||
val fileTree = parser.smali_file()
|
||||
|
||||
if (lexer.numberOfSyntaxErrors > 0 || parser.numberOfSyntaxErrors > 0) {
|
||||
throw IllegalStateException(
|
||||
"Lexer errors: ${lexer.numberOfSyntaxErrors}, Parser errors: ${parser.numberOfSyntaxErrors}"
|
||||
)
|
||||
}
|
||||
|
||||
val treeStream = CommonTreeNodeStream(fileTree.tree).apply {
|
||||
tokenStream = tokens
|
||||
}
|
||||
|
||||
val walker = smaliTreeWalker(treeStream)
|
||||
walker.setDexBuilder(DexBuilder(Opcodes.getDefault()))
|
||||
|
||||
val classDef = walker.smali_file()
|
||||
return classDef.methods.first().instructions.map { it as BuilderInstruction }
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.Label
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.*
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.MethodImplementation
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod
|
||||
|
||||
fun Method.accessFlags(vararg flags: AccessFlags) =
|
||||
accessFlags.and(flags.map { it.ordinal }.reduce { acc, i -> acc or i }) != 0
|
||||
|
||||
/**
|
||||
* Add instructions to a method at the given index.
|
||||
*
|
||||
* @param index The index to add the instructions at.
|
||||
* @param instructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethodImplementation.addInstructions(
|
||||
index: Int,
|
||||
instructions: List<BuilderInstruction>,
|
||||
) = instructions.asReversed().forEach { addInstruction(index, it) }
|
||||
|
||||
/**
|
||||
* Add instructions to a method.
|
||||
* The instructions will be added at the end of the method.
|
||||
*
|
||||
* @param instructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethodImplementation.addInstructions(instructions: List<BuilderInstruction>) =
|
||||
instructions.forEach { addInstruction(it) }
|
||||
|
||||
/**
|
||||
* Remove instructions from a method at the given index.
|
||||
*
|
||||
* @param index The index to remove the instructions at.
|
||||
* @param count The amount of instructions to remove.
|
||||
*/
|
||||
fun MutableMethodImplementation.removeInstructions(
|
||||
index: Int,
|
||||
count: Int,
|
||||
) = repeat(count) {
|
||||
removeInstruction(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the first instructions from a method.
|
||||
*
|
||||
* @param count The amount of instructions to remove.
|
||||
*/
|
||||
fun MutableMethodImplementation.removeInstructions(count: Int) = removeInstructions(0, count)
|
||||
|
||||
/**
|
||||
* Replace instructions at the given index with the given instructions.
|
||||
* The amount of instructions to replace is the amount of instructions in the given list.
|
||||
*
|
||||
* @param index The index to replace the instructions at.
|
||||
* @param instructions The instructions to replace the instructions with.
|
||||
*/
|
||||
fun MutableMethodImplementation.replaceInstructions(
|
||||
index: Int,
|
||||
instructions: List<BuilderInstruction>,
|
||||
) = instructions.forEachIndexed { i, instruction -> replaceInstruction(index + i, instruction) }
|
||||
|
||||
/**
|
||||
* Add an instruction to a method at the given index.
|
||||
*
|
||||
* @param index The index to add the instruction at.
|
||||
* @param instruction The instruction to add.
|
||||
*/
|
||||
fun MutableMethod.addInstruction(
|
||||
index: Int,
|
||||
instruction: BuilderInstruction,
|
||||
) = implementation!!.addInstruction(index, instruction)
|
||||
|
||||
/**
|
||||
* Add an instruction to a method.
|
||||
*
|
||||
* @param instruction The instructions to add.
|
||||
*/
|
||||
fun MutableMethod.addInstruction(instruction: BuilderInstruction) = implementation!!.addInstruction(instruction)
|
||||
|
||||
/**
|
||||
* Add an instruction to a method at the given index.
|
||||
*
|
||||
* @param index The index to add the instruction at.
|
||||
* @param smaliInstructions The instruction to add.
|
||||
*/
|
||||
fun MutableMethod.addInstruction(
|
||||
index: Int,
|
||||
smaliInstructions: String,
|
||||
) = implementation!!.addInstruction(index, smaliInstructions.toInstructions(this).first())
|
||||
|
||||
/**
|
||||
* Add an instruction to a method.
|
||||
*
|
||||
* @param smaliInstructions The instruction to add.
|
||||
*/
|
||||
fun MutableMethod.addInstruction(smaliInstructions: String) =
|
||||
implementation!!.addInstruction(smaliInstructions.toInstructions(this).first())
|
||||
|
||||
/**
|
||||
* Add instructions to a method at the given index.
|
||||
*
|
||||
* @param index The index to add the instructions at.
|
||||
* @param instructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethod.addInstructions(
|
||||
index: Int,
|
||||
instructions: List<BuilderInstruction>,
|
||||
) = implementation!!.addInstructions(index, instructions)
|
||||
|
||||
/**
|
||||
* Add instructions to a method.
|
||||
*
|
||||
* @param instructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethod.addInstructions(instructions: List<BuilderInstruction>) =
|
||||
implementation!!.addInstructions(instructions)
|
||||
|
||||
/**
|
||||
* Add instructions to a method.
|
||||
*
|
||||
* @param smaliInstructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethod.addInstructions(
|
||||
index: Int,
|
||||
smaliInstructions: String,
|
||||
) = implementation!!.addInstructions(index, smaliInstructions.toInstructions(this))
|
||||
|
||||
/**
|
||||
* Add instructions to a method.
|
||||
*
|
||||
* @param smaliInstructions The instructions to add.
|
||||
*/
|
||||
fun MutableMethod.addInstructions(smaliInstructions: String) =
|
||||
implementation!!.addInstructions(smaliInstructions.toInstructions(this))
|
||||
|
||||
/**
|
||||
* Add instructions to a method at the given index.
|
||||
*
|
||||
* @param index The index to add the instructions at.
|
||||
* @param smaliInstructions The instructions to add.
|
||||
* @param externalLabels A list of [ExternalLabel] for instructions outside of [smaliInstructions].
|
||||
*/
|
||||
// Special function for adding instructions with external labels.
|
||||
fun MutableMethod.addInstructionsWithLabels(
|
||||
index: Int,
|
||||
smaliInstructions: String,
|
||||
vararg externalLabels: ExternalLabel,
|
||||
) {
|
||||
// Create reference dummy instructions for the instructions.
|
||||
val nopSmali =
|
||||
StringBuilder(smaliInstructions).also { builder ->
|
||||
externalLabels.forEach { (name, _) ->
|
||||
builder.append("\n:$name\nnop")
|
||||
}
|
||||
}.toString()
|
||||
|
||||
// Compile the instructions with the dummy labels
|
||||
val compiledInstructions = nopSmali.toInstructions(this)
|
||||
|
||||
// Add the compiled list of instructions to the method.
|
||||
addInstructions(
|
||||
index,
|
||||
compiledInstructions.subList(0, compiledInstructions.size - externalLabels.size),
|
||||
)
|
||||
|
||||
implementation!!.apply {
|
||||
this@apply.instructions.subList(index, index + compiledInstructions.size - externalLabels.size)
|
||||
.forEachIndexed { compiledInstructionIndex, compiledInstruction ->
|
||||
// If the compiled instruction is not an offset instruction, skip it.
|
||||
if (compiledInstruction !is BuilderOffsetInstruction) return@forEachIndexed
|
||||
|
||||
/**
|
||||
* Create a new label for the instruction
|
||||
* and replace it with the label of the [compiledInstruction] at [compiledInstructionIndex].
|
||||
*/
|
||||
fun Instruction.makeNewLabel() {
|
||||
fun replaceOffset(
|
||||
i: BuilderOffsetInstruction,
|
||||
label: Label,
|
||||
): BuilderOffsetInstruction {
|
||||
return when (i) {
|
||||
is BuilderInstruction10t -> BuilderInstruction10t(i.opcode, label)
|
||||
is BuilderInstruction20t -> BuilderInstruction20t(i.opcode, label)
|
||||
is BuilderInstruction21t -> BuilderInstruction21t(i.opcode, i.registerA, label)
|
||||
is BuilderInstruction22t ->
|
||||
BuilderInstruction22t(
|
||||
i.opcode,
|
||||
i.registerA,
|
||||
i.registerB,
|
||||
label,
|
||||
)
|
||||
|
||||
is BuilderInstruction30t -> BuilderInstruction30t(i.opcode, label)
|
||||
is BuilderInstruction31t -> BuilderInstruction31t(i.opcode, i.registerA, label)
|
||||
else -> throw IllegalStateException(
|
||||
"A non-offset instruction was given, this should never happen!",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the final label.
|
||||
val label = newLabelForIndex(this@apply.instructions.indexOf(this))
|
||||
|
||||
// Create the final instruction with the new label.
|
||||
val newInstruction =
|
||||
replaceOffset(
|
||||
compiledInstruction,
|
||||
label,
|
||||
)
|
||||
|
||||
// Replace the instruction pointing to the dummy label
|
||||
// with the new instruction pointing to the real instruction.
|
||||
replaceInstruction(index + compiledInstructionIndex, newInstruction)
|
||||
}
|
||||
|
||||
// If the compiled instruction targets its own instruction,
|
||||
// which means it points to some of its own, simply an offset has to be applied.
|
||||
val labelIndex = compiledInstruction.target.location.index
|
||||
if (labelIndex < compiledInstructions.size - externalLabels.size) {
|
||||
// Get the targets index (insertion index + the index of the dummy instruction).
|
||||
this.instructions[index + labelIndex].makeNewLabel()
|
||||
return@forEachIndexed
|
||||
}
|
||||
|
||||
// Since the compiled instruction points to a dummy instruction,
|
||||
// we can find the real instruction which it was created for by calculation.
|
||||
|
||||
// Get the index of the instruction in the externalLabels list
|
||||
// which the dummy instruction was created for.
|
||||
// This works because we created the dummy instructions in the same order as the externalLabels list.
|
||||
val (_, instruction) = externalLabels[(compiledInstructions.size - 1) - labelIndex]
|
||||
instruction.makeNewLabel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an instruction at the given index.
|
||||
*
|
||||
* @param index The index to remove the instruction at.
|
||||
*/
|
||||
fun MutableMethod.removeInstruction(index: Int) = implementation!!.removeInstruction(index)
|
||||
|
||||
/**
|
||||
* Remove instructions at the given index.
|
||||
*
|
||||
* @param index The index to remove the instructions at.
|
||||
* @param count The amount of instructions to remove.
|
||||
*/
|
||||
fun MutableMethod.removeInstructions(
|
||||
index: Int,
|
||||
count: Int,
|
||||
) = implementation!!.removeInstructions(index, count)
|
||||
|
||||
/**
|
||||
* Remove instructions at the given index.
|
||||
*
|
||||
* @param count The amount of instructions to remove.
|
||||
*/
|
||||
fun MutableMethod.removeInstructions(count: Int) = implementation!!.removeInstructions(count)
|
||||
|
||||
/**
|
||||
* Replace an instruction at the given index.
|
||||
*
|
||||
* @param index The index to replace the instruction at.
|
||||
* @param instruction The instruction to replace the instruction with.
|
||||
*/
|
||||
fun MutableMethod.replaceInstruction(
|
||||
index: Int,
|
||||
instruction: BuilderInstruction,
|
||||
) = implementation!!.replaceInstruction(index, instruction)
|
||||
|
||||
/**
|
||||
* Replace an instruction at the given index.
|
||||
*
|
||||
* @param index The index to replace the instruction at.
|
||||
* @param smaliInstruction The smali instruction to replace the instruction with.
|
||||
*/
|
||||
fun MutableMethod.replaceInstruction(
|
||||
index: Int,
|
||||
smaliInstruction: String,
|
||||
) = implementation!!.replaceInstruction(index, smaliInstruction.toInstructions(this).first())
|
||||
|
||||
/**
|
||||
* Replace instructions at the given index.
|
||||
*
|
||||
* @param index The index to replace the instructions at.
|
||||
* @param instructions The instructions to replace the instructions with.
|
||||
*/
|
||||
fun MutableMethod.replaceInstructions(
|
||||
index: Int,
|
||||
instructions: List<BuilderInstruction>,
|
||||
) = implementation!!.replaceInstructions(index, instructions)
|
||||
|
||||
/**
|
||||
* Replace instructions at the given index.
|
||||
*
|
||||
* @param index The index to replace the instructions at.
|
||||
* @param smaliInstructions The smali instructions to replace the instructions with.
|
||||
*/
|
||||
fun MutableMethod.replaceInstructions(
|
||||
index: Int,
|
||||
smaliInstructions: String,
|
||||
) = implementation!!.replaceInstructions(index, smaliInstructions.toInstructions(this))
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun MethodImplementation.getInstruction(index: Int) = instructions.elementAt(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> MethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun MutableMethodImplementation.getInstruction(index: Int): BuilderInstruction = instructions[index]
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
*
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> MutableMethodImplementation.getInstruction(index: Int): T = getInstruction(index) as T
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun Method.getInstructionOrNull(index: Int): Instruction? = implementation?.getInstruction(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun Method.getInstruction(index: Int): Instruction = getInstructionOrNull(index)!!
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun <T> Method.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun <T> Method.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun MutableMethod.getInstructionOrNull(index: Int): BuilderInstruction? = implementation?.getInstruction(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun MutableMethod.getInstruction(index: Int): BuilderInstruction = getInstructionOrNull(index)!!
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction or null if the method has no implementation.
|
||||
*/
|
||||
fun <T> MutableMethod.getInstructionOrNull(index: Int): T? = implementation?.getInstruction<T>(index)
|
||||
|
||||
/**
|
||||
* Get an instruction at the given index.
|
||||
* @param index The index to get the instruction at.
|
||||
* @param T The type of instruction to return.
|
||||
* @return The instruction.
|
||||
*/
|
||||
fun <T> MutableMethod.getInstruction(index: Int): T = getInstructionOrNull<T>(index)!!
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions or null if the method has no implementation.
|
||||
*/
|
||||
val Method.instructionsOrNull: Iterable<Instruction>? get() = implementation?.instructions
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions.
|
||||
*/
|
||||
val Method.instructions: Iterable<Instruction> get() = instructionsOrNull!!
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions or null if the method has no implementation.
|
||||
*/
|
||||
val MutableMethod.instructionsOrNull: MutableList<BuilderInstruction>? get() = implementation?.instructions
|
||||
|
||||
/**
|
||||
* The instructions of a method.
|
||||
* @return The instructions.
|
||||
*/
|
||||
val MutableMethod.instructions: MutableList<BuilderInstruction> get() = instructionsOrNull!!
|
||||
|
||||
/**
|
||||
* Create a label for the instruction at given index.
|
||||
*
|
||||
* @param index The index to create the label for the instruction at.
|
||||
* @return The label.
|
||||
*/
|
||||
fun MutableMethod.newLabel(index: Int) = implementation!!.newLabelForIndex(index)
|
||||
|
||||
/**
|
||||
* A class that represents a label for an instruction.
|
||||
* @param name The label name.
|
||||
* @param instruction The instruction that this label is for.
|
||||
*/
|
||||
data class ExternalLabel(internal val name: String, internal val instruction: Instruction)
|
||||
@@ -0,0 +1,264 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchesResult
|
||||
import app.revanced.patcher.extensions.instructionsOrNull
|
||||
import app.revanced.patcher.extensions.string
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import app.revanced.patcher.util.MethodNavigator
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import lanchon.multidexlib2.RawDexIO
|
||||
import java.io.*
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
/**
|
||||
* A context for patches containing the current state of the bytecode.
|
||||
*
|
||||
* @param apkFile The apk [File] to patch.
|
||||
* @param patchedFilesPath The path to the temporary apk files directory.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class BytecodePatchContext internal constructor(
|
||||
internal val apkFile: File,
|
||||
internal val patchedFilesPath: File,
|
||||
) : PatchContext<Set<PatchesResult.PatchedDexFile>> {
|
||||
private val logger = Logger.getLogger(this::class.jvmName)
|
||||
|
||||
inner class ClassDefs private constructor(
|
||||
dexFile: DexFile,
|
||||
private val classDefs: MutableSet<ClassDef> = dexFile.classes.toMutableSet()
|
||||
) :
|
||||
MutableSet<ClassDef> by classDefs {
|
||||
private val byType = mutableMapOf<String, ClassDef>()
|
||||
|
||||
operator fun get(name: String): ClassDef? = byType[name]
|
||||
|
||||
// Better performance according to
|
||||
// https://github.com/LisoUseInAIKyrios/revanced-patcher/commit/9b6d95d4f414a35ed68da37b0ecd8549df1ef63a
|
||||
private val _methodsByStrings =
|
||||
LinkedHashMap<String, MutableSet<Method>>(2 * size, 0.5f)
|
||||
|
||||
val methodsByString: Map<String, Set<Method>> = _methodsByStrings
|
||||
|
||||
// Can have a use-case in the future:
|
||||
// private val _methodsWithString = methodsByString.values.flatten().toMutableSet()
|
||||
// val methodsWithString: Set<Method> = _methodsWithString
|
||||
|
||||
constructor() : this(
|
||||
MultiDexIO.readDexFile(
|
||||
true,
|
||||
apkFile,
|
||||
BasicDexFileNamer(),
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
|
||||
internal val opcodes = dexFile.opcodes
|
||||
|
||||
override fun add(element: ClassDef): Boolean {
|
||||
val added = classDefs.add(element)
|
||||
if (added) addCache(element)
|
||||
|
||||
return added
|
||||
}
|
||||
|
||||
override fun addAll(elements: Collection<ClassDef>): Boolean {
|
||||
var anyAdded = false
|
||||
elements.forEach { element ->
|
||||
val added = classDefs.add(element)
|
||||
if (added) {
|
||||
addCache(element)
|
||||
anyAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
return anyAdded
|
||||
}
|
||||
|
||||
// TODO: There is one default method "removeIf" in MutableSet, which we cannot override in the common module.
|
||||
// The method must be overloaded with a NotImplementedException to avoid cache desynchronization.
|
||||
|
||||
override fun clear() {
|
||||
classDefs.clear()
|
||||
byType.clear()
|
||||
_methodsByStrings.clear()
|
||||
}
|
||||
|
||||
override fun remove(element: ClassDef): Boolean {
|
||||
val removed = classDefs.remove(element)
|
||||
if (removed) removeCache(element)
|
||||
|
||||
return removed
|
||||
}
|
||||
|
||||
override fun removeAll(elements: Collection<ClassDef>): Boolean {
|
||||
var anyRemoved = false
|
||||
elements.forEach { element ->
|
||||
val removed = classDefs.remove(element)
|
||||
if (removed) {
|
||||
removeCache(element)
|
||||
anyRemoved = true
|
||||
}
|
||||
}
|
||||
|
||||
return anyRemoved
|
||||
}
|
||||
|
||||
override fun retainAll(elements: Collection<ClassDef>) =
|
||||
removeAll(classDefs.asSequence().filter { it !in elements })
|
||||
|
||||
private fun addCache(classDef: ClassDef) {
|
||||
byType[classDef.type] = classDef
|
||||
|
||||
classDef.forEachString { method, string ->
|
||||
_methodsByStrings.getOrPut(string) {
|
||||
// Maybe adjusting load factor/ initial size can improve performance.
|
||||
mutableSetOf()
|
||||
} += method
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeCache(classDef: ClassDef) {
|
||||
byType -= classDef.type
|
||||
|
||||
classDef.forEachString { method, string ->
|
||||
if (_methodsByStrings[string]?.also { it -= method }?.isEmpty() == true)
|
||||
_methodsByStrings -= string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun ClassDef.forEachString(action: (Method, String) -> Unit) {
|
||||
methods.asSequence().forEach { method ->
|
||||
method.instructionsOrNull?.asSequence()
|
||||
?.mapNotNull { it.string }
|
||||
?.forEach { string -> action(method, string) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a mutable version of the given [classDef], replacing it in the set if necessary.
|
||||
*
|
||||
* @param classDef The [ClassDef] to get or replace.
|
||||
* @return The mutable version of the [classDef].
|
||||
* @see MutableClassDef
|
||||
* @see toMutable
|
||||
*/
|
||||
fun getOrReplaceMutable(classDef: ClassDef): MutableClassDef {
|
||||
if (classDef !is MutableClassDef) {
|
||||
val mutableClassDef = classDef.toMutable()
|
||||
this -= classDef
|
||||
this += mutableClassDef
|
||||
|
||||
return mutableClassDef
|
||||
}
|
||||
|
||||
return classDef
|
||||
}
|
||||
|
||||
internal fun initializeCache() = classDefs.forEach(::addCache)
|
||||
|
||||
internal fun clearCache() {
|
||||
byType.clear()
|
||||
_methodsByStrings.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of classes.
|
||||
*/
|
||||
val classDefs = ClassDefs()
|
||||
|
||||
/**
|
||||
* Extend this [BytecodePatchContext] with [extensionInputStream].
|
||||
*
|
||||
* @param extensionInputStream The input stream for an extension dex file.
|
||||
*/
|
||||
internal fun extendWith(extensionInputStream: InputStream) {
|
||||
RawDexIO.readRawDexFile(
|
||||
extensionInputStream, 0, null
|
||||
).classes.forEach { classDef ->
|
||||
val existingClass = classDefs[classDef.type] ?: run {
|
||||
logger.fine { "Adding class \"$classDef\"" }
|
||||
|
||||
classDefs += classDef
|
||||
|
||||
return@forEach
|
||||
}
|
||||
|
||||
logger.fine { "Class \"$classDef\" exists already. Adding missing methods and fields." }
|
||||
|
||||
existingClass.merge(classDef, this@BytecodePatchContext).let { mergedClass ->
|
||||
// If the class was merged, replace the original class with the merged class.
|
||||
if (mergedClass === existingClass) {
|
||||
return@let
|
||||
}
|
||||
|
||||
classDefs -= existingClass
|
||||
classDefs += mergedClass
|
||||
}
|
||||
}
|
||||
|
||||
extensionInputStream.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate a method.
|
||||
*
|
||||
* @param method The method to navigate.
|
||||
*
|
||||
* @return A [MethodNavigator] for the method.
|
||||
*/
|
||||
fun navigate(method: MethodReference) = MethodNavigator(method)
|
||||
|
||||
/**
|
||||
* Compile bytecode from the [BytecodePatchContext].
|
||||
*
|
||||
* @return The compiled bytecode.
|
||||
*/
|
||||
override fun get(): Set<PatchesResult.PatchedDexFile> {
|
||||
logger.info("Compiling patched dex files")
|
||||
|
||||
classDefs.clearCache()
|
||||
System.gc()
|
||||
|
||||
val patchedDexFileResults =
|
||||
patchedFilesPath.resolve("dex").also {
|
||||
it.deleteRecursively() // Make sure the directory is empty.
|
||||
it.mkdirs()
|
||||
}.apply {
|
||||
MultiDexIO.writeDexFile(
|
||||
true,
|
||||
-1,
|
||||
this,
|
||||
BasicDexFileNamer(),
|
||||
object : DexFile {
|
||||
override fun getClasses() = classDefs.let {
|
||||
// More performant according to
|
||||
// https://github.com/LisoUseInAIKyrios/revanced-patcher/
|
||||
// commit/8c26ad08457fb1565ea5794b7930da42a1c81cf1
|
||||
// #diff-be698366d9868784ecf7da3fd4ac9d2b335b0bb637f9f618fbe067dbd6830b8fR197
|
||||
// TODO: Benchmark, if actually faster.
|
||||
HashSet<ClassDef>(it.size * 3 / 2).apply { addAll(it) }
|
||||
}
|
||||
|
||||
override fun getOpcodes() = classDefs.opcodes
|
||||
},
|
||||
DexIO.DEFAULT_MAX_DEX_POOL_SIZE,
|
||||
) { _, entryName, _ -> logger.info { "Compiled $entryName" } }
|
||||
}.listFiles { it.isFile }!!.map {
|
||||
PatchesResult.PatchedDexFile(it.name, it.inputStream())
|
||||
}.toSet()
|
||||
|
||||
return patchedDexFileResults
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.KType
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
/**
|
||||
* An option.
|
||||
*
|
||||
* @param T The value type of the option.
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param type The type of the option value (to handle type erasure).
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @constructor Create a new [Option].
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
class Option<T>
|
||||
@PublishedApi
|
||||
internal constructor(
|
||||
val name: String,
|
||||
val default: T? = null,
|
||||
val values: Map<String, T?>? = null,
|
||||
val description: String? = null,
|
||||
val required: Boolean = false,
|
||||
val type: KType,
|
||||
val validator: Option<T>.(T?) -> Boolean = { true },
|
||||
) {
|
||||
/**
|
||||
* The value of the [Option].
|
||||
*/
|
||||
var value: T?
|
||||
/**
|
||||
* Set the value of the [Option].
|
||||
*
|
||||
* @param value The value to set.
|
||||
*
|
||||
* @throws OptionException.ValueRequiredException If the value is required but null.
|
||||
* @throws OptionException.ValueValidationException If the value is invalid.
|
||||
*/
|
||||
set(value) {
|
||||
assertRequiredButNotNull(value)
|
||||
assertValid(value)
|
||||
|
||||
uncheckedValue = value
|
||||
}
|
||||
/**
|
||||
* Get the value of the [Option].
|
||||
*
|
||||
* @return The value.
|
||||
*
|
||||
* @throws OptionException.ValueRequiredException If the value is required but null.
|
||||
* @throws OptionException.ValueValidationException If the value is invalid.
|
||||
*/
|
||||
get() {
|
||||
assertRequiredButNotNull(uncheckedValue)
|
||||
assertValid(uncheckedValue)
|
||||
|
||||
return uncheckedValue
|
||||
}
|
||||
|
||||
// The unchecked value is used to allow setting the value without validation.
|
||||
private var uncheckedValue = default
|
||||
|
||||
/**
|
||||
* Reset the [Option] to its default value.
|
||||
* Override this method if you need to mutate the value instead of replacing it.
|
||||
*/
|
||||
fun reset() {
|
||||
uncheckedValue = default
|
||||
}
|
||||
|
||||
private fun assertRequiredButNotNull(value: T?) {
|
||||
if (required && value == null) throw OptionException.ValueRequiredException(this)
|
||||
}
|
||||
|
||||
private fun assertValid(value: T?) {
|
||||
if (!validator(value)) throw OptionException.ValueValidationException(value, this)
|
||||
}
|
||||
|
||||
override fun toString() = value.toString()
|
||||
|
||||
operator fun getValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
) = value
|
||||
|
||||
operator fun setValue(
|
||||
thisRef: Any?,
|
||||
property: KProperty<*>,
|
||||
value: T?,
|
||||
) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of [Option]s where options can be set and retrieved by their key.
|
||||
*
|
||||
* @param options The options.
|
||||
*
|
||||
* @constructor Create a new [Options].
|
||||
*/
|
||||
class Options internal constructor(
|
||||
private val options: Map<String, Option<*>>,
|
||||
) : Map<String, Option<*>> by options {
|
||||
internal constructor(options: Set<Option<*>>) : this(options.associateBy { it.name })
|
||||
|
||||
/**
|
||||
* Set an option's value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param value The value.
|
||||
*
|
||||
* @throws OptionException.OptionNotFoundException If the option does not exist.
|
||||
*/
|
||||
operator fun <T : Any> set(name: String, value: T?) {
|
||||
val option = this[name]
|
||||
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(option as Option<T>).value = value
|
||||
} catch (e: ClassCastException) {
|
||||
throw OptionException.InvalidValueTypeException(
|
||||
value?.let { it::class.jvmName } ?: "null",
|
||||
option.value?.let { it::class.jvmName } ?: "null",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an option.
|
||||
*
|
||||
* @param key The name.
|
||||
*
|
||||
* @return The option.
|
||||
*/
|
||||
override fun get(key: String) = options[key] ?: throw OptionException.OptionNotFoundException(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a string value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun stringOption(
|
||||
name: String,
|
||||
default: String? = null,
|
||||
values: Map<String, String?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<String>.(String?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a string value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.stringOption(
|
||||
name: String,
|
||||
default: String? = null,
|
||||
values: Map<String, String?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<String>.(String?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with an integer value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun intOption(
|
||||
name: String,
|
||||
default: Int? = null,
|
||||
values: Map<String, Int?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Int>.(Int?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with an integer value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.intOption(
|
||||
name: String,
|
||||
default: Int? = null,
|
||||
values: Map<String, Int?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Int>.(Int?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a boolean value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun booleanOption(
|
||||
name: String,
|
||||
default: Boolean? = null,
|
||||
values: Map<String, Boolean?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Boolean>.(Boolean?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a boolean value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.booleanOption(
|
||||
name: String,
|
||||
default: Boolean? = null,
|
||||
values: Map<String, Boolean?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Boolean>.(Boolean?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a float value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun floatOption(
|
||||
name: String,
|
||||
default: Float? = null,
|
||||
values: Map<String, Float?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Float>.(Float?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a float value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.floatOption(
|
||||
name: String,
|
||||
default: Float? = null,
|
||||
values: Map<String, Float?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Float>.(Float?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a long value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun longOption(
|
||||
name: String,
|
||||
default: Long? = null,
|
||||
values: Map<String, Long?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Long>.(Long?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a long value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.longOption(
|
||||
name: String,
|
||||
default: Long? = null,
|
||||
values: Map<String, Long?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<Long>.(Long?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a string list value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun stringsOption(
|
||||
name: String,
|
||||
default: List<String>? = null,
|
||||
values: Map<String, List<String>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a string list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.stringsOption(
|
||||
name: String,
|
||||
default: List<String>? = null,
|
||||
values: Map<String, List<String>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<String>>.(List<String>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with an integer list value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun intsOption(
|
||||
name: String,
|
||||
default: List<Int>? = null,
|
||||
values: Map<String, List<Int>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with an integer list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.intsOption(
|
||||
name: String,
|
||||
default: List<Int>? = null,
|
||||
values: Map<String, List<Int>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Int>>.(List<Int>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a boolean list value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun booleansOption(
|
||||
name: String,
|
||||
default: List<Boolean>? = null,
|
||||
values: Map<String, List<Boolean>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a boolean list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.booleansOption(
|
||||
name: String,
|
||||
default: List<Boolean>? = null,
|
||||
values: Map<String, List<Boolean>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Boolean>>.(List<Boolean>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a float list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.floatsOption(
|
||||
name: String,
|
||||
default: List<Float>? = null,
|
||||
values: Map<String, List<Float>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Float>>.(List<Float>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a long list value.
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun longsOption(
|
||||
name: String,
|
||||
default: List<Long>? = null,
|
||||
values: Map<String, List<Long>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] with a long list value and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
fun PatchBuilder<*>.longsOption(
|
||||
name: String,
|
||||
default: List<Long>? = null,
|
||||
values: Map<String, List<Long>?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
validator: Option<List<Long>>.(List<Long>?) -> Boolean = { true },
|
||||
) = option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
inline fun <reified T> option(
|
||||
name: String,
|
||||
default: T? = null,
|
||||
values: Map<String, T?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
noinline validator: Option<T>.(T?) -> Boolean = { true },
|
||||
) = Option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
typeOf<T>(),
|
||||
validator,
|
||||
)
|
||||
|
||||
/**
|
||||
* Create a new [Option] and add it to the current [PatchBuilder].
|
||||
*
|
||||
* @param name The name.
|
||||
* @param default The default value.
|
||||
* @param values Eligible option values mapped to a human-readable name.
|
||||
* @param description A description.
|
||||
* @param required Whether the option is required.
|
||||
* @param validator The function to validate the option value.
|
||||
*
|
||||
* @return The created [Option].
|
||||
*
|
||||
* @see Option
|
||||
*/
|
||||
inline fun <reified T> PatchBuilder<*>.option(
|
||||
name: String,
|
||||
default: T? = null,
|
||||
values: Map<String, T?>? = null,
|
||||
description: String? = null,
|
||||
required: Boolean = false,
|
||||
noinline validator: Option<T>.(T?) -> Boolean = { true },
|
||||
) = app.revanced.patcher.patch.option(
|
||||
name,
|
||||
default,
|
||||
values,
|
||||
description,
|
||||
required,
|
||||
validator,
|
||||
).also { it() }
|
||||
|
||||
/**
|
||||
* An exception thrown when using [Option]s.
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
*/
|
||||
sealed class OptionException(errorMessage: String) : Exception(errorMessage, null) {
|
||||
/**
|
||||
* An exception thrown when a [Option] is set to an invalid value.
|
||||
*
|
||||
* @param invalidType The type of the value that was passed.
|
||||
* @param expectedType The type of the value that was expected.
|
||||
*/
|
||||
class InvalidValueTypeException(invalidType: String, expectedType: String) :
|
||||
OptionException("Type $expectedType was expected but received type $invalidType")
|
||||
|
||||
/**
|
||||
* An exception thrown when a value did not satisfy the value conditions specified by the [Option].
|
||||
*
|
||||
* @param value The value that failed validation.
|
||||
*/
|
||||
class ValueValidationException(value: Any?, option: Option<*>) :
|
||||
OptionException("The option value \"$value\" failed validation for ${option.name}")
|
||||
|
||||
/**
|
||||
* An exception thrown when a value is required but null was passed.
|
||||
*
|
||||
* @param option The [Option] that requires a value.
|
||||
*/
|
||||
class ValueRequiredException(option: Option<*>) :
|
||||
OptionException("The option ${option.name} requires a value, but the value was null")
|
||||
|
||||
/**
|
||||
* An exception thrown when a [Option] is not found.
|
||||
*
|
||||
* @param name The name of the [Option].
|
||||
*/
|
||||
class OptionNotFoundException(name: String) : OptionException("No option with name $name")
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
@file:Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.lang.reflect.Member
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.util.function.Supplier
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
|
||||
typealias PackageName = String
|
||||
typealias VersionName = String
|
||||
typealias Package = Pair<PackageName, Set<VersionName>?>
|
||||
|
||||
enum class PatchType(internal val prefix: String) {
|
||||
BYTECODE("Bytecode"),
|
||||
RAW_RESOURCE("RawResource"),
|
||||
RESOURCE("Resource")
|
||||
}
|
||||
|
||||
internal val Patch.patchesResources: Boolean get() = type == PatchType.RESOURCE || dependencies.any { it.patchesResources }
|
||||
|
||||
open class Patch internal constructor(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val use: Boolean,
|
||||
val dependencies: Set<Patch>,
|
||||
val compatiblePackages: Set<Package>?,
|
||||
options: Set<Option<*>>,
|
||||
internal val apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit,
|
||||
// Must be nullable, so that Patcher.invoke can check,
|
||||
// if a patch has an "afterDependents" in order to not emit it twice.
|
||||
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)?,
|
||||
internal val type: PatchType,
|
||||
) {
|
||||
val options = Options(options)
|
||||
|
||||
override fun toString() = name ?: "${type.prefix}Patch@${System.identityHashCode(this)}"
|
||||
}
|
||||
|
||||
sealed class PatchBuilder<C : PatchContext<*>>(
|
||||
private val type: PatchType,
|
||||
private val getPatchContext: context(BytecodePatchContext, ResourcePatchContext) () -> C
|
||||
) {
|
||||
private var compatiblePackages: MutableSet<Package>? = null
|
||||
private val dependencies = mutableSetOf<Patch>()
|
||||
private val options = mutableSetOf<Option<*>>()
|
||||
|
||||
internal var apply: context(BytecodePatchContext, ResourcePatchContext) () -> Unit = { }
|
||||
internal var afterDependents: (context(BytecodePatchContext, ResourcePatchContext) () -> Unit)? = null
|
||||
|
||||
context(_: BytecodePatchContext, _: ResourcePatchContext)
|
||||
private val patchContext get() = getPatchContext()
|
||||
|
||||
fun apply(block: C.() -> Unit) {
|
||||
apply = { block(patchContext) }
|
||||
}
|
||||
|
||||
fun afterDependents(block: C.() -> Unit) {
|
||||
afterDependents = { block(patchContext) }
|
||||
}
|
||||
|
||||
operator fun <T> Option<T>.invoke() = apply {
|
||||
options += this
|
||||
}
|
||||
|
||||
operator fun String.invoke(vararg versions: VersionName) = invoke(versions.toSet())
|
||||
|
||||
private operator fun String.invoke(versions: Set<VersionName>? = null): Package = this to versions
|
||||
|
||||
fun compatibleWith(vararg packages: Package) {
|
||||
if (compatiblePackages == null) {
|
||||
compatiblePackages = mutableSetOf()
|
||||
}
|
||||
|
||||
compatiblePackages!! += packages
|
||||
}
|
||||
|
||||
fun compatibleWith(vararg packages: String) = compatibleWith(*packages.map { it() }.toTypedArray())
|
||||
|
||||
fun dependsOn(vararg patches: Patch) {
|
||||
dependencies += patches
|
||||
}
|
||||
|
||||
|
||||
fun build(name: String?, description: String?, use: Boolean) = Patch(
|
||||
name,
|
||||
description,
|
||||
use,
|
||||
dependencies,
|
||||
compatiblePackages,
|
||||
options,
|
||||
apply,
|
||||
afterDependents,
|
||||
type,
|
||||
)
|
||||
}
|
||||
|
||||
class BytecodePatchBuilder private constructor(
|
||||
private var extensionInputStream: InputStream? = null
|
||||
) : PatchBuilder<BytecodePatchContext>(
|
||||
PatchType.BYTECODE,
|
||||
{
|
||||
// Extend the context with the extension, before returning it to the patch before applying it.
|
||||
contextOf<BytecodePatchContext>().apply {
|
||||
if (extensionInputStream != null) extendWith(extensionInputStream)
|
||||
}
|
||||
}
|
||||
) {
|
||||
internal constructor() : this(null)
|
||||
|
||||
fun extendWith(extension: String) {
|
||||
// Should be the classloader which loaded the patch class.
|
||||
val classLoader = Class.forName(Thread.currentThread().stackTrace[2].className).classLoader!!
|
||||
|
||||
extensionInputStream = classLoader.getResourceAsStream(extension)
|
||||
?: throw PatchException("Extension \"$extension\" not found")
|
||||
}
|
||||
}
|
||||
|
||||
open class ResourcePatchBuilder internal constructor(type: PatchType) : PatchBuilder<ResourcePatchContext>(
|
||||
type,
|
||||
{ contextOf<ResourcePatchContext>() }
|
||||
) {
|
||||
internal constructor() : this(PatchType.RESOURCE)
|
||||
}
|
||||
|
||||
class RawResourcePatchBuilder internal constructor() : ResourcePatchBuilder()
|
||||
|
||||
fun bytecodePatch(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: BytecodePatchBuilder.() -> Unit
|
||||
) = BytecodePatchBuilder().apply(block).build(name, description, use)
|
||||
|
||||
fun resourcePatch(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: ResourcePatchBuilder.() -> Unit
|
||||
) = ResourcePatchBuilder().apply(block).build(name, description, use)
|
||||
|
||||
fun rawResourcePatch(
|
||||
name: String? = null,
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: RawResourcePatchBuilder.() -> Unit
|
||||
) = RawResourcePatchBuilder().apply(block).build(name, description, use)
|
||||
|
||||
private fun <B : PatchBuilder<*>> creatingPatch(
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: B.() -> Unit,
|
||||
patchSupplier: (String?, String?, Boolean, B.() -> Unit) -> Patch
|
||||
) = ReadOnlyProperty<Any?, Patch> { _, property -> patchSupplier(property.name, description, use, block) }
|
||||
|
||||
fun creatingBytecodePatch(
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: BytecodePatchBuilder.() -> Unit,
|
||||
) = creatingPatch(description, use, block) { name, description, use, block ->
|
||||
bytecodePatch(name, description, use, block)
|
||||
}
|
||||
|
||||
fun creatingResourcePatch(
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: ResourcePatchBuilder.() -> Unit,
|
||||
) = creatingPatch(description, use, block) { name, description, use, block ->
|
||||
resourcePatch(name, description, use, block)
|
||||
}
|
||||
|
||||
fun creatingRawResourcePatch(
|
||||
description: String? = null,
|
||||
use: Boolean = true,
|
||||
block: RawResourcePatchBuilder.() -> Unit,
|
||||
) = creatingPatch(description, use, block) { name, description, use, block ->
|
||||
rawResourcePatch(name, description, use, block)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A common interface for contexts such as [ResourcePatchContext] and [BytecodePatchContext].
|
||||
*/
|
||||
|
||||
sealed interface PatchContext<T> : Supplier<T>
|
||||
|
||||
/**
|
||||
* An exception thrown when patching.
|
||||
*
|
||||
* @param errorMessage The exception message.
|
||||
* @param cause The corresponding [Throwable].
|
||||
*/
|
||||
class PatchException(errorMessage: String?, cause: Throwable?) : Exception(errorMessage, cause) {
|
||||
constructor(errorMessage: String) : this(errorMessage, null)
|
||||
constructor(cause: Throwable) : this(cause.message, cause)
|
||||
}
|
||||
|
||||
/**
|
||||
* A result of applying a [Patch].
|
||||
*
|
||||
* @param patch The [Patch] that ran.
|
||||
* @param exception The [PatchException] thrown, if any.
|
||||
*/
|
||||
class PatchResult internal constructor(val patch: Patch, val exception: PatchException? = null)
|
||||
|
||||
/**
|
||||
* Creates a [PatchResult] for this [Patch].
|
||||
*
|
||||
* @param exception The [PatchException] thrown, if any.
|
||||
* @return The created [PatchResult].
|
||||
*/
|
||||
internal fun Patch.patchResult(exception: Exception? = null) = PatchResult(this, exception?.toPatchException())
|
||||
|
||||
/**
|
||||
* Creates a [PatchResult] for this [Patch] with the given error message.
|
||||
*
|
||||
* @param errorMessage The error message.
|
||||
* @return The created [PatchResult].
|
||||
*/
|
||||
internal fun Patch.patchResult(errorMessage: String) = PatchResult(this, PatchException(errorMessage))
|
||||
private fun Exception.toPatchException() = this as? PatchException ?: PatchException(this)
|
||||
|
||||
/**
|
||||
* A collection of patches loaded from patches files.
|
||||
*
|
||||
* @property patchesByFile The patches mapped by their patches file.
|
||||
*/
|
||||
class Patches internal constructor(val patchesByFile: Map<File, Set<Patch>>) : Set<Patch>
|
||||
by patchesByFile.values.flatten().toSet()
|
||||
|
||||
// Must be internal and a separate function for testing.
|
||||
@Suppress("MISSING_DEPENDENCY_IN_INFERRED_TYPE_ANNOTATION_WARNING")
|
||||
internal fun getPatches(classNames: List<String>, classLoader: ClassLoader): Set<Patch> {
|
||||
fun Member.isUsable() =
|
||||
Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && (this !is Method || parameterCount == 0)
|
||||
|
||||
fun Class<*>.getPatchFields() = fields
|
||||
.filter { it.type.isPatch && it.isUsable() }
|
||||
.map { it.get(null) as Patch }
|
||||
|
||||
fun Class<*>.getPatchMethods() = methods
|
||||
.filter { it.returnType.isPatch && it.parameterCount == 0 && it.isUsable() }
|
||||
.map { it.invoke(null) as Patch }
|
||||
|
||||
return classNames
|
||||
.map { classLoader.loadClass(it) }
|
||||
.flatMap { it.getPatchMethods() + it.getPatchFields() }
|
||||
.filter { it.name != null }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
internal fun loadPatches(
|
||||
vararg patchesFiles: File,
|
||||
getBinaryClassNames: (patchesFile: File) -> List<String>,
|
||||
classLoader: ClassLoader,
|
||||
onFailedToLoad: (File, Throwable) -> Unit
|
||||
) = Patches(patchesFiles.map { file ->
|
||||
file to getBinaryClassNames(file)
|
||||
}.mapNotNull { (file, classNames) ->
|
||||
runCatching { file to getPatches(classNames, classLoader) }
|
||||
.onFailure { onFailedToLoad(file, it) }.getOrNull()
|
||||
}.toMap())
|
||||
|
||||
expect fun loadPatches(
|
||||
vararg patchesFiles: File,
|
||||
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit = { _, _ -> },
|
||||
): Patches
|
||||
|
||||
internal expect val Class<*>.isPatch: Boolean
|
||||
@@ -0,0 +1,229 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import app.revanced.patcher.PatchesResult
|
||||
import app.revanced.patcher.util.Document
|
||||
import brut.androlib.AaptInvoker
|
||||
import brut.androlib.ApkDecoder
|
||||
import brut.androlib.Config
|
||||
import brut.androlib.apk.ApkInfo
|
||||
import brut.androlib.apk.UsesFramework
|
||||
import brut.androlib.res.Framework
|
||||
import brut.androlib.res.ResourcesDecoder
|
||||
import brut.androlib.res.decoder.AndroidManifestPullStreamDecoder
|
||||
import brut.androlib.res.decoder.AndroidManifestResourceParser
|
||||
import brut.androlib.res.xml.ResXmlUtils
|
||||
import brut.directory.ExtFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.resolve
|
||||
import java.nio.file.Files
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
/**
|
||||
* A context for patches containing the current state of resources.
|
||||
*
|
||||
* @param apkFile The apk file to patch.
|
||||
* @param apkFilesPath The path to the temporary apk files directory.
|
||||
* @param patchedFilesPath The path to the temporary patched files directory.
|
||||
* @param aaptBinaryPath The path to a custom aapt binary.
|
||||
* @param frameworkFileDirectory The path to the directory to cache the framework file in.
|
||||
*/
|
||||
class ResourcePatchContext internal constructor(
|
||||
private val apkFile: File,
|
||||
private val apkFilesPath: File,
|
||||
private val patchedFilesPath: File,
|
||||
aaptBinaryPath: File? = null,
|
||||
frameworkFileDirectory: String? = null,
|
||||
) : PatchContext<PatchesResult.PatchedResources?> {
|
||||
private val apkInfo = ApkInfo(ExtFile(apkFile))
|
||||
|
||||
private val logger = Logger.getLogger(ResourcePatchContext::class.jvmName)
|
||||
|
||||
private val resourceConfig = Config.getDefaultConfig().apply {
|
||||
aaptBinary = aaptBinaryPath
|
||||
frameworkDirectory = frameworkFileDirectory
|
||||
}
|
||||
|
||||
internal var decodingMode = ResourceDecodingMode.MANIFEST
|
||||
|
||||
/**
|
||||
* Read a document from an [InputStream].
|
||||
*/
|
||||
fun document(inputStream: InputStream) = Document(inputStream)
|
||||
|
||||
/**
|
||||
* Read and write documents in the [apkFile].
|
||||
*/
|
||||
fun document(path: String) = Document(get(path))
|
||||
|
||||
/**
|
||||
* Set of resources from [apkFile] to delete.
|
||||
*/
|
||||
private val deleteResources = mutableSetOf<String>()
|
||||
|
||||
internal fun decodeManifest(): Pair<PackageName, VersionName> {
|
||||
logger.info("Decoding manifest")
|
||||
|
||||
val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo)
|
||||
|
||||
// Decode manually instead of using resourceDecoder.decodeManifest
|
||||
// because it does not support decoding to an OutputStream.
|
||||
AndroidManifestPullStreamDecoder(
|
||||
AndroidManifestResourceParser(resourcesDecoder.resTable),
|
||||
resourcesDecoder.newXmlSerializer(),
|
||||
).decode(
|
||||
apkInfo.apkFile.directory.getFileInput("AndroidManifest.xml"),
|
||||
// Older Android versions do not support OutputStream.nullOutputStream()
|
||||
object : OutputStream() {
|
||||
override fun write(b: Int) { // Do nothing.
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Get the package name and version from the manifest using the XmlPullStreamDecoder.
|
||||
// The call to AndroidManifestPullStreamDecoder.decode() above sets apkInfo.
|
||||
val packageName = resourcesDecoder.resTable.packageRenamed
|
||||
val packageVersion =
|
||||
apkInfo.versionInfo.versionName ?: apkInfo.versionInfo.versionCode
|
||||
|
||||
/*
|
||||
When the main resource package is not loaded, the ResTable is flagged as sparse.
|
||||
Because ResourcesDecoder.decodeResources loads the main package and is not called here,
|
||||
set sparseResources to false again to prevent the ResTable from being flagged as sparse falsely,
|
||||
in case ResourcesDecoder.decodeResources is not later used in the patching process
|
||||
to set sparseResources correctly.
|
||||
|
||||
See ARSCDecoder.readTableType for more info.
|
||||
*/
|
||||
apkInfo.sparseResources = false
|
||||
|
||||
return packageName to packageVersion
|
||||
}
|
||||
|
||||
internal fun decodeResources() {
|
||||
logger.info("Decoding resources")
|
||||
|
||||
val resourcesDecoder = ResourcesDecoder(resourceConfig, apkInfo).also {
|
||||
it.decodeResources(apkFilesPath)
|
||||
it.decodeManifest(apkFilesPath)
|
||||
}
|
||||
|
||||
// Record uncompressed files to preserve their state when recompiling.
|
||||
ApkDecoder(apkInfo, resourceConfig).recordUncompressedFiles(resourcesDecoder.resFileMapping)
|
||||
|
||||
// Get the ids of the used framework packages to include them for reference when recompiling.
|
||||
apkInfo.usesFramework = UsesFramework().apply {
|
||||
ids = resourcesDecoder.resTable.listFramePackages().map { it.id }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile resources in [apkFilesPath].
|
||||
*
|
||||
* @return The [PatchesResult.PatchedResources].
|
||||
*/
|
||||
override fun get(): PatchesResult.PatchedResources {
|
||||
logger.info("Compiling patched resources")
|
||||
|
||||
val resourcesPath = patchedFilesPath.resolve("resources").also { it.mkdirs() }
|
||||
|
||||
val resourcesApkFile = if (decodingMode == ResourceDecodingMode.ALL) {
|
||||
val resourcesApkFile = resourcesPath.resolve("resources.apk").also { it.createNewFile() }
|
||||
|
||||
val manifestFile = apkFilesPath.resolve("AndroidManifest.xml").also {
|
||||
ResXmlUtils.fixingPublicAttrsInProviderAttributes(it)
|
||||
}
|
||||
val resPath = apkFilesPath.resolve("res")
|
||||
val frameworkApkFiles = with(Framework(resourceConfig)) {
|
||||
apkInfo.usesFramework.ids.map { id -> getFrameworkApk(id, null) }
|
||||
}.toTypedArray()
|
||||
|
||||
AaptInvoker(
|
||||
resourceConfig,
|
||||
apkInfo
|
||||
).invoke(resourcesApkFile, manifestFile, resPath, null, null, frameworkApkFiles)
|
||||
|
||||
resourcesApkFile
|
||||
} else null
|
||||
|
||||
|
||||
val otherFiles = apkFilesPath.listFiles()!!.filter {
|
||||
// Excluded because present in resources.other.
|
||||
// TODO: We are reusing apkFiles as a temporarily directory for extracting resources.
|
||||
// This is not ideal as it could conflict with files such as the ones that are filtered here.
|
||||
// The problem is that ResourcePatchContext#get returns a File relative to apkFiles,
|
||||
// and we need to extract files to that directory.
|
||||
// A solution would be to use apkFiles as the working directory for the patching process.
|
||||
// Once all patches have been executed, we can move the decoded resources to a new directory.
|
||||
// The filters wouldn't be needed anymore.
|
||||
// For now, we assume that the files we filter here are not needed for the patching process.
|
||||
it.name != "AndroidManifest.xml" &&
|
||||
it.name != "res" &&
|
||||
// Generated by Androlib.
|
||||
it.name != "build"
|
||||
}
|
||||
val otherResourceFiles = if (otherFiles.isNotEmpty()) {
|
||||
// Move the other resources files.
|
||||
resourcesPath.resolve("other").also { it.mkdirs() }.apply {
|
||||
otherFiles.forEach { file ->
|
||||
Files.move(file.toPath(), resolve(file.name).toPath())
|
||||
}
|
||||
}
|
||||
} else null
|
||||
|
||||
return PatchesResult.PatchedResources(
|
||||
resourcesApkFile,
|
||||
otherResourceFiles,
|
||||
apkInfo.doNotCompress?.toSet() ?: emptySet(),
|
||||
deleteResources,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a file from [apkFilesPath].
|
||||
*
|
||||
* @param path The path of the file.
|
||||
* @param copy Whether to copy the file from [apkFile] if it does not exist yet in [apkFilesPath].
|
||||
*/
|
||||
operator fun get(
|
||||
path: String,
|
||||
copy: Boolean = true,
|
||||
) = apkFilesPath.resolve(path).apply {
|
||||
if (copy && !exists()) {
|
||||
with(ExtFile(apkFile).directory) {
|
||||
if (containsFile(path) || containsDir(path)) {
|
||||
copyToDir(apkFilesPath, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a file for deletion when the APK is rebuilt.
|
||||
*
|
||||
* @param name The name of the file to delete.
|
||||
*/
|
||||
fun delete(name: String) = deleteResources.add(name)
|
||||
|
||||
/**
|
||||
* How to handle resources decoding and compiling.
|
||||
*/
|
||||
internal enum class ResourceDecodingMode {
|
||||
/**
|
||||
* Decode and compile all resources.
|
||||
*/
|
||||
ALL,
|
||||
|
||||
/**
|
||||
* Do not decode or compile any resources.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* Do not decode or compile any resources.
|
||||
*/
|
||||
MANIFEST,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableClassDef
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableClassDef.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableField
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.asMutableClass
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.filterAny
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.filterNotAny
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.isPublic
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.toPublic
|
||||
import app.revanced.patcher.util.ClassMerger.Utils.traverseClassHierarchy
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
import java.util.logging.Logger
|
||||
import kotlin.reflect.KFunction2
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
/**
|
||||
* Experimental class to merge a [ClassDef] with another.
|
||||
* Note: This will not consider method implementations or if the class is missing a superclass or interfaces.
|
||||
*/
|
||||
internal object ClassMerger {
|
||||
private val logger = Logger.getLogger(ClassMerger::class.jvmName)
|
||||
|
||||
/**
|
||||
* Merge a class with [otherClass].
|
||||
*
|
||||
* @param otherClass The class to merge with
|
||||
* @param context The context to traverse the class hierarchy in.
|
||||
* @return The merged class or the original class if no merge was needed.
|
||||
*/
|
||||
fun ClassDef.merge(
|
||||
otherClass: ClassDef,
|
||||
context: BytecodePatchContext,
|
||||
) = this
|
||||
// .fixFieldAccess(otherClass)
|
||||
// .fixMethodAccess(otherClass)
|
||||
.addMissingFields(otherClass)
|
||||
.addMissingMethods(otherClass)
|
||||
.publicize(otherClass, context)
|
||||
|
||||
/**
|
||||
* Add methods which are missing but existing in [fromClass].
|
||||
*
|
||||
* @param fromClass The class to add missing methods from.
|
||||
*/
|
||||
private fun ClassDef.addMissingMethods(fromClass: ClassDef): ClassDef {
|
||||
val missingMethods =
|
||||
fromClass.methods.let { fromMethods ->
|
||||
methods.filterNot { method ->
|
||||
fromMethods.any { fromMethod ->
|
||||
MethodUtil.methodSignaturesMatch(fromMethod, method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingMethods.isEmpty()) return this
|
||||
|
||||
logger.fine { "Found ${missingMethods.size} missing methods" }
|
||||
|
||||
return asMutableClass().apply {
|
||||
methods.addAll(missingMethods.map { it.toMutable() })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add fields which are missing but existing in [fromClass].
|
||||
*
|
||||
* @param fromClass The class to add missing fields from.
|
||||
*/
|
||||
private fun ClassDef.addMissingFields(fromClass: ClassDef): ClassDef {
|
||||
val missingFields =
|
||||
fields.filterNotAny(fromClass.fields) { field, fromField ->
|
||||
fromField.name == field.name
|
||||
}
|
||||
|
||||
if (missingFields.isEmpty()) return this
|
||||
|
||||
logger.fine { "Found ${missingFields.size} missing fields" }
|
||||
|
||||
return asMutableClass().apply {
|
||||
fields.addAll(missingFields.map { it.toMutable() })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a class and its super class public recursively.
|
||||
* @param reference The class to check the [AccessFlags] of.
|
||||
* @param context The context to traverse the class hierarchy in.
|
||||
*/
|
||||
private fun ClassDef.publicize(
|
||||
reference: ClassDef,
|
||||
context: BytecodePatchContext,
|
||||
) = if (reference.accessFlags.isPublic() && !accessFlags.isPublic()) {
|
||||
this.asMutableClass().apply {
|
||||
context.traverseClassHierarchy(this) {
|
||||
if (accessFlags.isPublic()) return@traverseClassHierarchy
|
||||
|
||||
logger.fine { "Publicizing ${this.type}" }
|
||||
|
||||
accessFlags = accessFlags.toPublic()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
/**
|
||||
* Publicize fields if they are public in [reference].
|
||||
*
|
||||
* @param reference The class to check the [AccessFlags] of the fields in.
|
||||
*/
|
||||
private fun ClassDef.fixFieldAccess(reference: ClassDef): ClassDef {
|
||||
val brokenFields =
|
||||
fields.filterAny(reference.fields) { field, referenceField ->
|
||||
if (field.name != referenceField.name) return@filterAny false
|
||||
|
||||
referenceField.accessFlags.isPublic() && !field.accessFlags.isPublic()
|
||||
}
|
||||
|
||||
if (brokenFields.isEmpty()) return this
|
||||
|
||||
logger.fine { "Found ${brokenFields.size} broken fields" }
|
||||
|
||||
/**
|
||||
* Make a field public.
|
||||
*/
|
||||
fun MutableField.publicize() {
|
||||
accessFlags = accessFlags.toPublic()
|
||||
}
|
||||
|
||||
return asMutableClass().apply {
|
||||
fields.filter { brokenFields.contains(it) }.forEach(MutableField::publicize)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publicize methods if they are public in [reference].
|
||||
*
|
||||
* @param reference The class to check the [AccessFlags] of the methods in.
|
||||
*/
|
||||
private fun ClassDef.fixMethodAccess(reference: ClassDef): ClassDef {
|
||||
val brokenMethods =
|
||||
methods.filterAny(reference.methods) { method, referenceMethod ->
|
||||
if (!MethodUtil.methodSignaturesMatch(method, referenceMethod)) return@filterAny false
|
||||
|
||||
referenceMethod.accessFlags.isPublic() && !method.accessFlags.isPublic()
|
||||
}
|
||||
|
||||
if (brokenMethods.isEmpty()) return this
|
||||
|
||||
logger.fine { "Found ${brokenMethods.size} methods" }
|
||||
|
||||
/**
|
||||
* Make a method public.
|
||||
*/
|
||||
fun MutableMethod.publicize() {
|
||||
accessFlags = accessFlags.toPublic()
|
||||
}
|
||||
|
||||
return asMutableClass().apply {
|
||||
methods.filter { brokenMethods.contains(it) }.forEach(MutableMethod::publicize)
|
||||
}
|
||||
}
|
||||
|
||||
private object Utils {
|
||||
/**
|
||||
* traverse the class hierarchy starting from the given root class
|
||||
*
|
||||
* @param targetClass the class to start traversing the class hierarchy from
|
||||
* @param callback function that is called for every class in the hierarchy
|
||||
*/
|
||||
fun BytecodePatchContext.traverseClassHierarchy(
|
||||
targetClass: MutableClassDef,
|
||||
callback: MutableClassDef.() -> Unit,
|
||||
) {
|
||||
callback(targetClass)
|
||||
|
||||
classDefs[targetClass.superclass ?: return]?.let { classDef ->
|
||||
traverseClassHierarchy(classDefs.getOrReplaceMutable(classDef), callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun ClassDef.asMutableClass() = this as? MutableClassDef ?: this.toMutable()
|
||||
|
||||
/**
|
||||
* Check if the [AccessFlags.PUBLIC] flag is set.
|
||||
*
|
||||
* @return True, if the flag is set.
|
||||
*/
|
||||
fun Int.isPublic() = AccessFlags.PUBLIC.isSet(this)
|
||||
|
||||
/**
|
||||
* Make [AccessFlags] public.
|
||||
*
|
||||
* @return The new [AccessFlags].
|
||||
*/
|
||||
fun Int.toPublic() = or(AccessFlags.PUBLIC.value).and(AccessFlags.PRIVATE.value.inv())
|
||||
|
||||
/**
|
||||
* Filter [this] on [needles] matching the given [predicate].
|
||||
*
|
||||
* @param needles The needles to filter [this] with.
|
||||
* @param predicate The filter.
|
||||
* @return The [this] filtered on [needles] matching the given [predicate].
|
||||
*/
|
||||
fun <HayType, NeedleType> Iterable<HayType>.filterAny(
|
||||
needles: Iterable<NeedleType>,
|
||||
predicate: (HayType, NeedleType) -> Boolean,
|
||||
) = Iterable<HayType>::filter.any(this, needles, predicate)
|
||||
|
||||
/**
|
||||
* Filter [this] on [needles] not matching the given [predicate].
|
||||
*
|
||||
* @param needles The needles to filter [this] with.
|
||||
* @param predicate The filter.
|
||||
* @return The [this] filtered on [needles] not matching the given [predicate].
|
||||
*/
|
||||
fun <HayType, NeedleType> Iterable<HayType>.filterNotAny(
|
||||
needles: Iterable<NeedleType>,
|
||||
predicate: (HayType, NeedleType) -> Boolean,
|
||||
) = Iterable<HayType>::filterNot.any(this, needles, predicate)
|
||||
|
||||
fun <HayType, NeedleType> KFunction2<Iterable<HayType>, (HayType) -> Boolean, List<HayType>>.any(
|
||||
haystack: Iterable<HayType>,
|
||||
needles: Iterable<NeedleType>,
|
||||
predicate: (HayType, NeedleType) -> Boolean,
|
||||
) = this(haystack) { hay ->
|
||||
needles.any { needle ->
|
||||
predicate(hay, needle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import collections.merge
|
||||
import com.google.common.base.Charsets
|
||||
import org.w3c.dom.Document
|
||||
import java.io.Closeable
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.bufferedWriter
|
||||
import java.io.inputStream
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.OutputKeys
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
import kotlin.use
|
||||
|
||||
class Document internal constructor(
|
||||
inputStream: InputStream,
|
||||
) : Document by DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream), Closeable {
|
||||
private var file: File? = null
|
||||
|
||||
init {
|
||||
normalize()
|
||||
}
|
||||
|
||||
internal constructor(file: File) : this(file.inputStream()) {
|
||||
this.file = file
|
||||
readerCount.merge(file, 1, Int::plus)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
file?.let {
|
||||
if (readerCount[it]!! > 1) {
|
||||
throw IllegalStateException(
|
||||
"Two or more instances are currently reading $it." +
|
||||
"To be able to close this instance, no other instances may be reading $it at the same time.",
|
||||
)
|
||||
} else {
|
||||
readerCount.remove(it)
|
||||
}
|
||||
|
||||
val transformer = TransformerFactory.newInstance().newTransformer()
|
||||
// Set to UTF-16 to prevent surrogate pairs from being escaped to invalid numeric character references, but save as UTF-8.
|
||||
if (isAndroid) {
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-16")
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes")
|
||||
val writer = it.bufferedWriter(charset = Charsets.UTF_8)
|
||||
transformer.transform(DOMSource(this), StreamResult(writer))
|
||||
writer.close()
|
||||
} else {
|
||||
transformer.transform(DOMSource(this), StreamResult(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val readerCount = mutableMapOf<File, Int>()
|
||||
private val isAndroid = System.getProperty("java.runtime.name") == "Android Runtime"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod
|
||||
import app.revanced.patcher.extensions.instructionsOrNull
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.Instruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
* A navigator for methods.
|
||||
*
|
||||
* @param startMethod The [Method] to start navigating from.
|
||||
*
|
||||
* @constructor Creates a new [MethodNavigator].
|
||||
*
|
||||
* @throws NavigateException If the method does not have an implementation.
|
||||
* @throws NavigateException If the instruction at the specified index is not a method reference.
|
||||
*/
|
||||
class MethodNavigator internal constructor(
|
||||
private var startMethod: MethodReference,
|
||||
) {
|
||||
private var lastNavigatedMethodReference = startMethod
|
||||
|
||||
context(_: BytecodePatchContext)
|
||||
private val lastNavigatedMethodInstructions
|
||||
get() = with(original()) {
|
||||
instructionsOrNull ?: throw NavigateException("Method $this does not have an implementation.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the method at the specified index.
|
||||
*
|
||||
* @param index The index of the method to navigate to.
|
||||
*
|
||||
* @return This [MethodNavigator].
|
||||
*/
|
||||
context(_: BytecodePatchContext)
|
||||
fun to(vararg index: Int): MethodNavigator {
|
||||
index.forEach {
|
||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.getMethodReferenceAt(it)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to the method at the specified index that matches the specified predicate.
|
||||
*
|
||||
* @param index The index of the method to navigate to.
|
||||
* @param predicate The predicate to match.
|
||||
*/
|
||||
context(_: BytecodePatchContext)
|
||||
fun to(index: Int = 0, predicate: (Instruction) -> Boolean): MethodNavigator {
|
||||
lastNavigatedMethodReference = lastNavigatedMethodInstructions.asSequence()
|
||||
.filter(predicate).asIterable().getMethodReferenceAt(index)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the method reference at the specified index.
|
||||
*
|
||||
* @param index The index of the method reference to get.
|
||||
*/
|
||||
private fun Iterable<Instruction>.getMethodReferenceAt(index: Int): MethodReference {
|
||||
val instruction = elementAt(index) as? ReferenceInstruction
|
||||
?: throw NavigateException("Instruction at index $index is not a method reference.")
|
||||
|
||||
return instruction.reference as MethodReference
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last navigated method mutably.
|
||||
*
|
||||
* @return The last navigated method mutably.
|
||||
*/
|
||||
context(context: BytecodePatchContext)
|
||||
fun stop() = context.classDefs[lastNavigatedMethodReference.definingClass]!!
|
||||
.firstMethodBySignature as MutableMethod
|
||||
|
||||
|
||||
/**
|
||||
* Get the last navigated method mutably.
|
||||
*
|
||||
* @return The last navigated method mutably.
|
||||
*/
|
||||
operator fun getValue(context: BytecodePatchContext?, property: KProperty<*>) =
|
||||
context(requireNotNull(context)) { stop() }
|
||||
|
||||
/**
|
||||
* Get the last navigated method immutably.
|
||||
*
|
||||
* @return The last navigated method immutably.
|
||||
*/
|
||||
context(context: BytecodePatchContext)
|
||||
fun original(): Method = context.classDefs[lastNavigatedMethodReference.definingClass]!!.firstMethodBySignature
|
||||
|
||||
/**
|
||||
* Find the first [lastNavigatedMethodReference] in the class.
|
||||
*/
|
||||
private val ClassDef.firstMethodBySignature
|
||||
get() = methods.first {
|
||||
MethodUtil.methodSignaturesMatch(it, lastNavigatedMethodReference)
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception thrown when navigating fails.
|
||||
*
|
||||
* @param message The message of the exception.
|
||||
*/
|
||||
internal class NavigateException internal constructor(message: String) : Exception(message)
|
||||
}
|
||||
13
patcher/src/commonMain/kotlin/collections/MutableMap.kt
Normal file
13
patcher/src/commonMain/kotlin/collections/MutableMap.kt
Normal file
@@ -0,0 +1,13 @@
|
||||
package collections
|
||||
|
||||
internal expect fun <K, V> MutableMap<K, V>.kmpMerge(
|
||||
key: K,
|
||||
value: V,
|
||||
remappingFunction: (oldValue: V, newValue: V) -> V,
|
||||
)
|
||||
|
||||
internal fun <K, V> MutableMap<K, V>.merge(
|
||||
key: K,
|
||||
value: V,
|
||||
remappingFunction: (oldValue: V, newValue: V) -> V,
|
||||
) = kmpMerge(key, value, remappingFunction)
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseAnnotationEncodedValue
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
|
||||
|
||||
class MutableAnnotationEncodedValue(annotationEncodedValue: AnnotationEncodedValue) :
|
||||
BaseAnnotationEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var type = annotationEncodedValue.type
|
||||
|
||||
private val _elements by lazy {
|
||||
annotationEncodedValue.elements.map { annotationElement -> annotationElement.toMutable() }.toMutableSet()
|
||||
}
|
||||
|
||||
fun setType(type: String) {
|
||||
this.type = type
|
||||
}
|
||||
|
||||
override fun getType() = this.type
|
||||
|
||||
override fun getElements() = _elements
|
||||
|
||||
companion object {
|
||||
fun AnnotationEncodedValue.toMutable() = MutableAnnotationEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseArrayEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
|
||||
|
||||
class MutableArrayEncodedValue(arrayEncodedValue: ArrayEncodedValue) : BaseArrayEncodedValue(), MutableEncodedValue {
|
||||
private val _value by lazy {
|
||||
arrayEncodedValue.value.map { encodedValue -> encodedValue.toMutable() }.toMutableList()
|
||||
}
|
||||
|
||||
override fun getValue() = _value
|
||||
|
||||
companion object {
|
||||
fun ArrayEncodedValue.toMutable(): MutableArrayEncodedValue = MutableArrayEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseBooleanEncodedValue
|
||||
|
||||
class MutableBooleanEncodedValue(booleanEncodedValue: BooleanEncodedValue) :
|
||||
BaseBooleanEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var value = booleanEncodedValue.value
|
||||
|
||||
fun setValue(value: Boolean) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Boolean = this.value
|
||||
|
||||
companion object {
|
||||
fun BooleanEncodedValue.toMutable(): MutableBooleanEncodedValue = MutableBooleanEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.base.value.BaseByteEncodedValue
|
||||
import org.jf.dexlib2.iface.value.ByteEncodedValue
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseByteEncodedValue
|
||||
|
||||
class MutableByteEncodedValue(byteEncodedValue: ByteEncodedValue) : BaseByteEncodedValue(), MutableEncodedValue {
|
||||
private var value = byteEncodedValue.value
|
||||
|
||||
override fun getValue(): Byte {
|
||||
return this.value
|
||||
}
|
||||
|
||||
fun setValue(value: Byte) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Byte = this.value
|
||||
|
||||
companion object {
|
||||
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue {
|
||||
return MutableByteEncodedValue(this)
|
||||
}
|
||||
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.base.value.BaseCharEncodedValue
|
||||
import org.jf.dexlib2.iface.value.CharEncodedValue
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseCharEncodedValue
|
||||
|
||||
class MutableCharEncodedValue(charEncodedValue: CharEncodedValue) : BaseCharEncodedValue(), MutableEncodedValue {
|
||||
private var value = charEncodedValue.value
|
||||
|
||||
override fun getValue(): Char {
|
||||
return this.value
|
||||
}
|
||||
|
||||
fun setValue(value: Char) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Char = this.value
|
||||
|
||||
companion object {
|
||||
fun CharEncodedValue.toMutable(): MutableCharEncodedValue {
|
||||
return MutableCharEncodedValue(this)
|
||||
}
|
||||
fun CharEncodedValue.toMutable(): MutableCharEncodedValue = MutableCharEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseDoubleEncodedValue
|
||||
|
||||
class MutableDoubleEncodedValue(doubleEncodedValue: DoubleEncodedValue) :
|
||||
BaseDoubleEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var value = doubleEncodedValue.value
|
||||
|
||||
fun setValue(value: Double) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Double = this.value
|
||||
|
||||
companion object {
|
||||
fun DoubleEncodedValue.toMutable(): MutableDoubleEncodedValue = MutableDoubleEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.ValueType
|
||||
import org.jf.dexlib2.iface.value.*
|
||||
import com.android.tools.smali.dexlib2.ValueType
|
||||
|
||||
interface MutableEncodedValue : EncodedValue {
|
||||
companion object {
|
||||
@@ -29,4 +28,4 @@ interface MutableEncodedValue : EncodedValue {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseEnumEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
class MutableEnumEncodedValue(enumEncodedValue: EnumEncodedValue) : BaseEnumEncodedValue(), MutableEncodedValue {
|
||||
private var value = enumEncodedValue.value
|
||||
|
||||
fun setValue(value: FieldReference) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): FieldReference = this.value
|
||||
|
||||
companion object {
|
||||
fun EnumEncodedValue.toMutable(): MutableEnumEncodedValue = MutableEnumEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.ValueType
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseFieldEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
class MutableFieldEncodedValue(fieldEncodedValue: FieldEncodedValue) : BaseFieldEncodedValue(), MutableEncodedValue {
|
||||
private var value = fieldEncodedValue.value
|
||||
|
||||
fun setValue(value: FieldReference) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValueType(): Int = ValueType.FIELD
|
||||
|
||||
override fun getValue(): FieldReference = this.value
|
||||
|
||||
companion object {
|
||||
fun FieldEncodedValue.toMutable(): MutableFieldEncodedValue = MutableFieldEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.base.value.BaseFloatEncodedValue
|
||||
import org.jf.dexlib2.iface.value.FloatEncodedValue
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseFloatEncodedValue
|
||||
|
||||
class MutableFloatEncodedValue(floatEncodedValue: FloatEncodedValue) : BaseFloatEncodedValue(), MutableEncodedValue {
|
||||
private var value = floatEncodedValue.value
|
||||
|
||||
override fun getValue(): Float {
|
||||
return this.value
|
||||
}
|
||||
|
||||
fun setValue(value: Float) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Float = this.value
|
||||
|
||||
companion object {
|
||||
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue {
|
||||
return MutableFloatEncodedValue(this)
|
||||
}
|
||||
fun FloatEncodedValue.toMutable(): MutableFloatEncodedValue = MutableFloatEncodedValue(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.base.value.BaseIntEncodedValue
|
||||
import org.jf.dexlib2.iface.value.IntEncodedValue
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseIntEncodedValue
|
||||
|
||||
class MutableIntEncodedValue(intEncodedValue: IntEncodedValue) : BaseIntEncodedValue(), MutableEncodedValue {
|
||||
private var value = intEncodedValue.value
|
||||
|
||||
override fun getValue(): Int {
|
||||
return this.value
|
||||
}
|
||||
|
||||
fun setValue(value: Int) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Int = this.value
|
||||
|
||||
companion object {
|
||||
fun IntEncodedValue.toMutable(): MutableIntEncodedValue {
|
||||
return MutableIntEncodedValue(this)
|
||||
}
|
||||
fun IntEncodedValue.toMutable(): MutableIntEncodedValue = MutableIntEncodedValue(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.base.value.BaseLongEncodedValue
|
||||
import org.jf.dexlib2.iface.value.LongEncodedValue
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseLongEncodedValue
|
||||
|
||||
class MutableLongEncodedValue(longEncodedValue: LongEncodedValue) : BaseLongEncodedValue(), MutableEncodedValue {
|
||||
private var value = longEncodedValue.value
|
||||
|
||||
override fun getValue(): Long {
|
||||
return this.value
|
||||
}
|
||||
|
||||
fun setValue(value: Long) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Long = this.value
|
||||
|
||||
companion object {
|
||||
fun LongEncodedValue.toMutable(): MutableLongEncodedValue {
|
||||
return MutableLongEncodedValue(this)
|
||||
}
|
||||
fun LongEncodedValue.toMutable(): MutableLongEncodedValue = MutableLongEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseMethodEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
class MutableMethodEncodedValue(methodEncodedValue: MethodEncodedValue) :
|
||||
BaseMethodEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var value = methodEncodedValue.value
|
||||
|
||||
fun setValue(value: MethodReference) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): MethodReference = this.value
|
||||
|
||||
companion object {
|
||||
fun MethodEncodedValue.toMutable(): MutableMethodEncodedValue = MutableMethodEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseMethodHandleEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodHandleReference
|
||||
|
||||
class MutableMethodHandleEncodedValue(methodHandleEncodedValue: MethodHandleEncodedValue) :
|
||||
BaseMethodHandleEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var value = methodHandleEncodedValue.value
|
||||
|
||||
fun setValue(value: MethodHandleReference) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): MethodHandleReference = this.value
|
||||
|
||||
companion object {
|
||||
fun MethodHandleEncodedValue.toMutable(): MutableMethodHandleEncodedValue = MutableMethodHandleEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseMethodTypeEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodProtoReference
|
||||
|
||||
class MutableMethodTypeEncodedValue(methodTypeEncodedValue: MethodTypeEncodedValue) : BaseMethodTypeEncodedValue(), MutableEncodedValue {
|
||||
private var value = methodTypeEncodedValue.value
|
||||
|
||||
fun setValue(value: MethodProtoReference) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): MethodProtoReference = this.value
|
||||
|
||||
companion object {
|
||||
fun MethodTypeEncodedValue.toMutable(): MutableMethodTypeEncodedValue = MutableMethodTypeEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseNullEncodedValue
|
||||
|
||||
class MutableNullEncodedValue : BaseNullEncodedValue(), MutableEncodedValue {
|
||||
companion object {
|
||||
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseShortEncodedValue
|
||||
|
||||
class MutableShortEncodedValue(shortEncodedValue: ShortEncodedValue) :
|
||||
BaseShortEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var value = shortEncodedValue.value
|
||||
|
||||
fun setValue(value: Short) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): Short = this.value
|
||||
|
||||
companion object {
|
||||
fun ShortEncodedValue.toMutable(): MutableShortEncodedValue = MutableShortEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseStringEncodedValue
|
||||
|
||||
class MutableStringEncodedValue(stringEncodedValue: StringEncodedValue) :
|
||||
BaseStringEncodedValue(),
|
||||
MutableEncodedValue {
|
||||
private var value = stringEncodedValue.value
|
||||
|
||||
fun setValue(value: String) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue(): String = this.value
|
||||
|
||||
companion object {
|
||||
fun ByteEncodedValue.toMutable(): MutableByteEncodedValue = MutableByteEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package app.revanced.patcher.util.proxy.mutableTypes.encodedValue
|
||||
package com.android.tools.smali.dexlib2.iface.value
|
||||
|
||||
import org.jf.dexlib2.base.value.BaseTypeEncodedValue
|
||||
import org.jf.dexlib2.iface.value.TypeEncodedValue
|
||||
import com.android.tools.smali.dexlib2.base.value.BaseTypeEncodedValue
|
||||
|
||||
class MutableTypeEncodedValue(typeEncodedValue: TypeEncodedValue) : BaseTypeEncodedValue(), MutableEncodedValue {
|
||||
private var value = typeEncodedValue.value
|
||||
|
||||
override fun getValue(): String {
|
||||
return this.value
|
||||
}
|
||||
|
||||
fun setValue(value: String) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getValue() = this.value
|
||||
|
||||
companion object {
|
||||
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue {
|
||||
return MutableTypeEncodedValue(this)
|
||||
}
|
||||
fun TypeEncodedValue.toMutable(): MutableTypeEncodedValue = MutableTypeEncodedValue(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.android.tools.smali.dexlib2.mutable
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.BaseAnnotation
|
||||
import com.android.tools.smali.dexlib2.iface.Annotation
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableAnnotationElement.Companion.toMutable
|
||||
|
||||
class MutableAnnotation(annotation: Annotation) : BaseAnnotation() {
|
||||
private val visibility = annotation.visibility
|
||||
private val type = annotation.type
|
||||
private val _elements by lazy { annotation.elements.map { element -> element.toMutable() }.toMutableSet() }
|
||||
|
||||
override fun getType() = type
|
||||
|
||||
override fun getElements() = _elements
|
||||
|
||||
override fun getVisibility() = visibility
|
||||
|
||||
companion object {
|
||||
fun Annotation.toMutable() = MutableAnnotation(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.android.tools.smali.dexlib2.mutable
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.BaseAnnotationElement
|
||||
import com.android.tools.smali.dexlib2.iface.AnnotationElement
|
||||
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
|
||||
|
||||
class MutableAnnotationElement(annotationElement: AnnotationElement) : BaseAnnotationElement() {
|
||||
private var name = annotationElement.name
|
||||
private var value = annotationElement.value.toMutable()
|
||||
|
||||
fun setName(name: String) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun setValue(value: MutableEncodedValue) {
|
||||
this.value = value
|
||||
}
|
||||
|
||||
override fun getName() = name
|
||||
|
||||
override fun getValue() = value
|
||||
|
||||
companion object {
|
||||
fun AnnotationElement.toMutable() = MutableAnnotationElement(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.android.tools.smali.dexlib2.mutable
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.reference.BaseTypeReference
|
||||
import com.android.tools.smali.dexlib2.iface.ClassDef
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableField.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.util.FieldUtil
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
class MutableClassDef(classDef: ClassDef) : BaseTypeReference(), ClassDef {
|
||||
// Class
|
||||
private var type = classDef.type
|
||||
private var sourceFile = classDef.sourceFile
|
||||
private var accessFlags = classDef.accessFlags
|
||||
private var superclass = classDef.superclass
|
||||
|
||||
private val _interfaces by lazy { classDef.interfaces.toMutableList() }
|
||||
private val _annotations by lazy {
|
||||
classDef.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
|
||||
}
|
||||
|
||||
// Methods
|
||||
private val _methods by lazy { classDef.methods.map { method -> method.toMutable() }.toMutableSet() }
|
||||
private val _directMethods by lazy { methods.filter { method -> MethodUtil.isDirect(method) }.toMutableSet() }
|
||||
private val _virtualMethods by lazy { methods.filter { method -> !MethodUtil.isDirect(method) }.toMutableSet() }
|
||||
|
||||
// Fields
|
||||
private val _fields by lazy { classDef.fields.map { field -> field.toMutable() }.toMutableSet() }
|
||||
private val _staticFields by lazy { _fields.filter { field -> FieldUtil.isStatic(field) }.toMutableSet() }
|
||||
private val _instanceFields by lazy { _fields.filter { field -> !FieldUtil.isStatic(field) }.toMutableSet() }
|
||||
|
||||
fun setType(type: String) {
|
||||
this.type = type
|
||||
}
|
||||
|
||||
fun setSourceFile(sourceFile: String?) {
|
||||
this.sourceFile = sourceFile
|
||||
}
|
||||
|
||||
fun setAccessFlags(accessFlags: Int) {
|
||||
this.accessFlags = accessFlags
|
||||
}
|
||||
|
||||
fun setSuperClass(superclass: String?) {
|
||||
this.superclass = superclass
|
||||
}
|
||||
|
||||
override fun getType() = type
|
||||
|
||||
override fun getAccessFlags() = accessFlags
|
||||
|
||||
override fun getSourceFile() = sourceFile
|
||||
|
||||
override fun getSuperclass() = superclass
|
||||
|
||||
override fun getInterfaces() = _interfaces
|
||||
|
||||
override fun getAnnotations() = _annotations
|
||||
|
||||
override fun getStaticFields() = _staticFields
|
||||
|
||||
override fun getInstanceFields() = _instanceFields
|
||||
|
||||
override fun getFields() = _fields
|
||||
|
||||
override fun getDirectMethods() = _directMethods
|
||||
|
||||
override fun getVirtualMethods() = _virtualMethods
|
||||
|
||||
override fun getMethods() = _methods
|
||||
|
||||
companion object {
|
||||
fun ClassDef.toMutable(): MutableClassDef = MutableClassDef(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.android.tools.smali.dexlib2.mutable
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.reference.BaseFieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.Field
|
||||
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue
|
||||
import com.android.tools.smali.dexlib2.iface.value.MutableEncodedValue.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
|
||||
|
||||
class MutableField(field: Field) : BaseFieldReference(), Field {
|
||||
private var definingClass = field.definingClass
|
||||
private var name = field.name
|
||||
private var type = field.type
|
||||
private var accessFlags = field.accessFlags
|
||||
|
||||
private var initialValue = field.initialValue?.toMutable()
|
||||
private val _annotations by lazy { field.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
|
||||
private val _hiddenApiRestrictions by lazy { field.hiddenApiRestrictions }
|
||||
|
||||
fun setDefiningClass(definingClass: String) {
|
||||
this.definingClass = definingClass
|
||||
}
|
||||
|
||||
fun setName(name: String) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun setType(type: String) {
|
||||
this.type = type
|
||||
}
|
||||
|
||||
fun setAccessFlags(accessFlags: Int) {
|
||||
this.accessFlags = accessFlags
|
||||
}
|
||||
|
||||
fun setInitialValue(initialValue: MutableEncodedValue?) {
|
||||
this.initialValue = initialValue
|
||||
}
|
||||
|
||||
override fun getDefiningClass() = this.definingClass
|
||||
|
||||
override fun getName() = this.name
|
||||
|
||||
override fun getType() = this.type
|
||||
|
||||
override fun getAnnotations() = this._annotations
|
||||
|
||||
override fun getAccessFlags() = this.accessFlags
|
||||
|
||||
override fun getHiddenApiRestrictions() = this._hiddenApiRestrictions
|
||||
|
||||
override fun getInitialValue() = this.initialValue
|
||||
|
||||
companion object {
|
||||
fun Field.toMutable() = MutableField(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.android.tools.smali.dexlib2.mutable
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.reference.BaseMethodReference
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethodParameter.Companion.toMutable
|
||||
|
||||
class MutableMethod(method: Method) : BaseMethodReference(), Method {
|
||||
private var definingClass = method.definingClass
|
||||
private var name = method.name
|
||||
private var accessFlags = method.accessFlags
|
||||
private var returnType = method.returnType
|
||||
|
||||
// TODO: Create own mutable MethodImplementation (due to not being able to change members like register count).
|
||||
private var implementation = method.implementation?.let(::MutableMethodImplementation)
|
||||
private val _annotations by lazy { method.annotations.map { annotation -> annotation.toMutable() }.toMutableSet() }
|
||||
private val _parameters by lazy { method.parameters.map { parameter -> parameter.toMutable() }.toMutableList() }
|
||||
private val _parameterTypes by lazy { method.parameterTypes.toMutableList() }
|
||||
private val _hiddenApiRestrictions by lazy { method.hiddenApiRestrictions }
|
||||
|
||||
fun setDefiningClass(definingClass: String) {
|
||||
this.definingClass = definingClass
|
||||
}
|
||||
|
||||
fun setName(name: String) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
fun setAccessFlags(accessFlags: Int) {
|
||||
this.accessFlags = accessFlags
|
||||
}
|
||||
|
||||
fun setReturnType(returnType: String) {
|
||||
this.returnType = returnType
|
||||
}
|
||||
|
||||
fun setImplementation(implementation: MutableMethodImplementation?) {
|
||||
this.implementation = implementation
|
||||
}
|
||||
|
||||
override fun getDefiningClass() = definingClass
|
||||
|
||||
override fun getName() = name
|
||||
|
||||
override fun getParameterTypes() = _parameterTypes
|
||||
|
||||
override fun getReturnType() = returnType
|
||||
|
||||
override fun getAnnotations() = _annotations
|
||||
|
||||
override fun getAccessFlags() = accessFlags
|
||||
|
||||
override fun getHiddenApiRestrictions() = _hiddenApiRestrictions
|
||||
|
||||
override fun getParameters() = _parameters
|
||||
|
||||
override fun getImplementation() = implementation
|
||||
|
||||
companion object {
|
||||
fun Method.toMutable() = MutableMethod(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.android.tools.smali.dexlib2.mutable
|
||||
|
||||
import com.android.tools.smali.dexlib2.base.BaseMethodParameter
|
||||
import com.android.tools.smali.dexlib2.iface.MethodParameter
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableAnnotation.Companion.toMutable
|
||||
|
||||
class MutableMethodParameter(parameter: MethodParameter) : BaseMethodParameter(), MethodParameter {
|
||||
private var type = parameter.type
|
||||
private var name = parameter.name
|
||||
private var signature = parameter.signature
|
||||
private val _annotations by lazy {
|
||||
parameter.annotations.map { annotation -> annotation.toMutable() }.toMutableSet()
|
||||
}
|
||||
|
||||
override fun getType() = type
|
||||
|
||||
override fun getName() = name
|
||||
|
||||
override fun getSignature() = signature
|
||||
|
||||
override fun getAnnotations() = _annotations
|
||||
|
||||
companion object {
|
||||
fun MethodParameter.toMutable() = MutableMethodParameter(this)
|
||||
}
|
||||
}
|
||||
17
patcher/src/commonMain/kotlin/java/io/File.kt
Normal file
17
patcher/src/commonMain/kotlin/java/io/File.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package java.io
|
||||
|
||||
internal expect fun File.kmpResolve(child: String): File
|
||||
|
||||
internal fun File.resolve(child: String) = kmpResolve(child)
|
||||
|
||||
internal expect fun File.kmpDeleteRecursively(): Boolean
|
||||
|
||||
internal fun File.deleteRecursively() = kmpDeleteRecursively()
|
||||
|
||||
internal expect fun File.kmpInputStream(): InputStream
|
||||
|
||||
internal fun File.inputStream() = kmpInputStream()
|
||||
|
||||
internal expect fun File.kmpBufferedWriter(charset: java.nio.charset.Charset): BufferedWriter
|
||||
|
||||
internal fun File.bufferedWriter(charset: java.nio.charset.Charset) = kmpBufferedWriter(charset)
|
||||
@@ -0,0 +1,32 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import java.io.File
|
||||
import java.net.URLClassLoader
|
||||
import java.util.jar.JarFile
|
||||
|
||||
actual val Class<*>.isPatch get() = Patch::class.java.isAssignableFrom(this)
|
||||
|
||||
/**
|
||||
* Loads patches from JAR files declared as public static fields
|
||||
* or returned by public static and non-parametrized methods.
|
||||
* Patches with no name are not loaded. If a patches file fails to load,
|
||||
* the [onFailedToLoad] callback is invoked with the file and the throwable
|
||||
* and the loading continues for the other files.
|
||||
*
|
||||
* @param patchesFiles The JAR files to load the patches from.
|
||||
* @param onFailedToLoad A callback invoked when a patches file fails to load.
|
||||
*
|
||||
* @return The loaded patches.
|
||||
*/
|
||||
actual fun loadPatches(
|
||||
vararg patchesFiles: File,
|
||||
onFailedToLoad: (patchesFile: File, throwable: Throwable) -> Unit,
|
||||
) = loadPatches(
|
||||
patchesFiles = patchesFiles,
|
||||
{ file ->
|
||||
JarFile(file).entries().toList().filter { it.name.endsWith(".class") }
|
||||
.map { it.name.substringBeforeLast('.').replace('/', '.') }
|
||||
},
|
||||
URLClassLoader(patchesFiles.map { it.toURI().toURL() }.toTypedArray()),
|
||||
onFailedToLoad = onFailedToLoad
|
||||
)
|
||||
7
patcher/src/jvmMain/kotlin/collections/MutableMap.jvm.kt
Normal file
7
patcher/src/jvmMain/kotlin/collections/MutableMap.jvm.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
package collections
|
||||
|
||||
internal actual fun <K, V> MutableMap<K, V>.kmpMerge(
|
||||
key: K,
|
||||
value: V,
|
||||
remappingFunction: (oldValue: V, newValue: V) -> V
|
||||
) = merge(key, value, remappingFunction)
|
||||
9
patcher/src/jvmMain/kotlin/java/io/File.jvm.kt
Normal file
9
patcher/src/jvmMain/kotlin/java/io/File.jvm.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package java.io
|
||||
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
internal actual fun File.kmpResolve(child: String) = resolve(child)
|
||||
internal actual fun File.kmpDeleteRecursively() = deleteRecursively()
|
||||
internal actual fun File.kmpInputStream() = inputStream()
|
||||
internal actual fun File.kmpBufferedWriter(charset: Charset) = bufferedWriter(charset)
|
||||
@@ -0,0 +1,27 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class FingerprintTest : PatcherTestBase() {
|
||||
@BeforeAll
|
||||
fun setup() = setupMock()
|
||||
|
||||
@Test
|
||||
fun `matches fingerprints correctly`() {
|
||||
with(bytecodePatchContext) {
|
||||
assertNotNull(
|
||||
fingerprint { returns("V") }.originalMethodOrNull,
|
||||
"Fingerprints should match correctly."
|
||||
)
|
||||
assertNull(
|
||||
fingerprint { returns("does not exist") }.originalMethodOrNull,
|
||||
"Fingerprints should match correctly."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
165
patcher/src/jvmTest/kotlin/app/revanced/patcher/MatchingTest.kt
Normal file
165
patcher/src/jvmTest/kotlin/app/revanced/patcher/MatchingTest.kt
Normal file
@@ -0,0 +1,165 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethod
|
||||
import app.revanced.patcher.BytecodePatchContextMethodMatching.firstMethodDeclarativelyOrNull
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22t
|
||||
import org.junit.jupiter.api.Assertions
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class MatchingTest : PatcherTestBase() {
|
||||
@BeforeAll
|
||||
fun setup() = setupMock()
|
||||
|
||||
@Test
|
||||
fun `finds via builder api`() {
|
||||
fun firstMethodComposite(fail: Boolean = false) = firstMethodComposite {
|
||||
name("method")
|
||||
definingClass("class")
|
||||
|
||||
if (fail) returnType("doesnt exist")
|
||||
|
||||
instructions(
|
||||
at(Opcode.CONST_STRING()),
|
||||
`is`<TwoRegisterInstruction>(),
|
||||
noneOf(registers()),
|
||||
string("test", String::contains),
|
||||
after(1..3, allOf(Opcode.INVOKE_VIRTUAL(), registers(1, 0))),
|
||||
allOf(),
|
||||
type("PrintStream;", String::endsWith)
|
||||
)
|
||||
}
|
||||
|
||||
with(bytecodePatchContext) {
|
||||
assertNotNull(firstMethodComposite().methodOrNull) { "Expected to find a method" }
|
||||
Assertions.assertNull(firstMethodComposite(fail = true).immutableMethodOrNull) { "Expected to not find a method" }
|
||||
Assertions.assertNotNull(
|
||||
firstMethodComposite().match(classDefs.first()).methodOrNull
|
||||
) { "Expected to find a method matching in a specific class" }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `finds via declarative api`() {
|
||||
bytecodePatch {
|
||||
apply {
|
||||
val method = firstMethodDeclarativelyOrNull {
|
||||
anyOf {
|
||||
predicate { name == "method" }
|
||||
add { false }
|
||||
}
|
||||
allOf {
|
||||
predicate { returnType == "V" }
|
||||
}
|
||||
predicate { definingClass == "class" }
|
||||
}
|
||||
assertNotNull(method) { "Expected to find a method" }
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `predicate api works correctly`() {
|
||||
bytecodePatch {
|
||||
apply {
|
||||
assertDoesNotThrow("Should find method") { firstMethod { name == "method" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `matcher finds indices correctly`() {
|
||||
val iterable = (1..10).toList()
|
||||
val matcher = indexedMatcher<Int>()
|
||||
|
||||
matcher.apply {
|
||||
+at<Int> { this > 5 }
|
||||
}
|
||||
assertFalse(
|
||||
matcher(iterable),
|
||||
"Should not match at any other index than first"
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply { +at<Int> { this == 1 } }(iterable)
|
||||
assertEquals(
|
||||
listOf(0),
|
||||
matcher.indices,
|
||||
"Should match at first index."
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply { add { _, _, _ -> this > 0 } }(iterable)
|
||||
assertEquals(1, matcher.indices.size, "Should only match once.")
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply { add { _, _, _ -> this == 2 } }(iterable)
|
||||
assertEquals(
|
||||
listOf(1),
|
||||
matcher.indices,
|
||||
"Should find the index correctly."
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply {
|
||||
+at<Int> { this == 1 }
|
||||
add { _, _, _ -> this == 2 }
|
||||
add { _, _, _ -> this == 4 }
|
||||
}(iterable)
|
||||
assertEquals(
|
||||
listOf(0, 1, 3),
|
||||
matcher.indices,
|
||||
"Should match 1, 2 and 4 at indices 0, 1 and 3."
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply {
|
||||
+after<Int> { this == 1 }
|
||||
}(iterable)
|
||||
assertEquals(
|
||||
listOf(0),
|
||||
matcher.indices,
|
||||
"Should match index 0 after nothing"
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply {
|
||||
+after<Int>(2..Int.MAX_VALUE) { this == 1 }
|
||||
}
|
||||
assertFalse(
|
||||
matcher(iterable),
|
||||
"Should not match, because 1 is out of range"
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply {
|
||||
+after<Int>(1..1) { this == 2 }
|
||||
}
|
||||
assertFalse(
|
||||
matcher(iterable),
|
||||
"Should not match, because 2 is at index 1"
|
||||
)
|
||||
matcher.clear()
|
||||
|
||||
matcher.apply {
|
||||
+at<Int> { this == 1 }
|
||||
+after<Int>(2..5) { this == 4 }
|
||||
add { _, _, _ -> this == 8 }
|
||||
add { _, _, _ -> this == 9 }
|
||||
}(iterable)
|
||||
assertEquals(
|
||||
listOf(0, 3, 7, 8),
|
||||
matcher.indices,
|
||||
"Should match indices correctly."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class PatcherTest : PatcherTestBase() {
|
||||
@BeforeAll
|
||||
fun setup() = setupMock()
|
||||
|
||||
@Test
|
||||
fun `applies patches in correct order`() {
|
||||
val applied = mutableListOf<String>()
|
||||
|
||||
infix fun Patch.resultsIn(equals: List<String>) = this to equals
|
||||
infix fun Pair<Patch, List<String>>.because(reason: String) {
|
||||
runCatching { setOf(first)() }
|
||||
|
||||
assertEquals(second, applied, reason)
|
||||
|
||||
applied.clear()
|
||||
}
|
||||
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
apply { applied += "1" }
|
||||
afterDependents { applied += "-2" }
|
||||
},
|
||||
bytecodePatch { apply { applied += "2" } },
|
||||
)
|
||||
apply { applied += "3" }
|
||||
afterDependents { applied += "-1" }
|
||||
} resultsIn listOf("1", "2", "3", "-1", "-2") because
|
||||
"Patches should apply in post-order and afterDependents in pre-order."
|
||||
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
apply { throw PatchException("1") }
|
||||
afterDependents { applied += "-2" }
|
||||
},
|
||||
)
|
||||
apply { applied += "2" }
|
||||
afterDependents { applied += "-1" }
|
||||
} resultsIn emptyList() because
|
||||
"Patches that depend on a patched that failed to apply should not be applied."
|
||||
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
apply { applied += "1" }
|
||||
afterDependents { applied += "-2" }
|
||||
},
|
||||
)
|
||||
apply { throw PatchException("2") }
|
||||
afterDependents { applied += "-1" }
|
||||
} resultsIn listOf("1", "-2") because
|
||||
"afterDependents of a patch should not be called if it failed to apply."
|
||||
|
||||
bytecodePatch {
|
||||
dependsOn(
|
||||
bytecodePatch {
|
||||
apply { applied += "1" }
|
||||
afterDependents { applied += "-2" }
|
||||
},
|
||||
)
|
||||
apply { applied += "2" }
|
||||
afterDependents { throw PatchException("-1") }
|
||||
} resultsIn listOf("1", "2", "-2") because
|
||||
"afterDependents of a patch should be called " +
|
||||
"regardless of dependant patches failing."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.extensions.toInstructions
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.ResourcePatchContext
|
||||
import com.android.tools.smali.dexlib2.Opcodes
|
||||
import com.android.tools.smali.dexlib2.iface.DexFile
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableClassDef
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class PatcherTestBase {
|
||||
protected lateinit var bytecodePatchContext: BytecodePatchContext
|
||||
protected lateinit var resourcePatchContext: ResourcePatchContext
|
||||
|
||||
protected fun setupMock(
|
||||
method: ImmutableMethod = ImmutableMethod(
|
||||
"class",
|
||||
"method",
|
||||
emptyList(),
|
||||
"V",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
ImmutableMethodImplementation(
|
||||
2,
|
||||
"""
|
||||
const-string v0, "Hello, World!"
|
||||
iput-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||
iget-object v0, p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
|
||||
return-void
|
||||
const-string v0, "This is a test."
|
||||
return-object v0
|
||||
invoke-virtual { p0, v0 }, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
|
||||
invoke-static { p0 }, Ljava/lang/System;->currentTimeMillis()J
|
||||
check-cast p0, Ljava/io/PrintStream;
|
||||
""".toInstructions(),
|
||||
null,
|
||||
null
|
||||
),
|
||||
),
|
||||
) {
|
||||
resourcePatchContext = mockk<ResourcePatchContext>(relaxed = true)
|
||||
bytecodePatchContext = mockk<BytecodePatchContext> bytecodePatchContext@{
|
||||
mockkStatic(MultiDexIO::readDexFile)
|
||||
every {
|
||||
MultiDexIO.readDexFile(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
} returns mockk<DexFile> {
|
||||
every { classes } returns mutableSetOf(
|
||||
ImmutableClassDef(
|
||||
"class",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
listOf(method),
|
||||
)
|
||||
)
|
||||
every { opcodes } returns Opcodes.getDefault()
|
||||
}
|
||||
|
||||
every { this@bytecodePatchContext.getProperty("apkFile") } returns mockk<File>()
|
||||
|
||||
every { this@bytecodePatchContext.classDefs } returns ClassDefs().apply {
|
||||
javaClass.getDeclaredMethod($$"initializeCache$patcher").apply {
|
||||
isAccessible = true
|
||||
}.invoke(this)
|
||||
}
|
||||
|
||||
every { get() } returns emptySet()
|
||||
|
||||
justRun { this@bytecodePatchContext["extendWith"](any<InputStream>()) }
|
||||
}
|
||||
}
|
||||
|
||||
protected operator fun Set<Patch>.invoke() {
|
||||
runCatching {
|
||||
apply(
|
||||
bytecodePatchContext,
|
||||
resourcePatchContext
|
||||
) { }
|
||||
}.fold(
|
||||
{ it.dexFiles },
|
||||
{ it.printStackTrace() }
|
||||
)
|
||||
}
|
||||
|
||||
protected operator fun Patch.invoke() = setOf(this)()
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package app.revanced.patcher.extensions
|
||||
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21s
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal class MethodExtensionsTest {
|
||||
private val testInstructions = (0..9).map { i -> TestInstruction(i) }
|
||||
private var method = ImmutableMethod(
|
||||
"TestClass;",
|
||||
"testMethod",
|
||||
null,
|
||||
"V",
|
||||
AccessFlags.PUBLIC.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(16)
|
||||
).toMutable()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
method.instructions.clear()
|
||||
method.addInstructions(testInstructions)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addInstructionsToImplementationIndexed() =
|
||||
applyToImplementation {
|
||||
addInstructions(5, getTestInstructions(5..6)).also {
|
||||
assertRegisterIs(5, 5)
|
||||
assertRegisterIs(6, 6)
|
||||
|
||||
assertRegisterIs(5, 7)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addInstructionsToImplementation() =
|
||||
applyToImplementation {
|
||||
addInstructions(getTestInstructions(10..11)).also {
|
||||
assertRegisterIs(10, 10)
|
||||
assertRegisterIs(11, 11)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeInstructionsFromImplementationIndexed() =
|
||||
applyToImplementation {
|
||||
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeInstructionsFromImplementation() =
|
||||
applyToImplementation {
|
||||
removeInstructions(0).also { assertRegisterIs(9, 9) }
|
||||
removeInstructions(1).also { assertRegisterIs(1, 0) }
|
||||
removeInstructions(2).also { assertRegisterIs(3, 0) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceInstructionsInImplementationIndexed() =
|
||||
applyToImplementation {
|
||||
replaceInstructions(5, getTestInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 5)
|
||||
assertRegisterIs(1, 6)
|
||||
assertRegisterIs(7, 7)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addInstructionToMethodIndexed() =
|
||||
applyToMethod {
|
||||
addInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addInstructionToMethod() =
|
||||
applyToMethod {
|
||||
addInstruction(TestInstruction(0)).also { assertRegisterIs(0, 10) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addSmaliInstructionToMethodIndexed() =
|
||||
applyToMethod {
|
||||
addInstruction(5, getTestSmaliInstruction(0)).also { assertRegisterIs(0, 5) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addSmaliInstructionToMethod() =
|
||||
applyToMethod {
|
||||
addInstruction(getTestSmaliInstruction(0)).also { assertRegisterIs(0, 10) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addInstructionsToMethodIndexed() =
|
||||
applyToMethod {
|
||||
addInstructions(5, getTestInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 5)
|
||||
assertRegisterIs(1, 6)
|
||||
|
||||
assertRegisterIs(5, 7)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addInstructionsToMethod() =
|
||||
applyToMethod {
|
||||
addInstructions(getTestInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 10)
|
||||
assertRegisterIs(1, 11)
|
||||
|
||||
assertRegisterIs(9, 9)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addSmaliInstructionsToMethodIndexed() =
|
||||
applyToMethod {
|
||||
addInstructionsWithLabels(5, getTestSmaliInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 5)
|
||||
assertRegisterIs(1, 6)
|
||||
|
||||
assertRegisterIs(5, 7)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addSmaliInstructionsToMethod() =
|
||||
applyToMethod {
|
||||
addInstructions(getTestSmaliInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 10)
|
||||
assertRegisterIs(1, 11)
|
||||
|
||||
assertRegisterIs(9, 9)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addSmaliInstructionsWithExternalLabelToMethodIndexed() =
|
||||
applyToMethod {
|
||||
val label = ExternalLabel("testLabel", getInstruction(5))
|
||||
|
||||
addInstructionsWithLabels(
|
||||
5,
|
||||
getTestSmaliInstructions(0..1).plus("\n").plus("goto :${label.name}"),
|
||||
label,
|
||||
).also {
|
||||
assertRegisterIs(0, 5)
|
||||
assertRegisterIs(1, 6)
|
||||
assertRegisterIs(5, 8)
|
||||
|
||||
val gotoTarget =
|
||||
getInstruction<BuilderOffsetInstruction>(7)
|
||||
.target.location.instruction as OneRegisterInstruction
|
||||
|
||||
assertEquals(5, gotoTarget.registerA)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeInstructionFromMethodIndexed() =
|
||||
applyToMethod {
|
||||
removeInstruction(5).also {
|
||||
assertRegisterIs(4, 4)
|
||||
assertRegisterIs(6, 5)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeInstructionsFromMethodIndexed() =
|
||||
applyToMethod {
|
||||
removeInstructions(5, 5).also { assertRegisterIs(4, 4) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeInstructionsFromMethod() =
|
||||
applyToMethod {
|
||||
removeInstructions(0).also { assertRegisterIs(9, 9) }
|
||||
removeInstructions(1).also { assertRegisterIs(1, 0) }
|
||||
removeInstructions(2).also { assertRegisterIs(3, 0) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceInstructionInMethodIndexed() =
|
||||
applyToMethod {
|
||||
replaceInstruction(5, TestInstruction(0)).also { assertRegisterIs(0, 5) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceInstructionsInMethodIndexed() =
|
||||
applyToMethod {
|
||||
replaceInstructions(5, getTestInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 5)
|
||||
assertRegisterIs(1, 6)
|
||||
assertRegisterIs(7, 7)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceSmaliInstructionsInMethodIndexed() =
|
||||
applyToMethod {
|
||||
replaceInstructions(5, getTestSmaliInstructions(0..1)).also {
|
||||
assertRegisterIs(0, 5)
|
||||
assertRegisterIs(1, 6)
|
||||
assertRegisterIs(7, 7)
|
||||
}
|
||||
}
|
||||
|
||||
// region Helper methods
|
||||
|
||||
private fun applyToImplementation(block: MutableMethodImplementation.() -> Unit) {
|
||||
method.implementation!!.apply(block)
|
||||
}
|
||||
|
||||
private fun applyToMethod(block: MutableMethod.() -> Unit) {
|
||||
method.apply(block)
|
||||
}
|
||||
|
||||
private fun MutableMethodImplementation.assertRegisterIs(
|
||||
register: Int,
|
||||
atIndex: Int,
|
||||
) = assertEquals(
|
||||
register,
|
||||
getInstruction<OneRegisterInstruction>(atIndex).registerA,
|
||||
)
|
||||
|
||||
private fun MutableMethod.assertRegisterIs(
|
||||
register: Int,
|
||||
atIndex: Int,
|
||||
) = implementation!!.assertRegisterIs(register, atIndex)
|
||||
|
||||
private fun getTestInstructions(range: IntRange) = range.map { TestInstruction(it) }
|
||||
|
||||
private fun getTestSmaliInstruction(register: Int) = "const/16 v$register, 0"
|
||||
|
||||
private fun getTestSmaliInstructions(range: IntRange) =
|
||||
range.joinToString("\n") {
|
||||
getTestSmaliInstruction(it)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
private class TestInstruction(register: Int) : BuilderInstruction21s(Opcode.CONST_16, register, 0)
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import org.junit.jupiter.api.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import kotlin.reflect.typeOf
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal object OptionsTest {
|
||||
private val externalOption = stringOption("external", "default")
|
||||
|
||||
private val optionsTestPatch = bytecodePatch {
|
||||
externalOption()
|
||||
|
||||
booleanOption("bool", true)
|
||||
|
||||
stringOption("required", "default", required = true)
|
||||
|
||||
stringsOption("list", listOf("1", "2"))
|
||||
|
||||
stringOption("choices", "value", values = mapOf("Valid option value" to "valid"))
|
||||
|
||||
stringOption("validated", "default") { it == "valid" }
|
||||
|
||||
stringOption("resettable", null, required = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not fail because default value is unvalidated`() = options {
|
||||
assertDoesNotThrow { get("required") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not allow setting custom value with validation`() = options {
|
||||
// Getter validation on incorrect value.
|
||||
assertThrows<OptionException.ValueValidationException> {
|
||||
set("validated", get("validated"))
|
||||
}
|
||||
|
||||
// Setter validation on incorrect value.
|
||||
assertThrows<OptionException.ValueValidationException> {
|
||||
set("validated", "invalid")
|
||||
}
|
||||
|
||||
// Setter validation on correct value.
|
||||
assertDoesNotThrow {
|
||||
set("validated", "valid")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw due to incorrect type`() = options {
|
||||
assertThrows<OptionException.InvalidValueTypeException> {
|
||||
set("bool", "not a boolean")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should be nullable`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("bool", null)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option should not be found`() = options {
|
||||
assertThrows<OptionException.OptionNotFoundException> {
|
||||
set("this option does not exist", 1)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should be able to add options manually`() = options {
|
||||
assertDoesNotThrow {
|
||||
bytecodePatch {
|
||||
get("list")()
|
||||
}.options["list"]
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow setting value from values`() = options {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val option = get("choices") as Option<String>
|
||||
|
||||
option.value = option.values!!.values.last()
|
||||
|
||||
assertTrue(option.value == "valid")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow setting custom value`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("choices", "unknown")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow resetting value`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("choices", null)
|
||||
}
|
||||
|
||||
assert(get("choices").value == null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset should not fail`() = options {
|
||||
assertDoesNotThrow {
|
||||
set("resettable", "test")
|
||||
get("resettable").reset()
|
||||
}
|
||||
|
||||
assertThrows<OptionException.ValueRequiredException> {
|
||||
get("resettable").value
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `option types should be known`() = options {
|
||||
assertEquals(typeOf<List<String>>(), get("list").type)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getting default value should work`() = options {
|
||||
assertDoesNotThrow {
|
||||
assertNull(get("resettable").default)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `external option should be accessible`() {
|
||||
assertDoesNotThrow {
|
||||
externalOption.value = "test"
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow getting the external option from the patch`() {
|
||||
assertEquals(optionsTestPatch.options["external"].value, externalOption.value)
|
||||
}
|
||||
|
||||
private fun options(block: Options.() -> Unit) = optionsTestPatch.options.let(block)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package app.revanced.patcher.patch
|
||||
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
internal object PatchTest {
|
||||
@Test
|
||||
fun `can create patch with name`() {
|
||||
val patch = bytecodePatch(name = "Test") {}
|
||||
|
||||
assertEquals("Test", patch.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with compatible packages`() {
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
compatibleWith(
|
||||
"compatible.package"("1.0.0"),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(1, patch.compatiblePackages!!.size)
|
||||
assertEquals("compatible.package", patch.compatiblePackages!!.first().first)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with dependencies`() {
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
dependsOn(resourcePatch {})
|
||||
}
|
||||
|
||||
assertEquals(1, patch.dependencies.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `can create patch with options`() {
|
||||
val patch = bytecodePatch(name = "Test") {
|
||||
val print by stringOption("print")
|
||||
val custom = option<String>("custom")()
|
||||
|
||||
this.apply {
|
||||
println(print)
|
||||
println(custom.value)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(2, patch.options.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loads patches correctly`() {
|
||||
val patchesClass = ::Public.javaField!!.declaringClass.name
|
||||
val classLoader = ::Public.javaClass.classLoader
|
||||
|
||||
val patches = getPatches(listOf(patchesClass), classLoader)
|
||||
|
||||
assertEquals(
|
||||
2,
|
||||
patches.size,
|
||||
"Expected 2 patches to be loaded, " +
|
||||
"because there's only two named patches declared as public static fields " +
|
||||
"or returned by public static and non-parametrized methods.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val publicUnnamedPatch = bytecodePatch {} // Not loaded, because it's unnamed.
|
||||
|
||||
val Public by creatingBytecodePatch {} // Loaded, because it's named.
|
||||
|
||||
private val privateUnnamedPatch = bytecodePatch {} // Not loaded, because it's private.
|
||||
|
||||
private val Private by creatingBytecodePatch {} // Not loaded, because it's private.
|
||||
|
||||
fun publicUnnamedPatchFunction() = publicUnnamedPatch // Not loaded, because it's unnamed.
|
||||
|
||||
fun publicNamedPatchFunction() = bytecodePatch("Public") { } // Loaded, because it's named.
|
||||
|
||||
fun parameterizedFunction(@Suppress("UNUSED_PARAMETER") param: Any) =
|
||||
publicNamedPatchFunction() // Not loaded, because it's parameterized.
|
||||
|
||||
private fun privateUnnamedPatchFunction() = privateUnnamedPatch // Not loaded, because it's private.
|
||||
|
||||
private fun privateNamedPatchFunction() = Private // Not loaded, because it's private.
|
||||
@@ -0,0 +1,76 @@
|
||||
package app.revanced.patcher.util
|
||||
|
||||
import app.revanced.patcher.extensions.*
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.builder.BuilderOffsetInstruction
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import com.android.tools.smali.dexlib2.mutable.MutableMethod.Companion.toMutable
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
internal class SmaliTest {
|
||||
val method = ImmutableMethod(
|
||||
"Ldummy;",
|
||||
"name",
|
||||
emptyList(), // parameters
|
||||
"V",
|
||||
AccessFlags.PUBLIC.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(1),
|
||||
).toMutable()
|
||||
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
method.instructions.clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `own branches work`() {
|
||||
method.addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
:test
|
||||
const/4 v0, 0x1
|
||||
if-eqz v0, :test
|
||||
""",
|
||||
)
|
||||
|
||||
val targetLocationIndex = method.getInstruction<BuilderOffsetInstruction>(1).target.location.index
|
||||
|
||||
assertEquals(0, targetLocationIndex, "Label should point to index 0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `external branches work`() {
|
||||
val instructionIndex = 3
|
||||
val labelIndex = 1
|
||||
|
||||
method.addInstructions(
|
||||
"""
|
||||
const/4 v0, 0x1
|
||||
const/4 v0, 0x0
|
||||
""",
|
||||
)
|
||||
|
||||
method.addInstructionsWithLabels(
|
||||
method.instructions.size,
|
||||
"""
|
||||
const/4 v0, 0x1
|
||||
if-eqz v0, :test
|
||||
return-void
|
||||
""",
|
||||
ExternalLabel("test", method.getInstruction(1)),
|
||||
)
|
||||
|
||||
val instruction = method.getInstruction<BuilderInstruction21t>(instructionIndex)
|
||||
|
||||
assertTrue(instruction.target.isPlaced, "Label should be placed")
|
||||
assertEquals(labelIndex, instruction.target.location.index)
|
||||
}
|
||||
}
|
||||
@@ -1 +1,24 @@
|
||||
rootProject.name = "revanced-patcher"
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
google()
|
||||
maven {
|
||||
name = "githubPackages"
|
||||
// A repository must be specified for some reason. "registry" is a dummy.
|
||||
url = uri("https://maven.pkg.github.com/revanced/registry")
|
||||
credentials(PasswordCredentials::class)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include(":patcher")
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.Context
|
||||
import app.revanced.patcher.extensions.PatchExtensions.dependencies
|
||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||
import app.revanced.patcher.extensions.PatchExtensions.requiresIntegrations
|
||||
import app.revanced.patcher.extensions.nullOutputStream
|
||||
import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve
|
||||
import app.revanced.patcher.patch.*
|
||||
import app.revanced.patcher.util.VersionReader
|
||||
import brut.androlib.Androlib
|
||||
import brut.androlib.meta.UsesFramework
|
||||
import brut.androlib.options.BuildOptions
|
||||
import brut.androlib.res.AndrolibResources
|
||||
import brut.androlib.res.data.ResPackage
|
||||
import brut.androlib.res.decoder.AXmlResourceParser
|
||||
import brut.androlib.res.decoder.ResAttrDecoder
|
||||
import brut.androlib.res.decoder.XmlPullStreamDecoder
|
||||
import brut.androlib.res.xml.ResXmlPatcher
|
||||
import brut.directory.ExtFile
|
||||
import lanchon.multidexlib2.BasicDexFileNamer
|
||||
import lanchon.multidexlib2.DexIO
|
||||
import lanchon.multidexlib2.MultiDexIO
|
||||
import org.jf.dexlib2.Opcodes
|
||||
import org.jf.dexlib2.iface.DexFile
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
internal val NAMER = BasicDexFileNamer()
|
||||
|
||||
/**
|
||||
* The ReVanced Patcher.
|
||||
* @param options The options for the patcher.
|
||||
*/
|
||||
class Patcher(private val options: PatcherOptions) {
|
||||
private val logger = options.logger
|
||||
private val opcodes: Opcodes
|
||||
private var resourceDecodingMode = ResourceDecodingMode.MANIFEST_ONLY
|
||||
private var mergeIntegrations = false
|
||||
val context: PatcherContext
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val version = VersionReader.read()
|
||||
private fun BuildOptions.setBuildOptions(options: PatcherOptions) {
|
||||
this.aaptPath = options.aaptPath
|
||||
this.useAapt2 = true
|
||||
this.frameworkFolderLocation = options.frameworkFolderLocation
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
logger.info("Reading dex files")
|
||||
// read dex files
|
||||
val dexFile = MultiDexIO.readDexFile(true, options.inputFile, NAMER, null, null)
|
||||
// get the opcodes
|
||||
opcodes = dexFile.opcodes
|
||||
// finally create patcher context
|
||||
context = PatcherContext(dexFile.classes.toMutableList(), File(options.resourceCacheDirectory))
|
||||
|
||||
// decode manifest file
|
||||
decodeResources(ResourceDecodingMode.MANIFEST_ONLY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add integrations to be merged by the patcher.
|
||||
* The integrations will only be merged, if necessary.
|
||||
*
|
||||
* @param integrations The integrations, must be dex files or dex file container such as ZIP, APK or DEX files.
|
||||
* @param callback The callback for [integrations] which are being added.
|
||||
*/
|
||||
fun addIntegrations(
|
||||
integrations: List<File>,
|
||||
callback: (File) -> Unit
|
||||
) {
|
||||
context.integrations.apply integrations@{
|
||||
add(integrations)
|
||||
this@integrations.callback = callback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the patched dex file.
|
||||
*/
|
||||
fun save(): PatcherResult {
|
||||
val packageMetadata = context.packageMetadata
|
||||
val metaInfo = packageMetadata.metaInfo
|
||||
var resourceFile: File? = null
|
||||
|
||||
when (resourceDecodingMode) {
|
||||
ResourceDecodingMode.FULL -> {
|
||||
val cacheDirectory = ExtFile(options.resourceCacheDirectory)
|
||||
try {
|
||||
val androlibResources = AndrolibResources().also { resources ->
|
||||
resources.buildOptions = BuildOptions().also { buildOptions ->
|
||||
buildOptions.setBuildOptions(options)
|
||||
buildOptions.isFramework = metaInfo.isFrameworkApk
|
||||
buildOptions.resourcesAreCompressed = metaInfo.compressionType
|
||||
buildOptions.doNotCompress = metaInfo.doNotCompress
|
||||
}
|
||||
|
||||
resources.setSdkInfo(metaInfo.sdkInfo)
|
||||
resources.setVersionInfo(metaInfo.versionInfo)
|
||||
resources.setSharedLibrary(metaInfo.sharedLibrary)
|
||||
resources.setSparseResources(metaInfo.sparseResources)
|
||||
}
|
||||
|
||||
val manifestFile = cacheDirectory.resolve("AndroidManifest.xml")
|
||||
|
||||
ResXmlPatcher.fixingPublicAttrsInProviderAttributes(manifestFile)
|
||||
|
||||
val aaptFile = cacheDirectory.resolve("aapt_temp_file")
|
||||
|
||||
// delete if it exists
|
||||
Files.deleteIfExists(aaptFile.toPath())
|
||||
|
||||
val resDirectory = cacheDirectory.resolve("res")
|
||||
val includedFiles = metaInfo.usesFramework.ids.map { id ->
|
||||
androlibResources.getFrameworkApk(
|
||||
id, metaInfo.usesFramework.tag
|
||||
)
|
||||
}.toTypedArray()
|
||||
|
||||
logger.info("Compiling resources")
|
||||
androlibResources.aaptPackage(
|
||||
aaptFile, manifestFile, resDirectory, null, null, includedFiles
|
||||
)
|
||||
|
||||
resourceFile = aaptFile
|
||||
} finally {
|
||||
cacheDirectory.close()
|
||||
}
|
||||
}
|
||||
|
||||
else -> logger.info("Not compiling resources because resource patching is not required")
|
||||
}
|
||||
|
||||
logger.trace("Creating new dex file")
|
||||
val newDexFile = object : DexFile {
|
||||
override fun getClasses() = context.bytecodeContext.classes.also { it.replaceClasses() }
|
||||
override fun getOpcodes() = this@Patcher.opcodes
|
||||
}
|
||||
|
||||
// write modified dex files
|
||||
logger.info("Writing modified dex files")
|
||||
val dexFiles = mutableMapOf<String, MemoryDataStore>()
|
||||
MultiDexIO.writeDexFile(
|
||||
true, -1, // core count
|
||||
dexFiles, NAMER, newDexFile, DexIO.DEFAULT_MAX_DEX_POOL_SIZE, null
|
||||
)
|
||||
|
||||
return PatcherResult(
|
||||
dexFiles.map {
|
||||
app.revanced.patcher.util.dex.DexFile(it.key, it.value.readAt(0))
|
||||
},
|
||||
metaInfo.doNotCompress?.toList(),
|
||||
resourceFile
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add [Patch]es to the patcher.
|
||||
* @param patches [Patch]es The patches to add.
|
||||
*/
|
||||
fun addPatches(patches: Iterable<Class<out Patch<Context>>>) {
|
||||
/**
|
||||
* Returns true if at least one patches or its dependencies matches the given predicate.
|
||||
*/
|
||||
fun Class<out Patch<Context>>.anyRecursively(predicate: (Class<out Patch<Context>>) -> Boolean): Boolean =
|
||||
predicate(this) || dependencies?.any { it.java.anyRecursively(predicate) } == true
|
||||
|
||||
|
||||
// Determine if resource patching is required.
|
||||
for (patch in patches) {
|
||||
if (patch.anyRecursively { ResourcePatch::class.java.isAssignableFrom(it) }) {
|
||||
resourceDecodingMode = ResourceDecodingMode.FULL
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if merging integrations is required.
|
||||
for (patch in patches) {
|
||||
if (patch.anyRecursively { it.requiresIntegrations }) {
|
||||
mergeIntegrations = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
context.patches.addAll(patches)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode resources for the patcher.
|
||||
*
|
||||
* @param mode The [ResourceDecodingMode] to use when decoding.
|
||||
*/
|
||||
private fun decodeResources(mode: ResourceDecodingMode) {
|
||||
val extInputFile = ExtFile(options.inputFile)
|
||||
try {
|
||||
val androlib = Androlib(BuildOptions().also { it.setBuildOptions(options) })
|
||||
val resourceTable = androlib.getResTable(extInputFile, true)
|
||||
when (mode) {
|
||||
ResourceDecodingMode.FULL -> {
|
||||
val outDir = File(options.resourceCacheDirectory)
|
||||
if (outDir.exists()) {
|
||||
logger.info("Deleting existing resource cache directory")
|
||||
if (!outDir.deleteRecursively()) {
|
||||
logger.error("Failed to delete existing resource cache directory")
|
||||
}
|
||||
}
|
||||
outDir.mkdirs()
|
||||
|
||||
logger.info("Decoding resources")
|
||||
|
||||
// decode resources to cache directory
|
||||
androlib.decodeManifestWithResources(extInputFile, outDir, resourceTable)
|
||||
androlib.decodeResourcesFull(extInputFile, outDir, resourceTable)
|
||||
|
||||
// read additional metadata from the resource table
|
||||
context.packageMetadata.let { metadata ->
|
||||
metadata.metaInfo.usesFramework = UsesFramework().also { framework ->
|
||||
framework.ids = resourceTable.listFramePackages().map { it.id }.sorted()
|
||||
}
|
||||
|
||||
// read files to not compress
|
||||
metadata.metaInfo.doNotCompress = buildList {
|
||||
androlib.recordUncompressedFiles(extInputFile, this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ResourceDecodingMode.MANIFEST_ONLY -> {
|
||||
logger.info("Decoding AndroidManifest.xml only, because resources are not needed")
|
||||
|
||||
// create decoder for the resource table
|
||||
val decoder = ResAttrDecoder()
|
||||
decoder.currentPackage = ResPackage(resourceTable, 0, null)
|
||||
|
||||
// create xml parser with the decoder
|
||||
val axmlParser = AXmlResourceParser()
|
||||
axmlParser.attrDecoder = decoder
|
||||
|
||||
// parse package information with the decoder and parser which will set required values in the resource table
|
||||
// instead of decodeManifest another more low level solution can be created to make it faster/better
|
||||
XmlPullStreamDecoder(
|
||||
axmlParser, AndrolibResources().resXmlSerializer
|
||||
).decodeManifest(
|
||||
extInputFile.directory.getFileInput("AndroidManifest.xml"), nullOutputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// read of the resourceTable which is created by reading the manifest file
|
||||
context.packageMetadata.let { metadata ->
|
||||
metadata.packageName = resourceTable.currentResPackage.name
|
||||
metadata.packageVersion = resourceTable.versionInfo.versionName
|
||||
metadata.metaInfo.versionInfo = resourceTable.versionInfo
|
||||
metadata.metaInfo.sdkInfo = resourceTable.sdkInfo
|
||||
}
|
||||
} finally {
|
||||
extInputFile.close()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute patches added the patcher.
|
||||
*
|
||||
* @param stopOnError If true, the patches will stop on the first error.
|
||||
* @return A pair of the name of the [Patch] and its [PatchResult].
|
||||
*/
|
||||
fun executePatches(stopOnError: Boolean = false): Sequence<Pair<String, Result<PatchResultSuccess>>> {
|
||||
/**
|
||||
* Execute a [Patch] and its dependencies recursively.
|
||||
*
|
||||
* @param patchClass The [Patch] to execute.
|
||||
* @param executedPatches A map of [Patch]es paired to a boolean indicating their success, to prevent infinite recursion.
|
||||
* @return The result of executing the [Patch].
|
||||
*/
|
||||
fun executePatch(
|
||||
patchClass: Class<out Patch<Context>>,
|
||||
executedPatches: LinkedHashMap<String, ExecutedPatch>
|
||||
): PatchResult {
|
||||
val patchName = patchClass.patchName
|
||||
|
||||
// if the patch has already applied silently skip it
|
||||
if (executedPatches.contains(patchName)) {
|
||||
if (!executedPatches[patchName]!!.success)
|
||||
return PatchResultError("'$patchName' did not succeed previously")
|
||||
|
||||
logger.trace("Skipping '$patchName' because it has already been applied")
|
||||
|
||||
return PatchResultSuccess()
|
||||
}
|
||||
|
||||
// recursively execute all dependency patches
|
||||
patchClass.dependencies?.forEach { dependencyClass ->
|
||||
val dependency = dependencyClass.java
|
||||
|
||||
val result = executePatch(dependency, executedPatches)
|
||||
if (result.isSuccess()) return@forEach
|
||||
|
||||
return PatchResultError(
|
||||
"'$patchName' depends on '${dependency.patchName}' but the following error was raised: " +
|
||||
result.error()!!.let { it.cause?.stackTraceToString() ?: it.message }
|
||||
)
|
||||
}
|
||||
|
||||
val isResourcePatch = ResourcePatch::class.java.isAssignableFrom(patchClass)
|
||||
val patchInstance = patchClass.getDeclaredConstructor().newInstance()
|
||||
|
||||
// TODO: implement this in a more polymorphic way
|
||||
val patchContext = if (isResourcePatch) {
|
||||
context.resourceContext
|
||||
} else {
|
||||
context.bytecodeContext.also { context ->
|
||||
(patchInstance as BytecodePatch).fingerprints?.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 executing a [Patch].
|
||||
*
|
||||
* @param patchInstance The instance of the [Patch] that was applied.
|
||||
* @param success The result of the [Patch].
|
||||
*/
|
||||
internal data class ExecutedPatch(val patchInstance: Patch<Context>, val success: Boolean)
|
||||
@@ -1,64 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.data.*
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.util.ClassMerger.merge
|
||||
import org.jf.dexlib2.iface.ClassDef
|
||||
import java.io.File
|
||||
|
||||
data class PatcherContext(
|
||||
val classes: MutableList<ClassDef>,
|
||||
val resourceCacheDirectory: File,
|
||||
) {
|
||||
val packageMetadata = PackageMetadata()
|
||||
internal val patches = mutableListOf<Class<out Patch<Context>>>()
|
||||
internal val integrations = Integrations(this)
|
||||
internal val bytecodeContext = BytecodeContext(classes)
|
||||
internal val resourceContext = ResourceContext(resourceCacheDirectory)
|
||||
|
||||
internal class Integrations(val context: PatcherContext) {
|
||||
var callback: ((File) -> Unit)? = null
|
||||
private val integrations: MutableList<File> = mutableListOf()
|
||||
|
||||
fun add(integrations: List<File>) = this@Integrations.integrations.addAll(integrations)
|
||||
|
||||
/**
|
||||
* Merge integrations.
|
||||
* @param logger A logger.
|
||||
*/
|
||||
fun merge(logger: Logger) {
|
||||
with(context.bytecodeContext.classes) {
|
||||
for (integrations in integrations) {
|
||||
callback?.let { it(integrations) }
|
||||
|
||||
for (classDef in lanchon.multidexlib2.MultiDexIO.readDexFile(
|
||||
true,
|
||||
integrations,
|
||||
NAMER,
|
||||
null,
|
||||
null
|
||||
).classes) {
|
||||
val type = classDef.type
|
||||
|
||||
val result = classes.findIndexed { it.type == type }
|
||||
if (result == null) {
|
||||
logger.trace("Merging type $type")
|
||||
classes.add(classDef)
|
||||
continue
|
||||
}
|
||||
|
||||
val (existingClass, existingClassIndex) = result
|
||||
|
||||
logger.trace("Type $type exists. Adding missing methods and fields.")
|
||||
|
||||
existingClass.merge(classDef, context, logger).let { mergedClass ->
|
||||
if (mergedClass !== existingClass) // referential equality check
|
||||
classes[existingClassIndex] = mergedClass
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.logging.Logger
|
||||
import app.revanced.patcher.logging.impl.NopLogger
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Options for the [Patcher].
|
||||
* @param inputFile The input file (usually an apk file).
|
||||
* @param resourceCacheDirectory Directory to cache resources.
|
||||
* @param aaptPath Optional path to a custom aapt binary.
|
||||
* @param frameworkFolderLocation Optional path to a custom framework folder.
|
||||
* @param logger Custom logger implementation for the [Patcher].
|
||||
*/
|
||||
data class PatcherOptions(
|
||||
internal val inputFile: File,
|
||||
internal val resourceCacheDirectory: String,
|
||||
internal val aaptPath: String? = null,
|
||||
internal val frameworkFolderLocation: String? = null,
|
||||
internal val logger: Logger = NopLogger
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
package app.revanced.patcher
|
||||
|
||||
import app.revanced.patcher.util.dex.DexFile
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The result of a patcher.
|
||||
* @param dexFiles The patched dex files.
|
||||
* @param doNotCompress List of relative paths to files to exclude from compressing.
|
||||
* @param resourceFile File containing resources that need to be extracted into the APK.
|
||||
*/
|
||||
data class PatcherResult(
|
||||
val dexFiles: List<DexFile>,
|
||||
val doNotCompress: List<String>? = null,
|
||||
val resourceFile: File?
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
package app.revanced.patcher.annotation
|
||||
|
||||
import app.revanced.patcher.patch.Patch
|
||||
|
||||
/**
|
||||
* Annotation to constrain a [Patch] to compatible packages.
|
||||
* @param compatiblePackages A list of packages a [Patch] is compatible with.
|
||||
*/
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
annotation class Compatibility(
|
||||
val compatiblePackages: Array<Package>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Annotation to represent packages a patch can be compatible with.
|
||||
* @param name The package identifier name.
|
||||
* @param versions The versions of the package the [Patch] is compatible with.
|
||||
*/
|
||||
@Target()
|
||||
annotation class Package(
|
||||
val name: String,
|
||||
val versions: Array<String> = [],
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user